refactor(ui): canvas v2 (wip)

Redo all UI components for different canvas entity types
This commit is contained in:
psychedelicious 2024-06-15 11:38:24 +10:00
parent ba66d7c9a6
commit cb69872dd3
54 changed files with 1196 additions and 1362 deletions

View File

@ -22,3 +22,4 @@ export const getSelectorsOptions: GetSelectorsOptions = {
}; };
export const createAppSelector = createSelector.withTypes<RootState>(); export const createAppSelector = createSelector.withTypes<RootState>();
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();

View File

@ -0,0 +1,29 @@
import { useDisclosure } from '@invoke-ai/ui-library';
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 { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const CA = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'control_adapter' }));
}, [dispatch, id]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}>
<CAHeader id={id} onToggleVisibility={onToggle} />
{isOpen && <CASettings id={id} />}
</CanvasEntityContainer>
);
});
CA.displayName = 'CA';

View File

@ -0,0 +1,87 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import {
caDeleted,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
selectCAOrThrow,
selectControlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
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,
};
}
);
export const CAActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const validActions = useAppSelector((s) => selectValidActions(s, id));
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(caMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(caMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(caMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(caMovedToBack({ id }));
}, [dispatch, id]);
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
);
});
CAActionsMenu.displayName = 'CAActionsMenu';

View File

@ -1,35 +0,0 @@
import { Flex, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CAHeaderItems } from 'features/controlLayers/components/ControlAdapter/CAHeaderItems';
import { CASettings } from 'features/controlLayers/components/ControlAdapter/CASettings';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const CAEntity = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => {
dispatch(entitySelected({ id, type: 'control_adapter' }));
}, [dispatch, id]);
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<CAHeaderItems id={id} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<CASettings id={id} />
</Flex>
)}
</LayerWrapper>
);
});
CAEntity.displayName = 'CAEntity';

View File

@ -0,0 +1,41 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { 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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
onToggleVisibility: () => void;
};
export const CAHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(caIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.globalControlAdapter')} />
<Spacer />
<CAOpacityAndFilter id={id} />
<CAActionsMenu id={id} />
<CanvasEntityDeleteButton onDelete={onDelete} />
</CanvasEntityHeader>
);
});
CAHeader.displayName = 'CAEntityHeader';

View File

@ -1,103 +0,0 @@
import { Menu, MenuItem, MenuList, Spacer } from '@invoke-ai/ui-library';
import { createAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CAOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/CAOpacityAndFilter';
import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle';
import { EntityMenuButton } from 'features/controlLayers/components/LayerCommon/EntityMenuButton';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle';
import {
caDeleted,
caIsEnabledToggled,
caMovedBackwardOne,
caMovedForwardOne,
caMovedToBack,
caMovedToFront,
selectCAOrThrow,
selectControlAdaptersV2Slice,
} from 'features/controlLayers/store/controlAdaptersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
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,
};
}
);
export const CAHeaderItems = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const validActions = useAppSelector((s) => selectValidActions(s, id));
const isEnabled = useAppSelector((s) => selectCAOrThrow(s.controlAdaptersV2, id).isEnabled);
const onToggle = useCallback(() => {
dispatch(caIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(caDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(caMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(caMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(caMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(caMovedToBack({ id }));
}, [dispatch, id]);
return (
<>
<EntityEnabledToggle isEnabled={isEnabled} onToggle={onToggle} />
<EntityTitle title={t('controlLayers.globalControlAdapter')} />
<Spacer />
<CAOpacityAndFilter id={id} />
<Menu>
<EntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
<EntityDeleteButton onDelete={onDelete} />
</>
);
});
CAHeaderItems.displayName = 'CAHeaderItems';

View File

@ -1,6 +1,7 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { Weight } from 'features/controlLayers/components/common/Weight'; import { Weight } from 'features/controlLayers/components/common/Weight';
import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect';
import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview';
@ -95,58 +96,60 @@ export const CASettings = memo(({ id }: Props) => {
const postUploadAction = useMemo<CAImagePostUploadAction>(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); const postUploadAction = useMemo<CAImagePostUploadAction>(() => ({ id, type: 'SET_CA_IMAGE' }), [id]);
return ( return (
<Flex flexDir="column" gap={3} position="relative" w="full"> <CanvasEntitySettings>
<Flex gap={3} alignItems="center" w="full"> <Flex flexDir="column" gap={3} position="relative" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s"> <Flex gap={3} alignItems="center" w="full">
<CAModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} /> <Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
</Box> <CAModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
</Box>
<IconButton <IconButton
size="sm" size="sm"
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')} tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')} aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
onClick={toggleIsExpanded} onClick={toggleIsExpanded}
variant="ghost" variant="ghost"
icon={ icon={
<Icon <Icon
boxSize={4} boxSize={4}
as={PiCaretUpBold} as={PiCaretUpBold}
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'} transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common" transitionProperty="common"
transitionDuration="normal" transitionDuration="normal"
/> />
} }
/>
</Flex>
<Flex gap={3} w="full">
<Flex flexDir="column" gap={3} w="full" h="full">
{controlAdapter.controlMode && (
<CAControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
)}
<Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<CAImagePreview
controlAdapter={controlAdapter}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/> />
</Flex> </Flex>
</Flex> <Flex gap={3} w="full">
{isExpanded && ( <Flex flexDir="column" gap={3} w="full" h="full">
<> {controlAdapter.controlMode && (
<Divider /> <CAControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
<Flex flexDir="column" gap={3} w="full"> )}
<CAProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} /> <Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
<CAProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} /> <BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex> </Flex>
</> <Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
)} <CAImagePreview
</Flex> controlAdapter={controlAdapter}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/>
</Flex>
</Flex>
{isExpanded && (
<>
<Divider />
<Flex flexDir="column" gap={3} w="full">
<CAProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
<CAProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
</Flex>
</>
)}
</Flex>
</CanvasEntitySettings>
); );
}); });

View File

@ -9,7 +9,7 @@ import { CALayer } from 'features/controlLayers/components/CALayer/CALayer';
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer'; import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer';
import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { Layer } from 'features/controlLayers/components/RasterLayer/RasterLayer';
import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import type { LayerData } from 'features/controlLayers/store/types'; import type { LayerData } from 'features/controlLayers/store/types';
@ -67,7 +67,7 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
return <IILayer key={id} layerId={id} />; return <IILayer key={id} layerId={id} />;
} }
if (type === 'raster_layer') { if (type === 'raster_layer') {
return <RasterLayer key={id} layerId={id} />; return <Layer key={id} layerId={id} />;
} }
}); });

View File

@ -11,7 +11,7 @@ import {
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice';
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity'; import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -20,8 +20,8 @@ import { RiSettings4Fill } from 'react-icons/ri';
const ControlLayersSettingsPopover = () => { const ControlLayersSettingsPopover = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( const onChangeInvertScroll = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), (e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
[dispatch] [dispatch]
); );
@ -33,13 +33,10 @@ const ControlLayersSettingsPopover = () => {
<PopoverContent> <PopoverContent>
<PopoverBody> <PopoverBody>
<Flex direction="column" gap={2}> <Flex direction="column" gap={2}>
<GlobalMaskLayerOpacity /> <RGGlobalOpacity />
<FormControl w="full"> <FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel> <FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
<Checkbox <Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />
isChecked={shouldInvertBrushSizeScrollDirection}
onChange={handleChangeShouldInvertBrushSizeScrollDirection}
/>
</FormControl> </FormControl>
</Flex> </Flex>
</PopoverBody> </PopoverBody>

View File

@ -1,92 +0,0 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InitialImagePreview } from 'features/controlLayers/components/IILayer/InitialImagePreview';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import {
iiLayerDenoisingStrengthChanged,
iiLayerImageChanged,
layerSelected,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isInitialImageLayer } from 'features/controlLayers/store/types';
import type { IILayerImageDropData } from 'features/dnd/types';
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
import { memo, useCallback, useMemo } from 'react';
import type { IILayerImagePostUploadAction, ImageDTO } from 'services/api/types';
type Props = {
layerId: string;
};
export const IILayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const layer = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, layerId, isInitialImageLayer));
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
},
[dispatch, layerId]
);
const onChangeDenoisingStrength = useCallback(
(denoisingStrength: number) => {
dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength }));
},
[dispatch, layerId]
);
const droppableData = useMemo<IILayerImageDropData>(
() => ({
actionType: 'SET_II_LAYER_IMAGE',
context: {
layerId,
},
id: layerId,
}),
[layerId]
);
const postUploadAction = useMemo<IILayerImagePostUploadAction>(
() => ({
layerId,
type: 'SET_II_LAYER_IMAGE',
}),
[layerId]
);
return (
<LayerWrapper onClick={onClick} borderColor={layer.isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="initial_image_layer" />
<Spacer />
<LayerOpacity layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<ImageToImageStrength value={layer.denoisingStrength} onChange={onChangeDenoisingStrength} />
<InitialImagePreview
image={layer.image}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
)}
</LayerWrapper>
);
});
IILayer.displayName = 'IILayer';

View File

@ -1,111 +0,0 @@
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
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 { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
import type { ImageWithDims } from 'features/controlLayers/util/controlAdapters';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = {
image: ImageWithDims | null;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
};
export const InitialImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isConnected = useAppSelector((s) => s.system.isConnected);
const activeTabName = useAppSelector(activeTabNameSelector);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken);
const onReset = useCallback(() => {
onChangeImage(null);
}, [onChangeImage]);
const onUseSize = useCallback(() => {
if (!imageDTO) {
return;
}
if (activeTabName === 'canvas') {
dispatch(setBoundingBoxDimensions({ width: imageDTO.width, height: imageDTO.height }, optimalDimension));
} else {
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = imageDTO;
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
imageDTO.width / imageDTO.height,
optimalDimension * optimalDimension
);
dispatch(widthChanged({ width, ...options }));
dispatch(heightChanged({ height, ...options }));
}
}
}, [imageDTO, activeTabName, dispatch, optimalDimension, shift]);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (imageDTO) {
return {
id: 'initial_image_layer',
payloadType: 'IMAGE_DTO',
payload: { imageDTO: imageDTO },
};
}
}, [imageDTO]);
useEffect(() => {
if (isConnected && isErrorControlImage) {
onReset();
}
}, [onReset, isConnected, isErrorControlImage]);
return (
<Flex w="full" alignItems="center" justifyContent="center">
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={imageDTO}
postUploadAction={postUploadAction}
/>
{imageDTO && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={onReset}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={onUseSize}
icon={<PiRulerBold size={16} />}
tooltip={
shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
}
/>
</Flex>
)}
</Flex>
</Flex>
);
});
InitialImagePreview.displayName = 'InitialImagePreview';

View File

@ -0,0 +1,29 @@
import { useDisclosure } from '@invoke-ai/ui-library';
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 { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const IPA = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'ip_adapter' }));
}, [dispatch, id]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}>
<IPAHeader id={id} onToggleVisibility={onToggle} />
{isOpen && <IPASettings id={id} />}
</CanvasEntityContainer>
);
});
IPA.displayName = 'IPA';

View File

@ -1,35 +0,0 @@
import { Flex, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPAHeaderItems } from 'features/controlLayers/components/IPAdapter/IPAHeaderItems';
import { IPASettings } from 'features/controlLayers/components/IPAdapter/IPASettings';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const IPAEntity = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => {
dispatch(entitySelected({ id, type: 'ip_adapter' }));
}, [dispatch, id]);
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<IPAHeaderItems id={id} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
<IPASettings id={id} />
</Flex>
)}
</LayerWrapper>
);
});
IPAEntity.displayName = 'IPAEntity';

View File

@ -0,0 +1,37 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
onToggleVisibility: () => void;
};
export const IPAHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(ipaIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(ipaDeleted({ id }));
}, [dispatch, id]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.ipAdapter')} />
<Spacer />
<CanvasEntityDeleteButton onDelete={onDelete} />
</CanvasEntityHeader>
);
});
IPAHeader.displayName = 'IPAHeader';

View File

@ -1,39 +0,0 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { EntityDeleteButton } from 'features/controlLayers/components/LayerCommon/EntityDeleteButton';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/EntityEnabledToggle';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/EntityTitle';
import {
ipaDeleted,
ipaIsEnabledToggled,
selectIPAOrThrow,
} from 'features/controlLayers/store/ipAdaptersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
};
export const IPAHeaderItems = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectIPAOrThrow(s.ipAdapters, id).isEnabled);
const onToggle = useCallback(() => {
dispatch(ipaIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(ipaDeleted({ id }));
}, [dispatch, id]);
return (
<>
<EntityEnabledToggle isEnabled={isEnabled} onToggle={onToggle} />
<EntityTitle title={t('controlLayers.ipAdapter')} />
<Spacer />
<EntityDeleteButton onDelete={onDelete} />
</>
);
});
IPAHeaderItems.displayName = 'IPAHeaderItems';

View File

@ -1,6 +1,7 @@
import { Box, Flex } from '@invoke-ai/ui-library'; import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { Weight } from 'features/controlLayers/components/common/Weight'; import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod';
import { import {
@ -70,52 +71,40 @@ export const IPASettings = memo(({ id }: Props) => {
[dispatch, id] [dispatch, id]
); );
const droppableData = useMemo<IPAImageDropData>( const droppableData = useMemo<IPAImageDropData>(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]);
() => ({ const postUploadAction = useMemo<IPALayerImagePostUploadAction>(() => ({ type: 'SET_IPA_IMAGE', id }), [id]);
actionType: 'SET_IPA_IMAGE',
context: { id },
id,
}),
[id]
);
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
() => ({
type: 'SET_IPA_IMAGE',
id,
}),
[id]
);
return ( return (
<Flex flexDir="column" gap={4} position="relative" w="full"> <CanvasEntitySettings>
<Flex gap={3} alignItems="center" w="full"> <Flex flexDir="column" gap={4} position="relative" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s"> <Flex gap={3} alignItems="center" w="full">
<IPAModelCombobox <Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
modelKey={ipAdapter.model?.key ?? null} <IPAModelCombobox
onChangeModel={onChangeModel} modelKey={ipAdapter.model?.key ?? null}
clipVisionModel={ipAdapter.clipVisionModel} onChangeModel={onChangeModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel} clipVisionModel={ipAdapter.clipVisionModel}
/> onChangeCLIPVisionModel={onChangeCLIPVisionModel}
</Box> />
</Flex> </Box>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
<IPAMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex> </Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1"> <Flex gap={4} w="full" alignItems="center">
<IPAImagePreview <Flex flexDir="column" gap={3} w="full">
image={ipAdapter.image} <IPAMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
onChangeImage={onChangeImage} <Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
ipAdapterId={ipAdapter.id} <BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
droppableData={droppableData} </Flex>
postUploadAction={postUploadAction} <Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
/> <IPAImagePreview
image={ipAdapter.image}
onChangeImage={onChangeImage}
ipAdapterId={ipAdapter.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </CanvasEntitySettings>
); );
}); });

