feat(ui): controlnet/image dnd wip

Implement `dnd-kit` for image drag and drop
- vastly simplifies logic bc we can drag and drop non-serializable data (like an `ImageDTO`)
- also much prettier
- also will fix conflicts with file upload via OS drag and drop, bc `dnd-kit` does not use native HTML drag and drop API
- Implemented for Init image, controlnet, and node editor so far

More progress on the ControlNet UI
This commit is contained in:
psychedelicious 2023-06-01 18:27:39 +10:00
parent e2e07696fc
commit 3b9426eb72
42 changed files with 853 additions and 234 deletions

View File

@ -60,6 +60,7 @@
"@chakra-ui/styled-system": "^2.9.0",
"@chakra-ui/theme-tools": "^2.0.16",
"@dagrejs/graphlib": "^2.1.12",
"@dnd-kit/core": "^6.0.8",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@floating-ui/react-dom": "^2.0.0",

View File

@ -0,0 +1,45 @@
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
} from '@dnd-kit/core';
import { PropsWithChildren, memo, useCallback, useState } from 'react';
import OverlayDragImage from './OverlayDragImage';
import { ImageDTO } from 'services/api';
import { isImageDTO } from 'services/types/guards';
type ImageDndContextProps = PropsWithChildren;
const ImageDndContext = (props: ImageDndContextProps) => {
const [draggedImage, setDraggedImage] = useState<ImageDTO | null>(null);
const handleDragStart = useCallback((event: DragStartEvent) => {
const dragData = event.active.data.current;
if (dragData && 'image' in dragData && isImageDTO(dragData.image)) {
setDraggedImage(dragData.image);
}
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const handleDrop = event.over?.data.current?.handleDrop;
if (handleDrop && typeof handleDrop === 'function' && draggedImage) {
handleDrop(draggedImage);
}
setDraggedImage(null);
},
[draggedImage]
);
return (
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
{props.children}
<DragOverlay dropAnimation={null}>
{draggedImage && <OverlayDragImage image={draggedImage} />}
</DragOverlay>
</DndContext>
);
};
export default memo(ImageDndContext);

View File

@ -0,0 +1,23 @@
import { Image } from '@chakra-ui/react';
import { memo } from 'react';
import { ImageDTO } from 'services/api';
type OverlayDragImageProps = {
image: ImageDTO;
};
const OverlayDragImage = (props: OverlayDragImageProps) => {
return (
<Image
sx={{
maxW: 32,
maxH: 32,
borderRadius: 'base',
shadow: 'dark-lg',
}}
src={props.image.thumbnail_url}
/>
);
};
export default memo(OverlayDragImage);

View File

@ -16,6 +16,7 @@ import { PartialAppConfig } from 'app/types/invokeai';
import '../../i18n';
import { socketMiddleware } from 'services/events/middleware';
import { Middleware } from '@reduxjs/toolkit';
import ImageDndContext from './ImageDnd/ImageDndContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -69,11 +70,13 @@ const InvokeAIUI = ({
<Provider store={store}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
<ImageDndContext>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
</ImageDndContext>
</ThemeLocaleProvider>
</React.Suspense>
</Provider>

View File

@ -2,12 +2,10 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { startAppListening } from '..';
import {
initialImageSelected,
isImageDTO,
} from 'features/parameters/store/actions';
import { initialImageSelected } from 'features/parameters/store/actions';
import { makeToast } from 'app/components/Toaster';
import { selectImagesById } from 'features/gallery/store/imagesSlice';
import { isImageDTO } from 'services/types/guards';
export const addInitialImageSelectedListener = () => {
startAppListening({

View File

@ -13,6 +13,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice';
import imagesReducer from 'features/gallery/store/imagesSlice';
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
// import sessionReducer from 'features/system/store/sessionSlice';
@ -45,6 +46,7 @@ const allReducers = {
ui: uiReducer,
hotkeys: hotkeysReducer,
images: imagesReducer,
controlNet: controlNetReducer,
// session: sessionReducer,
};
@ -62,6 +64,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'postprocessing',
'system',
'ui',
'controlNet',
// 'hotkeys',
// 'config',
];

View File

@ -1,17 +0,0 @@
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
import { memo, ReactNode } from 'react';
type IAICheckboxProps = CheckboxProps & {
label: string | ReactNode;
};
const IAICheckbox = (props: IAICheckboxProps) => {
const { label, ...rest } = props;
return (
<Checkbox colorScheme="accent" {...rest}>
{label}
</Checkbox>
);
};
export default memo(IAICheckbox);

View File

@ -0,0 +1,25 @@
import {
Checkbox,
CheckboxProps,
FormControl,
FormControlProps,
FormLabel,
} from '@chakra-ui/react';
import { memo, ReactNode } from 'react';
type IAIFullCheckboxProps = CheckboxProps & {
label: string | ReactNode;
formControlProps?: FormControlProps;
};
const IAIFullCheckbox = (props: IAIFullCheckboxProps) => {
const { label, formControlProps, ...rest } = props;
return (
<FormControl {...formControlProps}>
<FormLabel>{label}</FormLabel>
<Checkbox colorScheme="accent" {...rest} />
</FormControl>
);
};
export default memo(IAIFullCheckbox);

View File

@ -0,0 +1,19 @@
import { Checkbox, CheckboxProps, Text } from '@chakra-ui/react';
import { memo, ReactNode } from 'react';
type IAISimpleCheckboxProps = CheckboxProps & {
label: string | ReactNode;
};
const IAISimpleCheckbox = (props: IAISimpleCheckboxProps) => {
const { label, ...rest } = props;
return (
<Checkbox colorScheme="accent" {...rest}>
<Text color="base.200" fontSize="md">
{label}
</Text>
</Checkbox>
);
};
export default memo(IAISimpleCheckbox);

View File

@ -23,9 +23,9 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
flexDirection: 'column',
position: 'absolute',
top: 0,
right: 0,
insetInlineStart: 0,
p: 2,
alignItems: 'flex-end',
alignItems: 'flex-start',
gap: 2,
}}
>

