mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
This commit is contained in:
parent
ba66d7c9a6
commit
cb69872dd3
@ -22,3 +22,4 @@ export const getSelectorsOptions: GetSelectorsOptions = {
|
||||
};
|
||||
|
||||
export const createAppSelector = createSelector.withTypes<RootState>();
|
||||
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();
|
||||
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -1,6 +1,7 @@
|
||||
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
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 { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect';
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={3} position="relative" w="full">
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||
<CAModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
|
||||
</Box>
|
||||
<CanvasEntitySettings>
|
||||
<Flex flexDir="column" gap={3} position="relative" w="full">
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||
<CAModelCombobox modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="sm"
|
||||
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||
onClick={toggleIsExpanded}
|
||||
variant="ghost"
|
||||
icon={
|
||||
<Icon
|
||||
boxSize={4}
|
||||
as={PiCaretUpBold}
|
||||
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
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}
|
||||
<IconButton
|
||||
size="sm"
|
||||
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||
onClick={toggleIsExpanded}
|
||||
variant="ghost"
|
||||
icon={
|
||||
<Icon
|
||||
boxSize={4}
|
||||
as={PiCaretUpBold}
|
||||
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="normal"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Divider />
|
||||
<Flex flexDir="column" gap={3} w="full">
|
||||
<CAProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||
<CAProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||
<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>
|
||||
<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>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Divider />
|
||||
<Flex flexDir="column" gap={3} w="full">
|
||||
<CAProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||
<CAProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</CanvasEntitySettings>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { CALayer } from 'features/controlLayers/components/CALayer/CALayer';
|
||||
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
|
||||
import { IILayer } from 'features/controlLayers/components/IILayer/IILayer';
|
||||
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 { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import type { LayerData } from 'features/controlLayers/store/types';
|
||||
@ -67,7 +67,7 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
|
||||
return <IILayer key={id} layerId={id} />;
|
||||
}
|
||||
if (type === 'raster_layer') {
|
||||
return <RasterLayer key={id} layerId={id} />;
|
||||
return <Layer key={id} layerId={id} />;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
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 { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -20,8 +20,8 @@ import { RiSettings4Fill } from 'react-icons/ri';
|
||||
const ControlLayersSettingsPopover = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
||||
const handleChangeShouldInvertBrushSizeScrollDirection = useCallback(
|
||||
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
|
||||
const onChangeInvertScroll = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
|
||||
[dispatch]
|
||||
);
|
||||
@ -33,13 +33,10 @@ const ControlLayersSettingsPopover = () => {
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
<Flex direction="column" gap={2}>
|
||||
<GlobalMaskLayerOpacity />
|
||||
<RGGlobalOpacity />
|
||||
<FormControl w="full">
|
||||
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
||||
<Checkbox
|
||||
isChecked={shouldInvertBrushSizeScrollDirection}
|
||||
onChange={handleChangeShouldInvertBrushSizeScrollDirection}
|
||||
/>
|
||||
<Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -1,6 +1,7 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
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 { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod';
|
||||
import {
|
||||
@ -70,52 +71,40 @@ export const IPASettings = memo(({ id }: Props) => {
|
||||
[dispatch, id]
|
||||
);
|
||||
|
||||
const droppableData = useMemo<IPAImageDropData>(
|
||||
() => ({
|
||||
actionType: 'SET_IPA_IMAGE',
|
||||
context: { id },
|
||||
id,
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
|
||||
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(
|
||||
() => ({
|
||||
type: 'SET_IPA_IMAGE',
|
||||
id,
|
||||
}),
|
||||
[id]
|
||||
);
|
||||
const droppableData = useMemo<IPAImageDropData>(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]);
|
||||
const postUploadAction = useMemo<IPALayerImagePostUploadAction>(() => ({ type: 'SET_IPA_IMAGE', id }), [id]);
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<CanvasEntitySettings>
|
||||
<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 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 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>
|
||||
</CanvasEntitySettings>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -11,43 +11,29 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { stopPropagation } from 'common/util/stopPropagation';
|
||||
import {
|
||||
layerOpacityChanged,
|
||||
selectCanvasV2Slice,
|
||||
selectLayerOrThrow,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { isLayerWithOpacity } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDropHalfFill } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
layerId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const marks = [0, 25, 50, 75, 100];
|
||||
const formatPct = (v: number | string) => `${v} %`;
|
||||
|
||||
export const LayerOpacity = memo(({ layerId }: Props) => {
|
||||
export const LayerOpacity = memo(({ id }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectOpacity = useMemo(
|
||||
() =>
|
||||
createSelector(selectCanvasV2Slice, (controlLayers) => {
|
||||
const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity);
|
||||
return Math.round(layer.opacity * 100);
|
||||
}),
|
||||
[layerId]
|
||||
);
|
||||
const opacity = useAppSelector(selectOpacity);
|
||||
const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100));
|
||||
const onChangeOpacity = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
|
||||
dispatch(layerOpacityChanged({ id, opacity: v / 100 }));
|
||||
},
|
||||
[dispatch, layerId]
|
||||
[dispatch, id]
|
||||
);
|
||||
return (
|
||||
<Popover isLazy>
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -1,24 +1,19 @@
|
||||
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
globalMaskLayerOpacityChanged,
|
||||
initialControlLayersState,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const marks = [0, 25, 50, 75, 100];
|
||||
const formatPct = (v: number | string) => `${v} %`;
|
||||
|
||||
export const GlobalMaskLayerOpacity = memo(() => {
|
||||
export const RGGlobalOpacity = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const globalMaskLayerOpacity = useAppSelector((s) =>
|
||||
Math.round(s.canvasV2.globalMaskLayerOpacity * 100)
|
||||
);
|
||||
const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100));
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(globalMaskLayerOpacityChanged(v / 100));
|
||||
dispatch(rgGlobalOpacityChanged({ opacity: v / 100 }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@ -30,8 +25,8 @@ export const GlobalMaskLayerOpacity = memo(() => {
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={globalMaskLayerOpacity}
|
||||
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100}
|
||||
value={opacity}
|
||||
defaultValue={0.3}
|
||||
onChange={onChange}
|
||||
marks={marks}
|
||||
minW={48}
|
||||
@ -40,8 +35,8 @@ export const GlobalMaskLayerOpacity = memo(() => {
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={globalMaskLayerOpacity}
|
||||
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100}
|
||||
value={opacity}
|
||||
defaultValue={0.3}
|
||||
onChange={onChange}
|
||||
w={28}
|
||||
format={formatPct}
|
||||
@ -51,4 +46,4 @@ export const GlobalMaskLayerOpacity = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity';
|
||||
RGGlobalOpacity.displayName = 'RGGlobalOpacity';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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);
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -1,8 +1,7 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton';
|
||||
import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks';
|
||||
import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
|
||||
import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
layerId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
||||
const prompt = useLayerNegativePrompt(layerId);
|
||||
export const RGNegativePrompt = memo(({ id }: Props) => {
|
||||
const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? '');
|
||||
const dispatch = useAppDispatch();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const _onChange = useCallback(
|
||||
(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({
|
||||
prompt,
|
||||
textareaRef,
|
||||
@ -47,7 +49,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
||||
fontSize="sm"
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
||||
<RGDeletePromptButton onDelete={onDeletePrompt} />
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
</Box>
|
||||
@ -55,4 +57,4 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
RGLayerNegativePrompt.displayName = 'RGLayerNegativePrompt';
|
||||
RGNegativePrompt.displayName = 'RGNegativePrompt';
|
@ -1,8 +1,7 @@
|
||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton';
|
||||
import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks';
|
||||
import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton';
|
||||
import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice';
|
||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||
@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
layerId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
||||
const prompt = useLayerPositivePrompt(layerId);
|
||||
export const RGPositivePrompt = memo(({ id }: Props) => {
|
||||
const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? '');
|
||||
const dispatch = useAppDispatch();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const _onChange = useCallback(
|
||||
(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({
|
||||
prompt,
|
||||
textareaRef,
|
||||
@ -47,7 +49,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
||||
minH={28}
|
||||
/>
|
||||
<PromptOverlayButtonWrapper>
|
||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
||||
<RGDeletePromptButton onDelete={onDeletePrompt} />
|
||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||
</PromptOverlayButtonWrapper>
|
||||
</Box>
|
||||
@ -55,4 +57,4 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
RGLayerPositivePrompt.displayName = 'RGLayerPositivePrompt';
|
||||
RGPositivePrompt.displayName = 'RGPositivePrompt';
|
@ -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';
|
@ -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';
|
@ -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';
|
@ -6,7 +6,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
type Props = { onDelete: () => void };
|
||||
|
||||
export const EntityDeleteButton = memo(({ onDelete }: Props) => {
|
||||
export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<IconButton
|
||||
@ -21,4 +21,4 @@ export const EntityDeleteButton = memo(({ onDelete }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
EntityDeleteButton.displayName = 'EntityDeleteButton';
|
||||
CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton';
|
@ -9,7 +9,7 @@ type Props = {
|
||||
onToggle: () => void;
|
||||
};
|
||||
|
||||
export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
|
||||
export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -26,4 +26,4 @@ export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
EntityEnabledToggle.displayName = 'EntityEnabledToggle';
|
||||
CanvasEntityEnabledToggle.displayName = 'CanvasEntityEnabledToggle';
|
@ -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';
|
@ -3,7 +3,7 @@ import { stopPropagation } from 'common/util/stopPropagation';
|
||||
import { memo } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityMenuButton = memo(() => {
|
||||
export const CanvasEntityMenuButton = memo(() => {
|
||||
return (
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
@ -15,4 +15,4 @@ export const EntityMenuButton = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
EntityMenuButton.displayName = 'EntityMenuButton';
|
||||
CanvasEntityMenuButton.displayName = 'CanvasEntityMenuButton';
|
@ -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';
|
@ -5,7 +5,7 @@ type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const EntityTitle = memo(({ title }: Props) => {
|
||||
export const CanvasEntityTitle = memo(({ title }: Props) => {
|
||||
return (
|
||||
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
|
||||
{title}
|
||||
@ -13,4 +13,4 @@ export const EntityTitle = memo(({ title }: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
EntityTitle.displayName = 'EntityTitle';
|
||||
CanvasEntityTitle.displayName = 'CanvasEntityTitle';
|
@ -4,6 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
||||
import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { assert } from 'tsafe';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type {
|
||||
@ -22,7 +23,12 @@ type LayersState = {
|
||||
};
|
||||
|
||||
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({
|
||||
name: 'layers',
|
||||
@ -48,13 +54,13 @@ export const layersSlice = createSlice({
|
||||
layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => {
|
||||
state.layers.push(action.payload.data);
|
||||
},
|
||||
layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
|
||||
const { id, isEnabled } = action.payload;
|
||||
layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const layer = selectLayer(state, id);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
layer.isEnabled = isEnabled;
|
||||
layer.isEnabled = !layer.isEnabled;
|
||||
},
|
||||
layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
|
||||
const { id, x, y } = action.payload;
|
||||
@ -239,7 +245,7 @@ export const {
|
||||
layerMovedToFront,
|
||||
layerMovedBackwardOne,
|
||||
layerMovedToBack,
|
||||
layerIsEnabledChanged,
|
||||
layerIsEnabledToggled,
|
||||
layerOpacityChanged,
|
||||
layerTranslated,
|
||||
layerBboxChanged,
|
||||
|
@ -36,7 +36,12 @@ const initialState: RegionalGuidanceState = {
|
||||
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[] = [
|
||||
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
|
||||
@ -89,7 +94,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgReset: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -102,16 +107,16 @@ export const regionalGuidanceSlice = createSlice({
|
||||
const { data } = action.payload;
|
||||
state.regions.push(data);
|
||||
},
|
||||
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => {
|
||||
const { id, isEnabled } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRG(state, id);
|
||||
if (rg) {
|
||||
rg.isEnabled = isEnabled;
|
||||
rg.isEnabled = !rg.isEnabled;
|
||||
}
|
||||
},
|
||||
rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => {
|
||||
const { id, x, y } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (rg) {
|
||||
rg.x = x;
|
||||
rg.y = y;
|
||||
@ -119,7 +124,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => {
|
||||
const { id, bbox } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (rg) {
|
||||
rg.bbox = bbox;
|
||||
rg.bboxNeedsUpdate = false;
|
||||
@ -135,7 +140,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -143,7 +148,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -151,7 +156,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -159,7 +164,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => {
|
||||
const { id } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -167,7 +172,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
|
||||
const { id, prompt } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -175,7 +180,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => {
|
||||
const { id, prompt } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -183,7 +188,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => {
|
||||
const { id, fill } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -191,7 +196,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
|
||||
const { id, imageDTO } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -199,7 +204,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
|
||||
const { id, autoNegative } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -207,7 +212,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => {
|
||||
const { id, ipAdapter } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -215,7 +220,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => {
|
||||
const { id, ipAdapterId } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -226,7 +231,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }>
|
||||
) => {
|
||||
const { id, ipAdapterId, imageDTO } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -238,7 +243,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => {
|
||||
const { id, ipAdapterId, weight } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -253,7 +258,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }>
|
||||
) => {
|
||||
const { id, ipAdapterId, beginEndStepPct } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -268,7 +273,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }>
|
||||
) => {
|
||||
const { id, ipAdapterId, method } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -287,7 +292,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
const { id, ipAdapterId, modelConfig } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -306,7 +311,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }>
|
||||
) => {
|
||||
const { id, ipAdapterId, clipVisionModel } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -319,7 +324,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
rgBrushLineAdded: {
|
||||
reducer: (state, action: PayloadAction<BrushLineAddedArg & { lineId: string }>) => {
|
||||
const { id, points, lineId, color, width } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -340,7 +345,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
rgEraserLineAdded: {
|
||||
reducer: (state, action: PayloadAction<EraserLineAddedArg & { lineId: string }>) => {
|
||||
const { id, points, lineId, width } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -359,7 +364,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
},
|
||||
rgLinePointAdded: (state, action: PayloadAction<PointAddedToLineArg>) => {
|
||||
const { id, point } = action.payload;
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
@ -378,7 +383,7 @@ export const regionalGuidanceSlice = createSlice({
|
||||
// Ignore zero-area rectangles
|
||||
return;
|
||||
}
|
||||
const rg = selectRg(state, id);
|
||||
const rg = selectRG(state, id);
|
||||
if (!rg) {
|
||||
return;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user