mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): dnd actions to image actions
We don't need a "dnd" image system. We need a "image action" system. We need to execute specific flows with images from various "origins": - internal dnd e.g. from gallery - external dnd e.g. user drags an image file into the browser - direct file upload e.g. user clicks an upload button - some other internal app button e.g. a context menu The actions are now generalized to better support these various use-cases.
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import { Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { newCanvasEntityFromImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const addRasterLayerFromImageDndTargetData = Dnd.Target.newRasterLayerFromImage.getData();
|
||||
const addControlLayerFromImageDndTargetData = Dnd.Target.newControlLayerFromImage.getData();
|
||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData =
|
||||
Dnd.Target.newRegionalGuidanceReferenceImageFromImage.getData();
|
||||
const addGlobalReferenceImageFromImageDndTargetData = Dnd.Target.newGlobalReferenceImageFromImage.getData();
|
||||
const addRasterLayerFromImageDndTargetData = newCanvasEntityFromImageActionApi.getData({ type: 'raster_layer' });
|
||||
const addControlLayerFromImageDndTargetData = newCanvasEntityFromImageActionApi.getData({ type: 'control_layer' });
|
||||
const addRegionalGuidanceReferenceImageFromImageDndTargetData = newCanvasEntityFromImageActionApi.getData({
|
||||
type: 'regional_guidance_with_reference_image',
|
||||
});
|
||||
const addGlobalReferenceImageFromImageDndTargetData = newCanvasEntityFromImageActionApi.getData({ type: 'reference_image' });
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTi
|
||||
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { isRenderableEntityType } from 'features/controlLayers/store/types';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/util';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
@@ -2,18 +2,26 @@ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import { buildDndSourceApi, idle } from 'features/dnd/dnd';
|
||||
import { type DndListTargetState, idle } from 'features/dnd/types';
|
||||
import type { ActionData, ActionSourceApi } from 'features/imageActions/actions';
|
||||
import { buildGetData, buildTypeAndKey, buildTypeGuard } from 'features/imageActions/actions';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Dnd source API for a single canvas entity.
|
||||
*/
|
||||
export const singleCanvasEntity = buildDndSourceApi<{ entityIdentifier: CanvasEntityIdentifier }>('SingleCanvasEntity');
|
||||
const _singleCanvasEntity = buildTypeAndKey('single-canvas-entity');
|
||||
type SingleCanvasEntitySourceData = ActionData<
|
||||
typeof _singleCanvasEntity.type,
|
||||
typeof _singleCanvasEntity.key,
|
||||
{ entityIdentifier: CanvasEntityIdentifier }
|
||||
>;
|
||||
export const singleCanvasEntity: ActionSourceApi<SingleCanvasEntitySourceData> = {
|
||||
..._singleCanvasEntity,
|
||||
typeGuard: buildTypeGuard(_singleCanvasEntity.key),
|
||||
getData: buildGetData(_singleCanvasEntity.key, _singleCanvasEntity.type),
|
||||
};
|
||||
|
||||
export const useCanvasEntityListDnd = (ref: RefObject<HTMLElement>, entityIdentifier: CanvasEntityIdentifier) => {
|
||||
const [dndListState, setDndListState] = useState<DndListState>(idle);
|
||||
const [dndListState, setDndListState] = useState<DndListTargetState>(idle);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -109,7 +109,9 @@ export const CanvasMainPanelContent = memo(() => {
|
||||
<SelectObject />
|
||||
</CanvasManagerProviderGate>
|
||||
</Flex>
|
||||
<CanvasDropArea />
|
||||
<CanvasManagerProviderGate>
|
||||
<CanvasDropArea />
|
||||
</CanvasManagerProviderGate>
|
||||
<GatedImageViewer />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -6,10 +6,11 @@ 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 { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { multipleImageSourceApi, singleImageSourceApi } from 'features/imageActions/actions';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectActiveTabCanvasRightPanel } from 'features/ui/store/uiSelectors';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
@@ -84,8 +85,8 @@ const PanelTabs = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const activeEntityCount = useAppSelector(selectEntityCountActive);
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<Dnd.types['DndState']>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<Dnd.types['DndState']>('idle');
|
||||
const [layersTabDndState, setLayersTabDndState] = useState<DndTargetState>('idle');
|
||||
const [galleryTabDndState, setGalleryTabDndState] = useState<DndTargetState>('idle');
|
||||
const layersTabRef = useRef<HTMLDivElement>(null);
|
||||
const galleryTabRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
@@ -208,7 +209,7 @@ const PanelTabs = memo(() => {
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: ({ source }) => {
|
||||
if (!Dnd.Source.singleImage.typeGuard(source.data) || !Dnd.Source.multipleImage.typeGuard(source.data)) {
|
||||
if (!singleImageSourceApi.typeGuard(source.data) || !multipleImageSourceApi.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
// Only monitor if we are not already on the gallery tab
|
||||
|
||||
@@ -11,8 +11,9 @@ import { ControlLayerAdapterGate } from 'features/controlLayers/contexts/EntityA
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import type { ReplaceCanvasEntityObjectsWithImageActionData} from 'features/imageActions/actions';
|
||||
import {replaceCanvasEntityObjectsWithImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -27,8 +28,8 @@ export const ControlLayer = memo(({ id }: Props) => {
|
||||
() => ({ id, type: 'control_layer' }),
|
||||
[id]
|
||||
);
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['replaceLayerWithImage']>(
|
||||
() => Dnd.Target.replaceLayerWithImage.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
const targetData = useMemo<ReplaceCanvasEntityObjectsWithImageActionData>(
|
||||
() => replaceCanvasEntityObjectsWithImageActionApi.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import type { SetGlobalReferenceImageActionData, SetRegionalGuidanceReferenceImageActionData } from 'features/imageActions/actions';
|
||||
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: Dnd.types['TargetDataUnion'];
|
||||
targetData: SetGlobalReferenceImageActionData | SetRegionalGuidanceReferenceImageActionData;
|
||||
};
|
||||
|
||||
export const IPAdapterImagePreview = memo(({ image, onChangeImage, targetData }: Props) => {
|
||||
|
||||
@@ -19,7 +19,8 @@ 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 { Dnd } from 'features/dnd/dnd';
|
||||
import type { SetGlobalReferenceImageActionData} from 'features/imageActions/actions';
|
||||
import {setGlobalReferenceImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold } from 'react-icons/pi';
|
||||
@@ -80,8 +81,8 @@ export const IPAdapterSettings = memo(() => {
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setGlobalReferenceImage']>(
|
||||
() => Dnd.Target.setGlobalReferenceImage.getData({ entityIdentifier }, ipAdapter.image?.image_name),
|
||||
const targetData = useMemo<SetGlobalReferenceImageActionData>(
|
||||
() => setGlobalReferenceImageActionApi.getData({ entityIdentifier }, ipAdapter.image?.image_name),
|
||||
[entityIdentifier, ipAdapter.image?.image_name]
|
||||
);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
||||
|
||||
@@ -8,8 +8,9 @@ import { RasterLayerAdapterGate } from 'features/controlLayers/contexts/EntityAd
|
||||
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import type { ReplaceCanvasEntityObjectsWithImageActionData} from 'features/imageActions/actions';
|
||||
import {replaceCanvasEntityObjectsWithImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -21,8 +22,8 @@ export const RasterLayer = memo(({ id }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier<'raster_layer'>>(() => ({ id, type: 'raster_layer' }), [id]);
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['replaceLayerWithImage']>(
|
||||
() => Dnd.Target.replaceLayerWithImage.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
const targetData = useMemo<ReplaceCanvasEntityObjectsWithImageActionData>(
|
||||
() => replaceCanvasEntityObjectsWithImageActionApi.getData({ entityIdentifier }, entityIdentifier.id),
|
||||
[entityIdentifier]
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import type { SetRegionalGuidanceReferenceImageActionData} from 'features/imageActions/actions';
|
||||
import {setRegionalGuidanceReferenceImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
|
||||
@@ -91,12 +92,9 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Pro
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setRegionalGuidanceReferenceImage']>(
|
||||
const targetData = useMemo<SetRegionalGuidanceReferenceImageActionData>(
|
||||
() =>
|
||||
Dnd.Target.setRegionalGuidanceReferenceImage.getData(
|
||||
{ entityIdentifier, referenceImageId },
|
||||
ipAdapter.image?.image_name
|
||||
),
|
||||
setRegionalGuidanceReferenceImageActionApi.getData({ entityIdentifier, referenceImageId }, ipAdapter.image?.image_name),
|
||||
[entityIdentifier, ipAdapter.image?.image_name, referenceImageId]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import { Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import type { Dnd } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/util';
|
||||
import type { MultipleImageSourceData } from 'features/imageActions/actions';
|
||||
import { memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -39,7 +39,7 @@ export const createMultipleImageDragPreview = (arg: DndDragPreviewMultipleImageS
|
||||
createPortal(<DndDragPreviewMultipleImage imageDTOs={arg.imageDTOs} />, arg.container);
|
||||
|
||||
type SetMultipleDragPreviewArg = {
|
||||
multipleImageDndData: Dnd.types['SourceDataTypeMap']['multipleImage'];
|
||||
multipleImageDndData: MultipleImageSourceData;
|
||||
setDragPreviewState: (dragPreviewState: DndDragPreviewMultipleImageState | null) => void;
|
||||
onGenerateDragPreviewArgs: Param0<Param0<typeof draggable>['onGenerateDragPreview']>;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import { chakra, Flex } from '@invoke-ai/ui-library';
|
||||
import type { Dnd } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/dnd';
|
||||
import { DND_IMAGE_DRAG_PREVIEW_SIZE, preserveOffsetOnSourceFallbackCentered } from 'features/dnd/util';
|
||||
import type { SingleImageSourceData } from 'features/imageActions/actions';
|
||||
import { memo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -37,7 +37,7 @@ export const createSingleImageDragPreview = (arg: DndDragPreviewSingleImageState
|
||||
createPortal(<DndDragPreviewSingleImage imageDTO={arg.imageDTO} />, arg.container);
|
||||
|
||||
type SetSingleDragPreviewArg = {
|
||||
singleImageDndData: Dnd.types['SourceDataTypeMap']['singleImage'];
|
||||
singleImageDndData: SingleImageSourceData;
|
||||
setDragPreviewState: (dragPreviewState: DndDragPreviewSingleImageState | null) => void;
|
||||
onGenerateDragPreviewArgs: Param0<Param0<typeof draggable>['onGenerateDragPreview']>;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import type { Dnd } from 'features/dnd/dnd';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
type Props = {
|
||||
dndState: Dnd.types['DndState'];
|
||||
dndState: DndTargetState;
|
||||
label?: string;
|
||||
withBackdrop?: boolean;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,17 @@ import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/exter
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
|
||||
import type { DndTargetState } from 'features/dnd/types';
|
||||
import type { MultipleImageAction, RecordUnknown, SingleImageAction } from 'features/imageActions/actions';
|
||||
import {
|
||||
multipleImageActions,
|
||||
multipleImageSourceApi,
|
||||
singleImageActions,
|
||||
singleImageSourceApi,
|
||||
} from 'features/imageActions/actions';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { uploadImage } from 'services/api/endpoints/images';
|
||||
import { z } from 'zod';
|
||||
@@ -58,7 +66,7 @@ const zUploadFile = z
|
||||
);
|
||||
|
||||
type Props = {
|
||||
targetData: Dnd.types['TargetDataUnion'];
|
||||
targetData: SingleImageAction | MultipleImageAction;
|
||||
label: string;
|
||||
externalLabel?: string;
|
||||
isDisabled?: boolean;
|
||||
@@ -66,31 +74,59 @@ type Props = {
|
||||
|
||||
export const DndDropTarget = memo((props: Props) => {
|
||||
const { targetData, label, externalLabel = label, isDisabled } = props;
|
||||
const [dndState, setDndState] = useState<Dnd.types['DndState']>('idle');
|
||||
const [dndState, setDndState] = useState<DndTargetState>('idle');
|
||||
const [dndOrigin, setDndOrigin] = useState<'element' | 'external' | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dispatch, getState } = getStore();
|
||||
|
||||
const isValidDrop = (sourceData: RecordUnknown, targetData: RecordUnknown) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
for (const target of singleImageActions) {
|
||||
if (target.typeGuard(targetData)) {
|
||||
// TS cannot infer `targetData` but we've just checked it. This is safe.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
if (target.isValid(sourceData, targetData as any, dispatch, getState)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
for (const target of multipleImageActions) {
|
||||
if (target.typeGuard(targetData)) {
|
||||
// TS cannot infer `targetData` but we've just checked it. This is safe.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
if (target.isValid(sourceData, targetData as any, dispatch, getState)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return combine(
|
||||
dropTargetForElements({
|
||||
element: ref.current,
|
||||
canDrop: (args) => {
|
||||
const sourceData = args.source.data;
|
||||
if (!Dnd.Util.isDndSourceData(sourceData)) {
|
||||
element,
|
||||
canDrop: ({ source }) => {
|
||||
const sourceData = source.data;
|
||||
if (sourceData.id === targetData.id) {
|
||||
return false;
|
||||
}
|
||||
if (Dnd.Util.getDndId(targetData) === Dnd.Util.getDndId(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return Dnd.Util.isValidDrop(sourceData, targetData);
|
||||
return isValidDrop(sourceData, targetData);
|
||||
},
|
||||
onDragEnter: () => {
|
||||
setDndState('over');
|
||||
@@ -101,15 +137,12 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
getData: () => targetData,
|
||||
}),
|
||||
monitorForElements({
|
||||
canMonitor: (args) => {
|
||||
const sourceData = args.source.data;
|
||||
if (!Dnd.Util.isDndSourceData(sourceData)) {
|
||||
canMonitor: ({ source }) => {
|
||||
const sourceData = source.data;
|
||||
if (sourceData.id === targetData.id) {
|
||||
return false;
|
||||
}
|
||||
if (Dnd.Util.getDndId(targetData) === Dnd.Util.getDndId(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return Dnd.Util.isValidDrop(sourceData, targetData);
|
||||
return isValidDrop(sourceData, targetData);
|
||||
},
|
||||
onDragStart: () => {
|
||||
setDndOrigin('element');
|
||||
@@ -124,17 +157,17 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
}, [targetData, dispatch, isDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return combine(
|
||||
dropTargetForExternal({
|
||||
element: ref.current,
|
||||
element,
|
||||
canDrop: (args) => {
|
||||
if (!containsFiles(args)) {
|
||||
return false;
|
||||
@@ -162,7 +195,7 @@ export const DndDropTarget = memo((props: Props) => {
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
});
|
||||
Dnd.Util.handleDrop(Dnd.Source.singleImage.getData({ imageDTO }), targetData);
|
||||
// Dnd.Util.handleDrop(Dnd.Source.singleImage.getData({ imageDTO }), targetData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -2,10 +2,10 @@ 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 { Dnd } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { createSingleImageDragPreview, setSingleImageDragPreview } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
import { useImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||
import { singleImageSourceApi } from 'features/imageActions/actions';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@@ -36,7 +36,7 @@ export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
|
||||
}
|
||||
return draggable({
|
||||
element,
|
||||
getInitialData: () => Dnd.Source.singleImage.getData({ imageDTO }, imageDTO.image_name),
|
||||
getInitialData: () => singleImageSourceApi.getData({ imageDTO }, imageDTO.image_name),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const DndImage = memo(({ imageDTO, ...rest }: Props) => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onGenerateDragPreview: (args) => {
|
||||
if (Dnd.Source.singleImage.typeGuard(args.source.data)) {
|
||||
if (singleImageSourceApi.typeGuard(args.source.data)) {
|
||||
setSingleImageDragPreview({
|
||||
singleImageDndData: args.source.data,
|
||||
onGenerateDragPreviewArgs: args,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box } from '@invoke-ai/ui-library';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import type { DndListTargetState } from 'features/dnd/types';
|
||||
|
||||
/**
|
||||
* Design decisions for the drop indicator's main line
|
||||
@@ -104,7 +104,7 @@ function DndDropIndicatorInternal({ edge, gap = '0px' }: DropIndicatorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export const DndListDropIndicator = ({ dndState }: { dndState: DndListState }) => {
|
||||
export const DndListDropIndicator = ({ dndState }: { dndState: DndListTargetState }) => {
|
||||
if (dndState.type !== 'is-dragging-over') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,795 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */ // We will use namespaces to organize the Dnd types
|
||||
|
||||
import type { Input } from '@atlaskit/pragmatic-drag-and-drop/dist/types/entry-point/types';
|
||||
import type { GetOffsetFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/element/custom-native-drag-preview/types';
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/closest-edge';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
inpaintMaskAdded,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { ValueOf } from 'type-fest';
|
||||
import type { Jsonifiable } from 'type-fest/source/jsonifiable';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type for unknown Dnd data. `pragmatic-drag-and-drop` types all data as this type.
|
||||
*/
|
||||
type UnknownDndData = Record<string | symbol, unknown>;
|
||||
|
||||
/**
|
||||
* A Dnd kind, which can be either a source or a target.
|
||||
*/
|
||||
type DndKind = 'source' | 'target';
|
||||
|
||||
/**
|
||||
* Data for a given Dnd source or target, which contains metadata and payload.
|
||||
* @template T The type string of the Dnd data. This should be unique for each type of Dnd data.
|
||||
* @template K The kind of the Dnd data ('source' or 'target').
|
||||
* @template P The optional payload of the Dnd data. This can be any "Jsonifiable" data - that is, data that can be
|
||||
* serialized to JSON. This ensures the data can be safely stored in Redux, logged, etc.
|
||||
*/
|
||||
type DndData<
|
||||
T extends string = string,
|
||||
K extends DndKind = DndKind,
|
||||
P extends Jsonifiable | undefined = Jsonifiable | undefined,
|
||||
> = {
|
||||
/**
|
||||
* Metadata about the DndData.
|
||||
*/
|
||||
meta: {
|
||||
/**
|
||||
* An identifier for this data. This may or may not be unique. This is primarily used to prevent a source from
|
||||
* dropping on itself.
|
||||
*
|
||||
* A consumer may be both a Dnd source and target of the same type. For example, the upscaling initial image is
|
||||
* a Dnd target and may contain an image, which is itself a Dnd source. In this case, the Dnd ID is used to prevent
|
||||
* the upscaling initial image (and other instances of that same image) from being dropped onto itself.
|
||||
*
|
||||
* This is accomplished by checking the Dnd ID of the source against the Dnd ID of the target. If they match, the
|
||||
* drop is rejected.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The type of the DndData.
|
||||
*/
|
||||
type: T;
|
||||
/**
|
||||
* The kind of the DndData (source or target).
|
||||
*/
|
||||
kind: K;
|
||||
};
|
||||
/**
|
||||
* The arbitrarily-shaped payload of the DndData.
|
||||
*/
|
||||
payload: P;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a type guard for a specific DndData type.
|
||||
* @template T The Dnd data type.
|
||||
* @param type The type of the Dnd source or target data.
|
||||
* @param kind The kind of the Dnd source or target data.
|
||||
* @returns A type guard for the Dnd data.
|
||||
*/
|
||||
const _buildDataTypeGuard = <T extends DndData>(type: T['meta']['type'], kind: T['meta']['kind']) => {
|
||||
// pragmatic-drag-and-drop types all data as unknown, so we need to cast it to the expected type
|
||||
return (data: UnknownDndData): data is T => {
|
||||
try {
|
||||
return (data as DndData).meta.type === type && (data as DndData).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.
|
||||
*
|
||||
* @template T The Dnd data type.
|
||||
* @param type The type of the Dnd source or target data.
|
||||
* @param kind The kind of the Dnd source or target data.
|
||||
* @returns A getter for the DndData type.
|
||||
*/
|
||||
const _buildDataGetter =
|
||||
<T extends DndData>(type: T['meta']['type'], kind: T['meta']['kind']) =>
|
||||
(payload: T['payload'] extends undefined ? void : T['payload'], dndId?: string | null): T => {
|
||||
return {
|
||||
meta: {
|
||||
id: dndId ?? getPrefixedId(`dnd-${kind}-${type}`),
|
||||
type,
|
||||
kind,
|
||||
},
|
||||
payload,
|
||||
} as T;
|
||||
};
|
||||
|
||||
/**
|
||||
* The API for a Dnd source.
|
||||
*/
|
||||
type DndSourceAPI<T extends DndData> = {
|
||||
/**
|
||||
* The type of the Dnd source.
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* The kind of the Dnd source. It is always 'source'.
|
||||
*/
|
||||
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>>;
|
||||
/**
|
||||
* Gets a typed DndData object for the parent type.
|
||||
* @param payload The payload for this DndData.
|
||||
* @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 Dnd source API.
|
||||
* @template P The optional payload of the Dnd source.
|
||||
* @param type The type of the Dnd source.
|
||||
*/
|
||||
export const buildDndSourceApi = <P extends Jsonifiable | undefined = undefined>(type: string) => {
|
||||
return {
|
||||
type,
|
||||
kind: 'source',
|
||||
typeGuard: _buildDataTypeGuard<DndData<typeof type, 'source', P>>(type, 'source'),
|
||||
getData: _buildDataGetter<DndData<typeof type, 'source', P>>(type, 'source'),
|
||||
} satisfies DndSourceAPI<DndData<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
|
||||
/**
|
||||
* The API for a Dnd target.
|
||||
*/
|
||||
type DndTargetApi<T extends DndData> = 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: DndData<string, 'source', Jsonifiable>, targetData: T) => boolean;
|
||||
handleDrop: (sourceData: DndData<string, 'source', Jsonifiable>, targetData: T) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a Dnd target API.
|
||||
* @template P The optional payload of the Dnd target.
|
||||
* @param type The type of the Dnd target.
|
||||
* @param validateDrop A function that validates whether a drop is valid.
|
||||
*/
|
||||
const buildDndTargetApi = <P extends Jsonifiable | undefined = undefined>(
|
||||
type: string,
|
||||
validateDrop: (
|
||||
sourceData: DndData<string, 'source', Jsonifiable>,
|
||||
targetData: DndData<typeof type, 'target', P>
|
||||
) => boolean,
|
||||
handleDrop: (
|
||||
sourceData: DndData<string, 'source', Jsonifiable>,
|
||||
targetData: DndData<typeof type, 'target', P>
|
||||
) => void
|
||||
) => {
|
||||
return {
|
||||
type,
|
||||
kind: 'source',
|
||||
typeGuard: _buildDataTypeGuard<DndData<typeof type, 'target', P>>(type, 'target'),
|
||||
getData: _buildDataGetter<DndData<typeof type, 'target', P>>(type, 'target'),
|
||||
validateDrop,
|
||||
handleDrop,
|
||||
} satisfies DndTargetApi<DndData<typeof type, 'target', P>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the image on an existing Global Reference Image layer.
|
||||
*/
|
||||
const setGlobalReferenceImage = buildDndTargetApi<{ entityIdentifier: CanvasEntityIdentifier<'reference_image'> }>(
|
||||
'SetGlobalReferenceImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the image on an existing Regional Guidance layer's Reference Image.
|
||||
*/
|
||||
const setRegionalGuidanceReferenceImage = buildDndTargetApi<{
|
||||
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>;
|
||||
referenceImageId: string;
|
||||
}>('SetRegionalGuidanceReferenceImage', singleImage.typeGuard, (sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier, referenceImageId } = targetData.payload;
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||
});
|
||||
|
||||
/**
|
||||
* Dnd target API for creating a new a Raster Layer from an image.
|
||||
*/
|
||||
const newRasterLayerFromImage = buildDndTargetApi(
|
||||
'NewRasterLayerFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for creating a new a Control Layer from an image.
|
||||
*/
|
||||
const newControlLayerFromImage = buildDndTargetApi(
|
||||
'NewControlLayerFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { dispatch, getState } = getStore();
|
||||
const state = getState();
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(state);
|
||||
const controlAdapter = selectDefaultControlAdapter(state);
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
controlAdapter,
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding an Inpaint Mask from an image.
|
||||
*/
|
||||
const newInpaintMaskFromImage = buildDndTargetApi(
|
||||
'NewInpaintMaskFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasInpaintMaskState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const overrides: Partial<CanvasReferenceImageState> = { ipAdapter };
|
||||
dispatch(referenceImageAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for adding a new Regional Guidance layer from an image.
|
||||
*/
|
||||
const newRegionalGuidanceFromImage = buildDndTargetApi(
|
||||
'NewRegionalGuidanceFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
(sourceData, _targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }],
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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, (sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch, getState } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
dispatch(
|
||||
entityRasterized({
|
||||
entityIdentifier,
|
||||
imageObject,
|
||||
position: { x, y },
|
||||
replaceObjects: true,
|
||||
isSelected: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Dnd target API for setting the initial image on the upscaling tab.
|
||||
*/
|
||||
const setUpscaleInitialImageFromImage = buildDndTargetApi(
|
||||
'SetUpscaleInitialImageFromImage',
|
||||
singleImage.typeGuard,
|
||||
(sourceData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Dnd target API for setting an image field on a node.
|
||||
*/
|
||||
const setNodeImageField = buildDndTargetApi<{ nodeId: string; fieldName: string }>(
|
||||
'SetNodeImageField',
|
||||
singleImage.typeGuard,
|
||||
(sourceData, targetData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { fieldName, nodeId } = targetData.payload;
|
||||
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
(sourceData) => {
|
||||
if (!singleImage.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
(sourceData, targetData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImageToBoard.initiate({ imageDTO, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImagesToBoard.initiate({ imageDTOs, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
(sourceData) => {
|
||||
if (singleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImageFromBoard.initiate({ imageDTO }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImage.typeGuard(sourceData)) {
|
||||
const { dispatch } = getStore();
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ imageDTOs }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* The Dnd namespace, providing types and APIs for Dnd functionality.
|
||||
*/
|
||||
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 Dnd kind, which can be either a source or a target.
|
||||
*/
|
||||
DndKind: DndKind;
|
||||
/**
|
||||
* A type for unknown Dnd data. `pragmatic-drag-and-drop` types all data as this type.
|
||||
*/
|
||||
UnknownDndData: UnknownDndData;
|
||||
/**
|
||||
* 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: DndData): string => {
|
||||
return data.meta.id;
|
||||
},
|
||||
/**
|
||||
* Checks if the data is a Dnd source data object.
|
||||
* @param data The data to check.
|
||||
*/
|
||||
isDndSourceData: (data: UnknownDndData): data is SourceDataUnion => {
|
||||
try {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
return (data as DndData).meta.kind === 'source';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Checks if the data is a Dnd target data object.
|
||||
* @param data The data to check.
|
||||
*/
|
||||
isDndTargetData: (data: UnknownDndData): data is TargetDataUnion => {
|
||||
try {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
return (data as DndData).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;
|
||||
},
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
handleDrop: (sourceData: SourceDataUnion, targetData: TargetDataUnion): void => {
|
||||
for (const targetApi of targetApisArray) {
|
||||
if (targetApi.typeGuard(targetData)) {
|
||||
/**
|
||||
* TS cannot narrow the type of the targetApi and will error in the handleDrop 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 */
|
||||
targetApi.handleDrop(sourceData, targetData as any);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The size of the image drag preview in theme units.
|
||||
*/
|
||||
export const DND_IMAGE_DRAG_PREVIEW_SIZE = 32 satisfies SystemStyleObject['w'];
|
||||
|
||||
/**
|
||||
* A drag preview offset function that works like the provided `preserveOffsetOnSource`, except when either the X or Y
|
||||
* offset is outside the container, in which case it centers the preview in the container.
|
||||
*/
|
||||
export function preserveOffsetOnSourceFallbackCentered({
|
||||
element,
|
||||
input,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
input: Input;
|
||||
}): GetOffsetFn {
|
||||
return ({ container }) => {
|
||||
const sourceRect = element.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
let offsetX = input.clientX - sourceRect.x;
|
||||
let offsetY = input.clientY - sourceRect.y;
|
||||
|
||||
if (offsetY > containerRect.height || offsetX > containerRect.width) {
|
||||
offsetX = containerRect.width / 2;
|
||||
offsetY = containerRect.height / 2;
|
||||
}
|
||||
|
||||
return { x: offsetX, y: offsetY };
|
||||
};
|
||||
}
|
||||
|
||||
// Based on https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/flourish/src/trigger-post-move-flash.tsx
|
||||
// That package has a lot of extra deps so we just copied the function here
|
||||
export function triggerPostMoveFlash(element: HTMLElement, backgroundColor: CSSProperties['backgroundColor']) {
|
||||
element.animate([{ backgroundColor }, {}], {
|
||||
duration: 700,
|
||||
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
|
||||
iterations: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export type DndListState =
|
||||
| {
|
||||
type: 'idle';
|
||||
}
|
||||
| {
|
||||
type: 'preview';
|
||||
container: HTMLElement;
|
||||
}
|
||||
| {
|
||||
type: 'is-dragging';
|
||||
}
|
||||
| {
|
||||
type: 'is-dragging-over';
|
||||
closestEdge: Edge | null;
|
||||
};
|
||||
export const idle: DndListState = { type: 'idle' };
|
||||
30
invokeai/frontend/web/src/features/dnd/types.ts
Normal file
30
invokeai/frontend/web/src/features/dnd/types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
|
||||
/**
|
||||
* States for a dnd target.
|
||||
* - `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.
|
||||
*/
|
||||
export type DndTargetState = 'idle' | 'potential' | 'over';
|
||||
|
||||
/**
|
||||
* States for a dnd list.
|
||||
*/
|
||||
export type DndListTargetState =
|
||||
| {
|
||||
type: 'idle';
|
||||
}
|
||||
| {
|
||||
type: 'preview';
|
||||
container: HTMLElement;
|
||||
}
|
||||
| {
|
||||
type: 'is-dragging';
|
||||
}
|
||||
| {
|
||||
type: 'is-dragging-over';
|
||||
closestEdge: Edge | null;
|
||||
};
|
||||
export const idle: DndListTargetState = { type: 'idle' };
|
||||
@@ -4,8 +4,10 @@ import { monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/a
|
||||
import { containsFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
|
||||
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { multipleImageSourceApi, multipleImageActions, singleImageSourceApi, singleImageActions } from 'features/imageActions/actions';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const log = logger('dnd');
|
||||
@@ -20,7 +22,7 @@ export const useDndMonitor = () => {
|
||||
const sourceData = source.data;
|
||||
|
||||
// Check for allowed sources
|
||||
if (!Dnd.Source.singleImage.typeGuard(sourceData) && !Dnd.Source.multipleImage.typeGuard(sourceData)) {
|
||||
if (!singleImageSourceApi.typeGuard(sourceData) && !multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,19 +37,40 @@ export const useDndMonitor = () => {
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
const { dispatch, getState } = getStore();
|
||||
|
||||
// Check for allowed sources
|
||||
if (!Dnd.Source.singleImage.typeGuard(sourceData) && !Dnd.Source.multipleImage.typeGuard(sourceData)) {
|
||||
return;
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
for (const target of singleImageActions) {
|
||||
if (target.typeGuard(targetData)) {
|
||||
// TS cannot infer `targetData` but we've just checked it. This is safe.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
if (target.isValid(sourceData, targetData as any, dispatch, getState)) {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
target.handler(sourceData, targetData as any, dispatch, getState);
|
||||
log.debug(parseify({ sourceData, targetData }), 'Dropped single image');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for allowed targets
|
||||
if (!Dnd.Util.isDndTargetData(targetData)) {
|
||||
return;
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
for (const target of multipleImageActions) {
|
||||
if (target.typeGuard(targetData)) {
|
||||
// TS cannot infer `targetData` but we've just checked it. This is safe.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
if (target.isValid(sourceData, targetData as any, dispatch, getState)) {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
target.handler(sourceData, targetData as any, dispatch, getState);
|
||||
log.debug(parseify({ sourceData, targetData }), 'Dropped multiple images');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug({ sourceData, targetData }, 'Dropped image');
|
||||
|
||||
Dnd.Util.handleDrop(sourceData, targetData);
|
||||
log.warn(parseify({ sourceData, targetData }), 'Invalid image drop');
|
||||
},
|
||||
}),
|
||||
monitorForExternal({
|
||||
|
||||
46
invokeai/frontend/web/src/features/dnd/util.ts
Normal file
46
invokeai/frontend/web/src/features/dnd/util.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { GetOffsetFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/element/custom-native-drag-preview/types';
|
||||
import type { Input } from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
/**
|
||||
* The size of the image drag preview in theme units.
|
||||
*/
|
||||
export const DND_IMAGE_DRAG_PREVIEW_SIZE = 32 satisfies SystemStyleObject['w'];
|
||||
|
||||
/**
|
||||
* A drag preview offset function that works like the provided `preserveOffsetOnSource`, except when either the X or Y
|
||||
* offset is outside the container, in which case it centers the preview in the container.
|
||||
*/
|
||||
export function preserveOffsetOnSourceFallbackCentered({
|
||||
element,
|
||||
input,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
input: Input;
|
||||
}): GetOffsetFn {
|
||||
return ({ container }) => {
|
||||
const sourceRect = element.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
let offsetX = input.clientX - sourceRect.x;
|
||||
let offsetY = input.clientY - sourceRect.y;
|
||||
|
||||
if (offsetY > containerRect.height || offsetX > containerRect.width) {
|
||||
offsetX = containerRect.width / 2;
|
||||
offsetY = containerRect.height / 2;
|
||||
}
|
||||
|
||||
return { x: offsetX, y: offsetY };
|
||||
};
|
||||
}
|
||||
|
||||
// Based on https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/flourish/src/trigger-post-move-flash.tsx
|
||||
// That package has a lot of extra deps so we just copied the function here
|
||||
export function triggerPostMoveFlash(element: HTMLElement, backgroundColor: CSSProperties['backgroundColor']) {
|
||||
element.animate([{ backgroundColor }, {}], {
|
||||
duration: 700,
|
||||
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
|
||||
iterations: 1,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
||||
@@ -14,6 +13,8 @@ import {
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import type { AddImageToBoardActionData} from 'features/imageActions/actions';
|
||||
import {addImageToBoardActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArchiveBold, PiImageSquare } from 'react-icons/pi';
|
||||
@@ -44,10 +45,7 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
|
||||
}
|
||||
}, [selectedBoardId, board.board_id, autoAssignBoardOnClick, autoAddBoardId, dispatch]);
|
||||
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['addToBoard']>(
|
||||
() => Dnd.Target.addToBoard.getData({ boardId: board.board_id }),
|
||||
[board.board_id]
|
||||
);
|
||||
const targetData = useMemo<AddImageToBoardActionData>(() => addImageToBoardActionApi.getData({ boardId: board.board_id }), [board.board_id]);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h={12}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
selectBoardSearchText,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import type { RemoveImageFromBoardActionData} from 'features/imageActions/actions';
|
||||
import {removeImageFromBoardActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
|
||||
@@ -43,10 +44,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
}
|
||||
}, [dispatch, autoAssignBoardOnClick]);
|
||||
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['removeFromBoard']>(
|
||||
() => Dnd.Target.removeFromBoard.getData(),
|
||||
[]
|
||||
);
|
||||
const targetData = useMemo<RemoveImageFromBoardActionData>(() => removeImageFromBoardActionApi.getData(), []);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -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 { Dnd } from 'features/dnd/dnd';
|
||||
import type { DndDragPreviewMultipleImageState } from 'features/dnd/DndDragPreviewMultipleImage';
|
||||
import { createMultipleImageDragPreview, setMultipleImageDragPreview } from 'features/dnd/DndDragPreviewMultipleImage';
|
||||
import type { DndDragPreviewSingleImageState } from 'features/dnd/DndDragPreviewSingleImage';
|
||||
@@ -18,6 +17,7 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
|
||||
import { SizedSkeletonLoader } from 'features/gallery/components/ImageGrid/SizedSkeletonLoader';
|
||||
import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { multipleImageSourceApi, singleImageSourceApi } from 'features/imageActions/actions';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -122,32 +122,32 @@ 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 Dnd.Source.multipleImage.getData({
|
||||
return multipleImageSourceApi.getData({
|
||||
imageDTOs: gallery.selection,
|
||||
boardId: gallery.selectedBoardId,
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, initiate a single-image drag
|
||||
return Dnd.Source.singleImage.getData({ imageDTO }, imageDTO.image_name);
|
||||
return singleImageSourceApi.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: ({ 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 (Dnd.Source.singleImage.typeGuard(source.data)) {
|
||||
if (singleImageSourceApi.typeGuard(source.data)) {
|
||||
setIsDragging(true);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onGenerateDragPreview: (args) => {
|
||||
if (Dnd.Source.multipleImage.typeGuard(args.source.data)) {
|
||||
if (multipleImageSourceApi.typeGuard(args.source.data)) {
|
||||
setMultipleImageDragPreview({
|
||||
multipleImageDndData: args.source.data,
|
||||
onGenerateDragPreviewArgs: args,
|
||||
setDragPreviewState,
|
||||
});
|
||||
} else if (Dnd.Source.singleImage.typeGuard(args.source.data)) {
|
||||
} else if (singleImageSourceApi.typeGuard(args.source.data)) {
|
||||
setSingleImageDragPreview({
|
||||
singleImageDndData: args.source.data,
|
||||
onGenerateDragPreviewArgs: args,
|
||||
@@ -161,7 +161,7 @@ export const GalleryImage = memo(({ imageDTO }: Props) => {
|
||||
onDragStart: ({ 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 (Dnd.Source.multipleImage.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) {
|
||||
if (multipleImageSourceApi.typeGuard(source.data) && source.data.payload.imageDTOs.includes(imageDTO)) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { type SetComparisonImageActionData, setComparisonImageActionApi } from 'features/imageActions/actions';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { selectComparisonImages } from './common';
|
||||
|
||||
export const ImageComparisonDroppable = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const comparisonImages = useAppSelector(selectComparisonImages);
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['selectForCompare']>(() => {
|
||||
const { firstImage, secondImage } = comparisonImages;
|
||||
return Dnd.Target.selectForCompare.getData({
|
||||
firstImageName: firstImage?.image_name,
|
||||
secondImageName: secondImage?.image_name,
|
||||
});
|
||||
}, [comparisonImages]);
|
||||
const targetData = useMemo<SetComparisonImageActionData>(() => setComparisonImageActionApi.getData(), []);
|
||||
|
||||
return <DndDropTarget targetData={targetData} label={t('gallery.selectForCompare')} />;
|
||||
});
|
||||
|
||||
458
invokeai/frontend/web/src/features/imageActions/actions.ts
Normal file
458
invokeai/frontend/web/src/features/imageActions/actions.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
inpaintMaskAdded,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectBboxRect } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
CanvasEntityType,
|
||||
CanvasRenderableEntityIdentifier,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
export type RecordUnknown = Record<string | symbol, unknown>;
|
||||
|
||||
export type ActionData<
|
||||
Type extends string = string,
|
||||
PrivateKey extends symbol = symbol,
|
||||
Payload extends JsonObject | void = JsonObject | void,
|
||||
> = {
|
||||
[key in PrivateKey]: true;
|
||||
} & {
|
||||
id: string;
|
||||
type: Type;
|
||||
payload: Payload;
|
||||
};
|
||||
|
||||
export const buildTypeAndKey = <T extends string>(type: T) => {
|
||||
const key = Symbol(type);
|
||||
return { type, key } as const;
|
||||
};
|
||||
|
||||
export const buildTypeGuard = <T extends ActionData>(key: symbol) => {
|
||||
const typeGuard = (val: RecordUnknown): val is T => Boolean(val[key]);
|
||||
return typeGuard;
|
||||
};
|
||||
|
||||
export const buildGetData = <T extends ActionData>(key: symbol, type: T['type']) => {
|
||||
const getData = (payload: T['payload'] extends undefined ? void : T['payload'], id?: string): T =>
|
||||
({
|
||||
[key]: true,
|
||||
id: id ?? getPrefixedId(type),
|
||||
type,
|
||||
payload,
|
||||
}) as T;
|
||||
return getData;
|
||||
};
|
||||
|
||||
export type ActionSourceApi<SourceData extends ActionData> = {
|
||||
key: symbol;
|
||||
type: SourceData['type'];
|
||||
typeGuard: ReturnType<typeof buildTypeGuard<SourceData>>;
|
||||
getData: ReturnType<typeof buildGetData<SourceData>>;
|
||||
};
|
||||
//#region Single Image
|
||||
const _singleImage = buildTypeAndKey('single-image');
|
||||
export type SingleImageSourceData = ActionData<
|
||||
typeof _singleImage.type,
|
||||
typeof _singleImage.key,
|
||||
{ imageDTO: ImageDTO }
|
||||
>;
|
||||
export const singleImageSourceApi: ActionSourceApi<SingleImageSourceData> = {
|
||||
..._singleImage,
|
||||
typeGuard: buildTypeGuard(_singleImage.key),
|
||||
getData: buildGetData(_singleImage.key, _singleImage.type),
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Multiple Image
|
||||
const _multipleImage = buildTypeAndKey('multiple-image');
|
||||
export type MultipleImageSourceData = ActionData<
|
||||
typeof _multipleImage.type,
|
||||
typeof _multipleImage.key,
|
||||
{ imageDTOs: ImageDTO[]; boardId: BoardId }
|
||||
>;
|
||||
export const multipleImageSourceApi: ActionSourceApi<MultipleImageSourceData> = {
|
||||
..._multipleImage,
|
||||
typeGuard: buildTypeGuard(_multipleImage.key),
|
||||
getData: buildGetData(_multipleImage.key, _multipleImage.type),
|
||||
};
|
||||
//#endregion
|
||||
|
||||
type ActionTargetApi<TargetData extends ActionData, SourceData extends ActionData> = {
|
||||
key: symbol;
|
||||
type: TargetData['type'];
|
||||
typeGuard: ReturnType<typeof buildTypeGuard<TargetData>>;
|
||||
getData: ReturnType<typeof buildGetData<TargetData>>;
|
||||
isValid: (
|
||||
sourceData: RecordUnknown,
|
||||
targetData: TargetData,
|
||||
dispatch: AppDispatch,
|
||||
getState: () => RootState
|
||||
) => boolean;
|
||||
handler: (sourceData: SourceData, targetData: TargetData, dispatch: AppDispatch, getState: () => RootState) => void;
|
||||
};
|
||||
|
||||
//#region Set Global Reference Image
|
||||
const _setGlobalReferenceImage = buildTypeAndKey('set-global-reference-image');
|
||||
export type SetGlobalReferenceImageActionData = ActionData<
|
||||
typeof _setGlobalReferenceImage.type,
|
||||
typeof _setGlobalReferenceImage.key,
|
||||
{ entityIdentifier: CanvasEntityIdentifier<'reference_image'> }
|
||||
>;
|
||||
export const setGlobalReferenceImageActionApi: ActionTargetApi<
|
||||
SetGlobalReferenceImageActionData,
|
||||
SingleImageSourceData
|
||||
> = {
|
||||
..._setGlobalReferenceImage,
|
||||
typeGuard: buildTypeGuard(_setGlobalReferenceImage.key),
|
||||
getData: buildGetData(_setGlobalReferenceImage.key, _setGlobalReferenceImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, _getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Set Regional Guidance Reference Image
|
||||
const _setRegionalGuidanceReferenceImage = buildTypeAndKey('set-regional-guidance-reference-image');
|
||||
export type SetRegionalGuidanceReferenceImageActionData = ActionData<
|
||||
typeof _setRegionalGuidanceReferenceImage.type,
|
||||
typeof _setRegionalGuidanceReferenceImage.key,
|
||||
{ entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>; referenceImageId: string }
|
||||
>;
|
||||
export const setRegionalGuidanceReferenceImageActionApi: ActionTargetApi<
|
||||
SetRegionalGuidanceReferenceImageActionData,
|
||||
SingleImageSourceData
|
||||
> = {
|
||||
..._setRegionalGuidanceReferenceImage,
|
||||
typeGuard: buildTypeGuard(_setRegionalGuidanceReferenceImage.key),
|
||||
getData: buildGetData(_setRegionalGuidanceReferenceImage.key, _setRegionalGuidanceReferenceImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, _getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier, referenceImageId } = targetData.payload;
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//# Set Upscale Initial Image
|
||||
const _setUpscaleInitialImage = buildTypeAndKey('set-upscale-initial-image');
|
||||
export type SetUpscaleInitialImageActionData = ActionData<
|
||||
typeof _setUpscaleInitialImage.type,
|
||||
typeof _setUpscaleInitialImage.key,
|
||||
void
|
||||
>;
|
||||
export const setUpscaleInitialImageActionApi: ActionTargetApi<SetUpscaleInitialImageActionData, SingleImageSourceData> =
|
||||
{
|
||||
..._setUpscaleInitialImage,
|
||||
typeGuard: buildTypeGuard(_setUpscaleInitialImage.key),
|
||||
getData: buildGetData(_setUpscaleInitialImage.key, _setUpscaleInitialImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, _targetData, dispatch, _getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Set Node Image Field Image
|
||||
const _setNodeImageFieldImage = buildTypeAndKey('set-node-image-field-image');
|
||||
export type SetNodeImageFieldImageActionData = ActionData<
|
||||
typeof _setNodeImageFieldImage.type,
|
||||
typeof _setNodeImageFieldImage.key,
|
||||
{ fieldIdentifer: FieldIdentifier }
|
||||
>;
|
||||
export const setNodeImageFieldImageActionApi: ActionTargetApi<SetNodeImageFieldImageActionData, SingleImageSourceData> =
|
||||
{
|
||||
..._setNodeImageFieldImage,
|
||||
typeGuard: buildTypeGuard(_setNodeImageFieldImage.key),
|
||||
getData: buildGetData(_setNodeImageFieldImage.key, _setNodeImageFieldImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, _getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { fieldIdentifer } = targetData.payload;
|
||||
dispatch(fieldImageValueChanged({ ...fieldIdentifer, value: imageDTO }));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//# Set Comparison Image
|
||||
const _setComparisonImage = buildTypeAndKey('set-comparison-image');
|
||||
export type SetComparisonImageActionData = ActionData<
|
||||
typeof _setComparisonImage.type,
|
||||
typeof _setComparisonImage.key,
|
||||
void
|
||||
>;
|
||||
export const setComparisonImageActionApi: ActionTargetApi<SetComparisonImageActionData, SingleImageSourceData> = {
|
||||
..._setComparisonImage,
|
||||
typeGuard: buildTypeGuard(_setComparisonImage.key),
|
||||
getData: buildGetData(_setComparisonImage.key, _setComparisonImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, getState) => {
|
||||
if (!singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
const { firstImage, secondImage } = selectComparisonImages(getState());
|
||||
// Do not allow the same images to be selected for comparison
|
||||
if (sourceData.payload.imageDTO.image_name === firstImage?.image_name) {
|
||||
return false;
|
||||
}
|
||||
if (sourceData.payload.imageDTO.image_name === secondImage?.image_name) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: (sourceData, _targetData, dispatch, _getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imageToCompareChanged(imageDTO));
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region New Canvas Entity
|
||||
const _newCanvasEntity = buildTypeAndKey('new-canvas-entity');
|
||||
export type NewCanvasEntityFromImageActionData = ActionData<
|
||||
typeof _newCanvasEntity.type,
|
||||
typeof _newCanvasEntity.key,
|
||||
{ type: CanvasEntityType | 'regional_guidance_with_reference_image' }
|
||||
>;
|
||||
export const newCanvasEntityFromImageActionApi: ActionTargetApi<
|
||||
NewCanvasEntityFromImageActionData,
|
||||
SingleImageSourceData
|
||||
> = {
|
||||
..._newCanvasEntity,
|
||||
typeGuard: buildTypeGuard(_newCanvasEntity.key),
|
||||
getData: buildGetData(_newCanvasEntity.key, _newCanvasEntity.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (!singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, getState) => {
|
||||
const { type } = targetData.payload;
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const state = getState();
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(state);
|
||||
const overrides = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
switch (type) {
|
||||
case 'raster_layer': {
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
break;
|
||||
}
|
||||
case 'control_layer': {
|
||||
const controlAdapter = selectDefaultControlAdapter(state);
|
||||
dispatch(controlLayerAdded({ overrides: { ...overrides, controlAdapter }, isSelected: true }));
|
||||
break;
|
||||
}
|
||||
case 'inpaint_mask': {
|
||||
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance': {
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
break;
|
||||
}
|
||||
case 'reference_image': {
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
dispatch(referenceImageAdded({ overrides: { ipAdapter }, isSelected: true }));
|
||||
break;
|
||||
}
|
||||
case 'regional_guidance_with_reference_image': {
|
||||
const ipAdapter = selectDefaultIPAdapter(getState());
|
||||
ipAdapter.image = imageDTOToImageWithDims(imageDTO);
|
||||
const referenceImages = [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }];
|
||||
dispatch(rgAdded({ overrides: { referenceImages }, isSelected: true }));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Replace Canvas Entity Objects With Image
|
||||
const _replaceCanvasEntityObjectsWithImage = buildTypeAndKey('replace-canvas-entity-objects-with-image');
|
||||
export type ReplaceCanvasEntityObjectsWithImageActionData = ActionData<
|
||||
typeof _replaceCanvasEntityObjectsWithImage.type,
|
||||
typeof _replaceCanvasEntityObjectsWithImage.key,
|
||||
{ entityIdentifier: CanvasRenderableEntityIdentifier }
|
||||
>;
|
||||
export const replaceCanvasEntityObjectsWithImageActionApi: ActionTargetApi<
|
||||
ReplaceCanvasEntityObjectsWithImageActionData,
|
||||
SingleImageSourceData
|
||||
> = {
|
||||
..._replaceCanvasEntityObjectsWithImage,
|
||||
typeGuard: buildTypeGuard(_replaceCanvasEntityObjectsWithImage.key),
|
||||
getData: buildGetData(_replaceCanvasEntityObjectsWithImage.key, _replaceCanvasEntityObjectsWithImage.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (!singleImageSourceApi.typeGuard(sourceData)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, getState) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { entityIdentifier } = targetData.payload;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const { x, y } = selectBboxRect(getState());
|
||||
dispatch(
|
||||
entityRasterized({
|
||||
entityIdentifier,
|
||||
imageObject,
|
||||
position: { x, y },
|
||||
replaceObjects: true,
|
||||
isSelected: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Add To Board
|
||||
const _addToBoard = buildTypeAndKey('add-to-board');
|
||||
export type AddImageToBoardActionData = ActionData<
|
||||
typeof _addToBoard.type,
|
||||
typeof _addToBoard.key,
|
||||
{ boardId: BoardId }
|
||||
>;
|
||||
export const addImageToBoardActionApi: ActionTargetApi<
|
||||
AddImageToBoardActionData,
|
||||
SingleImageSourceData | MultipleImageSourceData
|
||||
> = {
|
||||
..._addToBoard,
|
||||
typeGuard: buildTypeGuard(_addToBoard.key),
|
||||
getData: buildGetData(_addToBoard.key, _addToBoard.type),
|
||||
isValid: (sourceData, targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
const destinationBoard = targetData.payload.boardId;
|
||||
return currentBoard !== destinationBoard;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, targetData, dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImageToBoard.initiate({ imageDTO, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
const { boardId } = targetData.payload;
|
||||
dispatch(imagesApi.endpoints.addImagesToBoard.initiate({ imageDTOs, board_id: boardId }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Remove From Board
|
||||
const _removeFromBoard = buildTypeAndKey('add-to-board');
|
||||
export type RemoveImageFromBoardActionData = ActionData<
|
||||
typeof _removeFromBoard.type,
|
||||
typeof _removeFromBoard.key,
|
||||
void
|
||||
>;
|
||||
export const removeImageFromBoardActionApi: ActionTargetApi<
|
||||
RemoveImageFromBoardActionData,
|
||||
SingleImageSourceData | MultipleImageSourceData
|
||||
> = {
|
||||
..._removeFromBoard,
|
||||
typeGuard: buildTypeGuard(_removeFromBoard.key),
|
||||
getData: buildGetData(_removeFromBoard.key, _removeFromBoard.type),
|
||||
isValid: (sourceData, _targetData, _dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.imageDTO.board_id ?? 'none';
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
const currentBoard = sourceData.payload.boardId;
|
||||
return currentBoard !== 'none';
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
handler: (sourceData, _targetData, dispatch, _getState) => {
|
||||
if (singleImageSourceApi.typeGuard(sourceData)) {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImageFromBoard.initiate({ imageDTO }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
|
||||
if (multipleImageSourceApi.typeGuard(sourceData)) {
|
||||
const { imageDTOs } = sourceData.payload;
|
||||
dispatch(imagesApi.endpoints.removeImagesFromBoard.initiate({ imageDTOs }, { track: false }));
|
||||
dispatch(selectionChanged([]));
|
||||
}
|
||||
},
|
||||
};
|
||||
//#endregion
|
||||
|
||||
export const singleImageActions = [
|
||||
setGlobalReferenceImageActionApi,
|
||||
setRegionalGuidanceReferenceImageActionApi,
|
||||
setUpscaleInitialImageActionApi,
|
||||
setNodeImageFieldImageActionApi,
|
||||
setComparisonImageActionApi,
|
||||
newCanvasEntityFromImageActionApi,
|
||||
replaceCanvasEntityObjectsWithImageActionApi,
|
||||
addImageToBoardActionApi,
|
||||
removeImageFromBoardActionApi,
|
||||
] as const;
|
||||
export type SingleImageAction = ReturnType<(typeof singleImageActions)[number]['getData']>;
|
||||
|
||||
export const multipleImageActions = [addImageToBoardActionApi, removeImageFromBoardActionApi] as const;
|
||||
export type MultipleImageAction = ReturnType<(typeof multipleImageActions)[number]['getData']>;
|
||||
@@ -2,10 +2,11 @@ import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import type { SetNodeImageFieldImageActionData } from 'features/imageActions/actions';
|
||||
import { setNodeImageFieldImageActionApi } from 'features/imageActions/actions';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ImageFieldInputInstance, ImageFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
@@ -32,9 +33,10 @@ const ImageFieldInputComponent = (props: FieldComponentProps<ImageFieldInputInst
|
||||
);
|
||||
}, [dispatch, field.name, nodeId]);
|
||||
|
||||
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]
|
||||
const targetData = useMemo<SetNodeImageFieldImageActionData>(
|
||||
() =>
|
||||
setNodeImageFieldImageActionApi.getData({ fieldIdentifer: { nodeId, fieldName: field.name } }, field.value?.image_name),
|
||||
[field, nodeId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/dnd';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/util';
|
||||
import LinearViewFieldInternal from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
|
||||
import { singleWorkflowField } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import type { DndListState } from 'features/dnd/dnd';
|
||||
import { buildDndSourceApi, idle } from 'features/dnd/dnd';
|
||||
import type { DndListTargetState } from 'features/dnd/types';
|
||||
import { idle } from 'features/dnd/types';
|
||||
import type { ActionData, ActionSourceApi } from 'features/imageActions/actions';
|
||||
import { buildGetData, buildTypeAndKey, buildTypeGuard } from 'features/imageActions/actions';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Dnd source API for a single workflow field.
|
||||
*/
|
||||
export const singleWorkflowField = buildDndSourceApi<{ fieldIdentifier: FieldIdentifier }>('SingleWorkflowField');
|
||||
const _singleWorkflowField = buildTypeAndKey('single-workflow-field');
|
||||
type SingleWorkflowFieldSourceData = ActionData<
|
||||
typeof _singleWorkflowField.type,
|
||||
typeof _singleWorkflowField.key,
|
||||
{ fieldIdentifier: FieldIdentifier }
|
||||
>;
|
||||
export const singleWorkflowField: ActionSourceApi<SingleWorkflowFieldSourceData> = {
|
||||
..._singleWorkflowField,
|
||||
typeGuard: buildTypeGuard(_singleWorkflowField.key),
|
||||
getData: buildGetData(_singleWorkflowField.key, _singleWorkflowField.type),
|
||||
};
|
||||
|
||||
export const useLinearViewFieldDnd = (ref: RefObject<HTMLElement>, fieldIdentifier: FieldIdentifier) => {
|
||||
const [dndListState, setListDndState] = useState<DndListState>(idle);
|
||||
const [dndListState, setListDndState] = useState<DndListTargetState>(idle);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { Dnd } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import type { SetUpscaleInitialImageActionData } from 'features/imageActions/actions';
|
||||
import { setUpscaleInitialImageActionApi } from 'features/imageActions/actions';
|
||||
import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -12,10 +13,7 @@ import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
export const UpscaleInitialImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useAppSelector(selectUpscaleInitialImage);
|
||||
const targetData = useMemo<Dnd.types['TargetDataTypeMap']['setUpscaleInitialImageFromImage']>(
|
||||
() => Dnd.Target.setUpscaleInitialImageFromImage.getData(),
|
||||
[]
|
||||
);
|
||||
const targetData = useMemo<SetUpscaleInitialImageActionData>(() => setUpscaleInitialImageActionApi.getData(), []);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(upscaleInitialImageChanged(null));
|
||||
|
||||
Reference in New Issue
Block a user