View File

@ -2,7 +2,7 @@ import { ButtonGroup, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIColorPicker from 'common/components/IAIColorPicker';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
@ -117,12 +117,12 @@ const IAICanvasMaskOptions = () => {
}
>
<Flex direction="column" gap={2}>
<IAICheckbox
<IAISimpleCheckbox
label={`${t('unifiedCanvas.enableMask')} (H)`}
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.preserveMaskedArea')}
isChecked={shouldPreserveMaskedArea}
onChange={(e) =>

View File

@ -1,7 +1,7 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
@ -102,50 +102,50 @@ const IAICanvasSettingsButtonPopover = () => {
}
>
<Flex direction="column" gap={2}>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showIntermediates')}
isChecked={shouldShowIntermediates}
onChange={(e) =>
dispatch(setShouldShowIntermediates(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showGrid')}
isChecked={shouldShowGrid}
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.snapToGrid')}
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnapToGrid}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.darkenOutsideSelection')}
isChecked={shouldDarkenOutsideBoundingBox}
onChange={(e) =>
dispatch(setShouldDarkenOutsideBoundingBox(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.autoSaveToGallery')}
isChecked={shouldAutoSave}
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.saveBoxRegionOnly')}
isChecked={shouldCropToBoundingBoxOnSave}
onChange={(e) =>
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.limitStrokesToBox')}
isChecked={shouldRestrictStrokesToBox}
onChange={(e) =>
dispatch(setShouldRestrictStrokesToBox(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showCanvasDebugInfo')}
isChecked={shouldShowCanvasDebugInfo}
onChange={(e) =>
@ -153,7 +153,7 @@ const IAICanvasSettingsButtonPopover = () => {
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.antialiasing')}
isChecked={shouldAntialias}
onChange={(e) => dispatch(setShouldAntialias(e.target.checked))}

View File

@ -1,7 +1,32 @@
import { memo } from 'react';
import { memo, useCallback } from 'react';
import { ControlNetProcessorNode } from '../store/types';
import { ImageDTO } from 'services/api';
import CannyProcessor from './processors/CannyProcessor';
import {
ControlNet,
ControlNetModel,
controlNetBeginStepPctChanged,
controlNetEndStepPctChanged,
controlNetImageChanged,
controlNetModelChanged,
controlNetProcessedImageChanged,
controlNetRemoved,
controlNetToggled,
controlNetWeightChanged,
isControlNetImageProcessedToggled,
} from '../store/controlNetSlice';
import { useAppDispatch } from 'app/store/storeHooks';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAISlider from 'common/components/IAISlider';
import ParamControlNetIsEnabled from './parameters/ParamControlNetIsEnabled';
import ParamControlNetModel from './parameters/ParamControlNetModel';
import ParamControlNetWeight from './parameters/ParamControlNetWeight';
import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct';
import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct';
import { Flex, HStack, VStack } from '@chakra-ui/react';
import IAISelectableImage from './parameters/IAISelectableImage';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
export type ControlNetProcessorProps = {
controlNetId: string;
@ -16,11 +41,83 @@ const renderProcessorComponent = (props: ControlNetProcessorProps) => {
}
};
const ControlNet = () => {
type ControlNetProps = {
controlNet: ControlNet;
};
const ControlNet = (props: ControlNetProps) => {
const {
controlNetId,
isEnabled,
model,
weight,
beginStepPct,
endStepPct,
controlImage,
isControlImageProcessed,
processedControlImage,
} = props.controlNet;
const dispatch = useAppDispatch();
const handleControlImageChanged = useCallback(
(controlImage: ImageDTO) => {
dispatch(controlNetImageChanged({ controlNetId, controlImage }));
},
[controlNetId, dispatch]
);
const handleControlImageReset = useCallback(() => {
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
}, [controlNetId, dispatch]);
const handleControlNetRemoved = useCallback(() => {
dispatch(controlNetRemoved(controlNetId));
}, [controlNetId, dispatch]);
const handleIsControlImageProcessedToggled = useCallback(() => {
dispatch(
isControlNetImageProcessedToggled({
controlNetId,
})
);
}, [controlNetId, dispatch]);
const handleProcessedControlImageChanged = useCallback(
(processedControlImage: ImageDTO | null) => {
dispatch(
controlNetProcessedImageChanged({
controlNetId,
processedControlImage,
})
);
},
[controlNetId, dispatch]
);
return (
<div>
<h1>ControlNet</h1>
</div>
<Flex sx={{ flexDir: 'column', gap: 3, pb: 4 }}>
<IAIButton onClick={handleControlNetRemoved}>Remove ControlNet</IAIButton>
<IAISelectableImage
image={controlImage}
onChange={handleControlImageChanged}
onReset={handleControlImageReset}
resetIconSize="sm"
/>
<ParamControlNetModel controlNetId={controlNetId} model={model} />
<ParamControlNetIsEnabled
controlNetId={controlNetId}
isEnabled={isEnabled}
/>
<ParamControlNetWeight controlNetId={controlNetId} weight={weight} />
<ParamControlNetBeginStepPct
controlNetId={controlNetId}
beginStepPct={beginStepPct}
/>
<ParamControlNetEndStepPct
controlNetId={controlNetId}
endStepPct={endStepPct}
/>
</Flex>
);
};

View File

@ -0,0 +1,200 @@
import {
Box,
Flex,
Icon,
IconButtonProps,
Image,
Text,
} from '@chakra-ui/react';
import { useDroppable } from '@dnd-kit/core';
import IAIIconButton from 'common/components/IAIIconButton';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useGetUrl } from 'common/util/getUrl';
import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner';
import { AnimatePresence, motion } from 'framer-motion';
import { SyntheticEvent } from 'react';
import { memo, useRef } from 'react';
import { FaImage, FaUndo } from 'react-icons/fa';
import { ImageDTO } from 'services/api';
import { v4 as uuidv4 } from 'uuid';
type IAISelectableImageProps = {
image: ImageDTO | null | undefined;
onChange: (image: ImageDTO) => void;
onReset?: () => void;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
resetIconSize?: IconButtonProps['size'];
};
const IAISelectableImage = (props: IAISelectableImageProps) => {
const { image, onChange, onReset, onError, resetIconSize = 'md' } = props;
const droppableId = useRef(uuidv4());
const { getUrl } = useGetUrl();
const { isOver, setNodeRef, active } = useDroppable({
id: droppableId.current,
data: {
handleDrop: onChange,
},
});
return (
<Flex
sx={{
width: 'full',
height: 'full',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
}}
ref={setNodeRef}
>
{image && (
<Flex sx={{ position: 'relative' }}>
<Image
src={getUrl(image.image_url)}
fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallbackSpinner />}
onError={onError}
sx={{
borderRadius: 'base',
}}
/>
<ImageMetadataOverlay image={image} />
<AnimatePresence>
{active && <DropOverlay isOver={isOver} />}
</AnimatePresence>
</Flex>
)}
{!image && (
<>
<Flex
sx={{
p: 8,
bg: 'base.850',
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'base',
}}
>
<Icon
as={FaImage}
sx={{
boxSize: 24,
color: 'base.500',
}}
/>
</Flex>
<AnimatePresence>
{active && <DropOverlay isOver={isOver} />}
</AnimatePresence>
</>
)}
{image && onReset && (
<Box
sx={{
position: 'absolute',
top: 0,
right: 0,
p: 2,
}}
>
<IAIIconButton
size={resetIconSize}
aria-label="Reset Image"
icon={<FaUndo />}
onClick={onReset}
/>
</Box>
)}
</Flex>
);
};
export default memo(IAISelectableImage);
type DropOverlayProps = {
isOver: boolean;
};
const DropOverlay = (props: DropOverlayProps) => {
const { isOver } = props;
return (
<motion.div
key="statusText"
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.1 },
}}
>
<Flex
sx={{
position: 'absolute',
top: 0,
left: 0,
w: 'full',
h: 'full',
}}
>
<Flex
sx={{
position: 'absolute',
top: 0,
left: 0,
w: 'full',
h: 'full',
bg: 'base.900',
opacity: isOver ? 0.9 : 0.7,
borderRadius: 'base',
alignItems: 'center',
justifyContent: 'center',
transitionProperty: 'common',
transitionDuration: '0.15s',
}}
/>
<Flex
sx={{
position: 'absolute',
top: 0,
left: 0,
w: 'full',
h: 'full',
opacity: isOver ? 1 : 0.9,
alignItems: 'center',
justifyContent: 'center',
transitionProperty: 'common',
transitionDuration: '0.15s',
}}
>
<Text sx={{ fontSize: '2xl', fontWeight: 600, color: 'base.50' }}>
Drop Image
</Text>
</Flex>
<Flex
sx={{
position: 'absolute',
top: 0,
left: 0,
w: 'full',
h: 'full',
opacity: isOver ? 1 : 0.7,
borderWidth: 2,
borderColor: 'base.500',
borderRadius: 'base',
borderStyle: 'dashed',
transitionProperty: 'common',
transitionDuration: '0.15s',
}}
></Flex>
</Flex>
</motion.div>
);
};

View File

@ -0,0 +1,44 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { controlNetBeginStepPctChanged } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
type ParamControlNetBeginStepPctProps = {
controlNetId: string;
beginStepPct: number;
};
const ParamControlNetBeginStepPct = (
props: ParamControlNetBeginStepPctProps
) => {
const { controlNetId, beginStepPct } = props;
const dispatch = useAppDispatch();
const handleBeginStepPctChanged = useCallback(
(beginStepPct: number) => {
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct }));
},
[controlNetId, dispatch]
);
const handleBeginStepPctReset = () => {
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
};
return (
<IAISlider
label="Begin Step %"
value={beginStepPct}
onChange={handleBeginStepPctChanged}
withInput
withReset
handleReset={handleBeginStepPctReset}
withSliderMarks
min={0}
max={1}
step={0.01}
/>
);
};
export default memo(ParamControlNetBeginStepPct);

View File

@ -0,0 +1,42 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { controlNetEndStepPctChanged } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
type ParamControlNetEndStepPctProps = {
controlNetId: string;
endStepPct: number;
};
const ParamControlNetEndStepPct = (props: ParamControlNetEndStepPctProps) => {
const { controlNetId, endStepPct } = props;
const dispatch = useAppDispatch();
const handleEndStepPctChanged = useCallback(
(endStepPct: number) => {
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct }));
},
[controlNetId, dispatch]
);
const handleEndStepPctReset = () => {
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 }));
};
return (
<IAISlider
label="End Step %"
value={endStepPct}
onChange={handleEndStepPctChanged}
withInput
withReset
handleReset={handleEndStepPctReset}
withSliderMarks
min={0}
max={1}
step={0.01}
/>
);
};
export default memo(ParamControlNetEndStepPct);

