feat(ui): normalize all actions to accept an entityIdentifier

Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type.

All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed.

`selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed.

The types for canvas entities are updated with optional type parameters for this purpose.

All reducers, actions and components have been updated.
This commit is contained in:
psychedelicious 2024-08-26 18:52:28 +10:00
parent f86b50d18a
commit a3179e7a3f
60 changed files with 588 additions and 430 deletions

View File

@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@ -51,9 +52,9 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
// };
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => {
if (ipAdapter.image?.image_name === imageDTO.image_name) {
dispatch(ipaImageChanged({ id, imageDTO: null }));
state.canvasV2.ipAdapters.entities.forEach((entity) => {
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
}
});
};

View File

@ -51,7 +51,9 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payload.imageDTO
) {
const { id } = overData.context;
dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
dispatch(
ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO: activeData.payload.imageDTO })
);
return;
}
@ -64,7 +66,13 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payload.imageDTO
) {
const { id, ipAdapterId } = overData.context;
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO }));
dispatch(
rgIPAdapterImageChanged({
entityIdentifier: { id, type: 'regional_guidance' },
ipAdapterId,
imageDTO: activeData.payload.imageDTO,
})
);
return;
}

View File

@ -89,14 +89,16 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;
dispatch(ipaImageChanged({ id, imageDTO }));
dispatch(ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
const { id, ipAdapterId } = postUploadAction;
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
dispatch(
rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, ipAdapterId, imageDTO })
);
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}

View File

@ -13,6 +13,7 @@ import {
rgIPAdapterModelChanged,
vaeSelected,
} from 'features/controlLayers/store/canvasV2Slice';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
@ -178,7 +179,7 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log)
if (isModelAvailable) {
return;
}
dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null }));
dispatch(controlLayerModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
};
@ -189,16 +190,18 @@ const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
if (isModelAvailable) {
return;
}
dispatch(ipaModelChanged({ id: entity.id, modelConfig: null }));
dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
});
state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => {
ipAdapters.forEach(({ id: ipAdapterId, model }) => {
state.canvasV2.regions.entities.forEach((entity) => {
entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => {
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
if (isModelAvailable) {
return;
}
dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null }));
dispatch(
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), ipAdapterId, modelConfig: null })
);
});
});
};

View File

@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';

View File

@ -1,15 +1,15 @@
import { Badge } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const ControlLayerBadges = memo(() => {
const { id } = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const { t } = useTranslation();
const withTransparencyEffect = useAppSelector(
(s) => selectControlLayerEntityOrThrow(s.canvasV2, id).withTransparencyEffect
(s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).withTransparencyEffect
);
return (

View File

@ -18,35 +18,35 @@ import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/
export const ControlLayerControlAdapter = memo(() => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const controlAdapter = useControlLayerControlAdapter(entityIdentifier);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(controlLayerBeginEndStepPctChanged({ id: entityIdentifier.id, beginEndStepPct }));
dispatch(controlLayerBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
dispatch(controlLayerControlModeChanged({ id: entityIdentifier.id, controlMode }));
dispatch(controlLayerControlModeChanged({ entityIdentifier, controlMode }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(controlLayerWeightChanged({ id: entityIdentifier.id, weight }));
dispatch(controlLayerWeightChanged({ entityIdentifier, weight }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(controlLayerModelChanged({ id: entityIdentifier.id, modelConfig }));
dispatch(controlLayerModelChanged({ entityIdentifier, modelConfig }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
return (

View File

@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { ControlLayer } from 'features/controlLayers/components/ControlLayer/ControlLayer';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {

View File

@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi';
export const ControlLayerMenuItemsControlToRaster = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const convertControlLayerToRasterLayer = useCallback(() => {
dispatch(controlLayerConvertedToRasterLayer({ id: entityIdentifier.id }));
}, [dispatch, entityIdentifier.id]);
dispatch(controlLayerConvertedToRasterLayer({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={convertControlLayerToRasterLayer} icon={<PiLightningBold />}>

View File

@ -2,11 +2,8 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
controlLayerWithTransparencyEffectToggled,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
import { controlLayerWithTransparencyEffectToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfBold } from 'react-icons/pi';
@ -14,18 +11,18 @@ import { PiDropHalfBold } from 'react-icons/pi';
export const ControlLayerMenuItemsTransparencyEffect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('control_layer');
const selectWithTransparencyEffect = useMemo(
() =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
const entity = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id);
const entity = selectEntityOrThrow(canvasV2, entityIdentifier);
return entity.withTransparencyEffect;
}),
[entityIdentifier.id]
[entityIdentifier]
);
const withTransparencyEffect = useAppSelector(selectWithTransparencyEffect);
const onToggle = useCallback(() => {
dispatch(controlLayerWithTransparencyEffectToggled({ id: entityIdentifier.id }));
dispatch(controlLayerWithTransparencyEffectToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (

View File

@ -4,7 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { IPAdapter } from 'features/controlLayers/components/IPAdapter/IPAdapter';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {

View File

@ -13,7 +13,7 @@ import {
ipaModelChanged,
ipaWeightChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectIPAdapterEntityOrThrow } from 'features/controlLayers/store/ipAdaptersReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { IPAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@ -24,53 +24,59 @@ import { IPAdapterModel } from './IPAdapterModel';
export const IPAdapterSettings = memo(() => {
const dispatch = useAppDispatch();
const { id } = useEntityIdentifierContext();
const ipAdapter = useAppSelector((s) => selectIPAdapterEntityOrThrow(s.canvasV2, id).ipAdapter);
const entityIdentifier = useEntityIdentifierContext('ip_adapter');
const ipAdapter = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapter);
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(ipaBeginEndStepPctChanged({ id, beginEndStepPct }));
dispatch(ipaBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(ipaWeightChanged({ id, weight }));
dispatch(ipaWeightChanged({ entityIdentifier, weight }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(ipaMethodChanged({ id, method }));
dispatch(ipaMethodChanged({ entityIdentifier, method }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(ipaModelChanged({ id, modelConfig }));
dispatch(ipaModelChanged({ entityIdentifier, modelConfig }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(ipaCLIPVisionModelChanged({ id, clipVisionModel }));
dispatch(ipaCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(ipaImageChanged({ id, imageDTO }));
dispatch(ipaImageChanged({ entityIdentifier, imageDTO }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const droppableData = useMemo<IPAImageDropData>(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(() => ({ type: 'SET_IPA_IMAGE', id }), [id]);
const droppableData = useMemo<IPAImageDropData>(
() => ({ actionType: 'SET_IPA_IMAGE', context: { id: entityIdentifier.id }, id: entityIdentifier.id }),
[entityIdentifier.id]
);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
[entityIdentifier.id]
);
return (
<CanvasEntitySettingsWrapper>
@ -95,7 +101,7 @@ export const IPAdapterSettings = memo(() => {
<IPAdapterImagePreview
image={ipAdapter.image ?? null}
onChangeImage={onChangeImage}
ipAdapterId={id}
ipAdapterId={entityIdentifier.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>

View File

@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {

View File

@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { inpaintMaskFillColorChanged, inpaintMaskFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectInpaintMaskEntityOrThrow } from 'features/controlLayers/store/inpaintMaskReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -13,8 +13,8 @@ import { useTranslation } from 'react-i18next';
export const InpaintMaskMaskFillColorPicker = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const fill = useAppSelector((s) => selectInpaintMaskEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
const entityIdentifier = useEntityIdentifierContext('inpaint_mask');
const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {

View File

@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {

View File

@ -9,11 +9,11 @@ import { PiLightningBold } from 'react-icons/pi';
export const RasterLayerMenuItemsRasterToControl = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('raster_layer');
const convertRasterLayerToControlLayer = useCallback(() => {
dispatch(rasterLayerConvertedToControlLayer({ id: entityIdentifier.id }));
}, [dispatch, entityIdentifier.id]);
dispatch(rasterLayerConvertedToControlLayer({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem onClick={convertRasterLayerToControlLayer} icon={<PiLightningBold />}>

View File

@ -1,44 +1,42 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
type AddPromptButtonProps = {
id: string;
};
export const RegionalGuidanceAddPromptsIPAdapterButtons = ({ id }: AddPromptButtonProps) => {
export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
const entity = selectEntityOrThrow(canvasV2, entityIdentifier);
return {
canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: rg?.negativePrompt === null,
canAddPositivePrompt: entity?.positivePrompt === null,
canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
[id]
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id, prompt: '' }));
}, [dispatch, id]);
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: '' }));
}, [dispatch, id]);
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
dispatch(rgIPAdapterAdded({ id }));
}, [dispatch, id]);
dispatch(rgIPAdapterAdded({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<Flex w="full" p={2} justifyContent="space-between">

View File

@ -1,14 +1,14 @@
import { Badge } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceBadges = memo(() => {
const { id } = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative);
const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative);
return (
<>

View File

@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
import { RegionalGuidance } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidance';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {

View File

@ -5,6 +5,7 @@ import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterCLIPVisionModelChanged,
@ -14,7 +15,7 @@ import {
rgIPAdapterModelChanged,
rgIPAdapterWeightChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
@ -23,71 +24,75 @@ import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction }
import { assert } from 'tsafe';
type Props = {
id: string;
ipAdapterId: string;
ipAdapterNumber: number;
};
export const RegionalGuidanceIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => {
export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterNumber }: Props) => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(rgIPAdapterDeleted({ id, ipAdapterId }));
}, [dispatch, ipAdapterId, id]);
dispatch(rgIPAdapterDeleted({ entityIdentifier, ipAdapterId }));
}, [dispatch, entityIdentifier, ipAdapterId]);
const ipAdapter = useAppSelector((s) => {
const ipa = selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.find((ipa) => ipa.id === ipAdapterId);
const ipa = selectRegionalGuidanceIPAdapter(s.canvasV2, entityIdentifier, ipAdapterId);
assert(ipa, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`);
return ipa;
});
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(rgIPAdapterBeginEndStepPctChanged({ id, ipAdapterId, beginEndStepPct }));
dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, ipAdapterId, beginEndStepPct }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(rgIPAdapterWeightChanged({ id, ipAdapterId, weight }));
dispatch(rgIPAdapterWeightChanged({ entityIdentifier, ipAdapterId, weight }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(rgIPAdapterMethodChanged({ id, ipAdapterId, method }));
dispatch(rgIPAdapterMethodChanged({ entityIdentifier, ipAdapterId, method }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig }));
dispatch(rgIPAdapterModelChanged({ entityIdentifier, ipAdapterId, modelConfig }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgIPAdapterCLIPVisionModelChanged({ id, ipAdapterId, clipVisionModel }));
dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, ipAdapterId, clipVisionModel }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
dispatch(rgIPAdapterImageChanged({ entityIdentifier, ipAdapterId, imageDTO }));
},
[dispatch, ipAdapterId, id]
[dispatch, entityIdentifier, ipAdapterId]
);
const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }),
[ipAdapterId, id]
() => ({
actionType: 'SET_RG_IP_ADAPTER_IMAGE',
context: { id: entityIdentifier.id, ipAdapterId },
id: entityIdentifier.id,
}),
[entityIdentifier.id, ipAdapterId]
);
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }),
[ipAdapterId, id]
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, ipAdapterId }),
[entityIdentifier.id, ipAdapterId]
);
return (

View File

@ -3,25 +3,23 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceIPAdapterSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceIPAdapterSettings';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { Fragment, memo, useMemo } from 'react';
type Props = {
id: string;
};
export const RegionalGuidanceIPAdapters = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const ipAdapterIds = selectRegionalGuidanceEntityOrThrow(canvasV2, id).ipAdapters.map(({ id }) => id);
const ipAdapterIds = selectEntityOrThrow(canvasV2, entityIdentifier).ipAdapters.map(({ id }) => id);
if (ipAdapterIds.length === 0) {
return EMPTY_ARRAY;
}
return ipAdapterIds;
}),
[id]
[entityIdentifier]
);
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
@ -35,7 +33,7 @@ export const RegionalGuidanceIPAdapters = memo(({ id }: Props) => {
{ipAdapterIds.map((ipAdapterId, index) => (
<Fragment key={ipAdapterId}>
{index > 0 && <Divider />}
<RegionalGuidanceIPAdapterSettings id={id} ipAdapterId={ipAdapterId} ipAdapterNumber={index + 1} />
<RegionalGuidanceIPAdapterSettings ipAdapterId={ipAdapterId} ipAdapterNumber={index + 1} />
</Fragment>
))}
</>

View File

@ -5,27 +5,27 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { MaskFillStyle } from 'features/controlLayers/components/common/MaskFillStyle';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgFillColorChanged, rgFillStyleChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { FillStyle, RgbColor } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMaskFillColorPicker = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, entityIdentifier.id).fill);
const fill = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).fill);
const onChangeFillColor = useCallback(
(color: RgbColor) => {
dispatch(rgFillColorChanged({ id: entityIdentifier.id, color }));
dispatch(rgFillColorChanged({ entityIdentifier, color }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
const onChangeFillStyle = useCallback(
(style: FillStyle) => {
dispatch(rgFillStyleChanged({ id: entityIdentifier.id, style }));
dispatch(rgFillStyleChanged({ entityIdentifier, style }));
},
[dispatch, entityIdentifier.id]
[dispatch, entityIdentifier]
);
return (
<Popover isLazy>

View File

@ -6,36 +6,36 @@ import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
const { id } = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
const rg = canvasV2.regions.entities.find((rg) => rg.id === id);
const entity = selectEntity(canvasV2, entityIdentifier);
return {
canAddPositivePrompt: rg?.positivePrompt === null,
canAddNegativePrompt: rg?.negativePrompt === null,
canAddPositivePrompt: entity?.positivePrompt === null,
canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
[id]
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id: id, prompt: '' }));
}, [dispatch, id]);
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
dispatch(rgIPAdapterAdded({ id }));
}, [dispatch, id]);
dispatch(rgIPAdapterAdded({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<>

View File

@ -2,19 +2,19 @@ import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgAutoNegativeToggled } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiSelectionInverseBold } from 'react-icons/pi';
export const RegionalGuidanceMenuItemsAutoNegative = memo(() => {
const { id } = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoNegative = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).autoNegative);
const autoNegative = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).autoNegative);
const onClick = useCallback(() => {
dispatch(rgAutoNegativeToggled({ id }));
}, [dispatch, id]);
dispatch(rgAutoNegativeToggled({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<MenuItem icon={<PiSelectionInverseBold />} onClick={onClick}>

View File

@ -1,8 +1,9 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgNegativePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
};
export const RegionalGuidanceNegativePrompt = memo(({ id }: Props) => {
const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt ?? '');
export const RegionalGuidanceNegativePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
dispatch(rgNegativePromptChanged({ id, prompt: v }));
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: v }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onDeletePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: null }));
}, [dispatch, id]);
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: null }));
}, [dispatch, entityIdentifier]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt,
textareaRef,

