diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index eb09641845..fb49d796e1 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -22,3 +22,4 @@ export const getSelectorsOptions: GetSelectorsOptions = { }; export const createAppSelector = createSelector.withTypes(); +export const createMemoizedAppSelector = createMemoizedSelector.withTypes(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx new file mode 100644 index 0000000000..cd636b71ca --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -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 ( + + + {isOpen && } + + ); +}); + +CA.displayName = 'CA'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx new file mode 100644 index 0000000000..75b9471942 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -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 ( + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +CAActionsMenu.displayName = 'CAActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx deleted file mode 100644 index f58ba6770c..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntity.tsx +++ /dev/null @@ -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 ( - - - - - {isOpen && ( - - - - )} - - ); -}); - -CAEntity.displayName = 'CAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx new file mode 100644 index 0000000000..1aeaacca8f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -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 ( + + + + + + + + + ); +}); + +CAHeader.displayName = 'CAEntityHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx deleted file mode 100644 index 3987787af7..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAHeaderItems.tsx +++ /dev/null @@ -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 ( - <> - - - - - - - - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - - - - - - ); -}); - -CAHeaderItems.displayName = 'CAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx index e20b79b8d2..01f7edcc79 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CASettings.tsx @@ -1,6 +1,7 @@ import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { CAControlModeSelect } from 'features/controlLayers/components/ControlAdapter/CAControlModeSelect'; import { CAImagePreview } from 'features/controlLayers/components/ControlAdapter/CAImagePreview'; @@ -95,58 +96,60 @@ export const CASettings = memo(({ id }: Props) => { const postUploadAction = useMemo(() => ({ id, type: 'SET_CA_IMAGE' }), [id]); return ( - - - - - + + + + + + - - } - /> - - - - {controlAdapter.controlMode && ( - - )} - - - - - + } /> - - {isExpanded && ( - <> - - - - + + + {controlAdapter.controlMode && ( + + )} + + - - )} - + + + + + {isExpanded && ( + <> + + + + + + + )} + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx index 50e39c43e3..2dfd08c1b0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersPanelContent.tsx @@ -9,7 +9,7 @@ import { CALayer } from 'features/controlLayers/components/CALayer/CALayer'; import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton'; import { IILayer } from 'features/controlLayers/components/IILayer/IILayer'; import { IPAEntity } from 'features/controlLayers/components/IPALayer/IPALayer'; -import { RasterLayer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; +import { Layer } from 'features/controlLayers/components/RasterLayer/RasterLayer'; import { RGLayer } from 'features/controlLayers/components/RGLayer/RGLayer'; import { selectCanvasV2Slice } from 'features/controlLayers/store/controlLayersSlice'; import type { LayerData } from 'features/controlLayers/store/types'; @@ -67,7 +67,7 @@ const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => { return ; } if (type === 'raster_layer') { - return ; + return ; } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 1f3307bdf1..9b9f957dfc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -11,7 +11,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice'; -import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity'; +import { RGGlobalOpacity } from 'features/controlLayers/components/RGGlobalOpacity'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,8 +20,8 @@ import { RiSettings4Fill } from 'react-icons/ri'; const ControlLayersSettingsPopover = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection); - const handleChangeShouldInvertBrushSizeScrollDirection = useCallback( + const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll); + const onChangeInvertScroll = useCallback( (e: ChangeEvent) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)), [dispatch] ); @@ -33,13 +33,10 @@ const ControlLayersSettingsPopover = () => { - + {t('unifiedCanvas.invertBrushSizeScrollDirection')} - + diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx deleted file mode 100644 index 34ef2ce0ac..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/IILayer.tsx +++ /dev/null @@ -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( - () => ({ - actionType: 'SET_II_LAYER_IMAGE', - context: { - layerId, - }, - id: layerId, - }), - [layerId] - ); - - const postUploadAction = useMemo( - () => ({ - layerId, - type: 'SET_II_LAYER_IMAGE', - }), - [layerId] - ); - - return ( - - - - - - - - - - {isOpen && ( - - - - - )} - - ); -}); - -IILayer.displayName = 'IILayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx deleted file mode 100644 index 0a9157068d..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IILayer/InitialImagePreview.tsx +++ /dev/null @@ -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(() => { - if (imageDTO) { - return { - id: 'initial_image_layer', - payloadType: 'IMAGE_DTO', - payload: { imageDTO: imageDTO }, - }; - } - }, [imageDTO]); - - useEffect(() => { - if (isConnected && isErrorControlImage) { - onReset(); - } - }, [onReset, isConnected, isErrorControlImage]); - - return ( - - - - - {imageDTO && ( - - } - tooltip={t('controlnet.resetControlImage')} - /> - } - tooltip={ - shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions') - } - /> - - )} - - - ); -}); - -InitialImagePreview.displayName = 'InitialImagePreview'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx new file mode 100644 index 0000000000..f94afe4da3 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -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 ( + + + {isOpen && } + + ); +}); + +IPA.displayName = 'IPA'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx deleted file mode 100644 index f000045179..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAEntity.tsx +++ /dev/null @@ -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 ( - - - - - {isOpen && ( - - - - )} - - ); -}); - -IPAEntity.displayName = 'IPAEntity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx new file mode 100644 index 0000000000..9604a3283f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeader.tsx @@ -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 ( + + + + + + + ); +}); + +IPAHeader.displayName = 'IPAHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx deleted file mode 100644 index d4de4f7f6f..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAHeaderItems.tsx +++ /dev/null @@ -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 ( - <> - - - - - - ); -}); - -IPAHeaderItems.displayName = 'IPAHeaderItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx index 1daacb941f..dfe26b41ed 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPASettings.tsx @@ -1,6 +1,7 @@ import { Box, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct'; +import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; import { Weight } from 'features/controlLayers/components/common/Weight'; import { IPAMethod } from 'features/controlLayers/components/IPAdapter/IPAMethod'; import { @@ -70,52 +71,40 @@ export const IPASettings = memo(({ id }: Props) => { [dispatch, id] ); - const droppableData = useMemo( - () => ({ - actionType: 'SET_IPA_IMAGE', - context: { id }, - id, - }), - [id] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_IPA_IMAGE', - id, - }), - [id] - ); + const droppableData = useMemo(() => ({ actionType: 'SET_IPA_IMAGE', context: { id }, id }), [id]); + const postUploadAction = useMemo(() => ({ type: 'SET_IPA_IMAGE', id }), [id]); return ( - - - - - - - - - - - + + + + + + - - + + + + + + + + + - + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx new file mode 100644 index 0000000000..214f4e825c --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -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 ( + + + {isOpen && } + + ); +}); + +Layer.displayName = 'Layer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx new file mode 100644 index 0000000000..03b4e8953d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -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 ( + + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +LayerActionsMenu.displayName = 'LayerActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx new file mode 100644 index 0000000000..cd8bc2d5f9 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -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 ( + + + + + + + + + ); +}); + +LayerHeader.displayName = 'LayerHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx similarity index 71% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx index b92ccc41f9..00de65b713 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx @@ -11,43 +11,29 @@ import { PopoverContent, PopoverTrigger, } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { - layerOpacityChanged, - selectCanvasV2Slice, - selectLayerOrThrow, -} from 'features/controlLayers/store/controlLayersSlice'; -import { isLayerWithOpacity } from 'features/controlLayers/store/types'; -import { memo, useCallback, useMemo } from 'react'; +import { layerOpacityChanged, selectLayerOrThrow } from 'features/controlLayers/store/layersSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; type Props = { - layerId: string; + id: string; }; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const LayerOpacity = memo(({ layerId }: Props) => { +export const LayerOpacity = memo(({ id }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectOpacity = useMemo( - () => - createSelector(selectCanvasV2Slice, (controlLayers) => { - const layer = selectLayerOrThrow(canvasV2, layerId, isLayerWithOpacity); - return Math.round(layer.opacity * 100); - }), - [layerId] - ); - const opacity = useAppSelector(selectOpacity); + const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.layers, id).opacity * 100)); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ layerId, opacity: v / 100 })); + dispatch(layerOpacityChanged({ id, opacity: v / 100 })); }, - [dispatch, layerId] + [dispatch, id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx new file mode 100644 index 0000000000..0ef45dc223 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -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( + () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), + [id] + ); + + return ( + + PLACEHOLDER + + + ); +}); + +LayerSettings.displayName = 'LayerSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx deleted file mode 100644 index a10079f160..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenu.tsx +++ /dev/null @@ -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 ( - - } - onDoubleClick={stopPropagation} // double click expands the layer - /> - - {layerType === 'regional_guidance_layer' && ( - <> - - - - )} - {shouldShowArrangeActions && ( - <> - - - - )} - {shouldShowResetAction && ( - }> - {t('accessibility.reset')} - - )} - } color="error.300"> - {t('common.delete')} - - - - ); -}); - -EntityMenu.displayName = 'EntityMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx deleted file mode 100644 index 4c86a98c36..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuArrangeActions.tsx +++ /dev/null @@ -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 ( - <> - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - ); -}); - -LayerMenuArrangeActions.displayName = 'LayerMenuArrangeActions'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx deleted file mode 100644 index 335a0d32cb..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerMenuRGActions.tsx +++ /dev/null @@ -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 ( - <> - }> - {t('controlLayers.addPositivePrompt')} - - }> - {t('controlLayers.addNegativePrompt')} - - } isDisabled={isAddIPAdapterDisabled}> - {t('controlLayers.addIPAdapter')} - - - ); -}); - -LayerMenuRGActions.displayName = 'LayerMenuRGActions'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx deleted file mode 100644 index 804ae40070..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/LayerWrapper.tsx +++ /dev/null @@ -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 ( - - - {children} - - - ); -}); - -LayerWrapper.displayName = 'LayerWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx similarity index 61% rename from invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx index 23720f4c22..db56cdd558 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/GlobalMaskLayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RGGlobalOpacity.tsx @@ -1,24 +1,19 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - globalMaskLayerOpacityChanged, - initialControlLayersState, -} from 'features/controlLayers/store/controlLayersSlice'; +import { rgGlobalOpacityChanged } from 'features/controlLayers/store/regionalGuidanceSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const GlobalMaskLayerOpacity = memo(() => { +export const RGGlobalOpacity = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const globalMaskLayerOpacity = useAppSelector((s) => - Math.round(s.canvasV2.globalMaskLayerOpacity * 100) - ); + const opacity = useAppSelector((s) => Math.round(s.regionalGuidance.opacity * 100)); const onChange = useCallback( (v: number) => { - dispatch(globalMaskLayerOpacityChanged(v / 100)); + dispatch(rgGlobalOpacityChanged({ opacity: v / 100 })); }, [dispatch] ); @@ -30,8 +25,8 @@ export const GlobalMaskLayerOpacity = memo(() => { min={0} max={100} step={1} - value={globalMaskLayerOpacity} - defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} + value={opacity} + defaultValue={0.3} onChange={onChange} marks={marks} minW={48} @@ -40,8 +35,8 @@ export const GlobalMaskLayerOpacity = memo(() => { min={0} max={100} step={1} - value={globalMaskLayerOpacity} - defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100} + value={opacity} + defaultValue={0.3} onChange={onChange} w={28} format={formatPct} @@ -51,4 +46,4 @@ export const GlobalMaskLayerOpacity = memo(() => { ); }); -GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity'; +RGGlobalOpacity.displayName = 'RGGlobalOpacity'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx deleted file mode 100644 index 5d5ce7fb02..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayer.tsx +++ /dev/null @@ -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 ( - - - - - - {autoNegative === 'invert' && ( - - {t('controlLayers.autoNegative')} - - )} - - - - - - {isOpen && ( - - {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } - {hasPositivePrompt && } - {hasNegativePrompt && } - {hasIPAdapters && } - - )} - - ); -}); - -RGLayer.displayName = 'RGLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx deleted file mode 100644 index 998682d4cc..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerAutoNegativeCheckbox.tsx +++ /dev/null @@ -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) => { - dispatch(regionalGuidanceAutoNegativeChanged({ layerId, autoNegative: e.target.checked ? 'invert' : 'off' })); - }, - [dispatch, layerId] - ); - - return ( - - {t('controlLayers.autoNegative')} - - - ); -}); - -RGLayerAutoNegativeCheckbox.displayName = 'RGLayerAutoNegativeCheckbox'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx deleted file mode 100644 index 7ce002817a..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerColorPicker.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - - - - - - ); -}); - -RGLayerColorPicker.displayName = 'RGLayerColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx deleted file mode 100644 index 5b6be683d1..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterList.tsx +++ /dev/null @@ -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) => ( - - {index > 0 && ( - - - - )} - - - ))} - - ); -}); - -RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx deleted file mode 100644 index 2fa392e0d0..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerIPAdapterWrapper.tsx +++ /dev/null @@ -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( - () => ({ - actionType: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', - context: { - layerId, - ipAdapterId, - }, - id: layerId, - }), - [ipAdapterId, layerId] - ); - - const postUploadAction = useMemo( - () => ({ - type: 'SET_RG_LAYER_IP_ADAPTER_IMAGE', - layerId, - ipAdapterId, - }), - [ipAdapterId, layerId] - ); - - return ( - - - {`IP Adapter ${ipAdapterNumber}`} - - } - aria-label="Delete IP Adapter" - onClick={onDeleteIPAdapter} - variant="ghost" - colorScheme="error" - /> - - - - ); -}); - -RGLayerIPAdapterWrapper.displayName = 'RGLayerIPAdapterWrapper'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx deleted file mode 100644 index cbc99e70de..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton.tsx +++ /dev/null @@ -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 ( - - } - onClick={onClick} - /> - - ); -}); - -RGLayerPromptDeleteButton.displayName = 'RGLayerPromptDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx deleted file mode 100644 index 9203069b3c..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerSettingsPopover.tsx +++ /dev/null @@ -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 ( - - - } - onDoubleClick={stopPropagation} // double click expands the layer - /> - - - - - - - - - - - - - ); -}; - -export default memo(RGLayerSettingsPopover); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx deleted file mode 100644 index 82851dbb0b..0000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayer.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - {isOpen && ( - - PLACEHOLDER - - )} - - - ); -}); - -RasterLayer.displayName = 'RasterLayer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx new file mode 100644 index 0000000000..6c503f98e1 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -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 ( + + + {isOpen && } + + ); +}); + +RG.displayName = 'RG'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx new file mode 100644 index 0000000000..db1cb078bc --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -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 ( + + + + }> + {t('controlLayers.addPositivePrompt')} + + }> + {t('controlLayers.addNegativePrompt')} + + } isDisabled={isAddIPAdapterDisabled}> + {t('controlLayers.addIPAdapter')} + + + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + }> + {t('accessibility.reset')} + + } color="error.300"> + {t('common.delete')} + + + + ); +}); + +RGActionsMenu.displayName = 'RGActionsMenu'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx new file mode 100644 index 0000000000..4e994b43ec --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGDeletePromptButton.tsx @@ -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 ( + + } + onClick={onDelete} + /> + + ); +}); + +RGDeletePromptButton.displayName = 'RGDeletePromptButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx new file mode 100644 index 0000000000..50f854e856 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -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 ( + + + + + {autoNegative === 'invert' && ( + + {t('controlLayers.autoNegative')} + + )} + + + + + + ); +}); + +RGHeader.displayName = 'RGHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx new file mode 100644 index 0000000000..678b70ee90 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapterSettings.tsx @@ -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( + () => ({ actionType: 'SET_RG_IP_ADAPTER_IMAGE', context: { id, ipAdapterId }, id }), + [ipAdapterId, id] + ); + const postUploadAction = useMemo( + () => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id, ipAdapterId }), + [ipAdapterId, id] + ); + + return ( + + + {`IP Adapter ${ipAdapterNumber}`} + + } + aria-label="Delete IP Adapter" + onClick={onDeleteIPAdapter} + variant="ghost" + colorScheme="error" + /> + + + + + + + + + + + + + + + + + + + + ); +}); + +RGIPAdapterSettings.displayName = 'RGIPAdapterSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx new file mode 100644 index 0000000000..5d787ffdda --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGIPAdapters.tsx @@ -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) => ( + + {index > 0 && ( + + + + )} + + + ))} + + ); +}); + +RGIPAdapters.displayName = 'RGLayerIPAdapterList'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx new file mode 100644 index 0000000000..e325b6d965 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -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 ( + + + + + + + + + + + ); +}); + +RGMaskFillColorPicker.displayName = 'RGMaskFillColorPicker'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx similarity index 65% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx index 92ae46a131..e42f2728aa 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGNegativePrompt.tsx @@ -1,8 +1,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; -import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { regionalGuidanceNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { rgNegativePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + id: string; }; -export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { - const prompt = useLayerNegativePrompt(layerId); +export const RGNegativePrompt = memo(({ id }: Props) => { + const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).negativePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(regionalGuidanceNegativePromptChanged({ layerId, prompt: v })); + dispatch(rgNegativePromptChanged({ id, prompt: v })); }, - [dispatch, layerId] + [dispatch, id] ); + const onDeletePrompt = useCallback(() => { + dispatch(rgNegativePromptChanged({ id, prompt: null })); + }, [dispatch, id]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, @@ -47,7 +49,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { fontSize="sm" /> - + @@ -55,4 +57,4 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => { ); }); -RGLayerNegativePrompt.displayName = 'RGLayerNegativePrompt'; +RGNegativePrompt.displayName = 'RGNegativePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx similarity index 65% rename from invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx index 34c4366dfc..0bc82526b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RGLayer/RGLayerPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGPositivePrompt.tsx @@ -1,8 +1,7 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayer/RGLayerPromptDeleteButton'; -import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks'; -import { regionalGuidancePositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { RGDeletePromptButton } from 'features/controlLayers/components/RegionalGuidance/RGDeletePromptButton'; +import { rgPositivePromptChanged, selectRGOrThrow } from 'features/controlLayers/store/regionalGuidanceSlice'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; @@ -11,20 +10,23 @@ import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; type Props = { - layerId: string; + id: string; }; -export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { - const prompt = useLayerPositivePrompt(layerId); +export const RGPositivePrompt = memo(({ id }: Props) => { + const prompt = useAppSelector((s) => selectRGOrThrow(s.regionalGuidance, id).positivePrompt ?? ''); const dispatch = useAppDispatch(); const textareaRef = useRef(null); const { t } = useTranslation(); const _onChange = useCallback( (v: string) => { - dispatch(regionalGuidancePositivePromptChanged({ layerId, prompt: v })); + dispatch(rgPositivePromptChanged({ id, prompt: v })); }, - [dispatch, layerId] + [dispatch, id] ); + const onDeletePrompt = useCallback(() => { + dispatch(rgPositivePromptChanged({ id, prompt: null })); + }, [dispatch, id]); const { onChange, isOpen, onClose, onOpen, onSelect, onKeyDown } = usePrompt({ prompt, textareaRef, @@ -47,7 +49,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { minH={28} /> - + @@ -55,4 +57,4 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => { ); }); -RGLayerPositivePrompt.displayName = 'RGLayerPositivePrompt'; +RGPositivePrompt.displayName = 'RGPositivePrompt'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx new file mode 100644 index 0000000000..c626facd01 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx @@ -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 ( + + {!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && } + {hasPositivePrompt && } + {hasNegativePrompt && } + {hasIPAdapters && } + + ); +}); + +RGSettings.displayName = 'RGSettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx new file mode 100644 index 0000000000..a74f0039fa --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx @@ -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) => { + dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); + }, + [dispatch, id] + ); + + return ( + + + } + onDoubleClick={stopPropagation} // double click expands the layer + /> + + + + + + + + {t('controlLayers.autoNegative')} + + + + + + + + ); +}); + +RGSettingsPopover.displayName = 'RGSettingsPopover'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx new file mode 100644 index 0000000000..700c1669bf --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -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 ( + + + {children} + + + ); +}); + +CanvasEntityContainer.displayName = 'CanvasEntityContainer'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx similarity index 81% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx index 441c839587..1cbb0fa29a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -6,7 +6,7 @@ import { PiTrashSimpleBold } from 'react-icons/pi'; type Props = { onDelete: () => void }; -export const EntityDeleteButton = memo(({ onDelete }: Props) => { +export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => { const { t } = useTranslation(); return ( { ); }); -EntityDeleteButton.displayName = 'EntityDeleteButton'; +CanvasEntityDeleteButton.displayName = 'CanvasEntityDeleteButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx similarity index 82% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index ca4855a64f..eaa41fcfe9 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -9,7 +9,7 @@ type Props = { onToggle: () => void; }; -export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { +export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { const { t } = useTranslation(); return ( @@ -26,4 +26,4 @@ export const EntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { ); }); -EntityEnabledToggle.displayName = 'EntityEnabledToggle'; +CanvasEntityEnabledToggle.displayName = 'CanvasEntityEnabledToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx new file mode 100644 index 0000000000..31977b61af --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeader.tsx @@ -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 ( + + {children} + + ); +}); + +CanvasEntityHeader.displayName = 'CanvasEntityHeader'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx similarity index 79% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx index 51887ed5e1..2f358f902d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityMenuButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuButton.tsx @@ -3,7 +3,7 @@ import { stopPropagation } from 'common/util/stopPropagation'; import { memo } from 'react'; import { PiDotsThreeVerticalBold } from 'react-icons/pi'; -export const EntityMenuButton = memo(() => { +export const CanvasEntityMenuButton = memo(() => { return ( { ); }); -EntityMenuButton.displayName = 'EntityMenuButton'; +CanvasEntityMenuButton.displayName = 'CanvasEntityMenuButton'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx new file mode 100644 index 0000000000..d9665c9f0a --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntitySettings.tsx @@ -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 ( + + {children} + + ); +}); + +CanvasEntitySettings.displayName = 'CanvasEntitySettings'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx similarity index 67% rename from invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index 31fd5902b8..ce72f2f002 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/LayerCommon/EntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -5,7 +5,7 @@ type Props = { title: string; }; -export const EntityTitle = memo(({ title }: Props) => { +export const CanvasEntityTitle = memo(({ title }: Props) => { return ( {title} @@ -13,4 +13,4 @@ export const EntityTitle = memo(({ title }: Props) => { ); }); -EntityTitle.displayName = 'EntityTitle'; +CanvasEntityTitle.displayName = 'CanvasEntityTitle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts index 7c6705ae9b..b25a700f3f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersSlice.ts @@ -4,6 +4,7 @@ import type { PersistConfig, RootState } from 'app/store/store'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getImageObjectId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { IRect } from 'konva/lib/types'; +import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { @@ -22,7 +23,12 @@ type LayersState = { }; const initialState: LayersState = { _version: 1, layers: [] }; -const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayer = (state: LayersState, id: string) => state.layers.find((layer) => layer.id === id); +export const selectLayerOrThrow = (state: LayersState, id: string) => { + const layer = selectLayer(state, id); + assert(layer, `Layer with id ${id} not found`); + return layer; +}; export const layersSlice = createSlice({ name: 'layers', @@ -48,13 +54,13 @@ export const layersSlice = createSlice({ layerRecalled: (state, action: PayloadAction<{ data: LayerData }>) => { state.layers.push(action.payload.data); }, - layerIsEnabledChanged: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; + layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; const layer = selectLayer(state, id); if (!layer) { return; } - layer.isEnabled = isEnabled; + layer.isEnabled = !layer.isEnabled; }, layerTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; @@ -239,7 +245,7 @@ export const { layerMovedToFront, layerMovedBackwardOne, layerMovedToBack, - layerIsEnabledChanged, + layerIsEnabledToggled, layerOpacityChanged, layerTranslated, layerBboxChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts index cb41306b7e..2d5c16d421 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionalGuidanceSlice.ts @@ -36,7 +36,12 @@ const initialState: RegionalGuidanceState = { opacity: 0.3, }; -const selectRg = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRG = (state: RegionalGuidanceState, id: string) => state.regions.find((rg) => rg.id === id); +export const selectRGOrThrow = (state: RegionalGuidanceState, id: string) => { + const rg = selectRG(state, id); + assert(rg, `Region with id ${id} not found`); + return rg; +}; const DEFAULT_MASK_COLORS: RgbColor[] = [ { r: 121, g: 157, b: 219 }, // rgb(121, 157, 219) @@ -89,7 +94,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgReset: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -102,16 +107,16 @@ export const regionalGuidanceSlice = createSlice({ const { data } = action.payload; state.regions.push(data); }, - rgIsEnabledToggled: (state, action: PayloadAction<{ id: string; isEnabled: boolean }>) => { - const { id, isEnabled } = action.payload; - const rg = selectRg(state, id); + rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { + const { id } = action.payload; + const rg = selectRG(state, id); if (rg) { - rg.isEnabled = isEnabled; + rg.isEnabled = !rg.isEnabled; } }, rgTranslated: (state, action: PayloadAction<{ id: string; x: number; y: number }>) => { const { id, x, y } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (rg) { rg.x = x; rg.y = y; @@ -119,7 +124,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { const { id, bbox } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (rg) { rg.bbox = bbox; rg.bboxNeedsUpdate = false; @@ -135,7 +140,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -143,7 +148,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -151,7 +156,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -159,7 +164,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { const { id } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -167,7 +172,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -175,7 +180,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgNegativePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -183,7 +188,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgFillChanged: (state, action: PayloadAction<{ id: string; fill: RgbColor }>) => { const { id, fill } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -191,7 +196,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgMaskImageUploaded: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => { const { id, imageDTO } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -199,7 +204,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => { const { id, autoNegative } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -207,7 +212,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterAdded: (state, action: PayloadAction<{ id: string; ipAdapter: IPAdapterData }>) => { const { id, ipAdapter } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -215,7 +220,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterDeleted: (state, action: PayloadAction<{ id: string; ipAdapterId: string }>) => { const { id, ipAdapterId } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -226,7 +231,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; imageDTO: ImageDTO | null }> ) => { const { id, ipAdapterId, imageDTO } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -238,7 +243,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgIPAdapterWeightChanged: (state, action: PayloadAction<{ id: string; ipAdapterId: string; weight: number }>) => { const { id, ipAdapterId, weight } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -253,7 +258,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; beginEndStepPct: [number, number] }> ) => { const { id, ipAdapterId, beginEndStepPct } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -268,7 +273,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; method: IPMethodV2 }> ) => { const { id, ipAdapterId, method } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -287,7 +292,7 @@ export const regionalGuidanceSlice = createSlice({ }> ) => { const { id, ipAdapterId, modelConfig } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -306,7 +311,7 @@ export const regionalGuidanceSlice = createSlice({ action: PayloadAction<{ id: string; ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }> ) => { const { id, ipAdapterId, clipVisionModel } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -319,7 +324,7 @@ export const regionalGuidanceSlice = createSlice({ rgBrushLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, color, width } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -340,7 +345,7 @@ export const regionalGuidanceSlice = createSlice({ rgEraserLineAdded: { reducer: (state, action: PayloadAction) => { const { id, points, lineId, width } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -359,7 +364,7 @@ export const regionalGuidanceSlice = createSlice({ }, rgLinePointAdded: (state, action: PayloadAction) => { const { id, point } = action.payload; - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; } @@ -378,7 +383,7 @@ export const regionalGuidanceSlice = createSlice({ // Ignore zero-area rectangles return; } - const rg = selectRg(state, id); + const rg = selectRG(state, id); if (!rg) { return; }