View File

@ -0,0 +1,29 @@
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { LayerHeader } from 'features/controlLayers/components/RasterLayer/LayerHeader';
import { LayerSettings } from 'features/controlLayers/components/RasterLayer/LayerSettings';
import { entitySelected } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const Layer = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'layer' }));
}, [dispatch, id]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect}>
<LayerHeader id={id} onToggleVisibility={onToggle} />
{isOpen && <LayerSettings id={id} />}
</CanvasEntityContainer>
);
});
Layer.displayName = 'Layer';

View File

@ -0,0 +1,87 @@
import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import {
layerDeleted,
layerMovedBackwardOne,
layerMovedForwardOne,
layerMovedToBack,
layerMovedToFront,
selectLayerOrThrow,
selectLayersSlice,
} from 'features/controlLayers/store/layersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
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,
};
}
);
export const LayerActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const validActions = useAppSelector((s) => selectValidActions(s, id));
const onDelete = useCallback(() => {
dispatch(layerDeleted({ id }));
}, [dispatch, id]);
const moveForwardOne = useCallback(() => {
dispatch(layerMovedForwardOne({ id }));
}, [dispatch, id]);
const moveToFront = useCallback(() => {
dispatch(layerMovedToFront({ id }));
}, [dispatch, id]);
const moveBackwardOne = useCallback(() => {
dispatch(layerMovedBackwardOne({ id }));
}, [dispatch, id]);
const moveToBack = useCallback(() => {
dispatch(layerMovedToBack({ id }));
}, [dispatch, id]);
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForwardOne} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackwardOne} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
);
});
LayerActionsMenu.displayName = 'LayerActionsMenu';

View File

@ -0,0 +1,42 @@
import { Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { LayerActionsMenu } from 'features/controlLayers/components/Layer/LayerActionsMenu';
import { layerDeleted, layerIsEnabledToggled, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { LayerOpacity } from './LayerOpacity';
type Props = {
id: string;
onToggleVisibility: () => void;
};
export const LayerHeader = memo(({ id, onToggleVisibility }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.layers, id).isEnabled);
const onToggleIsEnabled = useCallback(() => {
dispatch(layerIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(layerDeleted({ id }));
}, [dispatch, id]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.layer')} />
<Spacer />
<LayerOpacity id={id} />
<LayerActionsMenu id={id} />
<CanvasEntityDeleteButton onDelete={onDelete} />
</CanvasEntityHeader>
);
});
LayerHeader.displayName = 'LayerHeader';

View File

@ -11,43 +11,29 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation'; import { stopPropagation } from 'common/util/stopPropagation';
import { import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice';
layerOpacityChanged, import { memo, useCallback } from 'react';
selectCanvasV2Slice,
selectLayerOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import { isLayerWithOpacity } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi'; import { PiDropHalfFill } from 'react-icons/pi';
type Props = { type Props = {
layerId: string; id: string;
}; };
const marks = [0, 25, 50, 75, 100]; const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`; const formatPct = (v: number | string) => `${v} %`;
export const LayerOpacity = memo(({ layerId }: Props) => { export const LayerOpacity = memo(({ id }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selectOpacity = useMemo( const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100));
() =>
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity);
return Math.round(layer.opacity * 100);
}),
[layerId]
);
const opacity = useAppSelector(selectOpacity);
const onChangeOpacity = useCallback( const onChangeOpacity = useCallback(
(v: number) => { (v: number) => {
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); dispatch(layerOpacityChanged({ id, opacity: v / 100 }));
}, },
[dispatch, layerId] [dispatch, id]
); );
return ( return (
<Popover isLazy> <Popover isLazy>

View File

@ -0,0 +1,24 @@
import IAIDroppable from 'common/components/IAIDroppable';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import type { LayerImageDropData } from 'features/dnd/types';
import { memo, useMemo } from 'react';
type Props = {
id: string;
};
export const LayerSettings = memo(({ id }: Props) => {
const droppableData = useMemo<LayerImageDropData>(
() => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }),
[id]
);
return (
<CanvasEntitySettings>
PLACEHOLDER
<IAIDroppable data={droppableData} />
</CanvasEntitySettings>
);
});
LayerSettings.displayName = 'LayerSettings';

View File

@ -1,71 +0,0 @@
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerCommon/LayerMenuArrangeActions';
import { LayerMenuRGActions } from 'features/controlLayers/components/LayerCommon/LayerMenuRGActions';
import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks';
import { layerDeleted, layerReset } from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold } from 'react-icons/pi';
type Props = { layerId: string };
export const EntityMenu = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const layerType = useLayerType(layerId);
const resetLayer = useCallback(() => {
dispatch(layerReset(layerId));
}, [dispatch, layerId]);
const deleteLayer = useCallback(() => {
dispatch(layerDeleted(layerId));
}, [dispatch, layerId]);
const shouldShowArrangeActions = useMemo(() => {
return (
layerType === 'regional_guidance_layer' ||
layerType === 'control_adapter_layer' ||
layerType === 'initial_image_layer' ||
layerType === 'raster_layer'
);
}, [layerType]);
const shouldShowResetAction = useMemo(() => {
return layerType === 'regional_guidance_layer' || layerType === 'raster_layer';
}, [layerType]);
return (
<Menu>
<MenuButton
as={IconButton}
aria-label="Layer menu"
size="sm"
icon={<PiDotsThreeVerticalBold />}
onDoubleClick={stopPropagation} // double click expands the layer
/>
<MenuList>
{layerType === 'regional_guidance_layer' && (
<>
<LayerMenuRGActions layerId={layerId} />
<MenuDivider />
</>
)}
{shouldShowArrangeActions && (
<>
<LayerMenuArrangeActions layerId={layerId} />
<MenuDivider />
</>
)}
{shouldShowResetAction && (
<MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
)}
<MenuItem onClick={deleteLayer} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
);
});
EntityMenu.displayName = 'EntityMenu';

View File

@ -1,69 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
layerMovedBackward,
layerMovedForward,
layerMovedToBack,
layerMovedToFront,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { isRenderableLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = { layerId: string };
export const LayerMenuArrangeActions = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`);
const layerIndex = canvasV2.layers.findIndex((l) => l.id === layerId);
const layerCount = canvasV2.layers.length;
return {
canMoveForward: layerIndex < layerCount - 1,
canMoveBackward: layerIndex > 0,
canMoveToFront: layerIndex < layerCount - 1,
canMoveToBack: layerIndex > 0,
};
}),
[layerId]
);
const validActions = useAppSelector(selectValidActions);
const moveForward = useCallback(() => {
dispatch(layerMovedForward(layerId));
}, [dispatch, layerId]);
const moveToFront = useCallback(() => {
dispatch(layerMovedToFront(layerId));
}, [dispatch, layerId]);
const moveBackward = useCallback(() => {
dispatch(layerMovedBackward(layerId));
}, [dispatch, layerId]);
const moveToBack = useCallback(() => {
dispatch(layerMovedToBack(layerId));
}, [dispatch, layerId]);
return (
<>
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={moveForward} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={moveBackward} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
</>
);
});
LayerMenuArrangeActions.displayName = 'LayerMenuArrangeActions';

View File

