Skip to content

Commit ff34617

Browse files
feat(ui): use new image actions system for image menu
1 parent 92f6600 commit ff34617

File tree

3 files changed

+180
-194
lines changed

3 files changed

+180
-194
lines changed

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

Lines changed: 3 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
22
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
33
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import { deepClone } from 'common/util/deepClone';
5-
import { CanvasEntityAdapterBase } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterBase';
65
import { getPrefixedId } from 'features/controlLayers/konva/util';
7-
import { canvasReset } from 'features/controlLayers/store/actions';
86
import {
9-
bboxChangedFromCanvas,
107
controlLayerAdded,
118
inpaintMaskAdded,
129
rasterLayerAdded,
@@ -17,37 +14,20 @@ import {
1714
rgPositivePromptChanged,
1815
} from 'features/controlLayers/store/canvasSlice';
1916
import { selectBase } from 'features/controlLayers/store/paramsSlice';
20-
import {
21-
selectBboxModelBase,
22-
selectBboxRect,
23-
selectCanvasSlice,
24-
selectEntityOrThrow,
25-
} from 'features/controlLayers/store/selectors';
17+
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
2618
import type {
27-
CanvasControlLayerState,
2819
CanvasEntityIdentifier,
29-
CanvasInpaintMaskState,
30-
CanvasRasterLayerState,
3120
CanvasRegionalGuidanceState,
3221
ControlNetConfig,
3322
IPAdapterConfig,
3423
T2IAdapterConfig,
3524
} from 'features/controlLayers/store/types';
36-
import {
37-
imageDTOToImageObject,
38-
initialControlNet,
39-
initialIPAdapter,
40-
initialT2IAdapter,
41-
} from 'features/controlLayers/store/util';
42-
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
25+
import { initialControlNet, initialIPAdapter, initialT2IAdapter } from 'features/controlLayers/store/util';
4326
import { zModelIdentifierField } from 'features/nodes/types/common';
44-
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
4527
import { useCallback } from 'react';
4628
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
47-
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
29+
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
4830
import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types';
49-
import type { Equals } from 'tsafe';
50-
import { assert } from 'tsafe';
5131

5232
/** @knipignore */
5333
export const selectDefaultControlAdapter = createSelector(
@@ -110,150 +90,6 @@ export const useAddRasterLayer = () => {
11090
return func;
11191
};
11292

113-
export const useNewRasterLayerFromImage = () => {
114-
const dispatch = useAppDispatch();
115-
const bboxRect = useAppSelector(selectBboxRect);
116-
const func = useCallback(
117-
(imageDTO: ImageDTO) => {
118-
const imageObject = imageDTOToImageObject(imageDTO);
119-
const overrides: Partial<CanvasRasterLayerState> = {
120-
position: { x: bboxRect.x, y: bboxRect.y },
121-
objects: [imageObject],
122-
};
123-
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
124-
},
125-
[bboxRect.x, bboxRect.y, dispatch]
126-
);
127-
128-
return func;
129-
};
130-
131-
export const useNewControlLayerFromImage = () => {
132-
const dispatch = useAppDispatch();
133-
const bboxRect = useAppSelector(selectBboxRect);
134-
const func = useCallback(
135-
(imageDTO: ImageDTO) => {
136-
const imageObject = imageDTOToImageObject(imageDTO);
137-
const overrides: Partial<CanvasControlLayerState> = {
138-
position: { x: bboxRect.x, y: bboxRect.y },
139-
objects: [imageObject],
140-
};
141-
dispatch(controlLayerAdded({ overrides, isSelected: true }));
142-
},
143-
[bboxRect.x, bboxRect.y, dispatch]
144-
);
145-
146-
return func;
147-
};
148-
149-
export const useNewInpaintMaskFromImage = () => {
150-
const dispatch = useAppDispatch();
151-
const bboxRect = useAppSelector(selectBboxRect);
152-
const func = useCallback(
153-
(imageDTO: ImageDTO) => {
154-
const imageObject = imageDTOToImageObject(imageDTO);
155-
const overrides: Partial<CanvasInpaintMaskState> = {
156-
position: { x: bboxRect.x, y: bboxRect.y },
157-
objects: [imageObject],
158-
};
159-
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
160-
},
161-
[bboxRect.x, bboxRect.y, dispatch]
162-
);
163-
164-
return func;
165-
};
166-
167-
export const useNewRegionalGuidanceFromImage = () => {
168-
const dispatch = useAppDispatch();
169-
const bboxRect = useAppSelector(selectBboxRect);
170-
const func = useCallback(
171-
(imageDTO: ImageDTO) => {
172-
const imageObject = imageDTOToImageObject(imageDTO);
173-
const overrides: Partial<CanvasRegionalGuidanceState> = {
174-
position: { x: bboxRect.x, y: bboxRect.y },
175-
objects: [imageObject],
176-
};
177-
dispatch(rgAdded({ overrides, isSelected: true }));
178-
},
179-
[bboxRect.x, bboxRect.y, dispatch]
180-
);
181-
182-
return func;
183-
};
184-
185-
/**
186-
* Returns a function that adds a new canvas with the given image as the initial image, replicating the img2img flow:
187-
* - Reset the canvas
188-
* - Resize the bbox to the image's aspect ratio at the optimal size for the selected model
189-
* - Add the image as a raster layer
190-
* - Resizes the layer to fit the bbox using the 'fill' strategy
191-
*
192-
* This allows the user to immediately generate a new image from the given image without any additional steps.
193-
*/
194-
export const useNewCanvasFromImage = () => {
195-
const dispatch = useAppDispatch();
196-
const bboxRect = useAppSelector(selectBboxRect);
197-
const base = useAppSelector(selectBboxModelBase);
198-
const func = useCallback(
199-
(imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => {
200-
// Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size
201-
const ratio = imageDTO.width / imageDTO.height;
202-
const optimalDimension = getOptimalDimension(base);
203-
const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base);
204-
205-
// The overrides need to include the layer's ID so we can transform the layer it is initialized
206-
let overrides: Partial<CanvasRasterLayerState> | Partial<CanvasControlLayerState>;
207-
208-
if (type === 'raster_layer') {
209-
overrides = {
210-
id: getPrefixedId('raster_layer'),
211-
position: { x: bboxRect.x, y: bboxRect.y },
212-
objects: [imageDTOToImageObject(imageDTO)],
213-
} satisfies Partial<CanvasRasterLayerState>;
214-
} else if (type === 'control_layer') {
215-
overrides = {
216-
id: getPrefixedId('control_layer'),
217-
position: { x: bboxRect.x, y: bboxRect.y },
218-
objects: [imageDTOToImageObject(imageDTO)],
219-
} satisfies Partial<CanvasControlLayerState>;
220-
} else {
221-
// Catch unhandled types
222-
assert<Equals<typeof type, never>>(false);
223-
}
224-
225-
CanvasEntityAdapterBase.registerInitCallback(async (adapter) => {
226-
// Skip the callback if the adapter is not the one we are creating
227-
if (adapter.id !== overrides.id) {
228-
return false;
229-
}
230-
// Fit the layer to the bbox w/ fill strategy
231-
await adapter.transformer.startTransform({ silent: true });
232-
adapter.transformer.fitToBboxFill();
233-
await adapter.transformer.applyTransform();
234-
return true;
235-
});
236-
237-
dispatch(canvasReset());
238-
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
239-
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
240-
241-
// The type casts are safe because the type is checked above
242-
if (type === 'raster_layer') {
243-
dispatch(rasterLayerAdded({ overrides: overrides as Partial<CanvasRasterLayerState>, isSelected: true }));
244-
} else if (type === 'control_layer') {
245-
dispatch(controlLayerAdded({ overrides: overrides as Partial<CanvasControlLayerState>, isSelected: true }));
246-
} else {
247-
// Catch unhandled types
248-
assert<Equals<typeof type, never>>(false);
249-
}
250-
},
251-
[base, bboxRect.x, bboxRect.y, dispatch]
252-
);
253-
254-
return func;
255-
};
256-
25793
export const useAddInpaintMask = () => {
25894
const dispatch = useAppDispatch();
25995
const func = useCallback(() => {

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu.tsx

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
2+
import { useAppStore } from 'app/store/nanostores/store';
23
import { useAppDispatch } from 'app/store/storeHooks';
34
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
45
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
5-
import {
6-
useNewCanvasFromImage,
7-
useNewControlLayerFromImage,
8-
useNewInpaintMaskFromImage,
9-
useNewRasterLayerFromImage,
10-
useNewRegionalGuidanceFromImage,
11-
} from 'features/controlLayers/hooks/addLayerHooks';
126
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
137
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
148
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
159
import { sentImageToCanvas } from 'features/gallery/store/actions';
10+
import {
11+
newCanvasEntityFromImageActionApi,
12+
newCanvasFromImageActionApi,
13+
singleImageSourceApi,
14+
} from 'features/imageActions/actions';
1615
import { toast } from 'features/toast/toast';
1716
import { setActiveTab } from 'features/ui/store/uiSlice';
1817
import { memo, useCallback } from 'react';
@@ -22,85 +21,93 @@ import { PiFileBold, PiPlusBold } from 'react-icons/pi';
2221
export const ImageMenuItemNewFromImageSubMenu = memo(() => {
2322
const { t } = useTranslation();
2423
const subMenu = useSubMenu();
24+
const store = useAppStore();
2525
const dispatch = useAppDispatch();
2626
const imageDTO = useImageDTOContext();
2727
const imageViewer = useImageViewer();
2828
const isBusy = useCanvasIsBusy();
29-
const newRasterLayerFromImage = useNewRasterLayerFromImage();
30-
const newControlLayerFromImage = useNewControlLayerFromImage();
31-
const newInpaintMaskFromImage = useNewInpaintMaskFromImage();
32-
const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage();
33-
const newCanvasFromImage = useNewCanvasFromImage();
3429

3530
const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {
36-
newCanvasFromImage(imageDTO, 'raster_layer');
31+
const targetData = newCanvasFromImageActionApi.getData({ type: 'raster_layer' });
32+
const sourceData = singleImageSourceApi.getData({ imageDTO });
33+
newCanvasFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
3734
dispatch(setActiveTab('canvas'));
3835
imageViewer.close();
3936
toast({
4037
id: 'SENT_TO_CANVAS',
4138
title: t('toast.sentToCanvas'),
4239
status: 'success',
4340
});
44-
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
41+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
4542

4643
const onClickNewCanvasWithControlLayerFromImage = useCallback(() => {
47-
newCanvasFromImage(imageDTO, 'control_layer');
44+
const targetData = newCanvasFromImageActionApi.getData({ type: 'control_layer' });
45+
const sourceData = singleImageSourceApi.getData({ imageDTO });
46+
newCanvasFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
4847
dispatch(setActiveTab('canvas'));
4948
imageViewer.close();
5049
toast({
5150
id: 'SENT_TO_CANVAS',
5251
title: t('toast.sentToCanvas'),
5352
status: 'success',
5453
});
55-
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
54+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
5655

5756
const onClickNewRasterLayerFromImage = useCallback(() => {
57+
const targetData = newCanvasEntityFromImageActionApi.getData({ type: 'raster_layer' });
58+
const sourceData = singleImageSourceApi.getData({ imageDTO });
59+
newCanvasEntityFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
5860
dispatch(sentImageToCanvas());
59-
newRasterLayerFromImage(imageDTO);
6061
dispatch(setActiveTab('canvas'));
6162
imageViewer.close();
6263
toast({
6364
id: 'SENT_TO_CANVAS',
6465
title: t('toast.sentToCanvas'),
6566
status: 'success',
6667
});
67-
}, [dispatch, imageDTO, imageViewer, newRasterLayerFromImage, t]);
68+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
6869

6970
const onClickNewControlLayerFromImage = useCallback(() => {
71+
const targetData = newCanvasEntityFromImageActionApi.getData({ type: 'control_layer' });
72+
const sourceData = singleImageSourceApi.getData({ imageDTO });
73+
newCanvasEntityFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
7074
dispatch(sentImageToCanvas());
71-
newControlLayerFromImage(imageDTO);
7275
dispatch(setActiveTab('canvas'));
7376
imageViewer.close();
7477
toast({
7578
id: 'SENT_TO_CANVAS',
7679
title: t('toast.sentToCanvas'),
7780
status: 'success',
7881
});
79-
}, [dispatch, imageDTO, imageViewer, newControlLayerFromImage, t]);
82+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
8083

8184
const onClickNewInpaintMaskFromImage = useCallback(() => {
85+
const targetData = newCanvasEntityFromImageActionApi.getData({ type: 'inpaint_mask' });
86+
const sourceData = singleImageSourceApi.getData({ imageDTO });
87+
newCanvasEntityFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
8288
dispatch(sentImageToCanvas());
83-
newInpaintMaskFromImage(imageDTO);
8489
dispatch(setActiveTab('canvas'));
8590
imageViewer.close();
8691
toast({
8792
id: 'SENT_TO_CANVAS',
8893
title: t('toast.sentToCanvas'),
8994
status: 'success',
9095
});
91-
}, [dispatch, imageDTO, imageViewer, newInpaintMaskFromImage, t]);
96+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
9297

9398
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
99+
const targetData = newCanvasEntityFromImageActionApi.getData({ type: 'regional_guidance' });
100+
const sourceData = singleImageSourceApi.getData({ imageDTO });
101+
newCanvasEntityFromImageActionApi.handler(sourceData, targetData, store.dispatch, store.getState);
94102
dispatch(sentImageToCanvas());
95-
newRegionalGuidanceFromImage(imageDTO);
96103
dispatch(setActiveTab('canvas'));
97104
imageViewer.close();
98105
toast({
99106
id: 'SENT_TO_CANVAS',
100107
title: t('toast.sentToCanvas'),
101108
status: 'success',
102109
});
103-
}, [dispatch, imageDTO, imageViewer, newRegionalGuidanceFromImage, t]);
110+
}, [dispatch, imageDTO, imageViewer, store.dispatch, store.getState, t]);
104111

105112
return (
106113
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>

0 commit comments

Comments
 (0)