Skip to content

Commit b1ea45d

Browse files
author
Attila Cseh
committed
Multiple canvases displayed
1 parent 19ba409 commit b1ea45d

File tree

14 files changed

+311
-142
lines changed

14 files changed

+311
-142
lines changed

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

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

1617
const resetCanvasLayers = useCallback(() => {
1718
dispatch(allEntitiesDeleted());
1819
dispatch(inpaintMaskAdded({ isSelected: true, isBookmarked: true }));
19-
$canvasManager.get()?.stage.fitBboxToStage();
20-
}, [dispatch]);
20+
canvasManager?.stage.fitBboxToStage();
21+
}, [dispatch, canvasManager]);
2122
const resetGenerationSettings = useCallback(() => {
2223
dispatch(paramsReset());
2324
}, [dispatch]);

invokeai/frontend/web/src/features/controlLayers/components/InvokeCanvasComponent.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { Box } from '@invoke-ai/ui-library';
22
import { useInvokeCanvas } from 'features/controlLayers/hooks/useInvokeCanvas';
33
import { memo } from 'react';
44

5-
export const InvokeCanvasComponent = memo(() => {
6-
const ref = useInvokeCanvas();
5+
interface InvokeCanvasComponent {
6+
canvasId: string;
7+
}
8+
9+
export const InvokeCanvasComponent = memo(({ canvasId }: InvokeCanvasComponent) => {
10+
const ref = useInvokeCanvas(canvasId);
711

812
return (
913
<Box

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva
66
import { RefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
77
import { getDefaultRefImageConfig } from 'features/controlLayers/hooks/addLayerHooks';
88
import { useNewGlobalReferenceImageFromBbox } from 'features/controlLayers/hooks/saveCanvasHooks';
9-
import { useCanvasIsBusySafe } from 'features/controlLayers/hooks/useCanvasIsBusy';
9+
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
1010
import {
1111
refImageAdded,
1212
selectIsRefImagePanelOpen,
@@ -132,7 +132,7 @@ AddRefImageDropTargetAndButton.displayName = 'AddRefImageDropTargetAndButton';
132132

133133
const BboxButton = memo(() => {
134134
const { t } = useTranslation();
135-
const isBusy = useCanvasIsBusySafe();
135+
const isBusy = useCanvasIsBusy();
136136
const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox();
137137

138138
return (

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ 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 {
14-
CanvasManagerProviderGate,
15-
useCanvasManagerSafe,
16-
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
13+
import { CanvasManagerProviderGate, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
1714
import { useRefImageIdContext } from 'features/controlLayers/contexts/RefImageIdContext';
1815
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
1916
import {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { PropsWithChildren } from 'react';
2+
import { createContext, memo, useContext } from 'react';
3+
import { assert } from 'tsafe';
4+
5+
const CanvasInstanceContext = createContext<string | null>(null);
6+
7+
export const CanvasInstanceContextProvider = memo(({ canvasId, children }: PropsWithChildren<{ canvasId: string }>) => {
8+
return <CanvasInstanceContext.Provider value={canvasId}>{children}</CanvasInstanceContext.Provider>;
9+
});
10+
CanvasInstanceContextProvider.displayName = 'CanvasInstanceContextProvider';
11+
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 = () => {
19+
return useContext(CanvasInstanceContext);
20+
};
21+
22+
export const useHasScopedCanvas = () => {
23+
const canvasId = useContext(CanvasInstanceContext);
24+
25+
return !!canvasId;
26+
};
Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,112 @@
11
import { useStore } from '@nanostores/react';
2+
import { useAppSelector } from 'app/store/storeHooks';
23
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
3-
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
4+
import { $canvasManagers } from 'features/controlLayers/store/ephemeral';
5+
import { selectSelectedCanvas } from 'features/controlLayers/store/selectors';
46
import type { PropsWithChildren } from 'react';
5-
import { createContext, memo, useContext } from 'react';
7+
import { createContext, memo, useContext, useMemo } from 'react';
68
import { assert } from 'tsafe';
79

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

1014
export const CanvasManagerProviderGate = memo(({ children }: PropsWithChildren) => {
11-
const canvasManager = useStore($canvasManager);
15+
const canvasManagers = useStore($canvasManagers);
16+
const selectedCanvas = useAppSelector(selectSelectedCanvas);
1217

13-
if (!canvasManager) {
18+
if((Object.keys(canvasManagers).length === 0) || !canvasManagers[selectedCanvas.id]) {
1419
return null;
1520
}
1621

17-
return <CanvasManagerContext.Provider value={canvasManager}>{children}</CanvasManagerContext.Provider>;
22+
return <CanvasManagerContext.Provider value={canvasManagers}>{children}</CanvasManagerContext.Provider>;
1823
});
1924

2025
CanvasManagerProviderGate.displayName = 'CanvasManagerProviderGate';
2126

2227
/**
23-
* Consumes the CanvasManager from the context. This hook must be used within the CanvasManagerProviderGate, otherwise
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
2446
* it will throw an error.
2547
*/
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+
89+
/**
90+
* Consumes the CanvasManager from the context. If the CanvasManager is not available, it will throw an error.
91+
*/
2692
export const useCanvasManager = (): CanvasManager => {
27-
const canvasManager = useContext(CanvasManagerContext);
28-
assert(canvasManager, 'useCanvasManagerContext must be used within a CanvasManagerProviderGate');
93+
const canvasManager = useCanvasManagerSafe();
94+
assert(canvasManager, 'Selected canvas manager not initialised');
95+
2996
return canvasManager;
3097
};
3198

3299
/**
33100
* Consumes the CanvasManager from the context. If the CanvasManager is not available, it will return null.
34101
*/
35102
export const useCanvasManagerSafe = (): CanvasManager | null => {
36-
const canvasManager = useStore($canvasManager);
37-
return canvasManager;
103+
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]);
110+
111+
return canvasManager ?? null;
38112
};
Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,95 @@
11
import { useStore } from '@nanostores/react';
22
import { $false } from 'app/store/nanostores/util';
3-
import { useCanvasManager, useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
3+
import { useHasScopedCanvas } from 'features/controlLayers/contexts/CanvasInstanceContextProvider';
4+
import {
5+
useScopedCanvasManager,
6+
useScopedCanvasManagerSafe,
7+
useSelectedCanvasManager,
8+
useSelectedCanvasManagerSafe,
9+
} from 'features/controlLayers/contexts/CanvasManagerProviderGate';
10+
import { useMemo } from 'react';
411

512
/**
6-
* Returns a boolena indicating whether the canvas is busy:
13+
* Returns a boolena indicating whether the scoped canvas is busy:
714
* - While staging
815
* - While an entity is transforming
916
* - While an entity is filtering
1017
* - While the canvas is doing some other long-running operation, like rasterizing a layer
1118
*
1219
* This hook will throw an error if the canvas manager is not initialized.
1320
*/
14-
export const useCanvasIsBusy = () => {
15-
const canvasManager = useCanvasManager();
21+
export const useScopedCanvasIsBusy = () => {
22+
const canvasManager = useScopedCanvasManager();
1623
const isBusy = useStore(canvasManager.$isBusy);
1724

1825
return isBusy;
1926
};
2027

2128
/**
22-
* Returns a boolena indicating whether the canvas is busy:
29+
* Returns a boolena indicating whether the selected canvas is busy:
30+
* - While staging
31+
* - While an entity is transforming
32+
* - While an entity is filtering
33+
* - While the canvas is doing some other long-running operation, like rasterizing a layer
34+
*
35+
* This hook will throw an error if the canvas manager is not initialized.
36+
*/
37+
export const useSelectedCanvasIsBusy = () => {
38+
const canvasManager = useSelectedCanvasManager();
39+
const isBusy = useStore(canvasManager.$isBusy);
40+
41+
return isBusy;
42+
};
43+
44+
/**
45+
* Returns a boolena indicating whether the scoped canvas is busy:
46+
* - While staging
47+
* - While an entity is transforming
48+
* - While an entity is filtering
49+
* - While the canvas is doing some other long-running operation, like rasterizing a layer
50+
*
51+
* This hook will fall back to false if the canvas manager is not initialized.
52+
*/
53+
export const useScopedCanvasIsBusySafe = () => {
54+
const canvasManager = useScopedCanvasManagerSafe();
55+
const isBusy = useStore(canvasManager?.$isBusy ?? $false);
56+
57+
return isBusy;
58+
};
59+
60+
/**
61+
* Returns a boolena indicating whether the selected canvas is busy:
2362
* - While staging
2463
* - While an entity is transforming
2564
* - While an entity is filtering
2665
* - While the canvas is doing some other long-running operation, like rasterizing a layer
2766
*
2867
* This hook will fall back to false if the canvas manager is not initialized.
2968
*/
30-
export const useCanvasIsBusySafe = () => {
31-
const canvasManager = useCanvasManagerSafe();
69+
export const useSelectedCanvasIsBusySafe = () => {
70+
const canvasManager = useSelectedCanvasManagerSafe();
3271
const isBusy = useStore(canvasManager?.$isBusy ?? $false);
3372

3473
return isBusy;
3574
};
75+
76+
/**
77+
* Returns a boolena indicating whether the canvas is busy:
78+
* - While staging
79+
* - While an entity is transforming
80+
* - While an entity is filtering
81+
* - While the canvas is doing some other long-running operation, like rasterizing a layer
82+
*
83+
* This hook will fall back to false if the canvas manager is not initialized.
84+
*/
85+
export const useCanvasIsBusy = () => {
86+
const hasScopedCanvas = useHasScopedCanvas();
87+
const isScopedBusy = useScopedCanvasIsBusySafe();
88+
const isSelectedBusy = useSelectedCanvasIsBusySafe();
89+
90+
const isBusy = useMemo(() => {
91+
return hasScopedCanvas ? isScopedBusy : isSelectedBusy;
92+
}, [hasScopedCanvas, isScopedBusy, isSelectedBusy]);
93+
94+
return isBusy;
95+
};

invokeai/frontend/web/src/features/controlLayers/hooks/useInvokeCanvas.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import { logger } from 'app/logging/logger';
33
import { useAppStore } from 'app/store/storeHooks';
44
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
55
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
6-
import { $canvasManager } from 'features/controlLayers/store/ephemeral';
76
import Konva from 'konva';
87
import { useLayoutEffect, useState } from 'react';
98
import { $socket } from 'services/events/stores';
109
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
10+
import { $canvasManagers } from '../store/ephemeral';
1111

1212
const log = logger('canvas');
1313

1414
// This will log warnings when layers > 5
1515
Konva.showWarnings = import.meta.env.MODE === 'development';
1616

17-
const useKonvaPixelRatioWatcher = () => {
18-
useAssertSingleton('useKonvaPixelRatioWatcher');
17+
const useKonvaPixelRatioWatcher = (canvasId: string) => {
18+
useAssertSingleton(`useKonvaPixelRatioWatcher-${canvasId}`);
1919

2020
const dpr = useDevicePixelRatio({ round: false });
2121

@@ -24,12 +24,13 @@ const useKonvaPixelRatioWatcher = () => {
2424
}, [dpr]);
2525
};
2626

27-
export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => {
28-
useAssertSingleton('useInvokeCanvas');
29-
useKonvaPixelRatioWatcher();
27+
export const useInvokeCanvas = (canvasId: string): ((el: HTMLDivElement | null) => void) => {
28+
useAssertSingleton(`useInvokeCanvas-${canvasId}`);
29+
useKonvaPixelRatioWatcher(canvasId);
3030
const store = useAppStore();
3131
const socket = useStore($socket);
3232
const [container, containerRef] = useState<HTMLDivElement | null>(null);
33+
const currentManager = $canvasManagers.get()[canvasId];
3334

3435
useLayoutEffect(() => {
3536
log.debug('Initializing renderer');
@@ -44,20 +45,18 @@ export const useInvokeCanvas = (): ((el: HTMLDivElement | null) => void) => {
4445
return () => {};
4546
}
4647

47-
const currentManager = $canvasManager.get();
4848
if (currentManager) {
4949
currentManager.stage.setContainer(container);
5050
return;
5151
}
5252

53-
const manager = new CanvasManager(container, store, socket);
53+
const manager = new CanvasManager(canvasId, container, store, socket);
5454
manager.initialize();
5555

5656
return () => {
5757
manager.destroy();
58-
$canvasManager.set(null);
5958
};
60-
}, [container, socket, store]);
59+
}, [canvasId, container, socket, store, currentManager]);
6160

6261
return containerRef;
6362
};

0 commit comments

Comments
 (0)