feat(ui): add menu items to copy canvas/bbox to clipboard

This commit is contained in:
psychedelicious
2025-02-05 11:40:52 +11:00
committed by Kent Keirsey
parent dfb9e300d4
commit c5e5641f0e
4 changed files with 73 additions and 6 deletions

View File

@@ -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,
@@ -13,12 +14,13 @@ 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();
@@ -26,6 +28,8 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox();
const newRasterLayerFromBbox = useNewRasterLayerFromBbox();
const newControlLayerFromBbox = useNewControlLayerFromBbox();
const copyCanvasToClipboard = useCopyCanvasToClipboard('canvas');
const copyBboxToClipboard = useCopyCanvasToClipboard('bbox');
return (
<>
@@ -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>
</>
);

View File

@@ -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,
@@ -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';
@@ -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();

View File

@@ -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';
@@ -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'),