Skip to content

Commit d70c006

Browse files
author
Attila Cseh
committed
staging area multi-canvas support
1 parent b1ea45d commit d70c006

37 files changed

+299
-277
lines changed

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { logger } from 'app/logging/logger';
22
import type { AppStartListening } from 'app/store/store';
33
import { bboxSyncedToOptimalDimension, rgRefImageModelChanged } from 'features/controlLayers/store/canvasSlice';
4-
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
4+
import {
5+
buildSelectIsStagingBySessionId,
6+
selectSelectedCanvasSessionId,
7+
} from 'features/controlLayers/store/canvasStagingAreaSlice';
58
import { loraIsEnabledChanged } from 'features/controlLayers/store/lorasSlice';
69
import { modelChanged, syncedToOptimalDimension, vaeSelected } from 'features/controlLayers/store/paramsSlice';
710
import { refImageModelChanged, selectReferenceImageEntities } from 'features/controlLayers/store/refImagesSlice';
@@ -159,7 +162,9 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
159162
if (modelBase !== state.params.model?.base) {
160163
// Sync generate tab settings whenever the model base changes
161164
dispatch(syncedToOptimalDimension());
162-
const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
165+
const sessionId = selectSelectedCanvasSessionId(state);
166+
const selectIsStaging = buildSelectIsStagingBySessionId(sessionId);
167+
const isStaging = selectIsStaging(state);
163168
if (!isStaging) {
164169
// Canvas tab only syncs if not staging
165170
dispatch(bboxSyncedToOptimalDimension());

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { AppStartListening } from 'app/store/store';
22
import { isNil } from 'es-toolkit';
33
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
4-
import { buildSelectIsStaging, selectCanvasSessionId } from 'features/controlLayers/store/canvasStagingAreaSlice';
4+
import {
5+
buildSelectIsStagingBySessionId,
6+
selectSelectedCanvasSessionId,
7+
} from 'features/controlLayers/store/canvasStagingAreaSlice';
58
import {
69
heightChanged,
710
setCfgRescaleMultiplier,
@@ -115,7 +118,9 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
115118
}
116119
const setSizeOptions = { updateAspectRatio: true, clamp: true };
117120

118-
const isStaging = buildSelectIsStaging(selectCanvasSessionId(state))(state);
121+
const sessionId = selectSelectedCanvasSessionId(state);
122+
const selectIsStaging = buildSelectIsStagingBySessionId(sessionId);
123+
const isStaging = selectIsStaging(state);
119124

120125
const activeTab = selectActiveTab(getState());
121126
if (activeTab === 'generate') {

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { merge } from 'es-toolkit';
2222
import { omit, pick } from 'es-toolkit/compat';
2323
import { changeBoardModalSliceConfig } from 'features/changeBoardModal/store/slice';
2424
import { canvasSettingsSliceConfig } from 'features/controlLayers/store/canvasSettingsSlice';
25-
import { canvasSliceConfig, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice';
25+
import { canvasSliceConfig, migrateCanvas, undoableCanvasesReducer } from 'features/controlLayers/store/canvasSlice';
2626
import { canvasSessionSliceConfig } from 'features/controlLayers/store/canvasStagingAreaSlice';
2727
import { lorasSliceConfig } from 'features/controlLayers/store/lorasSlice';
2828
import { paramsSliceConfig } from 'features/controlLayers/store/paramsSlice';
@@ -219,8 +219,9 @@ export const createStore = (options?: { persist?: boolean; persistDebounce?: num
219219
// Once-off listener to support waiting for rehydration before rendering the app
220220
startAppListening({
221221
actionCreator: createAction(REMEMBER_REHYDRATED),
222-
effect: (action, { unsubscribe }) => {
222+
effect: (action, { dispatch, unsubscribe }) => {
223223
unsubscribe();
224+
dispatch(migrateCanvas());
224225
options?.onRehydrated?.();
225226
},
226227
});

invokeai/frontend/web/src/common/components/SessionMenuItems.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MenuItem } from '@invoke-ai/ui-library';
22
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3-
import { useSelectedCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
3+
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
44
import { allEntitiesDeleted, inpaintMaskAdded } from 'features/controlLayers/store/canvasSlice';
55
import { paramsReset } from 'features/controlLayers/store/paramsSlice';
66
import { selectActiveTab } from 'features/ui/store/uiSelectors';
@@ -12,7 +12,7 @@ export const SessionMenuItems = memo(() => {
1212
const { t } = useTranslation();
1313
const dispatch = useAppDispatch();
1414
const tab = useAppSelector(selectActiveTab);
15-
const canvasManager = useSelectedCanvasManagerSafe();
15+
const canvasManager = useCanvasManagerSafe();
1616

1717
const resetCanvasLayers = useCallback(() => {
1818
dispatch(allEntitiesDeleted());

invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageImage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { objectEquals } from '@observ33r/object-equals';
44
import { skipToken } from '@reduxjs/toolkit/query';
55
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
66
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
7+
import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging';
78
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
8-
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
99
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
1010
import type { CroppableImageWithDims } from 'features/controlLayers/store/types';
1111
import { imageDTOToCroppableImage, imageDTOToImageWithDims } from 'features/controlLayers/store/util';

invokeai/frontend/web/src/features/controlLayers/components/RefImage/RefImageSettings.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { IPAdapterMethod } from 'features/controlLayers/components/RefImage/IPAd
1010
import { RefImageModel } from 'features/controlLayers/components/RefImage/RefImageModel';
1111
import { RefImageNoImageState } from 'features/controlLayers/components/RefImage/RefImageNoImageState';
1212
import { RefImageNoImageStateWithCanvasOptions } from 'features/controlLayers/components/RefImage/RefImageNoImageStateWithCanvasOptions';
13-
import { CanvasManagerProviderGate, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
13+
import {
14+
CanvasManagerProviderGate,
15+
useCanvasManagerSafe,
16+
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
1417
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
1518
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
1619
import {

invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceRefImageImage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { useStore } from '@nanostores/react';
33
import { skipToken } from '@reduxjs/toolkit/query';
44
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
55
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
6+
import { useCanvasIsStaging } from 'features/controlLayers/hooks/useCanvasIsStaging';
67
import { bboxSizeOptimized, bboxSizeRecalled } from 'features/controlLayers/store/canvasSlice';
7-
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
88
import { sizeOptimized, sizeRecalled } from 'features/controlLayers/store/paramsSlice';
99
import type { ImageWithDims } from 'features/controlLayers/store/types';
1010
import type { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';

invokeai/frontend/web/src/features/controlLayers/components/StagingArea/context.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useStore } from '@nanostores/react';
22
import { useAppStore } from 'app/store/storeHooks';
3+
import { useScopedCanvasSessionId } from 'features/controlLayers/hooks/useCanvasSessionId';
34
import {
45
selectStagingAreaAutoSwitch,
56
settingsStagingAreaAutoSwitchChanged,
67
} from 'features/controlLayers/store/canvasSettingsSlice';
78
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
89
import {
9-
buildSelectCanvasQueueItems,
10+
buildSelectCanvasQueueItemsBySessionId,
1011
canvasQueueItemDiscarded,
1112
canvasSessionReset,
1213
} from 'features/controlLayers/store/canvasStagingAreaSlice';
@@ -26,11 +27,12 @@ import { getInitialProgressData, StagingAreaApi } from './state';
2627

2728
const StagingAreaContext = createContext<StagingAreaApi | null>(null);
2829

29-
export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWithChildren<{ sessionId: string }>) => {
30+
export const StagingAreaContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => {
3031
const store = useAppStore();
3132
const socket = useStore($socket);
33+
const sessionId = useScopedCanvasSessionId(canvasId);
3234
const stagingAreaAppApi = useMemo<StagingAreaAppApi>(() => {
33-
const selectQueueItems = buildSelectCanvasQueueItems(sessionId);
35+
const selectQueueItems = buildSelectCanvasQueueItemsBySessionId(sessionId);
3436

3537
const _stagingAreaAppApi: StagingAreaAppApi = {
3638
getAutoSwitch: () => selectStagingAreaAutoSwitch(store.getState()),
@@ -58,16 +60,18 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
5860
});
5961
},
6062
onDiscard: ({ item_id, status }) => {
61-
store.dispatch(canvasQueueItemDiscarded({ itemId: item_id }));
63+
store.dispatch(canvasQueueItemDiscarded({ canvasId, itemId: item_id }));
6264
if (status === 'in_progress' || status === 'pending') {
6365
store.dispatch(queueApi.endpoints.cancelQueueItem.initiate({ item_id }, { track: false }));
6466
}
6567
},
6668
onDiscardAll: () => {
67-
store.dispatch(canvasSessionReset());
68-
store.dispatch(
69-
queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false })
70-
);
69+
store.dispatch(canvasSessionReset({ canvasId }));
70+
if (sessionId) {
71+
store.dispatch(
72+
queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false })
73+
);
74+
}
7175
},
7276
onAccept: (item, imageDTO) => {
7377
const bboxRect = selectBboxRect(store.getState());
@@ -80,22 +84,30 @@ export const StagingAreaContextProvider = memo(({ children, sessionId }: PropsWi
8084
};
8185

8286
store.dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
83-
store.dispatch(canvasSessionReset());
84-
store.dispatch(
85-
queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false })
86-
);
87+
store.dispatch(canvasSessionReset({ canvasId }));
88+
if (sessionId) {
89+
store.dispatch(
90+
queueApi.endpoints.cancelQueueItemsByDestination.initiate({ destination: sessionId }, { track: false })
91+
);
92+
}
8793
},
8894
onAutoSwitchChange: (mode) => {
8995
store.dispatch(settingsStagingAreaAutoSwitchChanged(mode));
9096
},
9197
};
9298

9399
return _stagingAreaAppApi;
94-
}, [sessionId, socket, store]);
100+
}, [canvasId, sessionId, socket, store]);
95101

96102
const [stagingAreaApi] = useState(() => new StagingAreaApi());
97103

98104
useEffect(() => {
105+
if (!sessionId) {
106+
return () => {
107+
stagingAreaApi.cleanup();
108+
};
109+
}
110+
99111
stagingAreaApi.connectToApp(sessionId, stagingAreaAppApi);
100112

101113
// We need to subscribe to the queue items query manually to ensure the staging area actually gets the items
Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { PropsWithChildren } from 'react';
22
import { createContext, memo, useContext } from 'react';
3-
import { assert } from 'tsafe';
43

54
const CanvasInstanceContext = createContext<string | null>(null);
65

@@ -9,18 +8,6 @@ export const CanvasInstanceContextProvider = memo(({ canvasId, children }: Props
98
});
109
CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider';
1110

12-
export const useScopedCanvas = () => {
13-
const canvasId = useContext(CanvasInstanceContext);
14-
assert(canvasId, 'useCanvasInstanceContext must be used within a CanvasInstanceContext');
15-
return canvasId;
16-
};
17-
18-
export const useScopedCanvasSafe = () => {
11+
export const useScopedCanvasIdSafe = () => {
1912
return useContext(CanvasInstanceContext);
2013
};
21-
22-
export const useHasScopedCanvas = () => {
23-
const canvasId = useContext(CanvasInstanceContext);
24-
25-
return !!canvasId;
26-
};
Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
import { useStore } from '@nanostores/react';
22
import { useAppSelector } from 'app/store/storeHooks';
3+
import { useCanvasId } from 'features/controlLayers/hooks/useCanvasId';
34
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
45
import { $canvasManagers } from 'features/controlLayers/store/ephemeral';
5-
import { selectSelectedCanvas } from 'features/controlLayers/store/selectors';
6+
import { selectSelectedCanvasId } from 'features/controlLayers/store/selectors';
67
import type { PropsWithChildren } from 'react';
7-
import { createContext, memo, useContext, useMemo } from 'react';
8+
import { createContext, memo } from 'react';
89
import { assert } from 'tsafe';
910

10-
import { useScopedCanvas, useScopedCanvasSafe } from './CanvasInstanceContextProvider';
11-
1211
const CanvasManagerContext = createContext<{ [canvasId: string]: CanvasManager } | null>(null);
1312

1413
export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => {
1514
const canvasManagers = useStore($canvasManagers);
16-
const selectedCanvas = useAppSelector(selectSelectedCanvas);
15+
const selectedCanvasId = useAppSelector(selectSelectedCanvasId);
1716

18-
if((Object.keys(canvasManagers).length === 0) || !canvasManagers[selectedCanvas.id]) {
17+
if (Object.keys(canvasManagers).length === 0 || !canvasManagers[selectedCanvasId]) {
1918
return null;
2019
}
2120

@@ -24,68 +23,6 @@ export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren)
2423

2524
CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate';
2625

27-
/**
28-
* Consumes the scoped CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise
29-
* it will throw an error.
30-
*/
31-
export const useScopedCanvasManager = (): CanvasManager => {
32-
const canvasManagers = useContext(CanvasManagerContext);
33-
assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate');
34-
35-
const scopedCanvasId = useScopedCanvas();
36-
const canvasManager = useMemo(() => {
37-
return canvasManagers[scopedCanvasId];
38-
}, [canvasManagers, scopedCanvasId]);
39-
assert(canvasManager, 'Scoped canvas manager not initialised');
40-
41-
return canvasManager;
42-
};
43-
44-
/**
45-
* Consumes the selected CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise
46-
* it will throw an error.
47-
*/
48-
export const useSelectedCanvasManager = (): CanvasManager => {
49-
const canvasManagers = useContext(CanvasManagerContext);
50-
assert(canvasManagers, 'useScopedCanvasManager must be used within a CanvasManagerProviderGate');
51-
52-
const selectedCanvas = useAppSelector(selectSelectedCanvas);
53-
const canvasManager = useMemo(() => {
54-
return canvasManagers[selectedCanvas.id];
55-
}, [canvasManagers, selectedCanvas]);
56-
assert(canvasManager, 'Selected canvas manager not initialised');
57-
58-
return canvasManager;
59-
};
60-
61-
/**
62-
* Consumes the scoped CanvasManager from the context. If the CanvasManager is not available, it will return null.
63-
*/
64-
export const useScopedCanvasManagerSafe = (): CanvasManager | null => {
65-
const canvasManagers = useStore($canvasManagers);
66-
const scopedCanvasId = useScopedCanvasSafe();
67-
68-
const canvasManager = useMemo(() => {
69-
return scopedCanvasId ? canvasManagers[scopedCanvasId] : undefined;
70-
}, [canvasManagers, scopedCanvasId]);
71-
72-
return canvasManager ?? null;
73-
};
74-
75-
/**
76-
* Consumes the selected CanvasManager from the context. If the CanvasManager is not available, it will return null.
77-
*/
78-
export const useSelectedCanvasManagerSafe = (): CanvasManager | null => {
79-
const canvasManagers = useStore($canvasManagers);
80-
const selectedCanvas = useAppSelector(selectSelectedCanvas);
81-
82-
const canvasManager = useMemo(() => {
83-
return canvasManagers[selectedCanvas.id];
84-
}, [canvasManagers, selectedCanvas]);
85-
86-
return canvasManager ?? null;
87-
};
88-
8926
/**
9027
* Consumes the CanvasManager from the context. If the CanvasManager is not available, it will throw an error.
9128
*/
@@ -101,12 +38,7 @@ export const useCanvasManager = (): CanvasManager => {
10138
*/
10239
export const useCanvasManagerSafe = (): CanvasManager | null => {
10340
const canvasManagers = useStore($canvasManagers);
104-
const scopedCanvasId = useScopedCanvasSafe();
105-
const selectedCanvas = useAppSelector(selectSelectedCanvas);
106-
107-
const canvasManager = useMemo(() => {
108-
return scopedCanvasId ? canvasManagers[scopedCanvasId] : canvasManagers[selectedCanvas.id];
109-
}, [canvasManagers, selectedCanvas, scopedCanvasId]);
41+
const canvasId = useCanvasId();
11042

111-
return canvasManager ?? null;
43+
return canvasManagers[canvasId] ?? null;
11244
};

0 commit comments

Comments
 (0)