From 0b5f4cac57d3939f0c600295a9d30df402005098 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:37:43 +1000 Subject: [PATCH] feat(ui): dnd image into layer --- .../listeners/imageDropped.ts | 10 ++--- .../components/CanvasDropArea.tsx | 19 +++++++++ .../components/ControlLayersEditor.tsx | 2 + .../controlLayers/components/Layer/Layer.tsx | 4 +- .../controlLayers/konva/CanvasManager.ts | 12 +++--- .../controlLayers/store/canvasV2Slice.ts | 2 +- .../controlLayers/store/layersReducers.ts | 42 ++++++++++--------- .../web/src/features/dnd/types/index.ts | 9 ++-- .../web/src/features/dnd/util/isValidDrop.ts | 2 +- .../ui/components/tabs/TextToImageTab.tsx | 8 +++- 10 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx 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 8368e42ec3..c551fa88b7 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 @@ -5,9 +5,10 @@ import { parseify } from 'common/util/serialize'; import { caImageChanged, ipaImageChanged, - layerImageAdded, + layerAddedFromImage, rgIPAdapterImageChanged, } from 'features/controlLayers/store/canvasV2Slice'; +import { imageDTOToImageObject } from 'features/controlLayers/store/types'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; import { @@ -28,7 +29,7 @@ export const dndDropped = createAction<{ export const addImageDroppedListener = (startAppListening: AppStartListening) => { startAppListening({ actionCreator: dndDropped, - effect: async (action, { dispatch, getState }) => { + effect: (action, { dispatch, getState }) => { const log = logger('dnd'); const { activeData, overData } = action.payload; if (!isValidDrop(overData, activeData)) { @@ -101,12 +102,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => * Image dropped on Raster layer */ if ( - overData.actionType === 'ADD_LAYER_IMAGE' && + overData.actionType === 'ADD_LAYER_FROM_IMAGE' && activeData.payloadType === 'IMAGE_DTO' && activeData.payload.imageDTO ) { - const { id } = overData.context; - dispatch(layerImageAdded({ id, imageDTO: activeData.payload.imageDTO })); + dispatch(layerAddedFromImage({ imageObject: imageDTOToImageObject(activeData.payload.imageDTO) })); return; } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx new file mode 100644 index 0000000000..b5a1c636a5 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -0,0 +1,19 @@ +import { Flex } from '@invoke-ai/ui-library'; +import IAIDroppable from 'common/components/IAIDroppable'; +import type { AddLayerFromImageDropData } from 'features/dnd/types'; +import { memo } from 'react'; + +const addLayerFromImageDropData: AddLayerFromImageDropData = { + id: 'add-layer-from-image-drop-data', + actionType: 'ADD_LAYER_FROM_IMAGE', +}; + +export const CanvasDropArea = memo(() => { + return ( + + + + ); +}); + +CanvasDropArea.displayName = 'CanvasDropArea'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx index a2a4bf9126..71e35263b5 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersEditor.tsx @@ -1,5 +1,6 @@ /* eslint-disable i18next/no-literal-string */ import { Flex } from '@invoke-ai/ui-library'; +import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea'; import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar'; import { StageComponent } from 'features/controlLayers/components/StageComponent'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; @@ -24,6 +25,7 @@ export const ControlLayersEditor = memo(() => { {/* */} + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx index ffc6ce9b74..d8f73cbaaa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -9,7 +9,7 @@ import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerA import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; -import type { LayerImageDropData } from 'features/dnd/types'; +import type { AddLayerFromImageDropData } from 'features/dnd/types'; import { memo, useMemo } from 'react'; import { LayerOpacity } from './LayerOpacity'; @@ -21,7 +21,7 @@ type Props = { export const Layer = memo(({ id }: Props) => { const entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const droppableData = useMemo( + const droppableData = useMemo( () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), [id] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index 2d91a78577..722c6ce470 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -97,12 +97,12 @@ type EntityStateAndAdapter = state: CanvasInpaintMaskState; adapter: CanvasMaskAdapter; } - | { - id: string; - type: CanvasControlAdapterState['type']; - state: CanvasControlAdapterState; - adapter: CanvasControlAdapter; - } + // | { + // id: string; + // type: CanvasControlAdapterState['type']; + // state: CanvasControlAdapterState; + // adapter: CanvasControlAdapter; + // } | { id: string; type: CanvasRegionalGuidanceState['type']; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 4159d9f7da..ca37dc9517 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -411,9 +411,9 @@ export const { bboxSizeOptimized, // layers layerAdded, + layerAddedFromImage, layerRecalled, layerOpacityChanged, - layerImageAdded, layerAllDeleted, layerImageCacheChanged, // IP Adapters diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 78be3cf6f1..48e01cd1f3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -4,8 +4,8 @@ import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types'; -import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; +import type { CanvasImageState, CanvasLayerState, CanvasV2State } from './types'; +import { imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); export const selectLayerOrThrow = (state: CanvasV2State, id: string) => { @@ -25,6 +25,7 @@ export const layersReducers = { objects: [], opacity: 1, position: { x: 0, y: 0 }, + imageCache: null, }; merge(layer, action.payload.overrides); state.layers.entities.push(layer); @@ -41,6 +42,26 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, + layerAddedFromImage: { + reducer: (state, action: PayloadAction<{ id: string; imageObject: CanvasImageState }>) => { + const { id, imageObject } = action.payload; + const layer: CanvasLayerState = { + id, + type: 'layer', + isEnabled: true, + objects: [imageObject], + opacity: 1, + position: { x: 0, y: 0 }, + imageCache: null, + }; + state.layers.entities.push(layer); + state.selectedEntityIdentifier = { type: 'layer', id }; + state.layers.imageCache = null; + }, + prepare: (payload: { imageObject: CanvasImageState }) => ({ + payload: { ...payload, id: getPrefixedId('layer') }, + }), + }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null; @@ -54,23 +75,6 @@ export const layersReducers = { layer.opacity = opacity; state.layers.imageCache = null; }, - layerImageAdded: ( - state, - action: PayloadAction - ) => { - const { id, imageDTO, pos } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - const imageObject = imageDTOToImageObject(imageDTO); - if (pos) { - imageObject.x = pos.x; - imageObject.y = pos.y; - } - layer.objects.push(imageObject); - state.layers.imageCache = null; - }, layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 19dcbf19c0..43e2ca36f2 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -44,11 +44,8 @@ export type RGIPAdapterImageDropData = BaseDropData & { }; }; -export type LayerImageDropData = BaseDropData & { - actionType: 'ADD_LAYER_IMAGE'; - context: { - id: string; - }; +export type AddLayerFromImageDropData = BaseDropData & { + actionType: 'ADD_LAYER_FROM_IMAGE'; }; type UpscaleInitialImageDropData = BaseDropData & { @@ -94,7 +91,7 @@ export type TypesafeDroppableData = | RGIPAdapterImageDropData | SelectForCompareDropData | UpscaleInitialImageDropData - | LayerImageDropData; + | AddLayerFromImageDropData; 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 128e5c5d50..0de8d48a94 100644 --- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts +++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts @@ -21,7 +21,7 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData? return payloadType === 'IMAGE_DTO'; case 'SET_RG_IP_ADAPTER_IMAGE': return payloadType === 'IMAGE_DTO'; - case 'ADD_LAYER_IMAGE': + case 'ADD_LAYER_FROM_IMAGE': return payloadType === 'IMAGE_DTO'; case 'SET_UPSCALE_INITIAL_IMAGE': return payloadType === 'IMAGE_DTO'; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx index 5be0195046..736c5559e0 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImageTab.tsx @@ -10,8 +10,12 @@ const TextToImageTab = () => { return ( - {imageViewer.isOpen && } - + {imageViewer.isOpen && ( + <> + + + + )} ); };