View File

@ -0,0 +1,28 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAIFullCheckbox from 'common/components/IAIFullCheckbox';
import { controlNetToggled } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
type ParamControlNetIsEnabledProps = {
controlNetId: string;
isEnabled: boolean;
};
const ParamControlNetIsEnabled = (props: ParamControlNetIsEnabledProps) => {
const { controlNetId, isEnabled } = props;
const dispatch = useAppDispatch();
const handleIsEnabledChanged = useCallback(() => {
dispatch(controlNetToggled(controlNetId));
}, [dispatch, controlNetId]);
return (
<IAIFullCheckbox
label="Enabled"
isChecked={isEnabled}
onChange={handleIsEnabledChanged}
/>
);
};
export default memo(ParamControlNetIsEnabled);

View File

@ -0,0 +1,38 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAICustomSelect from 'common/components/IAICustomSelect';
import {
CONTROLNET_MODELS,
ControlNetModel,
controlNetModelChanged,
} from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
type ParamIsControlNetModelProps = {
controlNetId: string;
model: ControlNetModel;
};
const ParamIsControlNetModel = (props: ParamIsControlNetModelProps) => {
const { controlNetId, model } = props;
const dispatch = useAppDispatch();
const handleModelChanged = useCallback(
(val: string | null | undefined) => {
// TODO: do not cast
const model = val as ControlNetModel;
dispatch(controlNetModelChanged({ controlNetId, model }));
},
[controlNetId, dispatch]
);
return (
<IAICustomSelect
label="Model"
items={CONTROLNET_MODELS}
selectedItem={model}
setSelectedItem={handleModelChanged}
/>
);
};
export default memo(ParamIsControlNetModel);