View File

@ -1,8 +1,9 @@
import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RegionalGuidanceDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceDeletePromptButton';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { rgPositivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
@ -10,24 +11,21 @@ import { usePrompt } from 'features/prompt/usePrompt';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
};
export const RegionalGuidancePositivePrompt = memo(({ id }: Props) => {
const prompt = useAppSelector((s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt ?? '');
export const RegionalGuidancePositivePrompt = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const prompt = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt ?? '');
const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const _onChange = useCallback(
(v: string) => {
dispatch(rgPositivePromptChanged({ id, prompt: v }));
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: v }));
},
[dispatch, id]
[dispatch, entityIdentifier]
);
const onDeletePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id, prompt: null }));
}, [dispatch, id]);
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: null }));
}, [dispatch, entityIdentifier]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt,
textareaRef,

View File

@ -3,7 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { RegionalGuidanceAddPromptsIPAdapterButtons } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceAddPromptsIPAdapterButtons';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectRegionalGuidanceEntityOrThrow } from 'features/controlLayers/store/regionsReducers';
import { selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { RegionalGuidanceIPAdapters } from './RegionalGuidanceIPAdapters';
@ -11,35 +11,31 @@ import { RegionalGuidanceNegativePrompt } from './RegionalGuidanceNegativePrompt
import { RegionalGuidancePositivePrompt } from './RegionalGuidancePositivePrompt';
export const RegionalGuidanceSettings = memo(() => {
const { id } = useEntityIdentifierContext();
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const hasPositivePrompt = useAppSelector(
(s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).positivePrompt !== null
(s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).positivePrompt !== null
);
const hasNegativePrompt = useAppSelector(
(s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).negativePrompt !== null
);
const hasIPAdapters = useAppSelector(
(s) => selectRegionalGuidanceEntityOrThrow(s.canvasV2, id).ipAdapters.length > 0
(s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).negativePrompt !== null
);
const hasIPAdapters = useAppSelector((s) => selectEntityOrThrow(s.canvasV2, entityIdentifier).ipAdapters.length > 0);
return (
<CanvasEntitySettingsWrapper>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && (
<RegionalGuidanceAddPromptsIPAdapterButtons id={id} />
)}
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <RegionalGuidanceAddPromptsIPAdapterButtons />}
{hasPositivePrompt && (
<>
<RegionalGuidancePositivePrompt id={id} />
<RegionalGuidancePositivePrompt />
{(hasNegativePrompt || hasIPAdapters) && <Divider />}
</>
)}
{hasNegativePrompt && (
<>
<RegionalGuidanceNegativePrompt id={id} />
<RegionalGuidanceNegativePrompt />
{hasIPAdapters && <Divider />}
</>
)}
{hasIPAdapters && <RegionalGuidanceIPAdapters id={id} />}
{hasIPAdapters && <RegionalGuidanceIPAdapters />}
</CanvasEntitySettingsWrapper>
);
});

