Compare commits

...

9 Commits

Author SHA1 Message Date
psychedelicious
1acaa6c486 chore: bump version to v5.3.0rc2 2024-10-25 07:50:58 +11:00
psychedelicious
b0381076b7 revert(ui): drop targets for inpaint mask and rg 2024-10-25 07:42:46 +11:00
psychedelicious
ffff2d6dbb feat(ui): add New from Image submenu for image ctx menu 2024-10-25 07:42:46 +11:00
psychedelicious
afa9f07649 fix(ui): missing cursor when transforming 2024-10-25 07:42:46 +11:00
psychedelicious
addb5c49ea feat(ui): support dnd images onto inpaint mask/rg entities 2024-10-25 07:42:46 +11:00
psychedelicious
a112d2d55b feat(ui): add logging to useCopyLayerToClipboard 2024-10-25 07:42:46 +11:00
psychedelicious
619a271c8a feat(ui): disable copy to clipboard when layer is empty 2024-10-25 07:42:46 +11:00
psychedelicious
909f2ee36d feat(ui): add help tooltip to automask 2024-10-25 07:42:46 +11:00
psychedelicious
b4cf3d9d03 fix(ui): canvas context menu w/ eraser tool erases 2024-10-25 07:42:46 +11:00
20 changed files with 349 additions and 100 deletions

View File