View File

@ -0,0 +1,42 @@
import { useAppDispatch } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { controlNetWeightChanged } from 'features/controlNet/store/controlNetSlice';
import { memo, useCallback } from 'react';
type ParamControlNetWeightProps = {
controlNetId: string;
weight: number;
};
const ParamControlNetWeight = (props: ParamControlNetWeightProps) => {
const { controlNetId, weight } = props;
const dispatch = useAppDispatch();
const handleWeightChanged = useCallback(
(weight: number) => {
dispatch(controlNetWeightChanged({ controlNetId, weight }));
},
[controlNetId, dispatch]
);
const handleWeightReset = () => {
dispatch(controlNetWeightChanged({ controlNetId, weight: 1 }));
};
return (
<IAISlider
label="Weight"
value={weight}
onChange={handleWeightChanged}
withInput
withReset
handleReset={handleWeightReset}
withSliderMarks
min={0}
max={1}
step={0.01}
/>
);
};
export default memo(ParamControlNetWeight);

View File

@ -4,8 +4,6 @@ import { memo, useCallback, useState } from 'react';
import ControlNetProcessButton from './common/ControlNetProcessButton';
import { useAppDispatch } from 'app/store/storeHooks';
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
import { ImageDTO } from 'services/api';
import ControlNetProcessorImage from './common/ControlNetProcessorImage';
import { ControlNetProcessorProps } from '../ControlNet';
export const CANNY_PROCESSOR = 'canny_processor';

View File