View File

@ -7,8 +7,8 @@ import {
entityArrangedForwardOne,
entityArrangedToBack,
entityArrangedToFront,
selectCanvasV2Slice,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -15,7 +15,8 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { snapToNearest } from 'features/controlLayers/konva/util';
import { entityOpacityChanged, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { entityOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectEntity } from 'features/controlLayers/store/selectors';
import { isDrawableEntity } from 'features/controlLayers/store/types';
import { clamp, round } from 'lodash-es';
import type { KeyboardEvent } from 'react';

View File

@ -5,7 +5,7 @@ import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { useEntityAdapter } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { memo, useEffect, useMemo, useRef } from 'react';
import { useSelector } from 'react-redux';

View File

@ -1,11 +1,16 @@
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { CanvasEntityIdentifier, CanvasEntityType } from 'features/controlLayers/store/types';
import { createContext, useContext } from 'react';
import { assert } from 'tsafe';
export const EntityIdentifierContext = createContext<CanvasEntityIdentifier | null>(null);
export const useEntityIdentifierContext = (): CanvasEntityIdentifier => {
export const useEntityIdentifierContext = <T extends CanvasEntityType | undefined = CanvasEntityType>(
type?: T
): CanvasEntityIdentifier<T extends undefined ? CanvasEntityType : T> => {
const entityIdentifier = useContext(EntityIdentifierContext);
assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider');
return entityIdentifier;
if (type) {
assert(entityIdentifier.type === type, 'useEntityIdentifier must be used with the correct type');
}
return entityIdentifier as CanvasEntityIdentifier<T extends undefined ? CanvasEntityType : T>;
};

View File

@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { entityDeleted, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';

View File

@ -1,7 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { entityReset, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { entityReset } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import { type CanvasEntityIdentifier, isDrawableEntity } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'common/util/colorCodeTransformers';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react';

View File

@ -1,8 +1,7 @@
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectControlLayerEntityOrThrow } from 'features/controlLayers/store/controlLayersReducers';
import { selectCanvasV2Slice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type {
CanvasEntityIdentifier,
ControlNetConfig,
@ -14,11 +13,11 @@ import { zModelIdentifierField } from 'features/nodes/types/common';
import { useMemo } from 'react';
import { useControlNetAndT2IAdapterModels, useIPAdapterModels } from 'services/api/hooks/modelsByType';
export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier) => {
export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
const selectControlAdapter = useMemo(
() =>
createMemoizedAppSelector(selectCanvasV2Slice, (canvasV2) => {
const layer = selectControlLayerEntityOrThrow(canvasV2, entityIdentifier.id);
const layer = selectEntityOrThrow(canvasV2, entityIdentifier);
return layer.controlAdapter;
}),
[entityIdentifier]

View File

@ -25,10 +25,10 @@ import {
entitySelected,
eraserWidthChanged,
fillChanged,
selectAllRenderableEntities,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectAllRenderableEntities } from 'features/controlLayers/store/selectors';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,

View File

@ -1,6 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { PersistConfig } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@ -13,6 +13,7 @@ import { lorasReducers } from 'features/controlLayers/store/lorasReducers';
import { paramsReducers } from 'features/controlLayers/store/paramsReducers';
import { rasterLayersReducers } from 'features/controlLayers/store/rasterLayersReducers';
import { regionsReducers } from 'features/controlLayers/store/regionsReducers';
import { selectAllEntities, selectAllEntitiesOfType, selectEntity } from 'features/controlLayers/store/selectors';
import { sessionReducers } from 'features/controlLayers/store/sessionReducers';
import { settingsReducers } from 'features/controlLayers/store/settingsReducers';
import { toolReducers } from 'features/controlLayers/store/toolReducers';
@ -25,12 +26,7 @@ import { atom } from 'nanostores';
import { assert } from 'tsafe';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasEntityState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
EntityBrushLineAddedPayload,
@ -143,60 +139,6 @@ const initialState: CanvasV2State = {
},
};
export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) {
switch (type) {
case 'raster_layer':
return state.rasterLayers.entities.find((entity) => entity.id === id);
case 'control_layer':
return state.controlLayers.entities.find((entity) => entity.id === id);
case 'inpaint_mask':
return state.inpaintMasks.entities.find((entity) => entity.id === id);
case 'regional_guidance':
return state.regions.entities.find((entity) => entity.id === id);
case 'ip_adapter':
return state.ipAdapters.entities.find((entity) => entity.id === id);
default:
return;
}
}
function selectAllEntitiesOfType(state: CanvasV2State, type: CanvasEntityState['type']): CanvasEntityState[] {
switch (type) {
case 'raster_layer':
return state.rasterLayers.entities;
case 'control_layer':
return state.controlLayers.entities;
case 'inpaint_mask':
return state.inpaintMasks.entities;
case 'regional_guidance':
return state.regions.entities;
case 'ip_adapter':
return state.ipAdapters.entities;
}
}
function selectAllEntities(state: CanvasV2State): CanvasEntityState[] {
// These are in the same order as they are displayed in the list!
return [
...state.inpaintMasks.entities.toReversed(),
...state.regions.entities.toReversed(),
...state.ipAdapters.entities.toReversed(),
...state.controlLayers.entities.toReversed(),
...state.rasterLayers.entities.toReversed(),
];
}
export function selectAllRenderableEntities(
state: CanvasV2State
): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] {
return [
...state.rasterLayers.entities,
...state.controlLayers.entities,
...state.inpaintMasks.entities,
...state.regions.entities,
];
}
export const canvasV2Slice = createSlice({
name: 'canvasV2',
initialState,
@ -217,7 +159,7 @@ export const canvasV2Slice = createSlice({
const { entityIdentifier } = action.payload;
state.selectedEntityIdentifier = entityIdentifier;
},
entityNameChanged: (state, action: PayloadAction<EntityIdentifierPayload & { name: string | null }>) => {
entityNameChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ name: string | null }>>) => {
const { entityIdentifier, name } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
@ -617,8 +559,6 @@ export const {
sessionModeChanged,
} = canvasV2Slice.actions;
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrate = (state: any): any => {
return state;

View File

@ -1,10 +1,10 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectEntity } from 'features/controlLayers/store/selectors';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge, omit } from 'lodash-es';
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import type {
CanvasControlLayerState,
@ -12,18 +12,11 @@ import type {
CanvasV2State,
ControlModeV2,
ControlNetConfig,
EntityIdentifierPayload,
T2IAdapterConfig,
} from './types';
import { getEntityIdentifier, initialControlNet } from './types';
const selectControlLayerEntity = (state: CanvasV2State, id: string) =>
state.controlLayers.entities.find((entity) => entity.id === id);
export const selectControlLayerEntityOrThrow = (state: CanvasV2State, id: string) => {
const layer = selectControlLayerEntity(state, id);
assert(layer, `Layer with id ${id} not found`);
return layer;
};
export const controlLayersReducers = {
controlLayerAdded: {
reducer: (
@ -58,9 +51,9 @@ export const controlLayersReducers = {
state.selectedEntityIdentifier = { type: 'control_layer', id: data.id };
},
controlLayerConvertedToRasterLayer: {
reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
const { id, newId } = action.payload;
const layer = selectControlLayerEntity(state, id);
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'control_layer'>>) => {
const { entityIdentifier, newId } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
@ -73,26 +66,30 @@ export const controlLayersReducers = {
};
// Remove the control layer
state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== id);
state.controlLayers.entities = state.controlLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
// Add the new raster layer
state.rasterLayers.entities.push(rasterLayerState);
state.selectedEntityIdentifier = { type: rasterLayerState.type, id: rasterLayerState.id };
},
prepare: (payload: { id: string }) => ({
prepare: (payload: EntityIdentifierPayload<void, 'control_layer'>) => ({
payload: { ...payload, newId: getPrefixedId('raster_layer') },
}),
},
controlLayerModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
}>
action: PayloadAction<
EntityIdentifierPayload<
{
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
},
'control_layer'
>
>
) => {
const { id, modelConfig } = action.payload;
const layer = selectControlLayerEntity(state, id);
const { entityIdentifier, modelConfig } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
@ -118,17 +115,23 @@ export const controlLayersReducers = {
layer.controlAdapter = t2iAdapterConfig;
}
},
controlLayerControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload;
const layer = selectControlLayerEntity(state, id);
controlLayerControlModeChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ controlMode: ControlModeV2 }, 'control_layer'>>
) => {
const { entityIdentifier, controlMode } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter || layer.controlAdapter.type !== 'controlnet') {
return;
}
layer.controlAdapter.controlMode = controlMode;
},
controlLayerWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const layer = selectControlLayerEntity(state, id);
controlLayerWeightChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'control_layer'>>
) => {
const { entityIdentifier, weight } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
@ -136,18 +139,21 @@ export const controlLayersReducers = {
},
controlLayerBeginEndStepPctChanged: (
state,
action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'control_layer'>>
) => {
const { id, beginEndStepPct } = action.payload;
const layer = selectControlLayerEntity(state, id);
const { entityIdentifier, beginEndStepPct } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer || !layer.controlAdapter) {
return;
}
layer.controlAdapter.beginEndStepPct = beginEndStepPct;
},
controlLayerWithTransparencyEffectToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const layer = selectControlLayerEntity(state, id);
controlLayerWithTransparencyEffectToggled: (
state,
action: PayloadAction<EntityIdentifierPayload<void, 'control_layer'>>
) => {
const { entityIdentifier } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}

View File

@ -1,23 +1,15 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import {
type CanvasInpaintMaskState,
type CanvasV2State,
type EntityIdentifierPayload,
type FillStyle,
getEntityIdentifier,
type RgbColor,
import { selectEntity } from 'features/controlLayers/store/selectors';
import type {
CanvasInpaintMaskState,
CanvasV2State,
EntityIdentifierPayload,
FillStyle,
RgbColor,
} from 'features/controlLayers/store/types';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { merge } from 'lodash-es';
import { assert } from 'tsafe';
const selectInpaintMaskEntity = (state: CanvasV2State, id: string) =>
state.inpaintMasks.entities.find((layer) => layer.id === id);
export const selectInpaintMaskEntityOrThrow = (state: CanvasV2State, id: string) => {
const entity = selectInpaintMaskEntity(state, id);
assert(entity, `Inpaint mask with id ${id} not found`);
return entity;
};
export const inpaintMaskReducers = {
inpaintMaskAdded: {
@ -54,17 +46,23 @@ export const inpaintMaskReducers = {
state.inpaintMasks.entities = [data];
state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id };
},
inpaintMaskFillColorChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }>>) => {
inpaintMaskFillColorChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'inpaint_mask'>>
) => {
const { color, entityIdentifier } = action.payload;
const entity = selectInpaintMaskEntity(state, entityIdentifier.id);
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
inpaintMaskFillStyleChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }>>) => {
inpaintMaskFillStyleChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'inpaint_mask'>>
) => {
const { style, entityIdentifier } = action.payload;
const entity = selectInpaintMaskEntity(state, entityIdentifier.id);
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}

View File

@ -1,23 +1,20 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectEntity } from 'features/controlLayers/store/selectors';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge } from 'lodash-es';
import type { ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type { CanvasIPAdapterState, CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from './types';
import type {
CanvasIPAdapterState,
CanvasV2State,
CLIPVisionModelV2,
EntityIdentifierPayload,
IPMethodV2,
} from './types';
import { getEntityIdentifier, imageDTOToImageWithDims, initialIPAdapter } from './types';
const selectIPAdapterEntity = (state: CanvasV2State, id: string) =>
state.ipAdapters.entities.find((ipa) => ipa.id === id);
export const selectIPAdapterEntityOrThrow = (state: CanvasV2State, id: string) => {
const entity = selectIPAdapterEntity(state, id);
assert(entity, `IP Adapter with id ${id} not found`);
return entity;
};
export const ipAdaptersReducers = {
ipaAdded: {
reducer: (
@ -47,52 +44,61 @@ export const ipAdaptersReducers = {
state.ipAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
},
ipaImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null }>) => {
const { id, imageDTO } = action.payload;
const entity = selectIPAdapterEntity(state, id);
if (!entity) {
return;
}
entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
ipaImageChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ imageDTO: ImageDTO | null }, 'ip_adapter'>>
) => {
const { entityIdentifier, imageDTO } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
ipaMethodChanged: (state, action: PayloadAction<{ id: string; method: IPMethodV2 }>) => {
const { id, method } = action.payload;
const entity = selectIPAdapterEntity(state, id);
ipaMethodChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ method: IPMethodV2 }, 'ip_adapter'>>) => {
const { entityIdentifier, method } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.method = method;
},
ipaModelChanged: (state, action: PayloadAction<{ id: string; modelConfig: IPAdapterModelConfig | null }>) => {
const { id, modelConfig } = action.payload;
const entity = selectIPAdapterEntity(state, id);
ipaModelChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ modelConfig: IPAdapterModelConfig | null }, 'ip_adapter'>>
) => {
const { entityIdentifier, modelConfig } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
},
ipaCLIPVisionModelChanged: (state, action: PayloadAction<{ id: string; clipVisionModel: CLIPVisionModelV2 }>) => {
const { id, clipVisionModel } = action.payload;
const entity = selectIPAdapterEntity(state, id);
ipaCLIPVisionModelChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ clipVisionModel: CLIPVisionModelV2 }, 'ip_adapter'>>
) => {
const { entityIdentifier, clipVisionModel } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.clipVisionModel = clipVisionModel;
},
ipaWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const entity = selectIPAdapterEntity(state, id);
ipaWeightChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'ip_adapter'>>) => {
const { entityIdentifier, weight } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.ipAdapter.weight = weight;
},
ipaBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const entity = selectIPAdapterEntity(state, id);
ipaBeginEndStepPctChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'ip_adapter'>>
) => {
const { entityIdentifier, beginEndStepPct } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}

