diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx index cd3cafa12a..8f90797fd9 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNet.tsx @@ -1,10 +1,9 @@ -import { Box, ChakraProps, Flex, useColorMode } from '@chakra-ui/react'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { Box, Flex, useColorMode } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { memo, useCallback } from 'react'; import { FaCopy, FaTrash } from 'react-icons/fa'; import { - ControlNetConfig, - controlNetAdded, + controlNetDuplicated, controlNetRemoved, controlNetToggled, } from '../store/controlNetSlice'; @@ -12,9 +11,13 @@ import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import { ChevronUpIcon } from '@chakra-ui/icons'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; import { useToggle } from 'react-use'; +import { mode } from 'theme/util/mode'; import { v4 as uuidv4 } from 'uuid'; import ControlNetImagePreview from './ControlNetImagePreview'; import ControlNetProcessorComponent from './ControlNetProcessorComponent'; @@ -22,30 +25,28 @@ import ParamControlNetShouldAutoConfig from './ParamControlNetShouldAutoConfig'; import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; import ParamControlNetControlMode from './parameters/ParamControlNetControlMode'; import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect'; -import { mode } from 'theme/util/mode'; - -const expandedControlImageSx: ChakraProps['sx'] = { maxH: 96 }; type ControlNetProps = { - controlNet: ControlNetConfig; + controlNetId: string; }; const ControlNet = (props: ControlNetProps) => { - const { - controlNetId, - isEnabled, - model, - weight, - beginStepPct, - endStepPct, - controlMode, - controlImage, - processedControlImage, - processorNode, - processorType, - shouldAutoConfig, - } = props.controlNet; + const { controlNetId } = props; const dispatch = useAppDispatch(); + + const selector = createSelector( + stateSelector, + ({ controlNet }) => { + const { isEnabled, shouldAutoConfig } = + controlNet.controlNets[controlNetId]; + + return { isEnabled, shouldAutoConfig }; + }, + defaultSelectorOptions + ); + + const { isEnabled, shouldAutoConfig } = useAppSelector(selector); + const [isExpanded, toggleIsExpanded] = useToggle(false); const { colorMode } = useColorMode(); const handleDelete = useCallback(() => { @@ -54,9 +55,12 @@ const ControlNet = (props: ControlNetProps) => { const handleDuplicate = useCallback(() => { dispatch( - controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet }) + controlNetDuplicated({ + sourceControlNetId: controlNetId, + newControlNetId: uuidv4(), + }) ); - }, [dispatch, props.controlNet]); + }, [controlNetId, dispatch]); const handleToggleIsEnabled = useCallback(() => { dispatch(controlNetToggled({ controlNetId })); @@ -140,14 +144,8 @@ const ControlNet = (props: ControlNetProps) => { )} - - + + {isEnabled && ( <> @@ -166,13 +164,10 @@ const ControlNet = (props: ControlNetProps) => { > @@ -187,30 +182,24 @@ const ControlNet = (props: ControlNetProps) => { }} > )} - + {isExpanded && ( <> - + )} diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx index 1321f7b0c0..f82e39d34e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetImagePreview.tsx @@ -5,42 +5,51 @@ import { TypesafeDraggableData, TypesafeDroppableData, } from 'app/components/ImageDnd/typesafeDnd'; +import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIDndImage from 'common/components/IAIDndImage'; import { memo, useCallback, useMemo, useState } from 'react'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import { PostUploadAction } from 'services/api/thunks/image'; -import { - ControlNetConfig, - controlNetImageChanged, - controlNetSelector, -} from '../store/controlNetSlice'; - -const selector = createSelector( - controlNetSelector, - (controlNet) => { - const { pendingControlImages } = controlNet; - return { pendingControlImages }; - }, - defaultSelectorOptions -); +import { controlNetImageChanged } from '../store/controlNetSlice'; type Props = { - controlNet: ControlNetConfig; + controlNetId: string; height: SystemStyleObject['h']; }; const ControlNetImagePreview = (props: Props) => { - const { height } = props; - const { - controlNetId, - controlImage: controlImageName, - processedControlImage: processedControlImageName, - processorType, - } = props.controlNet; + const { height, controlNetId } = props; const dispatch = useAppDispatch(); - const { pendingControlImages } = useAppSelector(selector); + + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => { + const { pendingControlImages } = controlNet; + const { controlImage, processedControlImage, processorType } = + controlNet.controlNets[controlNetId]; + + return { + controlImageName: controlImage, + processedControlImageName: processedControlImage, + processorType, + pendingControlImages, + }; + }, + defaultSelectorOptions + ), + [controlNetId] + ); + + const { + controlImageName, + processedControlImageName, + processorType, + pendingControlImages, + } = useAppSelector(selector); const [isMouseOverImage, setIsMouseOverImage] = useState(false); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx index 4649f89b35..863e42632c 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetProcessorComponent.tsx @@ -1,10 +1,13 @@ -import { memo } from 'react'; -import { RequiredControlNetProcessorNode } from '../store/types'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { memo, useMemo } from 'react'; import CannyProcessor from './processors/CannyProcessor'; -import HedProcessor from './processors/HedProcessor'; -import LineartProcessor from './processors/LineartProcessor'; -import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; import ContentShuffleProcessor from './processors/ContentShuffleProcessor'; +import HedProcessor from './processors/HedProcessor'; +import LineartAnimeProcessor from './processors/LineartAnimeProcessor'; +import LineartProcessor from './processors/LineartProcessor'; import MediapipeFaceProcessor from './processors/MediapipeFaceProcessor'; import MidasDepthProcessor from './processors/MidasDepthProcessor'; import MlsdImageProcessor from './processors/MlsdImageProcessor'; @@ -15,11 +18,23 @@ import ZoeDepthProcessor from './processors/ZoeDepthProcessor'; export type ControlNetProcessorProps = { controlNetId: string; - processorNode: RequiredControlNetProcessorNode; }; const ControlNetProcessorComponent = (props: ControlNetProcessorProps) => { - const { controlNetId, processorNode } = props; + const { controlNetId } = props; + + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => controlNet.controlNets[controlNetId]?.processorNode, + defaultSelectorOptions + ), + [controlNetId] + ); + + const processorNode = useAppSelector(selector); + if (processorNode.type === 'canny_image_processor') { return ( { - const { controlNetId, shouldAutoConfig } = props; + const { controlNetId } = props; const dispatch = useAppDispatch(); - const isReady = useIsReadyToInvoke(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => + controlNet.controlNets[controlNetId]?.shouldAutoConfig, + defaultSelectorOptions + ), + [controlNetId] + ); + + const shouldAutoConfig = useAppSelector(selector); + const isBusy = useAppSelector(selectIsBusy); + const handleShouldAutoConfigChanged = useCallback(() => { dispatch(controlNetAutoConfigToggled({ controlNetId })); }, [controlNetId, dispatch]); @@ -23,7 +38,7 @@ const ParamControlNetShouldAutoConfig = (props: Props) => { aria-label="Auto configure processor" isChecked={shouldAutoConfig} onChange={handleShouldAutoConfigChanged} - isDisabled={!isReady} + isDisabled={isBusy} /> ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx index 7d0c53fe40..c08ecf1bb2 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx @@ -10,13 +10,15 @@ import { RangeSliderTrack, Tooltip, } from '@chakra-ui/react'; -import { useAppDispatch } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { controlNetBeginStepPctChanged, controlNetEndStepPctChanged, } from 'features/controlNet/store/controlNetSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo, useCallback, useMemo } from 'react'; const SLIDER_MARK_STYLES: ChakraProps['sx'] = { mt: 1.5, @@ -27,17 +29,30 @@ const SLIDER_MARK_STYLES: ChakraProps['sx'] = { type Props = { controlNetId: string; - beginStepPct: number; - endStepPct: number; mini?: boolean; }; const formatPct = (v: number) => `${Math.round(v * 100)}%`; const ParamControlNetBeginEnd = (props: Props) => { - const { controlNetId, beginStepPct, mini = false, endStepPct } = props; + const { controlNetId, mini = false } = props; const dispatch = useAppDispatch(); - const { t } = useTranslation(); + + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => { + const { beginStepPct, endStepPct } = + controlNet.controlNets[controlNetId]; + return { beginStepPct, endStepPct }; + }, + defaultSelectorOptions + ), + [controlNetId] + ); + + const { beginStepPct, endStepPct } = useAppSelector(selector); const handleStepPctChanged = useCallback( (v: number[]) => { diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx index b8737004fd..07b58384e1 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetControlMode.tsx @@ -1,15 +1,17 @@ -import { useAppDispatch } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; import { ControlModes, controlNetControlModeChanged, } from 'features/controlNet/store/controlNetSlice'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; type ParamControlNetControlModeProps = { controlNetId: string; - controlMode: string; }; const CONTROL_MODE_DATA = [ @@ -22,8 +24,19 @@ const CONTROL_MODE_DATA = [ export default function ParamControlNetControlMode( props: ParamControlNetControlModeProps ) { - const { controlNetId, controlMode = false } = props; + const { controlNetId } = props; const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => controlNet.controlNets[controlNetId]?.controlMode, + defaultSelectorOptions + ), + [controlNetId] + ); + + const controlMode = useAppSelector(selector); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx index 5d091be1ef..a57de5d70e 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetProcessorSelect.tsx @@ -1,24 +1,21 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIMantineSearchableSelect, { IAISelectDataType, } from 'common/components/IAIMantineSearchableSelect'; -import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; import { configSelector } from 'features/system/store/configSelectors'; +import { selectIsBusy } from 'features/system/store/systemSelectors'; import { map } from 'lodash-es'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { CONTROLNET_PROCESSORS } from '../../store/constants'; import { controlNetProcessorTypeChanged } from '../../store/controlNetSlice'; -import { - ControlNetProcessorNode, - ControlNetProcessorType, -} from '../../store/types'; +import { ControlNetProcessorType } from '../../store/types'; type ParamControlNetProcessorSelectProps = { controlNetId: string; - processorNode: ControlNetProcessorNode; }; const selector = createSelector( @@ -54,10 +51,22 @@ const selector = createSelector( const ParamControlNetProcessorSelect = ( props: ParamControlNetProcessorSelectProps ) => { - const { controlNetId, processorNode } = props; const dispatch = useAppDispatch(); - const isReady = useIsReadyToInvoke(); + const { controlNetId } = props; + const processorNodeSelector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => ({ + processorNode: controlNet.controlNets[controlNetId]?.processorNode, + }), + defaultSelectorOptions + ), + [controlNetId] + ); + const isBusy = useAppSelector(selectIsBusy); const controlNetProcessors = useAppSelector(selector); + const { processorNode } = useAppSelector(processorNodeSelector); const handleProcessorTypeChanged = useCallback( (v: string | null) => { @@ -77,7 +86,7 @@ const ParamControlNetProcessorSelect = ( value={processorNode.type ?? 'canny_image_processor'} data={controlNetProcessors} onChange={handleProcessorTypeChanged} - disabled={!isReady} + disabled={isBusy} /> ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx index c2b77125d0..d0973f3d81 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx @@ -1,18 +1,30 @@ -import { useAppDispatch } from 'app/store/storeHooks'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAISlider from 'common/components/IAISlider'; import { controlNetWeightChanged } from 'features/controlNet/store/controlNetSlice'; -import { memo, useCallback } from 'react'; +import { memo, useCallback, useMemo } from 'react'; type ParamControlNetWeightProps = { controlNetId: string; - weight: number; mini?: boolean; }; const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { - const { controlNetId, weight, mini = false } = props; + const { controlNetId, mini = false } = props; const dispatch = useAppDispatch(); + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ controlNet }) => controlNet.controlNets[controlNetId]?.weight, + defaultSelectorOptions + ), + [controlNetId] + ); + const weight = useAppSelector(selector); const handleWeightChanged = useCallback( (weight: number) => { dispatch(controlNetWeightChanged({ controlNetId, weight })); diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index c735a47510..8e6f96add3 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -1,7 +1,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { ControlNetModelParam } from 'features/parameters/types/parameterSchemas'; -import { forEach } from 'lodash-es'; +import { cloneDeep, forEach } from 'lodash-es'; import { imageDeleted } from 'services/api/thunks/image'; import { isAnySessionRejected } from 'services/api/thunks/session'; import { appSocketInvocationError } from 'services/events/actions'; @@ -84,6 +84,19 @@ export const controlNetSlice = createSlice({ controlNetId, }; }, + controlNetDuplicated: ( + state, + action: PayloadAction<{ + sourceControlNetId: string; + newControlNetId: string; + }> + ) => { + const { sourceControlNetId, newControlNetId } = action.payload; + + const newControlnet = cloneDeep(state.controlNets[sourceControlNetId]); + newControlnet.controlNetId = newControlNetId; + state.controlNets[newControlNetId] = newControlnet; + }, controlNetAddedFromImage: ( state, action: PayloadAction<{ controlNetId: string; controlImage: string }> @@ -315,6 +328,7 @@ export const controlNetSlice = createSlice({ export const { isControlNetEnabledToggled, controlNetAdded, + controlNetDuplicated, controlNetAddedFromImage, controlNetRemoved, controlNetImageChanged, diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx index 59bf7542eb..201cf860c9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse.tsx @@ -55,7 +55,7 @@ const ParamControlNetCollapse = () => { {controlNetsArray.map((c, i) => ( {i > 0 && } - + ))}