@ -1,33 +0,0 @@
import { Flex, Image } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectImagesById } from 'features/gallery/store/imagesSlice';
import { DragEvent, memo, useCallback } from 'react';
import { ImageDTO } from 'services/api';
type ControlNetProcessorImageProps = {
image: ImageDTO | undefined;
setImage: (image: ImageDTO) => void;
};
const ControlNetProcessorImage = (props: ControlNetProcessorImageProps) => {
const { image, setImage } = props;
const state = useAppSelector((state) => state);
const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
const name = e.dataTransfer.getData('invokeai/imageName');
const droppedImage = selectImagesById(state, name);
if (droppedImage) {
setImage(droppedImage);
}
},
[setImage, state]
);
if (!image) {
return <Flex onDrop={handleDrop}>Upload Image</Flex>;
}
return <Image src={image.image_url} />;
};
export default memo(ControlNetProcessorImage);

View File

@ -1,5 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import {
$CombinedState,
PayloadAction,
createSelector,
} from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { ImageDTO } from 'services/api';
export const CONTROLNET_MODELS = [
@ -11,22 +16,22 @@ export const CONTROLNET_MODELS = [
'lllyasviel/sd-controlnet-scribble',
'lllyasviel/sd-controlnet-normal',
'lllyasviel/sd-controlnet-mlsd',
] as const;
];
export const CONTROLNET_PROCESSORS = [
'canny',
'contentShuffle',
'hed',
'lineart',
'lineartAnime',
'mediapipeFace',
'midasDepth',
'mlsd',
'normalBae',
'openpose',
'pidi',
'zoeDepth',
] as const;
// export const CONTROLNET_PROCESSORS = [
// 'canny',
// 'contentShuffle',
// 'hed',
// 'lineart',
// 'lineartAnime',
// 'mediapipeFace',
// 'midasDepth',
// 'mlsd',
// 'normalBae',
// 'openpose',
// 'pidi',
// 'zoeDepth',
// ] as const;
export type ControlNetModel = (typeof CONTROLNET_MODELS)[number];
@ -37,6 +42,7 @@ export const initialControlNet: Omit<ControlNet, 'controlNetId'> = {
beginStepPct: 0,
endStepPct: 1,
controlImage: null,
isControlImageProcessed: false,
processedControlImage: null,
};
@ -48,6 +54,7 @@ export type ControlNet = {
beginStepPct: number;
endStepPct: number;
controlImage: ImageDTO | null;
isControlImageProcessed: boolean;
processedControlImage: ImageDTO | null;
};
@ -63,15 +70,14 @@ export const controlNetSlice = createSlice({
name: 'controlNet',
initialState: initialControlNetState,
reducers: {
controlNetAddedFromModel: (
controlNetAdded: (
state,
action: PayloadAction<{ controlNetId: string; model: ControlNetModel }>
action: PayloadAction<{ controlNetId: string }>
) => {
const { controlNetId, model } = action.payload;
const { controlNetId } = action.payload;
state.controlNets[controlNetId] = {
...initialControlNet,
controlNetId,
model,
};
},
controlNetAddedFromImage: (
@ -96,11 +102,24 @@ export const controlNetSlice = createSlice({
},
controlNetImageChanged: (
state,
action: PayloadAction<{ controlNetId: string; controlImage: ImageDTO }>
action: PayloadAction<{
controlNetId: string;
controlImage: ImageDTO | null;
}>
) => {
const { controlNetId, controlImage } = action.payload;
state.controlNets[controlNetId].controlImage = controlImage;
},
isControlNetImageProcessedToggled: (
state,
action: PayloadAction<{
controlNetId: string;
}>
) => {
const { controlNetId } = action.payload;
state.controlNets[controlNetId].isControlImageProcessed =
!state.controlNets[controlNetId].isControlImageProcessed;
},
controlNetProcessedImageChanged: (
state,
action: PayloadAction<{
@ -144,10 +163,11 @@ export const controlNetSlice = createSlice({
});
export const {
controlNetAddedFromModel,
controlNetAdded,
controlNetAddedFromImage,
controlNetRemoved,
controlNetImageChanged,
isControlNetImageProcessedToggled,
controlNetProcessedImageChanged,
controlNetToggled,
controlNetModelChanged,
@ -157,3 +177,5 @@ export const {
} = controlNetSlice.actions;
export default controlNetSlice.reducer;
export const controlNetSelector = (state: RootState) => state.controlNet;

View File

@ -39,6 +39,8 @@ import {
} from '../store/actions';
import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api';
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -117,6 +119,13 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
const { attributes, listeners, setNodeRef } = useDraggable({
id: image_name,
data: {
image,
},
});
const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false);
@ -212,7 +221,12 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
return (
<>
<Box
ref={setNodeRef}
{...listeners}
{...attributes}
sx={{ w: 'full', h: 'full' }}
>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
@ -291,8 +305,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
// draggable={true}
// onDragStart={handleDragStart}
onClick={handleSelectImage}
ref={ref}
sx={{
@ -373,7 +387,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
</>
</Box>
);
}, memoEqualityCheck);

View File

@ -10,7 +10,7 @@ import {
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider';
@ -233,7 +233,7 @@ const ImageGalleryContent = () => {
withReset
handleReset={() => dispatch(setGalleryImageMinimumWidth(64))}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('gallery.maintainAspectRatio')}
isChecked={galleryImageObjectFit === 'contain'}
onChange={() =>
@ -244,14 +244,14 @@ const ImageGalleryContent = () => {
)
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('gallery.autoSwitchNewImages')}
isChecked={shouldAutoSwitchToNewImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoSwitchToNewImages(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('gallery.singleColumnLayout')}
isChecked={shouldUseSingleGalleryColumn}
onChange={(e: ChangeEvent<HTMLInputElement>) =>

View File

@ -1,39 +1,26 @@
import { Box, Image } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl';
import useGetImageByName from 'features/gallery/hooks/useGetImageByName';
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
import {
ImageInputFieldTemplate,
ImageInputFieldValue,
} from 'features/nodes/types/types';
import { DragEvent, memo, useCallback, useState } from 'react';
import { memo, useCallback } from 'react';
import { FieldComponentProps } from './types';
import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage';
import { ImageDTO } from 'services/api';
import { Flex } from '@chakra-ui/react';
const ImageInputFieldComponent = (
props: FieldComponentProps<ImageInputFieldValue, ImageInputFieldTemplate>
) => {
const { nodeId, field } = props;
const getImageByName = useGetImageByName();
const dispatch = useAppDispatch();
const [url, setUrl] = useState<string | undefined>(field.value?.image_url);
const { getUrl } = useGetUrl();
const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
const name = e.dataTransfer.getData('invokeai/imageName');
const image = getImageByName(name);
if (!image) {
return;
}
setUrl(image.image_url);
const handleChange = useCallback(
(image: ImageDTO) => {
dispatch(
fieldValueChanged({
nodeId,
@ -42,13 +29,20 @@ const ImageInputFieldComponent = (
})
);
},
[getImageByName, dispatch, field.name, nodeId]
[dispatch, field.name, nodeId]
);
return (
<Box onDrop={handleDrop}>
<Image src={getUrl(url)} fallback={<SelectImagePlaceholder />} />
</Box>
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IAISelectableImage image={field.value} onChange={handleChange} />
</Flex>
);
};

View File

@ -1,61 +1,59 @@
import { Flex, Text, useDisclosure } from '@chakra-ui/react';
import { Flex, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import IAICollapse from 'common/components/IAICollapse';
import { memo, useCallback, useState } from 'react';
import IAICustomSelect from 'common/components/IAICustomSelect';
import { memo, useCallback } from 'react';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaPlus } from 'react-icons/fa';
import CannyProcessor from 'features/controlNet/components/processors/CannyProcessor';
import ControlNet from 'features/controlNet/components/ControlNet';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { createSelector } from '@reduxjs/toolkit';
import {
controlNetAdded,
controlNetSelector,
} from 'features/controlNet/store/controlNetSlice';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { map } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid';
const CONTROLNET_MODELS = [
'lllyasviel/sd-controlnet-canny',
'lllyasviel/sd-controlnet-depth',
'lllyasviel/sd-controlnet-hed',
'lllyasviel/sd-controlnet-seg',
'lllyasviel/sd-controlnet-openpose',
'lllyasviel/sd-controlnet-scribble',
'lllyasviel/sd-controlnet-normal',
'lllyasviel/sd-controlnet-mlsd',
];
const selector = createSelector(
controlNetSelector,
(controlNet) => {
const { controlNets } = controlNet;
return { controlNets };
},
defaultSelectorOptions
);
const ParamControlNetCollapse = () => {
const { t } = useTranslation();
const { isOpen, onToggle } = useDisclosure();
const [model, setModel] = useState<string>(CONTROLNET_MODELS[0]);
const { controlNets } = useAppSelector(selector);
const dispatch = useAppDispatch();
const handleSetControlNet = useCallback(
(model: string | null | undefined) => {
if (model) {
setModel(model);
}
},
[]
);
const handleClickedAddControlNet = useCallback(() => {
dispatch(controlNetAdded({ controlNetId: uuidv4() }));
}, [dispatch]);
return (
<ControlNet />
// <IAICollapse
// label={'ControlNet'}
// // label={t('parameters.seamCorrectionHeader')}
// isOpen={isOpen}
// onToggle={onToggle}
// >
// <Flex sx={{ alignItems: 'flex-end' }}>
// <IAICustomSelect
// label="ControlNet Model"
// items={CONTROLNET_MODELS}
// selectedItem={model}
// setSelectedItem={handleSetControlNet}
// />
// <IAIIconButton
// size="sm"
// aria-label="Add ControlNet"
// icon={<FaPlus />}
// />
// </Flex>
// <CannyProcessor />
// </IAICollapse>
<IAICollapse
label={'ControlNet'}
// label={t('parameters.seamCorrectionHeader')}
isOpen={isOpen}
onToggle={onToggle}
>
<Flex sx={{ alignItems: 'flex-end' }}>
<IAIIconButton
size="sm"
aria-label="Add ControlNet"
onClick={handleClickedAddControlNet}
icon={<FaPlus />}
/>
</Flex>
{map(controlNets, (c) => (
<ControlNet key={c.controlNetId} controlNet={c} />
))}
</IAICollapse>
);
};

View File

@ -28,7 +28,6 @@ const InitialImageDisplay = () => {
gap: 4,
}}
>
<InitialImageButtons />
<InitialImagePreview />
</Flex>
</Flex>

View File

@ -2,7 +2,10 @@ import { Flex, Icon, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import {
clearInitialImage,
initialImageChanged,
} from 'features/parameters/store/generationSlice';
import { DragEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
@ -13,6 +16,8 @@ import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinn
import { FaImage } from 'react-icons/fa';
import { configSelector } from '../../../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage';
import { ImageDTO } from 'services/api';
const selector = createSelector(
[generationSelector],
@ -51,14 +56,17 @@ const InitialImagePreview = () => {
}
}, [dispatch, t, toaster, shouldFetchImages]);
const handleDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => {
const name = e.dataTransfer.getData('invokeai/imageName');
dispatch(initialImageSelected(name));
const handleChange = useCallback(
(image: ImageDTO) => {
dispatch(initialImageChanged(image));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(clearInitialImage());
}, [dispatch]);
return (
<Flex
sx={{
@ -68,9 +76,14 @@ const InitialImagePreview = () => {
alignItems: 'center',
justifyContent: 'center',
}}
onDrop={handleDrop}
// onDrop={handleDrop}
>
{initialImage?.image_url && (
<IAISelectableImage
image={initialImage}
onChange={handleChange}
onReset={handleReset}
/>
{/* {initialImage?.image_url && (
<>
<Image
src={getUrl(initialImage?.image_url)}
@ -97,7 +110,7 @@ const InitialImagePreview = () => {
color: 'base.500',
}}
/>
)}
)} */}
</Flex>
);
};

View File

@ -7,25 +7,6 @@ export type ImageNameAndOrigin = {
image_origin: ResourceOrigin;
};
export const isImageDTO = (image: any): image is ImageDTO => {
return (
image &&
isObject(image) &&
'image_name' in image &&
image?.image_name !== undefined &&
'image_origin' in image &&
image?.image_origin !== undefined &&
'image_url' in image &&
image?.image_url !== undefined &&
'thumbnail_url' in image &&
image?.thumbnail_url !== undefined &&
'image_category' in image &&
image?.image_category !== undefined &&
'created_at' in image &&
image?.created_at !== undefined
);
};
export const initialImageSelected = createAction<ImageDTO | string | undefined>(
'generation/initialImageSelected'
);

View File

@ -10,7 +10,7 @@ import {
} from '@chakra-ui/react';
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIInput from 'common/components/IAIInput';
import IAINumberInput from 'common/components/IAINumberInput';
import React from 'react';
@ -74,12 +74,12 @@ export default function AddCheckpointModel() {
return (
<VStack gap={2} alignItems="flex-start">
<Flex columnGap={4}>
<IAICheckbox
<IAISimpleCheckbox
isChecked={!addManually}
label={t('modelManager.scanForModels')}
onChange={() => setAddmanually(!addManually)}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('modelManager.addManually')}
isChecked={addManually}
onChange={() => setAddmanually(!addManually)}

View File

@ -24,7 +24,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import * as InvokeAI from 'app/types/invokeai';
import IAISlider from 'common/components/IAISlider';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
export default function MergeModels() {
const dispatch = useAppDispatch();
@ -286,7 +286,7 @@ export default function MergeModels() {
)}
</Flex>
<IAICheckbox
<IAISimpleCheckbox
label={t('modelManager.ignoreMismatch')}
isChecked={modelMergeForce}
onChange={(e) => setModelMergeForce(e.target.checked)}

View File

@ -1,5 +1,5 @@
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import React from 'react';
@ -81,13 +81,13 @@ function SearchModelEntry({
borderRadius={4}
>
<Flex gap={4} alignItems="center" justifyContent="space-between">
<IAICheckbox
<IAISimpleCheckbox
value={model.name}
label={<Text fontWeight={500}>{model.name}</Text>}
isChecked={modelsToAdd.includes(model.name)}
isDisabled={existingModels.includes(model.location)}
onChange={foundModelsChangeHandler}
></IAICheckbox>
></IAISimpleCheckbox>
{existingModels.includes(model.location) && (
<Badge colorScheme="accent">{t('modelManager.modelExists')}</Badge>
)}
@ -324,7 +324,7 @@ export default function SearchModels() {
>
{t('modelManager.deselectAll')}
</IAIButton>
<IAICheckbox
<IAISimpleCheckbox
label={t('modelManager.showExisting')}
isChecked={shouldShowExistingModelsInSearch}
onChange={() =>

View File

@ -8,6 +8,7 @@ import ParamNoiseCollapse from 'features/parameters/components/Parameters/Noise/
import ParamSymmetryCollapse from 'features/parameters/components/Parameters/Symmetry/ParamSymmetryCollapse';
import ParamSeamlessCollapse from 'features/parameters/components/Parameters/Seamless/ParamSeamlessCollapse';
import ImageToImageTabCoreParameters from './ImageToImageTabCoreParameters';
import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse';
const ImageToImageTabParameters = () => {
return (
@ -17,6 +18,7 @@ const ImageToImageTabParameters = () => {
<ProcessButtons />
<ImageToImageTabCoreParameters />
<ParamSeedCollapse />
<ParamControlNetCollapse />
<ParamVariationCollapse />
<ParamNoiseCollapse />
<ParamSymmetryCollapse />

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setShouldDarkenOutsideBoundingBox } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
@ -14,7 +14,7 @@ export default function UnifiedCanvasDarkenOutsideSelection() {
const { t } = useTranslation();
return (
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.betaDarkenOutside')}
isChecked={shouldDarkenOutsideBoundingBox}
onChange={(e) =>

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setIsMaskEnabled } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
@ -16,7 +16,7 @@ export default function UnifiedCanvasEnableMask() {
dispatch(setIsMaskEnabled(!isMaskEnabled));
return (
<IAICheckbox
<IAISimpleCheckbox
label={`${t('unifiedCanvas.enableMask')} (H)`}
isChecked={isMaskEnabled}
onChange={handleToggleEnableMask}

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setShouldRestrictStrokesToBox } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
@ -14,7 +14,7 @@ export default function UnifiedCanvasLimitStrokesToBox() {
const { t } = useTranslation();
return (
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.betaLimitToBox')}
isChecked={shouldRestrictStrokesToBox}
onChange={(e) =>

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setShouldPreserveMaskedArea } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
@ -13,7 +13,7 @@ export default function UnifiedCanvasPreserveMask() {
);
return (
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.betaPreserveMasked')}
isChecked={shouldPreserveMaskedArea}
onChange={(e) => dispatch(setShouldPreserveMaskedArea(e.target.checked))}

View File

@ -1,7 +1,7 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import IAIIconButton from 'common/components/IAIIconButton';
import IAIPopover from 'common/components/IAIPopover';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
@ -73,33 +73,33 @@ const UnifiedCanvasSettings = () => {
}
>
<Flex direction="column" gap={2}>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showIntermediates')}
isChecked={shouldShowIntermediates}
onChange={(e) =>
dispatch(setShouldShowIntermediates(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.autoSaveToGallery')}
isChecked={shouldAutoSave}
onChange={(e) => dispatch(setShouldAutoSave(e.target.checked))}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.saveBoxRegionOnly')}
isChecked={shouldCropToBoundingBoxOnSave}
onChange={(e) =>
dispatch(setShouldCropToBoundingBoxOnSave(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showCanvasDebugInfo')}
isChecked={shouldShowCanvasDebugInfo}
onChange={(e) =>
dispatch(setShouldShowCanvasDebugInfo(e.target.checked))
}
/>
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.antialiasing')}
isChecked={shouldAntialias}
onChange={(e) => dispatch(setShouldAntialias(e.target.checked))}

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setShouldShowGrid } from 'features/canvas/store/canvasSlice';
import { useTranslation } from 'react-i18next';
@ -13,7 +13,7 @@ export default function UnifiedCanvasShowGrid() {
const { t } = useTranslation();
return (
<IAICheckbox
<IAISimpleCheckbox
label={t('unifiedCanvas.showGrid')}
isChecked={shouldShowGrid}
onChange={(e) => dispatch(setShouldShowGrid(e.target.checked))}

View File

@ -1,6 +1,6 @@
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAICheckbox from 'common/components/IAICheckbox';
import IAISimpleCheckbox from 'common/components/IAISimpleCheckbox';
import { setShouldSnapToGrid } from 'features/canvas/store/canvasSlice';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
@ -17,7 +17,7 @@ export default function UnifiedCanvasSnapToGrid() {
dispatch(setShouldSnapToGrid(e.target.checked));
return (
<IAICheckbox
<IAISimpleCheckbox
label={`${t('unifiedCanvas.snapToGrid')} (N)`}
isChecked={shouldSnapToGrid}
onChange={handleChangeShouldSnapToGrid}

View File

@ -10,8 +10,25 @@ import {
ImageField,
LatentsOutput,
ResourceOrigin,
ImageDTO,
} from 'services/api';
export const isImageDTO = (obj: unknown): obj is ImageDTO => {
return (
isObject(obj) &&
'image_name' in obj &&
isString(obj?.image_name) &&
'thumbnail_url' in obj &&
isString(obj?.thumbnail_url) &&
'image_url' in obj &&
isString(obj?.image_url) &&
'image_origin' in obj &&
isString(obj?.image_origin) &&
'created_at' in obj &&
isString(obj?.created_at)
);
};
export const isImageOutput = (
output: GraphExecutionState['results'][string]
): output is ImageOutput => output.type === 'image_output';

View File

@ -937,6 +937,29 @@
gonzales-pe "^4.3.0"
node-source-walk "^5.0.1"
"@dnd-kit/accessibility@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c"
integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@^6.0.8":
version "6.0.8"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005"
integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==
dependencies:
"@dnd-kit/accessibility" "^3.0.0"
"@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"
integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==
dependencies:
tslib "^2.0.0"
"@emotion/babel-plugin@^11.10.8":
version "11.10.8"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.8.tgz#bae325c902937665d00684038fd5294223ef9e1d"