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