mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): iterate on filter UI, flow
This commit is contained in:
parent
677dddcfc9
commit
7e400d876f
@ -1694,7 +1694,6 @@
|
||||
"objects_zero": "empty",
|
||||
"objects_one": "{{count}} object",
|
||||
"objects_other": "{{count}} objects",
|
||||
"filter": "Filter",
|
||||
"convertToControlLayer": "Convert to Control Layer",
|
||||
"convertToRasterLayer": "Convert to Raster Layer",
|
||||
"enableTransparencyEffect": "Enable Transparency Effect",
|
||||
@ -1723,6 +1722,14 @@
|
||||
"solid": "Solid",
|
||||
"checkerboard": "Checkerboard",
|
||||
"dynamicGrid": "Dynamic Grid"
|
||||
},
|
||||
"filter": {
|
||||
"filter": "Filter",
|
||||
"filters": "Filters",
|
||||
"filterType": "Filter Type",
|
||||
"preview": "Preview",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
|
@ -6,10 +6,10 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
|
||||
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
|
@ -2,8 +2,13 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { ipaImageChanged, rasterLayerAdded, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
ipaImageChanged,
|
||||
rasterLayerAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
@ -99,7 +104,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'ADD_LAYER_FROM_IMAGE' &&
|
||||
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
@ -113,6 +118,24 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
|
||||
const { x, y } = getState().canvasV2.bbox.rect;
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on node image field
|
||||
*/
|
||||
|
@ -33,28 +33,23 @@ type IAINoImageFallbackProps = FlexProps & {
|
||||
};
|
||||
|
||||
export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
|
||||
const { icon = PiImageBold, boxSize = 16, sx, ...rest } = props;
|
||||
|
||||
const styles = useMemo(
|
||||
() => ({
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
userSelect: 'none',
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
fontSize: 'md',
|
||||
...sx,
|
||||
}),
|
||||
[sx]
|
||||
);
|
||||
const { icon = PiImageBold, boxSize = 16, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Flex sx={styles} {...rest}>
|
||||
<Flex
|
||||
w="full"
|
||||
h="full"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
borderRadius="base"
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
userSelect="none"
|
||||
opacity={0.7}
|
||||
color="base.500"
|
||||
fontSize="md"
|
||||
{...rest}
|
||||
>
|
||||
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
|
||||
{props.label && <Text textAlign="center">{props.label}</Text>}
|
||||
</Flex>
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { AddLayerFromImageDropData } from 'features/dnd/types';
|
||||
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
|
||||
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
||||
id: 'add-layer-from-image-drop-data',
|
||||
actionType: 'ADD_LAYER_FROM_IMAGE',
|
||||
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
|
||||
id: 'add-raster-layer-from-image-drop-data',
|
||||
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE',
|
||||
};
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
@ -17,9 +22,14 @@ export const CanvasDropArea = memo(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
|
||||
</Flex>
|
||||
<>
|
||||
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Raster Layer" data={addRasterLayerFromImageDropData} />
|
||||
</Flex>
|
||||
<Flex position="absolute" top="50%" right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Control Layer" data={addControlLayerFromImageDropData} />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { $filterConfig, $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
|
||||
@ -13,6 +16,7 @@ type Props = {
|
||||
|
||||
export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const currentBaseModel = useAppSelector((s) => s.canvasV2.params.model?.base);
|
||||
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
|
||||
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
|
||||
@ -23,8 +27,27 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
return;
|
||||
}
|
||||
onChangeModel(modelConfig);
|
||||
|
||||
// When we set the model for the first time, we'll set the default filter settings and open the filter popup
|
||||
|
||||
if (modelKey) {
|
||||
// If there is already a model key, this is not the first time we're setting the model
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the filter, preferring the model's default
|
||||
if (isFilterType(modelConfig.default_settings?.preprocessor)) {
|
||||
$filterConfig.set(IMAGE_FILTERS[modelConfig.default_settings.preprocessor].buildDefaults(modelConfig.base));
|
||||
} else {
|
||||
$filterConfig.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
|
||||
}
|
||||
|
||||
// Open the filter popup by setting this entity as the filtering entity
|
||||
if (!$filteringEntity.get()) {
|
||||
$filteringEntity.set(entityIdentifier);
|
||||
}
|
||||
},
|
||||
[onChangeModel]
|
||||
[entityIdentifier, modelKey, onChangeModel]
|
||||
);
|
||||
|
||||
const getIsDisabled = useCallback(
|
||||
|
@ -3,6 +3,7 @@ import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||
import { memo, useRef } from 'react';
|
||||
@ -31,6 +32,9 @@ export const CanvasEditor = memo(() => {
|
||||
<Flex position="absolute" bottom={2} gap={2} align="center" justify="center">
|
||||
<StagingAreaToolbar />
|
||||
</Flex>
|
||||
<Flex position="absolute" bottom={16}>
|
||||
<Filter />
|
||||
</Flex>
|
||||
<CanvasDropArea />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,29 +0,0 @@
|
||||
/* eslint-disable i18next/no-literal-string */
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
|
||||
import { Filter } from 'features/controlLayers/components/Filters/Filter';
|
||||
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { memo } from 'react';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
export const ControlLayersPanelContent = memo(() => {
|
||||
const filteringEntity = useStore($filteringEntity);
|
||||
return (
|
||||
<PanelGroup direction="vertical">
|
||||
<Panel id="canvas-entity-list-panel" order={0}>
|
||||
<CanvasEntityList />
|
||||
</Panel>
|
||||
{Boolean(filteringEntity) && (
|
||||
<>
|
||||
<ResizeHandle orientation="horizontal" />
|
||||
<Panel id="filter-panel" order={1}>
|
||||
<Filter />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
);
|
||||
});
|
||||
|
||||
ControlLayersPanelContent.displayName = 'ControlLayersPanelContent';
|
@ -1,15 +1,21 @@
|
||||
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
|
||||
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
|
||||
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $filteringEntity } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { $filterConfig, $filteringEntity, $isProcessingFilter } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
|
||||
|
||||
export const Filter = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const filteringEntity = useStore($filteringEntity);
|
||||
const filterConfig = useStore($filterConfig);
|
||||
const isProcessingFilter = useStore($isProcessingFilter);
|
||||
|
||||
const preview = useCallback(() => {
|
||||
const previewFilter = useCallback(() => {
|
||||
if (!filteringEntity) {
|
||||
return;
|
||||
}
|
||||
@ -24,7 +30,7 @@ export const Filter = memo(() => {
|
||||
entity.adapter.filter.previewFilter();
|
||||
}, [filteringEntity]);
|
||||
|
||||
const apply = useCallback(() => {
|
||||
const applyFilter = useCallback(() => {
|
||||
if (!filteringEntity) {
|
||||
return;
|
||||
}
|
||||
@ -39,7 +45,7 @@ export const Filter = memo(() => {
|
||||
entity.adapter.filter.applyFilter();
|
||||
}, [filteringEntity]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
const cancelFilter = useCallback(() => {
|
||||
if (!filteringEntity) {
|
||||
return;
|
||||
}
|
||||
@ -54,21 +60,62 @@ export const Filter = memo(() => {
|
||||
entity.adapter.filter.cancelFilter();
|
||||
}, [filteringEntity]);
|
||||
|
||||
const onChangeFilterConfig = useCallback((filterConfig: FilterConfig) => {
|
||||
$filterConfig.set(filterConfig);
|
||||
}, []);
|
||||
|
||||
const onChangeFilterType = useCallback((filterType: FilterConfig['type']) => {
|
||||
$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
|
||||
}, []);
|
||||
|
||||
if (!filteringEntity || !filterConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={3} w="full" h="full">
|
||||
<FilterTypeSelect />
|
||||
<ButtonGroup isAttached={false}>
|
||||
<Button onClick={preview} isDisabled={!filteringEntity}>
|
||||
Preview
|
||||
<Flex
|
||||
bg="base.800"
|
||||
borderRadius="base"
|
||||
p={4}
|
||||
flexDir="column"
|
||||
gap={4}
|
||||
w={420}
|
||||
h="auto"
|
||||
shadow="dark-lg"
|
||||
transitionProperty="height"
|
||||
transitionDuration="normal"
|
||||
>
|
||||
<Heading size="md" color="base.300" userSelect="none">
|
||||
{t('controlLayers.filter.filter')}
|
||||
</Heading>
|
||||
<FilterTypeSelect filterType={filterConfig.type} onChange={onChangeFilterType} />
|
||||
<FilterSettings filterConfig={filterConfig} onChange={onChangeFilterConfig} />
|
||||
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
|
||||
<Button
|
||||
leftIcon={<PiShootingStarBold />}
|
||||
onClick={previewFilter}
|
||||
isLoading={isProcessingFilter}
|
||||
loadingText={t('controlLayers.filter.preview')}
|
||||
>
|
||||
{t('controlLayers.filter.preview')}
|
||||
</Button>
|
||||
<Button onClick={apply} isDisabled={!filteringEntity}>
|
||||
Apply
|
||||
<Button
|
||||
leftIcon={<PiCheckBold />}
|
||||
onClick={applyFilter}
|
||||
isLoading={isProcessingFilter}
|
||||
loadingText={t('controlLayers.filter.apply')}
|
||||
>
|
||||
{t('controlLayers.filter.apply')}
|
||||
</Button>
|
||||
<Button onClick={cancel} isDisabled={!filteringEntity}>
|
||||
Cancel
|
||||
<Button
|
||||
leftIcon={<PiXBold />}
|
||||
onClick={cancelFilter}
|
||||
isLoading={isProcessingFilter}
|
||||
loadingText={t('controlLayers.filter.cancel')}
|
||||
>
|
||||
{t('controlLayers.filter.cancel')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<FilterSettings />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { FilterCanny } from 'features/controlLayers/components/Filters/FilterCanny';
|
||||
import { FilterColorMap } from 'features/controlLayers/components/Filters/FilterColorMap';
|
||||
@ -11,67 +10,67 @@ import { FilterMediapipeFace } from 'features/controlLayers/components/Filters/F
|
||||
import { FilterMidasDepth } from 'features/controlLayers/components/Filters/FilterMidasDepth';
|
||||
import { FilterMlsdImage } from 'features/controlLayers/components/Filters/FilterMlsdImage';
|
||||
import { FilterPidi } from 'features/controlLayers/components/Filters/FilterPidi';
|
||||
import { filterConfigChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/types';
|
||||
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const FilterSettings = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
type Props = { filterConfig: FilterConfig; onChange: (filterConfig: FilterConfig) => void };
|
||||
|
||||
export const FilterSettings = memo(({ filterConfig, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const config = useAppSelector((s) => s.canvasV2.filter.config);
|
||||
const updateFilter = useCallback(
|
||||
(config: FilterConfig) => {
|
||||
dispatch(filterConfigChanged({ config }));
|
||||
},
|
||||
[dispatch]
|
||||
|
||||
if (filterConfig.type === 'canny_image_processor') {
|
||||
return <FilterCanny config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'color_map_image_processor') {
|
||||
return <FilterColorMap config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'content_shuffle_image_processor') {
|
||||
return <FilterContentShuffle config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'depth_anything_image_processor') {
|
||||
return <FilterDepthAnything config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'dw_openpose_image_processor') {
|
||||
return <FilterDWOpenpose config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'hed_image_processor') {
|
||||
return <FilterHed config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'lineart_image_processor') {
|
||||
return <FilterLineart config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'mediapipe_face_processor') {
|
||||
return <FilterMediapipeFace config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'midas_depth_image_processor') {
|
||||
return <FilterMidasDepth config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'mlsd_image_processor') {
|
||||
return <FilterMlsdImage config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
if (filterConfig.type === 'pidi_image_processor') {
|
||||
return <FilterPidi config={filterConfig} onChange={onChange} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<IAINoContentFallback
|
||||
py={4}
|
||||
label={`${t(IMAGE_FILTERS[filterConfig.type].labelTKey)} has no settings`}
|
||||
icon={null}
|
||||
/>
|
||||
);
|
||||
|
||||
if (config.type === 'canny_image_processor') {
|
||||
return <FilterCanny config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'color_map_image_processor') {
|
||||
return <FilterColorMap config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'content_shuffle_image_processor') {
|
||||
return <FilterContentShuffle config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'depth_anything_image_processor') {
|
||||
return <FilterDepthAnything config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'dw_openpose_image_processor') {
|
||||
return <FilterDWOpenpose config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'hed_image_processor') {
|
||||
return <FilterHed config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'lineart_image_processor') {
|
||||
return <FilterLineart config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'mediapipe_face_processor') {
|
||||
return <FilterMediapipeFace config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'midas_depth_image_processor') {
|
||||
return <FilterMidasDepth config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'mlsd_image_processor') {
|
||||
return <FilterMlsdImage config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
if (config.type === 'pidi_image_processor') {
|
||||
return <FilterPidi config={config} onChange={updateFilter} />;
|
||||
}
|
||||
|
||||
return <IAINoContentFallback label={`${t(IMAGE_FILTERS[config.type].labelTKey)} has no settings`} icon={null} />;
|
||||
});
|
||||
|
||||
FilterSettings.displayName = 'Filter';
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||
import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { filterSelected } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/types';
|
||||
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
|
||||
import { configSelector } from 'features/system/store/configSelectors';
|
||||
import { includes, map } from 'lodash-es';
|
||||
@ -16,10 +16,13 @@ const selectDisabledProcessors = createMemoizedSelector(
|
||||
(config) => config.sd.disabledControlNetProcessors
|
||||
);
|
||||
|
||||
export const FilterTypeSelect = memo(() => {
|
||||
type Props = {
|
||||
filterType: FilterConfig['type'];
|
||||
onChange: (filterType: FilterConfig['type']) => void;
|
||||
};
|
||||
|
||||
export const FilterTypeSelect = memo(({ filterType, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const filterType = useAppSelector((s) => s.canvasV2.filter.config.type);
|
||||
const disabledProcessors = useAppSelector(selectDisabledProcessors);
|
||||
const options = useMemo(() => {
|
||||
return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
|
||||
@ -33,9 +36,9 @@ export const FilterTypeSelect = memo(() => {
|
||||
return;
|
||||
}
|
||||
assert(isFilterType(v.value));
|
||||
dispatch(filterSelected({ type: v.value }));
|
||||
onChange(v.value);
|
||||
},
|
||||
[dispatch]
|
||||
[onChange]
|
||||
);
|
||||
const value = useMemo(() => options.find((o) => o.value === filterType) ?? null, [options, filterType]);
|
||||
|
||||
@ -43,7 +46,7 @@ export const FilterTypeSelect = memo(() => {
|
||||
<Flex gap={2}>
|
||||
<FormControl>
|
||||
<InformationalPopover feature="controlNetProcessor">
|
||||
<FormLabel m={0}>{t('controlLayers.filter')}</FormLabel>
|
||||
<FormLabel m={0}>{t('controlLayers.filter.filterType')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox value={value} options={options} onChange={_onChange} isSearchable={false} isClearable={false} />
|
||||
</FormControl>
|
||||
|
@ -14,7 +14,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={filter} icon={<PiShootingStarBold />}>
|
||||
{t('controlLayers.filter')}
|
||||
{t('controlLayers.filter.filter')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { JSONObject } from 'common/types';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { JSONObject, SerializableObject } from 'common/types';
|
||||
import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
@ -35,10 +34,10 @@ export class CanvasFilter {
|
||||
}
|
||||
|
||||
previewFilter = async () => {
|
||||
const { config } = this.manager.stateApi.getFilterState();
|
||||
const config = this.manager.stateApi.$filterConfig.get();
|
||||
this.log.trace({ config }, 'Previewing filter');
|
||||
const dispatch = this.manager.stateApi._store.dispatch;
|
||||
const rect = this.parent.transformer.getRelativeRect()
|
||||
const rect = this.parent.transformer.getRelativeRect();
|
||||
const imageDTO = await this.parent.renderer.rasterize(rect, false);
|
||||
// TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now
|
||||
const filterNode = IMAGE_FILTERS[config.type].buildNode(imageDTO, config as never);
|
||||
@ -66,22 +65,30 @@ export class CanvasFilter {
|
||||
if (event.origin !== this.id || event.invocation_source_id !== filterNode.id) {
|
||||
return;
|
||||
}
|
||||
this.log.trace({ event: parseify(event) }, 'Handling filter processing completion');
|
||||
this.manager.socket.off('invocation_complete', listener);
|
||||
|
||||
this.log.trace({ event } as SerializableObject, 'Handling filter processing completion');
|
||||
|
||||
const { result } = event;
|
||||
assert(result.type === 'image_output', `Processor did not return an image output, got: ${result}`);
|
||||
|
||||
const imageDTO = await getImageDTO(result.image.image_name);
|
||||
assert(imageDTO, "Failed to fetch processor output's image DTO");
|
||||
|
||||
this.imageState = imageDTOToImageObject(imageDTO);
|
||||
this.parent.renderer.clearBuffer();
|
||||
|
||||
await this.parent.renderer.setBuffer(this.imageState);
|
||||
this.parent.renderer.hideObjects([this.imageState.id]);
|
||||
this.manager.socket.off('invocation_complete', listener);
|
||||
|
||||
this.parent.renderer.hideObjects();
|
||||
this.manager.stateApi.$isProcessingFilter.set(false);
|
||||
};
|
||||
|
||||
this.manager.socket.on('invocation_complete', listener);
|
||||
|
||||
this.log.trace({ enqueueBatchArg: parseify(enqueueBatchArg) }, 'Enqueuing filter batch');
|
||||
this.log.trace({ enqueueBatchArg } as SerializableObject, 'Enqueuing filter batch');
|
||||
|
||||
this.manager.stateApi.$isProcessingFilter.set(true);
|
||||
dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
@ -119,6 +126,7 @@ export class CanvasFilter {
|
||||
this.parent.renderer.showObjects();
|
||||
this.manager.stateApi.$filteringEntity.set(null);
|
||||
this.imageState = null;
|
||||
this.manager.stateApi.$isProcessingFilter.set(false);
|
||||
};
|
||||
|
||||
destroy = () => {
|
||||
|
@ -4,9 +4,11 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
|
||||
import {
|
||||
$filterConfig,
|
||||
$filteringEntity,
|
||||
$isDrawing,
|
||||
$isMouseDown,
|
||||
$isProcessingFilter,
|
||||
$lastAddedPoint,
|
||||
$lastCursorPos,
|
||||
$lastMouseDownPos,
|
||||
@ -161,9 +163,6 @@ export class CanvasStateApi {
|
||||
getIsSelected = (id: string) => {
|
||||
return this.getState().selectedEntityIdentifier?.id === id;
|
||||
};
|
||||
getLogLevel = () => {
|
||||
return this._store.getState().system.consoleLogLevel;
|
||||
};
|
||||
getFilterState = () => {
|
||||
return this._store.getState().canvasV2.filter;
|
||||
};
|
||||
@ -234,6 +233,8 @@ export class CanvasStateApi {
|
||||
|
||||
$transformingEntity = $transformingEntity;
|
||||
$filteringEntity = $filteringEntity;
|
||||
$filterConfig = $filterConfig;
|
||||
$isProcessingFilter = $isProcessingFilter;
|
||||
|
||||
$toolState: WritableAtom<CanvasV2State['tool']> = atom();
|
||||
$currentFill: WritableAtom<RgbaColor> = atom();
|
||||
|
@ -143,10 +143,6 @@ const initialState: CanvasV2State = {
|
||||
stagedImages: [],
|
||||
selectedStagedImageIndex: 0,
|
||||
},
|
||||
filter: {
|
||||
autoProcess: true,
|
||||
config: IMAGE_FILTERS.canny_image_processor.buildDefaults(),
|
||||
},
|
||||
};
|
||||
|
||||
export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) {
|
||||
@ -486,12 +482,6 @@ export const canvasV2Slice = createSlice({
|
||||
state.inpaintMask = deepClone(initialState.inpaintMask);
|
||||
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
||||
},
|
||||
filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => {
|
||||
state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults();
|
||||
},
|
||||
filterConfigChanged: (state, action: PayloadAction<{ config: FilterConfig }>) => {
|
||||
state.filter.config = action.payload.config;
|
||||
},
|
||||
rasterizationCachesInvalidated: (state) => {
|
||||
// Invalidate the rasterization caches for all entities.
|
||||
|
||||
@ -669,9 +659,6 @@ export const {
|
||||
sessionStagingAreaReset,
|
||||
sessionNextStagedImageSelected,
|
||||
sessionPrevStagedImageSelected,
|
||||
// Filter
|
||||
filterSelected,
|
||||
filterConfigChanged,
|
||||
} = canvasV2Slice.actions;
|
||||
|
||||
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
|
||||
@ -699,6 +686,8 @@ export const $lastCursorPos = atom<Coordinate | null>(null);
|
||||
export const $spaceKey = atom<boolean>(false);
|
||||
export const $transformingEntity = atom<CanvasEntityIdentifier | null>(null);
|
||||
export const $filteringEntity = atom<CanvasEntityIdentifier | null>(null);
|
||||
export const $filterConfig = atom<FilterConfig>(IMAGE_FILTERS.canny_image_processor.buildDefaults());
|
||||
export const $isProcessingFilter = atom(false);
|
||||
|
||||
export const canvasV2PersistConfig: PersistConfig<CanvasV2State> = {
|
||||
name: canvasV2Slice.name,
|
||||
|
@ -4,14 +4,8 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { isEqual, merge } from 'lodash-es';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasV2State,
|
||||
ControlNetConfig,
|
||||
Rect,
|
||||
T2IAdapterConfig,
|
||||
} from './types';
|
||||
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, Rect } from './types';
|
||||
import { initialControlNetV2 } from './types';
|
||||
|
||||
export const selectRasterLayer = (state: CanvasV2State, id: string) =>
|
||||
state.rasterLayers.entities.find((layer) => layer.id === id);
|
||||
@ -73,11 +67,8 @@ export const rasterLayersReducers = {
|
||||
state.rasterLayers.compositeRasterizationCache.push(action.payload);
|
||||
},
|
||||
rasterLayerConvertedToControlLayer: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; newId: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }>
|
||||
) => {
|
||||
const { id, newId, controlAdapter } = action.payload;
|
||||
reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
|
||||
const { id, newId } = action.payload;
|
||||
const layer = selectRasterLayer(state, id);
|
||||
if (!layer) {
|
||||
return;
|
||||
@ -88,7 +79,7 @@ export const rasterLayersReducers = {
|
||||
...deepClone(layer),
|
||||
id: newId,
|
||||
type: 'control_layer',
|
||||
controlAdapter,
|
||||
controlAdapter: deepClone(initialControlNetV2),
|
||||
withTransparencyEffect: true,
|
||||
};
|
||||
|
||||
@ -103,7 +94,7 @@ export const rasterLayersReducers = {
|
||||
|
||||
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
|
||||
},
|
||||
prepare: (payload: { id: string; controlAdapter: ControlNetConfig | T2IAdapterConfig }) => ({
|
||||
prepare: (payload: { id: string }) => ({
|
||||
payload: { ...payload, newId: getPrefixedId('control_layer') },
|
||||
}),
|
||||
},
|
||||
|
@ -930,10 +930,6 @@ export type CanvasV2State = {
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
};
|
||||
filter: {
|
||||
autoProcess: boolean;
|
||||
config: FilterConfig;
|
||||
};
|
||||
};
|
||||
|
||||
export type StageAttrs = {
|
||||
|
@ -44,8 +44,12 @@ export type RGIPAdapterImageDropData = BaseDropData & {
|
||||
};
|
||||
};
|
||||
|
||||
export type AddLayerFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_LAYER_FROM_IMAGE';
|
||||
export type AddRasterLayerFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_RASTER_LAYER_FROM_IMAGE';
|
||||
};
|
||||
|
||||
export type AddControlLayerFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE';
|
||||
};
|
||||
|
||||
type UpscaleInitialImageDropData = BaseDropData & {
|
||||
@ -91,7 +95,8 @@ export type TypesafeDroppableData =
|
||||
| RGIPAdapterImageDropData
|
||||
| SelectForCompareDropData
|
||||
| UpscaleInitialImageDropData
|
||||
| AddLayerFromImageDropData;
|
||||
| AddRasterLayerFromImageDropData
|
||||
| AddControlLayerFromImageDropData;
|
||||
|
||||
type BaseDragData = {
|
||||
id: string;
|
||||
|
@ -14,19 +14,13 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?
|
||||
|
||||
switch (actionType) {
|
||||
case 'SET_CURRENT_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_CA_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_IPA_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_RG_IP_ADAPTER_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'ADD_LAYER_FROM_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'ADD_RASTER_LAYER_FROM_IMAGE':
|
||||
case 'ADD_CONTROL_LAYER_FROM_IMAGE':
|
||||
case 'SET_UPSCALE_INITIAL_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SET_NODES_IMAGE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'SELECT_FOR_COMPARE':
|
||||
return payloadType === 'IMAGE_DTO';
|
||||
case 'ADD_TO_BOARD': {
|
||||
|
@ -4,7 +4,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
|
||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
|
||||
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
@ -115,7 +115,7 @@ const ParametersPanelTextToImage = () => {
|
||||
</Flex>
|
||||
</TabPanel>
|
||||
<TabPanel p={0} w="full" h="full">
|
||||
<ControlLayersPanelContent />
|
||||
<CanvasEntityList />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
Loading…
Reference in New Issue
Block a user