Skip to content

Commit

Permalink
feat(ui): add menu items to copy canvas/bbox to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
psychedelicious authored and hipsterusername committed Feb 5, 2025
1 parent dfb9e30 commit c5e5641
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 6 deletions.
7 changes: 6 additions & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,8 @@
"cropLayerToBbox": "Crop Layer to Bbox",
"savedToGalleryOk": "Saved to Gallery",
"savedToGalleryError": "Error saving to gallery",
"regionCopiedToClipboard": "{{region}} Copied to Clipboard",
"copyRegionError": "Error copying {{region}}",
"newGlobalReferenceImageOk": "Created Global Reference Image",
"newGlobalReferenceImageError": "Problem Creating Global Reference Image",
"newRegionalReferenceImageOk": "Created Regional Reference Image",
Expand Down Expand Up @@ -2095,7 +2097,10 @@
"newRasterLayer": "New Raster Layer",
"newInpaintMask": "New Inpaint Mask",
"newRegionalGuidance": "New Regional Guidance",
"cropCanvasToBbox": "Crop Canvas to Bbox"
"cropCanvasToBbox": "Crop Canvas to Bbox",
"copyToClipboard": "Copy to Clipboard",
"copyCanvasToClipboard": "Copy Canvas to Clipboard",
"copyBboxToClipboard": "Copy Bbox to Clipboard"
},
"stagingArea": {
"accept": "Accept",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { CanvasContextMenuItemsCropCanvasToBbox } from 'features/controlLayers/components/CanvasContextMenu/CanvasContextMenuItemsCropCanvasToBbox';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import {
useCopyCanvasToClipboard,
useNewControlLayerFromBbox,
useNewGlobalReferenceImageFromBbox,
useNewRasterLayerFromBbox,
Expand All @@ -13,19 +14,22 @@ import {
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFloppyDiskBold } from 'react-icons/pi';
import { PiCopyBold, PiFloppyDiskBold } from 'react-icons/pi';

export const CanvasContextMenuGlobalMenuItems = memo(() => {
const { t } = useTranslation();
const saveSubMenu = useSubMenu();
const newSubMenu = useSubMenu();
const copySubMenu = useSubMenu();
const isBusy = useCanvasIsBusy();
const saveCanvasToGallery = useSaveCanvasToGallery();
const saveBboxToGallery = useSaveBboxToGallery();
const newRegionalReferenceImageFromBbox = useNewRegionalReferenceImageFromBbox();
const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox();
const newRasterLayerFromBbox = useNewRasterLayerFromBbox();
const newControlLayerFromBbox = useNewControlLayerFromBbox();
const copyCanvasToClipboard = useCopyCanvasToClipboard('canvas');
const copyBboxToClipboard = useCopyCanvasToClipboard('bbox');

return (
<>
Expand Down Expand Up @@ -67,6 +71,21 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
</MenuList>
</Menu>
</MenuItem>
<MenuItem {...copySubMenu.parentMenuItemProps} icon={<PiCopyBold />}>
<Menu {...copySubMenu.menuProps}>
<MenuButton {...copySubMenu.menuButtonProps}>
<SubMenuButtonContent label={t('controlLayers.canvasContextMenu.copyToClipboard')} />
</MenuButton>
<MenuList {...copySubMenu.menuListProps}>
<MenuItem icon={<PiCopyBold />} isDisabled={isBusy} onClick={copyCanvasToClipboard}>
{t('controlLayers.canvasContextMenu.copyCanvasToClipboard')}
</MenuItem>
<MenuItem icon={<PiCopyBold />} isDisabled={isBusy} onClick={copyBboxToClipboard}>
{t('controlLayers.canvasContextMenu.copyBboxToClipboard')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
</MenuGroup>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { deepClone } from 'common/util/deepClone';
import { withResultAsync } from 'common/util/result';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasToBlob, getPrefixedId } from 'features/controlLayers/konva/util';
import {
controlLayerAdded,
entityRasterized,
Expand All @@ -27,7 +27,9 @@ import type {
import { imageDTOToImageObject, imageDTOToImageWithDims, initialControlNet } from 'features/controlLayers/store/util';
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
import type { BoardId } from 'features/gallery/store/types';
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { startCase } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { serializeError } from 'serialize-error';
Expand Down Expand Up @@ -150,6 +152,42 @@ export const useSaveBboxToGallery = () => {
return func;
};

export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const copyCanvasToClipboard = useCallback(async () => {
const rect =
region === 'bbox'
? canvasManager.stateApi.getBbox().rect
: canvasManager.compositor.getVisibleRectOfType('raster_layer');

if (rect.width === 0 || rect.height === 0) {
toast({
title: t('controlLayers.copyRegionError', { region: startCase(region) }),
description: t('controlLayers.regionIsEmpty'),
status: 'warning',
});
return;
}

const result = await withResultAsync(async () => {
const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer');
const canvasElement = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect);
const blob = await canvasToBlob(canvasElement);
copyBlobToClipboard(blob);
});

if (result.isOk()) {
toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) });
} else {
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
toast({ title: t('controlLayers.copyRegionError', { region: startCase(region) }), status: 'error' });
}
}, [canvasManager.compositor, canvasManager.stateApi, region, t]);

return copyCanvasToClipboard;
};

export const useNewRegionalReferenceImageFromBbox = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { logger } from 'app/logging/logger';
import { withResultAsync } from 'common/util/result';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
Expand Down Expand Up @@ -26,17 +27,21 @@ export const useCopyLayerToClipboard = () => {
if (!adapter) {
return;
}
try {

const result = await withResultAsync(async () => {
const canvas = adapter.getCanvas();
const blob = await canvasToBlob(canvas);
copyBlobToClipboard(blob);
});

if (result.isOk()) {
log.trace('Layer copied to clipboard');
toast({
status: 'info',
title: t('toast.layerCopiedToClipboard'),
});
} catch (error) {
log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard');
} else {
log.error({ error: serializeError(result.error) }, 'Problem copying layer to clipboard');
toast({
status: 'error',
title: t('toast.problemCopyingLayer'),
Expand Down

0 comments on commit c5e5641

Please sign in to comment.