mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): revised entity list action bars
- Global action bar on top - Selected Entity action bar below
This commit is contained in:
@@ -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 (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<SelectedEntityOpacity />
|
||||
<Spacer />
|
||||
<EntityListActionBarSelectedEntityFill />
|
||||
<EntityListActionBarAddLayerButton />
|
||||
<EntityListActionBarDeleteButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBar.displayName = 'EntityListActionBar';
|
||||
@@ -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 (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size="sm"
|
||||
tooltip={t('controlLayers.addLayer')}
|
||||
aria-label={t('controlLayers.addLayer')}
|
||||
icon={<PiPlusBold />}
|
||||
variant="ghost"
|
||||
data-testid="control-layers-add-layer-menu-button"
|
||||
/>
|
||||
<MenuList>
|
||||
<CanvasEntityListMenuItems />
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton';
|
||||
@@ -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 (
|
||||
<>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={shift ? entityCount === 0 : !selectedEntityIdentifier}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||
tooltip={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||
icon={<PiTrashSimpleFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';
|
||||
@@ -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 (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<EntityListGlobalActionBarDenoisingStrength />
|
||||
<Spacer />
|
||||
<EntityListGlobalActionBarAddLayerMenu />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBar.displayName = 'EntityListGlobalActionBar';
|
||||
@@ -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 (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
tooltip={t('controlLayers.addLayer')}
|
||||
aria-label={t('controlLayers.addLayer')}
|
||||
icon={<PiPlusBold />}
|
||||
data-testid="control-layers-add-layer-menu-button"
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu';
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onBlur();
|
||||
}
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStrength(strength);
|
||||
}, [strength]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<FormControl w="min-content" gap={2}>
|
||||
<InformationalPopover feature="paramDenoisingStrength">
|
||||
<FormLabel m={0}>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<PopoverAnchor>
|
||||
<NumberInput
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
step={config.coarseStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
defaultValue={config.initial}
|
||||
value={localStrength}
|
||||
onChange={onChangeNumberInput}
|
||||
onBlur={onBlur}
|
||||
w="60px"
|
||||
onKeyDown={onKeyDown}
|
||||
clampValueOnBlur={false}
|
||||
variant="outline"
|
||||
>
|
||||
<NumberInputField paddingInlineEnd={7} _focusVisible={{ zIndex: 0 }} />
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="open-slider"
|
||||
icon={<PiCaretDownBold />}
|
||||
size="sm"
|
||||
variant="link"
|
||||
position="absolute"
|
||||
insetInlineEnd={0}
|
||||
h="full"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</NumberInput>
|
||||
</PopoverAnchor>
|
||||
</FormControl>
|
||||
<PopoverContent w={200} pt={0} pb={2} px={4}>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<CompositeSlider
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChangeSlider}
|
||||
value={localStrength}
|
||||
marks={marks}
|
||||
alwaysShowMarks
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListGlobalActionBarDenoisingStrength.displayName = 'EntityListGlobalActionBarDenoisingStrength';
|
||||
@@ -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 (
|
||||
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
|
||||
<EntityListSelectedEntityActionBarOpacity />
|
||||
<Spacer />
|
||||
<EntityListSelectedEntityActionBarFill />
|
||||
<Flex>
|
||||
<EntityListSelectedEntityActionBarFilterButton />
|
||||
<EntityListSelectedEntityActionBarTransformButton />
|
||||
<EntityListSelectedEntityActionBarDuplicateButton />
|
||||
<EntityListSelectedEntityActionBarDeleteButton />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('common.delete')}
|
||||
tooltip={t('common.delete')}
|
||||
icon={<PiTrashSimpleFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.duplicate')}
|
||||
tooltip={t('controlLayers.duplicate')}
|
||||
icon={<PiCopyFill />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton';
|
||||
@@ -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';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={isBusy}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.filter.filter')}
|
||||
tooltip={t('controlLayers.filter.filter')}
|
||||
icon={<PiShootingStarBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton';
|
||||
@@ -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';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={isBusy}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.transform.transform')}
|
||||
tooltip={t('controlLayers.transform.transform')}
|
||||
icon={<PiFrameCornersBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton';
|
||||
@@ -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 (
|
||||
<CanvasManagerProviderGate>
|
||||
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||
<EntityListActionBar />
|
||||
<EntityListGlobalActionBar />
|
||||
<Divider py={0} />
|
||||
<EntityListSelectedEntityActionBar />
|
||||
<Divider py={0} />
|
||||
{!hasEntities && <CanvasAddEntityButtons />}
|
||||
{hasEntities && <CanvasEntityList />}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
<CanvasEntityIsBookmarkedForQuickSwitchToggle />
|
||||
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityDeleteButton />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = <T extends CanvasEntityType = CanvasEntityType>(
|
||||
entityIdentifier: CanvasEntityIdentifier<T>
|
||||
): Extract<CanvasEntityAdapter, { state: { type: T } }> | 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;
|
||||
}
|
||||
|
||||
@@ -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 = <T extends CanvasEntityType>(
|
||||
|
||||
Reference in New Issue
Block a user