diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
index 71ac79b206..21f0fe1e53 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts
@@ -1,7 +1,11 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { getNodeManager } from 'features/controlLayers/konva/nodeManager';
-import { stagingAreaBatchIdAdded, stagingAreaInitialized } from 'features/controlLayers/store/canvasV2Slice';
+import {
+ stagingAreaBatchIdAdded,
+ stagingAreaInitialized,
+ stagingAreaReset,
+} from 'features/controlLayers/store/canvasV2Slice';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
@@ -19,47 +23,58 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
const model = state.canvasV2.params.model;
const { prepend } = action.payload;
- let g;
+ let didInitializeStagingArea = false;
- const manager = getNodeManager();
- assert(model, 'No model found in state');
- const base = model.base;
-
- if (base === 'sdxl') {
- g = await buildSDXLGraph(state, manager);
- } else if (base === 'sd-1' || base === 'sd-2') {
- g = await buildSD1Graph(state, manager);
- } else {
- assert(false, `No graph builders for base ${base}`);
+ if (state.canvasV2.stagingArea === null) {
+ dispatch(
+ stagingAreaInitialized({
+ batchIds: [],
+ bbox: state.canvasV2.bbox,
+ })
+ );
+ didInitializeStagingArea = true;
}
- const batchConfig = prepareLinearUIBatch(state, g, prepend);
-
- const req = dispatch(
- queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
- fixedCacheKey: 'enqueueBatch',
- })
- );
try {
+ let g;
+
+ const manager = getNodeManager();
+ assert(model, 'No model found in state');
+ const base = model.base;
+
+ if (base === 'sdxl') {
+ g = await buildSDXLGraph(state, manager);
+ } else if (base === 'sd-1' || base === 'sd-2') {
+ g = await buildSD1Graph(state, manager);
+ } else {
+ assert(false, `No graph builders for base ${base}`);
+ }
+
+ const batchConfig = prepareLinearUIBatch(state, g, prepend);
+
+ const req = dispatch(
+ queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
+ fixedCacheKey: 'enqueueBatch',
+ })
+ );
+
const enqueueResult = await req.unwrap();
+ req.reset();
+
if (shouldShowProgressInViewer) {
dispatch(isImageViewerOpenChanged(true));
}
// TODO(psyche): update the backend schema, this is always provided
const batchId = enqueueResult.batch.batch_id;
assert(batchId, 'No batch ID found in enqueue result');
- if (!state.canvasV2.stagingArea) {
- dispatch(
- stagingAreaInitialized({
- batchIds: [batchId],
- bbox: state.canvasV2.bbox,
- })
- );
- } else {
- dispatch(stagingAreaBatchIdAdded({ batchId }));
+ dispatch(stagingAreaBatchIdAdded({ batchId }));
+ } catch {
+ if (didInitializeStagingArea) {
+ // We initialized the staging area in this listener, and there was a problem at some point. This means
+ // there only possible canvas batch id is the one we just added, so we can reset the staging area without
+ // losing any data.
+ dispatch(stagingAreaReset());
}
- } finally {
- req.reset();
}
},
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
index 688be5f043..d6b14e9549 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx
@@ -43,6 +43,7 @@ export const ToolChooser: React.FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
+ const isStaging = useAppSelector((s) => s.canvasV2.stagingArea !== null);
const isDrawingToolDisabled = useMemo(
() => !getIsDrawingToolEnabled(selectedEntityIdentifier),
[selectedEntityIdentifier]
@@ -53,19 +54,35 @@ export const ToolChooser: React.FC = () => {
const setToolToBrush = useCallback(() => {
dispatch(toolChanged('brush'));
}, [dispatch]);
- useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToBrush]);
+ useHotkeys('b', setToolToBrush, { enabled: !isDrawingToolDisabled && !isStaging }, [
+ isDrawingToolDisabled,
+ isStaging,
+ setToolToBrush,
+ ]);
const setToolToEraser = useCallback(() => {
dispatch(toolChanged('eraser'));
}, [dispatch]);
- useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToEraser]);
+ useHotkeys('e', setToolToEraser, { enabled: !isDrawingToolDisabled && !isStaging }, [
+ isDrawingToolDisabled,
+ isStaging,
+ setToolToEraser,
+ ]);
const setToolToRect = useCallback(() => {
dispatch(toolChanged('rect'));
}, [dispatch]);
- useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled }, [isDrawingToolDisabled, setToolToRect]);
+ useHotkeys('u', setToolToRect, { enabled: !isDrawingToolDisabled && !isStaging }, [
+ isDrawingToolDisabled,
+ isStaging,
+ setToolToRect,
+ ]);
const setToolToMove = useCallback(() => {
dispatch(toolChanged('move'));
}, [dispatch]);
- useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled }, [isMoveToolDisabled, setToolToMove]);
+ useHotkeys('v', setToolToMove, { enabled: !isMoveToolDisabled && !isStaging }, [
+ isMoveToolDisabled,
+ isStaging,
+ setToolToMove,
+ ]);
const setToolToView = useCallback(() => {
dispatch(toolChanged('view'));
}, [dispatch]);
@@ -92,12 +109,16 @@ export const ToolChooser: React.FC = () => {
}, [dispatch, selectedEntityIdentifier]);
const isResetEnabled = useMemo(
() =>
- selectedEntityIdentifier?.type === 'layer' ||
+ (!isStaging && selectedEntityIdentifier?.type === 'layer') ||
selectedEntityIdentifier?.type === 'regional_guidance' ||
selectedEntityIdentifier?.type === 'inpaint_mask',
- [selectedEntityIdentifier]
+ [isStaging, selectedEntityIdentifier?.type]
);
- useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [isResetEnabled, resetSelectedLayer]);
+ useHotkeys('shift+c', resetSelectedLayer, { enabled: isResetEnabled }, [
+ isResetEnabled,
+ isStaging,
+ resetSelectedLayer,
+ ]);
const deleteSelectedLayer = useCallback(() => {
if (selectedEntityIdentifier === null) {
@@ -117,7 +138,10 @@ export const ToolChooser: React.FC = () => {
dispatch(ipaDeleted({ id }));
}
}, [dispatch, selectedEntityIdentifier]);
- const isDeleteEnabled = useMemo(() => selectedEntityIdentifier !== null, [selectedEntityIdentifier]);
+ const isDeleteEnabled = useMemo(
+ () => selectedEntityIdentifier !== null && !isStaging,
+ [selectedEntityIdentifier, isStaging]
+ );
useHotkeys('shift+d', deleteSelectedLayer, { enabled: isDeleteEnabled }, [isDeleteEnabled, deleteSelectedLayer]);
return (
@@ -128,7 +152,7 @@ export const ToolChooser: React.FC = () => {
icon={}
variant={tool === 'brush' ? 'solid' : 'outline'}
onClick={setToolToBrush}
- isDisabled={isDrawingToolDisabled}
+ isDisabled={isDrawingToolDisabled || isStaging}
/>
{
icon={}
variant={tool === 'eraser' ? 'solid' : 'outline'}
onClick={setToolToEraser}
- isDisabled={isDrawingToolDisabled}
+ isDisabled={isDrawingToolDisabled || isStaging}
/>
{
icon={}
variant={tool === 'rect' ? 'solid' : 'outline'}
onClick={setToolToRect}
- isDisabled={isDrawingToolDisabled}
+ isDisabled={isDrawingToolDisabled || isStaging}
/>
{
icon={}
variant={tool === 'move' ? 'solid' : 'outline'}
onClick={setToolToMove}
- isDisabled={isMoveToolDisabled}
+ isDisabled={isMoveToolDisabled || isStaging}
/>
{
icon={}
variant={tool === 'view' ? 'solid' : 'outline'}
onClick={setToolToView}
+ isDisabled={isStaging}
/>
{
icon={}
variant={tool === 'bbox' ? 'solid' : 'outline'}
onClick={setToolToBbox}
+ isDisabled={isStaging}
/>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
index a7b079f08e..d7f4a04581 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
@@ -128,8 +128,6 @@ export const setStageEventHandlers = (manager: KonvaNodeManager): (() => void) =
//#region mouseenter
stage.on('mouseenter', () => {
- const tool = getToolState().selected;
- stage.findOne(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
manager.renderToolPreview();
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
index 4eb5cb1c7a..5e5ee6b4d7 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts
@@ -446,7 +446,12 @@ export const initializeRenderer = (
const unsubscribeRenderer = subscribe(renderCanvas);
// When we this flag, we need to render the staging area
- $shouldShowStagedImage.subscribe(manager.renderStagingArea.bind(manager));
+ $shouldShowStagedImage.subscribe((shouldShowStagedImage, prevShouldShowStagedImage) => {
+ logIfDebugging('Rendering staging area');
+ if (shouldShowStagedImage !== prevShouldShowStagedImage) {
+ manager.renderStagingArea();
+ }
+ });
logIfDebugging('First render of konva stage');
// On first render, the document should be fit to the stage.
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts
index d7b05d0cb3..38642b8f2e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/tool.ts
@@ -2,13 +2,12 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import {
BRUSH_BORDER_INNER_COLOR,
BRUSH_BORDER_OUTER_COLOR,
- BRUSH_ERASER_BORDER_WIDTH
+ BRUSH_ERASER_BORDER_WIDTH,
} from 'features/controlLayers/konva/constants';
import { PREVIEW_RECT_ID } from 'features/controlLayers/konva/naming';
import type { CanvasEntity, CanvasV2State, Position, RgbaColor } from 'features/controlLayers/store/types';
import Konva from 'konva';
-
export class CanvasTool {
group: Konva.Group;
brush: {
@@ -125,7 +124,8 @@ export class CanvasTool {
isMouseDown: boolean
) {
const tool = toolState.selected;
- const isDrawableEntity = selectedEntity?.type === 'regional_guidance' ||
+ const isDrawableEntity =
+ selectedEntity?.type === 'regional_guidance' ||
selectedEntity?.type === 'layer' ||
selectedEntity?.type === 'inpaint_mask';
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
index 8b22d56b65..78d07b0fb8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/stagingAreaReducers.ts
@@ -11,6 +11,10 @@ export const stagingAreaReducers = {
selectedImageIndex: null,
images: [],
};
+ // When we start staging, the user should not be interacting with the stage except to move it around. Set the tool
+ // to view.
+ state.tool.selectedBuffer = state.tool.selected;
+ state.tool.selected = 'view';
},
stagingAreaImageAdded: (state, action: PayloadAction<{ imageDTO: ImageDTO }>) => {
const { imageDTO } = action.payload;
@@ -66,8 +70,19 @@ export const stagingAreaReducers = {
}
state.stagingArea.images = state.stagingArea.images.filter((image) => image.image_name !== imageDTO.image_name);
},
- stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => state,
+ stagingAreaImageAccepted: (state, _: PayloadAction<{ imageDTO: ImageDTO }>) => {
+ // When we finish staging, reset the tool back to the previous selection.
+ if (state.tool.selectedBuffer) {
+ state.tool.selected = state.tool.selectedBuffer;
+ state.tool.selectedBuffer = null;
+ }
+ },
stagingAreaReset: (state) => {
state.stagingArea = null;
+ // When we finish staging, reset the tool back to the previous selection.
+ if (state.tool.selectedBuffer) {
+ state.tool.selected = state.tool.selectedBuffer;
+ state.tool.selectedBuffer = null;
+ }
},
} satisfies SliceCaseReducers;