refactor(ui): canvas v2 (wip)

merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell)
This commit is contained in:
psychedelicious 2024-06-15 20:22:07 +10:00
parent b4daf29bd8
commit 4071e96245
79 changed files with 1313 additions and 1473 deletions

View File

@ -1,7 +1,7 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { resetCanvas } from 'features/canvas/store/canvasSlice';
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { allLayersDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { imagesApi } from 'services/api/endpoints/images';

View File

@ -10,7 +10,7 @@ import {
controlAdapterProcessorConfigChanged,
controlAdapterProcessorPendingBatchIdChanged,
controlAdapterRecalled,
} from 'features/controlLayers/store/controlLayersSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import { isControlAdapterLayer } from 'features/controlLayers/store/types';
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
import { toast } from 'features/toast/toast';

View File

@ -8,7 +8,7 @@ import {
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { layerDeleted } from 'features/controlLayers/store/controlLayersSlice';
import { layerDeleted } from 'features/controlLayers/store/canvasV2Slice';
import {
isControlAdapterLayer,
isInitialImageLayer,

View File

@ -13,7 +13,7 @@ import {
layerImageAdded,
ipAdapterImageChanged,
regionalGuidanceIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import {

View File

@ -1,19 +1,8 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import {
controlAdapterImageChanged,
controlAdapterIsEnabledChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import {
controlAdapterImageChanged,
iiLayerImageChanged,
ipAdapterImageChanged,
regionalGuidanceIPAdapterImageChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { caImageChanged, ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
@ -81,15 +70,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setAsCanvasInitialImage'),
});
return;
}
if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') {
dispatch(upscaleInitialImageChanged(imageDTO));
toast({
@ -99,57 +79,27 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') {
if (postUploadAction?.type === 'SET_CA_IMAGE') {
const { id } = postUploadAction;
dispatch(
controlAdapterIsEnabledChanged({
id,
isEnabled: true,
})
);
dispatch(
controlAdapterImageChanged({
id,
controlImage: imageDTO.image_name,
})
);
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
return;
}
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(controlAdapterImageChanged({ layerId, imageDTO }));
dispatch(caImageChanged({ id, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(ipAdapterImageChanged({ layerId, imageDTO }));
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;
dispatch(ipaImageChanged({ id, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
const { layerId, ipAdapterId } = postUploadAction;
dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),
});
}
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
const { layerId } = postUploadAction;
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
const { id, ipAdapterId } = postUploadAction;
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
toast({
...DEFAULT_UPLOADED_TOAST,
description: t('toast.setControlImage'),

View File

@ -6,7 +6,7 @@ import {
controlAdapterModelCleared,
selectControlAdapterAll,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { loraRemoved } from 'features/lora/store/loraSlice';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';

View File

@ -1,6 +1,6 @@
import { isAnyOf } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import {
combinatorialToggled,
isErrorChanged,

View File

@ -1,5 +1,5 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { setDefaultSettings } from 'features/parameters/store/actions';
import {
setCfgRescaleMultiplier,

View File

@ -5,17 +5,7 @@ import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { JSONObject } from 'common/types';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import {
controlAdaptersV2PersistConfig,
controlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { ipAdaptersPersistConfig, ipAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice';
import { layersPersistConfig, layersSlice } from 'features/controlLayers/store/layersSlice';
import {
regionalGuidancePersistConfig,
regionalGuidanceSlice,
} from 'features/controlLayers/store/regionalGuidanceSlice';
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
@ -70,10 +60,6 @@ const allReducers = {
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[upscaleSlice.name]: upscaleSlice.reducer,
[stylePresetSlice.name]: stylePresetSlice.reducer,
[layersSlice.name]: layersSlice.reducer,
[controlAdaptersV2Slice.name]: controlAdaptersV2Slice.reducer,
[ipAdaptersSlice.name]: ipAdaptersSlice.reducer,
[regionalGuidanceSlice.name]: regionalGuidanceSlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@ -118,10 +104,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
[upscalePersistConfig.name]: upscalePersistConfig,
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[layersPersistConfig.name]: layersPersistConfig,
[controlAdaptersV2PersistConfig.name]: controlAdaptersV2PersistConfig,
[ipAdaptersPersistConfig.name]: ipAdaptersPersistConfig,
[regionalGuidancePersistConfig.name]: regionalGuidancePersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice';
import { selectLayersSlice } from 'features/controlLayers/store/layersSlice';
import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice';

View File

@ -12,7 +12,7 @@ import {
controlAdapterImageChanged,
selectControlAdaptersSlice,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';

View File

@ -1,8 +1,7 @@
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useAddCALayer, useAddIPALayer } from 'features/controlLayers/hooks/addLayerHooks';
import { layerAdded } from 'features/controlLayers/store/layersSlice';
import { rgAdded } from 'features/controlLayers/store/regionalGuidanceSlice';
import { layerAdded, rgAdded } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';

View File

@ -5,8 +5,8 @@ import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerH
import {
rgNegativePromptChanged,
rgPositivePromptChanged,
selectRegionalGuidanceSlice,
} from 'features/controlLayers/store/regionalGuidanceSlice';
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
@ -21,8 +21,8 @@ export const AddPromptButtons = ({ id }: AddPromptButtonProps) => {
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectRegionalGuidanceSlice, (regionalGuidanceState) => {
const rg = regionalGuidanceState.regions.find((rg) => rg.id === id);
createMemoizedSelector(selectCanvasV2Slice, (caState) => {
const rg = caState.regions.find((rg) => rg.id === id);
return {
canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: rg?.negativePrompt === null,

View File

@ -10,7 +10,7 @@ import {
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { brushWidthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { brushWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CAHeader } from 'features/controlLayers/components/ControlAdapter/CAEntityHeader';
import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
type Props = {

View File

@ -8,9 +8,9 @@ import {
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
selectCAOrThrow,
selectControlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -25,20 +25,17 @@ type Props = {
id: string;
};
const selectValidActions = createAppSelector(
[selectControlAdaptersV2Slice, (caState, id: string) => id],
(caState, id) => {
const ca = selectCAOrThrow(caState, id);
const caIndex = caState.controlAdapters.indexOf(ca);
const caCount = caState.controlAdapters.length;
return {
canMoveForward: caIndex < caCount - 1,
canMoveBackward: caIndex > 0,
canMoveToFront: caIndex < caCount - 1,
canMoveToBack: caIndex > 0,
};
}
);
const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => {
const ca = selectCAOrThrow(canvasV2, id);
const caIndex = canvasV2.controlAdapters.indexOf(ca);
const caCount = canvasV2.controlAdapters.length;
return {
canMoveForward: caIndex < caCount - 1,
canMoveBackward: caIndex > 0,
canMoveToFront: caIndex < caCount - 1,
canMoveToBack: caIndex > 0,
};
});
export const CAActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();

View File

@ -6,7 +6,8 @@ import { CanvasEntityHeader } from 'features/controlLayers/components/common/Can
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { CAActionsMenu } from 'features/controlLayers/components/ControlAdapter/CAActionsMenu';
import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter';
import { caDeleted, caIsEnabledToggled, selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersSlice';
import { caDeleted, caIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -18,7 +19,7 @@ type Props = {
export const CAHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled);
const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(caIsEnabledToggled({ id }));
}, [dispatch, id]);

View File

@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ControlAdapterData } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';

View File

@ -15,7 +15,7 @@ import {
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useCALayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/controlAdaptersSlice';
import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -16,8 +16,8 @@ import {
caProcessedImageChanged,
caProcessorConfigChanged,
caWeightChanged,
selectCAOrThrow,
} from 'features/controlLayers/store/controlAdaptersSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import type { ControlModeV2, ProcessorConfig } from 'features/controlLayers/store/types';
import type { CAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@ -40,7 +40,7 @@ export const CASettings = memo(({ id }: Props) => {
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id));
const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id));
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {

View File

@ -11,39 +11,22 @@ import { IPA } from 'features/controlLayers/components/IPAdapter/IPA';
import { Layer } from 'features/controlLayers/components/Layer/Layer';
import { RG } from 'features/controlLayers/components/RegionalGuidance/RG';
import { mapId } from 'features/controlLayers/konva/util';
import { selectControlAdaptersV2Slice } from 'features/controlLayers/store/controlAdaptersSlice';
import { selectIPAdaptersSlice } from 'features/controlLayers/store/ipAdaptersSlice';
import { selectLayersSlice } from 'features/controlLayers/store/layersSlice';
import { selectRegionalGuidanceSlice } from 'features/controlLayers/store/regionalGuidanceSlice';
import { memo, useMemo } from 'react';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectRGIds = createMemoizedSelector(selectRegionalGuidanceSlice, (rgState) => {
return rgState.regions.map(mapId).reverse();
});
const selectCAIds = createMemoizedSelector(selectControlAdaptersV2Slice, (caState) => {
return caState.controlAdapters.map(mapId).reverse();
});
const selectIPAIds = createMemoizedSelector(selectIPAdaptersSlice, (ipaState) => {
return ipaState.ipAdapters.map(mapId).reverse();
});
const selectLayerIds = createMemoizedSelector(selectLayersSlice, (layersState) => {
return layersState.layers.map(mapId).reverse();
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2State) => {
const rgIds = canvasV2State.regions.map(mapId).reverse();
const caIds = canvasV2State.controlAdapters.map(mapId).reverse();
const ipaIds = canvasV2State.ipAdapters.map(mapId).reverse();
const layerIds = canvasV2State.layers.map(mapId).reverse();
const entityCount = rgIds.length + caIds.length + ipaIds.length + layerIds.length;
return { rgIds, caIds, ipaIds, layerIds, entityCount };
});
export const ControlLayersPanelContent = memo(() => {
const { t } = useTranslation();
const rgIds = useAppSelector(selectRGIds);
const caIds = useAppSelector(selectCAIds);
const ipaIds = useAppSelector(selectIPAIds);
const layerIds = useAppSelector(selectLayerIds);
const entityCount = useMemo(
() => rgIds.length + caIds.length + ipaIds.length + layerIds.length,
[rgIds.length, caIds.length, ipaIds.length, layerIds.length]
);
const { rgIds, caIds, ipaIds, layerIds, entityCount } = useAppSelector(selectEntityIds);
return (
<Flex flexDir="column" gap={2} w="full" h="full">

View File

@ -1,10 +1,6 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caAllDeleted } from 'features/controlLayers/store/controlAdaptersSlice';
import { ipaAllDeleted } from 'features/controlLayers/store/ipAdaptersSlice';
import { layerAllDeleted } from 'features/controlLayers/store/layersSlice';
import { rgAllDeleted } from 'features/controlLayers/store/regionalGuidanceSlice';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import { allEntitiesDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
@ -12,12 +8,16 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
export const DeleteAllLayersButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityCount = useAppSelector(selectEntityCount);
const entityCount = useAppSelector((s) => {
return (
s.canvasV2.regions.length +
s.canvasV2.controlAdapters.length +
s.canvasV2.ipAdapters.length +
s.canvasV2.layers.length
);
});
const onClick = useCallback(() => {
dispatch(caAllDeleted());
dispatch(rgAllDeleted());
dispatch(ipaAllDeleted());
dispatch(layerAllDeleted());
dispatch(allEntitiesDeleted());
}, [dispatch]);
return (

View File

@ -10,7 +10,7 @@ import {
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { eraserWidthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { eraserWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -2,7 +2,7 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@inv
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIColorPicker from 'common/components/IAIColorPicker';
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { fillChanged } from 'features/controlLayers/store/controlLayersSlice';
import { fillChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { RgbaColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -1,7 +1,7 @@
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { $stageAttrs } from 'features/controlLayers/store/controlLayersSlice';
import { $stageAttrs } from 'features/controlLayers/store/canvasV2Slice';
import { round } from 'lodash-es';
import { memo } from 'react';

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { IPAHeader } from 'features/controlLayers/components/IPAdapter/IPAHeader';
import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
type Props = {

View File

@ -4,7 +4,8 @@ import { CanvasEntityDeleteButton } from 'features/controlLayers/components/comm
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { ipaDeleted, ipaIsEnabledToggled, selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersSlice';
import { ipaDeleted, ipaIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -16,7 +17,7 @@ type Props = {
export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled);
const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(ipaIsEnabledToggled({ id }));
}, [dispatch, id]);

View File

@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';

View File

@ -11,8 +11,8 @@ import {
ipaMethodChanged,
ipaModelChanged,
ipaWeightChanged,
selectIPAOrThrow,
} from 'features/controlLayers/store/ipAdaptersSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import { selectIPAOrThrow } from 'features/controlLayers/store/ipAdaptersReducers';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { IPAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@ -27,7 +27,7 @@ type Props = {
export const IPASettings = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id));
const ipAdapter = useAppSelector((s) => selectIPAOrThrow(s.canvasV2, id));
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader';
import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
type Props = {

View File

@ -8,9 +8,9 @@ import {
layerMovedForwardOne,
layerMovedToBack,
layerMovedToFront,
selectLayerOrThrow,
selectLayersSlice,
} from 'features/controlLayers/store/layersSlice';
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -25,20 +25,17 @@ type Props = {
id: string;
};
const selectValidActions = createAppSelector(
[selectLayersSlice, (layersState, id: string) => id],
(layersState, id) => {
const layer = selectLayerOrThrow(layersState, id);
const layerIndex = layersState.layers.indexOf(layer);
const layerCount = layersState.layers.length;
return {
canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0,
canMoveToFront: layerIndex < layerCount - 1,
canMoveToBack: layerIndex > 0,
};
}
);
const selectValidActions = createAppSelector([selectCanvasV2Slice, (canvasV2, id: string) => id], (canvasV2, id) => {
const layer = selectLayerOrThrow(canvasV2, id);
const layerIndex = canvasV2.layers.indexOf(layer);
const layerCount = canvasV2.layers.length;
return {
canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0,
canMoveToFront: layerIndex < layerCount - 1,
canMoveToBack: layerIndex > 0,
};
});
export const LayerActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();

View File

@ -5,7 +5,8 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu';
import { layerDeleted, layerIsEnabledToggled, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice';
import { layerDeleted, layerIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -19,7 +20,7 @@ type Props = {
export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.layers, id).isEnabled);
const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(layerIsEnabledToggled({ id }));
}, [dispatch, id]);

View File

@ -13,7 +13,8 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice';
import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
@ -28,7 +29,7 @@ const formatPct = (v: number | string) => `${v} %`;
export const LayerOpacity = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100));
const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, id).opacity * 100));
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(layerOpacityChanged({ id, opacity: v / 100 }));

View File

@ -1,6 +1,6 @@
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgGlobalOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -10,7 +10,7 @@ const formatPct = (v: number | string) => `${v} %`;
export const RGGlobalOpacity = memo(() => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100));
const opacity = useAppSelector((s) => Math.round(s.canvasV2.maskFillOpacity * 100));
const onChange = useCallback(
(v: number) => {
dispatch(rgGlobalOpacityChanged({ opacity: v / 100 }));

View File

@ -4,8 +4,8 @@ import { rgbColorToString } from 'features/canvas/util/colorToString';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { RGHeader } from 'features/controlLayers/components/RegionalGuidance/RGHeader';
import { RGSettings } from 'features/controlLayers/components/RegionalGuidance/RGSettings';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { entitySelected } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react';
type Props = {
@ -14,7 +14,7 @@ type Props = {
export const RG = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.regionalGuidance, id).fill));
const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.canvasV2, id).fill));
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {

View File

@ -12,9 +12,9 @@ import {
rgNegativePromptChanged,
rgPositivePromptChanged,
rgReset,
selectRegionalGuidanceSlice,
selectRGOrThrow,
} from 'features/controlLayers/store/regionalGuidanceSlice';
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -32,11 +32,11 @@ type Props = {
};
const selectActionsValidity = createMemoizedAppSelector(
[selectRegionalGuidanceSlice, (rgState, id: string) => id],
(rgState, id) => {
const rg = selectRGOrThrow(rgState, id);
const rgIndex = rgState.regions.indexOf(rg);
const rgCount = rgState.regions.length;
[selectCanvasV2Slice, (canvasV2, id: string) => id],
(canvasV2, id) => {
const rg = selectRGOrThrow(canvasV2, id);
const rgIndex = canvasV2.regions.indexOf(rg);
const rgCount = canvasV2.regions.length;
return {
isMoveForwardOneDisabled: rgIndex < rgCount - 1,
isMoveBackardOneDisabled: rgIndex > 0,

View File

@ -5,7 +5,8 @@ import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/com
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu';
import { rgDeleted, rgIsEnabledToggled, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgDeleted, rgIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,8 +21,8 @@ type Props = {
export const RGHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).isEnabled);
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative);
const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled);
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative);
const onToggleIsEnabled = useCallback(() => {
dispatch(rgIsEnabledToggled({ id }));
}, [dispatch, id]);

View File

@ -13,8 +13,8 @@ import {
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterWeightChanged,
selectRGOrThrow,
} from 'features/controlLayers/store/regionalGuidanceSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@ -34,7 +34,7 @@ export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: P
dispatch(rgIPAdapterDeleted({ id, ipAdapterId }));
}, [dispatch, ipAdapterId, id]);
const ipAdapter = useAppSelector((s) => {
const ipa = selectRGOrThrow(s.regionalGuidance, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId);
const ipa = selectRGOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId);
assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`);
return ipa;
});

View File

@ -1,7 +1,7 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { RGIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings';
import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo } from 'react';
type Props = {
@ -9,7 +9,7 @@ type Props = {
};
export const RGIPAdapters = memo(({ id }: Props) => {
const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.map(({ id }) => id));
const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.map(({ id }) => id));
if (ipAdapterIds.length === 0) {
return null;

View File

@ -3,7 +3,8 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import RgbColorPicker from 'common/components/RgbColorPicker';
import { stopPropagation } from 'common/util/stopPropagation';
import { rgbColorToString } from 'features/canvas/util/colorToString';
import { rgFillChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
@ -15,7 +16,7 @@ type Props = {
export const RGMaskFillColorPicker = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).fill);
const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).fill);
const onChange = useCallback(
(fill: RgbColor) => {
dispatch(rgFillChanged({ id, fill }));

View File

@ -1,7 +1,8 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -14,7 +15,7 @@ type Props = {
};
export const RGNegativePrompt = memo(({ id }: Props) => {
const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? '');
const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();

View File

@ -1,7 +1,8 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -14,7 +15,7 @@ type Props = {
};
export const RGPositivePrompt = memo(({ id }: Props) => {
const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? '');
const prompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();

View File

@ -1,7 +1,7 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import { memo } from 'react';
import { RGIPAdapters } from './RGIPAdapters';
@ -13,9 +13,9 @@ type Props = {
};
export const RGSettings = memo(({ id }: Props) => {
const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt !== null);
const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt !== null);
const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.length > 0);
const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null);
const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null);
const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0);
return (
<CanvasEntitySettings>

View File

@ -12,7 +12,8 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { rgAutoNegativeChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -25,7 +26,7 @@ type Props = {
export const RGSettingsPopover = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative);
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' }));

View File

@ -1,14 +1,11 @@
import { $alt, $ctrl, $meta, $shift, Box, Flex, Heading } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
import { setStageEventHandlers } from 'features/controlLayers/konva/events';
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/konva/renderers/layers';
import { caBboxChanged, caTranslated } from 'features/controlLayers/store/controlAdaptersSlice';
import {
$bbox,
$currentFill,
@ -23,29 +20,26 @@ import {
$toolState,
bboxChanged,
brushWidthChanged,
caBboxChanged,
caTranslated,
eraserWidthChanged,
selectCanvasV2Slice,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import {
layerBboxChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerLinePointAdded,
layerRectAdded,
layerTranslated,
selectLayersSlice,
} from 'features/controlLayers/store/layersSlice';
import {
rgBboxChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgLinePointAdded,
rgRectAdded,
rgTranslated,
selectRegionalGuidanceSlice,
} from 'features/controlLayers/store/regionalGuidanceSlice';
selectCanvasV2Slice,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectEntityCount } from 'features/controlLayers/store/selectors';
import type {
BboxChangedArg,
BrushLineAddedArg,
@ -69,62 +63,42 @@ Konva.showWarnings = false;
const log = logger('controlLayers');
const selectBrushFill = createSelector(
selectCanvasV2Slice,
selectLayersSlice,
selectRegionalGuidanceSlice,
(canvas, layers, regionalGuidance) => {
const rg = regionalGuidance.regions.find((i) => i.id === canvas.selectedEntityIdentifier?.id);
if (rg) {
return rgbaColorToString({ ...rg.fill, a: regionalGuidance.opacity });
}
return rgbaColorToString(canvas.tool.fill);
}
);
const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null, asPreview: boolean) => {
const dispatch = useAppDispatch();
const canvasV2State = useAppSelector(selectCanvasV2Slice);
const layersState = useAppSelector((s) => s.layers);
const controlAdaptersState = useAppSelector((s) => s.controlAdaptersV2);
const ipAdaptersState = useAppSelector((s) => s.ipAdapters);
const regionalGuidanceState = useAppSelector((s) => s.regionalGuidance);
const lastCursorPos = useStore($lastCursorPos);
const lastMouseDownPos = useStore($lastMouseDownPos);
const isMouseDown = useStore($isMouseDown);
const isDrawing = useStore($isDrawing);
const brushColor = useAppSelector(selectBrushFill);
const selectedEntity = useMemo(() => {
const identifier = canvasV2State.selectedEntityIdentifier;
if (!identifier) {
return null;
} else if (identifier.type === 'layer') {
return layersState.layers.find((i) => i.id === identifier.id) ?? null;
return canvasV2State.layers.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'control_adapter') {
return controlAdaptersState.controlAdapters.find((i) => i.id === identifier.id) ?? null;
return canvasV2State.controlAdapters.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'ip_adapter') {
return ipAdaptersState.ipAdapters.find((i) => i.id === identifier.id) ?? null;
return canvasV2State.ipAdapters.find((i) => i.id === identifier.id) ?? null;
} else if (identifier.type === 'regional_guidance') {
return regionalGuidanceState.regions.find((i) => i.id === identifier.id) ?? null;
return canvasV2State.regions.find((i) => i.id === identifier.id) ?? null;
} else {
return null;
}
}, [
canvasV2State.controlAdapters,
canvasV2State.ipAdapters,
canvasV2State.layers,
canvasV2State.regions,
canvasV2State.selectedEntityIdentifier,
controlAdaptersState.controlAdapters,
ipAdaptersState.ipAdapters,
layersState.layers,
regionalGuidanceState.regions,
]);
const currentFill = useMemo(() => {
if (selectedEntity && selectedEntity.type === 'regional_guidance') {
return { ...selectedEntity.fill, a: regionalGuidanceState.opacity };
return { ...selectedEntity.fill, a: canvasV2State.maskFillOpacity };
}
return canvasV2State.tool.fill;
}, [canvasV2State.tool.fill, regionalGuidanceState.opacity, selectedEntity]);
}, [canvasV2State.maskFillOpacity, canvasV2State.tool.fill, selectedEntity]);
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
const dpr = useDevicePixelRatio({ round: false });
@ -341,7 +315,6 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
);
}, [
asPreview,
brushColor,
canvasV2State.tool,
currentFill,
isDrawing,
@ -376,10 +349,10 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
log.trace('Rendering layers');
renderers.renderLayers(
stage,
layersState.layers,
controlAdaptersState.controlAdapters,
regionalGuidanceState.regions,
regionalGuidanceState.opacity,
canvasV2State.layers,
canvasV2State.controlAdapters,
canvasV2State.regions,
canvasV2State.maskFillOpacity,
canvasV2State.tool.selected,
selectedEntity,
getImageDTO,
@ -388,13 +361,13 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
}, [
stage,
renderers,
layersState.layers,
controlAdaptersState.controlAdapters,
regionalGuidanceState.regions,
regionalGuidanceState.opacity,
onPosChanged,
canvasV2State.tool.selected,
selectedEntity,
canvasV2State.layers,
canvasV2State.controlAdapters,
canvasV2State.regions,
canvasV2State.maskFillOpacity,
]);
// useLayoutEffect(() => {
@ -450,7 +423,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
backgroundRepeat="repeat"
opacity={0.2}
/>
{!asPreview && <NoLayersFallback />}
{!asPreview && <NoEntitiesFallback />}
<Flex
position="absolute"
top={0}
@ -474,10 +447,11 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
StageComponent.displayName = 'StageComponent';
const NoLayersFallback = () => {
const NoEntitiesFallback = () => {
const { t } = useTranslation();
const layerCount = useAppSelector((s) => s.layers.layers.length);
if (layerCount) {
const entityCount = useAppSelector(selectEntityCount);
if (entityCount) {
return null;
}

View File

@ -1,11 +1,16 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caDeleted } from 'features/controlLayers/store/controlAdaptersSlice';
import { selectCanvasV2Slice, toolChanged } from 'features/controlLayers/store/controlLayersSlice';
import { ipaDeleted } from 'features/controlLayers/store/ipAdaptersSlice';
import { layerDeleted, layerReset } from 'features/controlLayers/store/layersSlice';
import { rgDeleted, rgReset } from 'features/controlLayers/store/regionalGuidanceSlice';
import {
caDeleted,
ipaDeleted,
layerDeleted,
layerReset,
rgDeleted,
rgReset,
selectCanvasV2Slice,
toolChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';

View File

@ -1,7 +1,7 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { redo, undo } from 'features/controlLayers/store/controlLayersSlice';
import { redo, undo } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';

View File

@ -1,7 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { caAdded } from 'features/controlLayers/store/controlAdaptersSlice';
import { ipaAdded } from 'features/controlLayers/store/ipAdaptersSlice';
import { rgIPAdapterAdded } from 'features/controlLayers/store/regionalGuidanceSlice';
import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice';
import {
buildControlNet,
buildIPAdapter,

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { isControlAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { assert } from 'tsafe';

View File

@ -3,6 +3,10 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
import { layersReducers } from 'features/controlLayers/store/layersReducers';
import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
@ -47,12 +51,21 @@ const initialState: CanvasV2State = {
width: 512,
height: 512,
},
controlAdapters: [],
ipAdapters: [],
regions: [],
layers: [],
maskFillOpacity: 0.3,
};
export const canvasV2Slice = createSlice({
name: 'canvasV2',
initialState,
reducers: {
...layersReducers,
...ipAdaptersReducers,
...controlAdaptersReducers,
...regionsReducers,
positivePromptChanged: (state, action: PayloadAction<string>) => {
state.prompts.positivePrompt = action.payload;
},
@ -110,9 +123,18 @@ export const canvasV2Slice = createSlice({
toolBufferChanged: (state, action: PayloadAction<Tool | null>) => {
state.tool.selectedBuffer = action.payload;
},
maskFillOpacityChanged: (state, action: PayloadAction<number>) => {
state.maskFillOpacity = action.payload;
},
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
},
allEntitiesDeleted: (state) => {
state.regions = [];
state.layers = [];
state.ipAdapters = [];
state.controlAdapters = [];
},
},
extraReducers(builder) {
builder.addCase(modelChanged, (state, action) => {
@ -148,7 +170,88 @@ export const {
invertScrollChanged,
toolChanged,
toolBufferChanged,
maskFillOpacityChanged,
entitySelected,
allEntitiesDeleted,
// layers
layerAdded,
layerDeleted,
layerReset,
layerMovedForwardOne,
layerMovedToFront,
layerMovedBackwardOne,
layerMovedToBack,
layerIsEnabledToggled,
layerOpacityChanged,
layerTranslated,
layerBboxChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerLinePointAdded,
layerRectAdded,
layerImageAdded,
// IP Adapters
ipaAdded,
ipaRecalled,
ipaIsEnabledToggled,
ipaDeleted,
ipaImageChanged,
ipaMethodChanged,
ipaModelChanged,
ipaCLIPVisionModelChanged,
ipaWeightChanged,
ipaBeginEndStepPctChanged,
// Control Adapters
caAdded,
caBboxChanged,
caDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
caOpacityChanged,
caTranslated,
caRecalled,
caImageChanged,
caProcessedImageChanged,
caModelChanged,
caControlModeChanged,
caProcessorConfigChanged,
caFilterChanged,
caProcessorPendingBatchIdChanged,
caWeightChanged,
caBeginEndStepPctChanged,
// Regions
rgAdded,
rgRecalled,
rgReset,
rgIsEnabledToggled,
rgTranslated,
rgBboxChanged,
rgDeleted,
rgGlobalOpacityChanged,
rgMovedForwardOne,
rgMovedToFront,
rgMovedBackwardOne,
rgMovedToBack,
rgPositivePromptChanged,
rgNegativePromptChanged,
rgFillChanged,
rgMaskImageUploaded,
rgAutoNegativeChanged,
rgIPAdapterAdded,
rgIPAdapterDeleted,
rgIPAdapterImageChanged,
rgIPAdapterWeightChanged,
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgLinePointAdded,
rgRectAdded,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;

View File

@ -0,0 +1,238 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
CanvasV2State,
ControlAdapterConfig,
ControlAdapterData,
ControlModeV2,
Filter,
ProcessorConfig,
} from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types';
export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id);
export const selectCAOrThrow = (state: CanvasV2State, id: string) => {
const ca = selectCA(state, id);
assert(ca, `Control Adapter with id ${id} not found`);
return ca;
};
export const controlAdaptersReducers = {
caAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => {
const { id, config } = action.payload;
state.controlAdapters.push({
id,
type: 'control_adapter',
x: 0,
y: 0,
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
opacity: 1,
filter: 'LightnessToAlphaFilter',
processorPendingBatchId: null,
...config,
});
},
prepare: (config: ControlAdapterConfig) => ({
payload: { id: uuidv4(), config },
}),
},
caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => {
state.controlAdapters.push(action.payload.data);
},
caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.isEnabled = !ca.isEnabled;
},
caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.x = x;
ca.y = y;
},
caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = bbox;
ca.bboxNeedsUpdate = false;
},
caDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id);
},
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.opacity = opacity;
},
caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToEnd(state.controlAdapters, ca);
},
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToEnd(state.controlAdapters, ca);
},
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToStart(state.controlAdapters, ca);
},
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToStart(state.controlAdapters, ca);
},
caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
if (imageDTO) {
const newImage = imageDTOToImageWithDims(imageDTO);
if (isEqual(newImage, ca.image)) {
return;
}
ca.image = newImage;
ca.processedImage = null;
} else {
ca.image = null;
ca.processedImage = null;
}
},
caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
caModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
if (!modelConfig) {
ca.model = null;
return;
}
ca.model = zModelIdentifierField.parse(modelConfig);
// We may need to convert the CA to match the model
if (!ca.controlMode && ca.model.type === 'controlnet') {
ca.controlMode = 'balanced';
} else if (ca.controlMode && ca.model.type === 't2i_adapter') {
ca.controlMode = null;
}
const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== ca.processorConfig?.type) {
// The processor has changed. For example, the previous model was a Canny model and the new model is a Depth
// model. We need to use the new processor.
ca.processedImage = null;
ca.processorConfig = candidateProcessorConfig;
}
},
caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.controlMode = controlMode;
},
caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>) => {
const { id, processorConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorConfig = processorConfig;
if (!processorConfig) {
ca.processedImage = null;
}
},
caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => {
const { id, filter } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.filter = filter;
},
caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => {
const { id, batchId } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorPendingBatchId = batchId;
},
caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.weight = weight;
},
caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.beginEndStepPct = beginEndStepPct;
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,291 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type { ControlAdapterConfig, ControlAdapterData, ControlModeV2, Filter, ProcessorConfig } from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageWithDims } from './types';
type ControlAdaptersV2State = {
_version: 1;
controlAdapters: ControlAdapterData[];
};
const initialState: ControlAdaptersV2State = {
_version: 1,
controlAdapters: [],
};
export const selectCA = (state: ControlAdaptersV2State, id: string) => state.controlAdapters.find((ca) => ca.id === id);
export const selectCAOrThrow = (state: ControlAdaptersV2State, id: string) => {
const ca = selectCA(state, id);
assert(ca, `Control Adapter with id ${id} not found`);
return ca;
};
export const controlAdaptersV2Slice = createSlice({
name: 'controlAdaptersV2',
initialState,
reducers: {
caAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: ControlAdapterConfig }>) => {
const { id, config } = action.payload;
state.controlAdapters.push({
id,
type: 'control_adapter',
x: 0,
y: 0,
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
opacity: 1,
filter: 'LightnessToAlphaFilter',
processorPendingBatchId: null,
...config,
});
},
prepare: (config: ControlAdapterConfig) => ({
payload: { id: uuidv4(), config },
}),
},
caRecalled: (state, action: PayloadAction<{ data: ControlAdapterData }>) => {
state.controlAdapters.push(action.payload.data);
},
caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.isEnabled = !ca.isEnabled;
},
caTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.x = x;
ca.y = y;
},
caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = bbox;
ca.bboxNeedsUpdate = false;
},
caDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.controlAdapters = state.controlAdapters.filter((ca) => ca.id !== id);
},
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.opacity = opacity;
},
caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToEnd(state.controlAdapters, ca);
},
caMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToEnd(state.controlAdapters, ca);
},
caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveOneToStart(state.controlAdapters, ca);
},
caMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
moveToStart(state.controlAdapters, ca);
},
caImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
if (imageDTO) {
const newImage = imageDTOToImageWithDims(imageDTO);
if (isEqual(newImage, ca.image)) {
return;
}
ca.image = newImage;
ca.processedImage = null;
} else {
ca.image = null;
ca.processedImage = null;
}
},
caProcessedImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
ca.processedImage = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
caModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
if (!modelConfig) {
ca.model = null;
return;
}
ca.model = zModelIdentifierField.parse(modelConfig);
// We may need to convert the CA to match the model
if (!ca.controlMode && ca.model.type === 'controlnet') {
ca.controlMode = 'balanced';
} else if (ca.controlMode && ca.model.type === 't2i_adapter') {
ca.controlMode = null;
}
const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== ca.processorConfig?.type) {
// The processor has changed. For example, the previous model was a Canny model and the new model is a Depth
// model. We need to use the new processor.
ca.processedImage = null;
ca.processorConfig = candidateProcessorConfig;
}
},
caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.controlMode = controlMode;
},
caProcessorConfigChanged: (
state,
action: PayloadAction<{ id: string; processorConfig: ProcessorConfig | null }>
) => {
const { id, processorConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorConfig = processorConfig;
if (!processorConfig) {
ca.processedImage = null;
}
},
caFilterChanged: (state, action: PayloadAction<{ id: string; filter: Filter }>) => {
const { id, filter } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.filter = filter;
},
caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => {
const { id, batchId } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorPendingBatchId = batchId;
},
caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.weight = weight;
},
caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.beginEndStepPct = beginEndStepPct;
},
caAllDeleted: (state) => {
state.controlAdapters = [];
},
},
});
export const {
caAdded,
caBboxChanged,
caDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
caOpacityChanged,
caTranslated,
caRecalled,
caImageChanged,
caProcessedImageChanged,
caModelChanged,
caControlModeChanged,
caProcessorConfigChanged,
caFilterChanged,
caProcessorPendingBatchIdChanged,
caWeightChanged,
caBeginEndStepPctChanged,
caAllDeleted,
} = controlAdaptersV2Slice.actions;
export const selectControlAdaptersV2Slice = (state: RootState) => state.controlAdaptersV2;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const controlAdaptersV2PersistConfig: PersistConfig<ControlAdaptersV2State> = {
name: controlAdaptersV2Slice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,102 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type { CanvasV2State, CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types';
import { imageDTOToImageWithDims } from './types';
export const selectIPA = (state: CanvasV2State, id: string) => state.ipAdapters.find((ipa) => ipa.id === id);
export const selectIPAOrThrow = (state: CanvasV2State, id: string) => {
const ipa = selectIPA(state, id);
assert(ipa, `IP Adapter with id ${id} not found`);
return ipa;
};
export const ipAdaptersReducers = {
ipaAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => {
const { id, config } = action.payload;
const layer: IPAdapterData = {
id,
type: 'ip_adapter',
isEnabled: true,
...config,
};
state.ipAdapters.push(layer);
},
prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }),
},
ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => {
state.ipAdapters.push(action.payload.data);
},
ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ipa = selectIPA(state, id);
if (ipa) {
ipa.isEnabled = !ipa.isEnabled;
}
},
ipaDeleted: (state, action: PayloadAction<{ id: string }>) => {
state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id);
},
ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => {
const { id, method } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.method = method;
},
ipaModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => {
const { id, clipVisionModel } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.weight = weight;
},
ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,149 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type { CLIPVisionModelV2, IPAdapterConfig, IPAdapterData, IPMethodV2 } from './types';
import { imageDTOToImageWithDims } from './types';
type IPAdaptersState = {
_version: 1;
ipAdapters: IPAdapterData[];
};
const initialState: IPAdaptersState = {
_version: 1,
ipAdapters: [],
};
export const selectIPA = (state: IPAdaptersState, id: string) => state.ipAdapters.find((ipa) => ipa.id === id);
export const selectIPAOrThrow = (state: IPAdaptersState, id: string) => {
const ipa = selectIPA(state, id);
assert(ipa, `IP Adapter with id ${id} not found`);
return ipa;
};
export const ipAdaptersSlice = createSlice({
name: 'ipAdapters',
initialState,
reducers: {
ipaAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: IPAdapterConfig }>) => {
const { id, config } = action.payload;
const layer: IPAdapterData = {
id,
type: 'ip_adapter',
isEnabled: true,
...config,
};
state.ipAdapters.push(layer);
},
prepare: (config: IPAdapterConfig) => ({ payload: { id: uuidv4(), config } }),
},
ipaRecalled: (state, action: PayloadAction<{ data: IPAdapterData }>) => {
state.ipAdapters.push(action.payload.data);
},
ipaIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const ipa = selectIPA(state, id);
if (ipa) {
ipa.isEnabled = !ipa.isEnabled;
}
},
ipaDeleted: (state, action: PayloadAction<{ id: string }>) => {
state.ipAdapters = state.ipAdapters.filter((ipa) => ipa.id !== action.payload.id);
},
ipaImageChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => {
const { id, method } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.method = method;
},
ipaModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => {
const { id, clipVisionModel } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.weight = weight;
},
ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ipa = selectIPA(state, id);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
ipaAllDeleted: (state) => {
state.ipAdapters = [];
},
},
});
export const {
ipaAdded,
ipaRecalled,
ipaIsEnabledToggled,
ipaDeleted,
ipaImageChanged,
ipaMethodChanged,
ipaModelChanged,
ipaCLIPVisionModelChanged,
ipaWeightChanged,
ipaBeginEndStepPctChanged,
ipaAllDeleted,
} = ipAdaptersSlice.actions;
export const selectIPAdaptersSlice = (state: RootState) => state.ipAdapters;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const ipAdaptersPersistConfig: PersistConfig<IPAdaptersState> = {
name: ipAdaptersSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,227 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
BrushLineAddedArg,
CanvasV2State,
EraserLineAddedArg,
ImageObjectAddedArg,
LayerData,
PointAddedToLineArg,
RectShapeAddedArg,
} from './types';
import { isLine } from './types';
export const selectLayer = (state: CanvasV2State, id: string) => state.layers.find((layer) => layer.id === id);
export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
const layer = selectLayer(state, id);
assert(layer, `Layer with id ${id} not found`);
return layer;
};
export const layersReducers = {
layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers.push({
id,
type: 'layer',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
opacity: 1,
x: 0,
y: 0,
});
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => {
state.layers.push(action.payload.data);
},
layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = !layer.isEnabled;
},
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.x = x;
layer.y = y;
},
layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.bbox = bbox;
layer.bboxNeedsUpdate = false;
if (bbox === null) {
layer.objects = [];
}
},
layerReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = true;
layer.objects = [];
layer.bbox = null;
layer.bboxNeedsUpdate = false;
},
layerDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers = state.layers.filter((l) => l.id !== id);
},
layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.opacity = opacity;
},
layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToEnd(state.layers, layer);
},
layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToEnd(state.layers, layer);
},
layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToStart(state.layers, layer);
},
layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToStart(state.layers, layer);
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
points,
strokeWidth: width,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
points,
strokeWidth: width,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const lastObject = layer.objects[layer.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
lastObject.points.push(...point);
layer.bboxNeedsUpdate = true;
},
layerRectAdded: {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
...rect,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
layerImageAdded: {
reducer: (state, action: PayloadAction<ImageObjectAddedArg & { imageId: string }>) => {
const { id, imageId, imageDTO } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const { width, height, image_name: name } = imageDTO;
layer.objects.push({
type: 'image',
id: getImageObjectId(id, imageId),
x: 0,
y: 0,
width,
height,
image: { width, height, name },
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }),
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,275 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
BrushLineAddedArg,
EraserLineAddedArg,
ImageObjectAddedArg,
LayerData,
PointAddedToLineArg,
RectShapeAddedArg,
} from './types';
import { isLine } from './types';
type LayersState = {
_version: 1;
layers: LayerData[];
};
const initialState: LayersState = { _version: 1, layers: [] };
export const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id);
export const selectLayerOrThrow = (state: LayersState, id: string) => {
const layer = selectLayer(state, id);
assert(layer, `Layer with id ${id} not found`);
return layer;
};
export const layersSlice = createSlice({
name: 'layers',
initialState,
reducers: {
layerAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers.push({
id,
type: 'layer',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
opacity: 1,
x: 0,
y: 0,
});
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => {
state.layers.push(action.payload.data);
},
layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = !layer.isEnabled;
},
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.x = x;
layer.y = y;
},
layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.bbox = bbox;
layer.bboxNeedsUpdate = false;
if (bbox === null) {
layer.objects = [];
}
},
layerReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.isEnabled = true;
layer.objects = [];
layer.bbox = null;
layer.bboxNeedsUpdate = false;
},
layerDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.layers = state.layers.filter((l) => l.id !== id);
},
layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.opacity = opacity;
},
layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToEnd(state.layers, layer);
},
layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToEnd(state.layers, layer);
},
layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveOneToStart(state.layers, layer);
},
layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
moveToStart(state.layers, layer);
},
layerBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
points,
strokeWidth: width,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
points,
strokeWidth: width,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
layerLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const lastObject = layer.objects[layer.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
lastObject.points.push(...point);
layer.bboxNeedsUpdate = true;
},
layerRectAdded: {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
...rect,
color,
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
layerImageAdded: {
reducer: (state, action: PayloadAction<ImageObjectAddedArg & { imageId: string }>) => {
const { id, imageId, imageDTO } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
const { width, height, image_name: name } = imageDTO;
layer.objects.push({
type: 'image',
id: getImageObjectId(id, imageId),
x: 0,
y: 0,
width,
height,
image: { width, height, name },
});
layer.bboxNeedsUpdate = true;
},
prepare: (payload: ImageObjectAddedArg) => ({ payload: { ...payload, imageId: uuidv4() } }),
},
layerAllDeleted: (state) => {
state.layers = [];
},
},
});
export const {
layerAdded,
layerDeleted,
layerReset,
layerMovedForwardOne,
layerMovedToFront,
layerMovedBackwardOne,
layerMovedToBack,
layerIsEnabledToggled,
layerOpacityChanged,
layerTranslated,
layerBboxChanged,
layerBrushLineAdded,
layerEraserLineAdded,
layerLinePointAdded,
layerRectAdded,
layerImageAdded,
layerAllDeleted,
} = layersSlice.actions;
export const selectLayersSlice = (state: RootState) => state.layers;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const layersPersistConfig: PersistConfig<LayersState> = {
name: layersSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -1,452 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
BrushLineAddedArg,
EraserLineAddedArg,
IPAdapterData,
PointAddedToLineArg,
RectShapeAddedArg,
RegionalGuidanceData,
RgbColor,
} from './types';
import { isLine } from './types';
type RegionalGuidanceState = {
_version: 1;
regions: RegionalGuidanceData[];
opacity: number;
};
const initialState: RegionalGuidanceState = {
_version: 1,
regions: [],
opacity: 0.3,
};
export const selectRG = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id);
export const selectRGOrThrow = (state: RegionalGuidanceState, id: string) => {
const rg = selectRG(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)
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
];
const getRGMaskFill = (state: RegionalGuidanceState): RgbColor => {
const lastFill = state.regions.slice(-1)[0]?.fill;
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
if (i === -1) {
i = 0;
}
i = (i + 1) % DEFAULT_MASK_COLORS.length;
const fill = DEFAULT_MASK_COLORS[i];
assert(fill, 'This should never happen');
return fill;
};
export const regionalGuidanceSlice = createSlice({
name: 'regionalGuidance',
initialState,
reducers: {
rgAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg: RegionalGuidanceData = {
id,
type: 'regional_guidance',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
fill: getRGMaskFill(state),
x: 0,
y: 0,
autoNegative: 'invert',
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
imageCache: null,
};
state.regions.push(rg);
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
rgReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects = [];
rg.bbox = null;
rg.bboxNeedsUpdate = false;
rg.imageCache = null;
},
rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => {
const { data } = action.payload;
state.regions.push(data);
},
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.isEnabled = !rg.isEnabled;
}
},
rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.x = x;
rg.y = y;
}
},
rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.bbox = bbox;
rg.bboxNeedsUpdate = false;
}
},
rgDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.regions = state.regions.filter((ca) => ca.id !== id);
},
rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => {
const { opacity } = action.payload;
state.opacity = opacity;
},
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToEnd(state.regions, rg);
},
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToEnd(state.regions, rg);
},
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToStart(state.regions, rg);
},
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToStart(state.regions, rg);
},
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.positivePrompt = prompt;
},
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.negativePrompt = prompt;
},
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
const { id, fill } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.fill = fill;
},
rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
const { id, imageDTO } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.imageCache = imageDTOToImageWithDims(imageDTO);
},
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.autoNegative = autoNegative;
},
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => {
const { id, ipAdapter } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.ipAdapters.push(ipAdapter);
},
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
const { id, ipAdapterId } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
},
rgIPAdapterImageChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
) => {
const { id, ipAdapterId, imageDTO } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
const { id, ipAdapterId, weight } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.weight = weight;
},
rgIPAdapterBeginEndStepPctChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
) => {
const { id, ipAdapterId, beginEndStepPct } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
rgIPAdapterMethodChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>
) => {
const { id, ipAdapterId, method } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.method = method;
},
rgIPAdapterModelChanged: (
state,
action: PayloadAction<{
id: string;
ipAdapterId: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, ipAdapterId, modelConfig } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
rgIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => {
const { id, ipAdapterId, clipVisionModel } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
rgBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
points,
strokeWidth: width,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
points,
strokeWidth: width,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const lastObject = rg.objects[rg.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
lastObject.points.push(...point);
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
rgRectAdded: {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
...rect,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
rgAllDeleted: (state) => {
state.regions = [];
},
},
});
export const {
rgAdded,
rgRecalled,
rgReset,
rgIsEnabledToggled,
rgTranslated,
rgBboxChanged,
rgDeleted,
rgGlobalOpacityChanged,
rgMovedForwardOne,
rgMovedToFront,
rgMovedBackwardOne,
rgMovedToBack,
rgPositivePromptChanged,
rgNegativePromptChanged,
rgFillChanged,
rgMaskImageUploaded,
rgAutoNegativeChanged,
rgIPAdapterAdded,
rgIPAdapterDeleted,
rgIPAdapterImageChanged,
rgIPAdapterWeightChanged,
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged,
rgBrushLineAdded,
rgEraserLineAdded,
rgLinePointAdded,
rgRectAdded,
rgAllDeleted,
} = regionalGuidanceSlice.actions;
export const selectRegionalGuidanceSlice = (state: RootState) => state.regionalGuidance;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;
};
export const regionalGuidancePersistConfig: PersistConfig<RegionalGuidanceState> = {
name: regionalGuidanceSlice.name,
initialState,
migrate,
persistDenylist: [],
};

View File

@ -0,0 +1,381 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types';
import { isEqual } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
BrushLineAddedArg,
EraserLineAddedArg,
IPAdapterData,
PointAddedToLineArg,
RectShapeAddedArg,
RegionalGuidanceData,
RgbColor,
} from './types';
import { isLine } from './types';
export const selectRG = (state: CanvasV2State, id: string) => state.regions.find((rg) => rg.id === id);
export const selectRGOrThrow = (state: CanvasV2State, id: string) => {
const rg = selectRG(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)
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
];
const getRGMaskFill = (state: CanvasV2State): RgbColor => {
const lastFill = state.regions.slice(-1)[0]?.fill;
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
if (i === -1) {
i = 0;
}
i = (i + 1) % DEFAULT_MASK_COLORS.length;
const fill = DEFAULT_MASK_COLORS[i];
assert(fill, 'This should never happen');
return fill;
};
export const regionsReducers = {
rgAdded: {
reducer: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg: RegionalGuidanceData = {
id,
type: 'regional_guidance',
isEnabled: true,
bbox: null,
bboxNeedsUpdate: false,
objects: [],
fill: getRGMaskFill(state),
x: 0,
y: 0,
autoNegative: 'invert',
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
imageCache: null,
};
state.regions.push(rg);
},
prepare: () => ({ payload: { id: uuidv4() } }),
},
rgReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects = [];
rg.bbox = null;
rg.bboxNeedsUpdate = false;
rg.imageCache = null;
},
rgRecalled: (state, action: PayloadAction<{ data: RegionalGuidanceData }>) => {
const { data } = action.payload;
state.regions.push(data);
},
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.isEnabled = !rg.isEnabled;
}
},
rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.x = x;
rg.y = y;
}
},
rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload;
const rg = selectRG(state, id);
if (rg) {
rg.bbox = bbox;
rg.bboxNeedsUpdate = false;
}
},
rgDeleted: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
state.regions = state.regions.filter((ca) => ca.id !== id);
},
rgGlobalOpacityChanged: (state, action: PayloadAction<{ opacity: number }>) => {
const { opacity } = action.payload;
state.maskFillOpacity = opacity;
},
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToEnd(state.regions, rg);
},
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToEnd(state.regions, rg);
},
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveOneToStart(state.regions, rg);
},
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
moveToStart(state.regions, rg);
},
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.positivePrompt = prompt;
},
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.negativePrompt = prompt;
},
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
const { id, fill } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.fill = fill;
},
rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
const { id, imageDTO } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.imageCache = imageDTOToImageWithDims(imageDTO);
},
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.autoNegative = autoNegative;
},
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => {
const { id, ipAdapter } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.ipAdapters.push(ipAdapter);
},
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
const { id, ipAdapterId } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.ipAdapters = rg.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
},
rgIPAdapterImageChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
) => {
const { id, ipAdapterId, imageDTO } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
const { id, ipAdapterId, weight } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.weight = weight;
},
rgIPAdapterBeginEndStepPctChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
) => {
const { id, ipAdapterId, beginEndStepPct } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.beginEndStepPct = beginEndStepPct;
},
rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => {
const { id, ipAdapterId, method } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.method = method;
},
rgIPAdapterModelChanged: (
state,
action: PayloadAction<{
id: string;
ipAdapterId: string;
modelConfig: IPAdapterModelConfig | null;
}>
) => {
const { id, ipAdapterId, modelConfig } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
if (modelConfig) {
ipa.model = zModelIdentifierField.parse(modelConfig);
} else {
ipa.model = null;
}
},
rgIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => {
const { id, ipAdapterId, clipVisionModel } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const ipa = rg.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
if (!ipa) {
return;
}
ipa.clipVisionModel = clipVisionModel;
},
rgBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getBrushLineId(id, lineId),
type: 'brush_line',
points,
strokeWidth: width,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: BrushLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
id: getEraserLineId(id, lineId),
type: 'eraser_line',
points,
strokeWidth: width,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: EraserLineAddedArg) => ({
payload: { ...payload, lineId: uuidv4() },
}),
},
rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
const lastObject = rg.objects[rg.objects.length - 1];
if (!lastObject || !isLine(lastObject)) {
return;
}
lastObject.points.push(...point);
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
rgRectAdded: {
reducer: (state, action: PayloadAction<RectShapeAddedArg & { rectId: string }>) => {
const { id, rect, rectId, color } = action.payload;
if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles
return;
}
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.objects.push({
type: 'rect_shape',
id: getRectShapeId(id, rectId),
...rect,
color,
});
rg.bboxNeedsUpdate = true;
rg.imageCache = null;
},
prepare: (payload: RectShapeAddedArg) => ({ payload: { ...payload, rectId: uuidv4() } }),
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -0,0 +1,57 @@
import type { ActionReducerMapBuilder, PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
type MySlice = {
flavour: 'vanilla' | 'chocolate' | 'strawberry';
sprinkles: boolean;
customers: { id: string; name: string }[];
};
const initialStateMySlice: MySlice = { flavour: 'vanilla', sprinkles: false, customers: [] };
const reducersInAnotherFile: SliceCaseReducers<MySlice> = {
sprinklesToggled: (state) => {
state.sprinkles = !state.sprinkles;
},
customerAdded: {
reducer: (state, action: PayloadAction<{ id: string; name: string }>) => {
state.customers.push(action.payload);
},
prepare: (name: string) => ({ payload: { name, id: crypto.randomUUID() } }),
},
};
const extraReducersInAnotherFile = (builder: ActionReducerMapBuilder<MySlice>) => {
builder.addCase(otherSlice.actions.fooChanged, (state, action) => {
if (action.payload === 'bar') {
state.flavour = 'vanilla';
}
});
};
export const mySlice = createSlice({
name: 'mySlice',
initialState: initialStateMySlice,
reducers: {
...reducersInAnotherFile,
flavourChanged: (state, action: PayloadAction<MySlice['flavour']>) => {
state.flavour = action.payload;
},
},
extraReducers: extraReducersInAnotherFile,
});
type OtherSlice = {
something: string;
};
const initialStateOtherSlice: OtherSlice = { something: 'foo' };
export const otherSlice = createSlice({
name: 'otherSlice',
initialState: initialStateOtherSlice,
reducers: {
fooChanged: (state, action: PayloadAction<string>) => {
state.something = action.payload;
},
},
});

View File

@ -783,6 +783,11 @@ export type CanvasV2State = {
aspectRatio: AspectRatioState;
};
bbox: IRect;
layers: LayerData[];
controlAdapters: ControlAdapterData[];
ipAdapters: IPAdapterData[];
regions: RegionalGuidanceData[];
maskFillOpacity: number;
};
export type StageAttrs = { x: number; y: number; width: number; height: number; scale: number };

View File

@ -3,7 +3,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {

View File

@ -7,7 +7,7 @@ import {
} from 'features/controlAdapters/store/controlAdaptersSlice';
import type { ControlAdaptersState } from 'features/controlAdapters/store/types';
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,

View File

@ -15,7 +15,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/canvas/store/canvasSlice';
import { selectControlAdaptersSlice } from 'features/controlAdapters/store/controlAdaptersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';

View File

@ -6,7 +6,7 @@ import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
import { useDownloadImage } from 'common/hooks/useDownloadImage';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { useImageActions } from 'features/gallery/hooks/useImageActions';
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';

View File

@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/query';
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice';
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';

View File

@ -1,7 +1,7 @@
import { getStore } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { objectKeys } from 'common/util/objectKeys';
import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice';
import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { LayerData } from 'features/controlLayers/store/types';
import type { LoRA } from 'features/lora/store/loraSlice';
import type {

View File

@ -19,7 +19,7 @@ import {
positivePromptChanged,
regionalGuidanceRecalled,
widthChanged,
} from 'features/controlLayers/store/controlLayersSlice';
} from 'features/controlLayers/store/canvasV2Slice';
import type { LayerData } from 'features/controlLayers/store/types';
import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice';
import type { LoRA } from 'features/lora/store/loraSlice';

View File

@ -5,7 +5,7 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { RG_LAYER_NAME } from 'features/controlLayers/konva/naming';
import { renderers } from 'features/controlLayers/konva/renderers/layers';
import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/controlLayersSlice';
import { regionalGuidanceMaskImageUploaded } from 'features/controlLayers/store/canvasV2Slice';
import type { InitialImageLayer, LayerData, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
import {
isControlAdapterLayer,

View File

@ -1,6 +1,6 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { negativePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';

View File

@ -1,6 +1,6 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';

View File

@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { StageComponent } from 'features/controlLayers/components/StageComponent';
import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSlice';
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
import { AspectRatioIconPreview } from 'features/parameters/components/ImageSize/AspectRatioIconPreview';
import { memo } from 'react';

View File

@ -1,7 +1,7 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { iiLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
import { iiLayerAdded } from 'features/controlLayers/store/canvasV2Slice';
import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { toast } from 'features/toast/toast';

View File

@ -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/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import type { PropsWithChildren } from 'react';

View File

@ -1,6 +1,6 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { negativePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
import { negativePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';

View File

@ -1,6 +1,6 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { positivePrompt2Changed } from 'features/controlLayers/store/controlLayersSlice';
import { positivePrompt2Changed } from 'features/controlLayers/store/canvasV2Slice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';

View File

@ -1,6 +1,6 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { shouldConcatPromptsChanged } from 'features/controlLayers/store/controlLayersSlice';
import { shouldConcatPromptsChanged } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi';

View File

@ -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/controlLayersSlice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { HrfSettings } from 'features/hrf/components/HrfSettings';
import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';

View File

@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import { aspectRatioChanged, heightChanged, widthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { ParamHeight } from 'features/parameters/components/Core/ParamHeight';
import { ParamWidth } from 'features/parameters/components/Core/ParamWidth';
import { AspectRatioCanvasPreview } from 'features/parameters/components/ImageSize/AspectRatioCanvasPreview';

View File

@ -4,7 +4,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
import { $isPreviewVisible } from 'features/controlLayers/store/controlLayersSlice';
import { $isPreviewVisible } from 'features/controlLayers/store/canvasV2Slice';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls';