mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Compare commits
3 Commits
main
...
feat/ref-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9536852dc7 | ||
|
|
1b796ad5b3 | ||
|
|
62ae599cef |
@@ -1,6 +1,10 @@
|
||||
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
|
||||
import { Button, Collapse, Divider, Flex, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { RefImagePreview } from 'features/controlLayers/components/RefImage/RefImagePreview';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
@@ -9,15 +13,18 @@ import { useNewGlobalReferenceImageFromBbox } from 'features/controlLayers/hooks
|
||||
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
refImageAdded,
|
||||
refImagesReordered,
|
||||
selectIsRefImagePanelOpen,
|
||||
selectRefImageEntityIds,
|
||||
selectSelectedRefEntityId,
|
||||
} from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToCroppableImage } from 'features/controlLayers/store/util';
|
||||
import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { addGlobalReferenceImageDndTarget, singleRefImageDndSource } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { triggerPostMoveFlash } from 'features/dnd/util';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useEffect, useMemo } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiUploadBold } from 'react-icons/pi';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
@@ -29,6 +36,69 @@ export const RefImageList = memo(() => {
|
||||
const ids = useAppSelector(selectRefImageEntityIds);
|
||||
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
|
||||
const selectedEntityId = useAppSelector(selectSelectedRefEntityId);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
return monitorForElements({
|
||||
canMonitor({ source }) {
|
||||
return singleRefImageDndSource.typeGuard(source.data);
|
||||
},
|
||||
onDrop({ location, source }) {
|
||||
const target = location.current.dropTargets[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = source.data;
|
||||
const targetData = target.data;
|
||||
|
||||
if (!singleRefImageDndSource.typeGuard(sourceData) || !singleRefImageDndSource.typeGuard(targetData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexOfSource = ids.indexOf(sourceData.payload.id);
|
||||
const indexOfTarget = ids.indexOf(targetData.payload.id);
|
||||
|
||||
if (indexOfTarget < 0 || indexOfSource < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexOfSource === indexOfTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closestEdgeOfTarget = extractClosestEdge(targetData);
|
||||
|
||||
let edgeIndexDelta = 0;
|
||||
if (closestEdgeOfTarget === 'right') {
|
||||
edgeIndexDelta = 1;
|
||||
} else if (closestEdgeOfTarget === 'left') {
|
||||
edgeIndexDelta = -1;
|
||||
}
|
||||
|
||||
if (indexOfSource === indexOfTarget + edgeIndexDelta) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIds = reorderWithEdge({
|
||||
list: ids,
|
||||
startIndex: indexOfSource,
|
||||
indexOfTarget,
|
||||
closestEdgeOfTarget,
|
||||
axis: 'horizontal',
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
dispatch(refImagesReordered({ ids: nextIds }));
|
||||
});
|
||||
|
||||
const element = document.querySelector(`[data-ref-image-id="${sourceData.payload.id}"]`);
|
||||
if (element instanceof HTMLElement) {
|
||||
triggerPostMoveFlash(element, colorTokenToCssVar('base.700'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [dispatch, ids]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, IconButton, Skeleton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Icon, IconButton, Skeleton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { round } from 'es-toolkit/compat';
|
||||
import { useRefImageDnd } from 'features/controlLayers/components/RefImage/useRefImageDnd';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
|
||||
import { selectMainModelConfig } from 'features/controlLayers/store/paramsSlice';
|
||||
@@ -12,7 +13,8 @@ import {
|
||||
} from 'features/controlLayers/store/refImagesSlice';
|
||||
import { isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiExclamationMarkBold, PiEyeSlashBold, PiImageBold } from 'react-icons/pi';
|
||||
import { useImageDTOFromCroppableImage } from 'services/api/endpoints/images';
|
||||
@@ -49,6 +51,13 @@ const weightDisplaySx: SystemStyleObject = {
|
||||
},
|
||||
};
|
||||
|
||||
// Scoped to ref image thumbnails only: prevents the iOS long-press "Save Image"
|
||||
// callout from hijacking drag attempts on iPad.
|
||||
const wrapperSx: SystemStyleObject = {
|
||||
WebkitTouchCallout: 'none',
|
||||
userSelect: 'none',
|
||||
};
|
||||
|
||||
const getImageSxWithWeight = (weight: number): SystemStyleObject => {
|
||||
const fillPercentage = Math.max(0, Math.min(100, weight * 100));
|
||||
|
||||
@@ -75,6 +84,8 @@ export const RefImagePreview = memo(() => {
|
||||
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
|
||||
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
|
||||
const isExternalModel = !!mainModelConfig && isExternalApiModelConfig(mainModelConfig);
|
||||
const dndRef = useRef<HTMLDivElement>(null);
|
||||
const [dndListState, isDragging] = useRefImageDnd(dndRef, id);
|
||||
|
||||
const imageDTO = useImageDTOFromCroppableImage(entity.config.image);
|
||||
|
||||
@@ -108,98 +119,124 @@ export const RefImagePreview = memo(() => {
|
||||
|
||||
if (!entity.config.image) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={t('controlLayers.selectRefImage')}
|
||||
<Box
|
||||
ref={dndRef}
|
||||
position="relative"
|
||||
h="full"
|
||||
variant="ghost"
|
||||
aspectRatio="1/1"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderColor="error.300"
|
||||
borderRadius="base"
|
||||
icon={<PiImageBold />}
|
||||
colorScheme="error"
|
||||
onClick={onClick}
|
||||
flexShrink={0}
|
||||
data-is-open={selectedEntityId === id && isPanelOpen}
|
||||
data-is-error={true}
|
||||
data-is-disabled={!entity.isEnabled}
|
||||
sx={sx}
|
||||
/>
|
||||
opacity={isDragging ? 0.3 : 1}
|
||||
data-ref-image-id={id}
|
||||
sx={wrapperSx}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('controlLayers.selectRefImage')}
|
||||
h="full"
|
||||
variant="ghost"
|
||||
aspectRatio="1/1"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderColor="error.300"
|
||||
borderRadius="base"
|
||||
icon={<PiImageBold />}
|
||||
colorScheme="error"
|
||||
onClick={onClick}
|
||||
flexShrink={0}
|
||||
data-is-open={selectedEntityId === id && isPanelOpen}
|
||||
data-is-error={true}
|
||||
data-is-disabled={!entity.isEnabled}
|
||||
sx={sx}
|
||||
/>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip label={warnings.length > 0 ? <RefImageWarningTooltipContent warnings={warnings} /> : undefined}>
|
||||
<Flex
|
||||
position="relative"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderRadius="base"
|
||||
aspectRatio="1/1"
|
||||
maxW="full"
|
||||
maxH="full"
|
||||
flexShrink={0}
|
||||
sx={sx}
|
||||
data-is-open={selectedEntityId === id && isPanelOpen}
|
||||
data-is-error={warnings.length > 0}
|
||||
data-is-disabled={!entity.isEnabled}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
cursor="pointer"
|
||||
overflow="hidden"
|
||||
>
|
||||
{imageDTO ? (
|
||||
<img
|
||||
src={imageDTO.image_url}
|
||||
style={{ objectFit: 'contain', aspectRatio: '1 / 1', maxWidth: '100%', maxHeight: '100%' }}
|
||||
height={imageDTO.height}
|
||||
alt={imageDTO.image_name}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton h="full" aspectRatio="1/1" />
|
||||
)}
|
||||
{isIPAdapterConfig(entity.config) && !isExternalModel && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
inset={0}
|
||||
fontWeight="semibold"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex={1}
|
||||
data-visible={showWeightDisplay}
|
||||
sx={weightDisplaySx}
|
||||
>
|
||||
<Text filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))">
|
||||
{`${round(entity.config.weight * 100, 2)}%`}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{!entity.isEnabled && (
|
||||
<Icon
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translateX(-50%) translateY(-50%)"
|
||||
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))"
|
||||
color="base.300"
|
||||
boxSize={8}
|
||||
as={PiEyeSlashBold}
|
||||
/>
|
||||
)}
|
||||
{entity.isEnabled && warnings.length > 0 && (
|
||||
<Icon
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translateX(-50%) translateY(-50%)"
|
||||
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))"
|
||||
color="error.500"
|
||||
boxSize={12}
|
||||
as={PiExclamationMarkBold}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Box
|
||||
ref={dndRef}
|
||||
position="relative"
|
||||
h="full"
|
||||
aspectRatio="1/1"
|
||||
flexShrink={0}
|
||||
opacity={isDragging ? 0.3 : 1}
|
||||
data-ref-image-id={id}
|
||||
sx={wrapperSx}
|
||||
>
|
||||
<Tooltip label={warnings.length > 0 ? <RefImageWarningTooltipContent warnings={warnings} /> : undefined}>
|
||||
<Flex
|
||||
position="relative"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderRadius="base"
|
||||
aspectRatio="1/1"
|
||||
maxW="full"
|
||||
h="full"
|
||||
maxH="full"
|
||||
flexShrink={0}
|
||||
sx={sx}
|
||||
data-is-open={selectedEntityId === id && isPanelOpen}
|
||||
data-is-error={warnings.length > 0}
|
||||
data-is-disabled={!entity.isEnabled}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
cursor="pointer"
|
||||
overflow="hidden"
|
||||
>
|
||||
{imageDTO ? (
|
||||
<img
|
||||
src={imageDTO.image_url}
|
||||
style={{ objectFit: 'contain', aspectRatio: '1 / 1', maxWidth: '100%', maxHeight: '100%' }}
|
||||
height={imageDTO.height}
|
||||
alt={imageDTO.image_name}
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton h="full" aspectRatio="1/1" />
|
||||
)}
|
||||
{isIPAdapterConfig(entity.config) && !isExternalModel && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
inset={0}
|
||||
fontWeight="semibold"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
zIndex={1}
|
||||
data-visible={showWeightDisplay}
|
||||
sx={weightDisplaySx}
|
||||
>
|
||||
<Text filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))">
|
||||
{`${round(entity.config.weight * 100, 2)}%`}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{!entity.isEnabled && (
|
||||
<Icon
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translateX(-50%) translateY(-50%)"
|
||||
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))"
|
||||
color="base.300"
|
||||
boxSize={8}
|
||||
as={PiEyeSlashBold}
|
||||
/>
|
||||
)}
|
||||
{entity.isEnabled && warnings.length > 0 && (
|
||||
<Icon
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translateX(-50%) translateY(-50%)"
|
||||
filter="drop-shadow(0px 0px 4px rgb(0, 0, 0)) drop-shadow(0px 0px 2px rgba(0, 0, 0, 1))"
|
||||
color="error.500"
|
||||
boxSize={12}
|
||||
as={PiExclamationMarkBold}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<DndListDropIndicator dndState={dndListState} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
RefImagePreview.displayName = 'RefImagePreview';
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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 { singleRefImageDndSource } from 'features/dnd/dnd';
|
||||
import { type DndListTargetState, idle } from 'features/dnd/types';
|
||||
import { firefoxDndFix } from 'features/dnd/util';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useRefImageDnd = (ref: RefObject<HTMLElement>, id: string) => {
|
||||
const [dndListState, setDndListState] = useState<DndListTargetState>(idle);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
return combine(
|
||||
firefoxDndFix(element),
|
||||
draggable({
|
||||
element,
|
||||
getInitialData() {
|
||||
return singleRefImageDndSource.getData({ id });
|
||||
},
|
||||
onDragStart() {
|
||||
setDndListState({ type: 'is-dragging' });
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop() {
|
||||
setDndListState(idle);
|
||||
setIsDragging(false);
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
element,
|
||||
canDrop({ source }) {
|
||||
if (!singleRefImageDndSource.typeGuard(source.data)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getData({ input }) {
|
||||
const data = singleRefImageDndSource.getData({ id });
|
||||
return attachClosestEdge(data, {
|
||||
element,
|
||||
input,
|
||||
allowedEdges: ['left', 'right'],
|
||||
});
|
||||
},
|
||||
getIsSticky() {
|
||||
return true;
|
||||
},
|
||||
onDragEnter({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
setDndListState({ type: 'is-dragging-over', closestEdge });
|
||||
},
|
||||
onDrag({ self }) {
|
||||
const closestEdge = extractClosestEdge(self.data);
|
||||
|
||||
setDndListState((current) => {
|
||||
if (current.type === 'is-dragging-over' && current.closestEdge === closestEdge) {
|
||||
return current;
|
||||
}
|
||||
return { type: 'is-dragging-over', closestEdge };
|
||||
});
|
||||
},
|
||||
onDragLeave() {
|
||||
setDndListState(idle);
|
||||
},
|
||||
onDrop() {
|
||||
setDndListState(idle);
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [id, ref]);
|
||||
|
||||
return [dndListState, isDragging] as const;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { refImagesReordered, refImagesSliceConfig } from './refImagesSlice';
|
||||
import type { RefImagesState } from './types';
|
||||
import { getReferenceImageState } from './util';
|
||||
|
||||
const buildState = (ids: string[]): RefImagesState => ({
|
||||
selectedEntityId: ids[0] ?? null,
|
||||
isPanelOpen: false,
|
||||
entities: ids.map((id) => getReferenceImageState(id)),
|
||||
});
|
||||
|
||||
describe('refImagesSlice', () => {
|
||||
const { reducer } = refImagesSliceConfig.slice;
|
||||
|
||||
describe('refImagesReordered', () => {
|
||||
it('reorders entities to match the provided id order', () => {
|
||||
const state = buildState(['a', 'b', 'c']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['c', 'a', 'b'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('swaps two entities', () => {
|
||||
const state = buildState(['a', 'b']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['b', 'a'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['b', 'a']);
|
||||
});
|
||||
|
||||
it('reverses the list', () => {
|
||||
const state = buildState(['a', 'b', 'c', 'd']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['d', 'c', 'b', 'a'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['d', 'c', 'b', 'a']);
|
||||
});
|
||||
|
||||
it('preserves entity config when reordering', () => {
|
||||
const state = buildState(['a', 'b']);
|
||||
state.entities[0]!.isEnabled = false;
|
||||
const result = reducer(state, refImagesReordered({ ids: ['b', 'a'] }));
|
||||
const movedA = result.entities.find((e) => e.id === 'a');
|
||||
expect(movedA?.isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('is a no-op when the ids length does not match the entities length', () => {
|
||||
const state = buildState(['a', 'b', 'c']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['a', 'b'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('is a no-op when ids contain an unknown id', () => {
|
||||
const state = buildState(['a', 'b', 'c']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['a', 'b', 'x'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('is a no-op when ids contain a duplicate', () => {
|
||||
// Duplicates imply one of the original ids is missing, so length-or-map-lookup fails.
|
||||
const state = buildState(['a', 'b', 'c']);
|
||||
const result = reducer(state, refImagesReordered({ ids: ['a', 'a', 'b'] }));
|
||||
expect(result.entities.map((e) => e.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('handles an empty list', () => {
|
||||
const state = buildState([]);
|
||||
const result = reducer(state, refImagesReordered({ ids: [] }));
|
||||
expect(result.entities).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not change selectedEntityId or isPanelOpen', () => {
|
||||
const state: RefImagesState = {
|
||||
...buildState(['a', 'b', 'c']),
|
||||
selectedEntityId: 'b',
|
||||
isPanelOpen: true,
|
||||
};
|
||||
const result = reducer(state, refImagesReordered({ ids: ['c', 'b', 'a'] }));
|
||||
expect(result.selectedEntityId).toBe('b');
|
||||
expect(result.isPanelOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -284,6 +284,25 @@ const slice = createSlice({
|
||||
entity.config = { ...config, image: entity.config.image };
|
||||
},
|
||||
refImagesReset: () => getInitialRefImagesState(),
|
||||
refImagesReordered: (state, action: PayloadAction<{ ids: string[] }>) => {
|
||||
const { ids } = action.payload;
|
||||
if (ids.length !== state.entities.length) {
|
||||
return;
|
||||
}
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
return;
|
||||
}
|
||||
const byId = new Map(state.entities.map((e) => [e.id, e]));
|
||||
const next: RefImageState[] = [];
|
||||
for (const id of ids) {
|
||||
const entity = byId.get(id);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
next.push(entity);
|
||||
}
|
||||
state.entities = next;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -301,6 +320,7 @@ export const {
|
||||
refImageFLUXReduxImageInfluenceChanged,
|
||||
refImageIsEnabledToggled,
|
||||
refImagesRecalled,
|
||||
refImagesReordered,
|
||||
} = slice.actions;
|
||||
|
||||
export const refImagesSliceConfig: SliceConfig<typeof slice> = {
|
||||
|
||||
@@ -97,6 +97,16 @@ export const multipleImageDndSource: DndSource<MultipleImageDndSourceData> = {
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region Single Reference Image (reorder)
|
||||
const _singleRefImage = buildTypeAndKey('single-ref-image');
|
||||
type SingleRefImageDndSourceData = DndData<typeof _singleRefImage.type, typeof _singleRefImage.key, { id: string }>;
|
||||
export const singleRefImageDndSource: DndSource<SingleRefImageDndSourceData> = {
|
||||
..._singleRefImage,
|
||||
typeGuard: buildTypeGuard(_singleRefImage.key),
|
||||
getData: buildGetData(_singleRefImage.key, _singleRefImage.type),
|
||||
};
|
||||
//#endregion
|
||||
|
||||
const _singleCanvasEntity = buildTypeAndKey('single-canvas-entity');
|
||||
type SingleCanvasEntityDndSourceData = DndData<
|
||||
typeof _singleCanvasEntity.type,
|
||||
|
||||
Reference in New Issue
Block a user