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 (
-
- );
-});
-
-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 (
+
+ );
+});
+
+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}
>
-
-
-
-
+