From 12e6f1be892acc6ef96a40f7683fbc26dc48d185 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:38:48 +1000 Subject: [PATCH] feat(ui): revised entity list action bars - Global action bar on top - Selected Entity action bar below --- .../CanvasEntityList/EntityListActionBar.tsx | 20 --- .../EntityListActionBarAddLayerMenuButton.tsx | 28 ---- .../EntityListActionBarAddLayerMenuItems.tsx | 57 -------- .../EntityListActionBarDeleteButton.tsx | 39 ------ .../EntityListGlobalActionBar.tsx | 16 +++ .../EntityListGlobalActionBarAddLayerMenu.tsx | 69 ++++++++++ ...tyListGlobalActionBarDenoisingStrength.tsx | 124 ++++++++++++++++++ .../EntityListSelectedEntityActionBar.tsx | 26 ++++ ...istSelectedEntityActionBarDeleteButton.tsx | 34 +++++ ...SelectedEntityActionBarDuplicateButton.tsx | 34 +++++ ...EntityListSelectedEntityActionBarFill.tsx} | 4 +- ...istSelectedEntityActionBarFilterButton.tsx | 50 +++++++ ...ityListSelectedEntityActionBarOpacity.tsx} | 4 +- ...SelectedEntityActionBarTransformButton.tsx | 54 ++++++++ .../components/CanvasPanelContent.tsx | 7 +- .../ControlLayerControlAdapterModel.tsx | 13 +- .../CanvasEntityHeaderCommonActions.tsx | 2 - .../common/CanvasEntityMenuItemsFilter.tsx | 8 ++ .../controlLayers/konva/CanvasFilterModule.ts | 2 +- .../controlLayers/konva/CanvasManager.ts | 34 ++++- .../src/features/controlLayers/store/types.ts | 25 +++- ...h.tsx => ParamGlobalDenoisingStrength.tsx} | 4 +- .../ImageToImage/ImageToImageStrength.tsx | 6 +- .../ImageSettingsAccordion.tsx | 6 +- 24 files changed, 494 insertions(+), 172 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDeleteButton.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx rename invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/{EntityListActionBarSelectedEntityFill.tsx => EntityListSelectedEntityActionBarFill.tsx} (94%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx rename invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/{EntityListActionBarSelectedEntityOpacity.tsx => EntityListSelectedEntityActionBarOpacity.tsx} (97%) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx rename invokeai/frontend/web/src/features/parameters/components/Canvas/{ParamImageToImageStrength.tsx => ParamGlobalDenoisingStrength.tsx} (74%) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx deleted file mode 100644 index 758eb36b4e..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBar.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Flex, Spacer } from '@invoke-ai/ui-library'; -import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton'; -import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton'; -import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill'; -import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity'; -import { memo } from 'react'; - -export const EntityListActionBar = memo(() => { - return ( - - - - - - - - ); -}); - -EntityListActionBar.displayName = 'EntityListActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx deleted file mode 100644 index 610edec6e7..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library'; -import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -export const EntityListActionBarAddLayerButton = memo(() => { - const { t } = useTranslation(); - - return ( - - } - variant="ghost" - data-testid="control-layers-add-layer-menu-button" - /> - - - - - ); -}); - -EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx deleted file mode 100644 index ebbd54b836..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { MenuItem } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; -import { - controlLayerAdded, - inpaintMaskAdded, - ipaAdded, - rasterLayerAdded, - rgAdded, -} from 'features/controlLayers/store/canvasSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiPlusBold } from 'react-icons/pi'; - -export const CanvasEntityListMenuItems = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const defaultIPAdapter = useDefaultIPAdapter(); - const addInpaintMask = useCallback(() => { - dispatch(inpaintMaskAdded({ isSelected: true })); - }, [dispatch]); - const addRegionalGuidance = useCallback(() => { - dispatch(rgAdded({ isSelected: true })); - }, [dispatch]); - const addRasterLayer = useCallback(() => { - dispatch(rasterLayerAdded({ isSelected: true })); - }, [dispatch]); - const addControlLayer = useCallback(() => { - dispatch(controlLayerAdded({ isSelected: true })); - }, [dispatch]); - const addIPAdapter = useCallback(() => { - const overrides = { ipAdapter: defaultIPAdapter }; - dispatch(ipaAdded({ isSelected: true, overrides })); - }, [defaultIPAdapter, dispatch]); - - return ( - <> - } onClick={addInpaintMask}> - {t('controlLayers.inpaintMask')} - - } onClick={addRegionalGuidance}> - {t('controlLayers.regionalGuidance')} - - } onClick={addRasterLayer}> - {t('controlLayers.rasterLayer')} - - } onClick={addControlLayer}> - {t('controlLayers.controlLayer')} - - } onClick={addIPAdapter}> - {t('controlLayers.globalIPAdapter')} - - - ); -}); - -CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx deleted file mode 100644 index dfe40c0d85..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice'; -import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiTrashSimpleFill } from 'react-icons/pi'; - -export const EntityListActionBarDeleteButton = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); - const entityCount = useAppSelector(selectEntityCount); - const shift = useShiftModifier(); - const onClick = useCallback(() => { - if (shift) { - dispatch(allEntitiesDeleted()); - return; - } - if (!selectedEntityIdentifier) { - return; - } - dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier })); - }, [dispatch, selectedEntityIdentifier, shift]); - - return ( - } - /> - ); -}); - -EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx new file mode 100644 index 0000000000..c9cfd6ac25 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar.tsx @@ -0,0 +1,16 @@ +import { Flex, Spacer } from '@invoke-ai/ui-library'; +import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu'; +import { EntityListGlobalActionBarDenoisingStrength } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength'; +import { memo } from 'react'; + +export const EntityListGlobalActionBar = memo(() => { + return ( + + + + + + ); +}); + +EntityListGlobalActionBar.displayName = 'EntityListGlobalActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx new file mode 100644 index 0000000000..b68b6215f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu.tsx @@ -0,0 +1,69 @@ +import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter'; +import { + controlLayerAdded, + inpaintMaskAdded, + ipaAdded, + rasterLayerAdded, + rgAdded, +} from 'features/controlLayers/store/canvasSlice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiPlusBold } from 'react-icons/pi'; + +export const EntityListGlobalActionBarAddLayerMenu = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const defaultIPAdapter = useDefaultIPAdapter(); + const addInpaintMask = useCallback(() => { + dispatch(inpaintMaskAdded({ isSelected: true })); + }, [dispatch]); + const addRegionalGuidance = useCallback(() => { + dispatch(rgAdded({ isSelected: true })); + }, [dispatch]); + const addRasterLayer = useCallback(() => { + dispatch(rasterLayerAdded({ isSelected: true })); + }, [dispatch]); + const addControlLayer = useCallback(() => { + dispatch(controlLayerAdded({ isSelected: true })); + }, [dispatch]); + const addIPAdapter = useCallback(() => { + const overrides = { ipAdapter: defaultIPAdapter }; + dispatch(ipaAdded({ isSelected: true, overrides })); + }, [defaultIPAdapter, dispatch]); + + return ( + + } + data-testid="control-layers-add-layer-menu-button" + /> + + } onClick={addInpaintMask}> + {t('controlLayers.inpaintMask')} + + } onClick={addRegionalGuidance}> + {t('controlLayers.regionalGuidance')} + + } onClick={addRasterLayer}> + {t('controlLayers.rasterLayer')} + + } onClick={addControlLayer}> + {t('controlLayers.controlLayer')} + + } onClick={addIPAdapter}> + {t('controlLayers.globalIPAdapter')} + + + + ); +}); + +EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength.tsx new file mode 100644 index 0000000000..0c93a1a5a8 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarDenoisingStrength.tsx @@ -0,0 +1,124 @@ +import { + CompositeSlider, + FormControl, + FormLabel, + IconButton, + NumberInput, + NumberInputField, + Popover, + PopoverAnchor, + PopoverArrow, + PopoverBody, + PopoverContent, + PopoverTrigger, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; +import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice'; +import { clamp } from 'lodash-es'; +import type { KeyboardEvent } from 'react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretDownBold } from 'react-icons/pi'; + +const marks = [0, 0.25, 0.5, 0.75, 1]; + +export const EntityListGlobalActionBarDenoisingStrength = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const strength = useAppSelector(selectImg2imgStrength); + const config = useAppSelector(selectImg2imgStrengthConfig); + + const [localStrength, setLocalStrength] = useState(strength); + + const onChangeSlider = useCallback( + (value: number) => { + dispatch(setImg2imgStrength(value)); + }, + [dispatch] + ); + + const onBlur = useCallback(() => { + if (isNaN(Number(localStrength))) { + setLocalStrength(config.initial); + return; + } + dispatch(setImg2imgStrength(clamp(localStrength, 0, 1))); + }, [config.initial, dispatch, localStrength]); + + const onChangeNumberInput = useCallback((valueAsString: string, valueAsNumber: number) => { + setLocalStrength(valueAsNumber); + }, []); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onBlur(); + } + }, + [onBlur] + ); + + useEffect(() => { + setLocalStrength(strength); + }, [strength]); + + return ( + + + + {`${t('parameters.denoisingStrength')}`} + + + + + + } + size="sm" + variant="link" + position="absolute" + insetInlineEnd={0} + h="full" + /> + + + + + + + + + + + + ); +}); + +EntityListGlobalActionBarDenoisingStrength.displayName = 'EntityListGlobalActionBarDenoisingStrength'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx new file mode 100644 index 0000000000..6a61ddba5b --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar.tsx @@ -0,0 +1,26 @@ +import { Flex, Spacer } from '@invoke-ai/ui-library'; +import { EntityListSelectedEntityActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDeleteButton'; +import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton'; +import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill'; +import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton'; +import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity'; +import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton'; +import { memo } from 'react'; + +export const EntityListSelectedEntityActionBar = memo(() => { + return ( + + + + + + + + + + + + ); +}); + +EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDeleteButton.tsx new file mode 100644 index 0000000000..1cfd175c5a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDeleteButton.tsx @@ -0,0 +1,34 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { entityDeleted } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleFill } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarDeleteButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const onClick = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier })); + }, [dispatch, selectedEntityIdentifier]); + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx new file mode 100644 index 0000000000..27d7542677 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton.tsx @@ -0,0 +1,34 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { entityDuplicated } from 'features/controlLayers/store/canvasSlice'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyFill } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const onClick = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + dispatch(entityDuplicated({ entityIdentifier: selectedEntityIdentifier })); + }, [dispatch, selectedEntityIdentifier]); + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx similarity index 94% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx index 83c53f1f86..117d3bc84f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill.tsx @@ -9,7 +9,7 @@ import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -export const EntityListActionBarSelectedEntityFill = memo(() => { +export const EntityListSelectedEntityActionBarFill = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); @@ -67,4 +67,4 @@ export const EntityListActionBarSelectedEntityFill = memo(() => { ); }); -EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill'; +EntityListSelectedEntityActionBarFill.displayName = 'EntityListSelectedEntityActionBarFill'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx new file mode 100644 index 0000000000..73445a9a55 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton.tsx @@ -0,0 +1,50 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShootingStarBold } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarFilterButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const canvasManager = useCanvasManager(); + const isBusy = useCanvasIsBusy(); + + const onClick = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) { + return; + } + + canvasManager.filter.startFilter(selectedEntityIdentifier); + }, [canvasManager, selectedEntityIdentifier]); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx similarity index 97% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx index c514e4ee8b..1662d19d06 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity.tsx @@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => { return selectedEntity.opacity; }); -export const SelectedEntityOpacity = memo(() => { +export const EntityListSelectedEntityActionBarOpacity = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); @@ -193,4 +193,4 @@ export const SelectedEntityOpacity = memo(() => { ); }); -SelectedEntityOpacity.displayName = 'SelectedEntityOpacity'; +EntityListSelectedEntityActionBarOpacity.displayName = 'EntityListSelectedEntityActionBarOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx new file mode 100644 index 0000000000..a182e221b7 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton.tsx @@ -0,0 +1,54 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors'; +import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFrameCornersBold } from 'react-icons/pi'; + +export const EntityListSelectedEntityActionBarTransformButton = memo(() => { + const { t } = useTranslation(); + const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier); + const canvasManager = useCanvasManager(); + + const isBusy = useCanvasIsBusy(); + + const onClick = useCallback(() => { + if (!selectedEntityIdentifier) { + return; + } + if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) { + return; + } + const adapter = canvasManager.getAdapter(selectedEntityIdentifier); + if (!adapter) { + return; + } + adapter.transformer.startTransform(); + }, [canvasManager, selectedEntityIdentifier]); + + if (!selectedEntityIdentifier) { + return null; + } + + if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) { + return null; + } + + return ( + } + /> + ); +}); + +EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index e1f435551e..90d20222b6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -2,7 +2,8 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons'; import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList'; -import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar'; +import { EntityListGlobalActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBar'; +import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar'; import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; @@ -13,7 +14,9 @@ export const CanvasPanelContent = memo(() => { return ( - + + + {!hasEntities && } {hasEntities && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index cd06c26f2b..83ce7cf28d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -4,7 +4,12 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectBase } from 'features/controlLayers/store/paramsSlice'; -import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types'; +import { + IMAGE_FILTERS, + isControlLayerEntityIdentifier, + isFilterType, + isRasterLayerEntityIdentifier, +} from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType'; @@ -39,6 +44,11 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha // Open the filter popup by setting this entity as the filtering entity if (!canvasManager.filter.$adapter.get()) { + // Can only filter raster and control layers + if (!isRasterLayerEntityIdentifier(entityIdentifier) && !isControlLayerEntityIdentifier(entityIdentifier)) { + return; + } + // Update the filter, preferring the model's default if (isFilterType(modelConfig.default_settings?.preprocessor)) { canvasManager.filter.$config.set( @@ -47,6 +57,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha } else { canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base)); } + canvasManager.filter.startFilter(entityIdentifier); canvasManager.filter.previewFilter(); } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx index 2615771b0d..492231dd64 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx @@ -1,5 +1,4 @@ import { Flex } from '@invoke-ai/ui-library'; -import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle'; import { CanvasEntityIsBookmarkedForQuickSwitchToggle } from 'features/controlLayers/components/common/CanvasEntityIsBookmarkedForQuickSwitchToggle'; import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle'; @@ -14,7 +13,6 @@ export const CanvasEntityHeaderCommonActions = memo(() => { {entityIdentifier.type !== 'ip_adapter' && } - ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx index 8616902045..8b9c43cade 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx @@ -2,6 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { isControlLayerEntityIdentifier, isRasterLayerEntityIdentifier } from 'features/controlLayers/store/types'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShootingStarBold } from 'react-icons/pi'; @@ -13,6 +14,13 @@ export const CanvasEntityMenuItemsFilter = memo(() => { const isBusy = useCanvasIsBusy(); const onClick = useCallback(() => { + if (!entityIdentifier) { + return; + } + // Can only filter raster and control layers + if (!isRasterLayerEntityIdentifier(entityIdentifier) && !isControlLayerEntityIdentifier(entityIdentifier)) { + return; + } canvasManager.filter.startFilter(entityIdentifier); }, [canvasManager.filter, entityIdentifier]); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts index 0d02512d4d..b65392a01a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilterModule.ts @@ -46,7 +46,7 @@ export class CanvasFilterModule extends CanvasModuleBase { this.log.debug('Creating filter module'); } - startFilter = (entityIdentifier: CanvasEntityIdentifier) => { + startFilter = (entityIdentifier: CanvasEntityIdentifier<'raster_layer' | 'control_layer'>) => { this.log.trace('Initializing filter'); const adapter = this.manager.getAdapter(entityIdentifier); if (!adapter) { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index b339776f5f..4b87f35bae 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -20,8 +20,8 @@ import { CanvasStagingAreaModule } from 'features/controlLayers/konva/CanvasStag import { CanvasToolModule } from 'features/controlLayers/konva/CanvasToolModule'; import { CanvasWorkerModule } from 'features/controlLayers/konva/CanvasWorkerModule.js'; import { getPrefixedId } from 'features/controlLayers/konva/util'; +import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types'; import { - type CanvasEntityIdentifier, isControlLayerEntityIdentifier, isInpaintMaskEntityIdentifier, isRasterLayerEntityIdentifier, @@ -133,16 +133,38 @@ export class CanvasManager extends CanvasModuleBase { }); } - getAdapter = (entityIdentifier: CanvasEntityIdentifier): CanvasEntityAdapter | null => { + getAdapter = ( + entityIdentifier: CanvasEntityIdentifier + ): Extract | null => { switch (entityIdentifier.type) { case 'raster_layer': - return this.adapters.rasterLayers.get(entityIdentifier.id) ?? null; + return ( + (this.adapters.rasterLayers.get(entityIdentifier.id) as Extract< + CanvasEntityAdapter, + { state: { type: T } } + >) ?? null + ); case 'control_layer': - return this.adapters.controlLayers.get(entityIdentifier.id) ?? null; + return ( + (this.adapters.controlLayers.get(entityIdentifier.id) as Extract< + CanvasEntityAdapter, + { state: { type: T } } + >) ?? null + ); case 'regional_guidance': - return this.adapters.regionMasks.get(entityIdentifier.id) ?? null; + return ( + (this.adapters.regionMasks.get(entityIdentifier.id) as Extract< + CanvasEntityAdapter, + { state: { type: T } } + >) ?? null + ); case 'inpaint_mask': - return this.adapters.inpaintMasks.get(entityIdentifier.id) ?? null; + return ( + (this.adapters.inpaintMasks.get(entityIdentifier.id) as Extract< + CanvasEntityAdapter, + { state: { type: T } } + >) ?? null + ); default: return null; } diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index e486f8cab7..b598f60a5f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -778,7 +778,7 @@ export type EntityRasterizedPayload = EntityIdentifierPayload<{ export type GenerationMode = 'txt2img' | 'img2img' | 'inpaint' | 'outpaint'; -function isDrawableEntityType( +export function isRenderableEntityType( entityType: CanvasEntityState['type'] ): entityType is CanvasRenderableEntityState['type'] { return ( @@ -813,8 +813,29 @@ export function isRegionalGuidanceEntityIdentifier( return entityIdentifier.type === 'regional_guidance'; } +export function isFilterableEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> | CanvasEntityIdentifier<'control_layer'> { + return isRasterLayerEntityIdentifier(entityIdentifier) || isControlLayerEntityIdentifier(entityIdentifier); +} + +export function isTransformableEntityIdentifier( + entityIdentifier: CanvasEntityIdentifier +): entityIdentifier is + | CanvasEntityIdentifier<'raster_layer'> + | CanvasEntityIdentifier<'control_layer'> + | CanvasEntityIdentifier<'inpaint_mask'> + | CanvasEntityIdentifier<'regional_guidance'> { + return ( + isRasterLayerEntityIdentifier(entityIdentifier) || + isControlLayerEntityIdentifier(entityIdentifier) || + isInpaintMaskEntityIdentifier(entityIdentifier) || + isRegionalGuidanceEntityIdentifier(entityIdentifier) + ); +} + export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState { - return isDrawableEntityType(entity.type); + return isRenderableEntityType(entity.type); } export const getEntityIdentifier = ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamGlobalDenoisingStrength.tsx similarity index 74% rename from invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx rename to invokeai/frontend/web/src/features/parameters/components/Canvas/ParamGlobalDenoisingStrength.tsx index c250b4a7a0..8bb33990cc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamGlobalDenoisingStrength.tsx @@ -1,6 +1,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; -import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; +import { DenoisingStrength } from 'features/parameters/components/ImageToImage/ImageToImageStrength'; import { memo, useCallback } from 'react'; const ParamImageToImageStrength = () => { @@ -14,7 +14,7 @@ const ParamImageToImageStrength = () => { [dispatch] ); - return ; + return ; }; export default memo(ParamImageToImageStrength); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx index 59e24d4278..c66bcc727c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx @@ -12,7 +12,7 @@ type Props = { onChange: (v: number) => void; }; -const ImageToImageStrength = ({ value, onChange }: Props) => { +export const DenoisingStrength = memo(({ value, onChange }: Props) => { const config = useAppSelector(selectImg2imgStrengthConfig); const { t } = useTranslation(); @@ -42,6 +42,6 @@ const ImageToImageStrength = ({ value, onChange }: Props) => { /> ); -}; +}); -export default memo(ImageToImageStrength); +DenoisingStrength.displayName = 'DenoisingStrength'; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 230d9e9925..f5341afed3 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -10,7 +10,6 @@ import BboxScaledHeight from 'features/parameters/components/Bbox/BboxScaledHeig import BboxScaledWidth from 'features/parameters/components/Bbox/BboxScaledWidth'; import BboxScaleMethod from 'features/parameters/components/Bbox/BboxScaleMethod'; import { BboxSettings } from 'features/parameters/components/Bbox/BboxSettings'; -import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength'; import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput'; import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize'; import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle'; @@ -72,10 +71,7 @@ export const ImageSettingsAccordion = memo(() => { onToggle={onToggleAccordion} > - - - - +