feat(ui): revised entity list action bars

- Global action bar on top
- Selected Entity action bar below
This commit is contained in:
psychedelicious
2024-09-06 19:38:48 +10:00
parent e1a66e22e9
commit 12e6f1be89
24 changed files with 494 additions and 172 deletions

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 />}

View File

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

View File

@@ -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>
);
});

View File

@@ -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]);

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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>(