From b7b3683befcc26d6a1dcdacc714c75729b620b4b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 24 Aug 2024 12:20:35 +1000 Subject: [PATCH] feat(ui): duplicate entity --- invokeai/frontend/web/public/locales/en.json | 1 + .../ControlLayer/ControlLayerMenuItems.tsx | 2 + .../IPAdapter/IPAdapterMenuItems.tsx | 2 + .../InpaintMask/InpaintMaskMenuItems.tsx | 2 + .../RasterLayer/RasterLayerMenuItems.tsx | 2 + .../RegionalGuidanceMenuItems.tsx | 2 + .../common/CanvasEntityMenuItemsDuplicate.tsx | 25 ++++++++++++ .../controlLayers/store/canvasV2Slice.ts | 38 +++++++++++++++++++ 8 files changed, 74 insertions(+) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 821de44110..be063256d9 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1657,6 +1657,7 @@ "recalculateRects": "Recalculate Rects", "clipToBbox": "Clip Strokes to Bbox", "addLayer": "Add Layer", + "duplicate": "Duplicate", "moveToFront": "Move to Front", "moveToBack": "Move to Back", "moveForward": "Move Forward", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx index 97ab0eb4ce..accf350bea 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { ControlLayerMenuItemsControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster'; @@ -17,6 +18,7 @@ export const ControlLayerMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx index 315d3b06c4..d9ac61e4fb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { memo } from 'react'; export const IPAdapterMenuItems = memo(() => { @@ -8,6 +9,7 @@ export const IPAdapterMenuItems = memo(() => { <> + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx index ac19e78ffa..925ba30e35 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { memo } from 'react'; @@ -11,6 +12,7 @@ export const InpaintMaskMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx index d787fc39f4..723db3ab81 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RasterLayerMenuItemsRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl'; @@ -15,6 +16,7 @@ export const RasterLayerMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx index 2ae566688b..ac21fd6357 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItems.tsx @@ -1,6 +1,7 @@ import { MenuDivider } from '@invoke-ai/ui-library'; import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; +import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; import { RegionalGuidanceMenuItemsAddPromptsAndIPAdapter } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter'; import { RegionalGuidanceMenuItemsAutoNegative } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative'; @@ -16,6 +17,7 @@ export const RegionalGuidanceMenuItems = memo(() => { + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx new file mode 100644 index 0000000000..dd84a84f42 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate.tsx @@ -0,0 +1,25 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDuplicated } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyFill } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsDuplicate = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + + const onClick = useCallback(() => { + dispatch(entityDuplicated({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + }> + {t('controlLayers.duplicate')} + + ); +}); + +CanvasEntityMenuItemsDuplicate.displayName = 'CanvasEntityMenuItemsDuplicate'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index a9a04b163c..702e42786b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -3,6 +3,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; +import { getPrefixedId } from 'features/controlLayers/konva/util'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; import { controlLayersReducers } from 'features/controlLayers/store/controlLayersReducers'; @@ -237,6 +238,42 @@ export const canvasV2Slice = createSlice({ assert(false, 'Not implemented'); } }, + entityDuplicated: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + + const newEntity = deepClone(entity); + if (newEntity.name) { + newEntity.name = `${newEntity.name} (Copy)`; + } + switch (newEntity.type) { + case 'raster_layer': + newEntity.id = getPrefixedId('raster_layer'); + state.rasterLayers.entities.push(newEntity); + break; + case 'control_layer': + newEntity.id = getPrefixedId('control_layer'); + state.controlLayers.entities.push(newEntity); + break; + case 'regional_guidance': + newEntity.id = getPrefixedId('regional_guidance'); + state.regions.entities.push(newEntity); + break; + case 'ip_adapter': + newEntity.id = getPrefixedId('ip_adapter'); + state.ipAdapters.entities.push(newEntity); + break; + case 'inpaint_mask': + newEntity.id = getPrefixedId('inpaint_mask'); + state.inpaintMasks.entities.push(newEntity); + break; + } + + state.selectedEntityIdentifier = getEntityIdentifier(newEntity); + }, entityIsEnabledToggled: (state, action: PayloadAction) => { const { entityIdentifier } = action.payload; const entity = selectEntity(state, entityIdentifier); @@ -460,6 +497,7 @@ export const { entityReset, entityIsEnabledToggled, entityMoved, + entityDuplicated, entityRasterized, entityBrushLineAdded, entityEraserLineAdded,