diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index c15b072a07..2c551c5f2d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -15,6 +15,7 @@ import { addDeleteBoardAndImagesFulfilledListener } from './listeners/boardAndIm import { addBoardIdSelectedListener } from './listeners/boardIdSelected'; import { addCanvasCopiedToClipboardListener } from './listeners/canvasCopiedToClipboard'; import { addCanvasDownloadedAsImageListener } from './listeners/canvasDownloadedAsImage'; +import { addCanvasMaskSavedToGalleryListener } from './listeners/canvasMaskSavedToGallery'; import { addCanvasMergedListener } from './listeners/canvasMerged'; import { addCanvasSavedToGalleryListener } from './listeners/canvasSavedToGallery'; import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess'; @@ -27,8 +28,8 @@ import { addImageDeletedFulfilledListener, addImageDeletedPendingListener, addImageDeletedRejectedListener, - addRequestedSingleImageDeletionListener, addRequestedMultipleImageDeletionListener, + addRequestedSingleImageDeletionListener, } from './listeners/imageDeleted'; import { addImageDroppedListener } from './listeners/imageDropped'; import { @@ -129,6 +130,7 @@ addSessionReadyToInvokeListener(); // Canvas actions addCanvasSavedToGalleryListener(); +addCanvasMaskSavedToGalleryListener(); addCanvasDownloadedAsImageListener(); addCanvasCopiedToClipboardListener(); addCanvasMergedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts new file mode 100644 index 0000000000..e701b93352 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery.ts @@ -0,0 +1,60 @@ +import { logger } from 'app/logging/logger'; +import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; +import { getCanvasData } from 'features/canvas/util/getCanvasData'; +import { addToast } from 'features/system/store/systemSlice'; +import { imagesApi } from 'services/api/endpoints/images'; +import { startAppListening } from '..'; + +export const addCanvasMaskSavedToGalleryListener = () => { + startAppListening({ + actionCreator: canvasMaskSavedToGallery, + effect: async (action, { dispatch, getState }) => { + const log = logger('canvas'); + const state = getState(); + + const canvasBlobsAndImageData = await getCanvasData( + state.canvas.layerState, + state.canvas.boundingBoxCoordinates, + state.canvas.boundingBoxDimensions, + state.canvas.isMaskEnabled, + state.canvas.shouldPreserveMaskedArea + ); + + if (!canvasBlobsAndImageData) { + return; + } + + const { maskBlob } = canvasBlobsAndImageData; + + if (!maskBlob) { + log.error('Problem getting mask layer blob'); + dispatch( + addToast({ + title: 'Problem Saving Mask', + description: 'Unable to export mask', + status: 'error', + }) + ); + return; + } + + const { autoAddBoardId } = state.gallery; + + dispatch( + imagesApi.endpoints.uploadImage.initiate({ + file: new File([maskBlob], 'canvasMaskImage.png', { + type: 'image/png', + }), + image_category: 'mask', + is_intermediate: false, + board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId, + crop_visible: true, + postUploadAction: { + type: 'TOAST', + toastOptions: { title: 'Mask Saved to Assets' }, + }, + }) + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx index 2f74e5542a..25ef295631 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasMaskOptions.tsx @@ -2,10 +2,11 @@ import { ButtonGroup, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; -import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; import IAIColorPicker from 'common/components/IAIColorPicker'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; +import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox'; +import { canvasMaskSavedToGallery } from 'features/canvas/store/actions'; import { canvasSelector, isStagingSelector, @@ -22,7 +23,7 @@ import { isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { FaMask, FaTrash } from 'react-icons/fa'; +import { FaMask, FaSave, FaTrash } from 'react-icons/fa'; export const selector = createSelector( [canvasSelector, isStagingSelector], @@ -102,6 +103,10 @@ const IAICanvasMaskOptions = () => { const handleToggleEnableMask = () => dispatch(setIsMaskEnabled(!isMaskEnabled)); + const handleSaveMask = async () => { + dispatch(canvasMaskSavedToGallery()); + }; + return ( { pickerColor={maskColor} onChange={(newColor) => dispatch(setMaskColor(newColor))} /> + } onClick={handleSaveMask}> + Save Mask + } onClick={handleClearMask}> {t('unifiedCanvas.clearMask')} (Shift+C) diff --git a/invokeai/frontend/web/src/features/canvas/store/actions.ts b/invokeai/frontend/web/src/features/canvas/store/actions.ts index 1f491874a0..b4efa76e42 100644 --- a/invokeai/frontend/web/src/features/canvas/store/actions.ts +++ b/invokeai/frontend/web/src/features/canvas/store/actions.ts @@ -3,6 +3,10 @@ import { ImageDTO } from 'services/api/types'; export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery'); +export const canvasMaskSavedToGallery = createAction( + 'canvas/canvasMaskSavedToGallery' +); + export const canvasCopiedToClipboard = createAction( 'canvas/canvasCopiedToClipboard' );