@ -1,56 +0,0 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
selectCanvasV2Slice,
} from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = { layerId: string };
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [addIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(layerId);
const selectValidActions = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return {
canAddPositivePrompt: layer.positivePrompt === null,
canAddNegativePrompt: layer.negativePrompt === null,
};
}),
[layerId]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
const addNegativePrompt = useCallback(() => {
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: '' }));
}, [dispatch, layerId]);
return (
<>
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt} icon={<PiPlusBold />}>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt} icon={<PiPlusBold />}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />} isDisabled={isAddIPAdapterDisabled}>
{t('controlLayers.addIPAdapter')}
</MenuItem>
</>
);
});
LayerMenuRGActions.displayName = 'LayerMenuRGActions';

View File

@ -1,31 +0,0 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{
onClick?: () => void;
borderColor: ChakraProps['bg'];
}>;
export const LayerWrapper = memo(({ onClick, borderColor, children }: Props) => {
return (
<Flex
position="relative"
gap={2}
onClick={onClick}
bg={borderColor}
px={2}
borderRadius="base"
py="1px"
transitionProperty="all"
transitionDuration="0.2s"
>
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
{children}
</Flex>
</Flex>
);
});
LayerWrapper.displayName = 'LayerWrapper';

View File

@ -1,24 +1,19 @@
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice';
globalMaskLayerOpacityChanged,
initialControlLayersState,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const marks = [0, 25, 50, 75, 100]; const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`; const formatPct = (v: number | string) => `${v} %`;
export const GlobalMaskLayerOpacity = memo(() => { export const RGGlobalOpacity = memo(() => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const globalMaskLayerOpacity = useAppSelector((s) => const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100));
Math.round(s.canvasV2.globalMaskLayerOpacity * 100)
);
const onChange = useCallback( const onChange = useCallback(
(v: number) => { (v: number) => {
dispatch(globalMaskLayerOpacityChanged(v / 100)); dispatch(rgGlobalOpacityChanged({ opacity: v / 100 }));
}, },
[dispatch] [dispatch]
); );
@ -30,8 +25,8 @@ export const GlobalMaskLayerOpacity = memo(() => {
min={0} min={0}
max={100} max={100}
step={1} step={1}
value={globalMaskLayerOpacity} value={opacity}
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} defaultValue={0.3}
onChange={onChange} onChange={onChange}
marks={marks} marks={marks}
minW={48} minW={48}
@ -40,8 +35,8 @@ export const GlobalMaskLayerOpacity = memo(() => {
min={0} min={0}
max={100} max={100}
step={1} step={1}
value={globalMaskLayerOpacity} value={opacity}
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} defaultValue={0.3}
onChange={onChange} onChange={onChange}
w={28} w={28}
format={formatPct} format={formatPct}
@ -51,4 +46,4 @@ export const GlobalMaskLayerOpacity = memo(() => {
); );
}); });
GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity'; RGGlobalOpacity.displayName = 'RGGlobalOpacity';

View File

@ -1,80 +0,0 @@
import { Badge, Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { rgbColorToString } from 'features/canvas/util/colorToString';
import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
import { RGLayerColorPicker } from './RGLayerColorPicker';
import { RGLayerIPAdapterList } from './RGLayerIPAdapterList';
import { RGLayerNegativePrompt } from './RGLayerNegativePrompt';
import { RGLayerPositivePrompt } from './RGLayerPositivePrompt';
import RGLayerSettingsPopover from './RGLayerSettingsPopover';
type Props = {
layerId: string;
};
export const RGLayer = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selector = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return {
color: rgbColorToString(layer.previewColor),
hasPositivePrompt: layer.positivePrompt !== null,
hasNegativePrompt: layer.negativePrompt !== null,
hasIPAdapters: layer.ipAdapters.length > 0,
isSelected: layerId === canvasV2.selectedLayerId,
autoNegative: layer.autoNegative,
};
}),
[layerId]
);
const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } =
useAppSelector(selector);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? color : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="regional_guidance_layer" />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')}
</Badge>
)}
<RGLayerColorPicker layerId={layerId} />
<RGLayerSettingsPopover layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons id={layerId} />}
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
</Flex>
)}
</LayerWrapper>
);
});
RGLayer.displayName = 'RGLayer';

View File

@ -1,48 +0,0 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { regionalGuidanceAutoNegativeChanged, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
layerId: string;
};
const useAutoNegative = (layerId: string) => {
const selectAutoNegative = useMemo(
() =>
createSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
return layer.autoNegative;
}),
[layerId]
);
const autoNegative = useAppSelector(selectAutoNegative);
return autoNegative;
};
export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoNegative = useAutoNegative(layerId);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' }));
},
[dispatch, layerId]
);
return (
<FormControl gap={2}>
<FormLabel m={0}>{t('controlLayers.autoNegative')}</FormLabel>
<Checkbox size="md" isChecked={autoNegative === 'invert'} onChange={onChange} />
</FormControl>
);
});
RGLayerAutoNegativeCheckbox.displayName = 'RGLayerAutoNegativeCheckbox';

View File

@ -1,66 +0,0 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
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, selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
layerId: string;
};
export const RGLayerColorPicker = memo(({ layerId }: Props) => {
const { t } = useTranslation();
const selectColor = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.find((l) => l.id === layerId);
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
return layer.previewColor;
}),
[layerId]
);
const color = useAppSelector(selectColor);
const dispatch = useAppDispatch();
const onColorChange = useCallback(
(color: RgbColor) => {
dispatch(rgFillChanged({ layerId, color }));
},
[dispatch, layerId]
);
return (
<Popover isLazy>
<PopoverTrigger>
<span>
<Tooltip label={t('controlLayers.maskPreviewColor')}>
<Flex
as="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="base"
borderWidth={1}
bg={rgbColorToString(color)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</Tooltip>
</span>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={color} onChange={onColorChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</Popover>
);
});
RGLayerColorPicker.displayName = 'RGLayerColorPicker';

View File

@ -1,46 +0,0 @@
import { Divider, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { RGLayerIPAdapterWrapper } from 'features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper';
import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { assert } from 'tsafe';
type Props = {
layerId: string;
};
export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
const selectIPAdapterIds = useMemo(
() =>
createMemoizedSelector(selectCanvasV2Slice, (controlLayers) => {
const layer = canvasV2.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
assert(layer, `Layer ${layerId} not found`);
return layer.ipAdapters;
}),
[layerId]
);
const ipAdapters = useAppSelector(selectIPAdapterIds);
if (ipAdapters.length === 0) {
return null;
}
return (
<>
{ipAdapters.map(({ id }, index) => (
<Flex flexDir="column" key={id}>
{index > 0 && (
<Flex pb={3}>
<Divider />
</Flex>
)}
<RGLayerIPAdapterWrapper layerId={layerId} ipAdapterId={id} ipAdapterNumber={index + 1} />
</Flex>
))}
</>
);
});
RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList';

View File

@ -1,131 +0,0 @@
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IPAdapter } from 'features/controlLayers/components/ControlAndIPAdapter/IPAdapter';
import {
regionalGuidanceIPAdapterBeginEndStepPctChanged,
regionalGuidanceIPAdapterCLIPVisionModelChanged,
regionalGuidanceIPAdapterDeleted,
regionalGuidanceIPAdapterImageChanged,
regionalGuidanceIPAdapterMethodChanged,
regionalGuidanceIPAdapterModelChanged,
regionalGuidanceIPAdapterWeightChanged,
selectRGLayerIPAdapterOrThrow,
} from 'features/controlLayers/store/controlLayersSlice';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/util/controlAdapters';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
type Props = {
layerId: string;
ipAdapterId: string;
ipAdapterNumber: number;
};
export const RGLayerIPAdapterWrapper = memo(({ layerId, ipAdapterId, ipAdapterNumber }: Props) => {
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(regionalGuidanceIPAdapterDeleted({ layerId, ipAdapterId }));
}, [dispatch, ipAdapterId, layerId]);
const ipAdapter = useAppSelector((s) => selectRGLayerIPAdapterOrThrow(s.canvasV2, layerId, ipAdapterId));
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(
regionalGuidanceIPAdapterBeginEndStepPctChanged({
layerId,
ipAdapterId,
beginEndStepPct,
})
);
},
[dispatch, ipAdapterId, layerId]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(regionalGuidanceIPAdapterWeightChanged({ layerId, ipAdapterId, weight }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(regionalGuidanceIPAdapterMethodChanged({ layerId, ipAdapterId, method }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(regionalGuidanceIPAdapterModelChanged({ layerId, ipAdapterId, modelConfig }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(regionalGuidanceIPAdapterCLIPVisionModelChanged({ layerId, ipAdapterId, clipVisionModel }));
},
[dispatch, ipAdapterId, layerId]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(regionalGuidanceIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
},
[dispatch, ipAdapterId, layerId]
);
const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({
actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
context: {
layerId,
ipAdapterId,
},
id: layerId,
}),
[ipAdapterId, layerId]
);
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({
type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE',
layerId,
ipAdapterId,
}),
[ipAdapterId, layerId]
);
return (
<Flex flexDir="column" gap={3}>
<Flex alignItems="center" gap={3}>
<Text fontWeight="semibold" color="base.400">{`IP Adapter ${ipAdapterNumber}`}</Text>
<Spacer />
<IconButton
size="sm"
icon={<PiTrashSimpleBold />}
aria-label="Delete IP Adapter"
onClick={onDeleteIPAdapter}
variant="ghost"
colorScheme="error"
/>
</Flex>
<IPAdapter
ipAdapter={ipAdapter}
onChangeBeginEndStepPct={onChangeBeginEndStepPct}
onChangeWeight={onChangeWeight}
onChangeIPMethod={onChangeIPMethod}
onChangeModel={onChangeModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
);
});
RGLayerIPAdapterWrapper.displayName = 'RGLayerIPAdapterWrapper';

View File

@ -1,38 +0,0 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
regionalGuidanceNegativePromptChanged,
regionalGuidancePositivePromptChanged,
} from 'features/controlLayers/store/controlLayersSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
type Props = {
layerId: string;
polarity: 'positive' | 'negative';
};
export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
if (polarity === 'positive') {
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: null }));
} else {
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: null }));
}
}, [dispatch, layerId, polarity]);
return (
<Tooltip label={t('controlLayers.deletePrompt')}>
<IconButton
variant="promptOverlay"
aria-label={t('controlLayers.deletePrompt')}
icon={<PiTrashSimpleBold />}
onClick={onClick}
/>
</Tooltip>
);
});
RGLayerPromptDeleteButton.displayName = 'RGLayerPromptDeleteButton';

View File

@ -1,55 +0,0 @@
import type { FormLabelProps } from '@invoke-ai/ui-library';
import {
Flex,
FormControlGroup,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { stopPropagation } from 'common/util/stopPropagation';
import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixBold } from 'react-icons/pi';
type Props = {
layerId: string;
};
const formLabelProps: FormLabelProps = {
flexGrow: 1,
minW: 32,
};
const RGLayerSettingsPopover = ({ layerId }: Props) => {
const { t } = useTranslation();
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
tooltip={t('common.settingsLabel')}
aria-label={t('common.settingsLabel')}
size="sm"
icon={<PiGearSixBold />}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControlGroup formLabelProps={formLabelProps}>
<RGLayerAutoNegativeCheckbox layerId={layerId} />
</FormControlGroup>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(RGLayerSettingsPopover);

View File

@ -1,58 +0,0 @@
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDroppable from 'common/components/IAIDroppable';
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
import { EntityMenu } from 'features/controlLayers/components/LayerCommon/LayerMenu';
import { LayerOpacity } from 'features/controlLayers/components/LayerCommon/LayerOpacity';
import { EntityTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
import { EntityEnabledToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
import { layerSelected, selectLayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
import { isRasterLayer } from 'features/controlLayers/store/types';
import type { LayerImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
type Props = {
layerId: string;
};
export const RasterLayer = memo(({ layerId }: Props) => {
const dispatch = useAppDispatch();
const isSelected = useAppSelector(
(s) => selectLayerOrThrow(s.canvasV2, layerId, isRasterLayer).isSelected
);
const onClick = useCallback(() => {
dispatch(layerSelected(layerId));
}, [dispatch, layerId]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const droppableData = useMemo(() => {
const _droppableData: LayerImageDropData = {
id: layerId,
actionType: 'ADD_RASTER_LAYER_IMAGE',
context: { layerId },
};
return _droppableData;
}, [layerId]);
return (
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
<EntityEnabledToggle layerId={layerId} />
<EntityTitle type="raster_layer" />
<Spacer />
<LayerOpacity layerId={layerId} />
<EntityMenu layerId={layerId} />
<LayerDeleteButton layerId={layerId} />
</Flex>
{isOpen && (
<Flex flexDir="column" gap={3} px={3} pb={3}>
PLACEHOLDER
</Flex>
)}
<IAIDroppable data={droppableData} />
</LayerWrapper>
);
});
RasterLayer.displayName = 'RasterLayer';

View File

@ -0,0 +1,31 @@
import { useDisclosure } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
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 { memo, useCallback } from 'react';
type Props = {
id: string;
};
export const RG = memo(({ id }: Props) => {
const dispatch = useAppDispatch();
const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.regionalGuidance, id).fill));
const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
const onSelect = useCallback(() => {
dispatch(entitySelected({ id, type: 'regional_guidance' }));
}, [dispatch, id]);
return (
<CanvasEntityContainer isSelected={isSelected} onSelect={onSelect} selectedBorderColor={selectedBorderColor}>
<RGHeader id={id} onToggleVisibility={onToggle} />
{isOpen && <RGSettings id={id} />}
</CanvasEntityContainer>
);
});
RG.displayName = 'RG';

View File

@ -0,0 +1,119 @@
import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks';
import {
rgDeleted,
rgMovedBackwardOne,
rgMovedForwardOne,
rgMovedToBack,
rgMovedToFront,
rgNegativePromptChanged,
rgPositivePromptChanged,
rgReset,
selectRegionalGuidanceSlice,
selectRGOrThrow,
} from 'features/controlLayers/store/regionalGuidanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
PiArrowCounterClockwiseBold,
PiArrowDownBold,
PiArrowLineDownBold,
PiArrowLineUpBold,
PiArrowUpBold,
PiPlusBold,
PiTrashSimpleBold,
} from 'react-icons/pi';
type Props = {
id: string;
};
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;
return {
isMoveForwardOneDisabled: rgIndex < rgCount - 1,
isMoveBackardOneDisabled: rgIndex > 0,
isMoveToFrontDisabled: rgIndex < rgCount - 1,
isMoveToBackDisabled: rgIndex > 0,
isAddPositivePromptDisabled: rg.positivePrompt === null,
isAddNegativePromptDisabled: rg.negativePrompt === null,
};
}
);
export const RGActionsMenu = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id);
const actions = useAppSelector((s) => selectActionsValidity(s, id));
const onDelete = useCallback(() => {
dispatch(rgDeleted({ id }));
}, [dispatch, id]);
const onReset = useCallback(() => {
dispatch(rgReset({ id }));
}, [dispatch, id]);
const onMoveForwardOne = useCallback(() => {
dispatch(rgMovedForwardOne({ id }));
}, [dispatch, id]);
const onMoveToFront = useCallback(() => {
dispatch(rgMovedToFront({ id }));
}, [dispatch, id]);
const onMoveBackwardOne = useCallback(() => {
dispatch(rgMovedBackwardOne({ id }));
}, [dispatch, id]);
const onMoveToBack = useCallback(() => {
dispatch(rgMovedToBack({ id }));
}, [dispatch, id]);
const onAddPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id, prompt: '' }));
}, [dispatch, id]);
const onAddNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: '' }));
}, [dispatch, id]);
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<MenuItem onClick={onAddPositivePrompt} isDisabled={actions.isAddPositivePromptDisabled} icon={<PiPlusBold />}>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={onAddNegativePrompt} isDisabled={actions.isAddNegativePromptDisabled} icon={<PiPlusBold />}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={onAddIPAdapter} icon={<PiPlusBold />} isDisabled={isAddIPAdapterDisabled}>
{t('controlLayers.addIPAdapter')}
</MenuItem>
<MenuDivider />
<MenuItem onClick={onMoveToFront} isDisabled={actions.isMoveToFrontDisabled} icon={<PiArrowLineUpBold />}>
{t('controlLayers.moveToFront')}
</MenuItem>
<MenuItem onClick={onMoveForwardOne} isDisabled={actions.isMoveForwardOneDisabled} icon={<PiArrowUpBold />}>
{t('controlLayers.moveForward')}
</MenuItem>
<MenuItem onClick={onMoveBackwardOne} isDisabled={actions.isMoveBackardOneDisabled} icon={<PiArrowDownBold />}>
{t('controlLayers.moveBackward')}
</MenuItem>
<MenuItem onClick={onMoveToBack} isDisabled={actions.isMoveToBackDisabled} icon={<PiArrowLineDownBold />}>
{t('controlLayers.moveToBack')}
</MenuItem>
<MenuDivider />
<MenuItem onClick={onReset} icon={<PiArrowCounterClockwiseBold />}>
{t('accessibility.reset')}
</MenuItem>
<MenuItem onClick={onDelete} icon={<PiTrashSimpleBold />} color="error.300">
{t('common.delete')}
</MenuItem>
</MenuList>
</Menu>
);
});
RGActionsMenu.displayName = 'RGActionsMenu';

View File

@ -0,0 +1,24 @@
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
type Props = {
onDelete: () => void;
};
export const RGDeletePromptButton = memo(({ onDelete }: Props) => {
const { t } = useTranslation();
return (
<Tooltip label={t('controlLayers.deletePrompt')}>
<IconButton
variant="promptOverlay"
aria-label={t('controlLayers.deletePrompt')}
icon={<PiTrashSimpleBold />}
onClick={onDelete}
/>
</Tooltip>
);
});
RGDeletePromptButton.displayName = 'RGDeletePromptButton';

View File

@ -0,0 +1,50 @@
import { Badge, Spacer } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { RGActionsMenu } from 'features/controlLayers/components/RegionalGuidance/RGActionsMenu';
import { rgDeleted, rgIsEnabledToggled, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RGMaskFillColorPicker } from './RGMaskFillColorPicker';
import { RGSettingsPopover } from './RGSettingsPopover';
type Props = {
id: string;
onToggleVisibility: () => void;
};
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 onToggleIsEnabled = useCallback(() => {
dispatch(rgIsEnabledToggled({ id }));
}, [dispatch, id]);
const onDelete = useCallback(() => {
dispatch(rgDeleted({ id }));
}, [dispatch, id]);
return (
<CanvasEntityHeader onToggle={onToggleVisibility}>
<CanvasEntityEnabledToggle isEnabled={isEnabled} onToggle={onToggleIsEnabled} />
<CanvasEntityTitle title={t('controlLayers.regionalGuidance')} />
<Spacer />
{autoNegative === 'invert' && (
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
{t('controlLayers.autoNegative')}
</Badge>
)}
<RGMaskFillColorPicker id={id} />
<RGSettingsPopover id={id} />
<RGActionsMenu id={id} />
<CanvasEntityDeleteButton onDelete={onDelete} />
</CanvasEntityHeader>
);
});
RGHeader.displayName = 'RGHeader';

View File

@ -0,0 +1,139 @@
import { Box, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { IPAImagePreview } from 'features/controlLayers/components/IPAdapter/IPAImagePreview';
import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod';
import { IPAModelCombobox } from 'features/controlLayers/components/IPAdapter/IPAModelCombobox';
import {
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterCLIPVisionModelChanged,
rgIPAdapterDeleted,
rgIPAdapterImageChanged,
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterWeightChanged,
selectRGOrThrow,
} from 'features/controlLayers/store/regionalGuidanceSlice';
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { PiTrashSimpleBold } from 'react-icons/pi';
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
import { assert } from 'tsafe';
type Props = {
id: string;
ipAdapterId: string;
ipAdapterNumber: number;
};
export const RGIPAdapterSettings = memo(({ id, ipAdapterId, ipAdapterNumber }: Props) => {
const dispatch = useAppDispatch();
const onDeleteIPAdapter = useCallback(() => {
dispatch(rgIPAdapterDeleted({ id, ipAdapterId }));
}, [dispatch, ipAdapterId, id]);
const ipAdapter = useAppSelector((s) => {
const ipa = selectRGOrThrow(s.regionalGuidance, id).ipAdapters.find((ipa) => ipa.id === 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, ipAdapterId, id]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(rgIPAdapterWeightChanged({ id, ipAdapterId, weight }));
},
[dispatch, ipAdapterId, id]
);
const onChangeIPMethod = useCallback(
(method: IPMethodV2) => {
dispatch(rgIPAdapterMethodChanged({ id, ipAdapterId, method }));
},
[dispatch, ipAdapterId, id]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig) => {
dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig }));
},
[dispatch, ipAdapterId, id]
);
const onChangeCLIPVisionModel = useCallback(
(clipVisionModel: CLIPVisionModelV2) => {
dispatch(rgIPAdapterCLIPVisionModelChanged({ id, ipAdapterId, clipVisionModel }));
},
[dispatch, ipAdapterId, id]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
},
[dispatch, ipAdapterId, id]
);
const droppableData = useMemo<RGIPAdapterImageDropData>(
() => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }),
[ipAdapterId, id]
);
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }),
[ipAdapterId, id]
);
return (
<Flex flexDir="column" gap={3}>
<Flex alignItems="center" gap={3}>
<Text fontWeight="semibold" color="base.400">{`IP Adapter ${ipAdapterNumber}`}</Text>
<Spacer />
<IconButton
size="sm"
icon={<PiTrashSimpleBold />}
aria-label="Delete IP Adapter"
onClick={onDeleteIPAdapter}
variant="ghost"
colorScheme="error"
/>
</Flex>
<Flex flexDir="column" gap={4} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<IPAModelCombobox
modelKey={ipAdapter.model?.key ?? null}
onChangeModel={onChangeModel}
clipVisionModel={ipAdapter.clipVisionModel}
onChangeCLIPVisionModel={onChangeCLIPVisionModel}
/>
</Box>
</Flex>
<Flex gap={4} w="full" alignItems="center">
<Flex flexDir="column" gap={3} w="full">
<IPAMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<IPAImagePreview
image={ipAdapter.image}
onChangeImage={onChangeImage}
ipAdapterId={ipAdapter.id}
droppableData={droppableData}
postUploadAction={postUploadAction}
/>
</Flex>
</Flex>
</Flex>
</Flex>
);
});
RGIPAdapterSettings.displayName = 'RGIPAdapterSettings';

View File

@ -0,0 +1,34 @@
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 { memo } from 'react';
type Props = {
id: string;
};
export const RGIPAdapters = memo(({ id }: Props) => {
const ipAdapterIds = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).ipAdapters.map(({ id }) => id));
if (ipAdapterIds.length === 0) {
return null;
}
return (
<>
{ipAdapterIds.map((id, index) => (
<Flex flexDir="column" key={id}>
{index > 0 && (
<Flex pb={3}>
<Divider />
</Flex>
)}
<RGIPAdapterSettings id={id} ipAdapterId={id} ipAdapterNumber={index + 1} />
</Flex>
))}
</>
);
});
RGIPAdapters.displayName = 'RGLayerIPAdapterList';

View File

@ -0,0 +1,50 @@
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
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 { memo, useCallback } from 'react';
import type { RgbColor } from 'react-colorful';
import { useTranslation } from 'react-i18next';
type Props = {
id: string;
};
export const RGMaskFillColorPicker = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const fill = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).fill);
const onChange = useCallback(
(fill: RgbColor) => {
dispatch(rgFillChanged({ id, fill }));
},
[dispatch, id]
);
return (
<Popover isLazy>
<PopoverTrigger>
<Flex
as="button"
aria-label={t('controlLayers.maskPreviewColor')}
borderRadius="full"
borderWidth={1}
bg={rgbColorToString(fill)}
w={8}
h={8}
cursor="pointer"
tabIndex={-1}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody minH={64}>
<RgbColorPicker color={fill} onChange={onChange} withNumberInput />
</PopoverBody>
</PopoverContent>
</Popover>
);
});
RGMaskFillColorPicker.displayName = 'RGMaskFillColorPicker';

View File

@ -1,8 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library'; import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover'; import { PromptPopover } from 'features/prompt/PromptPopover';
@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Props = { type Props = {
layerId: string; id: string;
}; };
export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { export const RGNegativePrompt = memo(({ id }: Props) => {
const prompt = useLayerNegativePrompt(layerId); const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? '');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const _onChange = useCallback( const _onChange = useCallback(
(v: string) => { (v: string) => {
dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v })); dispatch(rgNegativePromptChanged({ id, prompt: v }));
}, },
[dispatch, layerId] [dispatch, id]
); );
const onDeletePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ id, prompt: null }));
}, [dispatch, id]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt, prompt,
textareaRef, textareaRef,
@ -47,7 +49,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
fontSize="sm" fontSize="sm"
/> />
<PromptOverlayButtonWrapper> <PromptOverlayButtonWrapper>
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" /> <RGDeletePromptButton onDelete={onDeletePrompt} />
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} /> <AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper> </PromptOverlayButtonWrapper>
</Box> </Box>
@ -55,4 +57,4 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
); );
}); });
RGLayerNegativePrompt.displayName = 'RGLayerNegativePrompt'; RGNegativePrompt.displayName = 'RGNegativePrompt';

View File

@ -1,8 +1,7 @@
import { Box, Textarea } from '@invoke-ai/ui-library'; import { Box, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover'; import { PromptPopover } from 'features/prompt/PromptPopover';
@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
type Props = { type Props = {
layerId: string; id: string;
}; };
export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { export const RGPositivePrompt = memo(({ id }: Props) => {
const prompt = useLayerPositivePrompt(layerId); const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? '');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
const _onChange = useCallback( const _onChange = useCallback(
(v: string) => { (v: string) => {
dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v })); dispatch(rgPositivePromptChanged({ id, prompt: v }));
}, },
[dispatch, layerId] [dispatch, id]
); );
const onDeletePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ id, prompt: null }));
}, [dispatch, id]);
const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({
prompt, prompt,
textareaRef, textareaRef,
@ -47,7 +49,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
minH={28} minH={28}
/> />
<PromptOverlayButtonWrapper> <PromptOverlayButtonWrapper>
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" /> <RGDeletePromptButton onDelete={onDeletePrompt} />
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} /> <AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
</PromptOverlayButtonWrapper> </PromptOverlayButtonWrapper>
</Box> </Box>
@ -55,4 +57,4 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
); );
}); });
RGLayerPositivePrompt.displayName = 'RGLayerPositivePrompt'; RGPositivePrompt.displayName = 'RGPositivePrompt';

View File

@ -0,0 +1,30 @@
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 { memo } from 'react';
import { RGIPAdapters } from './RGIPAdapters';
import { RGNegativePrompt } from './RGNegativePrompt';
import { RGPositivePrompt } from './RGPositivePrompt';
type Props = {
id: string;
};
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);
return (
<CanvasEntitySettings>
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons id={id} />}
{hasPositivePrompt && <RGPositivePrompt id={id} />}
{hasNegativePrompt && <RGNegativePrompt id={id} />}
{hasIPAdapters && <RGIPAdapters id={id} />}
</CanvasEntitySettings>
);
});
RGSettings.displayName = 'RGSettings';

View File

@ -0,0 +1,64 @@
import {
Checkbox,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} 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 type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGearSixBold } from 'react-icons/pi';
type Props = {
id: string;
};
export const RGSettingsPopover = memo(({ id }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoNegative = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).autoNegative);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' }));
},
[dispatch, id]
);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
tooltip={t('common.settingsLabel')}
aria-label={t('common.settingsLabel')}
size="sm"
icon={<PiGearSixBold />}
onDoubleClick={stopPropagation} // double click expands the layer
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl gap={2}>
<FormLabel flexGrow={1} minW={32} m={0}>
{t('controlLayers.autoNegative')}
</FormLabel>
<Checkbox size="md" isChecked={autoNegative === 'invert'} onChange={onChange} />
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
RGSettingsPopover.displayName = 'RGSettingsPopover';

View File

@ -0,0 +1,38 @@
import type { ChakraProps } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo, useMemo } from 'react';
type Props = PropsWithChildren<{
isSelected: boolean;
onSelect: () => void;
selectedBorderColor?: ChakraProps['bg'];
}>;
export const CanvasEntityContainer = memo(({ isSelected, onSelect, selectedBorderColor, children }: Props) => {
const bg = useMemo(() => {
if (isSelected) {
return selectedBorderColor ?? 'base.400';
}
return 'base.800';
}, [isSelected, selectedBorderColor]);
return (
<Flex
position="relative"
gap={2}
onClick={onSelect}
bg={bg}
px={2}
borderRadius="base"
py="1px"
transitionProperty="all"
transitionDuration="0.2s"
>
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
{children}
</Flex>
</Flex>
);
});
CanvasEntityContainer.displayName = 'CanvasEntityContainer';

View File

@ -6,7 +6,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
type Props = { onDelete: () => void }; type Props = { onDelete: () => void };
export const EntityDeleteButton = memo(({ onDelete }: Props) => { export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<IconButton <IconButton
@ -21,4 +21,4 @@ export const EntityDeleteButton = memo(({ onDelete }: Props) => {
); );
}); });
EntityDeleteButton.displayName = 'EntityDeleteButton'; CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton';

View File

@ -9,7 +9,7 @@ type Props = {
onToggle: () => void; onToggle: () => void;
}; };
export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -26,4 +26,4 @@ export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
); );
}); });
EntityEnabledToggle.displayName = 'EntityEnabledToggle'; CanvasEntityEnabledToggle.displayName = 'CanvasEntityEnabledToggle';

View File

@ -0,0 +1,15 @@
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren<{ onToggle: () => void }>;
export const CanvasEntityHeader = memo(({ children, onToggle }: Props) => {
return (
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
{children}
</Flex>
);
});
CanvasEntityHeader.displayName = 'CanvasEntityHeader';

View File

@ -3,7 +3,7 @@ import { stopPropagation } from 'common/util/stopPropagation';
import { memo } from 'react'; import { memo } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi'; import { PiDotsThreeVerticalBold } from 'react-icons/pi';
export const EntityMenuButton = memo(() => { export const CanvasEntityMenuButton = memo(() => {
return ( return (
<MenuButton <MenuButton
as={IconButton} as={IconButton}
@ -15,4 +15,4 @@ export const EntityMenuButton = memo(() => {
); );
}); });
EntityMenuButton.displayName = 'EntityMenuButton'; CanvasEntityMenuButton.displayName = 'CanvasEntityMenuButton';

View File

@ -0,0 +1,13 @@
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
export const CanvasEntitySettings = memo(({ children }: PropsWithChildren) => {
return (
<Flex flexDir="column" gap={3} px={3} pb={3}>
{children}
</Flex>
);
});
CanvasEntitySettings.displayName = 'CanvasEntitySettings';

View File

@ -5,7 +5,7 @@ type Props = {
title: string; title: string;
}; };
export const EntityTitle = memo(({ title }: Props) => { export const CanvasEntityTitle = memo(({ title }: Props) => {
return ( return (
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300"> <Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
{title} {title}
@ -13,4 +13,4 @@ export const EntityTitle = memo(({ title }: Props) => {
); );
}); });
EntityTitle.displayName = 'EntityTitle'; CanvasEntityTitle.displayName = 'CanvasEntityTitle';

View File

@ -4,6 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { import type {
@ -22,7 +23,12 @@ type LayersState = {
}; };
const initialState: LayersState = { _version: 1, layers: [] }; const initialState: LayersState = { _version: 1, layers: [] };
const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); 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({ export const layersSlice = createSlice({
name: 'layers', name: 'layers',
@ -48,13 +54,13 @@ export const layersSlice = createSlice({
layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => {
state.layers.push(action.payload.data); state.layers.push(action.payload.data);
}, },
layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id, isEnabled } = action.payload; const { id } = action.payload;
const layer = selectLayer(state, id); const layer = selectLayer(state, id);
if (!layer) { if (!layer) {
return; return;
} }
layer.isEnabled = isEnabled; layer.isEnabled = !layer.isEnabled;
}, },
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload; const { id, x, y } = action.payload;
@ -239,7 +245,7 @@ export const {
layerMovedToFront, layerMovedToFront,
layerMovedBackwardOne, layerMovedBackwardOne,
layerMovedToBack, layerMovedToBack,
layerIsEnabledChanged, layerIsEnabledToggled,
layerOpacityChanged, layerOpacityChanged,
layerTranslated, layerTranslated,
layerBboxChanged, layerBboxChanged,

View File

@ -36,7 +36,12 @@ const initialState: RegionalGuidanceState = {
opacity: 0.3, opacity: 0.3,
}; };
const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); 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[] = [ const DEFAULT_MASK_COLORS: RgbColor[] = [
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
@ -89,7 +94,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgReset: (state, action: PayloadAction<{ id: string }>) => { rgReset: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -102,16 +107,16 @@ export const regionalGuidanceSlice = createSlice({
const { data } = action.payload; const { data } = action.payload;
state.regions.push(data); state.regions.push(data);
}, },
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
const { id, isEnabled } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (rg) { if (rg) {
rg.isEnabled = isEnabled; rg.isEnabled = !rg.isEnabled;
} }
}, },
rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
const { id, x, y } = action.payload; const { id, x, y } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (rg) { if (rg) {
rg.x = x; rg.x = x;
rg.y = y; rg.y = y;
@ -119,7 +124,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
const { id, bbox } = action.payload; const { id, bbox } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (rg) { if (rg) {
rg.bbox = bbox; rg.bbox = bbox;
rg.bboxNeedsUpdate = false; rg.bboxNeedsUpdate = false;
@ -135,7 +140,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -143,7 +148,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -151,7 +156,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -159,7 +164,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
const { id } = action.payload; const { id } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -167,7 +172,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload; const { id, prompt } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -175,7 +180,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
const { id, prompt } = action.payload; const { id, prompt } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -183,7 +188,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
const { id, fill } = action.payload; const { id, fill } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -191,7 +196,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
const { id, imageDTO } = action.payload; const { id, imageDTO } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -199,7 +204,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload; const { id, autoNegative } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -207,7 +212,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => {
const { id, ipAdapter } = action.payload; const { id, ipAdapter } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -215,7 +220,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
const { id, ipAdapterId } = action.payload; const { id, ipAdapterId } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -226,7 +231,7 @@ export const regionalGuidanceSlice = createSlice({
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
) => { ) => {
const { id, ipAdapterId, imageDTO } = action.payload; const { id, ipAdapterId, imageDTO } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -238,7 +243,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
const { id, ipAdapterId, weight } = action.payload; const { id, ipAdapterId, weight } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -253,7 +258,7 @@ export const regionalGuidanceSlice = createSlice({
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
) => { ) => {
const { id, ipAdapterId, beginEndStepPct } = action.payload; const { id, ipAdapterId, beginEndStepPct } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -268,7 +273,7 @@ export const regionalGuidanceSlice = createSlice({
action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>
) => { ) => {
const { id, ipAdapterId, method } = action.payload; const { id, ipAdapterId, method } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -287,7 +292,7 @@ export const regionalGuidanceSlice = createSlice({
}> }>
) => { ) => {
const { id, ipAdapterId, modelConfig } = action.payload; const { id, ipAdapterId, modelConfig } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -306,7 +311,7 @@ export const regionalGuidanceSlice = createSlice({
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
) => { ) => {
const { id, ipAdapterId, clipVisionModel } = action.payload; const { id, ipAdapterId, clipVisionModel } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -319,7 +324,7 @@ export const regionalGuidanceSlice = createSlice({
rgBrushLineAdded: { rgBrushLineAdded: {
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, color, width } = action.payload; const { id, points, lineId, color, width } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -340,7 +345,7 @@ export const regionalGuidanceSlice = createSlice({
rgEraserLineAdded: { rgEraserLineAdded: {
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => { reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
const { id, points, lineId, width } = action.payload; const { id, points, lineId, width } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -359,7 +364,7 @@ export const regionalGuidanceSlice = createSlice({
}, },
rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => { rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
const { id, point } = action.payload; const { id, point } = action.payload;
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }
@ -378,7 +383,7 @@ export const regionalGuidanceSlice = createSlice({
// Ignore zero-area rectangles // Ignore zero-area rectangles
return; return;
} }
const rg = selectRg(state, id); const rg = selectRG(state, id);
if (!rg) { if (!rg) {
return; return;
} }