feat(ui): more work on controlnet mini

This commit is contained in:
psychedelicious 2023-06-03 13:06:37 +10:00
parent b6b3b9f99c
commit a0dde66b5d
9 changed files with 166 additions and 92 deletions

View File

@ -48,7 +48,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
isDragDisabled = false, isDragDisabled = false,
fallback = <IAIImageFallback />, fallback = <IAIImageFallback />,
payloadImage, payloadImage,
minSize = 36, minSize = 24,
} = props; } = props;
const dndId = useRef(uuidv4()); const dndId = useRef(uuidv4());
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();

View File

@ -1,8 +1,8 @@
import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react'; import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react';
import { memo, ReactNode } from 'react'; import { memo, ReactElement } from 'react';
type IAISimpleCheckboxProps = CheckboxProps & { type IAISimpleCheckboxProps = CheckboxProps & {
label: string | ReactNode; label: string | ReactElement;
}; };
const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => { const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => {

View File

@ -58,11 +58,7 @@ const ControlNet = (props: ControlNetProps) => {
return ( return (
<Flex sx={{ flexDir: 'column', gap: 3 }}> <Flex sx={{ flexDir: 'column', gap: 3 }}>
<ControlNetImagePreview <ControlNetImagePreview controlNet={props.controlNet} />
controlNetId={controlNetId}
controlImage={controlImage}
processedControlImage={processedControlImage}
/>
<ParamControlNetModel controlNetId={controlNetId} model={model} /> <ParamControlNetModel controlNetId={controlNetId} model={model} />
<Tabs <Tabs
isFitted isFitted

View File

@ -1,6 +1,7 @@
import { memo, useCallback, useState } from 'react'; import { memo, useCallback, useRef, useState } from 'react';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { import {
ControlNet,
controlNetImageChanged, controlNetImageChanged,
controlNetSelector, controlNetSelector,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
@ -11,6 +12,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { useHoverDirty } from 'react-use';
const selector = createSelector( const selector = createSelector(
controlNetSelector, controlNetSelector,
@ -22,18 +24,21 @@ const selector = createSelector(
); );
type Props = { type Props = {
controlNetId: string; controlNet: ControlNet;
controlImage: ImageDTO | null;
processedControlImage: ImageDTO | null;
}; };
const ControlNetImagePreview = (props: Props) => { const ControlNetImagePreview = (props: Props) => {
const { controlNetId, controlImage, processedControlImage } = props; const {
controlNetId,
controlImage,
processedControlImage,
isControlImageProcessed,
} = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isProcessingControlImage } = useAppSelector(selector); const { isProcessingControlImage } = useAppSelector(selector);
const containerRef = useRef<HTMLDivElement>(null);
const [shouldShowProcessedImage, setShouldShowProcessedImage] = const isMouseOverImage = useHoverDirty(containerRef);
useState(true);
const handleControlImageChanged = useCallback( const handleControlImageChanged = useCallback(
(controlImage: ImageDTO) => { (controlImage: ImageDTO) => {
@ -46,12 +51,15 @@ const ControlNetImagePreview = (props: Props) => {
Number(controlImage?.width) > Number(processedControlImage?.width) || Number(controlImage?.width) > Number(processedControlImage?.width) ||
Number(controlImage?.height) > Number(processedControlImage?.height); Number(controlImage?.height) > Number(processedControlImage?.height);
const shouldShowProcessedImage =
controlImage &&
processedControlImage &&
!isMouseOverImage &&
!isProcessingControlImage &&
!isControlImageProcessed;
return ( return (
<Box <Box ref={containerRef} sx={{ position: 'relative', w: 'full', h: 'full' }}>
sx={{ position: 'relative', h: 'inherit' }}
onMouseOver={() => setShouldShowProcessedImage(false)}
onMouseOut={() => setShouldShowProcessedImage(true)}
>
<IAIDndImage <IAIDndImage
image={controlImage} image={controlImage}
onDrop={handleControlImageChanged} onDrop={handleControlImageChanged}

View File

@ -1,29 +1,26 @@
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { import {
ControlNet, ControlNet,
controlNetProcessedImageChanged, controlNetAdded,
controlNetRemoved, controlNetRemoved,
controlNetToggled,
isControlNetImageProcessedToggled,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import { useAppDispatch } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import ParamControlNetModel from './parameters/ParamControlNetModel'; import ParamControlNetModel from './parameters/ParamControlNetModel';
import ParamControlNetWeight from './parameters/ParamControlNetWeight'; import ParamControlNetWeight from './parameters/ParamControlNetWeight';
import { import {
Box, Checkbox,
Flex, Flex,
Tab, FormControl,
TabList, FormLabel,
TabPanel, HStack,
TabPanels,
Tabs,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton'; import { FaCopy, FaTrash } from 'react-icons/fa';
import { FaUndo } from 'react-icons/fa';
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
import ControlNetPreprocessButton from './ControlNetPreprocessButton';
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd'; import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
import ControlNetImagePreview from './ControlNetImagePreview'; import ControlNetImagePreview from './ControlNetImagePreview';
import IAIIconButton from 'common/components/IAIIconButton';
import { v4 as uuidv4 } from 'uuid';
type ControlNetProps = { type ControlNetProps = {
controlNet: ControlNet; controlNet: ControlNet;
@ -43,56 +40,111 @@ const ControlNet = (props: ControlNetProps) => {
processorNode, processorNode,
} = props.controlNet; } = props.controlNet;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleReset = useCallback(() => {
dispatch( const handleDelete = useCallback(() => {
controlNetProcessedImageChanged({ dispatch(controlNetRemoved(controlNetId));
controlNetId,
processedControlImage: null,
})
);
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
const handleControlNetRemoved = useCallback(() => { const handleDuplicate = useCallback(() => {
dispatch(controlNetRemoved(controlNetId)); dispatch(
controlNetAdded({ controlNetId: uuidv4(), controlNet: props.controlNet })
);
}, [dispatch, props.controlNet]);
const handleToggleIsEnabled = useCallback(() => {
dispatch(controlNetToggled(controlNetId));
}, [controlNetId, dispatch]);
const handleToggleIsPreprocessed = useCallback(() => {
dispatch(isControlNetImageProcessedToggled(controlNetId));
}, [controlNetId, dispatch]); }, [controlNetId, dispatch]);
return ( return (
<Flex <Flex
sx={{ sx={{
gap: 4, flexDir: 'column',
p: 2, gap: 2,
paddingInlineEnd: 4,
bg: 'base.850',
borderRadius: 'base',
}} }}
> >
<HStack>
<ParamControlNetModel controlNetId={controlNetId} model={model} />
<IAIIconButton
size="sm"
tooltip="Duplicate ControlNet"
aria-label="Duplicate ControlNet"
onClick={handleDuplicate}
icon={<FaCopy />}
/>
<IAIIconButton
size="sm"
tooltip="Delete ControlNet"
aria-label="Delete ControlNet"
colorScheme="error"
onClick={handleDelete}
icon={<FaTrash />}
/>
</HStack>
<Flex <Flex
sx={{ sx={{
alignItems: 'center', gap: 4,
justifyContent: 'center', paddingInlineEnd: 2,
h: 36,
w: 36,
}} }}
> >
<ControlNetImagePreview <Flex
controlNetId={controlNetId} sx={{
controlImage={controlImage} alignItems: 'center',
processedControlImage={processedControlImage} justifyContent: 'center',
/> h: 32,
</Flex> w: 32,
<Flex sx={{ flexDir: 'column', gap: 2, w: 'full', h: 'full' }}> aspectRatio: '1/1',
<ParamControlNetModel controlNetId={controlNetId} model={model} /> }}
<ParamControlNetWeight >
controlNetId={controlNetId} <ControlNetImagePreview controlNet={props.controlNet} />
weight={weight} </Flex>
mini <Flex
/> sx={{
<ParamControlNetBeginEnd flexDir: 'column',
controlNetId={controlNetId} gap: 2,
beginStepPct={beginStepPct} w: 'full',
endStepPct={endStepPct} justifyContent: 'space-between',
mini }}
/> >
<ParamControlNetWeight
controlNetId={controlNetId}
weight={weight}
mini
/>
<ParamControlNetBeginEnd
controlNetId={controlNetId}
beginStepPct={beginStepPct}
endStepPct={endStepPct}
mini
/>
<Flex
sx={{
justifyContent: 'space-between',
}}
>
<FormControl>
<HStack>
<Checkbox
isChecked={isEnabled}
onChange={handleToggleIsEnabled}
/>
<FormLabel>Enabled</FormLabel>
</HStack>
</FormControl>
<FormControl>
<HStack>
<Checkbox
isChecked={isControlImageProcessed}
onChange={handleToggleIsPreprocessed}
/>
<FormLabel>Preprocessed</FormLabel>
</HStack>
</FormControl>
</Flex>
</Flex>
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -50,7 +50,7 @@ const ParamControlNetBeginEnd = (props: Props) => {
return ( return (
<FormControl> <FormControl>
<FormLabel>Begin & End Step %</FormLabel> <FormLabel>Begin / End Step Percentage</FormLabel>
<HStack w="100%" gap={2} alignItems="center"> <HStack w="100%" gap={2} alignItems="center">
<RangeSlider <RangeSlider
aria-label={['Begin Step %', 'End Step %']} aria-label={['Begin Step %', 'End Step %']}
@ -72,7 +72,6 @@ const ParamControlNetBeginEnd = (props: Props) => {
</Tooltip> </Tooltip>
{!mini && ( {!mini && (
<> <>
{' '}
<RangeSliderMark <RangeSliderMark
value={0} value={0}
sx={{ sx={{

View File

@ -72,11 +72,11 @@ export const controlNetSlice = createSlice({
}, },
controlNetAdded: ( controlNetAdded: (
state, state,
action: PayloadAction<{ controlNetId: string }> action: PayloadAction<{ controlNetId: string; controlNet?: ControlNet }>
) => { ) => {
const { controlNetId } = action.payload; const { controlNetId, controlNet } = action.payload;
state.controlNets[controlNetId] = { state.controlNets[controlNetId] = {
...initialControlNet, ...(controlNet ?? initialControlNet),
controlNetId, controlNetId,
}; };
}, },
@ -116,11 +116,9 @@ export const controlNetSlice = createSlice({
}, },
isControlNetImageProcessedToggled: ( isControlNetImageProcessedToggled: (
state, state,
action: PayloadAction<{ action: PayloadAction<string>
controlNetId: string;
}>
) => { ) => {
const { controlNetId } = action.payload; const controlNetId = action.payload;
state.controlNets[controlNetId].isControlImageProcessed = state.controlNets[controlNetId].isControlImageProcessed =
!state.controlNets[controlNetId].isControlImageProcessed; !state.controlNets[controlNetId].isControlImageProcessed;
}, },

View File

@ -346,6 +346,11 @@ export const buildTextToImageGraph = (state: RootState): Graph => {
weight, weight,
} = controlNet; } = controlNet;
if (!isEnabled) {
// Skip disabled ControlNets
return;
}
const controlNetNode: ControlNetInvocation = { const controlNetNode: ControlNetInvocation = {
id: `control_net_${controlNetId}`, id: `control_net_${controlNetId}`,
type: 'controlnet', type: 'controlnet',
@ -355,14 +360,14 @@ export const buildTextToImageGraph = (state: RootState): Graph => {
control_weight: weight, control_weight: weight,
}; };
if (processedControlImage) { if (processedControlImage && !isControlImageProcessed) {
// We've already processed the image in the app, so we can just use the processed image // We've already processed the image in the app, so we can just use the processed image
const { image_name, image_origin } = processedControlImage; const { image_name, image_origin } = processedControlImage;
controlNetNode.image = { controlNetNode.image = {
image_name, image_name,
image_origin, image_origin,
}; };
} else if (controlImage) { } else if (controlImage && isControlImageProcessed) {
// The control image is preprocessed // The control image is preprocessed
const { image_name, image_origin } = controlImage; const { image_name, image_origin } = controlImage;
controlNetNode.image = { controlNetNode.image = {
@ -370,9 +375,10 @@ export const buildTextToImageGraph = (state: RootState): Graph => {
image_origin, image_origin,
}; };
} else { } else {
// The control image is not processed, so we need to add a preprocess node // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly
// TODO: Add preprocess node return;
} }
graph.nodes[controlNetNode.id] = controlNetNode; graph.nodes[controlNetNode.id] = controlNetNode;
if (size(controlNets) > 1) { if (size(controlNets) > 1) {

View File

@ -1,6 +1,6 @@
import { import {
Divider,
Flex, Flex,
Spacer,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
@ -9,7 +9,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import IAICollapse from 'common/components/IAICollapse'; import IAICollapse from 'common/components/IAICollapse';
import { memo, useCallback } from 'react'; import { Fragment, memo, useCallback } from 'react';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import ControlNet from 'features/controlNet/components/ControlNet'; import ControlNet from 'features/controlNet/components/ControlNet';
@ -21,11 +21,11 @@ import {
isControlNetEnabledToggled, isControlNetEnabledToggled,
} from 'features/controlNet/store/controlNetSlice'; } from 'features/controlNet/store/controlNetSlice';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { map, startCase } from 'lodash-es'; import { map } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { CloseIcon } from '@chakra-ui/icons';
import ControlNetMini from 'features/controlNet/components/ControlNetMini'; import ControlNetMini from 'features/controlNet/components/ControlNetMini';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import IAIButton from 'common/components/IAIButton';
const selector = createSelector( const selector = createSelector(
controlNetSelector, controlNetSelector,
@ -56,11 +56,26 @@ const ParamControlNetCollapse = () => {
} }
return ( return (
<> <IAICollapse
{controlNetsArray.map((c) => ( label={'ControlNet'}
<ControlNetMini key={c.controlNetId} controlNet={c} /> isOpen={isEnabled}
))} onToggle={handleClickControlNetToggle}
</> withSwitch
>
{controlNetsArray.length === 0 && (
<IAIButton onClick={handleClickedAddControlNet}>
Add ControlNet
</IAIButton>
)}
<Flex sx={{ flexDir: 'column', gap: 4 }}>
{controlNetsArray.map((c, i) => (
<Fragment key={c.controlNetId}>
{i > 0 && <Divider />}
<ControlNetMini controlNet={c} />
</Fragment>
))}
</Flex>
</IAICollapse>
); );
return ( return (