View File

@ -1,14 +1,12 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectEntity } from 'features/controlLayers/store/selectors';
import { merge } from 'lodash-es';
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State } from './types';
import type { CanvasControlLayerState, CanvasRasterLayerState, CanvasV2State, EntityIdentifierPayload } from './types';
import { getEntityIdentifier, initialControlNet } from './types';
const selectRasterLayerEntity = (state: CanvasV2State, id: string) =>
state.rasterLayers.entities.find((layer) => layer.id === id);
export const rasterLayersReducers = {
rasterLayerAdded: {
reducer: (
@ -38,12 +36,12 @@ export const rasterLayersReducers = {
rasterLayerRecalled: (state, action: PayloadAction<{ data: CanvasRasterLayerState }>) => {
const { data } = action.payload;
state.rasterLayers.entities.push(data);
state.selectedEntityIdentifier = { type: 'raster_layer', id: data.id };
state.selectedEntityIdentifier = getEntityIdentifier(data);
},
rasterLayerConvertedToControlLayer: {
reducer: (state, action: PayloadAction<{ id: string; newId: string }>) => {
const { id, newId } = action.payload;
const layer = selectRasterLayerEntity(state, id);
reducer: (state, action: PayloadAction<EntityIdentifierPayload<{ newId: string }, 'raster_layer'>>) => {
const { entityIdentifier, newId } = action.payload;
const layer = selectEntity(state, entityIdentifier);
if (!layer) {
return;
}
@ -58,14 +56,14 @@ export const rasterLayersReducers = {
};
// Remove the raster layer
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== id);
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => layer.id !== entityIdentifier.id);
// Add the converted control layer
state.controlLayers.entities.push(controlLayerState);
state.selectedEntityIdentifier = { type: controlLayerState.type, id: controlLayerState.id };
},
prepare: (payload: { id: string }) => ({
prepare: (payload: EntityIdentifierPayload<void, 'raster_layer'>) => ({
payload: { ...payload, newId: getPrefixedId('control_layer') },
}),
},

View File

@ -1,9 +1,11 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { selectEntity, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
import type {
CanvasV2State,
CLIPVisionModelV2,
EntityIdentifierPayload,
FillStyle,
IPMethodV2,
RegionalGuidanceIPAdapterConfig,
@ -17,22 +19,6 @@ import { assert } from 'tsafe';
import type { CanvasRegionalGuidanceState } from './types';
const selectRegionalGuidanceEntity = (state: CanvasV2State, id: string) => {
return state.regions.entities.find((rg) => rg.id === id);
};
const selectRegionalGuidanceIPAdapter = (state: CanvasV2State, id: string, ipAdapterId: string) => {
const entity = state.regions.entities.find((rg) => rg.id === id);
if (!entity) {
return;
}
return entity.ipAdapters.find((ipa) => ipa.id === ipAdapterId);
};
export const selectRegionalGuidanceEntityOrThrow = (state: CanvasV2State, id: string) => {
const rg = selectRegionalGuidanceEntity(state, id);
assert(rg, `Region with id ${id} not found`);
return rg;
};
const DEFAULT_MASK_COLORS: RgbColor[] = [
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
@ -94,42 +80,54 @@ export const regionsReducers = {
state.regions.entities.push(data);
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
},
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
rgPositivePromptChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
) => {
const { entityIdentifier, prompt } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.positivePrompt = prompt;
},
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
rgNegativePromptChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ prompt: string | null }, 'regional_guidance'>>
) => {
const { entityIdentifier, prompt } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.negativePrompt = prompt;
},
rgFillColorChanged: (state, action: PayloadAction<{ id: string; color: RgbColor }>) => {
const { id, color } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
rgFillColorChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ color: RgbColor }, 'regional_guidance'>>
) => {
const { entityIdentifier, color } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.color = color;
},
rgFillStyleChanged: (state, action: PayloadAction<{ id: string; style: FillStyle }>) => {
const { id, style } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
rgFillStyleChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ style: FillStyle }, 'regional_guidance'>>
) => {
const { entityIdentifier, style } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
entity.fill.style = style;
},
rgAutoNegativeToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload;
const rg = selectRegionalGuidanceEntity(state, id);
rgAutoNegativeToggled: (state, action: PayloadAction<EntityIdentifierPayload<void, 'regional_guidance'>>) => {
const { entityIdentifier } = action.payload;
const rg = selectEntity(state, entityIdentifier);
if (!rg) {
return;
}
@ -138,10 +136,15 @@ export const regionsReducers = {
rgIPAdapterAdded: {
reducer: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> }>
action: PayloadAction<
EntityIdentifierPayload<
{ ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> },
'regional_guidance'
>
>
) => {
const { id, overrides, ipAdapterId } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
const { entityIdentifier, overrides, ipAdapterId } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
@ -149,13 +152,18 @@ export const regionsReducers = {
merge(ipAdapter, overrides);
entity.ipAdapters.push(ipAdapter);
},
prepare: (payload: { id: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> }) => ({
prepare: (
payload: EntityIdentifierPayload<{ overrides?: Partial<RegionalGuidanceIPAdapterConfig> }, 'regional_guidance'>
) => ({
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
}),
},
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
const { id, ipAdapterId } = action.payload;
const entity = selectRegionalGuidanceEntity(state, id);
rgIPAdapterDeleted: (
state,
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string }, 'regional_guidance'>>
) => {
const { entityIdentifier, ipAdapterId } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
@ -163,18 +171,23 @@ export const regionsReducers = {
},
rgIPAdapterImageChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
action: PayloadAction<
EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
>
) => {
const { id, ipAdapterId, imageDTO } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
const { entityIdentifier, ipAdapterId, imageDTO } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
const { id, ipAdapterId, weight } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
rgIPAdapterWeightChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; weight: number }, 'regional_guidance'>>
) => {
const { entityIdentifier, ipAdapterId, weight } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@ -182,18 +195,23 @@ export const regionsReducers = {
},
rgIPAdapterBeginEndStepPctChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
action: PayloadAction<
EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
>
) => {
const { id, ipAdapterId, beginEndStepPct } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
ipAdapter.beginEndStepPct = beginEndStepPct;
},
rgIPAdapterMethodChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>) => {
const { id, ipAdapterId, method } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
rgIPAdapterMethodChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; method: IPMethodV2 }, 'regional_guidance'>>
) => {
const { entityIdentifier, ipAdapterId, method } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@ -201,14 +219,18 @@ export const regionsReducers = {
},
rgIPAdapterModelChanged: (
state,
action: PayloadAction<{
id: string;
ipAdapterId: string;
modelConfig: IPAdapterModelConfig | null;
}>
action: PayloadAction<
EntityIdentifierPayload<
{
ipAdapterId: string;
modelConfig: IPAdapterModelConfig | null;
},
'regional_guidance'
>
>
) => {
const { id, ipAdapterId, modelConfig } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
const { entityIdentifier, ipAdapterId, modelConfig } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}
@ -216,10 +238,12 @@ export const regionsReducers = {
},
rgIPAdapterCLIPVisionModelChanged: (
state,
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
action: PayloadAction<
EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
>
) => {
const { id, ipAdapterId, clipVisionModel } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, id, ipAdapterId);
const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload;
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
if (!ipAdapter) {
return;
}

View File

@ -1,7 +1,32 @@
import { createSelector } from '@reduxjs/toolkit';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import type { RootState } from 'app/store/store';
import type {
CanvasControlLayerState,
CanvasEntityIdentifier,
CanvasEntityState,
CanvasInpaintMaskState,
CanvasRasterLayerState,
CanvasRegionalGuidanceState,
CanvasV2State,
} from 'features/controlLayers/store/types';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { assert } from 'tsafe';
/**
* Selects the canvasV2 slice from the root state
*/
export const selectCanvasV2Slice = (state: RootState) => state.canvasV2;
/**
* Selects the total canvas entity count:
* - Regions
* - IP adapters
* - Raster layers
* - Control layers
* - Inpaint masks
*
* It does not check for validity of the entities.
*/
export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => {
return (
canvasV2.regions.entities.length +
@ -12,6 +37,134 @@ export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2)
);
});
/**
* Selects the optimal dimension for the canvas based on the currently-model
*/
export const selectOptimalDimension = createSelector(selectCanvasV2Slice, (canvasV2) => {
return getOptimalDimension(canvasV2.params.model);
});
/**
* Selects a single entity from the canvasV2 slice. If the entity identifier is narrowed to a specific type, the
* return type will be narrowed as well.
*/
export function selectEntity<T extends CanvasEntityIdentifier>(
state: CanvasV2State,
entityIdentifier: T
): Extract<CanvasEntityState, T> | undefined {
const { id, type } = entityIdentifier;
let entity: CanvasEntityState | undefined = undefined;
switch (type) {
case 'raster_layer':
entity = state.rasterLayers.entities.find((entity) => entity.id === id);
break;
case 'control_layer':
entity = state.controlLayers.entities.find((entity) => entity.id === id);
break;
case 'inpaint_mask':
entity = state.inpaintMasks.entities.find((entity) => entity.id === id);
break;
case 'regional_guidance':
entity = state.regions.entities.find((entity) => entity.id === id);
break;
case 'ip_adapter':
entity = state.ipAdapters.entities.find((entity) => entity.id === id);
break;
}
// This cast is safe, but TS seems to be unable to infer the type
return entity as Extract<CanvasEntityState, T>;
}
/**
* Selected an entity from the canvasV2 slice. If the entity is not found, an error is thrown.
* Wrapper around {@link selectEntity}.
*/
export function selectEntityOrThrow<T extends CanvasEntityIdentifier>(
state: CanvasV2State,
entityIdentifier: T
): Extract<CanvasEntityState, T> {
const entity = selectEntity(state, entityIdentifier);
assert(entity, `Entity with id ${entityIdentifier.id} not found`);
return entity;
}
/**
* Selects all entities of the given type.
*/
export function selectAllEntitiesOfType<T extends CanvasEntityState['type']>(
state: CanvasV2State,
type: T
): Extract<CanvasEntityState, { type: T }>[] {
let entities: CanvasEntityState[] = [];
switch (type) {
case 'raster_layer':
entities = state.rasterLayers.entities;
break;
case 'control_layer':
entities = state.controlLayers.entities;
break;
case 'inpaint_mask':
entities = state.inpaintMasks.entities;
break;
case 'regional_guidance':
entities = state.regions.entities;
break;
case 'ip_adapter':
entities = state.ipAdapters.entities;
break;
}
// This cast is safe, but TS seems to be unable to infer the type
return entities as Extract<CanvasEntityState, { type: T }>[];
}
/**
* Selects all entities, in the order they are displayed in the list.
*/
export function selectAllEntities(state: CanvasV2State): CanvasEntityState[] {
// These are in the same order as they are displayed in the list!
return [
...state.inpaintMasks.entities.toReversed(),
...state.regions.entities.toReversed(),
...state.ipAdapters.entities.toReversed(),
...state.controlLayers.entities.toReversed(),
...state.rasterLayers.entities.toReversed(),
];
}
/**
* Selects all _renderable_ entities:
* - Raster layers
* - Control layers
* - Inpaint masks
* - Regional guidance
*/
export function selectAllRenderableEntities(
state: CanvasV2State
): (CanvasRasterLayerState | CanvasControlLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState)[] {
return [
...state.rasterLayers.entities,
...state.controlLayers.entities,
...state.inpaintMasks.entities,
...state.regions.entities,
];
}
/**
* Selects the IP adapter for the specific Regional Guidance layer.
*/
export function selectRegionalGuidanceIPAdapter(
state: CanvasV2State,
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>,
ipAdapterId: string
) {
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return undefined;
}
return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
}