@@ -1889,7 +1889,12 @@
"reset": "Reset",
"saveAs": "Save As",
"cancel": "Cancel",
"process": "Process"
"process": "Process",
"help1": "Auto-mask creates a mask for a single target object. Add <Bold>Include</Bold> and <Bold>Exclude</Bold> points to indicate which parts of the layer are part of the target object.",
"help2": "Start with one <Bold>Include</Bold> point within the target object. Add more points to refine the mask. Fewer points typically produce better results.",
"clickToAdd": "Click on the layer to add a point",
"dragToMove": "Drag a point to move it",
"clickToRemove": "Click on a point to remove it"
},
"settings": {
"snapToGrid": {
@@ -1930,6 +1935,8 @@
"newRegionalReferenceImage": "New Regional Reference Image",
"newControlLayer": "New Control Layer",
"newRasterLayer": "New Raster Layer",
"newInpaintMask": "New Inpaint Mask",
"newRegionalGuidance": "New Regional Guidance",
"cropCanvasToBbox": "Crop Canvas to Bbox"
},
"stagingArea": {

View File

@@ -8,6 +8,7 @@ import {
controlLayerAdded,
entityRasterized,
entitySelected,
inpaintMaskAdded,
rasterLayerAdded,
referenceImageAdded,
referenceImageIPAdapterImageChanged,
@@ -17,6 +18,7 @@ import {
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasReferenceImageState,
CanvasRegionalGuidanceState,
@@ -110,6 +112,46 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
/**
* Image dropped on Inpaint Mask
*/
if (
overData.actionType === 'ADD_INPAINT_MASK_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasInpaintMaskState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
return;
}
/**
/**
* Image dropped on Regional Guidance
*/
if (
overData.actionType === 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasRegionalGuidanceState> = {
objects: [imageObject],
position: { x, y },
};
dispatch(rgAdded({ overrides, isSelected: true }));
return;
}
/**
* Image dropped on Raster layer
*/

View File

@@ -41,7 +41,7 @@ export const CanvasDropArea = memo(() => {
return (
<>
<Grid
gridTemplateRows="1fr 1fr"
gridTemplateRows="1fr 1fr 1fr"
gridTemplateColumns="1fr 1fr"
position="absolute"
top={0}
@@ -62,6 +62,7 @@ export const CanvasDropArea = memo(() => {
data={addControlLayerFromImageDropData}
/>
</GridItem>
<GridItem position="relative">
<IAIDroppable
dropLabel={t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}

View File

@@ -14,7 +14,7 @@ type Props = {
};
export const InpaintMask = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'inpaint_mask' }), [id]);
const entityIdentifier = useMemo<CanvasEntityIdentifier<'inpaint_mask'>>(() => ({ id, type: 'inpaint_mask' }), [id]);
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>

View File

@@ -16,7 +16,10 @@ type Props = {
};
export const RegionalGuidance = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]);
const entityIdentifier = useMemo<CanvasEntityIdentifier<'regional_guidance'>>(
() => ({ id, type: 'regional_guidance' }),
[id]
);
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>

View File

@@ -3,11 +3,16 @@ import {
ButtonGroup,
Flex,
Heading,
Icon,
ListItem,
Menu,
MenuButton,
MenuItem,
MenuList,
Spacer,
Text,
Tooltip,
UnorderedList,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
@@ -20,9 +25,10 @@ import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/kon
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import { selectAutoProcess } from 'features/controlLayers/store/canvasSettingsSlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiStarBold, PiXBold } from 'react-icons/pi';
import { Trans, useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiFloppyDiskBold, PiInfoBold, PiStarBold, PiXBold } from 'react-icons/pi';
const SegmentAnythingContent = memo(
({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
@@ -81,10 +87,17 @@ const SegmentAnythingContent = memo(
transitionProperty="height"
transitionDuration="normal"
>
<Flex w="full" gap={4}>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.segment.autoMask')}
</Heading>
<Flex w="full" gap={4} alignItems="center">
<Flex gap={2}>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.segment.autoMask')}
</Heading>
<Tooltip label={<SegmentAnythingHelpTooltipContent />}>
<Flex alignItems="center">
<Icon as={PiInfoBold} color="base.500" />
</Flex>
</Tooltip>
</Flex>
<Spacer />
<CanvasAutoProcessSwitch />
<CanvasOperationIsolatedLayerPreviewSwitch />
@@ -166,3 +179,31 @@ export const SegmentAnything = () => {
return <SegmentAnythingContent adapter={adapter} />;
};
const Bold = (props: PropsWithChildren) => (
<Text as="span" fontWeight="semibold">
{props.children}
</Text>
);
const SegmentAnythingHelpTooltipContent = memo(() => {
const { t } = useTranslation();
return (
<Flex gap={3} flexDir="column">
<Text>
<Trans i18nKey="controlLayers.segment.help1" components={{ Bold: <Bold /> }} />
</Text>
<Text>
<Trans i18nKey="controlLayers.segment.help2" components={{ Bold: <Bold /> }} />
</Text>
<UnorderedList>
<ListItem>{t('controlLayers.segment.clickToAdd')}</ListItem>
<ListItem>{t('controlLayers.segment.dragToMove')}</ListItem>
<ListItem>{t('controlLayers.segment.clickToRemove')}</ListItem>
</UnorderedList>
</Flex>
);
});
SegmentAnythingHelpTooltipContent.displayName = 'SegmentAnythingHelpTooltipContent';

View File

@@ -2,6 +2,7 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
import { useEntityIsEmpty } from 'features/controlLayers/hooks/useEntityIsEmpty';
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -12,6 +13,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const adapter = useEntityAdapterSafe(entityIdentifier);
const isInteractable = useIsEntityInteractable(entityIdentifier);
const isEmpty = useEntityIsEmpty(entityIdentifier);
const copyLayerToClipboard = useCopyLayerToClipboard();
const onClick = useCallback(() => {
@@ -19,7 +21,7 @@ export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
}, [copyLayerToClipboard, adapter]);
return (
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable}>
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable || isEmpty}>
{t('common.clipboard')}
</MenuItem>
);

View File

@@ -24,7 +24,9 @@ import {
selectEntityOrThrow,
} from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
ControlNetConfig,
@@ -124,6 +126,60 @@ export const useNewRasterLayerFromImage = () => {
return func;
};
export const useNewControlLayerFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasControlLayerState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
export const useNewInpaintMaskFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasInpaintMaskState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(inpaintMaskAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
export const useNewRegionalGuidanceFromImage = () => {
const dispatch = useAppDispatch();
const bboxRect = useAppSelector(selectBboxRect);
const func = useCallback(
(imageDTO: ImageDTO) => {
const imageObject = imageDTOToImageObject(imageDTO);
const overrides: Partial<CanvasRegionalGuidanceState> = {
position: { x: bboxRect.x, y: bboxRect.y },
objects: [imageObject],
};
dispatch(rgAdded({ overrides, isSelected: true }));
},
[bboxRect.x, bboxRect.y, dispatch]
);
return func;
};
/**
* Returns a function that adds a new canvas with the given image as the initial image, replicating the img2img flow:
* - Reset the canvas

View File

@@ -1,3 +1,4 @@
import { logger } from 'app/logging/logger';
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';
@@ -7,6 +8,9 @@ import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { serializeError } from 'serialize-error';
const log = logger('canvas');
export const useCopyLayerToClipboard = () => {
const { t } = useTranslation();
@@ -26,11 +30,13 @@ export const useCopyLayerToClipboard = () => {
const canvas = adapter.getCanvas();
const blob = await canvasToBlob(canvas);
copyBlobToClipboard(blob);
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');
toast({
status: 'error',
title: t('toast.problemCopyingLayer'),

View File

@@ -0,0 +1,11 @@
import { useAppSelector } from 'app/store/storeHooks';
import { buildSelectHasObjects } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
export const useEntityIsEmpty = (entityIdentifier: CanvasEntityIdentifier) => {
const selectHasObjects = useMemo(() => buildSelectHasObjects(entityIdentifier), [entityIdentifier]);
const hasObjects = useAppSelector(selectHasObjects);
return !hasObjects;
};

View File

@@ -216,12 +216,14 @@ export class CanvasEraserToolModule extends CanvasModuleBase {
*/
onStagePointerDown = async (e: KonvaEventObject<PointerEvent>) => {
const cursorPos = this.parent.$cursorPos.get();
const isPrimaryPointerDown = this.parent.$isPrimaryPointerDown.get();
const selectedEntity = this.manager.stateApi.getSelectedEntityAdapter();
if (!cursorPos || !selectedEntity) {
if (!cursorPos || !selectedEntity || !isPrimaryPointerDown) {
/**
* Can't do anything without:
* - A cursor position: the cursor is not on the stage
* - The mouse is down: the user is not drawing
* - A selected entity: there is no entity to draw on
*/
return;

View File

@@ -160,11 +160,16 @@ export class CanvasToolModule extends CanvasModuleBase {
const stage = this.manager.stage;
const tool = this.$tool.get();
const segmentingAdapter = this.manager.stateApi.$segmentingAdapter.get();
const transformingAdapter = this.manager.stateApi.$transformingAdapter.get();
if ((this.manager.stage.getIsDragging() || tool === 'view') && !segmentingAdapter) {
if (this.manager.stage.getIsDragging()) {
this.tools.view.syncCursorStyle();
} else if (tool === 'view') {
this.tools.view.syncCursorStyle();
} else if (segmentingAdapter) {
segmentingAdapter.segmentAnything.syncCursorStyle();
} else if (transformingAdapter) {
// The transformer handles cursor style via events
} else if (this.manager.stateApi.$isFiltering.get()) {
stage.setCursor('not-allowed');
} else if (this.manager.stagingArea.$isStaging.get()) {

View File

@@ -349,6 +349,27 @@ export const buildSelectIsSelected = (entityIdentifier: CanvasEntityIdentifier)
);
};
/**
* Builds a selector that selects if the entity is empty.
*
* Reference images are considered empty if the IP adapter is empty.
*
* Other entities are considered empty if they have no objects.
*/
export const buildSelectHasObjects = (entityIdentifier: CanvasEntityIdentifier) => {
return createSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntity(canvas, entityIdentifier);
if (!entity) {
return false;
}
if (entity.type === 'reference_image') {
return entity.ipAdapter.image !== null;
}
return entity.objects.length > 0;
});
};
export const selectWidth = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.width);
export const selectHeight = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.rect.height);
export const selectAspectRatioID = createSelector(selectCanvasSlice, (canvas) => canvas.bbox.aspectRatio.id);

View File

@@ -42,6 +42,14 @@ export type AddControlLayerFromImageDropData = BaseDropData & {
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE';
};
type AddInpaintMaskFromImageDropData = BaseDropData & {
actionType: 'ADD_INPAINT_MASK_FROM_IMAGE';
};
type AddRegionalGuidanceFromImageDropData = BaseDropData & {
actionType: 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE';
};
export type AddRegionalReferenceImageFromImageDropData = BaseDropData & {
actionType: 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE';
};
@@ -53,7 +61,7 @@ export type AddGlobalReferenceImageFromImageDropData = BaseDropData & {
export type ReplaceLayerImageDropData = BaseDropData & {
actionType: 'REPLACE_LAYER_WITH_IMAGE';
context: {
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer'>;
entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer' | 'inpaint_mask' | 'regional_guidance'>;
};
};
@@ -98,7 +106,9 @@ export type TypesafeDroppableData =
| AddControlLayerFromImageDropData
| ReplaceLayerImageDropData
| AddRegionalReferenceImageFromImageDropData
| AddGlobalReferenceImageFromImageDropData;
| AddGlobalReferenceImageFromImageDropData
| AddInpaintMaskFromImageDropData
| AddRegionalGuidanceFromImageDropData;
type BaseDragData = {
id: string;

View File

@@ -17,6 +17,8 @@ export const isValidDrop = (overData?: TypesafeDroppableData | null, activeData?
case 'SET_RG_IP_ADAPTER_IMAGE':
case 'ADD_RASTER_LAYER_FROM_IMAGE':
case 'ADD_CONTROL_LAYER_FROM_IMAGE':
case 'ADD_INPAINT_MASK_FROM_IMAGE':
case 'ADD_REGIONAL_GUIDANCE_FROM_IMAGE':
case 'SET_UPSCALE_INITIAL_IMAGE':
case 'SET_NODES_IMAGE':
case 'SELECT_FOR_COMPARE':

View File

@@ -1,39 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useNewCanvasFromImage } from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFileBold } from 'react-icons/pi';
export const ImageMenuItemNewCanvasFromImage = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const newCanvasFromImage = useNewCanvasFromImage();
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
newCanvasFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
return (
<MenuItem icon={<PiFileBold />} onClickCapture={onClick} isDisabled={isBusy}>
{t('controlLayers.newCanvasFromImage')}
</MenuItem>
);
});
ImageMenuItemNewCanvasFromImage.displayName = 'ImageMenuItemNewCanvasFromImage';

View File

@@ -0,0 +1,122 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import {
useNewCanvasFromImage,
useNewControlLayerFromImage,
useNewInpaintMaskFromImage,
useNewRasterLayerFromImage,
useNewRegionalGuidanceFromImage,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFileBold, PiPlusBold } from 'react-icons/pi';
export const ImageMenuItemNewFromImageSubMenu = memo(() => {
const { t } = useTranslation();
const subMenu = useSubMenu();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const isBusy = useCanvasIsBusy();
const newRasterLayerFromImage = useNewRasterLayerFromImage();
const newControlLayerFromImage = useNewControlLayerFromImage();
const newInpaintMaskFromImage = useNewInpaintMaskFromImage();
const newRegionalGuidanceFromImage = useNewRegionalGuidanceFromImage();
const newCanvasFromImage = useNewCanvasFromImage();
const onClickNewCanvasFromImage = useCallback(() => {
newCanvasFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newCanvasFromImage, t]);
const onClickNewRasterLayerFromImage = useCallback(() => {
dispatch(sentImageToCanvas());
newRasterLayerFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newRasterLayerFromImage, t]);
const onClickNewControlLayerFromImage = useCallback(() => {
dispatch(sentImageToCanvas());
newControlLayerFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newControlLayerFromImage, t]);
const onClickNewInpaintMaskFromImage = useCallback(() => {
dispatch(sentImageToCanvas());
newInpaintMaskFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newInpaintMaskFromImage, t]);
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
dispatch(sentImageToCanvas());
newRegionalGuidanceFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newRegionalGuidanceFromImage, t]);
return (
<MenuItem {...subMenu.parentMenuItemProps} icon={<PiPlusBold />}>
<Menu {...subMenu.menuProps}>
<MenuButton {...subMenu.menuButtonProps}>
<SubMenuButtonContent label="New from Image" />
</MenuButton>
<MenuList {...subMenu.menuListProps}>
<MenuItem icon={<PiFileBold />} onClickCapture={onClickNewCanvasFromImage} isDisabled={isBusy}>
{t('controlLayers.canvas')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewInpaintMaskFromImage} isDisabled={isBusy}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRegionalGuidanceFromImage} isDisabled={isBusy}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewControlLayerFromImage} isDisabled={isBusy}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClickNewRasterLayerFromImage} isDisabled={isBusy}>
{t('controlLayers.rasterLayer')}
</MenuItem>
</MenuList>
</Menu>
</MenuItem>
);
});
ImageMenuItemNewFromImageSubMenu.displayName = 'ImageMenuItemNewFromImageSubMenu';

View File

@@ -1,41 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { NewLayerIcon } from 'features/controlLayers/components/common/icons';
import { useNewRasterLayerFromImage } from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
import { sentImageToCanvas } from 'features/gallery/store/actions';
import { toast } from 'features/toast/toast';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const ImageMenuItemNewLayerFromImage = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const imageDTO = useImageDTOContext();
const imageViewer = useImageViewer();
const newRasterLayerFromImage = useNewRasterLayerFromImage();
const isBusy = useCanvasIsBusy();
const onClick = useCallback(() => {
dispatch(sentImageToCanvas());
newRasterLayerFromImage(imageDTO);
dispatch(setActiveTab('canvas'));
imageViewer.close();
toast({
id: 'SENT_TO_CANVAS',
title: t('toast.sentToCanvas'),
status: 'success',
});
}, [dispatch, imageDTO, imageViewer, newRasterLayerFromImage, t]);
return (
<MenuItem icon={<NewLayerIcon />} onClickCapture={onClick} isDisabled={isBusy}>
{t('controlLayers.newLayerFromImage')}
</MenuItem>
);
});
ImageMenuItemNewLayerFromImage.displayName = 'ImageMenuItemNewLayerFromImage';

