feat(ui): add save to gallery button

This commit is contained in:
psychedelicious 2024-08-30 21:32:44 +10:00
parent aeae6af0a1
commit 48edb6e023
5 changed files with 83 additions and 22 deletions

View File

@ -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.",

View File

@ -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(() => {
<CanvasResetViewButton />
<Spacer />
<ToolFillColorPicker />
<SaveToGalleryButton />
<CanvasSettingsPopover />
<ViewerToggle />
</Flex>

View File

@ -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 (
<IconButton
variant="ghost"
onClick={onClick}
icon={<PiFloppyDiskBold />}
isLoading={isSaving.isTrue}
aria-label={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
tooltip={shift ? t('controlLayers.saveBboxToGallery') : t('controlLayers.saveCanvasToGallery')}
/>
);
});
SaveToGalleryButton.displayName = 'SaveToGalleryButton';

View File

@ -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<ImageDTO> => {
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;
};

View File

@ -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) => {