feat(ui): migrate to pragmatic-drag-and-drop (wip 4)

This commit is contained in:
psychedelicious
2024-10-28 08:14:12 +10:00
parent cf67d084fd
commit 406fc58889
18 changed files with 244 additions and 108 deletions

View File

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

View File

@@ -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({});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

@@ -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')} />;
});

View File

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

View File

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

View File

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

View File

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