mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): migrate to pragmatic-drag-and-drop (wip 4)
This commit is contained in:
@@ -25,10 +25,10 @@ import type {
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import {
|
||||
addControlLayerFromImageDndTarget,
|
||||
newControlLayerFromImageDndTarget,
|
||||
addGlobalReferenceImageFromImageDndTarget,
|
||||
addInpaintMaskFromImageDndTarget,
|
||||
addRasterLayerFromImageDndTarget,
|
||||
newRasterLayerFromImageDndTarget,
|
||||
addRegionalGuidanceFromImageDndTarget,
|
||||
addRegionalGuidanceReferenceImageFromImageDndTarget,
|
||||
addToBoardDndTarget,
|
||||
@@ -100,8 +100,8 @@ export const addDndDroppedListener = (startAppListening: AppStartListening) => {
|
||||
|
||||
// Add raster layer from image
|
||||
if (
|
||||
addRasterLayerFromImageDndTarget.typeGuard(targetData) &&
|
||||
addRasterLayerFromImageDndTarget.validateDrop(sourceData, targetData)
|
||||
newRasterLayerFromImageDndTarget.typeGuard(targetData) &&
|
||||
newRasterLayerFromImageDndTarget.validateDrop(sourceData, targetData)
|
||||
) {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
|
||||
@@ -145,8 +145,8 @@ export const addDndDroppedListener = (startAppListening: AppStartListening) => {
|
||||
|
||||
// Add control layer from image
|
||||
if (
|
||||
addControlLayerFromImageDndTarget.typeGuard(targetData) &&
|
||||
addControlLayerFromImageDndTarget.validateDrop(sourceData, targetData)
|
||||
newControlLayerFromImageDndTarget.typeGuard(targetData) &&
|
||||
newControlLayerFromImageDndTarget.validateDrop(sourceData, targetData)
|
||||
) {
|
||||
const state = getState();
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
|
||||
import {
|
||||
addControlLayerFromImageDndTarget,
|
||||
newControlLayerFromImageDndTarget,
|
||||
addGlobalReferenceImageFromImageDndTarget,
|
||||
addRasterLayerFromImageDndTarget,
|
||||
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 = addRasterLayerFromImageDndTarget.getData({});
|
||||
const addControlLayerFromImageDndTargetData = addControlLayerFromImageDndTarget.getData({});
|
||||
const addRasterLayerFromImageDndTargetData = newRasterLayerFromImageDndTarget.getData({});
|
||||
const addControlLayerFromImageDndTargetData = newControlLayerFromImageDndTarget.getData({});
|
||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData =
|
||||
addRegionalGuidanceReferenceImageFromImageDndTarget.getData({});
|
||||
const addGlobalReferenceImageFromImageDndTargetData = addGlobalReferenceImageFromImageDndTarget.getData({});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
|
||||
import { replaceLayerWithImageDndTarget, type ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
|
||||
import { memo, useId, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
@@ -25,10 +25,9 @@ export const ControlLayer = memo(({ id }: Props) => {
|
||||
() => ({ id, type: 'control_layer' }),
|
||||
[id]
|
||||
);
|
||||
const dndId = useId();
|
||||
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
|
||||
() => replaceLayerWithImageDndTarget.getData({ dndId, entityIdentifier }),
|
||||
[dndId, entityIdentifier]
|
||||
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,22 +38,22 @@ type Props = {
|
||||
export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData, postUploadAction }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useStore($isConnected);
|
||||
const imageDTOQueryResult = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && imageDTOQueryResult.isError) {
|
||||
if (isConnected && isError) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, imageDTOQueryResult.isError, isConnected]);
|
||||
}, [handleResetControlImage, isError, isConnected]);
|
||||
|
||||
return (
|
||||
<Flex sx={sx} data-error={!imageDTOQueryResult.currentData && !image?.image_name}>
|
||||
{imageDTOQueryResult.currentData && (
|
||||
<Flex sx={sx} data-error={!imageDTO && !image?.image_name}>
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage dndId={targetData.dndId} imageDTO={imageDTOQueryResult.currentData} />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
|
||||
@@ -20,7 +20,7 @@ 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 { memo, useCallback, useId, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||
import type { ImageDTO, IPAdapterModelConfig, IPALayerImagePostUploadAction } from 'services/api/types';
|
||||
@@ -80,15 +80,17 @@ export const IPAdapterSettings = memo(() => {
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const dndId = useId();
|
||||
|
||||
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
|
||||
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
|
||||
[entityIdentifier.id]
|
||||
);
|
||||
const targetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ dndId, globalReferenceImageId: entityIdentifier.id }),
|
||||
[dndId, entityIdentifier.id]
|
||||
() =>
|
||||
setGlobalReferenceImageDndTarget.getData(
|
||||
{ globalReferenceImageId: entityIdentifier.id },
|
||||
ipAdapter.image?.image_name
|
||||
),
|
||||
[entityIdentifier.id, ipAdapter.image?.image_name]
|
||||
);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'
|
||||
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
|
||||
import type { ReplaceLayerWithImageDndTargetData } from 'features/dnd2/types';
|
||||
import { replaceLayerWithImageDndTarget } from 'features/dnd2/types';
|
||||
import { memo, useId, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
@@ -19,11 +19,10 @@ type Props = {
|
||||
|
||||
export const RasterLayer = memo(({ id }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dndId = useId();
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
|
||||
const targetData = useMemo<ReplaceLayerWithImageDndTargetData>(
|
||||
() => replaceLayerWithImageDndTarget.getData({ dndId, entityIdentifier }),
|
||||
[dndId, entityIdentifier]
|
||||
() => replaceLayerWithImageDndTarget.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,7 @@ import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'feature
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd2/types';
|
||||
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd2/types';
|
||||
import { memo, useCallback, useId, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
|
||||
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
|
||||
@@ -35,7 +35,6 @@ type Props = {
|
||||
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const { t } = useTranslation();
|
||||
const dndId = useId();
|
||||
const dispatch = useAppDispatch();
|
||||
const onDeleteIPAdapter = useCallback(() => {
|
||||
dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
|
||||
@@ -95,12 +94,14 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
|
||||
|
||||
const targetData = useMemo<SetRegionalGuidanceReferenceImageDndTargetData>(
|
||||
() =>
|
||||
setRegionalGuidanceReferenceImageDndTarget.getData({
|
||||
dndId,
|
||||
regionalGuidanceId: entityIdentifier.id,
|
||||
referenceImageId,
|
||||
}),
|
||||
[dndId, entityIdentifier.id, referenceImageId]
|
||||
setRegionalGuidanceReferenceImageDndTarget.getData(
|
||||
{
|
||||
regionalGuidanceId: entityIdentifier.id,
|
||||
referenceImageId,
|
||||
},
|
||||
ipAdapter.image?.image_name
|
||||
),
|
||||
[entityIdentifier.id, ipAdapter.image?.image_name, referenceImageId]
|
||||
);
|
||||
|
||||
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { DndDropOverlay } from 'features/dnd2/DndDropOverlay';
|
||||
import type { DndState, DndTargetData } from 'features/dnd2/types';
|
||||
import { isDndSourceData, isValidDrop, singleImageDndSource } 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';
|
||||
@@ -92,7 +92,7 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
if (!isDndSourceData(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
if (targetData.dndId === sourceData.dndId) {
|
||||
if (getDndId(targetData) === getDndId(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return isValidDrop(sourceData, targetData);
|
||||
@@ -118,7 +118,7 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
if (!isDndSourceData(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
if (targetData.dndId === sourceData.dndId) {
|
||||
if (getDndId(targetData) === getDndId(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return isValidDrop(sourceData, targetData);
|
||||
@@ -177,7 +177,7 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
});
|
||||
dispatch(
|
||||
dndDropped({
|
||||
sourceData: singleImageDndSource.getData({ dndId: getPrefixedId('random-dnd-id'), imageDTO }),
|
||||
sourceData: singleImageDndSource.getData({ imageDTO }, getPrefixedId('dnd-upload-image')),
|
||||
targetData,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -20,10 +20,9 @@ const sx = {
|
||||
|
||||
type Props = ImageProps & {
|
||||
imageDTO: ImageDTO;
|
||||
dndId: string;
|
||||
};
|
||||
|
||||
export const DndImage = memo(({ imageDTO, dndId, ...rest }: Props) => {
|
||||
export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
|
||||
const store = useAppStore();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [element, ref] = useState<HTMLImageElement | null>(null);
|
||||
@@ -34,9 +33,7 @@ export const DndImage = memo(({ imageDTO, dndId, ...rest }: Props) => {
|
||||
}
|
||||
return draggable({
|
||||
element,
|
||||
getInitialData: () => {
|
||||
return singleImageDndSource.getData({ dndId, imageDTO });
|
||||
},
|
||||
getInitialData: () => singleImageDndSource.getData({ imageDTO }, imageDTO.image_name),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
@@ -44,7 +41,7 @@ export const DndImage = memo(({ imageDTO, dndId, ...rest }: Props) => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
});
|
||||
}, [imageDTO, element, store, dndId]);
|
||||
}, [imageDTO, element, store]);
|
||||
|
||||
useImageContextMenu(imageDTO, element);
|
||||
|
||||
|
||||
@@ -1,41 +1,118 @@
|
||||
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';
|
||||
|
||||
type AnyRecord = Record<string | symbol, unknown>;
|
||||
export type BaseDndData = { dndId: string } & Record<string | symbol, unknown>;
|
||||
/**
|
||||
* 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 AnyRecord>(key: symbol) =>
|
||||
(data: AnyRecord): data is T => {
|
||||
<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 AnyRecord>(key: symbol) =>
|
||||
(data: Omit<T, typeof key>): T => {
|
||||
<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;
|
||||
};
|
||||
const buildDndSourceApi = <T extends AnyRecord>(key: symbol) =>
|
||||
({ key, typeGuard: _buildDataTypeGuard<T>(key), getData: _buildDataGetter<T>(key) }) as const;
|
||||
|
||||
type WithDndId<T> = T & { dndId: string };
|
||||
/**
|
||||
* 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');
|
||||
export type SingleImageDndSourceDataPrev = {
|
||||
[_SingleImageDndSourceDataKey]: true;
|
||||
imageDTO: ImageDTO;
|
||||
};
|
||||
/**
|
||||
* 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,
|
||||
{
|
||||
@@ -43,14 +120,21 @@ export type MultipleImageDndSourceData = DndData<
|
||||
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.
|
||||
*/
|
||||
const sourceApis = [singleImageDndSource, multipleImageDndSource] as const;
|
||||
export type DndSourceData = SingleImageDndSourceData | MultipleImageDndSourceData;
|
||||
export const isDndSourceData = (data: AnyRecord): data is DndSourceData => {
|
||||
/**
|
||||
* 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;
|
||||
@@ -62,24 +146,56 @@ export const isDndSourceData = (data: AnyRecord): data is DndSourceData => {
|
||||
//#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: (sourceData: DndSourceData, targetData: T) => boolean
|
||||
) => ({ key, typeGuard: _buildDataTypeGuard<T>(key), getData: _buildDataGetter<T>(key), validateDrop }) as const;
|
||||
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,
|
||||
{
|
||||
@@ -87,23 +203,38 @@ export type SetRegionalGuidanceReferenceImageDndTargetData = DndData<
|
||||
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 _AddRasterLayerFromImageDndTargetDataKey = Symbol('AddRasterLayerFromImageDndTargetData');
|
||||
export type AddRasterLayerFromImageDndTargetData = DndData<typeof _AddRasterLayerFromImageDndTargetDataKey>;
|
||||
export const addRasterLayerFromImageDndTarget = buildDndTargetApi<AddRasterLayerFromImageDndTargetData>(
|
||||
_AddRasterLayerFromImageDndTargetDataKey,
|
||||
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 _AddControlLayerFromImageDndTargetDataKey = Symbol('AddControlLayerFromImageDndTargetData');
|
||||
export type AddControlLayerFromImageDndTargetData = DndData<typeof _AddControlLayerFromImageDndTargetDataKey>;
|
||||
export const addControlLayerFromImageDndTarget = buildDndTargetApi<AddControlLayerFromImageDndTargetData>(
|
||||
_AddControlLayerFromImageDndTargetDataKey,
|
||||
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
|
||||
);
|
||||
|
||||
@@ -187,7 +318,19 @@ export type SelectForCompareDndTargetData = DndData<
|
||||
>;
|
||||
export const selectForCompareDndTarget = buildDndTargetApi<SelectForCompareDndTargetData>(
|
||||
_SelectForCompareDndTargetDataKey,
|
||||
singleImageDndSource.typeGuard
|
||||
(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');
|
||||
@@ -248,8 +391,8 @@ const targetApis = [
|
||||
setGlobalReferenceImageDndTarget,
|
||||
setRegionalGuidanceReferenceImageDndTarget,
|
||||
// Add layer from image
|
||||
addRasterLayerFromImageDndTarget,
|
||||
addControlLayerFromImageDndTarget,
|
||||
newRasterLayerFromImageDndTarget,
|
||||
newControlLayerFromImageDndTarget,
|
||||
// Add a layer w/ ref image preset
|
||||
addGlobalReferenceImageFromImageDndTarget,
|
||||
addRegionalGuidanceReferenceImageFromImageDndTarget,
|
||||
@@ -276,8 +419,8 @@ const targetApis = [
|
||||
export type DndTargetData =
|
||||
| SetGlobalReferenceImageDndTargetData
|
||||
| SetRegionalGuidanceReferenceImageDndTargetData
|
||||
| AddRasterLayerFromImageDndTargetData
|
||||
| AddControlLayerFromImageDndTargetData
|
||||
| NewRasterLayerFromImageDndTargetData
|
||||
| NewControlLayerFromImageDndTargetData
|
||||
| AddInpaintMaskFromImageDndTargetData
|
||||
| AddRegionalGuidanceFromImageDndTargetData
|
||||
| AddRegionalGuidanceReferenceImageFromImageDndTargetData
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useId, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
@@ -36,7 +36,6 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const dndId = useId();
|
||||
const onClick = useCallback(() => {
|
||||
if (selectedBoardId !== board.board_id) {
|
||||
dispatch(boardIdSelected({ boardId: board.board_id }));
|
||||
@@ -47,8 +46,8 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
}, [selectedBoardId, board.board_id, autoAssignBoardOnClick, autoAddBoardId, dispatch]);
|
||||
|
||||
const targetData: AddToBoardDndTargetData = useMemo(
|
||||
() => addToBoardDndTarget.getData({ dndId, boardId: board.board_id }),
|
||||
[board.board_id, dndId]
|
||||
() => addToBoardDndTarget.getData({ boardId: board.board_id }),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { multipleImageDndSource, singleImageDndSource } from 'features/dnd2/types';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { GalleryImageHoverIcons } from 'features/gallery/components/ImageGrid/GalleryImageHoverIcons';
|
||||
@@ -116,15 +115,17 @@ 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({
|
||||
dndId: getPrefixedId('dnd-gallery-selection'),
|
||||
imageDTOs: gallery.selection,
|
||||
boardId: gallery.selectedBoardId,
|
||||
});
|
||||
return multipleImageDndSource.getData(
|
||||
{
|
||||
imageDTOs: gallery.selection,
|
||||
boardId: gallery.selectedBoardId,
|
||||
},
|
||||
'gallery-selection'
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, initiate a single-image drag
|
||||
return singleImageDndSource.getData({ dndId, imageDTO });
|
||||
return singleImageDndSource.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) => {
|
||||
|
||||
@@ -96,7 +96,7 @@ const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
|
||||
<DndImage dndId="current-image" imageDTO={imageDTO} />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { DndDropTarget } from 'features/dnd2/DndDropTarget';
|
||||
import type { SelectForCompareDndTargetData } from 'features/dnd2/types';
|
||||
import { selectForCompareDndTarget } from 'features/dnd2/types';
|
||||
@@ -9,15 +9,14 @@ import { selectComparisonImages } from './common';
|
||||
|
||||
export const ImageComparisonDroppable = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const comparisonImages = useAppSelector(selectComparisonImages);
|
||||
const targetData = useMemo<SelectForCompareDndTargetData>(() => {
|
||||
const { firstImage, secondImage } = selectComparisonImages(store.getState());
|
||||
const { firstImage, secondImage } = comparisonImages;
|
||||
return selectForCompareDndTarget.getData({
|
||||
dndId: 'current-image',
|
||||
firstImageName: firstImage?.image_name,
|
||||
secondImageName: secondImage?.image_name,
|
||||
});
|
||||
}, [store]);
|
||||
}, [comparisonImages]);
|
||||
|
||||
return <DndDropTarget targetData={targetData} label={t('gallery.selectForCompare')} />;
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { CSSProperties, PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useId, useState } from 'react';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
import { $lastProgressEvent } from 'services/events/stores';
|
||||
@@ -18,7 +18,6 @@ import { $lastProgressEvent } from 'services/events/stores';
|
||||
const CurrentImageNode = (props: NodeProps) => {
|
||||
const imageDTO = useAppSelector(selectLastSelectedImage);
|
||||
const lastProgressEvent = useStore($lastProgressEvent);
|
||||
const dndId = useId();
|
||||
|
||||
if (lastProgressEvent?.image) {
|
||||
return (
|
||||
@@ -31,7 +30,7 @@ const CurrentImageNode = (props: NodeProps) => {
|
||||
if (imageDTO) {
|
||||
return (
|
||||
<Wrapper nodeProps={props}>
|
||||
<DndImage dndId={dndId} imageDTO={imageDTO} />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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, useId, useMemo } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
@@ -24,7 +24,6 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useStore($isConnected);
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(field.value?.image_name ?? skipToken);
|
||||
const dndId = useId();
|
||||
const handleReset = useCallback(() => {
|
||||
dispatch(
|
||||
fieldImageValueChanged({
|
||||
@@ -36,8 +35,8 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
}, [dispatch, field.name, nodeId]);
|
||||
|
||||
const targetData = useMemo<SetNodeImageFieldDndTargetData>(
|
||||
() => setNodeImageFieldDndTarget.getData({ dndId, nodeId, fieldName: field.name }),
|
||||
[dndId, field.name, nodeId]
|
||||
() => setNodeImageFieldDndTarget.getData({ nodeId, fieldName: field.name }, field.value?.image_name),
|
||||
[field.name, field.value?.image_name, nodeId]
|
||||
);
|
||||
|
||||
const postUploadAction = useMemo<PostUploadAction>(
|
||||
@@ -70,7 +69,7 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
>
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage dndId={dndId} imageDTO={imageDTO} minW={8} minH={8} />
|
||||
<DndImage imageDTO={imageDTO} minW={8} minH={8} />
|
||||
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleReset}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DndImage } from 'features/dnd2/DndImage';
|
||||
import { memo, useId } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageOutput } from 'services/api/types';
|
||||
|
||||
@@ -10,12 +10,11 @@ type Props = {
|
||||
const ImageOutputPreview = ({ output }: Props) => {
|
||||
const { image } = output;
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(image.image_name);
|
||||
const dndId = useId();
|
||||
if (!imageDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DndImage dndId={dndId} imageDTO={imageDTO} />;
|
||||
return <DndImage imageDTO={imageDTO} />;
|
||||
};
|
||||
|
||||
export default memo(ImageOutputPreview);
|
||||
|
||||
@@ -6,7 +6,7 @@ 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, useId, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import type { PostUploadAction } from 'services/api/types';
|
||||
|
||||
@@ -15,7 +15,6 @@ const targetData = setUpscaleInitialImageFromImageDndTarget.getData({});
|
||||
export const UpscaleInitialImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useAppSelector(selectUpscaleInitialImage);
|
||||
const dndId = useId();
|
||||
const postUploadAction = useMemo<PostUploadAction>(
|
||||
() => ({
|
||||
type: 'SET_UPSCALE_INITIAL_IMAGE',
|
||||
@@ -32,7 +31,7 @@ export const UpscaleInitialImage = () => {
|
||||
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage dndId={dndId} imageDTO={imageDTO} />
|
||||
<DndImage imageDTO={imageDTO} />
|
||||
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={onReset}
|
||||
|
||||
Reference in New Issue
Block a user