View File

@ -1,3 +1,4 @@
import type { SerializableObject } from 'common/types';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
@ -692,7 +693,9 @@ export type CanvasEntityState =
| CanvasRegionalGuidanceState
| CanvasInpaintMaskState
| CanvasIPAdapterState;
export type CanvasEntityIdentifier = Pick<CanvasEntityState, 'id' | 'type'>;
export type CanvasEntityType = CanvasEntityState['type'];
export type CanvasEntityIdentifier<T extends CanvasEntityType = CanvasEntityType> = { id: string; type: T };
export type LoRA = {
id: string;
@ -819,7 +822,17 @@ export type StageAttrs = {
scale: number;
};
export type EntityIdentifierPayload<T = object> = { entityIdentifier: CanvasEntityIdentifier } & T;
export type EntityIdentifierPayload<
T extends SerializableObject | void = void,
U extends CanvasEntityType = CanvasEntityType,
> = T extends void
? {
entityIdentifier: CanvasEntityIdentifier<U>;
}
: {
entityIdentifier: CanvasEntityIdentifier<U>;
} & T;
export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>;
export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;
export type EntityEraserLineAddedPayload = EntityIdentifierPayload<{ eraserLine: CanvasEraserLineState }>;
@ -858,6 +871,8 @@ export function isDrawableEntity(
return isDrawableEntityType(entity.type);
}
export const getEntityIdentifier = (entity: CanvasEntityState): CanvasEntityIdentifier => {
export const getEntityIdentifier = <T extends CanvasEntityType>(
entity: Extract<CanvasEntityState, { type: T }>
): CanvasEntityIdentifier<T> => {
return { id: entity.id, type: entity.type };
};

View File

@ -1,7 +1,7 @@
import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
import {

View File

@ -1,5 +1,5 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';

View File

@ -13,7 +13,7 @@ import {
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import type { ImageUsage } from 'features/deleteImageModal/store/types';

View File

@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { LoRACard } from 'features/lora/components/LoRACard';
import { memo } from 'react';

View File

@ -4,7 +4,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { loraAdded, selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { loraAdded } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLoRAModels } from 'services/api/hooks/modelsByType';

View File

@ -1,7 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt';
import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt';
import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt';

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

View File

@ -3,7 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier';
import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip';
import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis';

View File

@ -3,7 +3,7 @@ import { Box, Expander, Flex, FormControlGroup, StandaloneAccordion } from '@inv
import { EMPTY_ARRAY } from 'app/store/constants';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import { LoRAList } from 'features/lora/components/LoRAList';
import LoRASelect from 'features/lora/components/LoRASelect';
import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale';

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

View File

@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library';
import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { selectCanvasV2Slice } from 'features/controlLayers/store/selectors';
import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale';
import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect';
import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore';