From 8acc6379fb45104d94a0c2553c84464b51d3f3e9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:19:35 +1000 Subject: [PATCH] refactor(ui): canvas v2 (wip) --- .../listeners/controlAdapterPreprocessor.ts | 36 +- .../listeners/imageDropped.ts | 16 +- .../listeners/imageUploaded.ts | 12 +- invokeai/frontend/web/src/app/store/store.ts | 32 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 8 +- .../web/src/common/util/arrayUtils.test.ts | 207 +++- .../web/src/common/util/arrayUtils.ts | 44 +- .../components/AddLayerButton.tsx | 6 +- .../components/AddPromptButtons.tsx | 12 +- .../CALayer/CALayerControlAdapterWrapper.tsx | 22 +- .../components/ControlLayersPanelContent.tsx | 8 +- .../IPALayer/IPALayerIPAdapterWrapper.tsx | 16 +- .../LayerCommon/LayerMenuArrangeActions.tsx | 4 +- .../LayerCommon/LayerMenuRGActions.tsx | 12 +- .../components/LayerCommon/LayerOpacity.tsx | 4 +- .../components/LayerCommon/LayerTitle.tsx | 4 +- .../components/RGLayer/RGLayer.tsx | 4 +- .../RGLayer/RGLayerAutoNegativeCheckbox.tsx | 6 +- .../components/RGLayer/RGLayerColorPicker.tsx | 6 +- .../RGLayer/RGLayerIPAdapterList.tsx | 4 +- .../RGLayer/RGLayerIPAdapterWrapper.tsx | 28 +- .../RGLayer/RGLayerNegativePrompt.tsx | 4 +- .../RGLayer/RGLayerPositivePrompt.tsx | 4 +- .../RGLayer/RGLayerPromptDeleteButton.tsx | 8 +- .../components/StageComponent.tsx | 53 +- .../controlLayers/components/ToolChooser.tsx | 4 +- .../controlLayers/hooks/addLayerHooks.ts | 12 +- .../controlLayers/hooks/layerStateHooks.ts | 12 +- .../features/controlLayers/konva/events.ts | 19 +- .../features/controlLayers/konva/naming.ts | 2 + .../controlLayers/konva/renderers/bbox.ts | 8 +- .../controlLayers/konva/renderers/layers.ts | 16 +- .../controlLayers/konva/renderers/objects.ts | 4 +- .../konva/renderers/previewLayer.ts | 4 +- .../src/features/controlLayers/konva/util.ts | 4 +- .../store/controlAdaptersSlice.ts | 282 +++++ .../controlLayers/store/controlLayersSlice.ts | 1019 ++--------------- .../controlLayers/store/inpaintMaskSlice.ts | 0 .../controlLayers/store/ipAdaptersSlice.ts | 140 +++ .../controlLayers/store/layersSlice.ts | 268 +++++ .../store/regionalGuidanceSlice.ts | 440 +++++++ .../src/features/controlLayers/store/types.ts | 263 +++-- .../controlLayers/util/controlAdapters.ts | 12 +- .../components/DeleteImageModal.tsx | 4 +- .../deleteImageModal/store/selectors.ts | 8 +- .../components/Boards/DeleteBoardModal.tsx | 4 +- .../metadata/components/MetadataLayers.tsx | 8 +- .../src/features/metadata/util/handlers.ts | 6 +- .../web/src/features/metadata/util/parsers.ts | 10 +- .../src/features/metadata/util/recallers.ts | 18 +- .../src/features/metadata/util/validators.ts | 8 +- .../util/graph/generation/addControlLayers.ts | 10 +- .../queue/components/QueueButtonTooltip.tsx | 4 +- .../ImageSettingsAccordion.tsx | 4 +- 54 files changed, 1759 insertions(+), 1394 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts index cd8fb69ca0..3c7d626e77 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts @@ -4,12 +4,12 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware' import type { AppDispatch } from 'app/store/store'; import { parseify } from 'common/util/serialize'; import { - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerProcessorPendingBatchIdChanged, - caLayerRecalled, + controlAdapterImageChanged, + controlAdapterModelChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, + controlAdapterProcessorPendingBatchIdChanged, + controlAdapterRecalled, } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters'; @@ -23,11 +23,11 @@ import { socketInvocationComplete } from 'services/events/actions'; import { assert } from 'tsafe'; const matcher = isAnyOf( - caLayerImageChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, - caLayerModelChanged, - caLayerRecalled + controlAdapterImageChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, + controlAdapterModelChanged, + controlAdapterRecalled ); const DEBOUNCE_MS = 300; @@ -46,7 +46,7 @@ const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batc } finally { req.reset(); // Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); } }; @@ -54,7 +54,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni startAppListening({ matcher, effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => { - const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId; + const layerId = controlAdapterRecalled.match(action) ? action.payload.id : action.payload.layerId; const state = getState(); const originalState = getOriginalState(); @@ -91,7 +91,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // - If we have no image, we have nothing to process // - If we have no processor config, we have nothing to process // Clear the processed image and bail - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); return; } @@ -132,7 +132,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni const enqueueResult = await req.unwrap(); // TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue'); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id })); log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued')); // Wait for the processor node to complete @@ -155,8 +155,8 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni // Whew! We made it. Update the layer with the processed image log.debug({ layerId, imageDTO }, 'ControlNet image processed'); - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO })); - dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterProcessorPendingBatchIdChanged({ layerId, batchId: null })); } catch (error) { if (signal.aborted) { // The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now). @@ -174,7 +174,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni if (error instanceof Object) { if ('data' in error && 'status' in error) { if (error.status === 403) { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); return; } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index b5431508cf..663a8f7d19 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -8,11 +8,11 @@ import { controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - caLayerImageChanged, + controlAdapterImageChanged, iiLayerImageChanged, - imageAdded, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, + layerImageAdded, + ipAdapterImageChanged, + regionalGuidanceIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types'; import { isValidDrop } from 'features/dnd/util/isValidDrop'; @@ -99,7 +99,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - caLayerImageChanged({ + controlAdapterImageChanged({ layerId, imageDTO: activeData.payload.imageDTO, }) @@ -117,7 +117,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - ipaLayerImageChanged({ + ipAdapterImageChanged({ layerId, imageDTO: activeData.payload.imageDTO, }) @@ -135,7 +135,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId, ipAdapterId } = overData.context; dispatch( - rgLayerIPAdapterImageChanged({ + regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO: activeData.payload.imageDTO, @@ -172,7 +172,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) => ) { const { layerId } = overData.context; dispatch( - imageAdded({ + layerImageAdded({ layerId, imageDTO: activeData.payload.imageDTO, }) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts index 1aa47345e1..3c9059d9c9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts @@ -6,10 +6,10 @@ import { controlAdapterIsEnabledChanged, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { - caLayerImageChanged, + controlAdapterImageChanged, iiLayerImageChanged, - ipaLayerImageChanged, - rgLayerIPAdapterImageChanged, + ipAdapterImageChanged, + regionalGuidanceIPAdapterImageChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors'; import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; @@ -122,7 +122,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') { const { layerId } = postUploadAction; - dispatch(caLayerImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), @@ -131,7 +131,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') { const { layerId } = postUploadAction; - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + dispatch(ipAdapterImageChanged({ layerId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), @@ -140,7 +140,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') { const { layerId, ipAdapterId } = postUploadAction; - dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage'), diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index cacea30e48..0fcb842db3 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -7,14 +7,16 @@ import type { JSONObject } from 'common/types'; import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { - controlAdaptersPersistConfig, - controlAdaptersSlice, -} from 'features/controlAdapters/store/controlAdaptersSlice'; + controlAdaptersV2PersistConfig, + controlAdaptersV2Slice, +} from 'features/controlLayers/store/controlAdaptersSlice'; +import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import { ipAdaptersPersistConfig, ipAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice'; +import { layersPersistConfig, layersSlice } from 'features/controlLayers/store/layersSlice'; import { - controlLayersPersistConfig, - controlLayersSlice, - controlLayersUndoableConfig, -} from 'features/controlLayers/store/controlLayersSlice'; + regionalGuidancePersistConfig, + regionalGuidanceSlice, +} from 'features/controlLayers/store/regionalGuidanceSlice'; import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice'; import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice'; @@ -49,6 +51,7 @@ import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; const allReducers = { + [api.reducerPath]: api.reducer, [canvasSlice.name]: canvasSlice.reducer, [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, @@ -56,7 +59,6 @@ const allReducers = { [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, [uiSlice.name]: uiSlice.reducer, - [controlAdaptersSlice.name]: controlAdaptersSlice.reducer, [dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer, [deleteImageModalSlice.name]: deleteImageModalSlice.reducer, [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, @@ -66,11 +68,14 @@ const allReducers = { [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, [hrfSlice.name]: hrfSlice.reducer, - [controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig), + [canvasV2Slice.name]: canvasV2Slice.reducer, [workflowSettingsSlice.name]: workflowSettingsSlice.reducer, - [api.reducerPath]: api.reducer, [upscaleSlice.name]: upscaleSlice.reducer, [stylePresetSlice.name]: stylePresetSlice.reducer, + [layersSlice.name]: layersSlice.reducer, + [controlAdaptersV2Slice.name]: controlAdaptersV2Slice.reducer, + [ipAdaptersSlice.name]: ipAdaptersSlice.reducer, + [regionalGuidanceSlice.name]: regionalGuidanceSlice.reducer, }; const rootReducer = combineReducers(allReducers); @@ -107,16 +112,19 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = { [systemPersistConfig.name]: systemPersistConfig, [workflowPersistConfig.name]: workflowPersistConfig, [uiPersistConfig.name]: uiPersistConfig, - [controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig, [dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig, [sdxlPersistConfig.name]: sdxlPersistConfig, [loraPersistConfig.name]: loraPersistConfig, [modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig, [hrfPersistConfig.name]: hrfPersistConfig, - [controlLayersPersistConfig.name]: controlLayersPersistConfig, + [canvasV2PersistConfig.name]: canvasV2PersistConfig, [workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig, [upscalePersistConfig.name]: upscalePersistConfig, [stylePresetPersistConfig.name]: stylePresetPersistConfig, + [layersPersistConfig.name]: layersPersistConfig, + [controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig, + [ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig, + [regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig, }; const unserialize: UnserializeFunction = (data, key) => { diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 1d610d32c2..ac47d9005d 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -6,8 +6,8 @@ import { selectControlAdaptersSlice, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { LayerData } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -24,7 +24,7 @@ import { forEach, upperFirst } from 'lodash-es'; import { useMemo } from 'react'; import { getConnectedEdges } from 'reactflow'; -const LAYER_TYPE_TO_TKEY: Record = { +const LAYER_TYPE_TO_TKEY: Record = { initial_image_layer: 'controlLayers.globalInitialImage', control_adapter_layer: 'controlLayers.globalControlAdapter', ip_adapter_layer: 'controlLayers.globalIPAdapter', @@ -41,7 +41,7 @@ const createSelector = (templates: Templates) => selectNodesSlice, selectWorkflowSettingsSlice, selectDynamicPromptsSlice, - selectControlLayersSlice, + selectCanvasV2Slice, activeTabNameSelector, selectUpscalelice, selectConfigSlice, diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts index 5d0fd090f7..e1922fdbbe 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.test.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.test.ts @@ -1,85 +1,170 @@ -import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { describe, expect, it } from 'vitest'; describe('Array Manipulation Functions', () => { const originalArray = ['a', 'b', 'c', 'd']; - describe('moveForwardOne', () => { - it('should move an item forward by one position', () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'b'); - expect(result).toEqual(['a', 'c', 'b', 'd']); - }); - it('should do nothing if the item is at the end', () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'd'); - expect(result).toEqual(['a', 'b', 'c', 'd']); - }); + describe('moveOneToEnd', () => { + describe('with callback', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveForward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item forward by one position', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the end', () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveToFront', () => { - it('should move an item to the front', () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'c'); - expect(result).toEqual(['c', 'a', 'b', 'd']); - }); + describe('moveToStart', () => { + describe('with callback', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); - it('should do nothing if the item is already at the front', () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'a'); - expect(result).toEqual(['a', 'b', 'c', 'd']); - }); + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveToFront(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item to the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'c'); + expect(result).toEqual(['c', 'a', 'b', 'd']); + }); + + it('should do nothing if the item is already at the front', () => { + const array = [...originalArray]; + const result = moveToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveBackwardsOne', () => { - it('should move an item backward by one position', () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'c'); - expect(result).toEqual(['a', 'c', 'b', 'd']); - }); + describe('moveOneToStart', () => { + describe('with callback', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); - it('should do nothing if the item is at the beginning', () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'a'); - expect(result).toEqual(['a', 'b', 'c', 'd']); - }); + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveBackward(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item backward by one position', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'c'); + expect(result).toEqual(['a', 'c', 'b', 'd']); + }); + + it('should do nothing if the item is at the beginning', () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'a'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveOneToStart(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); - describe('moveToBack', () => { - it('should move an item to the back', () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'b'); - expect(result).toEqual(['a', 'c', 'd', 'b']); - }); + describe('moveToEnd', () => { + describe('with callback', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); - it('should do nothing if the item is already at the back', () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'd'); - expect(result).toEqual(['a', 'b', 'c', 'd']); - }); + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); - it("should leave the array unchanged if the item isn't in the array", () => { - const array = [...originalArray]; - const result = moveToBack(array, (item) => item === 'z'); - expect(result).toEqual(originalArray); + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, (item) => item === 'z'); + expect(result).toEqual(originalArray); + }); + }); + describe('with item', () => { + it('should move an item to the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'b'); + expect(result).toEqual(['a', 'c', 'd', 'b']); + }); + + it('should do nothing if the item is already at the back', () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'd'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it("should leave the array unchanged if the item isn't in the array", () => { + const array = [...originalArray]; + const result = moveToEnd(array, 'z'); + expect(result).toEqual(originalArray); + }); }); }); }); diff --git a/invokeai/frontend/web/src/common/util/arrayUtils.ts b/invokeai/frontend/web/src/common/util/arrayUtils.ts index 38c99b63ec..9f0d4cfbf6 100644 --- a/invokeai/frontend/web/src/common/util/arrayUtils.ts +++ b/invokeai/frontend/web/src/common/util/arrayUtils.ts @@ -1,37 +1,45 @@ -export const moveForward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); - if (index >= 0 && index < array.length - 1) { - //@ts-expect-error - These indicies are safe per the previous check - [array[index], array[index + 1]] = [array[index + 1], array[index]]; - } - return array; -}; - -export const moveToFront = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToStart(array: T[], item: T): T[]; +export function moveToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index > 0) { const [item] = array.splice(index, 1); //@ts-expect-error - These indicies are safe per the previous check array.unshift(item); } return array; -}; +} -export const moveBackward = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveOneToStart(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToStart(array: T[], item: T): T[]; +export function moveOneToStart(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index > 0) { //@ts-expect-error - These indicies are safe per the previous check [array[index], array[index - 1]] = [array[index - 1], array[index]]; } return array; -}; +} -export const moveToBack = (array: T[], callback: (item: T) => boolean): T[] => { - const index = array.findIndex(callback); +export function moveToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveToEnd(array: T[], item: T): T[]; +export function moveToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); if (index >= 0 && index < array.length - 1) { const [item] = array.splice(index, 1); //@ts-expect-error - These indicies are safe per the previous check array.push(item); } return array; -}; +} + +export function moveOneToEnd(array: T[], selectItemCallback: (item: T) => boolean): T[]; +export function moveOneToEnd(array: T[], item: T): T[]; +export function moveOneToEnd(array: T[], arg1: T | ((item: T) => boolean)): T[] { + const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1); + if (index >= 0 && index < array.length - 1) { + //@ts-expect-error - These indicies are safe per the previous check + [array[index], array[index + 1]] = [array[index + 1], array[index]]; + } + return array; +} diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx index fdfa70ae2c..72d18c3d17 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddLayerButton.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useAddCALayer, useAddIILayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; -import { rasterLayerAdded, rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice'; +import { layerAdded, regionalGuidanceAdded } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiPlusBold } from 'react-icons/pi'; @@ -13,10 +13,10 @@ export const AddLayerButton = memo(() => { const [addIPALayer, isAddIPALayerDisabled] = useAddIPALayer(); const [addIILayer, isAddIILayerDisabled] = useAddIILayer(); const addRGLayer = useCallback(() => { - dispatch(rgLayerAdded()); + dispatch(regionalGuidanceAdded()); }, [dispatch]); const addRasterLayer = useCallback(() => { - dispatch(rasterLayerAdded()); + dispatch(layerAdded()); }, [dispatch]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx index e339d8315e..073a188871 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/AddPromptButtons.tsx @@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, - selectControlLayersSlice, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useCallback, useMemo } from 'react'; @@ -22,7 +22,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { @@ -34,10 +34,10 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx index 6c498fe1aa..7b0e3aa332 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CALayer/CALayerControlAdapterWrapper.tsx @@ -1,13 +1,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { ControlAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/ControlAdapter'; import { - caLayerControlModeChanged, - caLayerImageChanged, - caLayerModelChanged, - caLayerProcessedImageChanged, - caLayerProcessorConfigChanged, caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, + controlAdapterControlModeChanged, + controlAdapterImageChanged, + controlAdapterModelChanged, + controlAdapterProcessedImageChanged, + controlAdapterProcessorConfigChanged, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer } from 'features/controlLayers/store/types'; @@ -46,7 +46,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeControlMode = useCallback( (controlMode: ControlModeV2) => { dispatch( - caLayerControlModeChanged({ + controlAdapterControlModeChanged({ layerId, controlMode, }) @@ -64,7 +64,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeProcessorConfig = useCallback( (processorConfig: ProcessorConfig | null) => { - dispatch(caLayerProcessorConfigChanged({ layerId, processorConfig })); + dispatch(controlAdapterProcessorConfigChanged({ layerId, processorConfig })); }, [dispatch, layerId] ); @@ -72,7 +72,7 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeModel = useCallback( (modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => { dispatch( - caLayerModelChanged({ + controlAdapterModelChanged({ layerId, modelConfig, }) @@ -83,17 +83,17 @@ export const CALayerControlAdapterWrapper = memo(({ layerId }: Props) => { const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(caLayerImageChanged({ layerId, imageDTO })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO })); }, [dispatch, layerId] ); const onErrorLoadingImage = useCallback(() => { - dispatch(caLayerImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterImageChanged({ layerId, imageDTO: null })); }, [dispatch, layerId]); const onErrorLoadingProcessedImage = useCallback(() => { - dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null })); + dispatch(controlAdapterProcessedImageChanged({ layerId, imageDTO: null })); }, [dispatch, layerId]); const droppableData = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index d4baabab8b..90f21a7253 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -11,14 +11,14 @@ import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPALayer } from 'features/controlLayers/components/IPALayer/IPALayer'; import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { LayerData } from 'features/controlLayers/store/types'; import { isRenderableLayer } from 'features/controlLayers/store/types'; import { partition } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { +const selectLayerIdTypePairs = createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer); return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse(); }); @@ -50,7 +50,7 @@ ControlLayersPanelContent.displayName = 'ControlLayersPanelContent'; type LayerWrapperProps = { id: string; - type: Layer['type']; + type: LayerData['type']; }; const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx index 6492e3cf32..08737e6e60 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper.tsx @@ -3,10 +3,10 @@ import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter import { caOrIPALayerBeginEndStepPctChanged, caOrIPALayerWeightChanged, - ipaLayerCLIPVisionModelChanged, - ipaLayerImageChanged, - ipaLayerMethodChanged, - ipaLayerModelChanged, + ipAdapterCLIPVisionModelChanged, + ipAdapterImageChanged, + ipAdapterMethodChanged, + ipAdapterModelChanged, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isIPAdapterLayer } from 'features/controlLayers/store/types'; @@ -46,28 +46,28 @@ export const IPALayerIPAdapterWrapper = memo(({ layerId }: Props) => { const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(ipaLayerMethodChanged({ layerId, method })); + dispatch(ipAdapterMethodChanged({ layerId, method })); }, [dispatch, layerId] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(ipaLayerModelChanged({ layerId, modelConfig })); + dispatch(ipAdapterModelChanged({ layerId, modelConfig })); }, [dispatch, layerId] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(ipaLayerCLIPVisionModelChanged({ layerId, clipVisionModel })); + dispatch(ipAdapterCLIPVisionModelChanged({ layerId, clipVisionModel })); }, [dispatch, layerId] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(ipaLayerImageChanged({ layerId, imageDTO })); + dispatch(ipAdapterImageChanged({ layerId, imageDTO })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx index 3e65eda783..67cf856c81 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx @@ -6,7 +6,7 @@ import { layerMovedForward, layerMovedToBack, layerMovedToFront, - selectControlLayersSlice, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRenderableLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; @@ -21,7 +21,7 @@ export const LayerMenuArrangeActions = memo(({ layerId }: Props) => { const { t } = useTranslation(); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`); const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx index 905abfd00d..7addbf45eb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx @@ -3,9 +3,9 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAddIPAdapterToIPALayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, - selectControlLayersSlice, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; @@ -21,7 +21,7 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToIPALayer(layerId); const selectValidActions = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { @@ -33,10 +33,10 @@ export const LayerMenuRGActions = memo(({ layerId }: Props) => { ); const validActions = useAppSelector(selectValidActions); const addPositivePrompt = useCallback(() => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); const addNegativePrompt = useCallback(() => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: '' })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' })); }, [dispatch, layerId]); return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx index f488d9600a..481a6597a8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx @@ -16,7 +16,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; import { layerOpacityChanged, - selectControlLayersSlice, + selectCanvasV2Slice, selectLayerOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import { isLayerWithOpacity } from 'features/controlLayers/store/types'; @@ -36,7 +36,7 @@ export const LayerOpacity = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const selectOpacity = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = selectLayerOrThrow(controlLayers.present, layerId, isLayerWithOpacity); return Math.round(layer.opacity * 100); }), diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx index a74729d91b..053fbd234e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerTitle.tsx @@ -1,10 +1,10 @@ import { Text } from '@invoke-ai/ui-library'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - type: Layer['type']; + type: LayerData['type']; }; export const LayerTitle = memo(({ type }: Props) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx index fa552dd4cf..54e6b502e6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx @@ -8,7 +8,7 @@ import { LayerMenu } from 'features/controlLayers/components/LayerCommon/LayerMe import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle'; import { LayerIsEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle'; import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper'; -import { layerSelected, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,7 +29,7 @@ export const RGLayer = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const selector = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx index c5a7be1c3e..ec52062062 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx @@ -1,7 +1,7 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgLayerAutoNegativeChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidanceAutoNegativeChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import type { ChangeEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -15,7 +15,7 @@ type Props = { const useAutoNegative = (layerId: string) => { const selectAutoNegative = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); return layer.autoNegative; @@ -32,7 +32,7 @@ export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => { const autoNegative = useAutoNegative(layerId); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(rgLayerAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); + dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx index 78c16a773b..40660a1ac2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx @@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { stopPropagation } from 'common/util/stopPropagation'; import { rgbColorToString } from 'features/canvas/util/colorToString'; -import { rgLayerPreviewColorChanged, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { rgFillChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useCallback, useMemo } from 'react'; import type { RgbColor } from 'react-colorful'; @@ -19,7 +19,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const { t } = useTranslation(); const selectColor = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`); return layer.previewColor; @@ -30,7 +30,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => { const dispatch = useAppDispatch(); const onColorChange = useCallback( (color: RgbColor) => { - dispatch(rgLayerPreviewColorChanged({ layerId, color })); + dispatch(rgFillChanged({ layerId, color })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx index 1d5698ce03..9a38123d4b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx @@ -2,7 +2,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { memo, useMemo } from 'react'; import { assert } from 'tsafe'; @@ -14,7 +14,7 @@ type Props = { export const RGLayerIPAdapterList = memo(({ layerId }: Props) => { const selectIPAdapterIds = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.ipAdapters; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx index f7be62eb0a..f0879f07cf 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx @@ -2,13 +2,13 @@ import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter'; import { - rgLayerIPAdapterBeginEndStepPctChanged, - rgLayerIPAdapterCLIPVisionModelChanged, - rgLayerIPAdapterDeleted, - rgLayerIPAdapterImageChanged, - rgLayerIPAdapterMethodChanged, - rgLayerIPAdapterModelChanged, - rgLayerIPAdapterWeightChanged, + regionalGuidanceIPAdapterBeginEndStepPctChanged, + regionalGuidanceIPAdapterCLIPVisionModelChanged, + regionalGuidanceIPAdapterDeleted, + regionalGuidanceIPAdapterImageChanged, + regionalGuidanceIPAdapterMethodChanged, + regionalGuidanceIPAdapterModelChanged, + regionalGuidanceIPAdapterWeightChanged, selectRGLayerIPAdapterOrThrow, } from 'features/controlLayers/store/controlLayersSlice'; import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; @@ -26,14 +26,14 @@ type Props = { export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => { const dispatch = useAppDispatch(); const onDeleteIPAdapter = useCallback(() => { - dispatch(rgLayerIPAdapterDeleted({ layerId, ipAdapterId })); + dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId })); }, [dispatch, ipAdapterId, layerId]); const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.controlLayers.present, layerId, ipAdapterId)); const onChangeBeginEndStepPct = useCallback( (beginEndStepPct: [number, number]) => { dispatch( - rgLayerIPAdapterBeginEndStepPctChanged({ + regionalGuidanceIPAdapterBeginEndStepPctChanged({ layerId, ipAdapterId, beginEndStepPct, @@ -45,35 +45,35 @@ export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNu const onChangeWeight = useCallback( (weight: number) => { - dispatch(rgLayerIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); + dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight })); }, [dispatch, ipAdapterId, layerId] ); const onChangeIPMethod = useCallback( (method: IPMethodV2) => { - dispatch(rgLayerIPAdapterMethodChanged({ layerId, ipAdapterId, method })); + dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method })); }, [dispatch, ipAdapterId, layerId] ); const onChangeModel = useCallback( (modelConfig: IPAdapterModelConfig) => { - dispatch(rgLayerIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); + dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig })); }, [dispatch, ipAdapterId, layerId] ); const onChangeCLIPVisionModel = useCallback( (clipVisionModel: CLIPVisionModelV2) => { - dispatch(rgLayerIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); + dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel })); }, [dispatch, ipAdapterId, layerId] ); const onChangeImage = useCallback( (imageDTO: ImageDTO | null) => { - dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); + dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO })); }, [dispatch, ipAdapterId, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx index ba02aa9242..92ae46a131 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { rgLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: v })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx index 6f85ea077c..34c4366dfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx @@ -2,7 +2,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { rgLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -21,7 +21,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: v })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v })); }, [dispatch, layerId] ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx index 62a4ddfaeb..cbc99e70de 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx @@ -1,8 +1,8 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { - rgLayerNegativePromptChanged, - rgLayerPositivePromptChanged, + regionalGuidanceNegativePromptChanged, + regionalGuidancePositivePromptChanged, } from 'features/controlLayers/store/controlLayersSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,9 +18,9 @@ export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => const dispatch = useAppDispatch(); const onClick = useCallback(() => { if (polarity === 'positive') { - dispatch(rgLayerPositivePromptChanged({ layerId, prompt: null })); + dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: null })); } else { - dispatch(rgLayerNegativePromptChanged({ layerId, prompt: null })); + dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: null })); } }, [dispatch, layerId, polarity]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index c03c985cfa..f285624fe6 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -2,8 +2,8 @@ import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-li import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { logger } from 'app/logging/logger'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { BRUSH_SPACING_PCT, @@ -15,16 +15,16 @@ import { setStageEventHandlers } from 'features/controlLayers/konva/events'; import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers'; import { $bbox, - $brushColor, - $brushSize, $brushSpacingPx, + $brushWidth, + $fill, + $invertScroll, $isDrawing, $isMouseDown, $lastAddedPoint, $lastCursorPos, $lastMouseDownPos, $selectedLayer, - $shouldInvertBrushSizeScrollDirection, $spaceKey, $stageAttrs, $tool, @@ -37,15 +37,16 @@ import { layerTranslated, linePointsAdded, rectAdded, - selectControlLayersSlice, + selectCanvasV2Slice, } from 'features/controlLayers/store/controlLayersSlice'; +import { selectLayersSlice } from 'features/controlLayers/store/layersSlice'; +import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice'; import type { AddBrushLineArg, AddEraserLineArg, AddPointToLineArg, AddRectShapeArg, } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { clamp } from 'lodash-es'; @@ -60,26 +61,26 @@ Konva.showWarnings = false; const log = logger('controlLayers'); -const selectBrushColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { - const layer = controlLayers.present.layers - .filter(isRegionalGuidanceLayer) - .find((l) => l.id === controlLayers.present.selectedLayerId); +const selectBrushColor = createSelector( + selectCanvasV2Slice, + selectLayersSlice, + selectRegionalGuidanceSlice, + (canvas, layers, regionalGuidance) => { + const rg = regionalGuidance.regions.find((i) => i.id === canvas.lastSelectedItem?.id); - if (layer) { - return { ...layer.previewColor, a: controlLayers.present.globalMaskLayerOpacity }; + if (rg) { + return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity }); + } + + return rgbaColorToString(canvas.tool.fill); } +); - return controlLayers.present.brushColor; -}); - -const selectSelectedLayer = createSelector(selectControlLayersSlice, (controlLayers) => { +const selectSelectedLayer = createSelector(selectCanvasV2Slice, (controlLayers) => { return controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId) ?? null; }); -const selectLayerCount = createSelector( - selectControlLayersSlice, - (controlLayers) => controlLayers.present.layers.length -); +const selectLayerCount = createSelector(selectCanvasV2Slice, (controlLayers) => controlLayers.present.layers.length); const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => { const dispatch = useAppDispatch(); @@ -100,11 +101,11 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, ); useLayoutEffect(() => { - $brushColor.set(brushColor); - $brushSize.set(state.brushSize); + $fill.set(brushColor); + $brushWidth.set(state.brushSize); $brushSpacingPx.set(brushSpacingPx); $selectedLayer.set(selectedLayer); - $shouldInvertBrushSizeScrollDirection.set(shouldInvertBrushSizeScrollDirection); + $invertScroll.set(shouldInvertBrushSizeScrollDirection); $bbox.set(state.bbox); }, [ brushSpacingPx, @@ -196,8 +197,8 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, setIsDrawing: $isDrawing.set, getIsMouseDown: $isMouseDown.get, setIsMouseDown: $isMouseDown.set, - getBrushColor: $brushColor.get, - getBrushSize: $brushSize.get, + getBrushColor: $fill.get, + getBrushSize: $brushWidth.get, getBrushSpacingPx: $brushSpacingPx.get, getSelectedLayer: $selectedLayer.get, getLastAddedPoint: $lastAddedPoint.get, @@ -206,7 +207,7 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, setLastCursorPos: $lastCursorPos.set, getLastMouseDownPos: $lastMouseDownPos.get, setLastMouseDownPos: $lastMouseDownPos.set, - getShouldInvert: $shouldInvertBrushSizeScrollDirection.get, + getShouldInvert: $invertScroll.get, getSpaceKey: $spaceKey.get, setStageAttrs: $stageAttrs.set, onBrushSizeChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 9e26fed592..c1daa11df4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $tool, layerReset, - selectControlLayersSlice, + selectCanvasV2Slice, selectedLayerDeleted, } from 'features/controlLayers/store/controlLayersSlice'; import { useCallback } from 'react'; @@ -20,7 +20,7 @@ import { PiRectangleBold, } from 'react-icons/pi'; -const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => { +const selectIsDisabled = createSelector(selectCanvasV2Slice, (controlLayers) => { const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId); return selectedLayer?.type !== 'regional_guidance_layer' && selectedLayer?.type !== 'raster_layer'; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts index 244e57c655..5e0976ed59 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/addLayerHooks.ts @@ -1,9 +1,9 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { - caLayerAdded, + controlAdapterAdded, iiLayerAdded, - ipaLayerAdded, - rgLayerIPAdapterAdded, + ipAdapterAdded, + regionalGuidanceIPAdapterAdded, } from 'features/controlLayers/store/controlLayersSlice'; import { isInitialImageLayer } from 'features/controlLayers/store/types'; import { @@ -46,7 +46,7 @@ export const useAddCALayer = () => { processorConfig, }); - dispatch(caLayerAdded(controlAdapter)); + dispatch(controlAdapterAdded(controlAdapter)); }, [dispatch, model, baseModel]); return [addCALayer, isDisabled] as const; @@ -70,7 +70,7 @@ export const useAddIPALayer = () => { const ipAdapter = buildIPAdapter(id, { model: zModelIdentifierField.parse(model), }); - dispatch(ipaLayerAdded(ipAdapter)); + dispatch(ipAdapterAdded(ipAdapter)); }, [dispatch, model]); return [addIPALayer, isDisabled] as const; @@ -94,7 +94,7 @@ export const useAddIPAdapterToIPALayer = (layerId: string) => { const ipAdapter = buildIPAdapter(id, { model: zModelIdentifierField.parse(model), }); - dispatch(rgLayerIPAdapterAdded({ layerId, ipAdapter })); + dispatch(regionalGuidanceIPAdapterAdded({ layerId, ipAdapter })); }, [dispatch, model, layerId]); return [addIPAdapter, isDisabled] as const; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts index c643b863fd..c9dfeb4e12 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/layerStateHooks.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { useMemo } from 'react'; import { assert } from 'tsafe'; @@ -9,7 +9,7 @@ import { assert } from 'tsafe'; export const useLayerPositivePrompt = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`); @@ -24,7 +24,7 @@ export const useLayerPositivePrompt = (layerId: string) => { export const useLayerNegativePrompt = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`); assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`); @@ -39,7 +39,7 @@ export const useLayerNegativePrompt = (layerId: string) => { export const useLayerIsEnabled = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.isEnabled; @@ -53,7 +53,7 @@ export const useLayerIsEnabled = (layerId: string) => { export const useLayerType = (layerId: string) => { const selectLayer = useMemo( () => - createSelector(selectControlLayersSlice, (controlLayers) => { + createSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return layer.type; @@ -67,7 +67,7 @@ export const useLayerType = (layerId: string) => { export const useCALayerOpacity = (layerId: string) => { const selectLayer = useMemo( () => - createMemoizedSelector(selectControlLayersSlice, (controlLayers) => { + createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => { const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId); assert(layer, `Layer ${layerId} not found`); return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts index 12a0efdfb7..1f1e99f316 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts @@ -6,7 +6,7 @@ import type { AddEraserLineArg, AddPointToLineArg, AddRectShapeArg, - Layer, + LayerData, StageAttrs, Tool, } from 'features/controlLayers/store/types'; @@ -38,7 +38,7 @@ type Arg = { getBrushColor: () => RgbaColor; getBrushSize: () => number; getBrushSpacingPx: () => number; - getSelectedLayer: () => Layer | null; + getSelectedLayer: () => LayerData | null; getShouldInvert: () => boolean; getSpaceKey: () => boolean; onBrushLineAdded: (arg: AddBrushLineArg) => void; @@ -72,7 +72,7 @@ const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastC * @param onPointAddedToLine The callback to add a point to a line */ const maybeAddNextPoint = ( - layerId: string, + selectedLayer: LayerData, currentPos: Vector2d, getLastAddedPoint: Arg['getLastAddedPoint'], setLastAddedPoint: Arg['setLastAddedPoint'], @@ -88,7 +88,7 @@ const maybeAddNextPoint = ( } } setLastAddedPoint(currentPos); - onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); + onPointAddedToLine({ layerId, point: [currentPos.x - selectedLayer.x, currentPos.y - selectedLayer.y] }); }; export const setStageEventHandlers = ({ @@ -158,7 +158,7 @@ export const setStageEventHandlers = ({ if (tool === 'brush') { onBrushLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); } @@ -166,7 +166,7 @@ export const setStageEventHandlers = ({ if (tool === 'eraser') { onEraserLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], }); } @@ -262,7 +262,7 @@ export const setStageEventHandlers = ({ // Start a new line onBrushLineAdded({ layerId: selectedLayer.id, - points: [pos.x, pos.y, pos.x, pos.y], + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR, }); setIsDrawing(true); @@ -282,7 +282,10 @@ export const setStageEventHandlers = ({ ); } else { // Start a new line - onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] }); + onEraserLineAdded({ + layerId: selectedLayer.id, + points: [pos.x - selectedLayer.x, pos.y - selectedLayer.y, pos.x - selectedLayer.x, pos.y - selectedLayer.y], + }); setIsDrawing(true); } } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts index 7fd088dc73..6c2b019fd8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/naming.ts @@ -41,6 +41,8 @@ export const RASTER_LAYER_ERASER_LINE_NAME = 'raster_layer.eraser_line'; export const RASTER_LAYER_RECT_SHAPE_NAME = 'raster_layer.rect_shape'; export const RASTER_LAYER_IMAGE_NAME = 'raster_layer.image'; +export const INPAINT_MASK_LAYER_NAME = 'inpaint_mask_layer'; + // Getters for non-singleton layer and object IDs export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`; export const getRasterLayerId = (layerId: string) => `${RASTER_LAYER_NAME}_${layerId}`; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts index 316ef85110..bc4c42fbd2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/bbox.ts @@ -6,8 +6,8 @@ import { RG_LAYER_OBJECT_GROUP_NAME, } from 'features/controlLayers/konva/naming'; import { createBboxRect } from 'features/controlLayers/konva/renderers/objects'; -import type { Layer } from 'features/controlLayers/store/types'; -import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; +import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect } from 'konva/lib/types'; import { assert } from 'tsafe'; @@ -185,10 +185,10 @@ const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RAST */ export const updateBboxes = ( stage: Konva.Stage, - layerStates: Layer[], + layerStates: LayerData[], onBboxChanged: (layerId: string, bbox: IRect | null) => void ): void => { - for (const layerState of layerStates.filter(isRGOrRasterlayer)) { + for (const layerState of layerStates) { const konvaLayer = stage.findOne(`#${layerState.id}`); assert(konvaLayer, `Layer ${layerState.id} not found in stage`); // We only need to recalculate the bbox if the layer has changed diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index 18e893fb24..266bfd4aaa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -7,10 +7,11 @@ import { renderBboxPreview, renderToolPreview } from 'features/controlLayers/kon import { renderRasterLayer } from 'features/controlLayers/konva/renderers/rasterLayer'; import { renderRGLayer } from 'features/controlLayers/konva/renderers/rgLayer'; import { mapId, selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { Layer, Tool } from 'features/controlLayers/store/types'; +import type { LayerData, Tool } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, + isInpaintMaskLayer, isRasterLayer, isRegionalGuidanceLayer, isRenderableLayer, @@ -34,7 +35,7 @@ import type { ImageDTO } from 'services/api/types'; */ const renderLayers = ( stage: Konva.Stage, - layerStates: Layer[], + layerStates: LayerData[], globalMaskLayerOpacity: number, tool: Tool, getImageDTO: (imageName: string) => Promise, @@ -52,15 +53,14 @@ const renderLayers = ( for (const layer of layerStates) { if (isRegionalGuidanceLayer(layer)) { renderRGLayer(stage, layer, globalMaskLayerOpacity, tool, zIndex, onLayerPosChanged); - } - if (isControlAdapterLayer(layer)) { + } else if (isControlAdapterLayer(layer)) { renderCALayer(stage, layer, zIndex, getImageDTO); - } - if (isInitialImageLayer(layer)) { + } else if (isInitialImageLayer(layer)) { renderIILayer(stage, layer, zIndex, getImageDTO); - } - if (isRasterLayer(layer)) { + } else if (isRasterLayer(layer)) { renderRasterLayer(stage, layer, tool, zIndex, onLayerPosChanged); + } else if (isInpaintMaskLayer(layer)) { + // } // IP Adapter layers are not rendered // Increment the z-index for the tool layer diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index d9ea85e9ca..351a39301e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -5,7 +5,7 @@ import { LAYER_BBOX_NAME, PREVIEW_GENERATION_BBOX_DUMMY_RECT, } from 'features/controlLayers/konva/naming'; -import type { BrushLine, EraserLine, ImageObject, Layer, RectShape } from 'features/controlLayers/store/types'; +import type { BrushLine, EraserLine, ImageObject, LayerData, RectShape } from 'features/controlLayers/store/types'; import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; @@ -177,7 +177,7 @@ export const createImageObjectGroup = async ( * @param layerState The layer state for the layer to create the bounding box for * @param konvaLayer The konva layer to attach the bounding box to */ -export const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect => { +export const createBboxRect = (layerState: LayerData, konvaLayer: Konva.Layer): Konva.Rect => { const rect = new Konva.Rect({ id: getLayerBboxId(layerState.id), name: LAYER_BBOX_NAME, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts index 1947d868e3..d58d459ee0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/previewLayer.ts @@ -18,7 +18,7 @@ import { PREVIEW_TOOL_GROUP_ID, } from 'features/controlLayers/konva/naming'; import { selectRenderableLayers } from 'features/controlLayers/konva/util'; -import type { Layer, RgbaColor, Tool } from 'features/controlLayers/store/types'; +import type { LayerData, RgbaColor, Tool } from 'features/controlLayers/store/types'; import Konva from 'konva'; import type { IRect, Vector2d } from 'konva/lib/types'; import { atom } from 'nanostores'; @@ -338,7 +338,7 @@ export const renderToolPreview = ( stage: Konva.Stage, tool: Tool, brushColor: RgbaColor, - selectedLayerType: Layer['type'] | null, + selectedLayerType: LayerData['type'] | null, globalMaskLayerOpacity: number, cursorPos: Vector2d | null, lastMouseDownPos: Vector2d | null, diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts index 1143940bfe..1e28392f2a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts @@ -1,6 +1,7 @@ import { CA_LAYER_NAME, INITIAL_IMAGE_LAYER_NAME, + INPAINT_MASK_LAYER_NAME, RASTER_LAYER_BRUSH_LINE_NAME, RASTER_LAYER_ERASER_LINE_NAME, RASTER_LAYER_IMAGE_NAME, @@ -98,7 +99,8 @@ export const selectRenderableLayers = (node: Konva.Node): boolean => node.name() === RG_LAYER_NAME || node.name() === CA_LAYER_NAME || node.name() === INITIAL_IMAGE_LAYER_NAME || - node.name() === RASTER_LAYER_NAME; + node.name() === RASTER_LAYER_NAME || + node.name() === INPAINT_MASK_LAYER_NAME; /** * Konva selection callback to select RG mask objects. This includes lines and rects. diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts new file mode 100644 index 0000000000..f53de4eb5a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersSlice.ts @@ -0,0 +1,282 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/util/controlAdapters'; +import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { ControlAdapterConfig, ControlAdapterData, Filter } from './types'; + +type ControlAdaptersV2State = { + _version: 1; + controlAdapters: ControlAdapterData[]; +}; + +const initialState: ControlAdaptersV2State = { + _version: 1, + controlAdapters: [], +}; + +const selectCa = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id); + +export const controlAdaptersV2Slice = createSlice({ + name: 'controlAdaptersV2', + initialState, + reducers: { + caAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => { + const { id, config } = action.payload; + state.controlAdapters.push({ + id, + type: 'control_adapter', + x: 0, + y: 0, + bbox: null, + bboxNeedsUpdate: false, + isEnabled: true, + opacity: 1, + filter: 'lightness_to_alpha', + processorPendingBatchId: null, + ...config, + }); + }, + prepare: (config: ControlAdapterConfig) => ({ + payload: { id: uuidv4(), config }, + }), + }, + caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => { + state.controlAdapters.push(action.payload.data); + }, + caIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.isEnabled = isEnabled; + }, + caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.x = x; + ca.y = y; + }, + caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = bbox; + ca.bboxNeedsUpdate = false; + }, + caDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id); + }, + caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.opacity = opacity; + }, + caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveOneToEnd(state.controlAdapters, ca); + }, + caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveToEnd(state.controlAdapters, ca); + }, + caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveOneToStart(state.controlAdapters, ca); + }, + caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + moveToStart(state.controlAdapters, ca); + }, + caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + if (imageDTO) { + const newImage = imageDTOToImageWithDims(imageDTO); + if (isEqual(newImage, ca.image)) { + return; + } + ca.image = newImage; + ca.processedImage = null; + } else { + ca.image = null; + ca.processedImage = null; + } + }, + caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.bbox = null; + ca.bboxNeedsUpdate = true; + ca.isEnabled = true; + ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + caModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + if (!modelConfig) { + ca.model = null; + return; + } + ca.model = zModelIdentifierField.parse(modelConfig); + + // We may need to convert the CA to match the model + if (!ca.controlMode && ca.model.type === 'controlnet') { + ca.controlMode = 'balanced'; + } else if (ca.controlMode && ca.model.type === 't2i_adapter') { + ca.controlMode = null; + } + + const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); + if (candidateProcessorConfig?.type !== ca.processorConfig?.type) { + // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth + // model. We need to use the new processor. + ca.processedImage = null; + ca.processorConfig = candidateProcessorConfig; + } + }, + caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => { + const { id, controlMode } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.controlMode = controlMode; + }, + caProcessorConfigChanged: ( + state, + action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }> + ) => { + const { id, processorConfig } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.processorConfig = processorConfig; + if (!processorConfig) { + ca.processedImage = null; + } + }, + caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => { + const { id, filter } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.filter = filter; + }, + caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => { + const { id, batchId } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.processorPendingBatchId = batchId; + }, + caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.weight = weight; + }, + caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ca = selectCa(state, id); + if (!ca) { + return; + } + ca.beginEndStepPct = beginEndStepPct; + }, + }, +}); + +export const { + caAdded, + caBboxChanged, + caDeleted, + caIsEnabledChanged, + caMovedBackwardOne, + caMovedForwardOne, + caMovedToBack, + caMovedToFront, + caOpacityChanged, + caTranslated, + caRecalled, + caImageChanged, + caProcessedImageChanged, + caModelChanged, + caControlModeChanged, + caProcessorConfigChanged, + caFilterChanged, + caProcessorPendingBatchIdChanged, + caWeightChanged, + caBeginEndStepPctChanged, +} = controlAdaptersV2Slice.actions; + +export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const controlAdaptersV2PersistConfig: PersistConfig = { + name: controlAdaptersV2Slice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts index 444eca8946..71df710258 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersSlice.ts @@ -1,93 +1,50 @@ -import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; -import { createSlice, isAnyOf } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; -import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; -import { - getBrushLineId, - getCALayerId, - getEraserLineId, - getImageObjectId, - getIPALayerId, - getRasterLayerId, - getRectShapeId, - getRGLayerId, - INITIAL_IMAGE_LAYER_ID, -} from 'features/controlLayers/konva/naming'; -import type { - CLIPVisionModelV2, - ControlModeV2, - ControlNetConfigV2, - IPAdapterConfigV2, - IPMethodV2, - ProcessorConfig, - T2IAdapterConfigV2, -} from 'features/controlLayers/util/controlAdapters'; -import { - buildControlAdapterProcessorV2, - controlNetToT2IAdapter, - imageDTOToImageWithDims, - t2iAdapterToControlNet, -} from 'features/controlLayers/util/controlAdapters'; -import { zModelIdentifierField } from 'features/nodes/types/common'; import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize'; import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import { modelChanged } from 'features/parameters/store/generationSlice'; -import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; import type { IRect, Vector2d } from 'konva/lib/types'; -import { isEqual, partition, unset } from 'lodash-es'; import { atom } from 'nanostores'; -import type { RgbColor } from 'react-colorful'; -import type { UndoableOptions } from 'redux-undo'; -import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; -import { assert } from 'tsafe'; -import { v4 as uuidv4 } from 'uuid'; import type { - AddBrushLineArg, - AddEraserLineArg, - AddImageObjectArg, - AddPointToLineArg, - AddRectShapeArg, - ControlAdapterLayer, - ControlLayersState, - InitialImageLayer, - IPAdapterLayer, - Layer, - RasterLayer, - RegionalGuidanceLayer, + CanvasV2State, + ControlAdapterData, + IPAdapterData, + LayerData, + RegionalGuidanceData, RgbaColor, StageAttrs, Tool, } from './types'; -import { - DEFAULT_RGBA_COLOR, - isCAOrIPALayer, - isControlAdapterLayer, - isInitialImageLayer, - isIPAdapterLayer, - isLine, - isRasterLayer, - isRegionalGuidanceLayer, - isRenderableLayer, - isRGOrRasterlayer, -} from './types'; +import { DEFAULT_RGBA_COLOR } from './types'; -export const initialControlLayersState: ControlLayersState = { +const initialState: CanvasV2State = { _version: 3, - selectedLayerId: null, - brushSize: 100, - brushColor: DEFAULT_RGBA_COLOR, - layers: [], - globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity - positivePrompt: '', - negativePrompt: '', - positivePrompt2: '', - negativePrompt2: '', - shouldConcatPrompts: true, + lastSelectedItem: null, + prompts: { + positivePrompt: '', + negativePrompt: '', + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, + }, + tool: { + selected: 'bbox', + selectedBuffer: null, + invertScroll: false, + fill: DEFAULT_RGBA_COLOR, + brush: { + width: 50, + }, + eraser: { + width: 50, + }, + }, size: { width: 512, height: 512, @@ -101,687 +58,24 @@ export const initialControlLayersState: ControlLayersState = { }, }; -/** - * A selector that accepts a type guard and returns the first layer that matches the guard. - * Throws if the layer is not found or does not match the guard. - */ -export const selectLayerOrThrow = ( - state: ControlLayersState, - layerId: string, - predicate: (layer: Layer) => layer is T -): T => { - const layer = state.layers.find((l) => l.id === layerId); - assert(layer && predicate(layer)); - return layer; -}; - -export const selectRGLayerIPAdapterOrThrow = ( - state: ControlLayersState, - layerId: string, - ipAdapterId: string -): IPAdapterConfigV2 => { - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - const ipAdapter = layer.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId); - assert(ipAdapter); - return ipAdapter; -}; - -const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => { - const rgLayers = state.layers.filter(isRegionalGuidanceLayer); - const lastColor = rgLayers[rgLayers.length - 1]?.previewColor; - return LayerColors.next(lastColor); -}; -const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => { - for (const layer of state.layers) { - layer.isSelected = layer.id === layerId; - } - state.selectedLayerId = layerId; -}; - -export const controlLayersSlice = createSlice({ - name: 'controlLayers', - initialState: initialControlLayersState, +export const canvasV2Slice = createSlice({ + name: 'canvasV2', + initialState, reducers: { - //#region Any Layer Type - layerSelected: (state, action: PayloadAction) => { - exclusivelySelectLayer(state, action.payload); - }, - layerIsEnabledToggled: (state, action: PayloadAction) => { - const layer = state.layers.find((l) => l.id === action.payload); - if (layer) { - layer.isEnabled = !layer.isEnabled; - } - }, - layerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => { - const { layerId, x, y } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isRenderableLayer(layer)) { - layer.x = x; - layer.y = y; - } - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => { - const { layerId, bbox } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isRenderableLayer(layer)) { - layer.bbox = bbox; - layer.bboxNeedsUpdate = false; - if (bbox === null) { - // The layer was fully erased, empty its objects to prevent accumulation of invisible objects - if (isRegionalGuidanceLayer(layer)) { - layer.objects = []; - layer.uploadedMaskImage = null; - } - if (isRasterLayer(layer)) { - layer.objects = []; - } - } - } - }, - layerReset: (state, action: PayloadAction) => { - const layer = state.layers.find((l) => l.id === action.payload); - // TODO(psyche): Should other layer types also have reset functionality? - if (isRegionalGuidanceLayer(layer)) { - layer.objects = []; - layer.bbox = null; - layer.isEnabled = true; - layer.bboxNeedsUpdate = false; - layer.uploadedMaskImage = null; - } - if (isRasterLayer(layer)) { - layer.isEnabled = true; - layer.objects = []; - layer.bbox = null; - layer.bboxNeedsUpdate = false; - } - }, - layerDeleted: (state, action: PayloadAction) => { - state.layers = state.layers.filter((l) => l.id !== action.payload); - state.selectedLayerId = state.layers[0]?.id ?? null; - }, - layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => { - const { layerId, opacity } = action.payload; - const layer = state.layers.find((l) => l.id === layerId); - if (isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)) { - layer.opacity = opacity; - } - }, - layerMovedForward: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - moveForward(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedToFront: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - // Because the layers are in reverse order, moving to the front is equivalent to moving to the back - moveToBack(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedBackward: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - moveBackward(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - layerMovedToBack: (state, action: PayloadAction) => { - const cb = (l: Layer) => l.id === action.payload; - const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer); - // Because the layers are in reverse order, moving to the back is equivalent to moving to the front - moveToFront(renderableLayers, cb); - state.layers = [...ipAdapterLayers, ...renderableLayers]; - }, - selectedLayerDeleted: (state) => { - state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId); - state.selectedLayerId = state.layers[0]?.id ?? null; - }, - allLayersDeleted: (state) => { - state.layers = []; - state.selectedLayerId = null; - }, - //#endregion - - //#region CA Layers - caLayerAdded: { - reducer: ( - state, - action: PayloadAction<{ layerId: string; controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2 }> - ) => { - const { layerId, controlAdapter } = action.payload; - const layer: ControlAdapterLayer = { - id: getCALayerId(layerId), - type: 'control_adapter_layer', - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - opacity: 1, - isSelected: true, - isFilterEnabled: true, - controlAdapter, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({ - payload: { layerId: uuidv4(), controlAdapter }, - }), - }, - caLayerRecalled: (state, action: PayloadAction) => { - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - if (imageDTO) { - const newImage = imageDTOToImageWithDims(imageDTO); - if (isEqual(newImage, layer.controlAdapter.image)) { - return; - } - layer.controlAdapter.image = newImage; - layer.controlAdapter.processedImage = null; - } else { - layer.controlAdapter.image = null; - layer.controlAdapter.processedImage = null; - } - }, - caLayerProcessedImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.controlAdapter.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - caLayerModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null; - }> - ) => { - const { layerId, modelConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - if (!modelConfig) { - layer.controlAdapter.model = null; - return; - } - layer.controlAdapter.model = zModelIdentifierField.parse(modelConfig); - - // We may need to convert the CA to match the model - if (layer.controlAdapter.type === 't2i_adapter' && layer.controlAdapter.model.type === 'controlnet') { - layer.controlAdapter = t2iAdapterToControlNet(layer.controlAdapter); - } else if (layer.controlAdapter.type === 'controlnet' && layer.controlAdapter.model.type === 't2i_adapter') { - layer.controlAdapter = controlNetToT2IAdapter(layer.controlAdapter); - } - - const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig); - if (candidateProcessorConfig?.type !== layer.controlAdapter.processorConfig?.type) { - // The processor has changed. For example, the previous model was a Canny model and the new model is a Depth - // model. We need to use the new processor. - layer.controlAdapter.processedImage = null; - layer.controlAdapter.processorConfig = candidateProcessorConfig; - } - }, - caLayerControlModeChanged: (state, action: PayloadAction<{ layerId: string; controlMode: ControlModeV2 }>) => { - const { layerId, controlMode } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - assert(layer.controlAdapter.type === 'controlnet'); - layer.controlAdapter.controlMode = controlMode; - }, - caLayerProcessorConfigChanged: ( - state, - action: PayloadAction<{ layerId: string; processorConfig: ProcessorConfig | null }> - ) => { - const { layerId, processorConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.controlAdapter.processorConfig = processorConfig; - if (!processorConfig) { - layer.controlAdapter.processedImage = null; - } - }, - caLayerIsFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => { - const { layerId, isFilterEnabled } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.isFilterEnabled = isFilterEnabled; - }, - caLayerProcessorPendingBatchIdChanged: ( - state, - action: PayloadAction<{ layerId: string; batchId: string | null }> - ) => { - const { layerId, batchId } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isControlAdapterLayer); - layer.controlAdapter.processorPendingBatchId = batchId; - }, - //#endregion - - //#region IP Adapter Layers - ipaLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { - const { layerId, ipAdapter } = action.payload; - const layer: IPAdapterLayer = { - id: getIPALayerId(layerId), - type: 'ip_adapter_layer', - isEnabled: true, - isSelected: true, - ipAdapter, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }), - }, - ipaLayerRecalled: (state, action: PayloadAction) => { - state.layers.push(action.payload); - }, - ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - ipaLayerMethodChanged: (state, action: PayloadAction<{ layerId: string; method: IPMethodV2 }>) => { - const { layerId, method } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.method = method; - }, - ipaLayerModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { layerId, modelConfig } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - if (!modelConfig) { - layer.ipAdapter.model = null; - return; - } - layer.ipAdapter.model = zModelIdentifierField.parse(modelConfig); - }, - ipaLayerCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ layerId: string; clipVisionModel: CLIPVisionModelV2 }> - ) => { - const { layerId, clipVisionModel } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isIPAdapterLayer); - layer.ipAdapter.clipVisionModel = clipVisionModel; - }, - //#endregion - - //#region CA or IPA Layers - caOrIPALayerWeightChanged: (state, action: PayloadAction<{ layerId: string; weight: number }>) => { - const { layerId, weight } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.weight = weight; - } else { - layer.ipAdapter.weight = weight; - } - }, - caOrIPALayerBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, beginEndStepPct } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isCAOrIPALayer); - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.beginEndStepPct = beginEndStepPct; - } else { - layer.ipAdapter.beginEndStepPct = beginEndStepPct; - } - }, - //#endregion - - //#region RG Layers - rgLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RegionalGuidanceLayer = { - id: getRGLayerId(layerId), - type: 'regional_guidance_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - previewColor: getVectorMaskPreviewColor(state), - x: 0, - y: 0, - autoNegative: 'invert', - positivePrompt: '', - negativePrompt: null, - ipAdapters: [], - isSelected: true, - uploadedMaskImage: null, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: () => ({ payload: { layerId: uuidv4() } }), - }, - rgLayerRecalled: (state, action: PayloadAction) => { - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { - const { layerId, prompt } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.positivePrompt = prompt; - }, - rgLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => { - const { layerId, prompt } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.negativePrompt = prompt; - }, - rgLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => { - const { layerId, color } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.previewColor = color; - }, - - rgLayerMaskImageUploaded: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.uploadedMaskImage = imageDTOToImageWithDims(imageDTO); - }, - rgLayerAutoNegativeChanged: ( - state, - action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }> - ) => { - const { layerId, autoNegative } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.autoNegative = autoNegative; - }, - rgLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapter: IPAdapterConfigV2 }>) => { - const { layerId, ipAdapter } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.ipAdapters.push(ipAdapter); - }, - rgLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => { - const { layerId, ipAdapterId } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRegionalGuidanceLayer); - layer.ipAdapters = layer.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); - }, - rgLayerIPAdapterImageChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; imageDTO: ImageDTO | null }> - ) => { - const { layerId, ipAdapterId, imageDTO } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - rgLayerIPAdapterWeightChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; weight: number }> - ) => { - const { layerId, ipAdapterId, weight } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.weight = weight; - }, - rgLayerIPAdapterBeginEndStepPctChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; beginEndStepPct: [number, number] }> - ) => { - const { layerId, ipAdapterId, beginEndStepPct } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.beginEndStepPct = beginEndStepPct; - }, - rgLayerIPAdapterMethodChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; method: IPMethodV2 }> - ) => { - const { layerId, ipAdapterId, method } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.method = method; - }, - rgLayerIPAdapterModelChanged: ( - state, - action: PayloadAction<{ - layerId: string; - ipAdapterId: string; - modelConfig: IPAdapterModelConfig | null; - }> - ) => { - const { layerId, ipAdapterId, modelConfig } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - if (!modelConfig) { - ipAdapter.model = null; - return; - } - ipAdapter.model = zModelIdentifierField.parse(modelConfig); - }, - rgLayerIPAdapterCLIPVisionModelChanged: ( - state, - action: PayloadAction<{ layerId: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> - ) => { - const { layerId, ipAdapterId, clipVisionModel } = action.payload; - const ipAdapter = selectRGLayerIPAdapterOrThrow(state, layerId, ipAdapterId); - ipAdapter.clipVisionModel = clipVisionModel; - }, - //#endregion - - //#region Initial Image Layer - iiLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - - // Retain opacity and denoising strength of existing initial image layer if exists - let opacity = 1; - let denoisingStrength = 0.75; - const iiLayer = state.layers.find((l) => l.id === layerId); - if (iiLayer) { - assert(isInitialImageLayer(iiLayer)); - opacity = iiLayer.opacity; - denoisingStrength = iiLayer.denoisingStrength; - } - - // Highlander! There can be only one! - state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); - - const layer: InitialImageLayer = { - id: layerId, - type: 'initial_image_layer', - opacity, - x: 0, - y: 0, - bbox: null, - bboxNeedsUpdate: false, - isEnabled: true, - image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null, - isSelected: true, - denoisingStrength, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }), - }, - iiLayerRecalled: (state, action: PayloadAction) => { - state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true)); - state.layers.push({ ...action.payload, isSelected: true }); - exclusivelySelectLayer(state, action.payload.id); - }, - iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => { - const { layerId, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); - layer.bbox = null; - layer.bboxNeedsUpdate = true; - layer.isEnabled = true; - layer.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; - }, - iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => { - const { layerId, denoisingStrength } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isInitialImageLayer); - layer.denoisingStrength = denoisingStrength; - }, - //#endregion - - //#region Raster Layers - rasterLayerAdded: { - reducer: (state, action: PayloadAction<{ layerId: string }>) => { - const { layerId } = action.payload; - const layer: RasterLayer = { - id: getRasterLayerId(layerId), - type: 'raster_layer', - isEnabled: true, - bbox: null, - bboxNeedsUpdate: false, - objects: [], - opacity: 1, - x: 0, - y: 0, - isSelected: true, - }; - state.layers.push(layer); - exclusivelySelectLayer(state, layer.id); - }, - prepare: () => ({ payload: { layerId: uuidv4() } }), - }, - //#endregion - - //#region Objects - brushLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddBrushLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid, color } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - layer.objects.push({ - id: getBrushLineId(layer.id, lineUuid), - type: 'brush_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - color, - }); - layer.bboxNeedsUpdate = true; - if (layer.type === 'regional_guidance_layer') { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddBrushLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - eraserLineAdded: { - reducer: ( - state, - action: PayloadAction< - AddEraserLineArg & { - lineUuid: string; - } - > - ) => { - const { layerId, points, lineUuid } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - layer.objects.push({ - id: getEraserLineId(layer.id, lineUuid), - type: 'eraser_line', - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener? - points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y], - strokeWidth: state.brushSize, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddEraserLineArg) => ({ - payload: { ...payload, lineUuid: uuidv4() }, - }), - }, - linePointsAdded: (state, action: PayloadAction) => { - const { layerId, point } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - const lastLine = layer.objects.findLast(isLine); - if (!lastLine || !isLine(lastLine)) { - return; - } - // Points must be offset by the layer's x and y coordinates - // TODO: Handle this in the event listener - lastLine.points.push(point[0] - layer.x, point[1] - layer.y); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - rectAdded: { - reducer: (state, action: PayloadAction) => { - const { layerId, rect, rectUuid, color } = action.payload; - if (rect.height === 0 || rect.width === 0) { - // Ignore zero-area rectangles - return; - } - const layer = selectLayerOrThrow(state, layerId, isRGOrRasterlayer); - const id = getRectShapeId(layer.id, rectUuid); - layer.objects.push({ - type: 'rect_shape', - id, - x: rect.x - layer.x, - y: rect.y - layer.y, - width: rect.width, - height: rect.height, - color, - }); - layer.bboxNeedsUpdate = true; - if (isRegionalGuidanceLayer(layer)) { - layer.uploadedMaskImage = null; - } - }, - prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectUuid: uuidv4() } }), - }, - imageAdded: { - reducer: (state, action: PayloadAction) => { - const { layerId, imageUuid, imageDTO } = action.payload; - const layer = selectLayerOrThrow(state, layerId, isRasterLayer); - const id = getImageObjectId(layer.id, imageUuid); - const { width, height, image_name: name } = imageDTO; - layer.objects.push({ - type: 'image', - id, - x: 0, - y: 0, - width, - height, - image: { width, height, name }, - }); - layer.bboxNeedsUpdate = true; - }, - prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageUuid: uuidv4() } }), - }, - //#endregion - - //#region Globals positivePromptChanged: (state, action: PayloadAction) => { - state.positivePrompt = action.payload; + state.prompts.positivePrompt = action.payload; }, negativePromptChanged: (state, action: PayloadAction) => { - state.negativePrompt = action.payload; + state.prompts.negativePrompt = action.payload; }, positivePrompt2Changed: (state, action: PayloadAction) => { - state.positivePrompt2 = action.payload; + state.prompts.positivePrompt2 = action.payload; }, negativePrompt2Changed: (state, action: PayloadAction) => { - state.negativePrompt2 = action.payload; + state.prompts.negativePrompt2 = action.payload; }, shouldConcatPromptsChanged: (state, action: PayloadAction) => { - state.shouldConcatPrompts = action.payload; + state.prompts.shouldConcatPrompts = action.payload; }, widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => { const { width, updateAspectRatio, clamp } = action.payload; @@ -807,28 +101,18 @@ export const controlLayersSlice = createSlice({ bboxChanged: (state, action: PayloadAction) => { state.bbox = action.payload; }, - brushSizeChanged: (state, action: PayloadAction) => { - state.brushSize = Math.round(action.payload); + brushWidthChanged: (state, action: PayloadAction) => { + state.tool.brush.width = Math.round(action.payload); }, - brushColorChanged: (state, action: PayloadAction) => { - state.brushColor = action.payload; + eraserWidthChanged: (state, action: PayloadAction) => { + state.tool.eraser.width = Math.round(action.payload); }, - globalMaskLayerOpacityChanged: (state, action: PayloadAction) => { - state.globalMaskLayerOpacity = action.payload; + fillChanged: (state, action: PayloadAction) => { + state.tool.fill = action.payload; }, - undo: (state) => { - // Invalidate the bbox for all layers to prevent stale bboxes - for (const layer of state.layers.filter(isRenderableLayer)) { - layer.bboxNeedsUpdate = true; - } + invertScrollChanged: (state, action: PayloadAction) => { + state.tool.invertScroll = action.payload; }, - redo: (state) => { - // Invalidate the bbox for all layers to prevent stale bboxes - for (const layer of state.layers.filter(isRenderableLayer)) { - layer.bboxNeedsUpdate = true; - } - }, - //#endregion }, extraReducers(builder) { builder.addCase(modelChanged, (state, action) => { @@ -845,113 +129,10 @@ export const controlLayersSlice = createSlice({ state.size.width = width; state.size.height = height; }); - - // // TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling - // // factor than the UNet. Hopefully we get an upstream fix in diffusers. - // builder.addMatcher(isAnyControlAdapterAdded, (state, action) => { - // if (action.payload.type === 't2i_adapter') { - // state.size.width = roundToMultiple(state.size.width, 64); - // state.size.height = roundToMultiple(state.size.height, 64); - // } - // }); }, }); -/** - * This class is used to cycle through a set of colors for the prompt region layers. - */ -class LayerColors { - static COLORS: RgbColor[] = [ - { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) - { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) - { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) - { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) - { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) - { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) - { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) - ]; - static i = this.COLORS.length - 1; - /** - * Get the next color in the sequence. If a known color is provided, the next color will be the one after it. - */ - static next(currentColor?: RgbColor): RgbColor { - if (currentColor) { - const i = this.COLORS.findIndex((c) => isEqual(c, currentColor)); - if (i !== -1) { - this.i = i; - } - } - this.i = (this.i + 1) % this.COLORS.length; - const color = this.COLORS[this.i]; - assert(color); - return color; - } -} - export const { - // Any Layer Type - layerSelected, - layerIsEnabledToggled, - layerTranslated, - layerBboxChanged, - layerReset, - layerDeleted, - layerOpacityChanged, - layerMovedForward, - layerMovedToFront, - layerMovedBackward, - layerMovedToBack, - selectedLayerDeleted, - allLayersDeleted, - // CA Layers - caLayerAdded, - caLayerRecalled, - caLayerImageChanged, - caLayerProcessedImageChanged, - caLayerModelChanged, - caLayerControlModeChanged, - caLayerProcessorConfigChanged, - caLayerIsFilterEnabledChanged, - caLayerProcessorPendingBatchIdChanged, - // IPA Layers - ipaLayerAdded, - ipaLayerRecalled, - ipaLayerImageChanged, - ipaLayerMethodChanged, - ipaLayerModelChanged, - ipaLayerCLIPVisionModelChanged, - // CA or IPA Layers - caOrIPALayerWeightChanged, - caOrIPALayerBeginEndStepPctChanged, - // RG Layers - rgLayerAdded, - rgLayerRecalled, - rgLayerPositivePromptChanged, - rgLayerNegativePromptChanged, - rgLayerPreviewColorChanged, - brushLineAdded, - eraserLineAdded, - linePointsAdded, - rectAdded, - imageAdded, - rgLayerMaskImageUploaded, - rgLayerAutoNegativeChanged, - rgLayerIPAdapterAdded, - rgLayerIPAdapterDeleted, - rgLayerIPAdapterImageChanged, - rgLayerIPAdapterWeightChanged, - rgLayerIPAdapterBeginEndStepPctChanged, - rgLayerIPAdapterMethodChanged, - rgLayerIPAdapterModelChanged, - rgLayerIPAdapterCLIPVisionModelChanged, - // II Layer - iiLayerAdded, - iiLayerRecalled, - iiLayerImageChanged, - iiLayerDenoisingStrengthChanged, - // Raster layers - rasterLayerAdded, - // Globals positivePromptChanged, negativePromptChanged, positivePrompt2Changed, @@ -961,30 +142,16 @@ export const { heightChanged, aspectRatioChanged, bboxChanged, - brushSizeChanged, - brushColorChanged, - globalMaskLayerOpacityChanged, - undo, - redo, -} = controlLayersSlice.actions; + brushWidthChanged, + eraserWidthChanged, + fillChanged, + invertScrollChanged, +} = canvasV2Slice.actions; -export const selectControlLayersSlice = (state: RootState) => state.controlLayers; +export const selectCanvasV2Slice = (state: RootState) => state.canvasV2; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateControlLayersState = (state: any): any => { - if (state._version === 1) { - // Reset state for users on v1 (e.g. beta users), some changes could cause - state = deepClone(initialControlLayersState); - } - if (state._version === 2) { - // The CA `isProcessingImage` flag was replaced with a `processorPendingBatchId` property, fix up CA layers - for (const layer of (state as ControlLayersState).layers) { - if (layer.type === 'control_adapter_layer') { - layer.controlAdapter.processorPendingBatchId = null; - unset(layer.controlAdapter, 'isProcessingImage'); - } - } - } +const migrate = (state: any): any => { return state; }; @@ -992,8 +159,6 @@ const migrateControlLayersState = (state: any): any => { export const $isDrawing = atom(false); export const $isMouseDown = atom(false); export const $lastMouseDownPos = atom(null); -export const $tool = atom('brush'); -export const $toolBuffer = atom(null); export const $lastCursorPos = atom(null); export const $isPreviewVisible = atom(true); export const $lastAddedPoint = atom(null); @@ -1007,72 +172,24 @@ export const $stageAttrs = atom({ }); // Some nanostores that are manually synced to redux state to provide imperative access -// TODO(psyche): This is a hack, figure out another way to handle this... -export const $brushSize = atom(0); -export const $brushColor = atom(DEFAULT_RGBA_COLOR); +// TODO(psyche): +export const $tool = atom('brush'); +export const $toolBuffer = atom(null); +export const $brushWidth = atom(0); export const $brushSpacingPx = atom(0); -export const $selectedLayer = atom(null); -export const $shouldInvertBrushSizeScrollDirection = atom(false); +export const $eraserWidth = atom(0); +export const $eraserSpacingPx = atom(0); +export const $fill = atom(DEFAULT_RGBA_COLOR); +export const $selectedLayer = atom(null); +export const $selectedRG = atom(null); +export const $selectedCA = atom(null); +export const $selectedIPA = atom(null); +export const $invertScroll = atom(false); export const $bbox = atom({ x: 0, y: 0, width: 0, height: 0 }); -export const controlLayersPersistConfig: PersistConfig = { - name: controlLayersSlice.name, - initialState: initialControlLayersState, - migrate: migrateControlLayersState, +export const canvasV2PersistConfig: PersistConfig = { + name: canvasV2Slice.name, + initialState, + migrate, persistDenylist: ['bbox'], }; - -// These actions are _individually_ grouped together as single undoable actions -const undoableGroupByMatcher = isAnyOf( - layerTranslated, - brushSizeChanged, - globalMaskLayerOpacityChanged, - positivePromptChanged, - negativePromptChanged, - positivePrompt2Changed, - negativePrompt2Changed, - rgLayerPositivePromptChanged, - rgLayerNegativePromptChanged, - rgLayerPreviewColorChanged -); - -// These are used to group actions into logical lines below (hate typos) -const LINE_1 = 'LINE_1'; -const LINE_2 = 'LINE_2'; - -export const controlLayersUndoableConfig: UndoableOptions = { - limit: 64, - undoType: controlLayersSlice.actions.undo.type, - redoType: controlLayersSlice.actions.redo.type, - groupBy: (action, state, history) => { - // Lines are started with `rgLayerLineAdded` and may have any number of subsequent `rgLayerPointsAdded` events. - // We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping - // separate logical lines as a single undo action. - if (brushLineAdded.match(action)) { - return history.group === LINE_1 ? LINE_2 : LINE_1; - } - if (linePointsAdded.match(action)) { - if (history.group === LINE_1 || history.group === LINE_2) { - return history.group; - } - } - if (undoableGroupByMatcher(action)) { - return action.type; - } - return null; - }, - filter: (action, _state, _history) => { - // TODO(psyche): TEMP OVERRIDE - return false; - // // Ignore all actions from other slices - // if (!action.type.startsWith(controlLayersSlice.name)) { - // return false; - // } - // // This action is triggered on state changes, including when we undo. If we do not ignore this action, when we - // // undo, this action triggers and empties the future states array. Therefore, we must ignore this action. - // if (layerBboxChanged.match(action)) { - // return false; - // } - // return true; - }, -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskSlice.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts new file mode 100644 index 0000000000..7424d41903 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersSlice.ts @@ -0,0 +1,140 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { IPAdapterConfig, IPAdapterData } from './types'; + +type IPAdaptersState = { + _version: 1; + ipAdapters: IPAdapterData[]; +}; + +const initialState: IPAdaptersState = { + _version: 1, + ipAdapters: [], +}; + +const selectIpa = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id); + +export const ipAdaptersSlice = createSlice({ + name: 'ipAdapters', + initialState, + reducers: { + ipaAdded: { + reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => { + const { id, config } = action.payload; + const layer: IPAdapterData = { + id, + type: 'ip_adapter', + isEnabled: true, + ...config, + }; + state.ipAdapters.push(layer); + }, + prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }), + }, + ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => { + state.ipAdapters.push(action.payload.data); + }, + ipaIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const ipa = selectIpa(state, id); + if (ipa) { + ipa.isEnabled = isEnabled; + } + }, + ipaDeleted: (state, action: PayloadAction<{ id: string }>) => { + state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id); + }, + ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => { + const { id, imageDTO } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => { + const { id, method } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.method = method; + }, + ipaModelChanged: ( + state, + action: PayloadAction<{ + id: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, modelConfig } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => { + const { id, clipVisionModel } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { + const { id, weight } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => { + const { id, beginEndStepPct } = action.payload; + const ipa = selectIpa(state, id); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, + }, +}); + +export const { + ipaAdded, + ipaRecalled, + ipaIsEnabledChanged, + ipaDeleted, + ipaImageChanged, + ipaMethodChanged, + ipaModelChanged, + ipaCLIPVisionModelChanged, + ipaWeightChanged, + ipaBeginEndStepPctChanged, +} = ipAdaptersSlice.actions; + +export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const ipAdaptersPersistConfig: PersistConfig = { + name: ipAdaptersSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts new file mode 100644 index 0000000000..45790bb2f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -0,0 +1,268 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { IRect } from 'konva/lib/types'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + AddBrushLineArg, + AddEraserLineArg, + AddImageObjectArg, + AddPointToLineArg, + AddRectShapeArg, + LayerData, +} from './types'; +import { isLine } from './types'; + +type LayersState = { + _version: 1; + layers: LayerData[]; +}; + +const initialState: LayersState = { _version: 1, layers: [] }; +const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); + +export const layersSlice = createSlice({ + name: 'layers', + initialState, + reducers: { + layerAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers.push({ + id, + type: 'layer', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + opacity: 1, + x: 0, + y: 0, + }); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { + state.layers.push(action.payload.data); + }, + layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = isEnabled; + }, + layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.x = x; + layer.y = y; + }, + layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.bbox = bbox; + layer.bboxNeedsUpdate = false; + if (bbox === null) { + layer.objects = []; + } + }, + layerReset: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.isEnabled = true; + layer.objects = []; + layer.bbox = null; + layer.bboxNeedsUpdate = false; + }, + layerDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.layers = state.layers.filter((l) => l.id !== id); + }, + layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => { + const { id, opacity } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.opacity = opacity; + }, + layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToEnd(state.layers, layer); + }, + layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToEnd(state.layers, layer); + }, + layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveOneToStart(state.layers, layer); + }, + layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + moveToStart(state.layers, layer); + }, + layerBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + points, + strokeWidth: width, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + + layer.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + points, + strokeWidth: width, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + layerLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const lastObject = layer.objects[layer.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + lastObject.points.push(...point); + layer.bboxNeedsUpdate = true; + }, + layerRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const layer = selectLayer(state, id); + if (!layer) { + return; + } + layer.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + x: rect.x - layer.x, + y: rect.y - layer.y, + width: rect.width, + height: rect.height, + color, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, + layerImageAdded: { + reducer: (state, action: PayloadAction) => { + const { id, imageId, imageDTO } = action.payload; + const layer = selectLayer(state, id); + if (!layer) { + return; + } + const { width, height, image_name: name } = imageDTO; + layer.objects.push({ + type: 'image', + id: getImageObjectId(id, imageId), + x: 0, + y: 0, + width, + height, + image: { width, height, name }, + }); + layer.bboxNeedsUpdate = true; + }, + prepare: (payload: AddImageObjectArg) => ({ payload: { ...payload, imageId: uuidv4() } }), + }, + }, +}); + +export const { + layerAdded, + layerDeleted, + layerReset, + layerMovedForwardOne, + layerMovedToFront, + layerMovedBackwardOne, + layerMovedToBack, + layerIsEnabledChanged, + layerOpacityChanged, + layerTranslated, + layerBboxChanged, + layerBrushLineAdded, + layerEraserLineAdded, + layerLinePointAdded, + layerRectAdded, + layerImageAdded, +} = layersSlice.actions; + +export const selectLayersSlice = (state: RootState) => state.layers; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const layersPersistConfig: PersistConfig = { + name: layersSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts new file mode 100644 index 0000000000..167feec396 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -0,0 +1,440 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; +import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; +import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters'; +import { imageDTOToImageWithDims } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; +import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; +import type { IRect } from 'konva/lib/types'; +import { isEqual } from 'lodash-es'; +import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types'; +import { assert } from 'tsafe'; +import { v4 as uuidv4 } from 'uuid'; + +import type { + AddBrushLineArg, + AddEraserLineArg, + AddPointToLineArg, + AddRectShapeArg, + IPAdapterData, + RegionalGuidanceData, + RgbColor, +} from './types'; +import { isLine } from './types'; + +type RegionalGuidanceState = { + _version: 1; + regions: RegionalGuidanceData[]; + opacity: number; +}; + +const initialState: RegionalGuidanceState = { + _version: 1, + regions: [], + opacity: 0.3, +}; + +const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); + +const DEFAULT_MASK_COLORS: RgbColor[] = [ + { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) + { r: 131, g: 214, b: 131 }, // rgb(131, 214, 131) + { r: 250, g: 225, b: 80 }, // rgb(250, 225, 80) + { r: 220, g: 144, b: 101 }, // rgb(220, 144, 101) + { r: 224, g: 117, b: 117 }, // rgb(224, 117, 117) + { r: 213, g: 139, b: 202 }, // rgb(213, 139, 202) + { r: 161, g: 120, b: 214 }, // rgb(161, 120, 214) +]; + +const getRGMaskFill = (state: RegionalGuidanceState): RgbColor => { + const lastFill = state.regions.slice(-1)[0]?.fill; + let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill)); + if (i === -1) { + i = 0; + } + i = (i + 1) % DEFAULT_MASK_COLORS.length; + const fill = DEFAULT_MASK_COLORS[i]; + assert(fill, 'This should never happen'); + return fill; +}; + +export const regionalGuidanceSlice = createSlice({ + name: 'regionalGuidance', + initialState, + reducers: { + rgAdded: { + reducer: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg: RegionalGuidanceData = { + id, + type: 'regional_guidance', + isEnabled: true, + bbox: null, + bboxNeedsUpdate: false, + objects: [], + fill: getRGMaskFill(state), + x: 0, + y: 0, + autoNegative: 'invert', + positivePrompt: '', + negativePrompt: null, + ipAdapters: [], + imageCache: null, + }; + state.regions.push(rg); + }, + prepare: () => ({ payload: { id: uuidv4() } }), + }, + rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => { + const { data } = action.payload; + state.regions.push(data); + }, + rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { + const { id, isEnabled } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.isEnabled = isEnabled; + } + }, + rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { + const { id, x, y } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.x = x; + rg.y = y; + } + }, + rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { + const { id, bbox } = action.payload; + const rg = selectRg(state, id); + if (rg) { + rg.bbox = bbox; + rg.bboxNeedsUpdate = false; + } + }, + rgDeleted: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + state.regions = state.regions.filter((ca) => ca.id !== id); + }, + rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => { + const { opacity } = action.payload; + state.opacity = opacity; + }, + rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveOneToEnd(state.regions, rg); + }, + rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveToEnd(state.regions, rg); + }, + rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveOneToStart(state.regions, rg); + }, + rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + moveToStart(state.regions, rg); + }, + rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.positivePrompt = prompt; + }, + rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { + const { id, prompt } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.negativePrompt = prompt; + }, + rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { + const { id, fill } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.fill = fill; + }, + rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { + const { id, imageDTO } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.imageCache = imageDTOToImageWithDims(imageDTO); + }, + rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { + const { id, autoNegative } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.autoNegative = autoNegative; + }, + rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { + const { id, ipAdapter } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.ipAdapters.push(ipAdapter); + }, + rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { + const { id, ipAdapterId } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId); + }, + rgIPAdapterImageChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> + ) => { + const { id, ipAdapterId, imageDTO } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; + }, + rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { + const { id, ipAdapterId, weight } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.weight = weight; + }, + rgIPAdapterBeginEndStepPctChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> + ) => { + const { id, ipAdapterId, beginEndStepPct } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.beginEndStepPct = beginEndStepPct; + }, + rgIPAdapterMethodChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> + ) => { + const { id, ipAdapterId, method } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.method = method; + }, + rgIPAdapterModelChanged: ( + state, + action: PayloadAction<{ + id: string; + ipAdapterId: string; + modelConfig: IPAdapterModelConfig | null; + }> + ) => { + const { id, ipAdapterId, modelConfig } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + if (modelConfig) { + ipa.model = zModelIdentifierField.parse(modelConfig); + } else { + ipa.model = null; + } + }, + rgIPAdapterCLIPVisionModelChanged: ( + state, + action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> + ) => { + const { id, ipAdapterId, clipVisionModel } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId); + if (!ipa) { + return; + } + ipa.clipVisionModel = clipVisionModel; + }, + rgBrushLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, color, width } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getBrushLineId(id, lineId), + type: 'brush_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + strokeWidth: width, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddBrushLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgEraserLineAdded: { + reducer: (state, action: PayloadAction) => { + const { id, points, lineId, width } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + id: getEraserLineId(id, lineId), + type: 'eraser_line', + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener? + points: [points[0] - rg.x, points[1] - rg.y, points[2] - rg.x, points[3] - rg.y], + strokeWidth: width, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddEraserLineArg) => ({ + payload: { ...payload, lineId: uuidv4() }, + }), + }, + rgLinePointAdded: (state, action: PayloadAction) => { + const { id, point } = action.payload; + const rg = selectRg(state, id); + if (!rg) { + return; + } + const lastObject = rg.objects[rg.objects.length - 1]; + if (!lastObject || !isLine(lastObject)) { + return; + } + // Points must be offset by the layer's x and y coordinates + // TODO: Handle this in the event listener + lastObject.points.push(point[0] - rg.x, point[1] - rg.y); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + rgRectAdded: { + reducer: (state, action: PayloadAction) => { + const { id, rect, rectId, color } = action.payload; + if (rect.height === 0 || rect.width === 0) { + // Ignore zero-area rectangles + return; + } + const rg = selectRg(state, id); + if (!rg) { + return; + } + rg.objects.push({ + type: 'rect_shape', + id: getRectShapeId(id, rectId), + x: rect.x - rg.x, + y: rect.y - rg.y, + width: rect.width, + height: rect.height, + color, + }); + rg.bboxNeedsUpdate = true; + rg.imageCache = null; + }, + prepare: (payload: AddRectShapeArg) => ({ payload: { ...payload, rectId: uuidv4() } }), + }, + }, +}); + +export const { + rgAdded, + rgRecalled, + rgIsEnabledToggled, + rgTranslated, + rgBboxChanged, + rgDeleted, + rgGlobalOpacityChanged, + rgMovedForwardOne, + rgMovedToFront, + rgMovedBackwardOne, + rgMovedToBack, + rgPositivePromptChanged, + rgNegativePromptChanged, + rgFillChanged, + rgMaskImageUploaded, + rgAutoNegativeChanged, + rgIPAdapterAdded, + rgIPAdapterDeleted, + rgIPAdapterImageChanged, + rgIPAdapterWeightChanged, + rgIPAdapterBeginEndStepPctChanged, + rgIPAdapterMethodChanged, + rgIPAdapterModelChanged, + rgIPAdapterCLIPVisionModelChanged, + rgBrushLineAdded, + rgEraserLineAdded, + rgLinePointAdded, + rgRectAdded, +} = regionalGuidanceSlice.actions; + +export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrate = (state: any): any => { + return state; +}; + +export const regionalGuidancePersistConfig: PersistConfig = { + name: regionalGuidanceSlice.name, + initialState, + migrate, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 5380fe79d5..860f3f2082 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -1,9 +1,13 @@ import { - zControlNetConfigV2, + zBeginEndStepPct, + zCLIPVisionModelV2, + zControlModeV2, + zId, zImageWithDims, - zIPAdapterConfigV2, - zT2IAdapterConfigV2, + zIPMethodV2, + zProcessorConfig, } from 'features/controlLayers/util/controlAdapters'; +import { zModelIdentifierField } from 'features/nodes/types/common'; import type { AspectRatioState } from 'features/parameters/components/ImageSize/types'; import type { ParameterHeight, @@ -17,7 +21,6 @@ import { zAutoNegative, zParameterNegativePrompt, zParameterPositivePrompt, - zParameterStrength, } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; @@ -31,7 +34,7 @@ const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, message: 'Must have an even number of points', }); const zOLD_VectorMaskLine = z.object({ - id: z.string(), + id: zId, type: z.literal('vector_mask_line'), tool: zDrawingTool, strokeWidth: z.number().min(1), @@ -39,7 +42,7 @@ const zOLD_VectorMaskLine = z.object({ }); const zOLD_VectorMaskRect = z.object({ - id: z.string(), + id: zId, type: z.literal('vector_mask_rect'), x: z.number(), y: z.number(), @@ -52,6 +55,7 @@ const zRgbColor = z.object({ g: z.number().int().min(0).max(255), b: z.number().int().min(0).max(255), }); +export type RgbColor = z.infer; const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); @@ -61,7 +65,7 @@ export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; const zOpacity = z.number().gte(0).lte(1); const zBrushLine = z.object({ - id: z.string(), + id: zId, type: z.literal('brush_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -70,7 +74,7 @@ const zBrushLine = z.object({ export type BrushLine = z.infer; const zEraserline = z.object({ - id: z.string(), + id: zId, type: z.literal('eraser_line'), strokeWidth: z.number().min(1), points: zPoints, @@ -78,7 +82,7 @@ const zEraserline = z.object({ export type EraserLine = z.infer; const zRectShape = z.object({ - id: z.string(), + id: zId, type: z.literal('rect_shape'), x: z.number(), y: z.number(), @@ -89,7 +93,7 @@ const zRectShape = z.object({ export type RectShape = z.infer; const zEllipseShape = z.object({ - id: z.string(), + id: zId, type: z.literal('ellipse_shape'), x: z.number(), y: z.number(), @@ -100,7 +104,7 @@ const zEllipseShape = z.object({ export type EllipseShape = z.infer; const zPolygonShape = z.object({ - id: z.string(), + id: zId, type: z.literal('polygon_shape'), points: zPoints, color: zRgbaColor, @@ -108,7 +112,7 @@ const zPolygonShape = z.object({ export type PolygonShape = z.infer; const zImageObject = z.object({ - id: z.string(), + id: zId, type: z.literal('image'), image: zImageWithDims, x: z.number(), @@ -118,7 +122,7 @@ const zImageObject = z.object({ }); export type ImageObject = z.infer; -const zAnyLayerObject = z.discriminatedUnion('type', [ +const zLayerObject = z.discriminatedUnion('type', [ zImageObject, zBrushLine, zEraserline, @@ -126,13 +130,7 @@ const zAnyLayerObject = z.discriminatedUnion('type', [ zEllipseShape, zPolygonShape, ]); -export type AnyLayerObject = z.infer; - -const zLayerBase = z.object({ - id: z.string(), - isEnabled: z.boolean().default(true), - isSelected: z.boolean().default(true), -}); +export type LayerObject = z.infer; const zRect = z.object({ x: z.number(), @@ -140,33 +138,36 @@ const zRect = z.object({ width: z.number().min(1), height: z.number().min(1), }); -const zRenderableLayerBase = zLayerBase.extend({ + +const zLayerData = z.object({ + id: zId, + type: z.literal('layer'), + isEnabled: z.boolean(), x: z.number(), y: z.number(), bbox: zRect.nullable(), bboxNeedsUpdate: z.boolean(), -}); - -const zRasterLayer = zRenderableLayerBase.extend({ - type: z.literal('raster_layer'), opacity: zOpacity, - objects: z.array(zAnyLayerObject), + objects: z.array(zLayerObject), }); -export type RasterLayer = z.infer; +export type LayerData = z.infer; -const zControlAdapterLayer = zRenderableLayerBase.extend({ - type: z.literal('control_adapter_layer'), - opacity: zOpacity, - isFilterEnabled: z.boolean(), - controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]), +const zIPAdapterData = z.object({ + id: zId, + type: z.literal('ip_adapter'), + isEnabled: z.boolean(), + weight: z.number().gte(-1).lte(2), + method: zIPMethodV2, + image: zImageWithDims.nullable(), + model: zModelIdentifierField.nullable(), + clipVisionModel: zCLIPVisionModelV2, + beginEndStepPct: zBeginEndStepPct, }); -export type ControlAdapterLayer = z.infer; - -const zIPAdapterLayer = zLayerBase.extend({ - type: z.literal('ip_adapter_layer'), - ipAdapter: zIPAdapterConfigV2, -}); -export type IPAdapterLayer = z.infer; +export type IPAdapterData = z.infer; +export type IPAdapterConfig = Pick< + IPAdapterData, + 'weight' | 'image' | 'beginEndStepPct' | 'model' | 'clipVisionModel' | 'method' +>; const zMaskObject = z .discriminatedUnion('type', [zOLD_VectorMaskLine, zOLD_VectorMaskRect, zBrushLine, zEraserline, zRectShape]) @@ -201,69 +202,109 @@ const zMaskObject = z }) .pipe(z.discriminatedUnion('type', [zBrushLine, zEraserline, zRectShape])); -const zOLD_RegionalGuidanceLayer = zRenderableLayerBase.extend({ - type: z.literal('regional_guidance_layer'), - maskObjects: z.array(zMaskObject), - positivePrompt: zParameterPositivePrompt.nullable(), - negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterConfigV2), - previewColor: zRgbColor, - autoNegative: zAutoNegative, - uploadedMaskImage: zImageWithDims.nullable(), -}); -const zRegionalGuidanceLayer = zRenderableLayerBase.extend({ - type: z.literal('regional_guidance_layer'), +const zRegionalGuidanceData = z.object({ + id: zId, + type: z.literal('regional_guidance'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), objects: z.array(zMaskObject), positivePrompt: zParameterPositivePrompt.nullable(), negativePrompt: zParameterNegativePrompt.nullable(), - ipAdapters: z.array(zIPAdapterConfigV2), - previewColor: zRgbColor, + ipAdapters: z.array(zIPAdapterData), + fill: zRgbColor, autoNegative: zAutoNegative, - uploadedMaskImage: zImageWithDims.nullable(), + imageCache: zImageWithDims.nullable(), }); -// TODO(psyche): This doesn't migrate correctly! -const zRGLayer = z - .union([zOLD_RegionalGuidanceLayer, zRegionalGuidanceLayer]) - .transform((val) => { - if ('maskObjects' in val) { - const { maskObjects, ...rest } = val; - return { ...rest, objects: maskObjects }; - } else { - return val; - } - }) - .pipe(zRegionalGuidanceLayer); -export type RegionalGuidanceLayer = z.infer; +export type RegionalGuidanceData = z.infer; -const zInitialImageLayer = zRenderableLayerBase.extend({ - type: z.literal('initial_image_layer'), +const zColorFill = z.object({ + type: z.literal('color_fill'), + color: zRgbaColor, +}); +const zImageFill = z.object({ + type: z.literal('image_fill'), + src: z.string(), +}); +const zFill = z.discriminatedUnion('type', [zColorFill, zImageFill]); +const zInpaintMaskData = z.object({ + id: zId, + type: z.literal('inpaint_mask'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), + maskObjects: z.array(zMaskObject), + fill: zFill, + imageCache: zImageWithDims.nullable(), +}); +export type InpaintMaskData = z.infer; + +const zFilter = z.enum(['none', 'lightness_to_alpha']); +export type Filter = z.infer; + +const zControlAdapterData = z.object({ + id: zId, + type: z.literal('control_adapter'), + isEnabled: z.boolean(), + x: z.number(), + y: z.number(), + bbox: zRect.nullable(), + bboxNeedsUpdate: z.boolean(), opacity: zOpacity, + filter: zFilter, + weight: z.number().gte(-1).lte(2), image: zImageWithDims.nullable(), - denoisingStrength: zParameterStrength, + processedImage: zImageWithDims.nullable(), + processorConfig: zProcessorConfig.nullable(), + processorPendingBatchId: z.string().nullable().default(null), + beginEndStepPct: zBeginEndStepPct, + model: zModelIdentifierField.nullable(), + controlMode: zControlModeV2.nullable(), }); -export type InitialImageLayer = z.infer; +export type ControlAdapterData = z.infer; +export type ControlAdapterConfig = Pick< + ControlAdapterData, + 'weight' | 'image' | 'processedImage' | 'processorConfig' | 'beginEndStepPct' | 'model' | 'controlMode' +>; -export const zLayer = z.discriminatedUnion('type', [ - zRegionalGuidanceLayer, - zControlAdapterLayer, - zIPAdapterLayer, - zInitialImageLayer, - zRasterLayer, -]); -export type Layer = z.infer; +const zCanvasItemIdentifier = z.object({ + type: z.enum([ + zLayerData.shape.type.value, + zIPAdapterData.shape.type.value, + zControlAdapterData.shape.type.value, + zRegionalGuidanceData.shape.type.value, + zInpaintMaskData.shape.type.value, + ]), + id: zId, +}); +type CanvasItemIdentifier = z.infer; -export type ControlLayersState = { +export type CanvasV2State = { _version: 3; - selectedLayerId: string | null; - layers: Layer[]; - brushSize: number; - brushColor: RgbaColor; - globalMaskLayerOpacity: number; - positivePrompt: ParameterPositivePrompt; - negativePrompt: ParameterNegativePrompt; - positivePrompt2: ParameterPositiveStylePromptSDXL; - negativePrompt2: ParameterNegativeStylePromptSDXL; - shouldConcatPrompts: boolean; + lastSelectedItem: CanvasItemIdentifier | null; + prompts: { + positivePrompt: ParameterPositivePrompt; + negativePrompt: ParameterNegativePrompt; + positivePrompt2: ParameterPositiveStylePromptSDXL; + negativePrompt2: ParameterNegativeStylePromptSDXL; + shouldConcatPrompts: boolean; + }; + tool: { + selected: Tool; + selectedBuffer: Tool | null; + invertScroll: boolean; + brush: { + width: number; + }; + eraser: { + width: number; + }; + fill: RgbaColor; + }; size: { width: ParameterWidth; height: ParameterHeight; @@ -273,45 +314,13 @@ export type ControlLayersState = { }; export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number }; -export type AddEraserLineArg = { layerId: string; points: [number, number, number, number] }; +export type AddEraserLineArg = { id: string; points: [number, number, number, number]; width: number }; export type AddBrushLineArg = AddEraserLineArg & { color: RgbaColor }; -export type AddPointToLineArg = { layerId: string; point: [number, number] }; -export type AddRectShapeArg = { layerId: string; rect: IRect; color: RgbaColor }; -export type AddImageObjectArg = { layerId: string; imageDTO: ImageDTO }; +export type AddPointToLineArg = { id: string; point: [number, number] }; +export type AddRectShapeArg = { id: string; rect: IRect; color: RgbaColor }; +export type AddImageObjectArg = { id: string; imageDTO: ImageDTO }; //#region Type guards -export const isLine = (obj: AnyLayerObject): obj is BrushLine | EraserLine => { +export const isLine = (obj: LayerObject): obj is BrushLine | EraserLine => { return obj.type === 'brush_line' || obj.type === 'eraser_line'; }; -export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer => { - return layer?.type === 'regional_guidance_layer'; -}; -export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer => { - return layer?.type === 'control_adapter_layer'; -}; -export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => { - return layer?.type === 'ip_adapter_layer'; -}; -export const isInitialImageLayer = (layer?: Layer): layer is InitialImageLayer => { - return layer?.type === 'initial_image_layer'; -}; -export const isRasterLayer = (layer?: Layer): layer is RasterLayer => { - return layer?.type === 'raster_layer'; -}; -export const isRenderableLayer = ( - layer?: Layer -): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer | RasterLayer => { - return ( - isRegionalGuidanceLayer(layer) || isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer) - ); -}; -export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => { - return isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer); -}; -export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => { - return isControlAdapterLayer(layer) || isIPAdapterLayer(layer); -}; -export const isRGOrRasterlayer = (layer?: Layer): layer is RegionalGuidanceLayer | RasterLayer => { - return isRegionalGuidanceLayer(layer) || isRasterLayer(layer); -}; -//#endregion diff --git a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts index 0330eceee1..892d2c5eac 100644 --- a/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/util/controlAdapters.ts @@ -10,7 +10,7 @@ import type { } from 'services/api/types'; import { z } from 'zod'; -const zId = z.string().min(1); +export const zId = z.string().min(1); const zCannyProcessorConfig = z.object({ id: zId, @@ -120,7 +120,7 @@ const zZoeDepthProcessorConfig = z.object({ }); export type ZoeDepthProcessorConfig = z.infer; -const zProcessorConfig = z.discriminatedUnion('type', [ +export const zProcessorConfig = z.discriminatedUnion('type', [ zCannyProcessorConfig, zColorMapProcessorConfig, zContentShuffleProcessorConfig, @@ -145,7 +145,7 @@ export const zImageWithDims = z.object({ }); export type ImageWithDims = z.infer; -const zBeginEndStepPct = z +export const zBeginEndStepPct = z .tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)]) .refine(([begin, end]) => begin < end, { message: 'Begin must be less than end', @@ -161,7 +161,7 @@ const zControlAdapterBase = z.object({ beginEndStepPct: zBeginEndStepPct, }); -const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); +export const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']); export type ControlModeV2 = z.infer; export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success; @@ -178,11 +178,11 @@ export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({ }); export type T2IAdapterConfigV2 = z.infer; -const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); +export const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']); export type CLIPVisionModelV2 = z.infer; export const isCLIPVisionModelV2 = (v: unknown): v is CLIPVisionModelV2 => zCLIPVisionModelV2.safeParse(v).success; -const zIPMethodV2 = z.enum(['full', 'style', 'composition']); +export const zIPMethodV2 = z.enum(['full', 'style', 'composition']); export type IPMethodV2 = z.infer; export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 3df7a02f43..e58c0eaad7 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -3,7 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors'; import { @@ -27,7 +27,7 @@ const selectImageUsages = createMemoizedSelector( selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, - selectControlLayersSlice, + selectCanvasV2Slice, selectImageUsage, ], (deleteImageModal, canvas, nodes, controlAdapters, controlLayers, imagesUsage) => { diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index aeb41c402c..0a2a0587c7 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -7,8 +7,8 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import type { ControlAdaptersState } from 'features/controlAdapters/store/types'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; -import type { ControlLayersState } from 'features/controlLayers/store/types'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; +import type { CanvasV2State } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, @@ -28,7 +28,7 @@ export const getImageUsage = ( canvas: CanvasState, nodes: NodesState, controlAdapters: ControlAdaptersState, - controlLayers: ControlLayersState, + controlLayers: CanvasV2State, image_name: string ) => { const isCanvasImage = canvas.layerState.objects.some((obj) => obj.kind === 'image' && obj.imageName === image_name); @@ -75,7 +75,7 @@ export const selectImageUsage = createMemoizedSelector( selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, - selectControlLayersSlice, + selectCanvasV2Slice, (deleteImageModal, canvas, nodes, controlAdapters, controlLayers) => { const { imagesToDelete } = deleteImageModal; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 2fe1963e76..a0fa496ea6 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; @@ -42,7 +42,7 @@ const DeleteBoardModal = (props: Props) => { const selectImageUsageSummary = useMemo( () => createMemoizedSelector( - [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectControlLayersSlice], + [selectCanvasSlice, selectNodesSlice, selectControlAdaptersSlice, selectCanvasV2Slice], (canvas, nodes, controlAdapters, controlLayers) => { const allImageUsage = (boardImageNames ?? []).map((imageName) => getImageUsage(canvas, nodes, controlAdapters, controlLayers.present, imageName) diff --git a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx index ab4ce03987..7bc6058037 100644 --- a/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx +++ b/invokeai/frontend/web/src/features/metadata/components/MetadataLayers.tsx @@ -1,4 +1,4 @@ -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { MetadataItemView } from 'features/metadata/components/MetadataItemView'; import type { MetadataHandlers } from 'features/metadata/types'; import { handlers } from 'features/metadata/util/handlers'; @@ -9,7 +9,7 @@ type Props = { }; export const MetadataLayers = ({ metadata }: Props) => { - const [layers, setLayers] = useState([]); + const [layers, setLayers] = useState([]); useEffect(() => { const parse = async () => { @@ -40,8 +40,8 @@ const MetadataViewLayer = ({ handlers, }: { label: string; - layer: Layer; - handlers: MetadataHandlers; + layer: LayerData; + handlers: MetadataHandlers; }) => { const onRecall = useCallback(() => { if (!handlers.recallItem) { diff --git a/invokeai/frontend/web/src/features/metadata/util/handlers.ts b/invokeai/frontend/web/src/features/metadata/util/handlers.ts index 33715cbbe1..f55f085b7d 100644 --- a/invokeai/frontend/web/src/features/metadata/util/handlers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/handlers.ts @@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store'; import { deepClone } from 'common/util/deepClone'; import { objectKeys } from 'common/util/objectKeys'; import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { AnyControlAdapterConfigMetadata, @@ -49,7 +49,7 @@ const renderControlAdapterValue: MetadataRenderValueFunc = async (layer) => { +const renderLayerValue: MetadataRenderValueFunc = async (layer) => { if (layer.type === 'initial_image_layer') { let rendered = t('controlLayers.globalInitialImageLayer'); if (layer.image) { @@ -89,7 +89,7 @@ const renderLayerValue: MetadataRenderValueFunc = async (layer) => { } assert(false, 'Unknown layer type'); }; -const renderLayersValue: MetadataRenderValueFunc = async (layers) => { +const renderLayersValue: MetadataRenderValueFunc = async (layers) => { return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`; }; diff --git a/invokeai/frontend/web/src/features/metadata/util/parsers.ts b/invokeai/frontend/web/src/features/metadata/util/parsers.ts index 78d569f987..3707d6b32d 100644 --- a/invokeai/frontend/web/src/features/metadata/util/parsers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/parsers.ts @@ -5,7 +5,7 @@ import { } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/konva/naming'; -import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types'; +import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, LayerData } from 'features/controlLayers/store/types'; import { zLayer } from 'features/controlLayers/store/types'; import { CA_PROCESSOR_DATA, @@ -431,22 +431,22 @@ const parseAllIPAdapters: MetadataParseFunc = async ( }; //#region Control Layers -const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); +const parseLayer: MetadataParseFunc = async (metadataItem) => zLayer.parseAsync(metadataItem); -const parseLayers: MetadataParseFunc = async (metadata) => { +const parseLayers: MetadataParseFunc = async (metadata) => { // We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles // taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For // example, CL Control Adapters don't support resize mode, so we simply omit that property. try { - const layers: Layer[] = []; + const layers: LayerData[] = []; try { const control_layers = await getProperty(metadata, 'control_layers'); const controlLayersRaw = await getProperty(control_layers, 'layers', isArray); const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer)); const controlLayers = controlLayersParseResults - .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .map((result) => result.value); layers.push(...controlLayers); } catch { diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts index b69a14810d..9e0ecf5b8b 100644 --- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts +++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts @@ -9,18 +9,18 @@ import { import { getCALayerId, getIPALayerId, getRGLayerId } from 'features/controlLayers/konva/naming'; import { allLayersDeleted, - caLayerRecalled, + controlAdapterRecalled, heightChanged, iiLayerRecalled, - ipaLayerRecalled, + ipAdapterRecalled, negativePrompt2Changed, negativePromptChanged, positivePrompt2Changed, positivePromptChanged, - rgLayerRecalled, + regionalGuidanceRecalled, widthChanged, } from 'features/controlLayers/store/controlLayersSlice'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice'; @@ -242,7 +242,7 @@ const recallIPAdapters: MetadataRecallFunc = (ipAdapt }; //#region Control Layers -const recallLayer: MetadataRecallFunc = async (layer) => { +const recallLayer: MetadataRecallFunc = async (layer) => { const { dispatch } = getStore(); // We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM! // Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID. @@ -269,7 +269,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } clone.id = getCALayerId(uuidv4()); clone.controlAdapter.id = uuidv4(); - dispatch(caLayerRecalled(clone)); + dispatch(controlAdapterRecalled(clone)); return; } if (layer.type === 'ip_adapter_layer') { @@ -289,7 +289,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } clone.id = getIPALayerId(uuidv4()); clone.ipAdapter.id = uuidv4(); - dispatch(ipaLayerRecalled(clone)); + dispatch(ipAdapterRecalled(clone)); return; } @@ -315,7 +315,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { ipAdapter.id = uuidv4(); } clone.id = getRGLayerId(uuidv4()); - dispatch(rgLayerRecalled(clone)); + dispatch(regionalGuidanceRecalled(clone)); return; } @@ -325,7 +325,7 @@ const recallLayer: MetadataRecallFunc = async (layer) => { } }; -const recallLayers: MetadataRecallFunc = (layers) => { +const recallLayers: MetadataRecallFunc = (layers) => { const { dispatch } = getStore(); dispatch(allLayersDeleted()); for (const l of layers) { diff --git a/invokeai/frontend/web/src/features/metadata/util/validators.ts b/invokeai/frontend/web/src/features/metadata/util/validators.ts index 759e8ba561..2d79854183 100644 --- a/invokeai/frontend/web/src/features/metadata/util/validators.ts +++ b/invokeai/frontend/web/src/features/metadata/util/validators.ts @@ -1,5 +1,5 @@ import { getStore } from 'app/store/nanostores/store'; -import type { Layer } from 'features/controlLayers/store/types'; +import type { LayerData } from 'features/controlLayers/store/types'; import type { LoRA } from 'features/lora/store/loraSlice'; import type { ControlNetConfigMetadata, @@ -110,7 +110,7 @@ const validateIPAdapters: MetadataValidateFunc = (ipA return new Promise((resolve) => resolve(validatedIPAdapters)); }; -const validateLayer: MetadataValidateFunc = async (layer) => { +const validateLayer: MetadataValidateFunc = async (layer) => { if (layer.type === 'control_adapter_layer') { const model = layer.controlAdapter.model; assert(model, 'Control Adapter layer missing model'); @@ -132,8 +132,8 @@ const validateLayer: MetadataValidateFunc = async (layer) => { return layer; }; -const validateLayers: MetadataValidateFunc = async (layers) => { - const validatedLayers: Layer[] = []; +const validateLayers: MetadataValidateFunc = async (layers) => { + const validatedLayers: LayerData[] = []; for (const l of layers) { try { const validated = await validateLayer(l); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts index 6adee17064..f130aa5671 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addControlLayers.ts @@ -5,8 +5,8 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming'; import { renderers } from 'features/controlLayers/konva/renderers/layers'; -import { rgLayerMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; -import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; +import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice'; +import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types'; import { isControlAdapterLayer, isInitialImageLayer, @@ -70,7 +70,7 @@ export const addControlLayers = async ( | Invocation<'vae_loader'> | Invocation<'main_model_loader'> | Invocation<'sdxl_model_loader'> -): Promise => { +): Promise => { const isSDXL = base === 'sdxl'; const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, base)); @@ -492,7 +492,7 @@ const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean return hasModel && modelMatchesBase && hasImage; }; -const isValidLayer = (layer: Layer, base: BaseModelType) => { +const isValidLayer = (layer: LayerData, base: BaseModelType) => { if (!layer.isEnabled) { return false; } @@ -532,7 +532,7 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise getShouldProcessPrompt(controlLayers.present.positivePrompt) ? dynamicPrompts.prompts.length : 1 diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 47392cdb8c..fbb6ca74de 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-a import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectCanvasSlice } from 'features/canvas/store/canvasSlice'; -import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { HrfSettings } from 'features/hrf/components/HrfSettings'; import { selectHrfSlice } from 'features/hrf/store/hrfSlice'; import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing'; @@ -24,7 +24,7 @@ import { ImageSizeCanvas } from './ImageSizeCanvas'; import { ImageSizeLinear } from './ImageSizeLinear'; const selector = createMemoizedSelector( - [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectControlLayersSlice, activeTabNameSelector], + [selectGenerationSlice, selectCanvasSlice, selectHrfSlice, selectCanvasV2Slice, activeTabNameSelector], (generation, canvas, hrf, controlLayers, activeTabName) => { const { shouldRandomizeSeed, model } = generation; const { hrfEnabled } = hrf;