mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): make ref images croppable
This commit is contained in:
@@ -12,7 +12,13 @@ import {
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { refImageModelChanged, selectRefImagesSlice } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier, isFLUXReduxConfig, isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
getEntityIdentifier,
|
||||
isFLUXReduxConfig,
|
||||
isIPAdapterConfig,
|
||||
isRegionalGuidanceFLUXReduxConfig,
|
||||
isRegionalGuidanceIPAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import {
|
||||
@@ -252,7 +258,7 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
|
||||
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
||||
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
|
||||
if (!isIPAdapterConfig(config)) {
|
||||
if (!isRegionalGuidanceIPAdapterConfig(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -295,7 +301,7 @@ const handleFLUXReduxModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
|
||||
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
||||
entity.referenceImages.forEach(({ id: referenceImageId, config }) => {
|
||||
if (!isFLUXReduxConfig(config)) {
|
||||
if (!isRegionalGuidanceFLUXReduxConfig(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { objectEquals } from '@observ33r/object-equals';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { CroppableImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { Editor } from 'features/cropper/lib/editor';
|
||||
import { cropImageModalApi } from 'features/cropper/store';
|
||||
import type { setGlobalReferenceImageDndTarget, setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
@@ -14,14 +18,14 @@ import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { PiArrowCounterClockwiseBold, PiCropBold, PiRulerBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery, useUploadImageMutation } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type Props<T extends typeof setGlobalReferenceImageDndTarget | typeof setRegionalGuidanceReferenceImageDndTarget> = {
|
||||
image: ImageWithDims | null;
|
||||
onChangeImage: (imageDTO: ImageDTO | null) => void;
|
||||
image: CroppableImageWithDims | null;
|
||||
onChangeImage: (croppableImage: CroppableImageWithDims | null) => void;
|
||||
dndTarget: T;
|
||||
dndTargetData: ReturnType<T['getData']>;
|
||||
};
|
||||
@@ -38,20 +42,28 @@ export const RefImageImage = memo(
|
||||
const isConnected = useStore($isConnected);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const imageWithDims = image?.crop?.image ?? image?.original.image ?? null;
|
||||
const croppedImageDTOReq = useGetImageDTOQuery(image?.crop?.image?.image_name ?? skipToken);
|
||||
const originalImageDTOReq = useGetImageDTOQuery(image?.original.image.image_name ?? skipToken);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
|
||||
const originalImageDTO = originalImageDTOReq.currentData;
|
||||
const croppedImageDTO = croppedImageDTOReq.currentData;
|
||||
const imageDTO = croppedImageDTO ?? originalImageDTO;
|
||||
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && isError) {
|
||||
if ((isConnected && croppedImageDTOReq.isError) || originalImageDTOReq.isError) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isError, isConnected]);
|
||||
}, [handleResetControlImage, isConnected, croppedImageDTOReq.isError, originalImageDTOReq.isError]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
onChangeImage(imageDTO);
|
||||
onChangeImage(imageDTOToCroppableImage(imageDTO));
|
||||
},
|
||||
[onChangeImage]
|
||||
);
|
||||
@@ -70,13 +82,67 @@ export const RefImageImage = memo(
|
||||
}
|
||||
}, [imageDTO, isStaging, store, tab]);
|
||||
|
||||
const edit = useCallback(() => {
|
||||
if (!originalImageDTO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We will create a new editor instance each time the user wants to edit
|
||||
const editor = new Editor();
|
||||
|
||||
// When the user applies the crop, we will upload the cropped image and store the applied crop box so if the user
|
||||
// re-opens the editor they see the same crop
|
||||
const onApplyCrop = async () => {
|
||||
const box = editor.getCropBox();
|
||||
if (objectEquals(box, image?.crop?.box)) {
|
||||
// If the box hasn't changed, don't do anything
|
||||
return;
|
||||
}
|
||||
if (!box || objectEquals(box, { x: 0, y: 0, width: originalImageDTO.width, height: originalImageDTO.height })) {
|
||||
// There is a crop applied but it is the whole iamge - revert to original image
|
||||
onChangeImage(imageDTOToCroppableImage(originalImageDTO));
|
||||
return;
|
||||
}
|
||||
const blob = await editor.exportImage('blob');
|
||||
const file = new File([blob], 'image.png', { type: 'image/png' });
|
||||
|
||||
const newCroppedImageDTO = await uploadImage({
|
||||
file,
|
||||
is_intermediate: true,
|
||||
image_category: 'user',
|
||||
}).unwrap();
|
||||
|
||||
onChangeImage(
|
||||
imageDTOToCroppableImage(originalImageDTO, {
|
||||
image: imageDTOToImageWithDims(newCroppedImageDTO),
|
||||
box,
|
||||
ratio: editor.getCropAspectRatio(),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onReady = async () => {
|
||||
const initial = image?.crop ? { cropBox: image.crop.box, aspectRatio: image.crop.ratio } : undefined;
|
||||
// Load the image into the editor and open the modal once it's ready
|
||||
await editor.loadImage(originalImageDTO.image_url, initial);
|
||||
};
|
||||
|
||||
cropImageModalApi.open({ editor, onApplyCrop, onReady });
|
||||
}, [image?.crop, onChangeImage, originalImageDTO, uploadImage]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||
<Flex
|
||||
position="relative"
|
||||
w="full"
|
||||
h="full"
|
||||
alignItems="center"
|
||||
data-error={!imageDTO && !imageWithDims?.image_name}
|
||||
>
|
||||
{!imageDTO && (
|
||||
<UploadImageIconButton
|
||||
w="full"
|
||||
h="full"
|
||||
isError={!imageDTO && !image?.image_name}
|
||||
isError={!imageDTO && !imageWithDims?.image_name}
|
||||
onUpload={onUpload}
|
||||
fontSize={36}
|
||||
/>
|
||||
@@ -99,6 +165,15 @@ export const RefImageImage = memo(
|
||||
isDisabled={!imageDTO || (tab === 'canvas' && isStaging)}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineStart={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={edit}
|
||||
icon={<PiCropBold size={16} />}
|
||||
tooltip={t('common.crop')}
|
||||
isDisabled={!imageDTO}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<DndDropTarget dndTarget={dndTarget} dndTargetData={dndTargetData} label={t('gallery.drop')} />
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
selectRefImageEntityIds,
|
||||
selectSelectedRefEntityId,
|
||||
} from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageDTOToCroppableImage } from 'features/controlLayers/store/util';
|
||||
import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
@@ -92,7 +92,7 @@ const AddRefImageDropTargetAndButton = memo(() => {
|
||||
({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
config.image = imageDTOToCroppableImage(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
},
|
||||
allowMultiple: false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Icon, IconButton, Image, Skeleton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { round } from 'es-toolkit/compat';
|
||||
import { useRefImageEntity } from 'features/controlLayers/components/RefImage/useRefImageEntity';
|
||||
@@ -15,7 +14,7 @@ import { isIPAdapterConfig } from 'features/controlLayers/store/types';
|
||||
import { getGlobalReferenceImageWarnings } from 'features/controlLayers/store/validators';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { PiExclamationMarkBold, PiEyeSlashBold, PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useImageDTOFromCroppableImage } from 'services/api/endpoints/images';
|
||||
|
||||
import { RefImageWarningTooltipContent } from './RefImageWarningTooltipContent';
|
||||
|
||||
@@ -72,7 +71,8 @@ export const RefImagePreview = memo(() => {
|
||||
const selectedEntityId = useAppSelector(selectSelectedRefEntityId);
|
||||
const isPanelOpen = useAppSelector(selectIsRefImagePanelOpen);
|
||||
const [showWeightDisplay, setShowWeightDisplay] = useState(false);
|
||||
const { data: imageDTO } = useGetImageDTOQuery(entity.config.image?.image_name ?? skipToken);
|
||||
|
||||
const imageDTO = useImageDTOFromCroppableImage(entity.config.image);
|
||||
|
||||
const sx = useMemo(() => {
|
||||
if (!isIPAdapterConfig(entity.config)) {
|
||||
@@ -145,7 +145,7 @@ export const RefImagePreview = memo(() => {
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image
|
||||
src={imageDTO?.thumbnail_url}
|
||||
src={imageDTO?.image_url}
|
||||
objectFit="contain"
|
||||
aspectRatio="1/1"
|
||||
height={imageDTO?.height}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from 'features/controlLayers/store/refImagesSlice';
|
||||
import type {
|
||||
CLIPVisionModelV2,
|
||||
CroppableImageWithDims,
|
||||
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
|
||||
IPMethodV2,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -42,7 +43,6 @@ import type {
|
||||
ChatGPT4oModelConfig,
|
||||
FLUXKontextModelConfig,
|
||||
FLUXReduxModelConfig,
|
||||
ImageDTO,
|
||||
IPAdapterModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
@@ -104,15 +104,19 @@ const RefImageSettingsContent = memo(() => {
|
||||
);
|
||||
|
||||
const onChangeImage = useCallback(
|
||||
(imageDTO: ImageDTO | null) => {
|
||||
dispatch(refImageImageChanged({ id, imageDTO }));
|
||||
(croppableImage: CroppableImageWithDims | null) => {
|
||||
dispatch(refImageImageChanged({ id, croppableImage }));
|
||||
},
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const dndTargetData = useMemo<SetGlobalReferenceImageDndTargetData>(
|
||||
() => setGlobalReferenceImageDndTarget.getData({ id }, config.image?.image_name),
|
||||
[id, config.image?.image_name]
|
||||
() =>
|
||||
setGlobalReferenceImageDndTarget.getData(
|
||||
{ id },
|
||||
config.image?.crop?.image.image_name ?? config.image?.original.image.image_name
|
||||
),
|
||||
[id, config.image?.crop?.image.image_name, config.image?.original.image.image_name]
|
||||
);
|
||||
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FLUXReduxImageInfluence } from 'features/controlLayers/components/commo
|
||||
import { IPAdapterCLIPVisionModel } from 'features/controlLayers/components/common/IPAdapterCLIPVisionModel';
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAdapterMethod';
|
||||
import { RefImageImage } from 'features/controlLayers/components/RefImage/RefImageImage';
|
||||
import { RegionalGuidanceIPAdapterSettingsEmptyState } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettingsEmptyState';
|
||||
import { RegionalReferenceImageModel } from 'features/controlLayers/components/RegionalGuidance/RegionalReferenceImageModel';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
@@ -37,6 +36,8 @@ import { PiBoundingBoxBold, PiXBold } from 'react-icons/pi';
|
||||
import type { FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { RegionalGuidanceRefImageImage } from './RegionalGuidanceRefImageImage';
|
||||
|
||||
type Props = {
|
||||
referenceImageId: string;
|
||||
};
|
||||
@@ -114,7 +115,7 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
||||
{ entityIdentifier, referenceImageId },
|
||||
config.image?.image_name
|
||||
),
|
||||
[entityIdentifier, config.image?.image_name, referenceImageId]
|
||||
[entityIdentifier, config.image, referenceImageId]
|
||||
);
|
||||
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
||||
@@ -170,7 +171,7 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
|
||||
<RefImageImage
|
||||
<RegionalGuidanceRefImageImage
|
||||
image={config.image}
|
||||
onChangeImage={onChangeImage}
|
||||
dndTarget={setRegionalGuidanceReferenceImageDndTarget}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
|
||||
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type Props = {
|
||||
image: ImageWithDims | null;
|
||||
onChangeImage: (imageDTO: ImageDTO | null) => void;
|
||||
dndTarget: typeof setRegionalGuidanceReferenceImageDndTarget;
|
||||
dndTargetData: ReturnType<(typeof setRegionalGuidanceReferenceImageDndTarget)['getData']>;
|
||||
};
|
||||
|
||||
export const RegionalGuidanceRefImageImage = memo(({ image, onChangeImage, dndTarget, dndTargetData }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
const isConnected = useStore($isConnected);
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const isStaging = useCanvasIsStaging();
|
||||
const { currentData: imageDTO, isError } = useGetImageDTOQuery(image?.image_name ?? skipToken);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && isError) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isError, isConnected]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
onChangeImage(imageDTO);
|
||||
},
|
||||
[onChangeImage]
|
||||
);
|
||||
|
||||
const recallSizeAndOptimize = useCallback(() => {
|
||||
if (!imageDTO || (tab === 'canvas' && isStaging)) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = imageDTO;
|
||||
if (tab === 'canvas') {
|
||||
store.dispatch(bboxSizeRecalled({ width, height }));
|
||||
store.dispatch(bboxSizeOptimized());
|
||||
} else if (tab === 'generate') {
|
||||
store.dispatch(sizeRecalled({ width, height }));
|
||||
store.dispatch(sizeOptimized());
|
||||
}
|
||||
}, [imageDTO, isStaging, store, tab]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center" data-error={!imageDTO && !image?.image_name}>
|
||||
{!imageDTO && (
|
||||
<UploadImageIconButton
|
||||
w="full"
|
||||
h="full"
|
||||
isError={!imageDTO && !image?.image_name}
|
||||
onUpload={onUpload}
|
||||
fontSize={36}
|
||||
/>
|
||||
)}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} borderRadius="base" borderWidth={1} borderStyle="solid" w="full" />
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('common.reset')}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex position="absolute" flexDir="column" bottom={2} insetInlineEnd={2} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={recallSizeAndOptimize}
|
||||
icon={<PiRulerBold size={16} />}
|
||||
tooltip={t('parameters.useSize')}
|
||||
isDisabled={!imageDTO || (tab === 'canvas' && isStaging)}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<DndDropTarget dndTarget={dndTarget} dndTargetData={dndTargetData} label={t('gallery.drop')} />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
RegionalGuidanceRefImageImage.displayName = 'RegionalGuidanceRefImageImage';
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
FluxKontextReferenceImageConfig,
|
||||
Gemini2_5ReferenceImageConfig,
|
||||
IPAdapterConfig,
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
T2IAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import {
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
initialFluxKontextReferenceImage,
|
||||
initialGemini2_5ReferenceImage,
|
||||
initialIPAdapter,
|
||||
initialRegionalGuidanceIPAdapter,
|
||||
initialT2IAdapter,
|
||||
} from 'features/controlLayers/store/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
@@ -125,7 +127,7 @@ export const getDefaultRefImageConfig = (
|
||||
return config;
|
||||
};
|
||||
|
||||
export const getDefaultRegionalGuidanceRefImageConfig = (getState: AppGetState): IPAdapterConfig => {
|
||||
export const getDefaultRegionalGuidanceRefImageConfig = (getState: AppGetState): RegionalGuidanceIPAdapterConfig => {
|
||||
// Regional guidance ref images do not support ChatGPT-4o, so we always return the IP Adapter config.
|
||||
const state = getState();
|
||||
|
||||
@@ -138,7 +140,7 @@ export const getDefaultRegionalGuidanceRefImageConfig = (getState: AppGetState):
|
||||
const modelConfig = ipAdapterModelConfigs.find((m) => m.base === base);
|
||||
|
||||
// Clone the initial IP Adapter config and set the model if available.
|
||||
const config = deepClone(initialIPAdapter);
|
||||
const config = deepClone(initialRegionalGuidanceIPAdapter);
|
||||
|
||||
if (modelConfig) {
|
||||
config.model = zModelIdentifierField.parse(modelConfig);
|
||||
|
||||
@@ -32,7 +32,12 @@ import type {
|
||||
RefImageState,
|
||||
RegionalGuidanceRefImageState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
|
||||
import {
|
||||
imageDTOToCroppableImage,
|
||||
imageDTOToImageObject,
|
||||
imageDTOToImageWithDims,
|
||||
initialControlNet,
|
||||
} from 'features/controlLayers/store/util';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
@@ -209,7 +214,7 @@ export const useNewGlobalReferenceImageFromBbox = () => {
|
||||
const overrides: Partial<RefImageState> = {
|
||||
config: {
|
||||
...getDefaultRefImageConfig(getState),
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
image: imageDTOToCroppableImage(imageDTO),
|
||||
},
|
||||
};
|
||||
dispatch(refImageAdded({ overrides }));
|
||||
@@ -312,7 +317,7 @@ export const usePullBboxIntoGlobalReferenceImage = (id: string) => {
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, _: Rect) => {
|
||||
dispatch(refImageImageChanged({ id, imageDTO }));
|
||||
dispatch(refImageImageChanged({ id, croppableImage: imageDTOToCroppableImage(imageDTO) }));
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -82,10 +82,10 @@ import {
|
||||
IMAGEN_ASPECT_RATIOS,
|
||||
isChatGPT4oAspectRatioID,
|
||||
isFluxKontextAspectRatioID,
|
||||
isFLUXReduxConfig,
|
||||
isGemini2_5AspectRatioID,
|
||||
isImagenAspectRatioID,
|
||||
isIPAdapterConfig,
|
||||
isRegionalGuidanceFLUXReduxConfig,
|
||||
isRegionalGuidanceIPAdapterConfig,
|
||||
zCanvasState,
|
||||
} from './types';
|
||||
import {
|
||||
@@ -99,6 +99,7 @@ import {
|
||||
initialControlNet,
|
||||
initialFLUXRedux,
|
||||
initialIPAdapter,
|
||||
initialRegionalGuidanceIPAdapter,
|
||||
initialT2IAdapter,
|
||||
makeDefaultRasterLayerAdjustments,
|
||||
} from './util';
|
||||
@@ -804,7 +805,7 @@ const slice = createSlice({
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
const config = { id: referenceImageId, config: deepClone(initialIPAdapter) };
|
||||
const config = { id: referenceImageId, config: deepClone(initialRegionalGuidanceIPAdapter) };
|
||||
merge(config, overrides);
|
||||
entity.referenceImages.push(config);
|
||||
},
|
||||
@@ -847,7 +848,7 @@ const slice = createSlice({
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (!isIPAdapterConfig(referenceImage.config)) {
|
||||
if (!isRegionalGuidanceIPAdapterConfig(referenceImage.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -864,7 +865,7 @@ const slice = createSlice({
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (!isIPAdapterConfig(referenceImage.config)) {
|
||||
if (!isRegionalGuidanceIPAdapterConfig(referenceImage.config)) {
|
||||
return;
|
||||
}
|
||||
referenceImage.config.beginEndStepPct = beginEndStepPct;
|
||||
@@ -880,7 +881,7 @@ const slice = createSlice({
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (!isIPAdapterConfig(referenceImage.config)) {
|
||||
if (!isRegionalGuidanceIPAdapterConfig(referenceImage.config)) {
|
||||
return;
|
||||
}
|
||||
referenceImage.config.method = method;
|
||||
@@ -899,7 +900,7 @@ const slice = createSlice({
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (!isFLUXReduxConfig(referenceImage.config)) {
|
||||
if (!isRegionalGuidanceFLUXReduxConfig(referenceImage.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -928,7 +929,7 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIPAdapterConfig(referenceImage.config) && isFluxReduxModelConfig(modelConfig)) {
|
||||
if (isRegionalGuidanceIPAdapterConfig(referenceImage.config) && isFluxReduxModelConfig(modelConfig)) {
|
||||
// Switching from ip_adapter to flux_redux
|
||||
referenceImage.config = {
|
||||
...initialFLUXRedux,
|
||||
@@ -938,7 +939,7 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFLUXReduxConfig(referenceImage.config) && isIPAdapterModelConfig(modelConfig)) {
|
||||
if (isRegionalGuidanceFLUXReduxConfig(referenceImage.config) && isIPAdapterModelConfig(modelConfig)) {
|
||||
// Switching from flux_redux to ip_adapter
|
||||
referenceImage.config = {
|
||||
...initialIPAdapter,
|
||||
@@ -948,7 +949,7 @@ const slice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIPAdapterConfig(referenceImage.config)) {
|
||||
if (isRegionalGuidanceIPAdapterConfig(referenceImage.config)) {
|
||||
referenceImage.config.model = zModelIdentifierField.parse(modelConfig);
|
||||
|
||||
// Ensure that the IP Adapter model is compatible with the CLIP Vision model
|
||||
@@ -971,7 +972,7 @@ const slice = createSlice({
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
if (!isIPAdapterConfig(referenceImage.config)) {
|
||||
if (!isRegionalGuidanceIPAdapterConfig(referenceImage.config)) {
|
||||
return;
|
||||
}
|
||||
referenceImage.config.clipVisionModel = clipVisionModel;
|
||||
|
||||
@@ -6,13 +6,16 @@ import type { RootState } from 'app/store/store';
|
||||
import type { SliceConfig } from 'app/store/types';
|
||||
import { clamp } from 'es-toolkit/compat';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type { FLUXReduxImageInfluence, RefImagesState } from 'features/controlLayers/store/types';
|
||||
import type {
|
||||
CroppableImageWithDims,
|
||||
FLUXReduxImageInfluence,
|
||||
RefImagesState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type {
|
||||
ChatGPT4oModelConfig,
|
||||
FLUXKontextModelConfig,
|
||||
FLUXReduxModelConfig,
|
||||
ImageDTO,
|
||||
IPAdapterModelConfig,
|
||||
} from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
@@ -22,7 +25,6 @@ import type { CLIPVisionModelV2, IPMethodV2, RefImageState } from './types';
|
||||
import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig, zRefImagesState } from './types';
|
||||
import {
|
||||
getReferenceImageState,
|
||||
imageDTOToImageWithDims,
|
||||
initialChatGPT4oReferenceImage,
|
||||
initialFluxKontextReferenceImage,
|
||||
initialFLUXRedux,
|
||||
@@ -65,13 +67,13 @@ const slice = createSlice({
|
||||
state.entities.push(...entities);
|
||||
}
|
||||
},
|
||||
refImageImageChanged: (state, action: PayloadActionWithId<{ imageDTO: ImageDTO | null }>) => {
|
||||
const { id, imageDTO } = action.payload;
|
||||
refImageImageChanged: (state, action: PayloadActionWithId<{ croppableImage: CroppableImageWithDims | null }>) => {
|
||||
const { id, croppableImage } = action.payload;
|
||||
const entity = selectRefImageEntity(state, id);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
entity.config.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||
entity.config.image = croppableImage;
|
||||
},
|
||||
refImageIPAdapterMethodChanged: (state, action: PayloadActionWithId<{ method: IPMethodV2 }>) => {
|
||||
const { id, method } = action.payload;
|
||||
|
||||
@@ -43,16 +43,37 @@ const zCropBox = z.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
});
|
||||
export const zCroppableImageWithDims = z.object({
|
||||
original: zImageWithDims,
|
||||
crop: z
|
||||
.object({
|
||||
box: zCropBox,
|
||||
ratio: z.number().gt(0).nullable(),
|
||||
image: zImageWithDims,
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
// This new schema is an extension of zImageWithDims, with an optional crop field.
|
||||
//
|
||||
// When we added cropping support to certain entities (e.g. Ref Images, video Starting Frame Image), we changed
|
||||
// their schemas from using zImageWithDims to this new schema. To support loading pre-existing entities that
|
||||
// were created before cropping was supported, we can use zod's preprocess to transform old data into the new format.
|
||||
// Its essentially a data migration step.
|
||||
//
|
||||
// This parsing happens currently in two places:
|
||||
// - Recalling metadata.
|
||||
// - Loading/rehydrating persisted client state from storage.
|
||||
export const zCroppableImageWithDims = z.preprocess(
|
||||
(val) => {
|
||||
try {
|
||||
const imageWithDims = zImageWithDims.parse(val);
|
||||
const migrated = { original: { image: deepClone(imageWithDims) } };
|
||||
return migrated;
|
||||
} catch {
|
||||
return val;
|
||||
}
|
||||
},
|
||||
z.object({
|
||||
original: z.object({ image: zImageWithDims }),
|
||||
crop: z
|
||||
.object({
|
||||
box: zCropBox,
|
||||
ratio: z.number().gt(0).nullable(),
|
||||
image: zImageWithDims,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
);
|
||||
export type CroppableImageWithDims = z.infer<typeof zCroppableImageWithDims>;
|
||||
|
||||
const zImageWithDimsDataURL = z.object({
|
||||
@@ -253,7 +274,7 @@ export type CanvasObjectState = z.infer<typeof zCanvasObjectState>;
|
||||
|
||||
const zIPAdapterConfig = z.object({
|
||||
type: z.literal('ip_adapter'),
|
||||
image: zImageWithDims.nullable(),
|
||||
image: zCroppableImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
weight: z.number().gte(-1).lte(2),
|
||||
beginEndStepPct: zBeginEndStepPct,
|
||||
@@ -262,21 +283,39 @@ const zIPAdapterConfig = z.object({
|
||||
});
|
||||
export type IPAdapterConfig = z.infer<typeof zIPAdapterConfig>;
|
||||
|
||||
const zRegionalGuidanceIPAdapterConfig = z.object({
|
||||
type: z.literal('ip_adapter'),
|
||||
image: zImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
weight: z.number().gte(-1).lte(2),
|
||||
beginEndStepPct: zBeginEndStepPct,
|
||||
method: zIPMethodV2,
|
||||
clipVisionModel: zCLIPVisionModelV2,
|
||||
});
|
||||
export type RegionalGuidanceIPAdapterConfig = z.infer<typeof zRegionalGuidanceIPAdapterConfig>;
|
||||
|
||||
const zFLUXReduxImageInfluence = z.enum(['lowest', 'low', 'medium', 'high', 'highest']);
|
||||
export const isFLUXReduxImageInfluence = (v: unknown): v is FLUXReduxImageInfluence =>
|
||||
zFLUXReduxImageInfluence.safeParse(v).success;
|
||||
export type FLUXReduxImageInfluence = z.infer<typeof zFLUXReduxImageInfluence>;
|
||||
const zFLUXReduxConfig = z.object({
|
||||
type: z.literal('flux_redux'),
|
||||
image: zImageWithDims.nullable(),
|
||||
image: zCroppableImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
imageInfluence: zFLUXReduxImageInfluence.default('highest'),
|
||||
});
|
||||
export type FLUXReduxConfig = z.infer<typeof zFLUXReduxConfig>;
|
||||
const zRegionalGuidanceFLUXReduxConfig = z.object({
|
||||
type: z.literal('flux_redux'),
|
||||
image: zImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
imageInfluence: zFLUXReduxImageInfluence.default('highest'),
|
||||
});
|
||||
type RegionalGuidanceFLUXReduxConfig = z.infer<typeof zRegionalGuidanceFLUXReduxConfig>;
|
||||
|
||||
const zChatGPT4oReferenceImageConfig = z.object({
|
||||
type: z.literal('chatgpt_4o_reference_image'),
|
||||
image: zImageWithDims.nullable(),
|
||||
image: zCroppableImageWithDims.nullable(),
|
||||
/**
|
||||
* TODO(psyche): Technically there is no model for ChatGPT 4o reference images - it's just a field in the API call.
|
||||
* But we use a model drop down to switch between different ref image types, so there needs to be a model here else
|
||||
@@ -288,14 +327,14 @@ export type ChatGPT4oReferenceImageConfig = z.infer<typeof zChatGPT4oReferenceIm
|
||||
|
||||
const zGemini2_5ReferenceImageConfig = z.object({
|
||||
type: z.literal('gemini_2_5_reference_image'),
|
||||
image: zImageWithDims.nullable(),
|
||||
image: zCroppableImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
});
|
||||
export type Gemini2_5ReferenceImageConfig = z.infer<typeof zGemini2_5ReferenceImageConfig>;
|
||||
|
||||
const zFluxKontextReferenceImageConfig = z.object({
|
||||
type: z.literal('flux_kontext_reference_image'),
|
||||
image: zImageWithDims.nullable(),
|
||||
image: zCroppableImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
});
|
||||
export type FluxKontextReferenceImageConfig = z.infer<typeof zFluxKontextReferenceImageConfig>;
|
||||
@@ -325,6 +364,7 @@ export const isIPAdapterConfig = (config: RefImageState['config']): config is IP
|
||||
|
||||
export const isFLUXReduxConfig = (config: RefImageState['config']): config is FLUXReduxConfig =>
|
||||
config.type === 'flux_redux';
|
||||
|
||||
export const isChatGPT4oReferenceImageConfig = (
|
||||
config: RefImageState['config']
|
||||
): config is ChatGPT4oReferenceImageConfig => config.type === 'chatgpt_4o_reference_image';
|
||||
@@ -344,10 +384,18 @@ const zFill = z.object({ style: zFillStyle, color: zRgbColor });
|
||||
|
||||
const zRegionalGuidanceRefImageState = z.object({
|
||||
id: zId,
|
||||
config: z.discriminatedUnion('type', [zIPAdapterConfig, zFLUXReduxConfig]),
|
||||
config: z.discriminatedUnion('type', [zRegionalGuidanceIPAdapterConfig, zRegionalGuidanceFLUXReduxConfig]),
|
||||
});
|
||||
export type RegionalGuidanceRefImageState = z.infer<typeof zRegionalGuidanceRefImageState>;
|
||||
|
||||
export const isRegionalGuidanceIPAdapterConfig = (
|
||||
config: RegionalGuidanceRefImageState['config']
|
||||
): config is RegionalGuidanceIPAdapterConfig => config.type === 'ip_adapter';
|
||||
|
||||
export const isRegionalGuidanceFLUXReduxConfig = (
|
||||
config: RegionalGuidanceRefImageState['config']
|
||||
): config is RegionalGuidanceFLUXReduxConfig => config.type === 'flux_redux';
|
||||
|
||||
const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
|
||||
type: z.literal('regional_guidance'),
|
||||
position: zCoordinate,
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
IPAdapterConfig,
|
||||
RasterLayerAdjustments,
|
||||
RefImageState,
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
RgbColor,
|
||||
T2IAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
@@ -52,7 +53,7 @@ export const imageDTOToCroppableImage = (
|
||||
): CroppableImageWithDims => {
|
||||
const { image_name, width, height } = originalImageDTO;
|
||||
const val: CroppableImageWithDims = {
|
||||
original: { image_name, width, height },
|
||||
original: { image: { image_name, width, height } },
|
||||
};
|
||||
if (crop) {
|
||||
val.crop = deepClone(crop);
|
||||
@@ -95,6 +96,15 @@ export const initialIPAdapter: IPAdapterConfig = {
|
||||
clipVisionModel: 'ViT-H',
|
||||
weight: 1,
|
||||
};
|
||||
export const initialRegionalGuidanceIPAdapter: RegionalGuidanceIPAdapterConfig = {
|
||||
type: 'ip_adapter',
|
||||
image: null,
|
||||
model: null,
|
||||
beginEndStepPct: [0, 1],
|
||||
method: 'full',
|
||||
clipVisionModel: 'ViT-H',
|
||||
weight: 1,
|
||||
};
|
||||
export const initialFLUXRedux: FLUXReduxConfig = {
|
||||
type: 'flux_redux',
|
||||
image: null,
|
||||
|
||||
@@ -236,8 +236,11 @@ const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, image
|
||||
|
||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, image_name: string) => {
|
||||
selectReferenceImageEntities(state).forEach((entity) => {
|
||||
if (entity.config.image?.image_name === image_name) {
|
||||
dispatch(refImageImageChanged({ id: entity.id, imageDTO: null }));
|
||||
if (
|
||||
entity.config.image?.original.image.image_name === image_name ||
|
||||
entity.config.image?.crop?.image.image_name === image_name
|
||||
) {
|
||||
dispatch(refImageImageChanged({ id: entity.id, croppableImage: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -284,7 +287,10 @@ export const getImageUsage = (
|
||||
|
||||
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
|
||||
|
||||
const isReferenceImage = refImages.entities.some(({ config }) => config.image?.image_name === image_name);
|
||||
const isReferenceImage = refImages.entities.some(
|
||||
({ config }) =>
|
||||
config.image?.original.image.image_name === image_name || config.image?.crop?.image.image_name === image_name
|
||||
);
|
||||
|
||||
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerH
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageDTOToCroppableImage } from 'features/controlLayers/store/util';
|
||||
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import {
|
||||
@@ -211,7 +211,7 @@ export const addGlobalReferenceImageDndTarget: DndTarget<
|
||||
handler: ({ sourceData, dispatch, getState }) => {
|
||||
const { imageDTO } = sourceData.payload;
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
config.image = imageDTOToCroppableImage(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageDTOToCroppableImage } from 'features/controlLayers/store/util';
|
||||
import { useItemDTOContextImageOnly } from 'features/gallery/contexts/ItemDTOContext';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -17,7 +17,7 @@ export const ContextMenuItemUseAsRefImage = memo(() => {
|
||||
const onClickNewGlobalReferenceImageFromImage = useCallback(() => {
|
||||
const { dispatch, getState } = store;
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
config.image = imageDTOToCroppableImage(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
|
||||
@@ -26,7 +26,12 @@ import type {
|
||||
CanvasRasterLayerState,
|
||||
CanvasRegionalGuidanceState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
|
||||
import {
|
||||
imageDTOToCroppableImage,
|
||||
imageDTOToImageObject,
|
||||
imageDTOToImageWithDims,
|
||||
initialControlNet,
|
||||
} from 'features/controlLayers/store/util';
|
||||
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
@@ -44,7 +49,7 @@ import { assert } from 'tsafe';
|
||||
|
||||
export const setGlobalReferenceImage = (arg: { imageDTO: ImageDTO; id: string; dispatch: AppDispatch }) => {
|
||||
const { imageDTO, id, dispatch } = arg;
|
||||
dispatch(refImageImageChanged({ id, imageDTO }));
|
||||
dispatch(refImageImageChanged({ id, croppableImage: imageDTOToCroppableImage(imageDTO) }));
|
||||
};
|
||||
|
||||
export const setRegionalGuidanceReferenceImage = (arg: {
|
||||
|
||||
@@ -975,7 +975,7 @@ const RefImages: CollectionMetadataHandler<RefImageState[]> = {
|
||||
|
||||
for (const refImage of parsed) {
|
||||
if (refImage.config.image) {
|
||||
await throwIfImageDoesNotExist(refImage.config.image.image_name, store);
|
||||
await throwIfImageDoesNotExist(refImage.config.image.original.image.image_name, store);
|
||||
}
|
||||
if (refImage.config.model) {
|
||||
await throwIfModelDoesNotExist(refImage.config.model.key, store);
|
||||
|
||||
@@ -87,7 +87,7 @@ const addFLUXRedux = (id: string, ipAdapter: FLUXReduxConfig, g: Graph, collecto
|
||||
type: 'flux_redux',
|
||||
redux_model: fluxReduxModel,
|
||||
image: {
|
||||
image_name: image.image_name,
|
||||
image_name: image.crop?.image.image_name ?? image.original.image.image_name,
|
||||
},
|
||||
...IMAGE_INFLUENCE_TO_SETTINGS[ipAdapter.imageInfluence ?? 'highest'],
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ const addIPAdapter = (id: string, ipAdapter: IPAdapterConfig, g: Graph, collecto
|
||||
begin_step_percent: beginEndStepPct[0],
|
||||
end_step_percent: beginEndStepPct[1],
|
||||
image: {
|
||||
image_name: image.image_name,
|
||||
image_name: image.crop?.image.image_name ?? image.original.image.image_name,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -77,7 +77,7 @@ const addIPAdapter = (id: string, ipAdapter: IPAdapterConfig, g: Graph, collecto
|
||||
begin_step_percent: beginEndStepPct[0],
|
||||
end_step_percent: beginEndStepPct[1],
|
||||
image: {
|
||||
image_name: image.image_name,
|
||||
image_name: image.crop?.image.image_name ?? image.original.image.image_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
type CanvasRegionalGuidanceState,
|
||||
isFLUXReduxConfig,
|
||||
isIPAdapterConfig,
|
||||
isRegionalGuidanceFLUXReduxConfig,
|
||||
isRegionalGuidanceIPAdapterConfig,
|
||||
type Rect,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getRegionalGuidanceWarnings } from 'features/controlLayers/store/validators';
|
||||
@@ -279,7 +279,7 @@ export const addRegions = async ({
|
||||
}
|
||||
|
||||
for (const { id, config } of region.referenceImages) {
|
||||
if (isIPAdapterConfig(config)) {
|
||||
if (isRegionalGuidanceIPAdapterConfig(config)) {
|
||||
assert(!isFLUX, 'Regional IP adapters are not supported for FLUX.');
|
||||
|
||||
result.addedIPAdapters++;
|
||||
@@ -304,7 +304,7 @@ export const addRegions = async ({
|
||||
// Connect the mask to the conditioning
|
||||
g.addEdge(maskToTensor, 'mask', ipAdapterNode, 'mask');
|
||||
g.addEdge(ipAdapterNode, 'ip_adapter', ipAdapterCollect, 'item');
|
||||
} else if (isFLUXReduxConfig(config)) {
|
||||
} else if (isRegionalGuidanceFLUXReduxConfig(config)) {
|
||||
assert(isFLUX, 'Regional FLUX Redux requires FLUX.');
|
||||
assert(fluxReduxCollect !== null, 'FLUX Redux collector is required.');
|
||||
result.addedFLUXReduxes++;
|
||||
|
||||
@@ -50,7 +50,7 @@ export const buildChatGPT4oGraph = async (arg: GraphBuilderArg): Promise<GraphBu
|
||||
for (const entity of validRefImages) {
|
||||
assert(entity.config.image, 'Image is required for reference image');
|
||||
reference_images.push({
|
||||
image_name: entity.config.image.image_name,
|
||||
image_name: entity.config.image.crop?.image.image_name ?? entity.config.image.original.image.image_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const buildFluxKontextGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
aspect_ratio: aspectRatio.id,
|
||||
prompt_upsampling: true,
|
||||
input_image: {
|
||||
image_name: firstImage.image_name,
|
||||
image_name: firstImage.crop?.image.image_name ?? firstImage.original.image.image_name,
|
||||
},
|
||||
...selectCanvasOutputFields(state),
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export const buildGemini2_5Graph = (arg: GraphBuilderArg): GraphBuilderReturn =>
|
||||
for (const entity of validRefImages) {
|
||||
assert(entity.config.image, 'Image is required for reference image');
|
||||
reference_images.push({
|
||||
image_name: entity.config.image.image_name,
|
||||
image_name: entity.config.image.crop?.image.image_name ?? entity.config.image.original.image.image_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ const getReasonsWhyCannotEnqueueVideoTab = (arg: {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||
}
|
||||
|
||||
if (video.videoModel?.base === 'runway' && !video.startingFrameImage?.original.image_name) {
|
||||
if (video.videoModel?.base === 'runway' && !video.startingFrameImage?.original.image.image_name) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noStartingFrameImage') });
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ export const StartingFrameImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const requiresStartingFrame = useAppSelector(selectVideoModelRequiresStartingFrame);
|
||||
const startingFrameImage = useAppSelector(selectStartingFrameImage);
|
||||
const originalImageDTO = useImageDTO(startingFrameImage?.original.image_name);
|
||||
const originalImageDTO = useImageDTO(startingFrameImage?.original.image.image_name);
|
||||
const croppedImageDTO = useImageDTO(startingFrameImage?.crop?.image.image_name);
|
||||
const videoAspectRatio = useAppSelector(selectVideoAspectRatio);
|
||||
const [uploadImage] = useUploadImageMutation({ fixedCacheKey: 'editorContainer' });
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(startingFrameImageChanged(null));
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppStore } from 'app/store/storeHooks';
|
||||
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
|
||||
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { refImageAdded } from 'features/controlLayers/store/refImagesSlice';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { imageDTOToCroppableImage } from 'features/controlLayers/store/util';
|
||||
import { addGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { LaunchpadButton } from 'features/ui/layouts/LaunchpadButton';
|
||||
@@ -23,7 +23,7 @@ export const LaunchpadAddStyleReference = memo((props: { extraAction?: () => voi
|
||||
({
|
||||
onUpload: (imageDTO: ImageDTO) => {
|
||||
const config = getDefaultRefImageConfig(getState);
|
||||
config.image = imageDTOToImageWithDims(imageDTO);
|
||||
config.image = imageDTOToCroppableImage(imageDTO);
|
||||
dispatch(refImageAdded({ overrides: { config } }));
|
||||
props.extraAction?.();
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import type { CroppableImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { components, paths } from 'services/api/schema';
|
||||
import type {
|
||||
@@ -593,3 +594,10 @@ export const useImageDTO = (imageName: string | null | undefined) => {
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
return imageDTO ?? null;
|
||||
};
|
||||
|
||||
export const useImageDTOFromCroppableImage = (croppableImage: CroppableImageWithDims | null) => {
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(
|
||||
croppableImage?.crop?.image.image_name ?? croppableImage?.original.image.image_name ?? skipToken
|
||||
);
|
||||
return imageDTO ?? null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user