Skip to content

Commit db1c5a9

Browse files
psychedelicioushipsterusername
authored andcommitted
feat(ui): image ctx -> New from Image -> Canvas as Raster/Control Layer
1 parent 56222a8 commit db1c5a9

File tree

3 files changed

+55
-11
lines changed

3 files changed

+55
-11
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,8 @@
16821682
"controlLayer": "Control Layer",
16831683
"inpaintMask": "Inpaint Mask",
16841684
"regionalGuidance": "Regional Guidance",
1685+
"canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)",
1686+
"canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)",
16851687
"referenceImage": "Reference Image",
16861688
"regionalReferenceImage": "Regional Reference Image",
16871689
"globalReferenceImage": "Global Reference Image",

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import { useCallback } from 'react';
4646
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
4747
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
4848
import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types';
49+
import type { Equals } from 'tsafe';
50+
import { assert } from 'tsafe';
4951

5052
export const selectDefaultControlAdapter = createSelector(
5153
selectModelConfigsQuery,
@@ -194,18 +196,31 @@ export const useNewCanvasFromImage = () => {
194196
const bboxRect = useAppSelector(selectBboxRect);
195197
const base = useAppSelector(selectBboxModelBase);
196198
const func = useCallback(
197-
(imageDTO: ImageDTO) => {
199+
(imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => {
198200
// Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size
199201
const ratio = imageDTO.width / imageDTO.height;
200202
const optimalDimension = getOptimalDimension(base);
201203
const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base);
202204

203205
// The overrides need to include the layer's ID so we can transform the layer it is initialized
204-
const overrides = {
205-
id: getPrefixedId('raster_layer'),
206-
position: { x: bboxRect.x, y: bboxRect.y },
207-
objects: [imageDTOToImageObject(imageDTO)],
208-
} satisfies Partial<CanvasRasterLayerState>;
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+
}
209224

210225
CanvasEntityAdapterBase.registerInitCallback(async (adapter) => {
211226
// Skip the callback if the adapter is not the one we are creating
@@ -222,7 +237,16 @@ export const useNewCanvasFromImage = () => {
222237
dispatch(canvasReset());
223238
// The `bboxChangedFromCanvas` reducer does no validation! Careful!
224239
dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height }));
225-
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
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+
}
226250
},
227251
[base, bboxRect.x, bboxRect.y, dispatch]
228252
);

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,19 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
3232
const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage();
3333
const newCanvasFromImage = useNewCanvasFromImage();
3434

35-
const onClickNewCanvasFromImage = useCallback(() => {
36-
newCanvasFromImage(imageDTO);
35+
const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => {
36+
newCanvasFromImage(imageDTO, 'raster_layer');
37+
dispatch(setActiveTab('canvas'));
38+
imageViewer.close();
39+
toast({
40+
id: 'SENT_TO_CANVAS',
41+
title: t('toast.sentToCanvas'),
42+
status: 'success',
43+
});
44+
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
45+
46+
const onClickNewCanvasWithControlLayerFromImage = useCallback(() => {
47+
newCanvasFromImage(imageDTO, 'control_layer');
3748
dispatch(setActiveTab('canvas'));
3849
imageViewer.close();
3950
toast({
@@ -98,8 +109,15 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => {
98109
<SubMenuButtonContent label="New from Image" />
99110
</MenuButton>
100111
<MenuList {...subMenu.menuListProps}>
101-
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasFromImage} isDisabled={isBusy}>
102-
{t('controlLayers.canvas')}
112+
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}>
113+
{t('controlLayers.canvasAsRasterLayer')}
114+
</MenuItem>
115+
<MenuItem
116+
icon={<PiFileBold />}
117+
onClickCapture={onClickNewCanvasWithControlLayerFromImage}
118+
isDisabled={isBusy}
119+
>
120+
{t('controlLayers.canvasAsControlLayer')}
103121
</MenuItem>
104122
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}>
105123
{t('controlLayers.inpaintMask')}

0 commit comments

Comments
 (0)