From 6b824eb112021eb210cc309eb8e214cb8a697a6d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 3 Jun 2023 11:13:33 +1000 Subject: [PATCH] feat(ui): initial mini controlnet UI, dnd improvements --- invokeai/frontend/web/package.json | 1 + .../components/ImageDnd/ImageDndContext.tsx | 5 +- .../components/ImageDnd/OverlayDragImage.tsx | 30 +++-- .../frontend/web/src/app/types/invokeai.ts | 1 + .../src/common/components/IAICustomSelect.tsx | 2 +- .../common/components/IAIImageFallback.tsx | 27 ++++ .../web/src/common/components/IAISlider.tsx | 10 +- .../controlNet/components/ControlNet.tsx | 16 +-- .../components/ControlNetImagePreview.tsx | 63 +++------- .../controlNet/components/ControlNetMini.tsx | 101 +++++++++++++++ .../parameters/IAISelectableImage.tsx | 118 ++++++++++-------- .../parameters/ParamControlNetBeginEnd.tsx | 98 ++++++++------- .../parameters/ParamControlNetWeight.tsx | 17 ++- .../components/CurrentImagePreview.tsx | 73 +++++------ .../fields/ImageInputFieldComponent.tsx | 6 +- .../ControlNet/ParamControlNetCollapse.tsx | 16 +++ .../ImageToImage/InitialImagePreview.tsx | 8 +- invokeai/frontend/web/yarn.lock | 8 ++ 18 files changed, 377 insertions(+), 223 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAIImageFallback.tsx create mode 100644 invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 104fad3364..a9d0bfba7e 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -61,6 +61,7 @@ "@chakra-ui/theme-tools": "^2.0.16", "@dagrejs/graphlib": "^2.1.12", "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@floating-ui/react-dom": "^2.0.0", diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx index 6c76731d4c..72487f329c 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/ImageDndContext.tsx @@ -6,6 +6,7 @@ import { KeyboardSensor, MouseSensor, TouchSensor, + pointerWithin, useSensor, useSensors, } from '@dnd-kit/core'; @@ -13,6 +14,7 @@ import { PropsWithChildren, memo, useCallback, useState } from 'react'; import OverlayDragImage from './OverlayDragImage'; import { ImageDTO } from 'services/api'; import { isImageDTO } from 'services/types/guards'; +import { snapCenterToCursor } from '@dnd-kit/modifiers'; type ImageDndContextProps = PropsWithChildren; @@ -53,9 +55,10 @@ const ImageDndContext = (props: ImageDndContextProps) => { onDragStart={handleDragStart} onDragEnd={handleDragEnd} sensors={sensors} + collisionDetection={pointerWithin} > {props.children} - + {draggedImage && } diff --git a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx index 25a5fe2449..deec1e96d2 100644 --- a/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx +++ b/invokeai/frontend/web/src/app/components/ImageDnd/OverlayDragImage.tsx @@ -1,4 +1,4 @@ -import { Image } from '@chakra-ui/react'; +import { Box, Image } from '@chakra-ui/react'; import { memo } from 'react'; import { ImageDTO } from 'services/api'; @@ -8,15 +8,27 @@ type OverlayDragImageProps = { const OverlayDragImage = (props: OverlayDragImageProps) => { return ( - + > + + ); }; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 8081ffa491..304b094749 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -95,6 +95,7 @@ export type AppFeature = * A disable-able Stable Diffusion feature */ export type SDFeature = + | 'controlNet' | 'noise' | 'variation' | 'symmetry' diff --git a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx index 5047a24c63..548b4d73e0 100644 --- a/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAICustomSelect.tsx @@ -109,7 +109,7 @@ const IAICustomSelect = (props: IAICustomSelectProps) => { top: 0, left: 0, flexDirection: 'column', - zIndex: 1, + zIndex: 2, bg: 'base.800', borderRadius: 'base', border: '1px', diff --git a/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx new file mode 100644 index 0000000000..3d34fbca9e --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIImageFallback.tsx @@ -0,0 +1,27 @@ +import { Flex, FlexProps, Spinner, SpinnerProps } from '@chakra-ui/react'; + +type Props = FlexProps & { + spinnerProps?: SpinnerProps; +}; + +export const IAIImageFallback = (props: Props) => { + const { spinnerProps, ...rest } = props; + const { sx, ...restFlexProps } = rest; + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index a2a3251f02..2777e35967 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -40,7 +40,7 @@ import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; export type IAIFullSliderProps = { - label: string; + label?: string; value: number; min?: number; max?: number; @@ -178,9 +178,11 @@ const IAISlider = (props: IAIFullSliderProps) => { isDisabled={isDisabled} {...sliderFormControlProps} > - - {label} - + {label && ( + + {label} + + )} { [controlNetId, dispatch] ); - const handleControlImageReset = useCallback(() => { - dispatch(controlNetImageChanged({ controlNetId, controlImage: null })); - }, [controlNetId, dispatch]); - const shouldShowProcessedImageBackdrop = Number(controlImage?.width) > Number(processedControlImage?.width) || Number(controlImage?.height) > Number(processedControlImage?.height); return ( - - setShouldShowProcessedImage(false)} + onMouseOut={() => setShouldShowProcessedImage(true)} + > + } - withResetIcon - resetIconSize="sm" /> {controlImage && @@ -108,13 +103,10 @@ const ControlNetImagePreview = (props: Props) => { h: 'full', }} > - } + onDrop={handleControlImageChanged} + payloadImage={controlImage} /> @@ -131,18 +123,7 @@ const ControlNetImagePreview = (props: Props) => { h: 'full', }} > - - - )} - {processedControlImage && !isProcessingControlImage && ( - - } - size="sm" - onMouseOver={() => setShouldShowProcessedImage(false)} - onMouseOut={() => setShouldShowProcessedImage(true)} - /> + )} @@ -150,19 +131,3 @@ const ControlNetImagePreview = (props: Props) => { }; export default memo(ControlNetImagePreview); - -const ProcessedImageFallback = () => ( - - - -); diff --git a/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx b/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx new file mode 100644 index 0000000000..7d7b329b71 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlNet/components/ControlNetMini.tsx @@ -0,0 +1,101 @@ +import { memo, useCallback } from 'react'; +import { + ControlNet, + controlNetProcessedImageChanged, + controlNetRemoved, +} from '../store/controlNetSlice'; +import { useAppDispatch } from 'app/store/storeHooks'; +import ParamControlNetModel from './parameters/ParamControlNetModel'; +import ParamControlNetWeight from './parameters/ParamControlNetWeight'; +import { + Box, + Flex, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '@chakra-ui/react'; +import IAIButton from 'common/components/IAIButton'; +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 ControlNetImagePreview from './ControlNetImagePreview'; + +type ControlNetProps = { + controlNet: ControlNet; +}; + +const ControlNet = (props: ControlNetProps) => { + const { + controlNetId, + isEnabled, + model, + weight, + beginStepPct, + endStepPct, + controlImage, + isControlImageProcessed, + processedControlImage, + processorNode, + } = props.controlNet; + const dispatch = useAppDispatch(); + const handleReset = useCallback(() => { + dispatch( + controlNetProcessedImageChanged({ + controlNetId, + processedControlImage: null, + }) + ); + }, [controlNetId, dispatch]); + + const handleControlNetRemoved = useCallback(() => { + dispatch(controlNetRemoved(controlNetId)); + }, [controlNetId, dispatch]); + + return ( + + + + + + + + + + + ); +}; + +export default memo(ControlNet); diff --git a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx index fd5ffef28e..26da39baf2 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/IAISelectableImage.tsx @@ -4,11 +4,12 @@ import { Icon, IconButtonProps, Image, - Spinner, Text, } from '@chakra-ui/react'; -import { useDroppable } from '@dnd-kit/core'; +import { useDraggable, useDroppable } from '@dnd-kit/core'; +import { useCombinedRefs } from '@dnd-kit/utilities'; import IAIIconButton from 'common/components/IAIIconButton'; +import { IAIImageFallback } from 'common/components/IAIImageFallback'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { useGetUrl } from 'common/util/getUrl'; import { AnimatePresence, motion } from 'framer-motion'; @@ -18,42 +19,65 @@ import { FaImage, FaTimes } from 'react-icons/fa'; import { ImageDTO } from 'services/api'; import { v4 as uuidv4 } from 'uuid'; -const PLACEHOLDER_MIN_HEIGHT = 48; +const PLACEHOLDER_MIN_HEIGHT = 36; type IAISelectableImageProps = { image: ImageDTO | null | undefined; - onChange: (image: ImageDTO) => void; + onDrop: (image: ImageDTO) => void; onReset?: () => void; onError?: (event: SyntheticEvent) => void; + onLoad?: (event: SyntheticEvent) => void; resetIconSize?: IconButtonProps['size']; withResetIcon?: boolean; withMetadataOverlay?: boolean; + isDragDisabled?: boolean; isDropDisabled?: boolean; fallback?: ReactElement; + payloadImage?: ImageDTO | null | undefined; }; -const IAISelectableImage = (props: IAISelectableImageProps) => { +const IAIDndImage = (props: IAISelectableImageProps) => { const { image, - onChange, + onDrop, onReset, onError, resetIconSize = 'md', withResetIcon = false, withMetadataOverlay = false, isDropDisabled = false, - fallback = , + isDragDisabled = false, + fallback = , + payloadImage, } = props; - const droppableId = useRef(uuidv4()); + const dndId = useRef(uuidv4()); const { getUrl } = useGetUrl(); - const { isOver, setNodeRef, active } = useDroppable({ - id: droppableId.current, + const { + isOver, + setNodeRef: setDroppableRef, + active, + } = useDroppable({ + id: dndId.current, disabled: isDropDisabled, data: { - handleDrop: onChange, + handleDrop: onDrop, }, }); + const { + attributes, + listeners, + setNodeRef: setDraggableRef, + } = useDraggable({ + id: dndId.current, + data: { + image: payloadImage ? payloadImage : image, + }, + disabled: isDragDisabled, + }); + + const setNodeRef = useCombinedRefs(setDroppableRef, setDraggableRef); + return ( { alignItems: 'center', justifyContent: 'center', position: 'relative', + minW: 36, + minH: 36, }} + {...attributes} + {...listeners} ref={setNodeRef} > {image && ( @@ -80,8 +108,11 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { fallbackStrategy="beforeLoadOrError" fallback={fallback} onError={onError} + objectFit="contain" draggable={false} sx={{ + maxW: 'full', + maxH: 'full', borderRadius: 'base', }} /> @@ -139,7 +170,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => { ); }; -export default memo(IAISelectableImage); +export default memo(IAIDndImage); type DropOverlayProps = { isOver: boolean; @@ -179,14 +210,15 @@ const DropOverlay = (props: DropOverlayProps) => { w: 'full', h: 'full', bg: 'base.900', - opacity: isOver ? 0.9 : 0.7, + opacity: 0.7, borderRadius: 'base', alignItems: 'center', justifyContent: 'center', transitionProperty: 'common', - transitionDuration: '0.15s', + transitionDuration: '0.1s', }} /> + { left: 0, w: 'full', h: 'full', - opacity: isOver ? 1 : 0.9, - alignItems: 'center', - justifyContent: 'center', - transitionProperty: 'common', - transitionDuration: '0.15s', - }} - > - - Drop Image - - - + > + + Drop + + ); }; - -const ImageFallback = () => ( - - - -); 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 fa7047126d..e258d4cf29 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetBeginEnd.tsx @@ -23,12 +23,13 @@ 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, endStepPct } = props; + const { controlNetId, beginStepPct, endStepPct, mini = false } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -69,52 +70,59 @@ const ParamControlNetBeginEnd = (props: Props) => { - - 0% - - - 50% - - - 100% - + {!mini && ( + <> + {' '} + + 0% + + + 50% + + + 100% + + + )} - } - onClick={handleStepPctReset} - /> + {!mini && ( + } + onClick={handleStepPctReset} + /> + )} ); 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 11272582d0..007ef355c3 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/parameters/ParamControlNetWeight.tsx @@ -6,10 +6,11 @@ import { memo, useCallback } from 'react'; type ParamControlNetWeightProps = { controlNetId: string; weight: number; + mini?: boolean; }; const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { - const { controlNetId, weight } = props; + const { controlNetId, weight, mini = false } = props; const dispatch = useAppDispatch(); const handleWeightChanged = useCallback( @@ -23,6 +24,20 @@ const ParamControlNetWeight = (props: ParamControlNetWeightProps) => { dispatch(controlNetWeightChanged({ controlNetId, weight: 1 })); }; + if (mini) { + return ( + + ); + } + return ( { shouldAntialiasProgressImage, } = useAppSelector(imagesSelector); const { shouldFetchImages } = useAppSelector(configSelector); - const { getUrl } = useGetUrl(); const toaster = useAppToaster(); const dispatch = useAppDispatch(); - const [isLoaded, setIsLoaded] = useState(false); - - const { attributes, listeners, setNodeRef } = useDraggable({ - id: `currentImage_${image?.image_name}`, - data: { - image, - }, - }); const handleError = useCallback(() => { dispatch(imageSelected()); @@ -75,9 +65,12 @@ const CurrentImagePreview = () => { } }, [dispatch, toaster, shouldFetchImages]); - useEffect(() => { - setIsLoaded(false); - }, [image]); + const handleDrop = useCallback( + (droppedImage: ImageDTO) => { + dispatch(imageSelected(droppedImage)); + }, + [dispatch] + ); return ( { image && ( - } - sx={{ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', - height: 'auto', - borderRadius: 'base', - touchAction: 'none', - }} + { - setIsLoaded(true); - }} + fallback={} /> - {isLoaded && } ) )} - {shouldShowImageDetails && image && 'metadata' in image && ( + {shouldShowImageDetails && image && image.metadata && ( { )} - {!shouldShowImageDetails && } + {!shouldShowImageDetails && ( + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx index 9889ade2f3..c83a0b4a40 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -8,7 +8,7 @@ import { import { memo, useCallback } from 'react'; import { FieldComponentProps } from './types'; -import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import IAIDndImage from 'features/controlNet/components/parameters/IAISelectableImage'; import { ImageDTO } from 'services/api'; import { Flex } from '@chakra-ui/react'; @@ -51,9 +51,9 @@ const ImageInputFieldComponent = ( justifyContent: 'center', }} > - 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 a3f91fd432..2359e5123c 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 @@ -24,6 +24,8 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map, startCase } from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { CloseIcon } from '@chakra-ui/icons'; +import ControlNetMini from 'features/controlNet/components/ControlNetMini'; +import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; const selector = createSelector( controlNetSelector, @@ -38,6 +40,7 @@ const selector = createSelector( const ParamControlNetCollapse = () => { const { t } = useTranslation(); const { controlNetsArray, isEnabled } = useAppSelector(selector); + const isControlNetDisabled = useFeatureStatus('controlNet').isFeatureDisabled; const dispatch = useAppDispatch(); const handleClickControlNetToggle = useCallback(() => { @@ -48,6 +51,18 @@ const ParamControlNetCollapse = () => { dispatch(controlNetAdded({ controlNetId: uuidv4() })); }, [dispatch]); + if (isControlNetDisabled) { + return null; + } + + return ( + <> + {controlNetsArray.map((c) => ( + + ))} + + ); + return ( { {controlNetsArray.map((c) => ( + {/* */} ))} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 2a0ed4ab5d..d8687581d6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -12,8 +12,9 @@ import { generationSelector } from 'features/parameters/store/generationSelector import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { configSelector } from '../../../../system/store/configSelectors'; import { useAppToaster } from 'app/components/Toaster'; -import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; +import IAIDndImage from 'features/controlNet/components/parameters/IAISelectableImage'; import { ImageDTO } from 'services/api'; +import { IAIImageFallback } from 'common/components/IAIImageFallback'; const selector = createSelector( [generationSelector], @@ -73,10 +74,11 @@ const InitialImagePreview = () => { justifyContent: 'center', }} > - } /> ); diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index e3b2978457..a4c9bf3633 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -953,6 +953,14 @@ "@dnd-kit/utilities" "^3.2.1" tslib "^2.0.0" +"@dnd-kit/modifiers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz#9e39b25fd6e323659604cc74488fe044d33188c8" + integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A== + dependencies: + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + "@dnd-kit/utilities@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a"