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,