diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
index fb4ffbca7c..cb93b9a255 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
@@ -4,6 +4,7 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import { parseify } from 'common/util/serialize';
import {
caImageChanged,
+ iiImageChanged,
ipaImageChanged,
layerImageAdded,
rgIPAdapterImageChanged,
@@ -110,6 +111,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
+ /**
+ * Image dropped on Raster layer
+ */
+ if (
+ overData.actionType === 'SET_INITIAL_IMAGE' &&
+ activeData.payloadType === 'IMAGE_DTO' &&
+ activeData.payload.imageDTO
+ ) {
+ dispatch(iiImageChanged({ imageDTO: activeData.payload.imageDTO }));
+ return;
+ }
+
/**
* Image dropped on node image field
*/
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx
index 4c8572a5f9..94e706b68a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx
@@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
+import { InitialImage } from 'features/controlLayers/components/InitialImage/InitialImage';
import { IM } from 'features/controlLayers/components/InpaintMask/IM';
import { memo } from 'react';
@@ -17,6 +18,7 @@ export const ControlLayersPanelContent = memo(() => {
{isCanvasSessionActive && }
+ {!isCanvasSessionActive && }
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx
new file mode 100644
index 0000000000..00fdc673c0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImage.tsx
@@ -0,0 +1,25 @@
+import { useDisclosure } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
+import { InitialImageHeader } from 'features/controlLayers/components/InitialImage/InitialImageHeader';
+import { InitialImageSettings } from 'features/controlLayers/components/InitialImage/InitialImageSettings';
+import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
+import { memo, useCallback } from 'react';
+
+export const InitialImage = memo(() => {
+ const dispatch = useAppDispatch();
+ const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'initial_image');
+ const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
+ const onSelect = useCallback(() => {
+ dispatch(entitySelected({ id: 'initial_image', type: 'initial_image' }));
+ }, [dispatch]);
+
+ return (
+
+
+ {isOpen && }
+
+ );
+});
+
+InitialImage.displayName = 'InitialImage';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageActionsMenu.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx
new file mode 100644
index 0000000000..8af3e98f40
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageHeader.tsx
@@ -0,0 +1,34 @@
+import { Spacer } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
+import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
+import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
+import { iiIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+ onToggleVisibility: () => void;
+};
+
+export const InitialImageHeader = memo(({ onToggleVisibility }: Props) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const isEnabled = useAppSelector((s) => s.canvasV2.initialImage.isEnabled);
+ const onToggleIsEnabled = useCallback(() => {
+ dispatch(iiIsEnabledToggled());
+ }, [dispatch]);
+ const title = useMemo(() => {
+ return `${t('controlLayers.initialImage')}`;
+ }, [t]);
+
+ return (
+
+
+
+
+
+ );
+});
+
+InitialImageHeader.displayName = 'InitialImageHeader';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx
new file mode 100644
index 0000000000..248c09a660
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImagePreview.tsx
@@ -0,0 +1,100 @@
+import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import IAIDndImage from 'common/components/IAIDndImage';
+import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
+import { documentHeightChanged, documentWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice';
+import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
+import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types';
+import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
+import { memo, useCallback, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
+import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+
+export const InitialImagePreview = memo(() => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const initialImage = useAppSelector((s) => s.canvasV2.initialImage);
+ const isConnected = useAppSelector((s) => s.system.isConnected);
+ const optimalDimension = useAppSelector(selectOptimalDimension);
+ const shift = useShiftModifier();
+
+ const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(
+ initialImage.imageObject?.image.name ?? skipToken
+ );
+
+ const onReset = useCallback(() => {
+ dispatch(iiReset());
+ }, [dispatch]);
+
+ const onUseSize = useCallback(() => {
+ if (!imageDTO) {
+ return;
+ }
+
+ const options = { updateAspectRatio: true, clamp: true };
+ if (shift) {
+ const { width, height } = imageDTO;
+ dispatch(documentWidthChanged({ width, ...options }));
+ dispatch(documentHeightChanged({ height, ...options }));
+ } else {
+ const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension);
+ dispatch(documentWidthChanged({ width, ...options }));
+ dispatch(documentHeightChanged({ height, ...options }));
+ }
+ }, [imageDTO, dispatch, optimalDimension, shift]);
+
+ const draggableData = useMemo(() => {
+ if (imageDTO) {
+ return {
+ id: 'initial_image',
+ payloadType: 'IMAGE_DTO',
+ payload: { imageDTO },
+ };
+ }
+ }, [imageDTO]);
+
+ const droppableData = useMemo(
+ () => ({ id: 'initial_image', actionType: 'SET_INITIAL_IMAGE' }),
+ []
+ );
+
+ useEffect(() => {
+ if (isConnected && isErrorControlImage) {
+ onReset();
+ }
+ }, [onReset, isConnected, isErrorControlImage]);
+
+ return (
+
+
+
+
+ {imageDTO && (
+
+ }
+ tooltip={t('controlnet.resetControlImage')}
+ />
+ }
+ tooltip={
+ shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
+ }
+ />
+
+ )}
+
+
+ );
+});
+
+InitialImagePreview.displayName = 'InitialImagePreview';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx
new file mode 100644
index 0000000000..9c9da2f536
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InitialImage/InitialImageSettings.tsx
@@ -0,0 +1,13 @@
+import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
+import { InitialImagePreview } from 'features/controlLayers/components/InitialImage/InitialImagePreview';
+import { memo } from 'react';
+
+export const InitialImageSettings = memo(() => {
+ return (
+
+
+
+ );
+});
+
+InitialImageSettings.displayName = 'InitialImageSettings';
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts
new file mode 100644
index 0000000000..e09fc42855
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInitialImage.ts
@@ -0,0 +1,73 @@
+import { CanvasImage } from 'features/controlLayers/konva/CanvasImage';
+import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
+import { getObjectGroupId } from 'features/controlLayers/konva/naming';
+import type { InitialImageEntity } from 'features/controlLayers/store/types';
+import Konva from 'konva';
+import { v4 as uuidv4 } from 'uuid';
+
+export class CanvasInitialImage {
+ id = 'initial_image';
+ manager: CanvasManager;
+ layer: Konva.Layer;
+ group: Konva.Group;
+ objectsGroup: Konva.Group;
+ image: CanvasImage | null;
+ private initialImageState: InitialImageEntity;
+
+ constructor(initialImageState: InitialImageEntity, manager: CanvasManager) {
+ this.manager = manager;
+ this.layer = new Konva.Layer({
+ id: this.id,
+ imageSmoothingEnabled: true,
+ listening: false,
+ });
+ this.group = new Konva.Group({
+ id: getObjectGroupId(this.layer.id(), uuidv4()),
+ listening: false,
+ });
+ this.objectsGroup = new Konva.Group({ listening: false });
+ this.group.add(this.objectsGroup);
+ this.layer.add(this.group);
+
+ this.image = null;
+ this.initialImageState = initialImageState;
+ }
+
+ async render(initialImageState: InitialImageEntity) {
+ this.initialImageState = initialImageState;
+
+ if (!this.initialImageState.imageObject) {
+ this.layer.visible(false);
+ return;
+ }
+
+ const imageObject = this.initialImageState.imageObject;
+
+ if (!imageObject) {
+ if (this.image) {
+ this.image.konvaImageGroup.visible(false);
+ }
+ } else if (!this.image) {
+ this.image = await new CanvasImage(imageObject, {
+ onLoad: () => {
+ this.updateGroup();
+ },
+ });
+ this.objectsGroup.add(this.image.konvaImageGroup);
+ await this.image.updateImageSource(imageObject.image.name);
+ } else if (!this.image.isLoading && !this.image.isError) {
+ await this.image.update(imageObject);
+ }
+
+ this.updateGroup();
+ }
+
+ updateGroup() {
+ const visible = this.initialImageState ? this.initialImageState.isEnabled : false;
+ this.layer.visible(visible);
+ }
+
+ destroy(): void {
+ this.layer.destroy();
+ }
+}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts
index 4dcd7be36b..b30dae3f5d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasInpaintMask.ts
@@ -13,7 +13,7 @@ import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
export class CanvasInpaintMask {
- id: string;
+ id = 'inpaint_mask';
manager: CanvasManager;
layer: Konva.Layer;
group: Konva.Group;
@@ -25,7 +25,6 @@ export class CanvasInpaintMask {
private inpaintMaskState: InpaintMaskEntity;
constructor(entity: InpaintMaskEntity, manager: CanvasManager) {
- this.id = 'inpaint_mask';
this.manager = manager;
this.layer = new Konva.Layer({ id: this.id });
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
index 804faa45e4..17b5394602 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
@@ -1,15 +1,17 @@
import type { Store } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
+import { CanvasInitialImage } from 'features/controlLayers/konva/CanvasInitialImage';
import {
+ getCompositeLayerImage,
getControlAdapterImage,
getGenerationMode,
- getImageSourceImage,
+ getInitialImage,
getInpaintMaskImage,
getRegionMaskImage,
} from 'features/controlLayers/konva/util';
import { $lastProgressEvent, $shouldShowStagedImage } from 'features/controlLayers/store/canvasV2Slice';
-import type { CanvasV2State } from 'features/controlLayers/store/types';
+import type { CanvasV2State, GenerationMode } from 'features/controlLayers/store/types';
import type Konva from 'konva';
import { atom } from 'nanostores';
import { getImageDTO as defaultGetImageDTO, uploadImage as defaultUploadImage } from 'services/api/endpoints/images';
@@ -58,6 +60,7 @@ export class CanvasManager {
layers: Map;
regions: Map;
inpaintMask: CanvasInpaintMask;
+ initialImage: CanvasInitialImage;
util: Util;
stateApi: CanvasStateApi;
preview: CanvasPreview;
@@ -102,6 +105,13 @@ export class CanvasManager {
this.layers = new Map();
this.regions = new Map();
this.controlAdapters = new Map();
+
+ this.initialImage = new CanvasInitialImage(this.stateApi.getInitialImageState(), this);
+ this.stage.add(this.initialImage.layer);
+ }
+
+ async renderInitialImage() {
+ this.initialImage.render(this.stateApi.getInitialImageState());
}
async renderLayers() {
@@ -180,6 +190,7 @@ export class CanvasManager {
const regions = getRegionsState().entities;
let zIndex = 0;
this.background.layer.zIndex(++zIndex);
+ this.initialImage.layer.zIndex(++zIndex);
for (const layer of layers) {
this.layers.get(layer.id)?.layer.zIndex(++zIndex);
}
@@ -225,6 +236,17 @@ export class CanvasManager {
this.renderLayers();
}
+ if (
+ this.isFirstRender ||
+ state.initialImage !== this.prevState.initialImage ||
+ state.document !== this.prevState.document ||
+ state.tool.selected !== this.prevState.tool.selected ||
+ state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
+ ) {
+ log.debug('Rendering intial image');
+ this.renderInitialImage();
+ }
+
if (
this.isFirstRender ||
state.regions.entities !== this.prevState.regions.entities ||
@@ -367,8 +389,19 @@ export class CanvasManager {
}
};
- getGenerationMode() {
- return getGenerationMode({ manager: this });
+ getGenerationMode(): GenerationMode {
+ const session = this.stateApi.getSession();
+ if (session.isActive) {
+ return getGenerationMode({ manager: this });
+ }
+
+ const initialImageState = this.stateApi.getInitialImageState();
+
+ if (initialImageState.imageObject && initialImageState.isEnabled) {
+ return 'img2img';
+ }
+
+ return 'txt2img';
}
getControlAdapterImage(arg: Omit[0], 'manager'>) {
@@ -383,7 +416,11 @@ export class CanvasManager {
return getInpaintMaskImage({ ...arg, manager: this });
}
- getImageSourceImage(arg: Omit[0], 'manager'>) {
- return getImageSourceImage({ ...arg, manager: this });
+ getInitialImage(arg: Omit[0], 'manager'>) {
+ if (this.stateApi.getSession().isActive) {
+ return getCompositeLayerImage({ ...arg, manager: this });
+ } else {
+ return getInitialImage({ ...arg, manager: this });
+ }
}
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
index d485290c15..cac8dcce27 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts
@@ -228,6 +228,9 @@ export class CanvasStateApi {
getInpaintMaskState = () => {
return this.getState().inpaintMask;
};
+ getInitialImageState = () => {
+ return this.getState().initialImage;
+ };
getMaskOpacity = () => {
return this.getState().settings.maskOpacity;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
index e729306ca0..103ef2811d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
@@ -317,6 +317,23 @@ export function getControlAdapterLayerClone(arg: { manager: CanvasManager; id: s
return controlAdapterClone;
}
+export function getInitialImageLayerClone(arg: { manager: CanvasManager }): Konva.Layer {
+ const { manager } = arg;
+
+ const initialImage = manager.initialImage;
+
+ const initialImageClone = initialImage.layer.clone();
+ const objectGroupClone = initialImage.group.clone();
+
+ initialImageClone.destroyChildren();
+ initialImageClone.add(objectGroupClone);
+
+ objectGroupClone.opacity(1);
+ objectGroupClone.cache();
+
+ return initialImageClone;
+}
+
export function getCompositeLayerStageClone(arg: { manager: CanvasManager }): Konva.Stage {
const { manager } = arg;
@@ -435,6 +452,34 @@ export async function getControlAdapterImage(arg: {
return imageDTO;
}
+export async function getInitialImage(arg: {
+ manager: CanvasManager;
+ bbox?: Rect;
+ preview?: boolean;
+}): Promise {
+ const { manager, bbox, preview = false } = arg;
+
+ // if (region.imageCache) {
+ // const imageDTO = await this.util.getImageDTO(region.imageCache.name);
+ // if (imageDTO) {
+ // return imageDTO;
+ // }
+ // }
+
+ const layerClone = getInitialImageLayerClone({ manager });
+ const blob = await konvaNodeToBlob(layerClone, bbox);
+
+ if (preview) {
+ previewBlob(blob, 'initial image');
+ }
+
+ layerClone.destroy();
+
+ const imageDTO = await manager.util.uploadImage(blob, 'initial_image.png', 'other', true);
+ // manager.stateApi.onRegionMaskImageCached(ca.id, imageDTO);
+ return imageDTO;
+}
+
export async function getInpaintMaskImage(arg: {
manager: CanvasManager;
bbox?: Rect;
@@ -464,7 +509,7 @@ export async function getInpaintMaskImage(arg: {
return imageDTO;
}
-export async function getImageSourceImage(arg: {
+export async function getCompositeLayerImage(arg: {
manager: CanvasManager;
bbox?: Rect;
preview?: boolean;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index a19556ed3a..28781563fd 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -6,6 +6,7 @@ import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
import { documentReducers } from 'features/controlLayers/store/documentReducers';
+import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers';
import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers';
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
import { layersReducers } from 'features/controlLayers/store/layersReducers';
@@ -30,6 +31,14 @@ const initialState: CanvasV2State = {
ipAdapters: { entities: [] },
regions: { entities: [] },
loras: [],
+ initialImage: {
+ id: 'initial_image',
+ type: 'initial_image',
+ bbox: null,
+ bboxNeedsUpdate: false,
+ isEnabled: true,
+ imageObject: null,
+ },
inpaintMask: {
id: 'inpaint_mask',
type: 'inpaint_mask',
@@ -141,6 +150,7 @@ export const canvasV2Slice = createSlice({
...inpaintMaskReducers,
...sessionReducers,
...documentReducers,
+ ...initialImageReducers,
entitySelected: (state, action: PayloadAction) => {
state.selectedEntityIdentifier = action.payload;
},
@@ -338,6 +348,11 @@ export const {
sessionStagingCanceled,
sessionNextStagedImageSelected,
sessionPrevStagedImageSelected,
+ // Initial image
+ iiRecalled,
+ iiIsEnabledToggled,
+ iiReset,
+ iiImageChanged,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts
index 16fb34f7c0..a4649ca541 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/documentReducers.ts
@@ -28,6 +28,11 @@ export const documentReducers = {
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
+
+ if (state.initialImage.imageObject) {
+ state.initialImage.imageObject.width = state.document.rect.width;
+ state.initialImage.imageObject.height = state.document.rect.height;
+ }
}
},
documentHeightChanged: (
@@ -51,6 +56,11 @@ export const documentReducers = {
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
+
+ if (state.initialImage.imageObject) {
+ state.initialImage.imageObject.width = state.document.rect.width;
+ state.initialImage.imageObject.height = state.document.rect.height;
+ }
}
},
documentAspectRatioLockToggled: (state) => {
@@ -74,6 +84,11 @@ export const documentReducers = {
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
+
+ if (state.initialImage.imageObject) {
+ state.initialImage.imageObject.width = state.document.rect.width;
+ state.initialImage.imageObject.height = state.document.rect.height;
+ }
}
},
documentDimensionsSwapped: (state) => {
@@ -95,6 +110,11 @@ export const documentReducers = {
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
+
+ if (state.initialImage.imageObject) {
+ state.initialImage.imageObject.width = state.document.rect.width;
+ state.initialImage.imageObject.height = state.document.rect.height;
+ }
}
},
documentSizeOptimized: (state) => {
@@ -111,6 +131,11 @@ export const documentReducers = {
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
+
+ if (state.initialImage.imageObject) {
+ state.initialImage.imageObject.width = state.document.rect.width;
+ state.initialImage.imageObject.height = state.document.rect.height;
+ }
}
},
} satisfies SliceCaseReducers;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts
new file mode 100644
index 0000000000..b30af45ab5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/store/initialImageReducers.ts
@@ -0,0 +1,38 @@
+import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
+import { isEqual } from 'lodash-es';
+import type { ImageDTO } from 'services/api/types';
+
+import type { CanvasV2State, InitialImageEntity } from './types';
+import { imageDTOToImageObject } from './types';
+
+export const initialImageReducers = {
+ iiRecalled: (state, action: PayloadAction<{ data: InitialImageEntity }>) => {
+ const { data } = action.payload;
+ state.initialImage = data;
+ state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' };
+ },
+ iiIsEnabledToggled: (state) => {
+ if (!state.initialImage) {
+ return;
+ }
+ state.initialImage.isEnabled = !state.initialImage.isEnabled;
+ },
+ iiReset: (state) => {
+ state.initialImage.imageObject = null;
+ },
+ iiImageChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => {
+ const { imageDTO } = action.payload;
+ if (!state.initialImage) {
+ return;
+ }
+ const newImageObject = imageDTOToImageObject('initial_image', 'initial_image_object', imageDTO);
+ if (isEqual(newImageObject, state.initialImage.imageObject)) {
+ return;
+ }
+ state.initialImage.bbox = null;
+ state.initialImage.bboxNeedsUpdate = true;
+ state.initialImage.isEnabled = true;
+ state.initialImage.imageObject = newImageObject;
+ state.selectedEntityIdentifier = { type: 'initial_image', id: 'initial_image' };
+ },
+} satisfies SliceCaseReducers;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index f215e1b194..6a1ea536f7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -668,6 +668,16 @@ const zInpaintMaskEntity = z.object({
});
export type InpaintMaskEntity = z.infer;
+const zInitialImageEntity = z.object({
+ id: z.literal('initial_image'),
+ type: z.literal('initial_image'),
+ isEnabled: z.boolean(),
+ bbox: zRect.nullable(),
+ bboxNeedsUpdate: z.boolean(),
+ imageObject: zImageObject.nullable(),
+});
+export type InitialImageEntity = z.infer;
+
const zControlAdapterEntityBase = z.object({
id: zId,
type: z.literal('control_adapter'),
@@ -790,7 +800,13 @@ export type BoundingBoxScaleMethod = z.infer;
export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod =>
zBoundingBoxScaleMethod.safeParse(v).success;
-export type CanvasEntity = LayerEntity | ControlAdapterEntity | RegionEntity | InpaintMaskEntity | IPAdapterEntity;
+export type CanvasEntity =
+ | LayerEntity
+ | ControlAdapterEntity
+ | RegionEntity
+ | InpaintMaskEntity
+ | IPAdapterEntity
+ | InitialImageEntity;
export type CanvasEntityIdentifier = Pick;
export type Size = {
@@ -822,6 +838,7 @@ export type CanvasV2State = {
ipAdapters: { entities: IPAdapterEntity[] };
regions: { entities: RegionEntity[] };
loras: LoRA[];
+ initialImage: InitialImageEntity;
tool: {
selected: Tool;
selectedBuffer: Tool | null;
diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts
index 692454ea50..b041546ec8 100644
--- a/invokeai/frontend/web/src/features/dnd/types/index.ts
+++ b/invokeai/frontend/web/src/features/dnd/types/index.ts
@@ -66,6 +66,10 @@ type UpscaleInitialImageDropData = BaseDropData & {
actionType: 'SET_UPSCALE_INITIAL_IMAGE';
};
+export type InitialImageDropData = BaseDropData & {
+ actionType: 'SET_INITIAL_IMAGE';
+};
+
type NodesImageDropData = BaseDropData & {
actionType: 'SET_NODES_IMAGE';
context: {
@@ -101,7 +105,8 @@ export type TypesafeDroppableData =
| RGIPAdapterImageDropData
| SelectForCompareDropData
| RasterLayerImageDropData
- | UpscaleInitialImageDropData;
+ | UpscaleInitialImageDropData
+ | LayerImageDropData;
type BaseDragData = {
id: string;
diff --git a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
index 128e5c5d50..80ea701727 100644
--- a/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
+++ b/invokeai/frontend/web/src/features/dnd/util/isValidDrop.ts
@@ -29,6 +29,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?
return payloadType === 'IMAGE_DTO';
case 'SELECT_FOR_COMPARE':
return payloadType === 'IMAGE_DTO';
+ case 'SET_INITIAL_IMAGE':
+ return payloadType === 'IMAGE_DTO';
case 'ADD_TO_BOARD': {
// If the board is the same, don't allow the drop
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
index 953e3505ef..2bac462f12 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts
@@ -17,8 +17,8 @@ export const addImageToImage = async (
): Promise> => {
denoise.denoising_start = denoising_start;
- const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
- const initialImage = await manager.getImageSourceImage({ bbox: cropBbox });
+ const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']);
+ const initialImage = await manager.getInitialImage({ bbox: cropBbox });
if (!isEqual(scaledSize, originalSize)) {
// Resize the initial image to the scaled size, denoise, then resize back to the original size
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
index 0b4520385f..fcdd49b3ff 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts
@@ -21,8 +21,8 @@ export const addInpaint = async (
): Promise> => {
denoise.denoising_start = denoising_start;
- const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
- const initialImage = await manager.getImageSourceImage({ bbox: cropBbox });
+ const cropBbox = pick(bbox.rect, ['x', 'y', 'width', 'height']);
+ const initialImage = await manager.getInitialImage({ bbox: cropBbox });
const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox });
if (!isEqual(scaledSize, originalSize)) {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
index e5c774d953..1dde41fb0c 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts
@@ -23,7 +23,7 @@ export const addOutpaint = async (
denoise.denoising_start = denoising_start;
const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
- const initialImage = await manager.getImageSourceImage({ bbox: cropBbox });
+ const initialImage = await manager.getInitialImage({ bbox: cropBbox });
const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox });
const infill = getInfill(g, compositing);
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index ab3fcef493..e2b7a33cf5 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -1,3 +1,4 @@
+import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
@@ -33,9 +34,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { addRegions } from './addRegions';
+const log = logger('system');
export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise => {
const generationMode = manager.getGenerationMode();
+ log.debug({ generationMode }, 'Building SD1/SD2 graph');
const { bbox, params } = state.canvasV2;
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index c6bb11b9ba..9b4490660d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -1,3 +1,4 @@
+import { logger } from 'app/logging/logger';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
@@ -32,9 +33,11 @@ import { isNonRefinerMainModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { addRegions } from './addRegions';
+const log = logger('system');
export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise => {
const generationMode = manager.getGenerationMode();
+ log.debug({ generationMode }, 'Building SDXL graph');
const { bbox, params } = state.canvasV2;