View File

@@ -7,8 +7,7 @@ import { ImageMenuItemDelete } from 'features/gallery/components/ImageContextMen
import { ImageMenuItemDownload } from 'features/gallery/components/ImageContextMenu/ImageMenuItemDownload';
import { ImageMenuItemLoadWorkflow } from 'features/gallery/components/ImageContextMenu/ImageMenuItemLoadWorkflow';
import { ImageMenuItemMetadataRecallActions } from 'features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions';
import { ImageMenuItemNewCanvasFromImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImage';
import { ImageMenuItemNewLayerFromImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImage';
import { ImageMenuItemNewFromImageSubMenu } from 'features/gallery/components/ImageContextMenu/ImageMenuItemNewFromImageSubMenu';
import { ImageMenuItemOpenInNewTab } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInNewTab';
import { ImageMenuItemOpenInViewer } from 'features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer';
import { ImageMenuItemSelectForCompare } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSelectForCompare';
@@ -39,8 +38,7 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
<MenuDivider />
<ImageMenuItemSendToUpscale />
<CanvasManagerProviderGate>
<ImageMenuItemNewLayerFromImage />
<ImageMenuItemNewCanvasFromImage />
<ImageMenuItemNewFromImageSubMenu />
</CanvasManagerProviderGate>
<MenuDivider />
<ImageMenuItemChangeBoard />

View File

@@ -1 +1 @@
__version__ = "5.3.0rc1"
__version__ = "5.3.0rc2"