From 3270d36fca183abaec8ba88929361db349545b51 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 26 Aug 2024 18:52:28 +1000
Subject: [PATCH] feat(ui): normalize all actions to accept an entityIdentifier
Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type.
All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed.
`selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed.
The types for canvas entities are updated with optional type parameters for this purpose.
All reducers, actions and components have been updated.
---
.../listeners/imageDeletionListeners.ts | 7 +-
.../listeners/imageDropped.ts | 12 +-
.../listeners/imageUploaded.ts | 6 +-
.../listeners/modelsLoaded.ts | 13 +-
.../src/common/hooks/useIsReadyToEnqueue.ts | 2 +-
.../ControlLayer/ControlLayerBadges.tsx | 6 +-
.../ControlLayerControlAdapter.tsx | 18 +-
.../ControlLayer/ControlLayerEntityList.tsx | 2 +-
.../ControlLayerMenuItemsControlToRaster.tsx | 6 +-
...ontrolLayerMenuItemsTransparencyEffect.tsx | 15 +-
.../components/IPAdapter/IPAdapterList.tsx | 2 +-
.../IPAdapter/IPAdapterSettings.tsx | 42 +++--
.../InpaintMask/InpaintMaskList.tsx | 2 +-
.../InpaintMaskMaskFillColorPicker.tsx | 6 +-
.../RasterLayer/RasterLayerEntityList.tsx | 2 +-
.../RasterLayerMenuItemsRasterToControl.tsx | 6 +-
...onalGuidanceAddPromptsIPAdapterButtons.tsx | 30 ++--
.../RegionalGuidanceBadges.tsx | 6 +-
.../RegionalGuidanceEntityList.tsx | 2 +-
.../RegionalGuidanceIPAdapterSettings.tsx | 49 +++---
.../RegionalGuidanceIPAdapters.tsx | 16 +-
.../RegionalGuidanceMaskFillColorPicker.tsx | 14 +-
...uidanceMenuItemsAddPromptsAndIPAdapter.tsx | 24 +--
.../RegionalGuidanceMenuItemsAutoNegative.tsx | 10 +-
.../RegionalGuidanceNegativePrompt.tsx | 20 +--
.../RegionalGuidancePositivePrompt.tsx | 20 +--
.../RegionalGuidanceSettings.tsx | 22 +--
.../common/CanvasEntityMenuItemsArrange.tsx | 2 +-
.../components/common/CanvasEntityOpacity.tsx | 3 +-
.../common/CanvasEntityPreviewImage.tsx | 2 +-
.../contexts/EntityIdentifierContext.ts | 11 +-
.../hooks/useCanvasDeleteLayerHotkey.ts | 3 +-
.../hooks/useCanvasResetLayerHotkey.ts | 3 +-
.../controlLayers/hooks/useEntityIsEnabled.ts | 2 +-
.../hooks/useEntityObjectCount.ts | 2 +-
.../hooks/useEntitySelectionColor.ts | 2 +-
.../controlLayers/hooks/useEntityTitle.ts | 2 +-
.../controlLayers/hooks/useEntityTypeCount.ts | 2 +-
.../hooks/useEntityTypeIsHidden.ts | 2 +-
.../hooks/useLayerControlAdapter.ts | 7 +-
.../konva/CanvasStateApiModule.ts | 2 +-
.../controlLayers/store/canvasV2Slice.ts | 66 +-------
.../store/controlLayersReducers.ts | 70 ++++----
.../store/inpaintMaskReducers.ts | 38 ++---
.../controlLayers/store/ipAdaptersReducers.ts | 78 +++++----
.../store/rasterLayersReducers.ts | 18 +-
.../controlLayers/store/regionsReducers.ts | 144 +++++++++-------
.../features/controlLayers/store/selectors.ts | 155 +++++++++++++++++-
.../src/features/controlLayers/store/types.ts | 21 ++-
.../components/DeleteImageModal.tsx | 2 +-
.../deleteImageModal/store/selectors.ts | 2 +-
.../components/Boards/DeleteBoardModal.tsx | 2 +-
.../src/features/lora/components/LoRAList.tsx | 2 +-
.../features/lora/components/LoRASelect.tsx | 3 +-
.../parameters/components/Prompts/Prompts.tsx | 2 +-
.../queue/components/QueueButtonTooltip.tsx | 2 +-
.../AdvancedSettingsAccordion.tsx | 2 +-
.../GenerationSettingsAccordion.tsx | 2 +-
.../ImageSettingsAccordion.tsx | 2 +-
.../RefinerSettingsAccordion.tsx | 2 +-
60 files changed, 588 insertions(+), 430 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
index d6c284f3c0..cf1fc0ff30 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
@@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@@ -51,9 +52,9 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
// };
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
- state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => {
- if (ipAdapter.image?.image_name === imageDTO.image_name) {
- dispatch(ipaImageChanged({ id, imageDTO: null }));
+ state.canvasV2.ipAdapters.entities.forEach((entity) => {
+ if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
+ dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
}
});
};
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 658b219dbb..1d6bdacaa6 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
@@ -51,7 +51,9 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payload.imageDTO
) {
const { id } = overData.context;
- dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
+ dispatch(
+ ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO: activeData.payload.imageDTO })
+ );
return;
}
@@ -64,7 +66,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payload.imageDTO
) {
const { id, ipAdapterId } = overData.context;
- dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO }));
+ dispatch(
+ rgIPAdapterImageChanged({
+ entityIdentifier: { id, type: 'regional_guidance' },
+ ipAdapterId,
+ imageDTO: activeData.payload.imageDTO,
+ })
+ );
return;
}
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 e5eaf71c30..c10fc60a4d 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
@@ -89,14 +89,16 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;
- dispatch(ipaImageChanged({ id, imageDTO }));
+ dispatch(ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
const { id, ipAdapterId } = postUploadAction;
- dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
+ dispatch(
+ rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, ipAdapterId, imageDTO })
+ );
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
index ade6772b1f..f83e34b036 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
@@ -13,6 +13,7 @@ import {
rgIPAdapterModelChanged,
vaeSelected,
} from 'features/controlLayers/store/canvasV2Slice';
+import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
@@ -178,7 +179,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log)
if (isModelAvailable) {
return;
}
- dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null }));
+ dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
};
@@ -189,16 +190,18 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
if (isModelAvailable) {
return;
}
- dispatch(ipaModelChanged({ id: entity.id, modelConfig: null }));
+ dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
- state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => {
- ipAdapters.forEach(({ id: ipAdapterId, model }) => {
+ state.canvasV2.regions.entities.forEach((entity) => {
+ entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => {
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
if (isModelAvailable) {
return;
}
- dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null }));
+ dispatch(
+ rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), ipAdapterId, modelConfig: null })
+ );
});
});
};
diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
index 64d47df650..cf17a945ed 100644
--- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
+++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx
index 28224876ff..ec68367b3a 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerBadges.tsx
@@ -1,15 +1,15 @@
import { Badge } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const ControlLayerBadges = memo(() => {
- const { id } = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('control_layer');
const { t } = useTranslation();
const withTransparencyEffect = useAppSelector(
- (s) => selectControlLayerEntityOrThrow(s.canvasV2, id).withTransparencyEffect
+ (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).withTransparencyEffect
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx
index 7df91e0084..3b9e6d1a31 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapter.tsx
@@ -18,35 +18,35 @@ import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/
export const ControlLayerControlAdapter = memo(() => {
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('control_layer');
const controlAdapter = useControlLayerControlAdapter(entityIdentifier);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(controlLayerBeginEndStepPctChanged({ id: entityIdentifier.id, beginEndStepPct }));
+ dispatch(controlLayerBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
- dispatch(controlLayerControlModeChanged({ id: entityIdentifier.id, controlMode }));
+ dispatch(controlLayerControlModeChanged({ entityIdentifier, controlMode }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(controlLayerWeightChanged({ id: entityIdentifier.id, weight }));
+ dispatch(controlLayerWeightChanged({ entityIdentifier, weight }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
- dispatch(controlLayerModelChanged({ id: entityIdentifier.id, modelConfig }));
+ dispatch(controlLayerModelChanged({ entityIdentifier, modelConfig }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx
index f19e4ca6f3..8f94481f86 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
import { mapId } from 'features/controlLayers/konva/util';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx
index e64df4e9e5..924122fe24 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsControlToRaster.tsx
@@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi';
export const ControlLayerMenuItemsControlToRaster = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('control_layer');
const convertControlLayerToRasterLayer = useCallback(() => {
- dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id }));
- }, [dispatch, entityIdentifier.id]);
+ dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
return (
}>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx
index fe53a785fd..5342e975ba 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect.tsx
@@ -2,11 +2,8 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import {
- controlLayerWithTransparencyEffectToggled,
- selectCanvasV2Slice,
-} from 'features/controlLayers/store/canvasV2Slice';
-import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
+import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfBold } from 'react-icons/pi';
@@ -14,18 +11,18 @@ import { PiDropHalfBold } from 'react-icons/pi';
export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('control_layer');
const selectWithTransparencyEffect = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
- const entity = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id);
+ const entity = selectEntityOrThrow(canvasV2, entityIdentifier);
return entity.withTransparencyEffect;
}),
- [entityIdentifier.id]
+ [entityIdentifier]
);
const withTransparencyEffect = useAppSelector(selectWithTransparencyEffect);
const onToggle = useCallback(() => {
- dispatch(controlLayerWithTransparencyEffectToggled({ id: entityIdentifier.id }));
+ dispatch(controlLayerWithTransparencyEffectToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
index b0ad9108a8..cdfcc897b5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterList.tsx
@@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
import { mapId } from 'features/controlLayers/konva/util';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
index d0d91646e9..0f8152fd9f 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterSettings.tsx
@@ -13,7 +13,7 @@ import {
ipaModelChanged,
ipaWeightChanged,
} from 'features/controlLayers/store/canvasV2Slice';
-import { selectIPAdapterEntityOrThrow } from 'features/controlLayers/store/ipAdaptersReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { IPAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@@ -24,53 +24,59 @@ import { IPAdapterModel } from './IPAdapterModel';
export const IPAdapterSettings = memo(() => {
const dispatch = useAppDispatch();
- const { id } = useEntityIdentifierContext();
- const ipAdapter = useAppSelector((s) => selectIPAdapterEntityOrThrow(s.canvasV2, id).ipAdapter);
+ const entityIdentifier = useEntityIdentifierContext('ip_adapter');
+ const ipAdapter = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapter);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(ipaBeginEndStepPctChanged({ id, beginEndStepPct }));
+ dispatch(ipaBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(ipaWeightChanged({ id, weight }));
+ dispatch(ipaWeightChanged({ entityIdentifier, weight }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
- dispatch(ipaMethodChanged({ id, method }));
+ dispatch(ipaMethodChanged({ entityIdentifier, method }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
- dispatch(ipaModelChanged({ id, modelConfig }));
+ dispatch(ipaModelChanged({ entityIdentifier, modelConfig }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
- dispatch(ipaCLIPVisionModelChanged({ id, clipVisionModel }));
+ dispatch(ipaCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
- dispatch(ipaImageChanged({ id, imageDTO }));
+ dispatch(ipaImageChanged({ entityIdentifier, imageDTO }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
- const droppableData = useMemo(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]);
- const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]);
+ const droppableData = useMemo(
+ () => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }),
+ [entityIdentifier.id]
+ );
+ const postUploadAction = useMemo(
+ () => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
+ [entityIdentifier.id]
+ );
return (
@@ -95,7 +101,7 @@ export const IPAdapterSettings = memo(() => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx
index 42351a0dab..b4f92759e4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskList.tsx
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
import { mapId } from 'features/controlLayers/konva/util';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
index 97855daad4..426d5261f5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskMaskFillColorPicker.tsx
@@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
-import { selectInpaintMaskEntityOrThrow } from 'features/controlLayers/store/inpaintMaskReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,8 +13,8 @@ import { useTranslation } from 'react-i18next';
export const InpaintMaskMaskFillColorPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext();
- const fill = useAppSelector((s) => selectInpaintMaskEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
+ const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
+ const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx
index 5c5ee557dc..82bf728d0b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerEntityList.tsx
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { mapId } from 'features/controlLayers/konva/util';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx
index 57c97b3d87..f844f3aa38 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerMenuItemsRasterToControl.tsx
@@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi';
export const RasterLayerMenuItemsRasterToControl = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('raster_layer');
const convertRasterLayerToControlLayer = useCallback(() => {
- dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id }));
- }, [dispatch, entityIdentifier.id]);
+ dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
return (
}>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
index 4533320714..4e1a302233 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons.tsx
@@ -1,44 +1,42 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
- selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
-type AddPromptButtonProps = {
- id: string;
-};
-
-export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => {
+export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
- const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
+ const entity = selectEntityOrThrow(canvasV2, entityIdentifier);
return {
- canAddPositivePrompt: rg?.positivePrompt === null,
- canAddNegativePrompt: rg?.negativePrompt === null,
+ canAddPositivePrompt: entity?.positivePrompt === null,
+ canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
- [id]
+ [entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
- dispatch(rgPositivePromptChanged({ id, prompt: '' }));
- }, [dispatch, id]);
+ dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
+ }, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
- dispatch(rgNegativePromptChanged({ id, prompt: '' }));
- }, [dispatch, id]);
+ dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
+ }, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
- dispatch(rgIPAdapterAdded({ id }));
- }, [dispatch, id]);
+ dispatch(rgIPAdapterAdded({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx
index fb35b5068e..be928090f1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges.tsx
@@ -1,14 +1,14 @@
import { Badge } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceBadges = memo(() => {
- const { id } = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
- const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative);
+ const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative);
return (
<>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx
index 8e4a5f588a..ef6faa51b1 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceEntityList.tsx
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
import { mapId } from 'features/controlLayers/konva/util';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
index 47497848bf..cc60a8a023 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings.tsx
@@ -5,6 +5,7 @@ import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterCLIPVisionModelChanged,
@@ -14,7 +15,7 @@ import {
rgIPAdapterModelChanged,
rgIPAdapterWeightChanged,
} from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@@ -23,71 +24,75 @@ import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction }
import { assert } from 'tsafe';
type Props = {
- id: string;
ipAdapterId: string;
ipAdapterNumber: number;
};
-export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => {
+export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterNumber }: Props) => {
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
- dispatch(rgIPAdapterDeleted({ id, ipAdapterId }));
- }, [dispatch, ipAdapterId, id]);
+ dispatch(rgIPAdapterDeleted({ entityIdentifier, ipAdapterId }));
+ }, [dispatch, entityIdentifier, ipAdapterId]);
const ipAdapter = useAppSelector((s) => {
- const ipa = selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId);
+ const ipa = selectRegionalGuidanceIPAdapter(s.canvasV2, entityIdentifier, ipAdapterId);
assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`);
return ipa;
});
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
- dispatch(rgIPAdapterBeginEndStepPctChanged({ id, ipAdapterId, beginEndStepPct }));
+ dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, ipAdapterId, beginEndStepPct }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const onChangeWeight = useCallback(
(weight: number) => {
- dispatch(rgIPAdapterWeightChanged({ id, ipAdapterId, weight }));
+ dispatch(rgIPAdapterWeightChanged({ entityIdentifier, ipAdapterId, weight }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
- dispatch(rgIPAdapterMethodChanged({ id, ipAdapterId, method }));
+ dispatch(rgIPAdapterMethodChanged({ entityIdentifier, ipAdapterId, method }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
- dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig }));
+ dispatch(rgIPAdapterModelChanged({ entityIdentifier, ipAdapterId, modelConfig }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
- dispatch(rgIPAdapterCLIPVisionModelChanged({ id, ipAdapterId, clipVisionModel }));
+ dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, ipAdapterId, clipVisionModel }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
- dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
+ dispatch(rgIPAdapterImageChanged({ entityIdentifier, ipAdapterId, imageDTO }));
},
- [dispatch, ipAdapterId, id]
+ [dispatch, entityIdentifier, ipAdapterId]
);
const droppableData = useMemo(
- () => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }),
- [ipAdapterId, id]
+ () => ({
+ actionType: 'SET_RG_IP_ADAPTER_IMAGE',
+ context: { id: entityIdentifier.id, ipAdapterId },
+ id: entityIdentifier.id,
+ }),
+ [entityIdentifier.id, ipAdapterId]
);
const postUploadAction = useMemo(
- () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }),
- [ipAdapterId, id]
+ () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, ipAdapterId }),
+ [entityIdentifier.id, ipAdapterId]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx
index 6967848d65..ae47edb3e4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapters.tsx
@@ -3,25 +3,23 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
+import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { Fragment, memo, useMemo } from 'react';
-type Props = {
- id: string;
-};
+export const RegionalGuidanceIPAdapters = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
-export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
- const ipAdapterIds = selectRegionalGuidanceEntityOrThrow(canvasV2, id).ipAdapters.map(({ id }) => id);
+ const ipAdapterIds = selectEntityOrThrow(canvasV2, entityIdentifier).ipAdapters.map(({ id }) => id);
if (ipAdapterIds.length === 0) {
return EMPTY_ARRAY;
}
return ipAdapterIds;
}),
- [id]
+ [entityIdentifier]
);
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
@@ -35,7 +33,7 @@ export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => {
{ipAdapterIds.map((ipAdapterId, index) => (
{index > 0 && }
-
+
))}
>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
index f42e5f44e4..331401b705 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMaskFillColorPicker.tsx
@@ -5,27 +5,27 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMaskFillColorPicker = memo(() => {
- const entityIdentifier = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
+ const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {
- dispatch(rgFillColorChanged({ id: entityIdentifier.id, color }));
+ dispatch(rgFillColorChanged({ entityIdentifier, color }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
const onChangeFillStyle = useCallback(
(style: FillStyle) => {
- dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style }));
+ dispatch(rgFillStyleChanged({ entityIdentifier, style }));
},
- [dispatch, entityIdentifier.id]
+ [dispatch, entityIdentifier]
);
return (
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
index 5556c25929..d47b6cc0f9 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAddPromptsAndIPAdapter.tsx
@@ -6,36 +6,36 @@ import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
- selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
- const { id } = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
- const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
+ const entity = selectEntity(canvasV2, entityIdentifier);
return {
- canAddPositivePrompt: rg?.positivePrompt === null,
- canAddNegativePrompt: rg?.negativePrompt === null,
+ canAddPositivePrompt: entity?.positivePrompt === null,
+ canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
- [id]
+ [entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
- dispatch(rgPositivePromptChanged({ id: id, prompt: '' }));
- }, [dispatch, id]);
+ dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
+ }, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
- dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
- }, [dispatch, id]);
+ dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
+ }, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
- dispatch(rgIPAdapterAdded({ id }));
- }, [dispatch, id]);
+ dispatch(rgIPAdapterAdded({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
return (
<>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx
index 66ab80be8a..955cea7d66 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceMenuItemsAutoNegative.tsx
@@ -2,19 +2,19 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSelectionInverseBold } from 'react-icons/pi';
export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
- const { id } = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
- const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative);
+ const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative);
const onClick = useCallback(() => {
- dispatch(rgAutoNegativeToggled({ id }));
- }, [dispatch, id]);
+ dispatch(rgAutoNegativeToggled({ entityIdentifier }));
+ }, [dispatch, entityIdentifier]);
return (
} onClick={onClick}>
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx
index f24dedce03..b83539fbc8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceNegativePrompt.tsx
@@ -1,8 +1,9 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-type Props = {
- id: string;
-};
-
-export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => {
- const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt ?? '');
+export const RegionalGuidanceNegativePrompt = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
+ const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef(null);
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
- dispatch(rgNegativePromptChanged({ id, prompt: v }));
+ dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: v }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onDeletePrompt = useCallback(() => {
- dispatch(rgNegativePromptChanged({ id, prompt: null }));
- }, [dispatch, id]);
+ dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: null }));
+ }, [dispatch, entityIdentifier]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt,
textareaRef,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx
index 44a371873e..e699dcf8b2 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidancePositivePrompt.tsx
@@ -1,8 +1,9 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
+import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-type Props = {
- id: string;
-};
-
-export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => {
- const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt ?? '');
+export const RegionalGuidancePositivePrompt = memo(() => {
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
+ const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef(null);
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
- dispatch(rgPositivePromptChanged({ id, prompt: v }));
+ dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: v }));
},
- [dispatch, id]
+ [dispatch, entityIdentifier]
);
const onDeletePrompt = useCallback(() => {
- dispatch(rgPositivePromptChanged({ id, prompt: null }));
- }, [dispatch, id]);
+ dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: null }));
+ }, [dispatch, entityIdentifier]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt,
textareaRef,
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx
index 54165cfbeb..b2c1e3db98 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings.tsx
@@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
-import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
+import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters';
@@ -11,35 +11,31 @@ import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt
import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt';
export const RegionalGuidanceSettings = memo(() => {
- const { id } = useEntityIdentifierContext();
+ const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const hasPositivePrompt = useAppSelector(
- (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt !== null
+ (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt !== null
);
const hasNegativePrompt = useAppSelector(
- (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null
- );
- const hasIPAdapters = useAppSelector(
- (s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0
+ (s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt !== null
);
+ const hasIPAdapters = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapters.length > 0);
return (
- {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && (
-
- )}
+ {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && }
{hasPositivePrompt && (
<>
-
+
{(hasNegativePrompt || hasIPAdapters) && }
>
)}
{hasNegativePrompt && (
<>
-
+
{hasIPAdapters && }
>
)}
- {hasIPAdapters && }
+ {hasIPAdapters && }
);
});
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
index 6342613ea5..1582cce92d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsArrange.tsx
@@ -7,8 +7,8 @@ import {
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
- selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx
index c5be221c6d..dc5c589510 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityOpacity.tsx
@@ -15,7 +15,8 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { snapToNearest } from 'features/controlLayers/konva/util';
-import { entityOpacityChanged, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { entityOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { selectEntity } from 'features/controlLayers/store/selectors';
import { isDrawableEntity } from 'features/controlLayers/store/types';
import { clamp, round } from 'lodash-es';
import type { KeyboardEvent } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx
index 142f2cdacc..2a7630c1a8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityPreviewImage.tsx
@@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
-import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { memo, useEffect, useMemo, useRef } from 'react';
import { useSelector } from 'react-redux';
diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts
index 04489e8e9d..d0d049a2b5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts
@@ -1,11 +1,16 @@
-import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
+import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import { createContext, useContext } from 'react';
import { assert } from 'tsafe';
export const EntityIdentifierContext = createContext(null);
-export const useEntityIdentifierContext = (): CanvasEntityIdentifier => {
+export const useEntityIdentifierContext = (
+ type?: T
+): CanvasEntityIdentifier => {
const entityIdentifier = useContext(EntityIdentifierContext);
assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider');
- return entityIdentifier;
+ if (type) {
+ assert(entityIdentifier.type === type, 'useEntityIdentifier must be used with the correct type');
+ }
+ return entityIdentifier as CanvasEntityIdentifier;
};
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
index 0baa6823d9..88f444401c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts
@@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { entityDeleted, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
index f15d087b0f..c752a7348c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts
@@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
-import { entityReset, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { entityReset } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts
index e37b402ea6..022cbeb212 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts
@@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts
index 29f14377ab..62f3a8e8d0 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityObjectCount.ts
@@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts
index 2815577bff..15061d28b8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts
@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
-import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
index 1c9586ce99..c5649963d8 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTitle.ts
@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount';
-import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
index f2209af4a9..e770f220fb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeCount.ts
@@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
index d35bf9efa0..0e867be2be 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityTypeIsHidden.ts
@@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts
index 827174c2df..93157c3564 100644
--- a/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useLayerControlAdapter.ts
@@ -1,8 +1,7 @@
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
-import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
+import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
ControlNetConfig,
@@ -14,11 +13,11 @@ import { zModelIdentifierField } from 'features/nodes/types/common';
import { useMemo } from 'react';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
-export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => {
+export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
const selectControlAdapter = useMemo(
() =>
createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => {
- const layer = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id);
+ const layer = selectEntityOrThrow(canvasV2, entityIdentifier);
return layer.controlAdapter;
}),
[entityIdentifier]
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
index 83ba26d8d2..377c15cebb 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts
@@ -25,10 +25,10 @@ import {
entitySelected,
eraserWidthChanged,
fillChanged,
- selectAllRenderableEntities,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/canvasV2Slice';
+import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
index 425e7529ae..611cf49622 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts
@@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
-import type { PersistConfig, RootState } from 'app/store/store';
+import type { PersistConfig } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@@ -13,6 +13,7 @@ import { lorasReducers } from 'features/controlLayers/store/lorasReducers';
import { paramsReducers } from 'features/controlLayers/store/paramsReducers';
import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers';
import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
+import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors';
import { sessionReducers } from 'features/controlLayers/store/sessionReducers';
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
import { toolReducers } from 'features/controlLayers/store/toolReducers';
@@ -25,12 +26,7 @@ import { atom } from 'nanostores';
import { assert } from 'tsafe';
import type {
- CanvasControlLayerState,
CanvasEntityIdentifier,
- CanvasEntityState,
- CanvasInpaintMaskState,
- CanvasRasterLayerState,
- CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
EntityBrushLineAddedPayload,
@@ -143,60 +139,6 @@ const initialState: CanvasV2State = {
},
};
-export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) {
- switch (type) {
- case 'raster_layer':
- return state.rasterLayers.entities.find((entity) => entity.id === id);
- case 'control_layer':
- return state.controlLayers.entities.find((entity) => entity.id === id);
- case 'inpaint_mask':
- return state.inpaintMasks.entities.find((entity) => entity.id === id);
- case 'regional_guidance':
- return state.regions.entities.find((entity) => entity.id === id);
- case 'ip_adapter':
- return state.ipAdapters.entities.find((entity) => entity.id === id);
- default:
- return;
- }
-}
-
-function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState['type']): CanvasEntityState[] {
- switch (type) {
- case 'raster_layer':
- return state.rasterLayers.entities;
- case 'control_layer':
- return state.controlLayers.entities;
- case 'inpaint_mask':
- return state.inpaintMasks.entities;
- case 'regional_guidance':
- return state.regions.entities;
- case 'ip_adapter':
- return state.ipAdapters.entities;
- }
-}
-
-function selectAllEntities(state: CanvasV2State): CanvasEntityState[] {
- // These are in the same order as they are displayed in the list!
- return [
- ...state.inpaintMasks.entities.toReversed(),
- ...state.regions.entities.toReversed(),
- ...state.ipAdapters.entities.toReversed(),
- ...state.controlLayers.entities.toReversed(),
- ...state.rasterLayers.entities.toReversed(),
- ];
-}
-
-export function selectAllRenderableEntities(
- state: CanvasV2State
-): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] {
- return [
- ...state.rasterLayers.entities,
- ...state.controlLayers.entities,
- ...state.inpaintMasks.entities,
- ...state.regions.entities,
- ];
-}
-
export const canvasV2Slice = createSlice({
name: 'canvasV2',
initialState,
@@ -217,7 +159,7 @@ export const canvasV2Slice = createSlice({
const { entityIdentifier } = action.payload;
state.selectedEntityIdentifier = entityIdentifier;
},
- entityNameChanged: (state, action: PayloadAction) => {
+ entityNameChanged: (state, action: PayloadAction>) => {
const { entityIdentifier, name } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
@@ -617,8 +559,6 @@ export const {
sessionModeChanged,
} = canvasV2Slice.actions;
-export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
-
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts
index d639984691..7ed4a341ce 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/controlLayersReducers.ts
@@ -1,10 +1,10 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectEntity } from 'features/controlLayers/store/selectors';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge, omit } from 'lodash-es';
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
-import { assert } from 'tsafe';
import type {
CanvasControlLayerState,
@@ -12,18 +12,11 @@ import type {
CanvasV2State,
ControlModeV2,
ControlNetConfig,
+ EntityIdentifierPayload,
T2IAdapterConfig,
} from './types';
import { getEntityIdentifier, initialControlNet } from './types';
-const selectControlLayerEntity = (state: CanvasV2State, id: string) =>
- state.controlLayers.entities.find((entity) => entity.id === id);
-export const selectControlLayerEntityOrThrow = (state: CanvasV2State, id: string) => {
- const layer = selectControlLayerEntity(state, id);
- assert(layer, `Layer with id ${id} not found`);
- return layer;
-};
-
export const controlLayersReducers = {
controlLayerAdded: {
reducer: (
@@ -58,9 +51,9 @@ export const controlLayersReducers = {
state.selectedEntityIdentifier = { type: 'control_layer', id: data.id };
},
controlLayerConvertedToRasterLayer: {
- reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
- const { id, newId } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ reducer: (state, action: PayloadAction>) => {
+ const { entityIdentifier, newId } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
@@ -73,26 +66,30 @@ export const controlLayersReducers = {
};
// Remove the control layer
- state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== id);
+ state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
// Add the new raster layer
state.rasterLayers.entities.push(rasterLayerState);
state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id };
},
- prepare: (payload: { id: string }) => ({
+ prepare: (payload: EntityIdentifierPayload) => ({
payload: { ...payload, newId: getPrefixedId('raster_layer') },
}),
},
controlLayerModelChanged: (
state,
- action: PayloadAction<{
- id: string;
- modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
- }>
+ action: PayloadAction<
+ EntityIdentifierPayload<
+ {
+ modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
+ },
+ 'control_layer'
+ >
+ >
) => {
- const { id, modelConfig } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ const { entityIdentifier, modelConfig } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
@@ -118,17 +115,23 @@ export const controlLayersReducers = {
layer.controlAdapter = t2iAdapterConfig;
}
},
- controlLayerControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
- const { id, controlMode } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ controlLayerControlModeChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, controlMode } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') {
return;
}
layer.controlAdapter.controlMode = controlMode;
},
- controlLayerWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
- const { id, weight } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ controlLayerWeightChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, weight } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
@@ -136,18 +139,21 @@ export const controlLayersReducers = {
},
controlLayerBeginEndStepPctChanged: (
state,
- action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>
+ action: PayloadAction>
) => {
- const { id, beginEndStepPct } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ const { entityIdentifier, beginEndStepPct } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
},
- controlLayerWithTransparencyEffectToggled: (state, action: PayloadAction<{ id: string }>) => {
- const { id } = action.payload;
- const layer = selectControlLayerEntity(state, id);
+ controlLayerWithTransparencyEffectToggled: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
index 9952c8a345..9056f082a6 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts
@@ -1,23 +1,15 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { getPrefixedId } from 'features/controlLayers/konva/util';
-import {
- type CanvasInpaintMaskState,
- type CanvasV2State,
- type EntityIdentifierPayload,
- type FillStyle,
- getEntityIdentifier,
- type RgbColor,
+import { selectEntity } from 'features/controlLayers/store/selectors';
+import type {
+ CanvasInpaintMaskState,
+ CanvasV2State,
+ EntityIdentifierPayload,
+ FillStyle,
+ RgbColor,
} from 'features/controlLayers/store/types';
+import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { merge } from 'lodash-es';
-import { assert } from 'tsafe';
-
-const selectInpaintMaskEntity = (state: CanvasV2State, id: string) =>
- state.inpaintMasks.entities.find((layer) => layer.id === id);
-export const selectInpaintMaskEntityOrThrow = (state: CanvasV2State, id: string) => {
- const entity = selectInpaintMaskEntity(state, id);
- assert(entity, `Inpaint mask with id ${id} not found`);
- return entity;
-};
export const inpaintMaskReducers = {
inpaintMaskAdded: {
@@ -54,17 +46,23 @@ export const inpaintMaskReducers = {
state.inpaintMasks.entities = [data];
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
- inpaintMaskFillColorChanged: (state, action: PayloadAction>) => {
+ inpaintMaskFillColorChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
const { color, entityIdentifier } = action.payload;
- const entity = selectInpaintMaskEntity(state, entityIdentifier.id);
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
- inpaintMaskFillStyleChanged: (state, action: PayloadAction>) => {
+ inpaintMaskFillStyleChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
const { style, entityIdentifier } = action.payload;
- const entity = selectInpaintMaskEntity(state, entityIdentifier.id);
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts
index 1fa9513975..d3bb67c9ce 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/ipAdaptersReducers.ts
@@ -1,23 +1,20 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectEntity } from 'features/controlLayers/store/selectors';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
-import { assert } from 'tsafe';
-import { v4 as uuidv4 } from 'uuid';
-import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from './types';
+import type {
+ CanvasIPAdapterState,
+ CanvasV2State,
+ CLIPVisionModelV2,
+ EntityIdentifierPayload,
+ IPMethodV2,
+} from './types';
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types';
-const selectIPAdapterEntity = (state: CanvasV2State, id: string) =>
- state.ipAdapters.entities.find((ipa) => ipa.id === id);
-export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) => {
- const entity = selectIPAdapterEntity(state, id);
- assert(entity, `IP Adapter with id ${id} not found`);
- return entity;
-};
-
export const ipAdaptersReducers = {
ipaAdded: {
reducer: (
@@ -47,52 +44,61 @@ export const ipAdaptersReducers = {
state.ipAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
},
- ipaImageChanged: {
- reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
- const { id, imageDTO } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
- if (!entity) {
- return;
- }
- entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
- },
- prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
+ ipaImageChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, imageDTO } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
+ if (!entity) {
+ return;
+ }
+ entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
- ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => {
- const { id, method } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
+ ipaMethodChanged: (state, action: PayloadAction>) => {
+ const { entityIdentifier, method } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.method = method;
},
- ipaModelChanged: (state, action: PayloadAction<{ id: string; modelConfig: IPAdapterModelConfig | null }>) => {
- const { id, modelConfig } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
+ ipaModelChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, modelConfig } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
},
- ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => {
- const { id, clipVisionModel } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
+ ipaCLIPVisionModelChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, clipVisionModel } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.clipVisionModel = clipVisionModel;
},
- ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
- const { id, weight } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
+ ipaWeightChanged: (state, action: PayloadAction>) => {
+ const { entityIdentifier, weight } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.weight = weight;
},
- ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
- const { id, beginEndStepPct } = action.payload;
- const entity = selectIPAdapterEntity(state, id);
+ ipaBeginEndStepPctChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, beginEndStepPct } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts
index 2427055500..0db696b84e 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/rasterLayersReducers.ts
@@ -1,14 +1,12 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectEntity } from 'features/controlLayers/store/selectors';
import { merge } from 'lodash-es';
-import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types';
+import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, EntityIdentifierPayload } from './types';
import { getEntityIdentifier, initialControlNet } from './types';
-const selectRasterLayerEntity = (state: CanvasV2State, id: string) =>
- state.rasterLayers.entities.find((layer) => layer.id === id);
-
export const rasterLayersReducers = {
rasterLayerAdded: {
reducer: (
@@ -38,12 +36,12 @@ export const rasterLayersReducers = {
rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => {
const { data } = action.payload;
state.rasterLayers.entities.push(data);
- state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id };
+ state.selectedEntityIdentifier = getEntityIdentifier(data);
},
rasterLayerConvertedToControlLayer: {
- reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
- const { id, newId } = action.payload;
- const layer = selectRasterLayerEntity(state, id);
+ reducer: (state, action: PayloadAction>) => {
+ const { entityIdentifier, newId } = action.payload;
+ const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
@@ -58,14 +56,14 @@ export const rasterLayersReducers = {
};
// Remove the raster layer
- state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== id);
+ state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
// Add the converted control layer
state.controlLayers.entities.push(controlLayerState);
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
},
- prepare: (payload: { id: string }) => ({
+ prepare: (payload: EntityIdentifierPayload) => ({
payload: { ...payload, newId: getPrefixedId('control_layer') },
}),
},
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
index 96d47395d6..ff2729fcc5 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts
@@ -1,9 +1,11 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
+import { selectEntity, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
import type {
CanvasV2State,
CLIPVisionModelV2,
+ EntityIdentifierPayload,
FillStyle,
IPMethodV2,
RegionalGuidanceIPAdapterConfig,
@@ -17,22 +19,6 @@ import { assert } from 'tsafe';
import type { CanvasRegionalGuidanceState } from './types';
-const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => {
- return state.regions.entities.find((rg) => rg.id === id);
-};
-const selectRegionalGuidanceIPAdapter = (state: CanvasV2State, id: string, ipAdapterId: string) => {
- const entity = state.regions.entities.find((rg) => rg.id === id);
- if (!entity) {
- return;
- }
- return entity.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
-};
-export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: string) => {
- const rg = selectRegionalGuidanceEntity(state, id);
- assert(rg, `Region with id ${id} not found`);
- return rg;
-};
-
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)
@@ -94,42 +80,54 @@ export const regionsReducers = {
state.regions.entities.push(data);
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
},
- rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
- const { id, prompt } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ rgPositivePromptChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, prompt } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.positivePrompt = prompt;
},
- rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
- const { id, prompt } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ rgNegativePromptChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, prompt } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.negativePrompt = prompt;
},
- rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => {
- const { id, color } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ rgFillColorChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, color } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
- rgFillStyleChanged: (state, action: PayloadAction<{ id: string; style: FillStyle }>) => {
- const { id, style } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ rgFillStyleChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, style } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.style = style;
},
- rgAutoNegativeToggled: (state, action: PayloadAction<{ id: string }>) => {
- const { id } = action.payload;
- const rg = selectRegionalGuidanceEntity(state, id);
+ rgAutoNegativeToggled: (state, action: PayloadAction>) => {
+ const { entityIdentifier } = action.payload;
+ const rg = selectEntity(state, entityIdentifier);
if (!rg) {
return;
}
@@ -138,10 +136,15 @@ export const regionsReducers = {
rgIPAdapterAdded: {
reducer: (
state,
- action: PayloadAction<{ id: string; ipAdapterId: string; overrides?: Partial }>
+ action: PayloadAction<
+ EntityIdentifierPayload<
+ { ipAdapterId: string; overrides?: Partial },
+ 'regional_guidance'
+ >
+ >
) => {
- const { id, overrides, ipAdapterId } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ const { entityIdentifier, overrides, ipAdapterId } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
@@ -149,13 +152,18 @@ export const regionsReducers = {
merge(ipAdapter, overrides);
entity.ipAdapters.push(ipAdapter);
},
- prepare: (payload: { id: string; overrides?: Partial }) => ({
+ prepare: (
+ payload: EntityIdentifierPayload<{ overrides?: Partial }, 'regional_guidance'>
+ ) => ({
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
}),
},
- rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
- const { id, ipAdapterId } = action.payload;
- const entity = selectRegionalGuidanceEntity(state, id);
+ rgIPAdapterDeleted: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, ipAdapterId } = action.payload;
+ const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
@@ -163,18 +171,23 @@ export const regionsReducers = {
},
rgIPAdapterImageChanged: (
state,
- action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
+ action: PayloadAction<
+ EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
+ >
) => {
- const { id, ipAdapterId, imageDTO } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ const { entityIdentifier, ipAdapterId, imageDTO } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
- rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
- const { id, ipAdapterId, weight } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ rgIPAdapterWeightChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, ipAdapterId, weight } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@@ -182,18 +195,23 @@ export const regionsReducers = {
},
rgIPAdapterBeginEndStepPctChanged: (
state,
- action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
+ action: PayloadAction<
+ EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
+ >
) => {
- const { id, ipAdapterId, beginEndStepPct } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
ipAdapter.beginEndStepPct = beginEndStepPct;
},
- rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => {
- const { id, ipAdapterId, method } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ rgIPAdapterMethodChanged: (
+ state,
+ action: PayloadAction>
+ ) => {
+ const { entityIdentifier, ipAdapterId, method } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@@ -201,14 +219,18 @@ export const regionsReducers = {
},
rgIPAdapterModelChanged: (
state,
- action: PayloadAction<{
- id: string;
- ipAdapterId: string;
- modelConfig: IPAdapterModelConfig | null;
- }>
+ action: PayloadAction<
+ EntityIdentifierPayload<
+ {
+ ipAdapterId: string;
+ modelConfig: IPAdapterModelConfig | null;
+ },
+ 'regional_guidance'
+ >
+ >
) => {
- const { id, ipAdapterId, modelConfig } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ const { entityIdentifier, ipAdapterId, modelConfig } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@@ -216,10 +238,12 @@ export const regionsReducers = {
},
rgIPAdapterCLIPVisionModelChanged: (
state,
- action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
+ action: PayloadAction<
+ EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
+ >
) => {
- const { id, ipAdapterId, clipVisionModel } = action.payload;
- const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
+ const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload;
+ const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
index d78417d55e..93e837ee2c 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/selectors.ts
@@ -1,7 +1,32 @@
import { createSelector } from '@reduxjs/toolkit';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import type { RootState } from 'app/store/store';
+import type {
+ CanvasControlLayerState,
+ CanvasEntityIdentifier,
+ CanvasEntityState,
+ CanvasInpaintMaskState,
+ CanvasRasterLayerState,
+ CanvasRegionalGuidanceState,
+ CanvasV2State,
+} from 'features/controlLayers/store/types';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
+import { assert } from 'tsafe';
+/**
+ * Selects the canvasV2 slice from the root state
+ */
+export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
+
+/**
+ * Selects the total canvas entity count:
+ * - Regions
+ * - IP adapters
+ * - Raster layers
+ * - Control layers
+ * - Inpaint masks
+ *
+ * It does not check for validity of the entities.
+ */
export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => {
return (
canvasV2.regions.entities.length +
@@ -12,6 +37,134 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2)
);
});
+/**
+ * Selects the optimal dimension for the canvas based on the currently-model
+ */
export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => {
return getOptimalDimension(canvasV2.params.model);
});
+
+/**
+ * Selects a single entity from the canvasV2 slice. If the entity identifier is narrowed to a specific type, the
+ * return type will be narrowed as well.
+ */
+export function selectEntity(
+ state: CanvasV2State,
+ entityIdentifier: T
+): Extract | undefined {
+ const { id, type } = entityIdentifier;
+
+ let entity: CanvasEntityState | undefined = undefined;
+
+ switch (type) {
+ case 'raster_layer':
+ entity = state.rasterLayers.entities.find((entity) => entity.id === id);
+ break;
+ case 'control_layer':
+ entity = state.controlLayers.entities.find((entity) => entity.id === id);
+ break;
+ case 'inpaint_mask':
+ entity = state.inpaintMasks.entities.find((entity) => entity.id === id);
+ break;
+ case 'regional_guidance':
+ entity = state.regions.entities.find((entity) => entity.id === id);
+ break;
+ case 'ip_adapter':
+ entity = state.ipAdapters.entities.find((entity) => entity.id === id);
+ break;
+ }
+
+ // This cast is safe, but TS seems to be unable to infer the type
+ return entity as Extract;
+}
+
+/**
+ * Selected an entity from the canvasV2 slice. If the entity is not found, an error is thrown.
+ * Wrapper around {@link selectEntity}.
+ */
+export function selectEntityOrThrow(
+ state: CanvasV2State,
+ entityIdentifier: T
+): Extract {
+ const entity = selectEntity(state, entityIdentifier);
+ assert(entity, `Entity with id ${entityIdentifier.id} not found`);
+ return entity;
+}
+
+/**
+ * Selects all entities of the given type.
+ */
+export function selectAllEntitiesOfType(
+ state: CanvasV2State,
+ type: T
+): Extract[] {
+ let entities: CanvasEntityState[] = [];
+
+ switch (type) {
+ case 'raster_layer':
+ entities = state.rasterLayers.entities;
+ break;
+ case 'control_layer':
+ entities = state.controlLayers.entities;
+ break;
+ case 'inpaint_mask':
+ entities = state.inpaintMasks.entities;
+ break;
+ case 'regional_guidance':
+ entities = state.regions.entities;
+ break;
+ case 'ip_adapter':
+ entities = state.ipAdapters.entities;
+ break;
+ }
+
+ // This cast is safe, but TS seems to be unable to infer the type
+ return entities as Extract[];
+}
+
+/**
+ * Selects all entities, in the order they are displayed in the list.
+ */
+export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] {
+ // These are in the same order as they are displayed in the list!
+ return [
+ ...state.inpaintMasks.entities.toReversed(),
+ ...state.regions.entities.toReversed(),
+ ...state.ipAdapters.entities.toReversed(),
+ ...state.controlLayers.entities.toReversed(),
+ ...state.rasterLayers.entities.toReversed(),
+ ];
+}
+
+/**
+ * Selects all _renderable_ entities:
+ * - Raster layers
+ * - Control layers
+ * - Inpaint masks
+ * - Regional guidance
+ */
+export function selectAllRenderableEntities(
+ state: CanvasV2State
+): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] {
+ return [
+ ...state.rasterLayers.entities,
+ ...state.controlLayers.entities,
+ ...state.inpaintMasks.entities,
+ ...state.regions.entities,
+ ];
+}
+
+/**
+ * Selects the IP adapter for the specific Regional Guidance layer.
+ */
+export function selectRegionalGuidanceIPAdapter(
+ state: CanvasV2State,
+ entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>,
+ ipAdapterId: string
+) {
+ const entity = selectEntity(state, entityIdentifier);
+ if (!entity) {
+ return undefined;
+ }
+ return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
+}
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index b9ac58c819..95eed0e25d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -1,3 +1,4 @@
+import type { SerializableObject } from 'common/types';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
@@ -692,7 +693,9 @@ export type CanvasEntityState =
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
| CanvasIPAdapterState;
-export type CanvasEntityIdentifier = Pick;
+
+export type CanvasEntityType = CanvasEntityState['type'];
+export type CanvasEntityIdentifier = { id: string; type: T };
export type LoRA = {
id: string;
@@ -819,7 +822,17 @@ export type StageAttrs = {
scale: number;
};
-export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier } & T;
+export type EntityIdentifierPayload<
+ T extends SerializableObject | void = void,
+ U extends CanvasEntityType = CanvasEntityType,
+> = T extends void
+ ? {
+ entityIdentifier: CanvasEntityIdentifier;
+ }
+ : {
+ entityIdentifier: CanvasEntityIdentifier;
+ } & T;
+
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>;
@@ -858,6 +871,8 @@ export function isDrawableEntity(
return isDrawableEntityType(entity.type);
}
-export const getEntityIdentifier = (entity: CanvasEntityState): CanvasEntityIdentifier => {
+export const getEntityIdentifier = (
+ entity: Extract
+): CanvasEntityIdentifier => {
return { id: entity.id, type: entity.type };
};
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
index 30f59aed1f..c42d92736c 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
+++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx
@@ -1,7 +1,7 @@
import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {
diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
index c5491f8bc3..036c25b9c2 100644
--- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts
@@ -1,5 +1,5 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
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 9ef21177da..42ecf584d3 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx
@@ -13,7 +13,7 @@ import {
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';
diff --git a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx
index b5a2b1bba9..e96e38797d 100644
--- a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx
+++ b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx
@@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { LoRACard } from 'features/lora/components/LoRACard';
import { memo } from 'react';
diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
index 8296031418..9d0a8164ae 100644
--- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
+++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx
@@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
-import { loraAdded, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { loraAdded } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLoRAModels } from 'services/api/hooks/modelsByType';
diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
index f0fd3b3afa..2afb05f135 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx
@@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx
index fe44e55025..0d6c216357 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx
@@ -2,7 +2,7 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import type { PropsWithChildren } from 'react';
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
index b58b5540fe..3a67ffa41a 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx
@@ -3,7 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier';
import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
index 0fd4cdcdb0..2d69401369 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx
@@ -3,7 +3,7 @@ import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@inv
import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { LoRAList } from 'features/lora/components/LoRAList';
import LoRASelect from 'features/lora/components/LoRASelect';
import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale';
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 92c480aecb..51dd3ed578 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx
@@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Expander, Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx
index 76f7ebe684..c854c4960e 100644
--- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx
+++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx
@@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
-import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
+import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale';
import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect';
import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore';