mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): simpler dnd typing implementation
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
414
invokeai/frontend/web/src/features/dnd2/dnd.ts
Normal file
414
invokeai/frontend/web/src/features/dnd2/dnd.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user