feat(ui): simpler dnd typing implementation

This commit is contained in:
psychedelicious
2024-10-28 15:10:54 +10:00
parent 406fc58889
commit 93a3ed56e7
19 changed files with 523 additions and 602 deletions

View File

@@ -1,20 +1,15 @@
import { Grid, GridItem } from '@invoke-ai/ui-library';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import {
newControlLayerFromImageDndTarget,
addGlobalReferenceImageFromImageDndTarget,
newRasterLayerFromImageDndTarget,
addRegionalGuidanceReferenceImageFromImageDndTarget,
} from 'features/dnd2/types';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const addRasterLayerFromImageDndTargetData = newRasterLayerFromImageDndTarget.getData({});
const addControlLayerFromImageDndTargetData = newControlLayerFromImageDndTarget.getData({});
const addRasterLayerFromImageDndTargetData = Dnd.Target.newRasterLayerFromImage.getData();
const addControlLayerFromImageDndTargetData = Dnd.Target.newControlLayerFromImage.getData();
const addRegionalGuidanceReferenceImageFromImageDndTargetData =
addRegionalGuidanceReferenceImageFromImageDndTarget.getData({});
const addGlobalReferenceImageFromImageDndTargetData = addGlobalReferenceImageFromImageDndTarget.getData({});
Dnd.Target.newRegionalGuidanceReferenceImageFromImage.getData();
const addGlobalReferenceImageFromImageDndTargetData = Dnd.Target.newGlobalReferenceImageFromImage.getData();
export const CanvasDropArea = memo(() => {
const { t } = useTranslation();

View File

@@ -6,8 +6,8 @@ import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHook
import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectEntityCountActive } from 'features/controlLayers/store/selectors';
import type { Dnd } from 'features/dnd2/dnd';
import { DndDropOverlay } from 'features/dnd2/DndDropOverlay';
import type { DndState } from 'features/dnd2/types';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
@@ -84,8 +84,8 @@ const PanelTabs = memo(() => {
const { t } = useTranslation();
const store = useAppStore();
const activeEntityCount = useAppSelector(selectEntityCountActive);
const [layersTabDndState, setLayersTabDndState] = useState<DndState>('idle');
const [galleryTabDndState, setGalleryTabDndState] = useState<DndState>('idle');
const [layersTabDndState, setLayersTabDndState] = useState<Dnd.types['DndState']>('idle');
const [galleryTabDndState, setGalleryTabDndState] = useState<Dnd.types['DndState']>('idle');
const layersTabRef = useRef<HTMLDivElement>(null);
const galleryTabRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<number | null>(null);

View File

@@ -10,8 +10,8 @@ import { ControlLayerSettings } from 'features/controlLayers/components/ControlL
import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { replaceLayerWithImageDndTarget, type ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -25,8 +25,8 @@ export const ControlLayer = memo(({ id }: Props) => {
() => ({ id, type: 'control_layer' }),
[id]
);
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['replaceLayerWithImage']>(
() => Dnd.Target.replaceLayerWithImage.getData({ entityIdentifier }, entityIdentifier.id),
[entityIdentifier]
);

View File

@@ -4,9 +4,9 @@ import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import type { DndTargetData } from 'features/dnd2/types';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
@@ -31,7 +31,7 @@ const sx = {
type Props = {
image: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
targetData: DndTargetData;
targetData: Dnd.types['TargetDataUnion'];
postUploadAction: PostUploadAction;
};

View File

@@ -19,7 +19,7 @@ import {
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { setGlobalReferenceImageDndTarget, type SetGlobalReferenceImageDndTargetData } from 'features/dnd2/types';
import { Dnd } from 'features/dnd2/dnd';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold } from 'react-icons/pi';
@@ -84,9 +84,9 @@ export const IPAdapterSettings = memo(() => {
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
[entityIdentifier.id]
);
const targetData = useMemo<SetGlobalReferenceImageDndTargetData>(
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setGlobalReferenceImage']>(
() =>
setGlobalReferenceImageDndTarget.getData(
Dnd.Target.setGlobalReferenceImage.getData(
{ globalReferenceImageId: entityIdentifier.id },
ipAdapter.image?.image_name
),

View File

@@ -7,9 +7,8 @@ import { CanvasEntityEditableTitle } from 'features/controlLayers/components/com
import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
import { replaceLayerWithImageDndTarget } from 'features/dnd2/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -20,8 +19,8 @@ type Props = {
export const RasterLayer = memo(({ id }: Props) => {
const { t } = useTranslation();
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['replaceLayerWithImage']>(
() => Dnd.Target.replaceLayerWithImage.getData({ entityIdentifier }, entityIdentifier.id),
[entityIdentifier]
);

View File

@@ -20,8 +20,7 @@ import {
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd2/types';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd2/types';
import { Dnd } from 'features/dnd2/dnd';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
@@ -92,13 +91,10 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
[dispatch, entityIdentifier, referenceImageId]
);
const targetData = useMemo<SetRegionalGuidanceReferenceImageDndTargetData>(
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setRegionalGuidanceReferenceImage']>(
() =>
setRegionalGuidanceReferenceImageDndTarget.getData(
{
regionalGuidanceId: entityIdentifier.id,
referenceImageId,
},
Dnd.Target.setRegionalGuidanceReferenceImage.getData(
{ regionalGuidanceId: entityIdentifier.id, referenceImageId },
ipAdapter.image?.image_name
),
[entityIdentifier.id, ipAdapter.image?.image_name, referenceImageId]

View File

@@ -1,9 +1,9 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import type { DndState } from 'features/dnd2/types';
import type { Dnd } from 'features/dnd2/dnd';
import { memo } from 'react';
type Props = {
dndState: DndState;
dndState: Dnd.types['DndState'];
label?: string;
withBackdrop?: boolean;
};

View File

@@ -7,10 +7,8 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import { dndDropped } from 'app/store/middleware/listenerMiddleware/listeners/dnd';
import { useAppDispatch } from 'app/store/storeHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropOverlay } from 'features/dnd2/DndDropOverlay';
import type { DndState, DndTargetData } from 'features/dnd2/types';
import { getDndId, isDndSourceData, isValidDrop, singleImageDndSource } from 'features/dnd2/types';
import { memo, useEffect, useRef, useState } from 'react';
import { uploadImage } from 'services/api/endpoints/images';
import { z } from 'zod';
@@ -32,6 +30,7 @@ const sx = {
left: 0,
w: 'full',
h: 'full',
pointerEvents: 'auto',
// We must disable pointer events when idle to prevent the overlay from blocking clicks
'&[data-dnd-state="idle"]': {
pointerEvents: 'none',
@@ -61,14 +60,14 @@ const zUploadFile = z
type Props = {
label: string;
targetData: DndTargetData;
targetData: Dnd.types['TargetDataUnion'];
elementDropEnabled?: boolean;
externalDropEnabled?: boolean;
};
export const DndDropTarget = memo((props: Props) => {
const { label, targetData, elementDropEnabled = true, externalDropEnabled = true } = props;
const [dndState, setDndState] = useState<DndState>('idle');
const [dndState, setDndState] = useState<Dnd.types['DndState']>('idle');
const ref = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
@@ -89,13 +88,13 @@ export const DndDropTarget = memo((props: Props) => {
return false;
}
const sourceData = args.source.data;
if (!isDndSourceData(sourceData)) {
if (!Dnd.Util.isDndSourceData(sourceData)) {
return false;
}
if (getDndId(targetData) === getDndId(sourceData)) {
if (Dnd.Util.getDndId(targetData) === Dnd.Util.getDndId(sourceData)) {
return false;
}
return isValidDrop(sourceData, targetData);
return Dnd.Util.isValidDrop(sourceData, targetData);
},
onDragEnter: () => {
setDndState('over');
@@ -106,7 +105,7 @@ export const DndDropTarget = memo((props: Props) => {
getData: () => targetData,
onDrop: (args) => {
const sourceData = args.source.data;
if (!isDndSourceData(sourceData)) {
if (!Dnd.Util.isDndSourceData(sourceData)) {
return;
}
dispatch(dndDropped({ sourceData, targetData }));
@@ -115,13 +114,13 @@ export const DndDropTarget = memo((props: Props) => {
monitorForElements({
canMonitor: (args) => {
const sourceData = args.source.data;
if (!isDndSourceData(sourceData)) {
if (!Dnd.Util.isDndSourceData(sourceData)) {
return false;
}
if (getDndId(targetData) === getDndId(sourceData)) {
if (Dnd.Util.getDndId(targetData) === Dnd.Util.getDndId(sourceData)) {
return false;
}
return isValidDrop(sourceData, targetData);
return Dnd.Util.isValidDrop(sourceData, targetData);
},
onDragStart: () => {
setDndState('potential');
@@ -177,7 +176,7 @@ export const DndDropTarget = memo((props: Props) => {
});
dispatch(
dndDropped({
sourceData: singleImageDndSource.getData({ imageDTO }, getPrefixedId('dnd-upload-image')),
sourceData: Dnd.Source.singleImage.getData({ imageDTO }),
targetData,
})
);

View File

@@ -2,7 +2,7 @@ import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import type { ImageProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { Image } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { singleImageDndSource } from 'features/dnd2/types';
import { Dnd } from 'features/dnd2/dnd';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { memo, useEffect, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
@@ -33,7 +33,7 @@ export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
}
return draggable({
element,
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
getInitialData: () => Dnd.Source.singleImage.getData({ imageDTO }, imageDTO.image_name),
onDragStart: () => {
setIsDragging(true);
},

View File

@@ -0,0 +1,414 @@
/* eslint-disable @typescript-eslint/no-namespace */ // We will use namespaces to organize the Dnd types
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { BoardId } from 'features/gallery/store/types';
import type { ImageDTO } from 'services/api/types';
import type { ValueOf } from 'type-fest';
import type { Jsonifiable } from 'type-fest/source/jsonifiable';
type EmptyObject = Record<string, never>;
type RecordUnknown = Record<string | symbol, unknown>;
/**
* This file contains types, APIs, and utilities for Dnd functionality, as provided by pragmatic-drag-and-drop:
* - Source and target data types
* - Builders for source and target data types, which create type guards, data-getters and validation functions
* - Other utilities for working with Dnd data
* - A function to validate whether a drop is valid, given the source and target data
*
* See:
* - https://github.com/atlassian/pragmatic-drag-and-drop
* - https://atlassian.design/components/pragmatic-drag-and-drop/about
*/
type DndKind = 'source' | 'target';
type Data<T extends string = string, K extends DndKind = DndKind, P extends Jsonifiable = Jsonifiable> = {
meta: {
id: string;
type: T;
kind: K;
};
payload: P;
};
/**
* Builds a type guard for a specific DndData type.
* @param key The unique symbol key for the DndData type.
* @returns A type guard for the DndData type.
*/
const _buildDataTypeGuard = <T extends Data>(type: string, kind: DndKind) => {
// pragmatic-drag-and-drop types all data as unknown, so we need to cast it to the expected type
return (data: RecordUnknown): data is T => {
try {
return (data as Data).meta.type === type && (data as Data).meta.kind === kind;
} catch {
return false;
}
};
};
/**
* Builds a getter for a specific DndData type.
*
* The getter accepts arbitrary data and an optional Dnd ID. If no Dnd ID is provided, a unique one is generated.
*
* @param key The unique symbol key for the DndData type.
* @returns A getter for the DndData type.
*/
const _buildDataGetter =
<T extends Data>(type: T['meta']['type'], kind: T['meta']['kind']) =>
(payload: T['payload'] extends EmptyObject ? void : T['payload'], dndId?: string | null): T => {
return {
meta: {
id: dndId ?? getPrefixedId(`dnd-${kind}-${type}`),
type,
kind,
},
payload,
} as T;
};
/**
* An API for a Dnd source. It provides a type guard, a getter, and a unique symbol key for the DndData type.
*/
type DndSourceAPI<T extends Data> = {
type: string;
kind: 'source';
/**
* A type guard for the DndData type.
* @param data The data to check.
* @returns Whether the data is of the DndData type.
*/
typeGuard: ReturnType<typeof _buildDataTypeGuard<T>>;
/**
* A getter for the DndData type.
* @param data The data to get.
* @param dndId The Dnd ID to use. If not provided, a unique one is generated.
* @returns The DndData.
*/
getData: ReturnType<typeof _buildDataGetter<T>>;
};
/**
* Builds a DndSourceAPI object.
* @param key The unique symbol key for the DndData type.
*/
const buildDndSourceApi = <P extends Jsonifiable = EmptyObject>(type: string) => {
return {
type,
kind: 'source',
typeGuard: _buildDataTypeGuard<Data<typeof type, 'source', P>>(type, 'source'),
getData: _buildDataGetter<Data<typeof type, 'source', P>>(type, 'source'),
} satisfies DndSourceAPI<Data<typeof type, 'source', P>>;
};
//#region DndSourceData
/**
* Dnd source API for single image source.
*/
const singleImage = buildDndSourceApi<{ imageDTO: ImageDTO }>('SingleImage');
/**
* Dnd source API for multiple image source.
*/
const multipleImage = buildDndSourceApi<{ imageDTOs: ImageDTO[]; boardId: BoardId }>('MultipleImage');
const DndSource = {
singleImage,
multipleImage,
} as const;
type SourceDataTypeMap = {
[K in keyof typeof DndSource]: ReturnType<(typeof DndSource)[K]['getData']>;
};
/**
* A union of all possible DndSourceData types.
*/
type SourceDataUnion = ValueOf<SourceDataTypeMap>;
//#endregion
//#region DndTargetData
/**
* An API for a Dnd target. It extends the DndSourceAPI with a validateDrop function.
*/
type DndTargetApi<T extends Data> = DndSourceAPI<T> & {
/**
* Validates whether a drop is valid, give the source and target data.
* @param sourceData The source data (i.e. the data being dragged)
* @param targetData The target data (i.e. the data being dragged onto)
* @returns Whether the drop is valid.
*/
validateDrop: (sourceData: Data<string, 'source', Jsonifiable>, targetData: T) => boolean;
};
/**
* Builds a DndTargetApi object.
* @param key The unique symbol key for the DndData type.
* @param validateDrop A function that validates whether a drop is valid.
*/
const buildDndTargetApi = <P extends Jsonifiable = EmptyObject>(
type: string,
validateDrop: (sourceData: Data<string, 'source', Jsonifiable>, targetData: Data<typeof type, 'target', P>) => boolean
) => {
return {
type,
kind: 'source',
typeGuard: _buildDataTypeGuard<Data<typeof type, 'target', P>>(type, 'target'),
getData: _buildDataGetter<Data<typeof type, 'target', P>>(type, 'target'),
validateDrop,
} satisfies DndTargetApi<Data<typeof type, 'target', P>>;
};
/**
* Dnd target API for setting the image on an existing Global Reference Image layer.
*/
const setGlobalReferenceImage = buildDndTargetApi<{ globalReferenceImageId: string }>(
'SetGlobalReferenceImage',
singleImage.typeGuard
);
/**
* Dnd target API for setting the image on an existing Regional Guidance layer's Reference Image.
*/
const setRegionalGuidanceReferenceImage = buildDndTargetApi<{
regionalGuidanceId: string;
referenceImageId: string;
}>('SetRegionalGuidanceReferenceImage', singleImage.typeGuard);
/**
* Dnd target API for creating a new a Raster Layer from an image.
*/
const newRasterLayerFromImage = buildDndTargetApi('NewRasterLayerFromImage', singleImage.typeGuard);
/**
* Dnd target API for creating a new a Control Layer from an image.
*/
const newControlLayerFromImage = buildDndTargetApi('NewControlLayerFromImage', singleImage.typeGuard);
/**
* Dnd target API for adding an Inpaint Mask from an image.
*/
const newInpaintMaskFromImage = buildDndTargetApi('NewInpaintMaskFromImage', singleImage.typeGuard);
/**
* Dnd target API for adding a new Global Reference Image layer with a pre-set Reference Image from an image.
*/
const newGlobalReferenceImageFromImage = buildDndTargetApi('NewGlobalReferenceImageFromImage', singleImage.typeGuard);
/**
* Dnd target API for adding a new Regional Guidance layer from an image.
*/
const newRegionalGuidanceFromImage = buildDndTargetApi('NewRegionalGuidanceFromImage', singleImage.typeGuard);
/**
* Dnd target API for adding a new Regional Guidance layer with a pre-set Reference Image from an image.
*/
const newRegionalGuidanceReferenceImageFromImage = buildDndTargetApi(
'NewRegionalGuidanceReferenceImageFromImage',
singleImage.typeGuard
);
/**
* Dnd target API for replacing the content of a layer with an image. This works for Control Layers, Raster Layers,
* Inpaint Masks, and Regional Guidance layers.
*/
const replaceLayerWithImage = buildDndTargetApi<{
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
}>('ReplaceLayerWithImage', singleImage.typeGuard);
/**
* Dnd target API for setting the initial image on the upscaling tab.
*/
const setUpscaleInitialImageFromImage = buildDndTargetApi('SetUpscaleInitialImageFromImage', singleImage.typeGuard);
/**
* Dnd target API for setting an image field on a node.
*/
const setNodeImageField = buildDndTargetApi<{ nodeId: string; fieldName: string }>(
'SetNodeImageField',
singleImage.typeGuard
);
/**
* Dnd target API for selecting images for comparison.
*/
const selectForCompare = buildDndTargetApi<{
firstImageName?: string | null;
secondImageName?: string | null;
}>('SelectForCompare', (sourceData, targetData) => {
if (!singleImage.typeGuard(sourceData)) {
return false;
}
// Do not allow the same images to be selected for comparison
if (sourceData.payload.imageDTO.image_name === targetData.payload.firstImageName) {
return false;
}
if (sourceData.payload.imageDTO.image_name === targetData.payload.secondImageName) {
return false;
}
return true;
});
/**
* Dnd target API for adding an image to a board.
*/
const addToBoard = buildDndTargetApi<{ boardId: string }>('AddToBoard', (sourceData, targetData) => {
if (singleImage.typeGuard(sourceData)) {
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
const destinationBoard = targetData.payload.boardId;
return currentBoard !== destinationBoard;
}
if (multipleImage.typeGuard(sourceData)) {
const currentBoard = sourceData.payload.boardId;
const destinationBoard = targetData.payload.boardId;
return currentBoard !== destinationBoard;
}
return false;
});
/**
* Dnd target API for removing an image from a board.
*/
const removeFromBoard = buildDndTargetApi('RemoveFromBoard', (sourceData) => {
if (singleImage.typeGuard(sourceData)) {
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
return currentBoard !== 'none';
}
if (multipleImage.typeGuard(sourceData)) {
const currentBoard = sourceData.payload.boardId;
return currentBoard !== 'none';
}
return false;
});
const DndTarget = {
/**
* Set the image on an existing Global Reference Image layer.
*/
setGlobalReferenceImage,
setRegionalGuidanceReferenceImage,
// Add layer from image
newRasterLayerFromImage,
newControlLayerFromImage,
// Add a layer w/ ref image preset
newGlobalReferenceImageFromImage,
newRegionalGuidanceReferenceImageFromImage,
// Replace layer content w/ image
replaceLayerWithImage,
// Set the upscale image
setUpscaleInitialImageFromImage,
// Set a field on a node
setNodeImageField,
// Select images for comparison
selectForCompare,
// Add an image to a board
addToBoard,
// Remove an image from a board - essentially add to Uncategorized
removeFromBoard,
// These are currently unused
newRegionalGuidanceFromImage,
newInpaintMaskFromImage,
} as const;
type TargetDataTypeMap = {
[K in keyof typeof DndTarget]: ReturnType<(typeof DndTarget)[K]['getData']>;
};
type TargetDataUnion = ValueOf<TargetDataTypeMap>;
const targetApisArray = Object.values(DndTarget);
//#endregion
export declare namespace Dnd {
export type types = {
/**
* A union of all Dnd states.
* - `idle`: No drag is occurring, or the drag is not valid for the current drop target.
* - `potential`: A drag is occurring, and the drag is valid for the current drop target, but the drag is not over the
* drop target.
* - `over`: A drag is occurring, and the drag is valid for the current drop target, and the drag is over the drop target.
*/
DndState: 'idle' | 'potential' | 'over';
/**
* A map of target APIs to their data types.
*/
SourceDataTypeMap: SourceDataTypeMap;
/**
* A union of all possible source data types.
*/
SourceDataUnion: SourceDataUnion;
/**
* A map of target APIs to their data types.
*/
TargetDataTypeMap: TargetDataTypeMap;
/**
* A union of all possible target data types.
*/
TargetDataUnion: TargetDataUnion;
};
}
export const Dnd = {
Source: DndSource,
Target: DndTarget,
Util: {
/**
* Gets the Dnd ID from a DndData object.
* @param data The DndData object.
* @returns The Dnd ID.
*/
getDndId: (data: Data): string => {
return data.meta.id;
},
/**
* Checks if the data is a Dnd source data object.
* @param data The data to check.
*/
isDndSourceData: (data: RecordUnknown): data is SourceDataUnion => {
try {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
return (data as Data).meta.kind === 'source';
} catch {
return false;
}
},
/**
* Checks if the data is a Dnd target data object.
* @param data The data to check.
*/
isDndTargetData: (data: RecordUnknown): data is TargetDataUnion => {
try {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
return (data as Data).meta.kind === 'target';
} catch {
return false;
}
},
/**
* Validates whether a drop is valid.
* @param sourceData The data being dragged.
* @param targetData The data of the target being dragged onto.
* @returns Whether the drop is valid.
*/
isValidDrop: (sourceData: SourceDataUnion, targetData: TargetDataUnion): boolean => {
for (const targetApi of targetApisArray) {
if (targetApi.typeGuard(targetData)) {
/**
* TS cannot narrow the type of the targetApi and will error in the validator call.
* We've just checked that targetData is of the right type, though, so this cast to `any` is safe.
*/
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
return targetApi.validateDrop(sourceData, targetData as any);
}
}
return false;
},
},
};

View File

@@ -1,465 +0,0 @@
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { BoardId } from 'features/gallery/store/types';
import type { ImageDTO } from 'services/api/types';
/**
* A unique symbol key for a DndData object's ID.
*/
const _dndIdKey = Symbol('DndId');
/**
* The base DndData type. It consists of an ID, keyed by the _dndIdKey symbol, and any arbitrary data.
*/
export type BaseDndData = { [_dndIdKey]: string } & Record<string | symbol, unknown>;
/**
* Builds a type guard for a specific DndData type.
* @param key The unique symbol key for the DndData type.
* @returns A type guard for the DndData type.
*/
const _buildDataTypeGuard =
<T extends BaseDndData>(key: symbol) =>
(data: Record<string | symbol, unknown>): data is T => {
return Boolean(data[key]);
};
/**
* Builds a getter for a specific DndData type.
*
* The getter accepts arbitrary data and an optional Dnd ID. If no Dnd ID is provided, a unique one is generated.
*
* @param key The unique symbol key for the DndData type.
* @returns A getter for the DndData type.
*/
const _buildDataGetter =
<T extends BaseDndData>(key: symbol) =>
(data: Omit<T, typeof key>, dndId?: string | null): T => {
return {
[key]: true,
[_dndIdKey]: dndId ?? getPrefixedId(`dnd-${key.toString()}`),
...data,
} as T;
};
/**
* An API for a Dnd source. It provides a type guard, a getter, and a unique symbol key for the DndData type.
*/
type DndSourceAPI<T extends BaseDndData> = {
/**
* The unique symbol key for the DndData type. This is used to identify the type of data.
*/
key: symbol;
/**
* A type guard for the DndData type.
* @param data The data to check.
* @returns Whether the data is of the DndData type.
*/
typeGuard: ReturnType<typeof _buildDataTypeGuard<T>>;
/**
* A getter for the DndData type.
* @param data The data to get.
* @param dndId The Dnd ID to use. If not provided, a unique one is generated.
* @returns The DndData.
*/
getData: ReturnType<typeof _buildDataGetter<T>>;
};
/**
* Builds a DndSourceAPI object.
* @param key The unique symbol key for the DndData type.
*/
const buildDndSourceApi = <T extends BaseDndData>(key: symbol): DndSourceAPI<T> => ({
key,
typeGuard: _buildDataTypeGuard<T>(key),
getData: _buildDataGetter<T>(key),
});
/**
* A helper type that adds a Dnd ID to a record type.
*/
type WithDndId<T extends Record<string | symbol, unknown>> = T & { [_dndIdKey]: string };
/**
* A DndData object. It has three parts:
* - A unique symbol key, PrivateKey, that identifies the type of data.
* - A Dnd ID, which is a unique string that identifies the data. This is keyed to the _dndIdKey symbol.
* - Arbitrary data
*/
type DndData<PrivateKey extends symbol, Data extends Record<string | symbol, unknown> = Record<string, never>> = {
[k in PrivateKey]: true;
} & WithDndId<Data>;
/**
* Gets the Dnd ID from a DndData object.
* @param data The DndData object.
* @returns The Dnd ID.
*/
export const getDndId = (data: BaseDndData): string => {
return data[_dndIdKey];
};
//#region DndSourceData
const _SingleImageDndSourceDataKey = Symbol('SingleImageDndSourceData');
/**
* Dnd source data for a single image being dragged.
*/
export type SingleImageDndSourceData = DndData<typeof _SingleImageDndSourceDataKey, { imageDTO: ImageDTO }>;
/**
* Dnd source API for single image source.
*/
export const singleImageDndSource = buildDndSourceApi<SingleImageDndSourceData>(_SingleImageDndSourceDataKey);
const _MultipleImageDndSourceDataKey = Symbol('MultipleImageDndSourceData');
/**
* Dnd source data for multiple images being dragged.
*/
export type MultipleImageDndSourceData = DndData<
typeof _MultipleImageDndSourceDataKey,
{
imageDTOs: ImageDTO[];
boardId: BoardId;
}
>;
/**
* Dnd source API for multiple image source.
*/
export const multipleImageDndSource = buildDndSourceApi<MultipleImageDndSourceData>(_MultipleImageDndSourceDataKey);
const sourceApis = [singleImageDndSource, multipleImageDndSource] as const;
/**
* A union of all possible DndSourceData types.
*/
export type DndSourceData = SingleImageDndSourceData | MultipleImageDndSourceData;
/**
* Checks if the data is a DndSourceData object.
* @param data The data to check.
*/
export const isDndSourceData = (data: Record<string | symbol, unknown>): data is DndSourceData => {
for (const sourceApi of sourceApis) {
if (sourceApi.typeGuard(data)) {
return true;
}
}
return false;
};
//#endregion
//#region DndTargetData
/**
* An API for a Dnd target. It extends the DndSourceAPI with a validateDrop function.
*/
type DndTargetApi<T extends BaseDndData> = DndSourceAPI<T> & {
/**
* Validates whether a drop is valid, give the source and target data.
* @param sourceData The source data (i.e. the data being dragged)
* @param targetData The target data (i.e. the data being dragged onto)
* @returns Whether the drop is valid.
*/
validateDrop: (sourceData: DndSourceData, targetData: T) => boolean;
};
/**
* Builds a DndTargetApi object.
* @param key The unique symbol key for the DndData type.
* @param validateDrop A function that validates whether a drop is valid.
*/
const buildDndTargetApi = <T extends BaseDndData>(
key: symbol,
validateDrop: DndTargetApi<T>['validateDrop']
): DndTargetApi<T> => ({
key,
typeGuard: _buildDataTypeGuard<T>(key),
getData: _buildDataGetter<T>(key),
validateDrop,
});
const _SetGlobalReferenceImageDndTargetDataKey = Symbol('SetGlobalReferenceImageDndTargetData');
/**
* Dnd target data for setting the image on an existing Global Reference Image layer.
*/
export type SetGlobalReferenceImageDndTargetData = DndData<
typeof _SetGlobalReferenceImageDndTargetDataKey,
{
globalReferenceImageId: string;
}
>;
/**
* Dnd target API for setting the image on an existing Global Reference Image layer.
*/
export const setGlobalReferenceImageDndTarget = buildDndTargetApi<SetGlobalReferenceImageDndTargetData>(
_SetGlobalReferenceImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetRegionalGuidanceReferenceImageDndTargetDataKey = Symbol('SetRegionalGuidanceReferenceImageDndTargetData');
/**
* Dnd target data for setting the image on an existing Regional Guidance layer's Reference Image.
*/
export type SetRegionalGuidanceReferenceImageDndTargetData = DndData<
typeof _SetRegionalGuidanceReferenceImageDndTargetDataKey,
{
regionalGuidanceId: string;
referenceImageId: string;
}
>;
/**
* Dnd target API for setting the image on an existing Regional Guidance layer's Reference Image.
*/
export const setRegionalGuidanceReferenceImageDndTarget =
buildDndTargetApi<SetRegionalGuidanceReferenceImageDndTargetData>(
_SetRegionalGuidanceReferenceImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _NewRasterLayerFromImageDndTargetDataKey = Symbol('NewRasterLayerFromImageDndTargetData');
/**
* Dnd target data for creating a new a Raster Layer from an image.
*/
export type NewRasterLayerFromImageDndTargetData = DndData<typeof _NewRasterLayerFromImageDndTargetDataKey>;
/**
* Dnd target API for creating a new a Raster Layer from an image.
*/
export const newRasterLayerFromImageDndTarget = buildDndTargetApi<NewRasterLayerFromImageDndTargetData>(
_NewRasterLayerFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _NewControlLayerFromImageDndTargetDataKey = Symbol('NewControlLayerFromImageDndTargetData');
/**
* Dnd target data for creating a new a Control Layer from an image.
*/
export type NewControlLayerFromImageDndTargetData = DndData<typeof _NewControlLayerFromImageDndTargetDataKey>;
/**
* Dnd target API for creating a new a Control Layer from an image.
*/
export const newControlLayerFromImageDndTarget = buildDndTargetApi<NewControlLayerFromImageDndTargetData>(
_NewControlLayerFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddInpaintMaskFromImageDndTargetDataKey = Symbol('AddInpaintMaskFromImageDndTargetData');
export type AddInpaintMaskFromImageDndTargetData = DndData<typeof _AddInpaintMaskFromImageDndTargetDataKey>;
export const addInpaintMaskFromImageDndTarget = buildDndTargetApi<AddInpaintMaskFromImageDndTargetData>(
_AddInpaintMaskFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddRegionalGuidanceFromImageDndTargetDataKey = Symbol('AddRegionalGuidanceFromImageDndTargetData');
export type AddRegionalGuidanceFromImageDndTargetData = DndData<typeof _AddRegionalGuidanceFromImageDndTargetDataKey>;
export const addRegionalGuidanceFromImageDndTarget = buildDndTargetApi<AddRegionalGuidanceFromImageDndTargetData>(
_AddRegionalGuidanceFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey = Symbol(
'AddRegionalGuidanceReferenceImageFromImageDndTargetData'
);
export type AddRegionalGuidanceReferenceImageFromImageDndTargetData = DndData<
typeof _AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey
>;
export const addRegionalGuidanceReferenceImageFromImageDndTarget =
buildDndTargetApi<AddRegionalGuidanceReferenceImageFromImageDndTargetData>(
_AddRegionalGuidanceReferenceImageFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddGlobalReferenceImageFromImageDndTargetDataKey = Symbol('AddGlobalReferenceImageFromImageDndTargetData');
export type AddGlobalReferenceImageFromImageDndTargetData = DndData<
typeof _AddGlobalReferenceImageFromImageDndTargetDataKey
>;
export const addGlobalReferenceImageFromImageDndTarget =
buildDndTargetApi<AddGlobalReferenceImageFromImageDndTargetData>(
_AddGlobalReferenceImageFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _ReplaceLayerWithImageDndTargetDataKey = Symbol('ReplaceLayerWithImageDndTargetData');
export type ReplaceLayerWithImageDndTargetData = DndData<
typeof _ReplaceLayerWithImageDndTargetDataKey,
{
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
}
>;
export const replaceLayerWithImageDndTarget = buildDndTargetApi<ReplaceLayerWithImageDndTargetData>(
_ReplaceLayerWithImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetUpscaleInitialImageFromImageDndTargetDataKey = Symbol('SetUpscaleInitialImageFromImageDndTargetData');
export type SetUpscaleInitialImageFromImageDndTargetData = DndData<
typeof _SetUpscaleInitialImageFromImageDndTargetDataKey
>;
export const setUpscaleInitialImageFromImageDndTarget = buildDndTargetApi<SetUpscaleInitialImageFromImageDndTargetData>(
_SetUpscaleInitialImageFromImageDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SetNodeImageFieldDndTargetDataKey = Symbol('SetNodeImageFieldDndTargetData');
export type SetNodeImageFieldDndTargetData = DndData<
typeof _SetNodeImageFieldDndTargetDataKey,
{
nodeId: string;
fieldName: string;
}
>;
export const setNodeImageFieldDndTarget = buildDndTargetApi<SetNodeImageFieldDndTargetData>(
_SetNodeImageFieldDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _SelectForCompareDndTargetDataKey = Symbol('SelectForCompareDndTargetData');
export type SelectForCompareDndTargetData = DndData<
typeof _SelectForCompareDndTargetDataKey,
{
firstImageName?: string | null;
secondImageName?: string | null;
}
>;
export const selectForCompareDndTarget = buildDndTargetApi<SelectForCompareDndTargetData>(
_SelectForCompareDndTargetDataKey,
(sourceData, targetData) => {
if (!singleImageDndSource.typeGuard(sourceData)) {
return false;
}
// Do not allow the same images to be selected for comparison
if (sourceData.imageDTO.image_name === targetData.firstImageName) {
return false;
}
if (sourceData.imageDTO.image_name === targetData.secondImageName) {
return false;
}
return true;
}
);
const _ToastDndTargetDataKey = Symbol('ToastDndTargetData');
export type ToastDndTargetData = DndData<typeof _ToastDndTargetDataKey>;
export const ToastDndTarget = buildDndTargetApi<ToastDndTargetData>(
_ToastDndTargetDataKey,
singleImageDndSource.typeGuard
);
const _AddToBoardDndTargetDataKey = Symbol('AddToBoardDndTargetData');
export type AddToBoardDndTargetData = DndData<
typeof _AddToBoardDndTargetDataKey,
{
boardId: string;
}
>;
export const addToBoardDndTarget = buildDndTargetApi<AddToBoardDndTargetData>(
_AddToBoardDndTargetDataKey,
(sourceData, targetData) => {
if (singleImageDndSource.typeGuard(sourceData)) {
const { imageDTO } = sourceData;
const currentBoard = imageDTO.board_id ?? 'none';
const destinationBoard = targetData.boardId;
return currentBoard !== destinationBoard;
}
if (multipleImageDndSource.typeGuard(sourceData)) {
const currentBoard = sourceData.boardId;
const destinationBoard = targetData.boardId;
return currentBoard !== destinationBoard;
}
return false;
}
);
const _RemoveFromBoardDndTargetDataKey = Symbol('RemoveFromBoardDndTargetData');
export type RemoveFromBoardDndTargetData = DndData<typeof _RemoveFromBoardDndTargetDataKey>;
export const removeFromBoardDndTarget = buildDndTargetApi<RemoveFromBoardDndTargetData>(
_RemoveFromBoardDndTargetDataKey,
(sourceData) => {
if (singleImageDndSource.typeGuard(sourceData)) {
const currentBoard = sourceData.imageDTO.board_id ?? 'none';
return currentBoard !== 'none';
}
if (multipleImageDndSource.typeGuard(sourceData)) {
const currentBoard = sourceData.boardId;
return currentBoard !== 'none';
}
return false;
}
);
const targetApis = [
// Set a reference image on existing layer
setGlobalReferenceImageDndTarget,
setRegionalGuidanceReferenceImageDndTarget,
// Add layer from image
newRasterLayerFromImageDndTarget,
newControlLayerFromImageDndTarget,
// Add a layer w/ ref image preset
addGlobalReferenceImageFromImageDndTarget,
addRegionalGuidanceReferenceImageFromImageDndTarget,
// Replace layer content w/ image
replaceLayerWithImageDndTarget,
// Set the upscale image
setUpscaleInitialImageFromImageDndTarget,
// Set a field on a node
setNodeImageFieldDndTarget,
// Select images for comparison
selectForCompareDndTarget,
// Add an image to a board
addToBoardDndTarget,
// Remove an image from a board - essentially add to Uncategorized
removeFromBoardDndTarget,
// These are currently unused
addRegionalGuidanceFromImageDndTarget,
addInpaintMaskFromImageDndTarget,
] as const;
/**
* A union of all possible DndTargetData types.
*/
export type DndTargetData =
| SetGlobalReferenceImageDndTargetData
| SetRegionalGuidanceReferenceImageDndTargetData
| NewRasterLayerFromImageDndTargetData
| NewControlLayerFromImageDndTargetData
| AddInpaintMaskFromImageDndTargetData
| AddRegionalGuidanceFromImageDndTargetData
| AddRegionalGuidanceReferenceImageFromImageDndTargetData
| AddGlobalReferenceImageFromImageDndTargetData
| ReplaceLayerWithImageDndTargetData
| SetUpscaleInitialImageFromImageDndTargetData
| SetNodeImageFieldDndTargetData
| AddToBoardDndTargetData
| RemoveFromBoardDndTargetData
| SelectForCompareDndTargetData;
export const isDndTargetData = (data: BaseDndData): data is DndTargetData => {
for (const targetApi of targetApis) {
if (targetApi.typeGuard(data)) {
return true;
}
}
return false;
};
//#endregion
/**
* Validates whether a drop is valid.
* @param sourceData The data being dragged.
* @param targetData The data of the target being dragged onto.
* @returns Whether the drop is valid.
*/
export const isValidDrop = (sourceData: DndSourceData, targetData: DndTargetData): boolean => {
for (const targetApi of targetApis) {
if (targetApi.typeGuard(targetData)) {
/**
* TS cannot narrow the type of the targetApi and will error in the validator call.
* We've just checked that targetData is of the right type, though, so this cast to `any` is safe.
*/
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
return targetApi.validateDrop(sourceData, targetData as any);
}
}
return false;
};
export type DndState = 'idle' | 'potential' | 'over';

View File

@@ -2,9 +2,8 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, Image, Text, Tooltip } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { AddToBoardDndTargetData } from 'features/dnd2/types';
import { addToBoardDndTarget } from 'features/dnd2/types';
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
import { BoardEditableTitle } from 'features/gallery/components/Boards/BoardsList/BoardEditableTitle';
@@ -45,8 +44,8 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
}
}, [selectedBoardId, board.board_id, autoAssignBoardOnClick, autoAddBoardId, dispatch]);
const targetData: AddToBoardDndTargetData = useMemo(
() => addToBoardDndTarget.getData({ boardId: board.board_id }),
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['addToBoard']>(
() => Dnd.Target.addToBoard.getData({ boardId: board.board_id }),
[board.board_id]
);

View File

@@ -1,9 +1,8 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { RemoveFromBoardDndTargetData } from 'features/dnd2/types';
import { removeFromBoardDndTarget } from 'features/dnd2/types';
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
@@ -44,7 +43,10 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
}
}, [dispatch, autoAssignBoardOnClick]);
const targetData: RemoveFromBoardDndTargetData = useMemo(() => removeFromBoardDndTarget.getData({}), []);
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['removeFromBoard']>(
() => Dnd.Target.removeFromBoard.getData(),
[]
);
const { t } = useTranslation();

View File

@@ -7,7 +7,7 @@ import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/lis
import { useAppStore } from 'app/store/nanostores/store';
import { useAppSelector } from 'app/store/storeHooks';
import { useBoolean } from 'common/hooks/useBoolean';
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd2/types';
import { Dnd } from 'features/dnd2/dnd';
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
@@ -115,7 +115,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
// When we have multiple images selected, and the dragged image is part of the selection, initiate a
// multi-image drag.
if (gallery.selection.length > 1 && gallery.selection.includes(imageDTO)) {
return multipleImageDndSource.getData(
return Dnd.Source.multipleImage.getData(
{
imageDTOs: gallery.selection,
boardId: gallery.selectedBoardId,
@@ -125,13 +125,13 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
}
// Otherwise, initiate a single-image drag
return singleImageDndSource.getData({ imageDTO }, imageDTO.image_name);
return Dnd.Source.singleImage.getData({ imageDTO }, imageDTO.image_name);
},
// This is a "local" drag start event, meaning that it is only called when this specific image is dragged.
onDragStart: (args) => {
onDragStart: ({ source }) => {
// When we start dragging a single image, set the dragging state to true. This is only called when this
// specific image is dragged.
if (singleImageDndSource.typeGuard(args.source.data)) {
if (Dnd.Source.singleImage.typeGuard(source.data)) {
setIsDragging(true);
return;
}
@@ -139,10 +139,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
}),
monitorForElements({
// This is a "global" drag start event, meaning that it is called for all drag events.
onDragStart: (args) => {
onDragStart: ({ source }) => {
console.log(source);
// When we start dragging multiple images, set the dragging state to true if the dragged image is part of the
// selection. This is called for all drag events.
if (multipleImageDndSource.typeGuard(args.source.data) && args.source.data.imageDTOs.includes(imageDTO)) {
if (Dnd.Source.multipleImage.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) {
setIsDragging(true);
}
},

View File

@@ -1,7 +1,6 @@
import { useAppSelector } from 'app/store/storeHooks';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import type { SelectForCompareDndTargetData } from 'features/dnd2/types';
import { selectForCompareDndTarget } from 'features/dnd2/types';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,9 +9,9 @@ import { selectComparisonImages } from './common';
export const ImageComparisonDroppable = memo(() => {
const { t } = useTranslation();
const comparisonImages = useAppSelector(selectComparisonImages);
const targetData = useMemo<SelectForCompareDndTargetData>(() => {
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['selectForCompare']>(() => {
const { firstImage, secondImage } = comparisonImages;
return selectForCompareDndTarget.getData({
return Dnd.Target.selectForCompare.getData({
firstImageName: firstImage?.image_name,
secondImageName: secondImage?.image_name,
});

View File

@@ -3,10 +3,9 @@ import { useStore } from '@nanostores/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import type { SetNodeImageFieldDndTargetData } from 'features/dnd2/types';
import { setNodeImageFieldDndTarget } from 'features/dnd2/types';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import type { ImageFieldInputInstance, ImageFieldInputTemplate } from 'features/nodes/types/field';
import { memo, useCallback, useEffect, useMemo } from 'react';
@@ -34,8 +33,8 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
);
}, [dispatch, field.name, nodeId]);
const targetData = useMemo<SetNodeImageFieldDndTargetData>(
() => setNodeImageFieldDndTarget.getData({ nodeId, fieldName: field.name }, field.value?.image_name),
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setNodeImageField']>(
() => Dnd.Target.setNodeImageField.getData({ nodeId, fieldName: field.name }, field.value?.image_name),
[field.name, field.value?.image_name, nodeId]
);

View File

@@ -1,17 +1,15 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { Dnd } from 'features/dnd2/dnd';
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
import { DndImage } from 'features/dnd2/DndImage';
import { setUpscaleInitialImageFromImageDndTarget } from 'features/dnd2/types';
import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { t } from 'i18next';
import { useCallback, useMemo } from 'react';
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
import type { PostUploadAction } from 'services/api/types';
const targetData = setUpscaleInitialImageFromImageDndTarget.getData({});
export const UpscaleInitialImage = () => {
const dispatch = useAppDispatch();
const imageDTO = useAppSelector(selectUpscaleInitialImage);
@@ -21,6 +19,10 @@ export const UpscaleInitialImage = () => {
}),
[]
);
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setUpscaleInitialImageFromImage']>(
() => Dnd.Target.setUpscaleInitialImageFromImage.getData(),
[]
);
const onReset = useCallback(() => {
dispatch(upscaleInitialImageChanged(null));