From 48edb6e02301793420fd4340ebab571be5e27bdf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 30 Aug 2024 21:32:44 +1000 Subject: [PATCH] feat(ui): add save to gallery button --- invokeai/frontend/web/public/locales/en.json | 4 ++ .../components/ControlLayersToolbar.tsx | 2 + .../components/SaveToGalleryButton.tsx | 53 +++++++++++++++++++ .../konva/CanvasCompositorModule.ts | 23 ++++---- .../controlLayers/konva/CanvasStageModule.ts | 23 ++++---- 5 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 7cd0c29fae..d35da423bb 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1654,6 +1654,10 @@ "storeNotInitialized": "Store is not initialized" }, "controlLayers": { + "saveCanvasToGallery": "Save Canvas To Gallery", + "saveBboxToGallery": "Save Bbox To Gallery", + "savedToGalleryOk": "Saved to Gallery", + "savedToGalleryError": "Error saving to gallery", "clearHistory": "Clear History", "generateMode": "Generate", "generateModeDesc": "Create individual images. Generated images are added directly to the gallery.", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx index 19ffcc25d7..70c3bc883f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersToolbar.tsx @@ -2,6 +2,7 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton'; import { CanvasScale } from 'features/controlLayers/components/CanvasScale'; +import { SaveToGalleryButton } from 'features/controlLayers/components/SaveToGalleryButton'; import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover'; import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser'; import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker'; @@ -27,6 +28,7 @@ export const ControlLayersToolbar = memo(() => { + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx new file mode 100644 index 0000000000..afb4fec35a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/SaveToGalleryButton.tsx @@ -0,0 +1,53 @@ +import { IconButton, useShiftModifier } from '@invoke-ai/ui-library'; +import { logger } from 'app/logging/logger'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { isOk, withResultAsync } from 'common/util/result'; +import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; +import { toast } from 'features/toast/toast'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFloppyDiskBold } from 'react-icons/pi'; +import { serializeError } from 'serialize-error'; + +const log = logger('canvas'); + +const [useIsSaving] = buildUseBoolean(false); + +export const SaveToGalleryButton = memo(() => { + const { t } = useTranslation(); + const shift = useShiftModifier(); + const canvasManager = useCanvasManager(); + const isSaving = useIsSaving(); + + const onClick = useCallback(async () => { + isSaving.setTrue(); + + const rect = shift ? canvasManager.stateApi.getBbox().rect : canvasManager.stage.getVisibleRect(); + + const result = await withResultAsync(() => + canvasManager.compositor.rasterizeAndUploadCompositeRasterLayer(rect, true) + ); + + if (isOk(result)) { + toast({ title: t('controlLayers.savedToGalleryOk') }); + } else { + log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery'); + toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' }); + } + + isSaving.setFalse(); + }, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, isSaving, shift, t]); + + return ( + } + isLoading={isSaving.isTrue} + aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')} + tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')} + /> + ); +}); + +SaveToGalleryButton.displayName = 'SaveToGalleryButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts index 3302e783f2..a371c2eff0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasCompositorModule.ts @@ -147,6 +147,19 @@ export class CanvasCompositorModule extends CanvasModuleABC { return stableHash(data); }; + rasterizeAndUploadCompositeRasterLayer = async (rect: Rect, saveToGallery: boolean) => { + this.log.trace({ rect }, 'Rasterizing composite raster layer'); + + const canvas = this.getCompositeRasterLayerCanvas(rect); + const blob = await canvasToBlob(canvas); + + if (this.manager._isDebugging) { + previewBlob(blob, 'Composite raster layer canvas'); + } + + return uploadImage(blob, 'composite-raster-layer.png', 'general', !saveToGallery); + }; + getCompositeRasterLayerImageDTO = async (rect: Rect): Promise => { let imageDTO: ImageDTO | null = null; @@ -161,15 +174,7 @@ export class CanvasCompositorModule extends CanvasModuleABC { } } - this.log.trace({ rect }, 'Rasterizing composite raster layer'); - - const canvas = this.getCompositeRasterLayerCanvas(rect); - const blob = await canvasToBlob(canvas); - if (this.manager._isDebugging) { - previewBlob(blob, 'Composite raster layer canvas'); - } - - imageDTO = await uploadImage(blob, 'composite-raster-layer.png', 'general', true); + imageDTO = await this.rasterizeAndUploadCompositeRasterLayer(rect, false); this.manager.cache.imageNameCache.set(hash, imageDTO.image_name); return imageDTO; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts index c8e65d88a0..beb77089c1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStageModule.ts @@ -85,26 +85,23 @@ export class CanvasStageModule extends CanvasModuleABC { } } - const rectUnion = getRectUnion(...rects); - - if (rectUnion.width === 0 || rectUnion.height === 0) { - // fall back to the bbox if there is no content - return this.manager.stateApi.getBbox().rect; - } else { - return rectUnion; - } + return getRectUnion(...rects); }; fitBboxToStage = () => { - this.log.trace('Fitting bbox to stage'); - const bbox = this.manager.stateApi.getBbox(); - this.fitRect(bbox.rect); + const { rect } = this.manager.stateApi.getBbox(); + this.log.trace({ rect }, 'Fitting bbox to stage'); + this.fitRect(rect); }; fitLayersToStage() { - this.log.trace('Fitting layers to stage'); const rect = this.getVisibleRect(); - this.fitRect(rect); + if (rect.width === 0 || rect.height === 0) { + this.fitBboxToStage(); + } else { + this.log.trace({ rect }, 'Fitting layers to stage'); + this.fitRect(rect); + } } fitRect = (rect: Rect) => {