diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 6e065570279..726ce2e008b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1682,6 +1682,8 @@ "controlLayer": "Control Layer", "inpaintMask": "Inpaint Mask", "regionalGuidance": "Regional Guidance", + "canvasAsRasterLayer": "$t(controlLayers.canvas) as $t(controlLayers.rasterLayer)", + "canvasAsControlLayer": "$t(controlLayers.canvas) as $t(controlLayers.controlLayer)", "referenceImage": "Reference Image", "regionalReferenceImage": "Regional Reference Image", "globalReferenceImage": "Global Reference Image", diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 2d19fb82659..6e22c1b6266 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -46,6 +46,8 @@ import { useCallback } from 'react'; import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models'; import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; import { isControlNetOrT2IAdapterModelConfig, isIPAdapterModelConfig } from 'services/api/types'; +import type { Equals } from 'tsafe'; +import { assert } from 'tsafe'; export const selectDefaultControlAdapter = createSelector( selectModelConfigsQuery, @@ -194,18 +196,31 @@ export const useNewCanvasFromImage = () => { const bboxRect = useAppSelector(selectBboxRect); const base = useAppSelector(selectBboxModelBase); const func = useCallback( - (imageDTO: ImageDTO) => { + (imageDTO: ImageDTO, type: CanvasRasterLayerState['type'] | CanvasControlLayerState['type']) => { // Calculate the new bbox dimensions to fit the image's aspect ratio at the optimal size const ratio = imageDTO.width / imageDTO.height; const optimalDimension = getOptimalDimension(base); const { width, height } = calculateNewSize(ratio, optimalDimension ** 2, base); // The overrides need to include the layer's ID so we can transform the layer it is initialized - const overrides = { - id: getPrefixedId('raster_layer'), - position: { x: bboxRect.x, y: bboxRect.y }, - objects: [imageDTOToImageObject(imageDTO)], - } satisfies Partial; + let overrides: Partial | Partial; + + if (type === 'raster_layer') { + overrides = { + id: getPrefixedId('raster_layer'), + position: { x: bboxRect.x, y: bboxRect.y }, + objects: [imageDTOToImageObject(imageDTO)], + } satisfies Partial; + } else if (type === 'control_layer') { + overrides = { + id: getPrefixedId('control_layer'), + position: { x: bboxRect.x, y: bboxRect.y }, + objects: [imageDTOToImageObject(imageDTO)], + } satisfies Partial; + } else { + // Catch unhandled types + assert>(false); + } CanvasEntityAdapterBase.registerInitCallback(async (adapter) => { // Skip the callback if the adapter is not the one we are creating @@ -222,7 +237,16 @@ export const useNewCanvasFromImage = () => { dispatch(canvasReset()); // The `bboxChangedFromCanvas` reducer does no validation! Careful! dispatch(bboxChangedFromCanvas({ x: 0, y: 0, width, height })); - dispatch(rasterLayerAdded({ overrides, isSelected: true })); + + // The type casts are safe because the type is checked above + if (type === 'raster_layer') { + dispatch(rasterLayerAdded({ overrides: overrides as Partial, isSelected: true })); + } else if (type === 'control_layer') { + dispatch(controlLayerAdded({ overrides: overrides as Partial, isSelected: true })); + } else { + // Catch unhandled types + assert>(false); + } }, [base, bboxRect.x, bboxRect.y, dispatch] ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu.tsx index 7d0d599c3ed..3ea5dd5bc5a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu.tsx @@ -32,8 +32,19 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => { const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage(); const newCanvasFromImage = useNewCanvasFromImage(); - const onClickNewCanvasFromImage = useCallback(() => { - newCanvasFromImage(imageDTO); + const onClickNewCanvasWithRasterLayerFromImage = useCallback(() => { + newCanvasFromImage(imageDTO, 'raster_layer'); + dispatch(setActiveTab('canvas')); + imageViewer.close(); + toast({ + id: 'SENT_TO_CANVAS', + title: t('toast.sentToCanvas'), + status: 'success', + }); + }, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]); + + const onClickNewCanvasWithControlLayerFromImage = useCallback(() => { + newCanvasFromImage(imageDTO, 'control_layer'); dispatch(setActiveTab('canvas')); imageViewer.close(); toast({ @@ -98,8 +109,15 @@ export const ImageMenuItemNewFromImageSubMenu = memo(() => { - } onClickCapture={onClickNewCanvasFromImage} isDisabled={isBusy}> - {t('controlLayers.canvas')} + } onClickCapture={onClickNewCanvasWithRasterLayerFromImage} isDisabled={isBusy}> + {t('controlLayers.canvasAsRasterLayer')} + + } + onClickCapture={onClickNewCanvasWithControlLayerFromImage} + isDisabled={isBusy} + > + {t('controlLayers.canvasAsControlLayer')} } onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}> {t('controlLayers.inpaintMask')}