feat(ui): dnd image into layer

This commit is contained in:
psychedelicious 2024-08-07 18:37:43 +10:00
parent c988c58c63
commit 0b5f4cac57
10 changed files with 68 additions and 42 deletions

View File

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

View File

@ -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 (
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
</Flex>
);
});
CanvasDropArea.displayName = 'CanvasDropArea';

View File

@ -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(() => {
{/* <Flex position="absolute" top={0} right={0} bottom={0} left={0} align="center" justify="center">
<CanvasResizer />
</Flex> */}
<CanvasDropArea />
</Flex>
);
});

View File

@ -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<CanvasEntityIdentifier>(() => ({ id, type: 'layer' }), [id]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false });
const droppableData = useMemo<LayerImageDropData>(
const droppableData = useMemo<AddLayerFromImageDropData>(
() => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }),
[id]
);

View File

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

View File

@ -411,9 +411,9 @@ export const {
bboxSizeOptimized,
// layers
layerAdded,
layerAddedFromImage,
layerRecalled,
layerOpacityChanged,
layerImageAdded,
layerAllDeleted,
layerImageCacheChanged,
// IP Adapters

View File

@ -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<ImageObjectAddedArg & { objectId: string; pos?: { x: number; y: number } }>
) => {
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;

View File

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

View File

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

View File

@ -10,8 +10,12 @@ const TextToImageTab = () => {
return (
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
<ControlLayersEditor />
{imageViewer.isOpen && <ImageViewer />}
<ImageComparisonDroppable />
{imageViewer.isOpen && (
<>
<ImageViewer />
<ImageComparisonDroppable />
</>
)}
</Box>
);
};