From 7e400d876f74ccd675e415ed8b10d6803c57ca05 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:45:21 +1000 Subject: [PATCH] feat(ui): iterate on filter UI, flow --- invokeai/frontend/web/public/locales/en.json | 9 +- .../frontend/web/src/app/components/App.tsx | 2 +- .../listeners/imageDropped.ts | 29 ++++- .../common/components/IAIImageFallback.tsx | 35 +++--- .../components/CanvasDropArea.tsx | 24 ++-- .../ControlLayerControlAdapterModel.tsx | 25 +++- .../components/ControlLayersEditor.tsx | 4 + .../components/ControlLayersPanelContent.tsx | 29 ----- .../components/Filters/Filter.tsx | 77 +++++++++--- .../components/Filters/FilterSettings.tsx | 115 +++++++++--------- .../components/Filters/FilterTypeSelect.tsx | 19 +-- .../common/CanvasEntityMenuItemsFilter.tsx | 2 +- .../controlLayers/konva/CanvasFilter.ts | 24 ++-- .../controlLayers/konva/CanvasStateApi.ts | 7 +- .../controlLayers/store/canvasV2Slice.ts | 15 +-- .../store/rasterLayersReducers.ts | 21 +--- .../src/features/controlLayers/store/types.ts | 4 - .../web/src/features/dnd/types/index.ts | 11 +- .../web/src/features/dnd/util/isValidDrop.ts | 10 +- .../ParametersPanelTextToImage.tsx | 4 +- 20 files changed, 266 insertions(+), 200 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 88eb4077bb..9fd475d476 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -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": { diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index d7b1acd839..ed32818dba 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 6a7302a55a..5e65502e71 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -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 = { + objects: [imageObject], + position: { x, y }, + }; + dispatch(controlLayerAdded({ overrides, isSelected: true })); + return; + } + /** * Image dropped on node image field */ diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx index 229f9ba6d9..0c8338f561 100644 --- a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -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 ( - + {icon && } {props.label && {props.label}} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx index 5c90c82a6f..d341c1c6d9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -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 ( - - - + <> + + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index f50218e4aa..daf5cd9292 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -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( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index d10af98f7a..d0036cf4bd 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -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(() => { + + + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx deleted file mode 100644 index e408d38bf9..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ /dev/null @@ -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 ( - - - - - {Boolean(filteringEntity) && ( - <> - - - - - - )} - - ); -}); - -ControlLayersPanelContent.displayName = 'ControlLayersPanelContent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx index e2052026ab..aaed63a823 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/Filter.tsx @@ -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 ( - - - - - - - ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx index 6b90ab9c14..69514c544c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterSettings.tsx @@ -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 ; + } + + if (filterConfig.type === 'color_map_image_processor') { + return ; + } + + if (filterConfig.type === 'content_shuffle_image_processor') { + return ; + } + + if (filterConfig.type === 'depth_anything_image_processor') { + return ; + } + + if (filterConfig.type === 'dw_openpose_image_processor') { + return ; + } + + if (filterConfig.type === 'hed_image_processor') { + return ; + } + + if (filterConfig.type === 'lineart_image_processor') { + return ; + } + + if (filterConfig.type === 'mediapipe_face_processor') { + return ; + } + + if (filterConfig.type === 'midas_depth_image_processor') { + return ; + } + + if (filterConfig.type === 'mlsd_image_processor') { + return ; + } + + if (filterConfig.type === 'pidi_image_processor') { + return ; + } + + return ( + ); - - if (config.type === 'canny_image_processor') { - return ; - } - - if (config.type === 'color_map_image_processor') { - return ; - } - - if (config.type === 'content_shuffle_image_processor') { - return ; - } - - if (config.type === 'depth_anything_image_processor') { - return ; - } - - if (config.type === 'dw_openpose_image_processor') { - return ; - } - - if (config.type === 'hed_image_processor') { - return ; - } - - if (config.type === 'lineart_image_processor') { - return ; - } - - if (config.type === 'mediapipe_face_processor') { - return ; - } - - if (config.type === 'midas_depth_image_processor') { - return ; - } - - if (config.type === 'mlsd_image_processor') { - return ; - } - - if (config.type === 'pidi_image_processor') { - return ; - } - - return ; }); FilterSettings.displayName = 'Filter'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx index 2e765848bd..fb2537e7ac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx @@ -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(() => { - {t('controlLayers.filter')} + {t('controlLayers.filter.filterType')} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx index 5e15543e5c..7dd4e48046 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsFilter.tsx @@ -14,7 +14,7 @@ export const CanvasEntityMenuItemsFilter = memo(() => { return ( }> - {t('controlLayers.filter')} + {t('controlLayers.filter.filter')} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts index e2821b74f7..c1a094a84a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasFilter.ts @@ -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 = () => { diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index cde1218bcd..32a7e5615e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -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 = atom(); $currentFill: WritableAtom = atom(); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 7a8670097c..0101170544 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -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(null); export const $spaceKey = atom(false); export const $transformingEntity = atom(null); export const $filteringEntity = atom(null); +export const $filterConfig = atom(IMAGE_FILTERS.canny_image_processor.buildDefaults()); +export const $isProcessingFilter = atom(false); export const canvasV2PersistConfig: PersistConfig = { name: canvasV2Slice.name, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts index 1fcbb7cd92..67af35c3f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts @@ -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') }, }), }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index aec0ca9891..369fb73f34 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -930,10 +930,6 @@ export type CanvasV2State = { stagedImages: StagingAreaImage[]; selectedStagedImageIndex: number; }; - filter: { - autoProcess: boolean; - config: FilterConfig; - }; }; export type StageAttrs = { diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 43e2ca36f2..88322da95a 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -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; diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts index 0de8d48a94..73c28545e7 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -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': { diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 9b73c16368..43c878d8f2 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -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 = () => { - +