From 894b8a29b9e7dab67948d1eb4f43097c5f23da2b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:27:00 +1000 Subject: [PATCH] tidy(ui): massive cleanup - create a context for entity identifiers, massively simplifying UI for each entity int he list - consolidate common redux actions - remove now-unused code --- .../listeners/imageDeletionListeners.ts | 4 +- .../listeners/modelSelected.ts | 9 +- .../components/ControlAdapter/CA.tsx | 22 +- .../ControlAdapter/CAActionsMenu.tsx | 80 +----- .../ControlAdapter/CAEntityHeader.tsx | 30 +- .../ControlAdapter/CAOpacityAndFilter.tsx | 8 +- .../components/IPAdapter/IPA.tsx | 2 +- .../components/InpaintMask/IM.tsx | 24 +- .../components/InpaintMask/IMActionsMenu.tsx | 19 +- .../components/InpaintMask/IMHeader.tsx | 19 +- .../controlLayers/components/Layer/Layer.tsx | 24 +- .../components/Layer/LayerActionsMenu.tsx | 80 +----- .../components/Layer/LayerHeader.tsx | 34 +-- .../components/Layer/LayerOpacity.tsx | 14 +- .../components/Layer/LayerSettings.tsx | 8 +- .../components/RegionalGuidance/RG.tsx | 25 +- .../RegionalGuidance/RGActionsMenu.tsx | 73 +---- .../components/RegionalGuidance/RGHeader.tsx | 31 +- .../RGMaskFillColorPicker.tsx | 14 +- .../RegionalGuidance/RGSettings.tsx | 8 +- .../RegionalGuidance/RGSettingsPopover.tsx | 14 +- .../controlLayers/components/ToolChooser.tsx | 35 +-- .../common/CanvasEntityActionMenuItems.tsx | 127 +++++++++ .../common/CanvasEntityContainer.tsx | 31 +- .../common/CanvasEntityDeleteButton.tsx | 16 +- .../common/CanvasEntityEnabledToggle.tsx | 21 +- .../components/common/CanvasEntityTitle.tsx | 30 +- .../contexts/EntityIdentifierContext.ts | 11 + .../hooks/useCanvasDeleteLayerHotkey.ts | 19 +- .../hooks/useCanvasResetLayerHotkey.ts | 15 +- .../controlLayers/hooks/useEntityIsEnabled.ts | 22 ++ .../hooks/useEntitySelectionColor.ts | 27 ++ .../hooks/useIsEntitySelected.ts | 12 + .../controlLayers/konva/CanvasBbox.ts | 7 - .../controlLayers/konva/CanvasLayerAdapter.ts | 14 +- .../controlLayers/konva/CanvasMaskAdapter.ts | 8 + .../konva/CanvasObjectRenderer.ts | 27 +- .../controlLayers/konva/CanvasStateApi.ts | 111 ++------ .../controlLayers/konva/CanvasTransformer.ts | 4 +- .../controlLayers/store/canvasV2Slice.ts | 268 +++++++++++++++--- .../store/controlAdaptersReducers.ts | 94 +----- .../store/inpaintMaskReducers.ts | 42 +-- .../controlLayers/store/layersReducers.ts | 135 +-------- .../controlLayers/store/regionsReducers.ts | 111 +------- .../src/features/controlLayers/store/types.ts | 17 +- 45 files changed, 718 insertions(+), 1028 deletions(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx create mode 100644 invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts create mode 100644 invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts index 4c9a045d60..41944fc405 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts @@ -4,8 +4,8 @@ import type { AppDispatch, RootState } from 'app/store/store'; import { caImageChanged, caProcessedImageChanged, + entityDeleted, ipaImageChanged, - layerDeleted, } from 'features/controlLayers/store/canvasV2Slice'; import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions'; import { isModalOpenChanged } from 'features/deleteImageModal/store/slice'; @@ -66,7 +66,7 @@ const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im } } if (shouldDelete) { - dispatch(layerDeleted({ id })); + dispatch(entityDeleted({ entityIdentifier: { id, type: 'layer' } })); } }); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index aa0e9a3d61..e07dcdc844 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -1,6 +1,11 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; -import { caIsEnabledToggled, loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice'; +import { + entityIsEnabledToggled, + loraDeleted, + modelChanged, + vaeSelected, +} from 'features/controlLayers/store/canvasV2Slice'; import { modelSelected } from 'features/parameters/store/actions'; import { zParameterModel } from 'features/parameters/types/parameterSchemas'; import { toast } from 'features/toast/toast'; @@ -49,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) = if (ca.model?.base !== newBaseModel) { modelsCleared += 1; if (ca.isEnabled) { - dispatch(caIsEnabledToggled({ id: ca.id })); + dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } })); } } }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx index 9bb6dff791..8240b3664a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CA.tsx @@ -1,28 +1,26 @@ 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/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } 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 entityIdentifier = useMemo(() => ({ id, type: 'control_adapter' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'control_adapter' })); - }, [dispatch, id]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx index 1c532dda0f..6285d7be9c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAActionsMenu.tsx @@ -1,84 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { - caDeleted, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; - -export const CAActionsMenu = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const ca = selectCAOrThrow(canvasV2, id); - const caIndex = canvasV2.controlAdapters.entities.indexOf(ca); - const caCount = canvasV2.controlAdapters.entities.length; - return { - canMoveForward: caIndex < caCount - 1, - canMoveBackward: caIndex > 0, - canMoveToFront: caIndex < caCount - 1, - canMoveToBack: caIndex > 0, - }; - }), - [id] - ); - const validActions = useAppSelector(selectValidActions); - 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]); +import { memo } from 'react'; +export const CAActionsMenu = memo(() => { return ( - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx index 2eef6f5cb6..b95945f4fe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAEntityHeader.tsx @@ -1,41 +1,25 @@ 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 } from 'features/controlLayers/store/canvasV2Slice'; -import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const CAHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id).isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(caIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(caDeleted({ id })); - }, [dispatch, id]); - +export const CAHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + - - - + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx index 25117459f6..b440aad03e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAdapter/CAOpacityAndFilter.tsx @@ -14,6 +14,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers'; import type { ChangeEvent } from 'react'; @@ -21,14 +22,11 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; -type Props = { - id: string; -}; - const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const CAOpacityAndFilter = memo(({ id }: Props) => { +export const CAOpacityAndFilter = memo(() => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100)); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx index 8281cadc32..ab1cdc987e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPA.tsx @@ -15,7 +15,7 @@ export const IPA = memo(({ id }: Props) => { 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(entitySelected({ entityIdentifier: { id, type: 'ip_adapter' } })); }, [dispatch, id]); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx index 55a332270d..8734419074 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IM.tsx @@ -1,25 +1,21 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { IMHeader } from 'features/controlLayers/components/InpaintMask/IMHeader'; import { IMSettings } from 'features/controlLayers/components/InpaintMask/IMSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; export const IM = memo(() => { - const dispatch = useAppDispatch(); - const selectedBorderColor = useAppSelector((s) => rgbColorToString(s.canvasV2.inpaintMask.fill)); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === 'inpaint_mask'); + const entityIdentifier = useMemo(() => ({ id: 'inpaint_mask', type: 'inpaint_mask' }), []); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id: 'inpaint_mask', type: 'inpaint_mask' })); - }, [dispatch]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx index 14462abc73..a7cfc75eb2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMActionsMenu.tsx @@ -1,25 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { imReset } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; +import { memo } from 'react'; export const IMActionsMenu = memo(() => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const onReset = useCallback(() => { - dispatch(imReset()); - }, [dispatch]); - return ( - }> - {t('accessibility.reset')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx index 888c5b1eb3..283752b65a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/IMHeader.tsx @@ -1,32 +1,21 @@ import { Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; 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 { IMActionsMenu } from 'features/controlLayers/components/InpaintMask/IMActionsMenu'; -import { imIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; import { IMMaskFillColorPicker } from './IMMaskFillColorPicker'; type Props = { - isSelected: boolean; onToggleVisibility: () => void; }; -export const IMHeader = memo(({ isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => s.canvasV2.inpaintMask.isEnabled); - const onToggleIsEnabled = useCallback(() => { - dispatch(imIsEnabledToggled()); - }, [dispatch]); - +export const IMHeader = memo(({ onToggleVisibility }: Props) => { 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 index 1e2b6fdf1d..49d2a4dba3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/Layer.tsx @@ -1,35 +1,33 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDroppable from 'common/components/IAIDroppable'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { LayerHeader } from 'features/controlLayers/components/Layer/LayerHeader'; import { LayerSettings } from 'features/controlLayers/components/Layer/LayerSettings'; -import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { LayerImageDropData } from 'features/dnd/types'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useMemo } 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 entityIdentifier = useMemo(() => ({ id, type: 'layer' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: false }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'layer' })); - }, [dispatch, id]); const droppableData = useMemo( () => ({ id, actionType: 'ADD_LAYER_IMAGE', context: { id } }), [id] ); return ( - - - {isOpen && } - - + + + + {isOpen && } + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx index 7ab753012f..53a30acc37 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerActionsMenu.tsx @@ -1,84 +1,14 @@ -import { Menu, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { Menu, MenuList } from '@invoke-ai/ui-library'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; -import { - layerDeleted, - layerMovedBackwardOne, - layerMovedForwardOne, - layerMovedToBack, - layerMovedToFront, - selectCanvasV2Slice, -} from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; - -type Props = { - id: string; -}; - -export const LayerActionsMenu = memo(({ id }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectValidActions = useMemo( - () => - createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { - const layer = selectLayerOrThrow(canvasV2, id); - const layerIndex = canvasV2.layers.entities.indexOf(layer); - const layerCount = canvasV2.layers.entities.length; - return { - canMoveForward: layerIndex < layerCount - 1, - canMoveBackward: layerIndex > 0, - canMoveToFront: layerIndex < layerCount - 1, - canMoveToBack: layerIndex > 0, - }; - }), - [id] - ); - const validActions = useAppSelector(selectValidActions); - 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]); +import { memo } from 'react'; +export const LayerActionsMenu = memo(() => { return ( - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - } color="error.300"> - {t('common.delete')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx index b53ca7f275..5d8c17f957 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerHeader.tsx @@ -1,46 +1,26 @@ 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 } from 'features/controlLayers/store/canvasV2Slice'; -import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; import { LayerOpacity } from './LayerOpacity'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const LayerHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).isEnabled); - const objectCount = useAppSelector((s) => selectLayerOrThrow(s.canvasV2, id).objects.length); - const onToggleIsEnabled = useCallback(() => { - dispatch(layerIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(layerDeleted({ id })); - }, [dispatch, id]); - const title = useMemo(() => { - return `${t('controlLayers.layer')} (${t('controlLayers.objects', { count: objectCount })})`; - }, [objectCount, t]); - +export const LayerHeader = memo(({ onToggleVisibility }: Props) => { return ( - - + + - - - + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx index da1582310e..0d42659966 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerOpacity.tsx @@ -13,28 +13,26 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDropHalfFill } from 'react-icons/pi'; -type Props = { - id: string; -}; - const marks = [0, 25, 50, 75, 100]; const formatPct = (v: number | string) => `${v} %`; -export const LayerOpacity = memo(({ id }: Props) => { +export const LayerOpacity = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, id).opacity * 100)); + const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, entityIdentifier.id).opacity * 100)); const onChangeOpacity = useCallback( (v: number) => { - dispatch(layerOpacityChanged({ id, opacity: v / 100 })); + dispatch(layerOpacityChanged({ id: entityIdentifier.id, opacity: v / 100 })); }, - [dispatch, id] + [dispatch, entityIdentifier.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 index 8af3e81814..111e30d96f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Layer/LayerSettings.tsx @@ -1,11 +1,9 @@ import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo } from 'react'; -type Props = { - id: string; -}; - -export const LayerSettings = memo(({ id }: Props) => { +export const LayerSettings = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); return PLACEHOLDER; }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx index fe923169cb..1da28a8fdc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RG.tsx @@ -1,30 +1,25 @@ import { useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { rgbColorToString } from 'common/util/colorCodeTransformers'; 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/canvasV2Slice'; -import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback } from 'react'; +import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { memo, useMemo } from 'react'; type Props = { id: string; }; export const RG = memo(({ id }: Props) => { - const dispatch = useAppDispatch(); - const selectedBorderColor = useAppSelector((s) => rgbColorToString(selectRGOrThrow(s.canvasV2, id).fill)); - const isSelected = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier?.id === id); + const entityIdentifier = useMemo(() => ({ id, type: 'regional_guidance' }), [id]); const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true }); - const onSelect = useCallback(() => { - dispatch(entitySelected({ id, type: 'regional_guidance' })); - }, [dispatch, id]); return ( - - - {isOpen && } - + + + + {isOpen && } + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx index 784253b5d2..0f59f275df 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGActionsMenu.tsx @@ -1,37 +1,22 @@ import { Menu, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems'; import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { useAddIPAdapterToRGLayer } from 'features/controlLayers/hooks/addLayerHooks'; import { - rgDeleted, - rgMovedBackwardOne, - rgMovedForwardOne, - rgMovedToBack, - rgMovedToFront, rgNegativePromptChanged, rgPositivePromptChanged, - rgReset, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { - PiArrowCounterClockwiseBold, - PiArrowDownBold, - PiArrowLineDownBold, - PiArrowLineUpBold, - PiArrowUpBold, - PiPlusBold, - PiTrashSimpleBold, -} from 'react-icons/pi'; +import { PiPlusBold } from 'react-icons/pi'; -type Props = { - id: string; -}; - -export const RGActionsMenu = memo(({ id }: Props) => { +export const RGActionsMenu = memo(() => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); const [onAddIPAdapter, isAddIPAdapterDisabled] = useAddIPAdapterToRGLayer(id); @@ -39,13 +24,7 @@ export const RGActionsMenu = memo(({ id }: Props) => { () => createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { const rg = selectRGOrThrow(canvasV2, id); - const rgIndex = canvasV2.regions.entities.indexOf(rg); - const rgCount = canvasV2.regions.entities.length; return { - isMoveForwardOneDisabled: rgIndex < rgCount - 1, - isMoveBackardOneDisabled: rgIndex > 0, - isMoveToFrontDisabled: rgIndex < rgCount - 1, - isMoveToBackDisabled: rgIndex > 0, isAddPositivePromptDisabled: rg.positivePrompt === null, isAddNegativePromptDisabled: rg.negativePrompt === null, }; @@ -53,29 +32,11 @@ export const RGActionsMenu = memo(({ id }: Props) => { [id] ); const actions = useAppSelector(selectActionsValidity); - 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(rgPositivePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); const onAddNegativePrompt = useCallback(() => { - dispatch(rgNegativePromptChanged({ id, prompt: '' })); + dispatch(rgNegativePromptChanged({ id: id, prompt: '' })); }, [dispatch, id]); return ( @@ -92,25 +53,7 @@ export const RGActionsMenu = memo(({ id }: Props) => { {t('controlLayers.addIPAdapter')} - }> - {t('controlLayers.moveToFront')} - - }> - {t('controlLayers.moveForward')} - - }> - {t('controlLayers.moveBackward')} - - }> - {t('controlLayers.moveToBack')} - - - }> - {t('accessibility.reset')} - - } color="error.300"> - {t('common.delete')} - + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx index 866913a08c..151e378ebe 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGHeader.tsx @@ -1,50 +1,41 @@ import { Badge, Spacer } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { 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 } from 'features/controlLayers/store/canvasV2Slice'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; -import { memo, useCallback } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { RGMaskFillColorPicker } from './RGMaskFillColorPicker'; import { RGSettingsPopover } from './RGSettingsPopover'; type Props = { - id: string; - isSelected: boolean; onToggleVisibility: () => void; }; -export const RGHeader = memo(({ id, isSelected, onToggleVisibility }: Props) => { +export const RGHeader = memo(({ onToggleVisibility }: Props) => { + const { id } = useEntityIdentifierContext(); const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const isEnabled = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).isEnabled); const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); - const onToggleIsEnabled = useCallback(() => { - dispatch(rgIsEnabledToggled({ id })); - }, [dispatch, id]); - const onDelete = useCallback(() => { - dispatch(rgDeleted({ id })); - }, [dispatch, id]); return ( - - + + {autoNegative === 'invert' && ( {t('controlLayers.autoNegative')} )} - - - - + + + + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx index 471febea28..2f1489cfb0 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGMaskFillColorPicker.tsx @@ -3,25 +3,23 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import RgbColorPicker from 'common/components/RgbColorPicker'; import { rgbColorToString } from 'common/util/colorCodeTransformers'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgFillChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; 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) => { +export const RGMaskFillColorPicker = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).fill); + const fill = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).fill); const onChange = useCallback( (fill: RgbColor) => { - dispatch(rgFillChanged({ id, fill })); + dispatch(rgFillChanged({ id: entityIdentifier.id, fill })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx index 7ba5ec84a3..c13081d644 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettings.tsx @@ -1,6 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { AddPromptButtons } from 'features/controlLayers/components/AddPromptButtons'; import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import { memo } from 'react'; @@ -8,11 +9,8 @@ import { RGIPAdapters } from './RGIPAdapters'; import { RGNegativePrompt } from './RGNegativePrompt'; import { RGPositivePrompt } from './RGPositivePrompt'; -type Props = { - id: string; -}; - -export const RGSettings = memo(({ id }: Props) => { +export const RGSettings = memo(() => { + const { id } = useEntityIdentifierContext(); const hasPositivePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).positivePrompt !== null); const hasNegativePrompt = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).negativePrompt !== null); const hasIPAdapters = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).ipAdapters.length > 0); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx index cf7c157460..b82469fafc 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/RegionalGuidance/RGSettingsPopover.tsx @@ -12,6 +12,7 @@ import { } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { rgAutoNegativeChanged } from 'features/controlLayers/store/canvasV2Slice'; import { selectRGOrThrow } from 'features/controlLayers/store/regionsReducers'; import type { ChangeEvent } from 'react'; @@ -19,19 +20,16 @@ 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) => { +export const RGSettingsPopover = memo(() => { + const entityIdentifier = useEntityIdentifierContext(); const { t } = useTranslation(); const dispatch = useAppDispatch(); - const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, id).autoNegative); + const autoNegative = useAppSelector((s) => selectRGOrThrow(s.canvasV2, entityIdentifier.id).autoNegative); const onChange = useCallback( (e: ChangeEvent) => { - dispatch(rgAutoNegativeChanged({ id, autoNegative: e.target.checked ? 'invert' : 'off' })); + dispatch(rgAutoNegativeChanged({ id: entityIdentifier.id, autoNegative: e.target.checked ? 'invert' : 'off' })); }, - [dispatch, id] + [dispatch, entityIdentifier.id] ); return ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx index 6d44039127..860bf3c601 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ToolChooser.tsx @@ -13,32 +13,19 @@ import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanva export const ToolChooser: React.FC = () => { useCanvasResetLayerHotkey(); useCanvasDeleteLayerHotkey(); - const isCanvasSessionActive = useAppSelector((s) => s.canvasV2.session.isActive); const isTransforming = useAppSelector((s) => s.canvasV2.tool.isTransforming); - if (isCanvasSessionActive) { - return ( - <> - - - - - - - - - - - ); - } - return ( - - - - - - - + <> + + + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx new file mode 100644 index 0000000000..cb0094fd56 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityActionMenuItems.tsx @@ -0,0 +1,127 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { + entityArrangedBackwardOne, + entityArrangedForwardOne, + entityArrangedToBack, + entityArrangedToFront, + entityDeleted, + entityReset, + selectCanvasV2Slice, +} from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier, CanvasV2State } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiArrowCounterClockwiseBold, + PiArrowDownBold, + PiArrowLineDownBold, + PiArrowLineUpBold, + PiArrowUpBold, + PiTrashSimpleBold, +} from 'react-icons/pi'; + +const getIndexAndCount = ( + canvasV2: CanvasV2State, + { id, type }: CanvasEntityIdentifier +): { index: number; count: number } => { + if (type === 'layer') { + return { + index: canvasV2.layers.entities.findIndex((entity) => entity.id === id), + count: canvasV2.layers.entities.length, + }; + } else if (type === 'control_adapter') { + return { + index: canvasV2.controlAdapters.entities.findIndex((entity) => entity.id === id), + count: canvasV2.controlAdapters.entities.length, + }; + } else if (type === 'regional_guidance') { + return { + index: canvasV2.regions.entities.findIndex((entity) => entity.id === id), + count: canvasV2.regions.entities.length, + }; + } else { + return { + index: -1, + count: 0, + }; + } +}; + +export const CanvasEntityActionMenuItems = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const selectValidActions = useMemo( + () => + createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => { + const { index, count } = getIndexAndCount(canvasV2, entityIdentifier); + return { + isArrangeable: + entityIdentifier.type === 'layer' || + entityIdentifier.type === 'control_adapter' || + entityIdentifier.type === 'regional_guidance', + isDeleteable: entityIdentifier.type !== 'inpaint_mask', + canMoveForwardOne: index < count - 1, + canMoveBackwardOne: index > 0, + canMoveToFront: index < count - 1, + canMoveToBack: index > 0, + }; + }), + [entityIdentifier] + ); + + const validActions = useAppSelector(selectValidActions); + + const deleteEntity = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const resetEntity = useCallback(() => { + dispatch(entityReset({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveForwardOne = useCallback(() => { + dispatch(entityArrangedForwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToFront = useCallback(() => { + dispatch(entityArrangedToFront({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveBackwardOne = useCallback(() => { + dispatch(entityArrangedBackwardOne({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + const moveToBack = useCallback(() => { + dispatch(entityArrangedToBack({ entityIdentifier })); + }, [dispatch, entityIdentifier]); + + return ( + <> + {validActions.isArrangeable && ( + <> + }> + {t('controlLayers.moveToFront')} + + }> + {t('controlLayers.moveForward')} + + }> + {t('controlLayers.moveBackward')} + + }> + {t('controlLayers.moveToBack')} + + + )} + }> + {t('accessibility.reset')} + + {validActions.isDeleteable && ( + } color="error.300"> + {t('common.delete')} + + )} + + ); +}); + +CanvasEntityActionMenuItems.displayName = 'CanvasEntityActionMenuItems'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx index 9093086fbd..550ba0d020 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityContainer.tsx @@ -1,22 +1,23 @@ -import type { ChakraProps } from '@invoke-ai/ui-library'; import { Flex } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor'; +import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { entitySelected } from 'features/controlLayers/store/canvasV2Slice'; import type { PropsWithChildren } from 'react'; import { memo, useCallback } from 'react'; -type Props = PropsWithChildren<{ - isSelected: boolean; - onSelect: () => void; - selectedBorderColor?: ChakraProps['bg']; -}>; - -export const CanvasEntityContainer = memo((props: Props) => { - const { isSelected, onSelect, selectedBorderColor = 'base.400', children } = props; - const _onSelect = useCallback(() => { +export const CanvasEntityContainer = memo((props: PropsWithChildren) => { + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const isSelected = useIsEntitySelected(entityIdentifier); + const selectionColor = useEntitySelectionColor(entityIdentifier); + const onClick = useCallback(() => { if (isSelected) { return; } - onSelect(); - }, [isSelected, onSelect]); + dispatch(entitySelected({ entityIdentifier })); + }, [dispatch, entityIdentifier, isSelected]); return ( { flexDir="column" w="full" bg={isSelected ? 'base.800' : 'base.850'} - onClick={_onSelect} + onClick={onClick} borderInlineStartWidth={5} - borderColor={isSelected ? selectedBorderColor : 'base.800'} + borderColor={isSelected ? selectionColor : 'base.800'} borderRadius="base" > - {children} + {props.children} ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx index 1cbb0fa29a..c51410413b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityDeleteButton.tsx @@ -1,13 +1,19 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { entityDeleted } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -type Props = { onDelete: () => void }; - -export const CanvasEntityDeleteButton = memo(({ onDelete }: Props) => { +export const CanvasEntityDeleteButton = memo(() => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const entityIdentifier = useEntityIdentifierContext(); + const onClick = useCallback(() => { + dispatch(entityDeleted({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( { aria-label={t('common.delete')} tooltip={t('common.delete')} icon={} - onClick={onDelete} + onClick={onClick} onDoubleClick={stopPropagation} // double click expands the layer /> ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx index eaa41fcfe9..e33a5ce9c3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityEnabledToggle.tsx @@ -1,16 +1,21 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; import { stopPropagation } from 'common/util/stopPropagation'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled'; +import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasV2Slice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; -type Props = { - isEnabled: boolean; - onToggle: () => void; -}; - -export const CanvasEntityEnabledToggle = memo(({ isEnabled, onToggle }: Props) => { +export const CanvasEntityEnabledToggle = memo(() => { const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isEnabled = useEntityIsEnabled(entityIdentifier); + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(entityIsEnabledToggled({ entityIdentifier })); + }, [dispatch, entityIdentifier]); return ( : undefined} - onClick={onToggle} + onClick={onClick} colorScheme="base" onDoubleClick={stopPropagation} // double click expands the layer /> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx index 1fc3d6e9f0..cbb48fa246 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityTitle.tsx @@ -1,12 +1,30 @@ import { Text } from '@invoke-ai/ui-library'; -import { memo } from 'react'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useIsEntitySelected } from 'features/controlLayers/hooks/useIsEntitySelected'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { assert } from 'tsafe'; -type Props = { - title: string; - isSelected: boolean; -}; +export const CanvasEntityTitle = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const isSelected = useIsEntitySelected(entityIdentifier); + const title = useMemo(() => { + if (entityIdentifier.type === 'inpaint_mask') { + return t('controlLayers.inpaintMask'); + } else if (entityIdentifier.type === 'control_adapter') { + return t('controlLayers.globalControlAdapter'); + } else if (entityIdentifier.type === 'layer') { + return t('controlLayers.layer'); + } else if (entityIdentifier.type === 'ip_adapter') { + return t('controlLayers.ipAdapter'); + } else if (entityIdentifier.type === 'regional_guidance') { + return t('controlLayers.regionalGuidance'); + } else { + assert(false, 'Unexpected entity type'); + } + }, [entityIdentifier.type, t]); -export const CanvasEntityTitle = memo(({ title, isSelected }: Props) => { return ( {title} diff --git a/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts new file mode 100644 index 0000000000..04489e8e9d --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/contexts/EntityIdentifierContext.ts @@ -0,0 +1,11 @@ +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { createContext, useContext } from 'react'; +import { assert } from 'tsafe'; + +export const EntityIdentifierContext = createContext(null); + +export const useEntityIdentifierContext = (): CanvasEntityIdentifier => { + const entityIdentifier = useContext(EntityIdentifierContext); + assert(entityIdentifier, 'useEntityIdentifier must be used within a EntityIdentifierProvider'); + return entityIdentifier; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts index 1e2fb57901..ca5607c97c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasDeleteLayerHotkey.ts @@ -2,10 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { - caDeleted, - ipaDeleted, - layerDeleted, - rgDeleted, + entityDeleted, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; @@ -26,19 +23,7 @@ export function useCanvasDeleteLayerHotkey() { if (selectedEntityIdentifier === null) { return; } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerDeleted({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgDeleted({ id })); - } - if (type === 'control_adapter') { - dispatch(caDeleted({ id })); - } - if (type === 'ip_adapter') { - dispatch(ipaDeleted({ id })); - } + dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier })); }, [dispatch, selectedEntityIdentifier]); const isDeleteEnabled = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts index 2d1c3c74f0..1a8301f204 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasResetLayerHotkey.ts @@ -2,9 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { - imReset, - layerReset, - rgReset, + entityReset, selectCanvasV2Slice, } from 'features/controlLayers/store/canvasV2Slice'; import { useCallback, useMemo } from 'react'; @@ -25,16 +23,7 @@ export function useCanvasResetLayerHotkey() { if (selectedEntityIdentifier === null) { return; } - const { type, id } = selectedEntityIdentifier; - if (type === 'layer') { - dispatch(layerReset({ id })); - } - if (type === 'regional_guidance') { - dispatch(rgReset({ id })); - } - if (type === 'inpaint_mask') { - dispatch(imReset()); - } + dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier })); }, [dispatch, selectedEntityIdentifier]); const isResetEnabled = useMemo( diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts new file mode 100644 index 0000000000..e37b402ea6 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntityIsEnabled.ts @@ -0,0 +1,22 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntityIsEnabled = (entityIdentifier: CanvasEntityIdentifier) => { + const selectIsEnabled = useMemo( + () => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return false; + } else { + return entity.isEnabled; + } + }), + [entityIdentifier] + ); + const isEnabled = useAppSelector(selectIsEnabled); + return isEnabled; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts new file mode 100644 index 0000000000..48f5698589 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useEntitySelectionColor.ts @@ -0,0 +1,27 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { rgbColorToString } from 'common/util/colorCodeTransformers'; +import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useEntitySelectionColor = (entityIdentifier: CanvasEntityIdentifier) => { + const selectSelectionColor = useMemo( + () => + createSelector(selectCanvasV2Slice, (canvasV2) => { + const entity = selectEntity(canvasV2, entityIdentifier); + if (!entity) { + return 'base.400'; + } else if (entity.type === 'inpaint_mask') { + return rgbColorToString(entity.fill); + } else if (entity.type === 'regional_guidance') { + return rgbColorToString(entity.fill); + } else { + return 'base.400'; + } + }), + [entityIdentifier] + ); + const selectionColor = useAppSelector(selectSelectionColor); + return selectionColor; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts new file mode 100644 index 0000000000..8d5c3ca86f --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useIsEntitySelected.ts @@ -0,0 +1,12 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; +import { useMemo } from 'react'; + +export const useIsEntitySelected = (entityIdentifier: CanvasEntityIdentifier) => { + const selectedEntityIdentifier = useAppSelector((s) => s.canvasV2.selectedEntityIdentifier); + const isSelected = useMemo(() => { + return selectedEntityIdentifier?.id === entityIdentifier.id; + }, [selectedEntityIdentifier, entityIdentifier.id]); + + return isSelected; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts index c9331df190..7e72ef0ca7 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBbox.ts @@ -218,16 +218,9 @@ export class CanvasBbox { } render() { - const session = this.manager.stateApi.getSession(); const bbox = this.manager.stateApi.getBbox(); const toolState = this.manager.stateApi.getToolState(); - if (!session.isActive) { - this.konva.group.listening(false); - this.konva.group.visible(false); - return; - } - this.konva.group.visible(true); this.konva.group.listening(toolState.selected === 'bbox'); this.konva.rect.setAttrs({ diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts index 7eae3240bc..511ab0e894 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasLayerAdapter.ts @@ -2,7 +2,12 @@ import { deepClone } from 'common/util/deepClone'; import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; -import type { CanvasLayerState, CanvasV2State, GetLoggingContext } from 'features/controlLayers/store/types'; +import type { + CanvasEntityIdentifier, + CanvasLayerState, + CanvasV2State, + GetLoggingContext, +} from 'features/controlLayers/store/types'; import Konva from 'konva'; import { get } from 'lodash-es'; import type { Logger } from 'roarr'; @@ -48,6 +53,13 @@ export class CanvasLayerAdapter { this.state = state; } + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return { id: this.id, type: this.type }; + }; + destroy = (): void => { this.log.debug('Destroying layer'); // We need to call the destroy method on all children so they can do their own cleanup. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts index 9481fa2857..13318f240d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasMaskAdapter.ts @@ -2,6 +2,7 @@ import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import { CanvasObjectRenderer } from 'features/controlLayers/konva/CanvasObjectRenderer'; import { CanvasTransformer } from 'features/controlLayers/konva/CanvasTransformer'; import type { + CanvasEntityIdentifier, CanvasInpaintMaskState, CanvasRegionalGuidanceState, CanvasV2State, @@ -54,6 +55,13 @@ export class CanvasMaskAdapter { this.maskOpacity = this.manager.stateApi.getMaskOpacity(); } + /** + * Get this entity's entity identifier + */ + getEntityIdentifier = (): CanvasEntityIdentifier => { + return { id: this.id, type: this.type }; + }; + destroy = (): void => { this.log.debug('Destroying mask'); // We need to call the destroy method on all children so they can do their own cleanup. diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts index 2e4c2b8194..aa82ba2567 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasObjectRenderer.ts @@ -8,11 +8,7 @@ import type { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLaye import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager'; import type { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter'; import { CanvasRectRenderer } from 'features/controlLayers/konva/CanvasRect'; -import { - getPrefixedId, - konvaNodeToBlob, - previewBlob, -} from 'features/controlLayers/konva/util'; +import { getPrefixedId, konvaNodeToBlob, previewBlob } from 'features/controlLayers/konva/util'; import { type CanvasBrushLineState, type CanvasEraserLineState, @@ -299,11 +295,17 @@ export class CanvasObjectRenderer { this.buffer.id = getPrefixedId(this.buffer.type); if (this.buffer.type === 'brush_line') { - this.manager.stateApi.addBrushLine({ id: this.parent.id, brushLine: this.buffer }, this.parent.type); + this.manager.stateApi.addBrushLine({ + entityIdentifier: this.parent.getEntityIdentifier(), + brushLine: this.buffer, + }); } else if (this.buffer.type === 'eraser_line') { - this.manager.stateApi.addEraserLine({ id: this.parent.id, eraserLine: this.buffer }, this.parent.type); + this.manager.stateApi.addEraserLine({ + entityIdentifier: this.parent.getEntityIdentifier(), + eraserLine: this.buffer, + }); } else if (this.buffer.type === 'rect') { - this.manager.stateApi.addRect({ id: this.parent.id, rect: this.buffer }, this.parent.type); + this.manager.stateApi.addRect({ entityIdentifier: this.parent.getEntityIdentifier(), rect: this.buffer }); } else { this.log.warn({ buffer: this.buffer }, 'Invalid buffer object type'); } @@ -356,10 +358,11 @@ export class CanvasObjectRenderer { const imageDTO = await uploadImage(blob, `${this.id}_rasterized.png`, 'other', true); const imageObject = imageDTOToImageObject(imageDTO); await this.renderObject(imageObject, true); - this.manager.stateApi.rasterizeEntity( - { id: this.parent.id, imageObject, position: { x: Math.round(rect.x), y: Math.round(rect.y) } }, - this.parent.type - ); + this.manager.stateApi.rasterizeEntity({ + entityIdentifier: this.parent.getEntityIdentifier(), + imageObject, + position: { x: Math.round(rect.x), y: Math.round(rect.y) }, + }); }; /** diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts index 6d6b236855..844b7a1b13 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApi.ts @@ -15,44 +15,31 @@ import { $stageAttrs, bboxChanged, brushWidthChanged, - caTranslated, + entityBrushLineAdded, + entityEraserLineAdded, + entityMoved, + entityRasterized, + entityRectAdded, + entityReset, entitySelected, eraserWidthChanged, - imBrushLineAdded, - imEraserLineAdded, imImageCacheChanged, - imMoved, - imRectAdded, - inpaintMaskRasterized, - layerBrushLineAdded, - layerEraserLineAdded, layerImageCacheChanged, - layerRasterized, - layerRectAdded, - layerReset, - layerTranslated, - rgBrushLineAdded, - rgEraserLineAdded, rgImageCacheChanged, - rgMoved, - rgRasterized, - rgRectAdded, toolBufferChanged, toolChanged, } from 'features/controlLayers/store/canvasV2Slice'; import type { - CanvasBrushLineState, - CanvasEntityIdentifier, - CanvasEntityState, - CanvasEraserLineState, - CanvasRectState, - EntityRasterizedArg, - PositionChangedArg, + EntityBrushLineAddedPayload, + EntityEraserLineAddedPayload, + EntityIdentifierPayload, + EntityMovedPayload, + EntityRasterizedPayload, + EntityRectAddedPayload, Rect, Tool, } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; -import { assert } from 'tsafe'; const log = logger('canvas'); @@ -69,67 +56,31 @@ export class CanvasStateApi { getState = () => { return this._store.getState().canvasV2; }; - resetEntity = (arg: { id: string }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Resetting entity'); - if (entityType === 'layer') { - this._store.dispatch(layerReset(arg)); - } + resetEntity = (arg: EntityIdentifierPayload) => { + log.trace(arg, 'Resetting entity'); + this._store.dispatch(entityReset(arg)); }; - setEntityPosition = (arg: PositionChangedArg, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Setting entity position'); - if (entityType === 'layer') { - this._store.dispatch(layerTranslated(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgMoved(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imMoved(arg)); - } else if (entityType === 'control_adapter') { - this._store.dispatch(caTranslated(arg)); - } + setEntityPosition = (arg: EntityMovedPayload) => { + log.trace(arg, 'Setting entity position'); + this._store.dispatch(entityMoved(arg)); }; - addBrushLine = (arg: { id: string; brushLine: CanvasBrushLineState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding brush line'); - if (entityType === 'layer') { - this._store.dispatch(layerBrushLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgBrushLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imBrushLineAdded(arg)); - } + addBrushLine = (arg: EntityBrushLineAddedPayload) => { + log.trace(arg, 'Adding brush line'); + this._store.dispatch(entityBrushLineAdded(arg)); }; - addEraserLine = (arg: { id: string; eraserLine: CanvasEraserLineState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding eraser line'); - if (entityType === 'layer') { - this._store.dispatch(layerEraserLineAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgEraserLineAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imEraserLineAdded(arg)); - } + addEraserLine = (arg: EntityEraserLineAddedPayload) => { + log.trace(arg, 'Adding eraser line'); + this._store.dispatch(entityEraserLineAdded(arg)); }; - addRect = (arg: { id: string; rect: CanvasRectState }, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Adding rect'); - if (entityType === 'layer') { - this._store.dispatch(layerRectAdded(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgRectAdded(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(imRectAdded(arg)); - } + addRect = (arg: EntityRectAddedPayload) => { + log.trace(arg, 'Adding rect'); + this._store.dispatch(entityRectAdded(arg)); }; - rasterizeEntity = (arg: EntityRasterizedArg, entityType: CanvasEntityState['type']) => { - log.trace({ arg, entityType }, 'Rasterizing entity'); - if (entityType === 'layer') { - this._store.dispatch(layerRasterized(arg)); - } else if (entityType === 'inpaint_mask') { - this._store.dispatch(inpaintMaskRasterized(arg)); - } else if (entityType === 'regional_guidance') { - this._store.dispatch(rgRasterized(arg)); - } else { - assert(false, 'Rasterizing not supported for this entity type'); - } + rasterizeEntity = (arg: EntityRasterizedPayload) => { + log.trace(arg, 'Rasterizing entity'); + this._store.dispatch(entityRasterized(arg)); }; - setSelectedEntity = (arg: CanvasEntityIdentifier) => { + setSelectedEntity = (arg: EntityIdentifierPayload) => { log.trace({ arg }, 'Setting selected entity'); this._store.dispatch(entitySelected(arg)); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts index cf6960052b..8ee2ba6ffb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasTransformer.ts @@ -356,7 +356,7 @@ export class CanvasTransformer { }; this.log.trace({ position }, 'Position changed'); - this.manager.stateApi.setEntityPosition({ id: this.parent.id, position }, this.parent.type); + this.manager.stateApi.setEntityPosition({ entityIdentifier: this.parent.getEntityIdentifier(), position }); }); this.subscriptions.add( @@ -600,7 +600,7 @@ export class CanvasTransformer { // We shouldn't reset on the first render - the bbox will be calculated on the next render if (!this.parent.renderer.hasObjects()) { // The layer is fully transparent but has objects - reset it - this.manager.stateApi.resetEntity({ id: this.parent.id }, this.parent.type); + this.manager.stateApi.resetEntity({ entityIdentifier: this.parent.getEntityIdentifier() }); } this.syncInteractionState(); return; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8252f4627f..4159d9f7da 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { deepClone } from 'common/util/deepClone'; import { bboxReducers } from 'features/controlLayers/store/bboxReducers'; import { compositingReducers } from 'features/controlLayers/store/compositingReducers'; @@ -20,8 +21,20 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { pick } from 'lodash-es'; import { atom } from 'nanostores'; import type { InvocationDenoiseProgressEvent } from 'services/events/types'; +import { assert } from 'tsafe'; -import type { CanvasEntityIdentifier, CanvasV2State, Coordinate, StageAttrs } from './types'; +import type { + CanvasEntityIdentifier, + CanvasV2State, + Coordinate, + EntityBrushLineAddedPayload, + EntityEraserLineAddedPayload, + EntityIdentifierPayload, + EntityMovedPayload, + EntityRasterizedPayload, + EntityRectAddedPayload, + StageAttrs, +} from './types'; import { RGBA_RED } from './types'; const initialState: CanvasV2State = { @@ -122,6 +135,23 @@ const initialState: CanvasV2State = { }, }; +export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIdentifier) { + switch (type) { + case 'layer': + return state.layers.entities.find((layer) => layer.id === id); + case 'control_adapter': + return state.controlAdapters.entities.find((ca) => ca.id === id); + case 'inpaint_mask': + return state.inpaintMask; + case 'regional_guidance': + return state.regions.entities.find((rg) => rg.id === id); + case 'ip_adapter': + return state.ipAdapters.entities.find((ip) => ip.id === id); + default: + return; + } +} + export const canvasV2Slice = createSlice({ name: 'canvasV2', initialState, @@ -138,8 +168,184 @@ export const canvasV2Slice = createSlice({ ...bboxReducers, ...inpaintMaskReducers, ...sessionReducers, - entitySelected: (state, action: PayloadAction) => { - state.selectedEntityIdentifier = action.payload; + entitySelected: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + state.selectedEntityIdentifier = entityIdentifier; + }, + entityReset: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.isEnabled = true; + entity.objects = []; + entity.position = { x: 0, y: 0 }; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.isEnabled = true; + entity.objects = []; + entity.position = { x: 0, y: 0 }; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityIsEnabledToggled: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + entity.isEnabled = !entity.isEnabled; + }, + entityMoved: (state, action: PayloadAction) => { + const { entityIdentifier, position } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.position = position; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.position = position; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityRasterized: (state, action: PayloadAction) => { + const { entityIdentifier, imageObject, position } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects = [imageObject]; + entity.position = position; + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects = [imageObject]; + entity.position = position; + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityBrushLineAdded: (state, action: PayloadAction) => { + const { entityIdentifier, brushLine } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(brushLine); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(brushLine); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityEraserLineAdded: (state, action: PayloadAction) => { + const { entityIdentifier, eraserLine } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(eraserLine); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(eraserLine); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityRectAdded: (state, action: PayloadAction) => { + const { entityIdentifier, rect } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } else if (entity.type === 'layer') { + entity.objects.push(rect); + state.layers.imageCache = null; + } else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') { + entity.objects.push(rect); + entity.imageCache = null; + } else { + assert(false, 'Not implemented'); + } + }, + entityDeleted: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + if (entityIdentifier.type === 'layer') { + state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id); + state.layers.imageCache = null; + } else if (entityIdentifier.type === 'regional_guidance') { + state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id); + } else { + assert(false, 'Not implemented'); + } + }, + entityArrangedForwardOne: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveOneToEnd(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveOneToEnd(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveOneToEnd(state.controlAdapters.entities, entity); + } + }, + entityArrangedToFront: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveToEnd(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveToEnd(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveToEnd(state.controlAdapters.entities, entity); + } + }, + entityArrangedBackwardOne: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveOneToStart(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveOneToStart(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveOneToStart(state.controlAdapters.entities, entity); + } + }, + entityArrangedToBack: (state, action: PayloadAction) => { + const { entityIdentifier } = action.payload; + const entity = selectEntity(state, entityIdentifier); + if (!entity) { + return; + } + if (entity.type === 'layer') { + moveToStart(state.layers.entities, entity); + state.layers.imageCache = null; + } else if (entity.type === 'regional_guidance') { + moveToStart(state.regions.entities, entity); + } else if (entity.type === 'control_adapter') { + moveToStart(state.controlAdapters.entities, entity); + } }, allEntitiesDeleted: (state) => { state.regions.entities = []; @@ -176,10 +382,23 @@ export const { toolChanged, toolBufferChanged, maskOpacityChanged, - entitySelected, allEntitiesDeleted, clipToBboxChanged, canvasReset, + // All entities + entitySelected, + entityReset, + entityIsEnabledToggled, + entityMoved, + entityRasterized, + entityBrushLineAdded, + entityEraserLineAdded, + entityRectAdded, + entityDeleted, + entityArrangedForwardOne, + entityArrangedToFront, + entityArrangedBackwardOne, + entityArrangedToBack, // bbox bboxChanged, bboxScaledSizeChanged, @@ -193,23 +412,10 @@ export const { // layers layerAdded, layerRecalled, - layerDeleted, - layerReset, - layerMovedForwardOne, - layerMovedToFront, - layerMovedBackwardOne, - layerMovedToBack, - layerIsEnabledToggled, layerOpacityChanged, - layerTranslated, - layerBboxChanged, layerImageAdded, layerAllDeleted, layerImageCacheChanged, - layerBrushLineAdded, - layerEraserLineAdded, - layerRectAdded, - layerRasterized, // IP Adapters ipaAdded, ipaRecalled, @@ -224,16 +430,8 @@ export const { ipaBeginEndStepPctChanged, // Control Adapters caAdded, - caBboxChanged, - caDeleted, caAllDeleted, - caIsEnabledToggled, - caMovedBackwardOne, - caMovedForwardOne, - caMovedToBack, - caMovedToFront, caOpacityChanged, - caTranslated, caRecalled, caImageChanged, caProcessedImageChanged, @@ -244,19 +442,10 @@ export const { caProcessorPendingBatchIdChanged, caWeightChanged, caBeginEndStepPctChanged, - caScaled, // Regions rgAdded, rgRecalled, - rgReset, - rgIsEnabledToggled, - rgMoved, - rgDeleted, rgAllDeleted, - rgMovedForwardOne, - rgMovedToFront, - rgMovedBackwardOne, - rgMovedToBack, rgPositivePromptChanged, rgNegativePromptChanged, rgFillChanged, @@ -270,10 +459,6 @@ export const { rgIPAdapterMethodChanged, rgIPAdapterModelChanged, rgIPAdapterCLIPVisionModelChanged, - rgBrushLineAdded, - rgEraserLineAdded, - rgRectAdded, - rgRasterized, // Compositing setInfillMethod, setInfillTileSize, @@ -319,16 +504,9 @@ export const { loraIsEnabledChanged, loraAllDeleted, // Inpaint mask - imReset, imRecalled, - imIsEnabledToggled, - imMoved, imFillChanged, imImageCacheChanged, - imBrushLineAdded, - imEraserLineAdded, - imRectAdded, - inpaintMaskRasterized, // Staging sessionStartedStaging, sessionImageStaged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts index aa77102263..ac819399da 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/controlAdaptersReducers.ts @@ -1,24 +1,20 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { zModelIdentifierField } from 'features/nodes/types/common'; -import type { IRect } from 'konva/lib/types'; import { isEqual } from 'lodash-es'; import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types'; import { assert } from 'tsafe'; import { v4 as uuidv4 } from 'uuid'; import type { - CanvasV2State, CanvasControlAdapterState, + CanvasControlNetState, + CanvasT2IAdapterState, + CanvasV2State, ControlModeV2, ControlNetConfig, - CanvasControlNetState, Filter, - PositionChangedArg, ProcessorConfig, - ScaleChangedArg, T2IAdapterConfig, - CanvasT2IAdapterState, } from './types'; import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types'; @@ -56,58 +52,6 @@ export const controlAdaptersReducers = { state.controlAdapters.entities.push(data); state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id }; }, - caIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.isEnabled = !ca.isEnabled; - }, - caTranslated: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.position = position; - }, - caScaled: (state, action: PayloadAction) => { - const { id, scale, position } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - if (ca.imageObject) { - ca.imageObject.x *= scale; - ca.imageObject.y *= scale; - ca.imageObject.height *= scale; - ca.imageObject.width *= scale; - } - - if (ca.processedImageObject) { - ca.processedImageObject.x *= scale; - ca.processedImageObject.y *= scale; - ca.processedImageObject.height *= scale; - ca.processedImageObject.width *= scale; - } - ca.position = position; - ca.bboxNeedsUpdate = true; - state.layers.imageCache = null; - }, - caBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - ca.bbox = bbox; - ca.bboxNeedsUpdate = false; - }, - caDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.controlAdapters.entities = state.controlAdapters.entities.filter((ca) => ca.id !== id); - }, caAllDeleted: (state) => { state.controlAdapters.entities = []; }, @@ -119,38 +63,6 @@ export const controlAdaptersReducers = { } ca.opacity = opacity; }, - caMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToEnd(state.controlAdapters.entities, ca); - }, - caMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToEnd(state.controlAdapters.entities, ca); - }, - caMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveOneToStart(state.controlAdapters.entities, ca); - }, - caMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const ca = selectCA(state, id); - if (!ca) { - return; - } - moveToStart(state.controlAdapters.entities, ca); - }, caImageChanged: { reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => { const { id, imageDTO, objectId } = action.payload; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 416c6bd341..ae7e52805d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,35 +1,16 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasInpaintMaskState, - CanvasRectState, - CanvasV2State, - Coordinate, - EntityRasterizedArg, -} from 'features/controlLayers/store/types'; +import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types'; import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import type { ImageDTO } from 'services/api/types'; import type { RgbColor } from './types'; export const inpaintMaskReducers = { - imReset: (state) => { - state.inpaintMask.objects = []; - state.inpaintMask.imageCache = null; - }, imRecalled: (state, action: PayloadAction<{ data: CanvasInpaintMaskState }>) => { const { data } = action.payload; state.inpaintMask = data; state.selectedEntityIdentifier = { type: 'inpaint_mask', id: data.id }; }, - imIsEnabledToggled: (state) => { - state.inpaintMask.isEnabled = !state.inpaintMask.isEnabled; - }, - imMoved: (state, action: PayloadAction<{ position: Coordinate }>) => { - const { position } = action.payload; - state.inpaintMask.position = position; - }, imFillChanged: (state, action: PayloadAction<{ fill: RgbColor }>) => { const { fill } = action.payload; state.inpaintMask.fill = fill; @@ -38,25 +19,4 @@ export const inpaintMaskReducers = { const { imageDTO } = action.payload; state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - imBrushLineAdded: (state, action: PayloadAction<{ brushLine: CanvasBrushLineState }>) => { - const { brushLine } = action.payload; - state.inpaintMask.objects.push(brushLine); - state.layers.imageCache = null; - }, - imEraserLineAdded: (state, action: PayloadAction<{ eraserLine: CanvasEraserLineState }>) => { - const { eraserLine } = action.payload; - state.inpaintMask.objects.push(eraserLine); - state.layers.imageCache = null; - }, - imRectAdded: (state, action: PayloadAction<{ rect: CanvasRectState }>) => { - const { rect } = action.payload; - state.inpaintMask.objects.push(rect); - state.layers.imageCache = null; - }, - inpaintMaskRasterized: (state, action: PayloadAction) => { - const { imageObject, position } = action.payload; - state.inpaintMask.objects = [imageObject]; - state.inpaintMask.position = position; - state.inpaintMask.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts index 4a00aa821f..78be3cf6f1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/layersReducers.ts @@ -1,21 +1,10 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { IRect } from 'konva/lib/types'; import { merge } from 'lodash-es'; import type { ImageDTO } from 'services/api/types'; import { assert } from 'tsafe'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasLayerState, - CanvasRectState, - CanvasV2State, - EntityRasterizedArg, - ImageObjectAddedArg, - PositionChangedArg, -} from './types'; +import type { CanvasLayerState, CanvasV2State, ImageObjectAddedArg } from './types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from './types'; export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id); @@ -52,52 +41,6 @@ export const layersReducers = { state.selectedEntityIdentifier = { type: 'layer', id: data.id }; state.layers.imageCache = null; }, - layerIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = !layer.isEnabled; - state.layers.imageCache = null; - }, - layerTranslated: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.position = position; - state.layers.imageCache = null; - }, - layerBboxChanged: (state, action: PayloadAction<{ id: string; bbox: IRect | null }>) => { - const { id, bbox } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - if (bbox === null) { - // TODO(psyche): Clear objects when bbox is cleared - right now this doesn't work bc bbox calculation for layers - // doesn't work - always returns null - // layer.objects = []; - } - }, - layerReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.isEnabled = true; - layer.objects = []; - state.layers.imageCache = null; - layer.position = { x: 0, y: 0 }; - }, - layerDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.layers.entities = state.layers.entities.filter((l) => l.id !== id); - state.layers.imageCache = null; - }, layerAllDeleted: (state) => { state.layers.entities = []; state.layers.imageCache = null; @@ -111,72 +54,6 @@ export const layersReducers = { layer.opacity = opacity; state.layers.imageCache = null; }, - layerMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToEnd(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToEnd(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveOneToStart(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - moveToStart(state.layers.entities, layer); - state.layers.imageCache = null; - }, - layerBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { - const { id, brushLine } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(brushLine); - state.layers.imageCache = null; - }, - layerEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { - const { id, eraserLine } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(eraserLine); - state.layers.imageCache = null; - }, - layerRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { - const { id, rect } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - - layer.objects.push(rect); - state.layers.imageCache = null; - }, layerImageAdded: ( state, action: PayloadAction @@ -198,14 +75,4 @@ export const layersReducers = { const { imageDTO } = action.payload; state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null; }, - layerRasterized: (state, action: PayloadAction) => { - const { id, imageObject, position } = action.payload; - const layer = selectLayer(state, id); - if (!layer) { - return; - } - layer.objects = [imageObject]; - layer.position = position; - state.layers.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 6d20996776..2b3a96be8d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -1,16 +1,6 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getPrefixedId } from 'features/controlLayers/konva/util'; -import type { - CanvasBrushLineState, - CanvasEraserLineState, - CanvasRectState, - CanvasV2State, - CLIPVisionModelV2, - EntityRasterizedArg, - IPMethodV2, - PositionChangedArg, -} from 'features/controlLayers/store/types'; +import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; @@ -71,73 +61,14 @@ export const regionsReducers = { }, prepare: () => ({ payload: { id: getPrefixedId('regional_guidance') } }), }, - rgReset: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects = []; - rg.imageCache = null; - }, rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => { const { data } = action.payload; state.regions.entities.push(data); state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id }; }, - rgIsEnabledToggled: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.isEnabled = !rg.isEnabled; - } - }, - rgMoved: (state, action: PayloadAction) => { - const { id, position } = action.payload; - const rg = selectRG(state, id); - if (rg) { - rg.position = position; - } - }, - rgDeleted: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - state.regions.entities = state.regions.entities.filter((ca) => ca.id !== id); - }, rgAllDeleted: (state) => { state.regions.entities = []; }, - rgMovedForwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToEnd(state.regions.entities, rg); - }, - rgMovedToFront: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToEnd(state.regions.entities, rg); - }, - rgMovedBackwardOne: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveOneToStart(state.regions.entities, rg); - }, - rgMovedToBack: (state, action: PayloadAction<{ id: string }>) => { - const { id } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - moveToStart(state.regions.entities, rg); - }, rgPositivePromptChanged: (state, action: PayloadAction<{ id: string; prompt: string | null }>) => { const { id, prompt } = action.payload; const rg = selectRG(state, id); @@ -286,44 +217,4 @@ export const regionsReducers = { } ipa.clipVisionModel = clipVisionModel; }, - rgBrushLineAdded: (state, action: PayloadAction<{ id: string; brushLine: CanvasBrushLineState }>) => { - const { id, brushLine } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(brushLine); - state.layers.imageCache = null; - }, - rgEraserLineAdded: (state, action: PayloadAction<{ id: string; eraserLine: CanvasEraserLineState }>) => { - const { id, eraserLine } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(eraserLine); - state.layers.imageCache = null; - }, - rgRectAdded: (state, action: PayloadAction<{ id: string; rect: CanvasRectState }>) => { - const { id, rect } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - - rg.objects.push(rect); - state.layers.imageCache = null; - }, - rgRasterized: (state, action: PayloadAction) => { - const { id, imageObject, position } = action.payload; - const rg = selectRG(state, id); - if (!rg) { - return; - } - rg.objects = [imageObject]; - rg.position = position; - rg.imageCache = null; - }, } satisfies SliceCaseReducers; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index e1a32fd22e..0d67d5d41f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -924,11 +924,20 @@ export type PositionChangedArg = { id: string; position: Coordinate }; export type ScaleChangedArg = { id: string; scale: Coordinate; position: Coordinate }; export type BboxChangedArg = { id: string; bbox: Rect | null }; -export type BrushLineAddedArg = { id: string; brushLine: CanvasBrushLineState }; -export type EraserLineAddedArg = { id: string; eraserLine: CanvasEraserLineState }; -export type RectAddedArg = { id: string; rect: CanvasRectState }; +export type EntityIdentifierPayload = { entityIdentifier: CanvasEntityIdentifier }; +export type EntityMovedPayload = { entityIdentifier: CanvasEntityIdentifier; position: Coordinate }; +export type EntityBrushLineAddedPayload = { entityIdentifier: CanvasEntityIdentifier; brushLine: CanvasBrushLineState }; +export type EntityEraserLineAddedPayload = { + entityIdentifier: CanvasEntityIdentifier; + eraserLine: CanvasEraserLineState; +}; +export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier; rect: CanvasRectState }; +export type EntityRasterizedPayload = { + entityIdentifier: CanvasEntityIdentifier; + imageObject: CanvasImageState; + position: Coordinate; +}; export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate }; -export type EntityRasterizedArg = { id: string; imageObject: CanvasImageState; position: Coordinate }; //#region Type guards export const isLine = (obj: CanvasObjectState): obj is CanvasBrushLineState | CanvasEraserLineState => {