feat(ui): implement cache for image rasterization, rip out some old controladapters code

This commit is contained in:
psychedelicious 2024-08-14 20:35:19 +10:00
parent abe8db8154
commit e49b72ee4e
52 changed files with 321 additions and 2074 deletions

View File

@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
@ -133,4 +132,4 @@ addAdHocPostProcessingRequestedListener(startAppListening);
addDynamicPromptsListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
addControlAdapterPreprocessor(startAppListening);
// addControlAdapterPreprocessor(startAppListening);

View File

@ -1,5 +1,5 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { caAllDeleted, ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { ipaAllDeleted, layerAllDeleted } from 'features/controlLayers/store/canvasV2Slice';
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { imagesApi } from 'services/api/endpoints/images';
@ -14,7 +14,7 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
let wereLayersReset = false;
let wasNodeEditorReset = false;
let wereControlAdaptersReset = false;
const wereControlAdaptersReset = false;
let wereIPAdaptersReset = false;
const { nodes, canvasV2 } = getState();
@ -31,10 +31,10 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
wasNodeEditorReset = true;
}
if (imageUsage.isControlAdapterImage && !wereControlAdaptersReset) {
dispatch(caAllDeleted());
wereControlAdaptersReset = true;
}
// if (imageUsage.isControlAdapterImage && !wereControlAdaptersReset) {
// dispatch(caAllDeleted());
// wereControlAdaptersReset = true;
// }
if (imageUsage.isIPAdapterImage && !wereIPAdaptersReset) {
dispatch(ipaAllDeleted());

View File

@ -1,12 +1,7 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { AppDispatch, RootState } from 'app/store/store';
import {
caImageChanged,
caProcessedImageChanged,
entityDeleted,
ipaImageChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
@ -39,14 +34,17 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
});
};
const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
if (imageObject?.image.image_name === imageDTO.image_name || processedImageObject?.image.image_name === imageDTO.image_name) {
dispatch(caImageChanged({ id, imageDTO: null }));
dispatch(caProcessedImageChanged({ id, imageDTO: null }));
}
});
};
// const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
// state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
// if (
// imageObject?.image.image_name === imageDTO.image_name ||
// processedImageObject?.image.image_name === imageDTO.image_name
// ) {
// dispatch(caImageChanged({ id, imageDTO: null }));
// dispatch(caProcessedImageChanged({ id, imageDTO: null }));
// }
// });
// };
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
state.canvasV2.ipAdapters.entities.forEach(({ id, imageObject }) => {
@ -120,7 +118,7 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
}
deleteNodesImages(state, dispatch, imageDTO);
deleteControlAdapterImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
} catch {
@ -161,7 +159,7 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
imageDTOs.forEach((imageDTO) => {
deleteNodesImages(state, dispatch, imageDTO);
deleteControlAdapterImages(state, dispatch, imageDTO);
// deleteControlAdapterImages(state, dispatch, imageDTO);
deleteIPAdapterImages(state, dispatch, imageDTO);
deleteLayerImages(state, dispatch, imageDTO);
});

View File

@ -3,7 +3,6 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { parseify } from 'common/util/serialize';
import {
caImageChanged,
ipaImageChanged,
layerAdded,
rgIPAdapterImageChanged,
@ -60,18 +59,18 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
return;
}
/**
* Image dropped on Control Adapter Layer
*/
if (
overData.actionType === 'SET_CA_IMAGE' &&
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const { id } = overData.context;
dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
return;
}
// /**
// * Image dropped on Control Adapter Layer
// */
// if (
// overData.actionType === 'SET_CA_IMAGE' &&
// activeData.payloadType === 'IMAGE_DTO' &&
// activeData.payload.imageDTO
// ) {
// const { id } = overData.context;
// dispatch(caImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
// return;
// }
/**
* Image dropped on IP Adapter Layer

View File

@ -1,6 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { caImageChanged, ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
@ -79,12 +79,12 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
return;
}
if (postUploadAction?.type === 'SET_CA_IMAGE') {
const { id } = postUploadAction;
dispatch(caImageChanged({ id, imageDTO }));
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
return;
}
// if (postUploadAction?.type === 'SET_CA_IMAGE') {
// const { id } = postUploadAction;
// dispatch(caImageChanged({ id, imageDTO }));
// toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
// return;
// }
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
const { id } = postUploadAction;

View File

@ -1,7 +1,6 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
entityIsEnabledToggled,
loraDeleted,
modelChanged,
vaeSelected,
@ -50,14 +49,14 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
}
// handle incompatible controlnets
state.canvasV2.controlAdapters.entities.forEach((ca) => {
if (ca.model?.base !== newBaseModel) {
modelsCleared += 1;
if (ca.isEnabled) {
dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
}
}
});
// state.canvasV2.controlAdapters.entities.forEach((ca) => {
// if (ca.model?.base !== newBaseModel) {
// modelsCleared += 1;
// if (ca.isEnabled) {
// dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
// }
// }
// });
if (modelsCleared > 0) {
toast({

View File

@ -5,7 +5,6 @@ import type { JSONObject } from 'common/types';
import {
bboxHeightChanged,
bboxWidthChanged,
caModelChanged,
ipaModelChanged,
loraDeleted,
modelChanged,
@ -21,7 +20,6 @@ import type { Logger } from 'roarr';
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
import type { AnyModelConfig } from 'services/api/types';
import {
isControlNetOrT2IAdapterModelConfig,
isIPAdapterModelConfig,
isLoRAModelConfig,
isNonRefinerMainModelConfig,
@ -171,14 +169,14 @@ const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => {
};
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
const caModels = models.filter(isControlNetOrT2IAdapterModelConfig);
state.canvasV2.controlAdapters.entities.forEach((ca) => {
const isModelAvailable = caModels.some((m) => m.key === ca.model?.key);
if (isModelAvailable) {
return;
}
dispatch(caModelChanged({ id: ca.id, modelConfig: null }));
});
// const caModels = models.filter(isControlNetOrT2IAdapterModelConfig);
// state.canvasV2.controlAdapters.entities.forEach((ca) => {
// const isModelAvailable = caModels.some((m) => m.key === ca.model?.key);
// if (isModelAvailable) {
// return;
// }
// dispatch(caModelChanged({ id: ca.id, modelConfig: null }));
// });
};
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {

View File

@ -125,41 +125,41 @@ const createSelector = (templates: Templates) =>
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
}
canvasV2.controlAdapters.entities
.filter((ca) => ca.isEnabled)
.forEach((ca, i) => {
const layerLiteral = i18n.t('controlLayers.layers_one');
const layerNumber = i + 1;
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]);
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
const problems: string[] = [];
// Must have model
if (!ca.model) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
}
// Model base must match
if (ca.model?.base !== model?.base) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
}
// Must have a control image OR, if it has a processor, it must have a processed image
if (!ca.imageObject) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
} else if (ca.processorConfig && !ca.processedImageObject) {
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
}
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
if (ca.adapterType === 't2i_adapter') {
const multiple = model?.base === 'sdxl' ? 32 : 64;
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
}
}
// canvasV2.controlAdapters.entities
// .filter((ca) => ca.isEnabled)
// .forEach((ca, i) => {
// const layerLiteral = i18n.t('controlLayers.layers_one');
// const layerNumber = i + 1;
// const layerType = i18n.t(LAYER_TYPE_TO_TKEY[ca.type]);
// const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
// const problems: string[] = [];
// // Must have model
// if (!ca.model) {
// problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
// }
// // Model base must match
// if (ca.model?.base !== model?.base) {
// problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
// }
// // Must have a control image OR, if it has a processor, it must have a processed image
// if (!ca.imageObject) {
// problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
// } else if (ca.processorConfig && !ca.processedImageObject) {
// problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
// }
// // T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
// if (ca.adapterType === 't2i_adapter') {
// const multiple = model?.base === 'sdxl' ? 32 : 64;
// if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
// problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
// }
// }
if (problems.length) {
const content = upperFirst(problems.join(', '));
reasons.push({ prefix, content });
}
});
// if (problems.length) {
// const content = upperFirst(problems.join(', '));
// reasons.push({ prefix, content });
// }
// });
canvasV2.ipAdapters.entities
.filter((ipa) => ipa.isEnabled)

View File

@ -1,6 +1,5 @@
import { Flex } from '@invoke-ai/ui-library';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { ControlAdapterList } from 'features/controlLayers/components/ControlAdapter/ControlAdapterList';
import { InpaintMask } from 'features/controlLayers/components/InpaintMask/InpaintMask';
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
import { LayerEntityList } from 'features/controlLayers/components/Layer/LayerEntityList';
@ -13,7 +12,6 @@ export const CanvasEntityList = memo(() => {
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">
<InpaintMask />
<RegionalGuidanceEntityList />
<ControlAdapterList />
<IPAdapterList />
<LayerEntityList />
</Flex>

View File

@ -1,39 +0,0 @@
import { Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { ControlAdapterActionsMenu } from 'features/controlLayers/components/ControlAdapter/ControlAdapterActionsMenu';
import { ControlAdapterOpacityAndFilter } from 'features/controlLayers/components/ControlAdapter/ControlAdapterOpacityAndFilter';
import { ControlAdapterSettings } from 'features/controlLayers/components/ControlAdapter/ControlAdapterSettings';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
type Props = {
id: string;
};
export const ControlAdapter = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'control_adapter' }), [id]);
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
return (
<EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer>
<CanvasEntityHeader onDoubleClick={onToggle}>
<CanvasEntityEnabledToggle />
<CanvasEntityTitle />
<Spacer />
<ControlAdapterOpacityAndFilter />
<ControlAdapterActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>
{isOpen && <ControlAdapterSettings />}
</CanvasEntityContainer>
</EntityIdentifierContext.Provider>
);
});
ControlAdapter.displayName = 'ControlAdapter';

View File

@ -1,17 +0,0 @@
import { Menu, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityActionMenuItems } from 'features/controlLayers/components/common/CanvasEntityActionMenuItems';
import { CanvasEntityMenuButton } from 'features/controlLayers/components/common/CanvasEntityMenuButton';
import { memo } from 'react';
export const ControlAdapterActionsMenu = memo(() => {
return (
<Menu>
<CanvasEntityMenuButton />
<MenuList>
<CanvasEntityActionMenuItems />
</MenuList>
</Menu>
);
});
ControlAdapterActionsMenu.displayName = 'ControlAdapterActionsMenu';

View File

@ -1,229 +0,0 @@
import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { CanvasControlAdapterState } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
import {
useAddImageToBoardMutation,
useChangeImageIsIntermediateMutation,
useGetImageDTOQuery,
useRemoveImageFromBoardMutation,
} from 'services/api/endpoints/images';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
type Props = {
controlAdapter: CanvasControlAdapterState;
onChangeImage: (imageDTO: ImageDTO | null) => void;
droppableData: TypesafeDroppableData;
postUploadAction: PostUploadAction;
onErrorLoadingImage: () => void;
onErrorLoadingProcessedImage: () => void;
};
export const ControlAdapterImagePreview = memo(
({
controlAdapter,
onChangeImage,
droppableData,
postUploadAction,
onErrorLoadingImage,
onErrorLoadingProcessedImage,
}: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
const isConnected = useAppSelector((s) => s.system.isConnected);
const optimalDimension = useAppSelector(selectOptimalDimension);
const shift = useShiftModifier();
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
controlAdapter.imageObject?.image.image_name ?? skipToken
);
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
controlAdapter.processedImageObject?.image.image_name ?? skipToken
);
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
const [addToBoard] = useAddImageToBoardMutation();
const [removeFromBoard] = useRemoveImageFromBoardMutation();
const handleResetControlImage = useCallback(() => {
onChangeImage(null);
}, [onChangeImage]);
const handleSaveControlImage = useCallback(async () => {
if (!processedControlImage) {
return;
}
await changeIsIntermediate({
imageDTO: processedControlImage,
is_intermediate: false,
}).unwrap();
if (autoAddBoardId !== 'none') {
addToBoard({
imageDTO: processedControlImage,
board_id: autoAddBoardId,
});
} else {
removeFromBoard({ imageDTO: processedControlImage });
}
}, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]);
const handleSetControlImageToDimensions = useCallback(() => {
if (!controlImage) {
return;
}
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = controlImage;
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
}
}, [controlImage, dispatch, optimalDimension, shift]);
const handleMouseEnter = useCallback(() => {
setIsMouseOverImage(true);
}, []);
const handleMouseLeave = useCallback(() => {
setIsMouseOverImage(false);
}, []);
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
if (controlImage) {
return {
id: controlAdapter.id,
payloadType: 'IMAGE_DTO',
payload: { imageDTO: controlImage },
};
}
}, [controlImage, controlAdapter.id]);
const shouldShowProcessedImage =
controlImage &&
processedControlImage &&
!isMouseOverImage &&
!controlAdapter.processorPendingBatchId &&
controlAdapter.processorConfig !== null;
useEffect(() => {
if (!isConnected) {
return;
}
if (isErrorControlImage) {
onErrorLoadingImage();
}
if (isErrorProcessedControlImage) {
onErrorLoadingProcessedImage();
}
}, [
handleResetControlImage,
isConnected,
isErrorControlImage,
isErrorProcessedControlImage,
onErrorLoadingImage,
onErrorLoadingProcessedImage,
]);
return (
<Flex
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
position="relative"
w={36}
h={36}
alignItems="center"
justifyContent="center"
>
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={controlImage}
isDropDisabled={shouldShowProcessedImage}
postUploadAction={postUploadAction}
/>
<Box
position="absolute"
top={0}
insetInlineStart={0}
w="full"
h="full"
opacity={shouldShowProcessedImage ? 1 : 0}
transitionProperty="common"
transitionDuration="normal"
pointerEvents="none"
>
<IAIDndImage
draggableData={draggableData}
droppableData={droppableData}
imageDTO={processedControlImage}
isUploadDisabled={true}
onError={handleResetControlImage}
/>
</Box>
{controlImage && (
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}
tooltip={t('controlnet.resetControlImage')}
/>
<IAIDndImageIcon
onClick={handleSaveControlImage}
icon={<PiFloppyDiskBold size={16} />}
tooltip={t('controlnet.saveControlImage')}
/>
<IAIDndImageIcon
onClick={handleSetControlImageToDimensions}
icon={<PiRulerBold size={16} />}
tooltip={
shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
}
/>
</Flex>
)}
{controlAdapter.processorPendingBatchId !== null && (
<Flex
position="absolute"
top={0}
insetInlineStart={0}
w="full"
h="full"
alignItems="center"
justifyContent="center"
opacity={0.8}
borderRadius="base"
bg="base.900"
>
<Spinner size="xl" color="base.400" />
</Flex>
)}
</Flex>
);
}
);
ControlAdapterImagePreview.displayName = 'ControlAdapterImagePreview';

View File

@ -1,38 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasEntityGroupTitle } from 'features/controlLayers/components/common/CanvasEntityGroupTitle';
import { ControlAdapter } from 'features/controlLayers/components/ControlAdapter/ControlAdapter';
import { mapId } from 'features/controlLayers/konva/util';
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectEntityIds = createMemoizedSelector(selectCanvasV2Slice, (canvasV2) => {
return canvasV2.controlAdapters.entities.map(mapId).reverse();
});
export const ControlAdapterList = memo(() => {
const { t } = useTranslation();
const isSelected = useAppSelector((s) => Boolean(s.canvasV2.selectedEntityIdentifier?.type === 'control_adapter'));
const caIds = useAppSelector(selectEntityIds);
if (caIds.length === 0) {
return null;
}
if (caIds.length > 0) {
return (
<>
<CanvasEntityGroupTitle
title={t('controlLayers.controlAdapters_withCount', { count: caIds.length })}
isSelected={isSelected}
/>
{caIds.map((id) => (
<ControlAdapter key={id} id={id} />
))}
</>
);
}
});
ControlAdapterList.displayName = 'ControlAdapterList';

View File

@ -1,99 +0,0 @@
import {
CompositeNumberInput,
CompositeSlider,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Switch,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { caFilterChanged, caOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
export const ControlAdapterOpacityAndFilter = memo(() => {
const { id } = useEntityIdentifierContext();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => Math.round(selectCAOrThrow(s.canvasV2, id).opacity * 100));
const isFilterEnabled = useAppSelector((s) =>
selectCAOrThrow(s.canvasV2, id).filters.includes('LightnessToAlphaFilter')
);
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(caOpacityChanged({ id, opacity: v / 100 }));
},
[dispatch, id]
);
const onChangeFilter = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(caFilterChanged({ id, filters: e.target.checked ? ['LightnessToAlphaFilter'] : [] }));
},
[dispatch, id]
);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
aria-label={t('controlLayers.opacity')}
size="sm"
icon={<PiDropHalfFill size={16} />}
variant="ghost"
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl orientation="horizontal" w="full">
<FormLabel m={0} flexGrow={1} cursor="pointer">
{t('controlLayers.opacityFilter')}
</FormLabel>
<Switch isChecked={isFilterEnabled} onChange={onChangeFilter} />
</FormControl>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
marks={marks}
w={48}
/>
<CompositeNumberInput
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
w={24}
format={formatPct}
/>
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
ControlAdapterOpacityAndFilter.displayName = 'ControlAdapterOpacityAndFilter';

View File

@ -1,84 +0,0 @@
import { CannyProcessor } from 'features/controlLayers/components/ControlAdapter/processors/CannyProcessor';
import { ColorMapProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ColorMapProcessor';
import { ContentShuffleProcessor } from 'features/controlLayers/components/ControlAdapter/processors/ContentShuffleProcessor';
import { DepthAnythingProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DepthAnythingProcessor';
import { DWOpenposeProcessor } from 'features/controlLayers/components/ControlAdapter/processors/DWOpenposeProcessor';
import { HedProcessor } from 'features/controlLayers/components/ControlAdapter/processors/HedProcessor';
import { LineartProcessor } from 'features/controlLayers/components/ControlAdapter/processors/LineartProcessor';
import { MediapipeFaceProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MediapipeFaceProcessor';
import { MidasDepthProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MidasDepthProcessor';
import { MlsdImageProcessor } from 'features/controlLayers/components/ControlAdapter/processors/MlsdImageProcessor';
import { PidiProcessor } from 'features/controlLayers/components/ControlAdapter/processors/PidiProcessor';
import type { FilterConfig } from 'features/controlLayers/store/types';
import { memo } from 'react';
type Props = {
config: FilterConfig | null;
onChange: (config: FilterConfig | null) => void;
};
export const ControlAdapterProcessorConfig = memo(({ config, onChange }: Props) => {
if (!config) {
return null;
}
if (config.type === 'canny_image_processor') {
return <CannyProcessor onChange={onChange} config={config} />;
}
if (config.type === 'color_map_image_processor') {
return <ColorMapProcessor onChange={onChange} config={config} />;
}
if (config.type === 'depth_anything_image_processor') {
return <DepthAnythingProcessor onChange={onChange} config={config} />;
}
if (config.type === 'hed_image_processor') {
return <HedProcessor onChange={onChange} config={config} />;
}
if (config.type === 'lineart_image_processor') {
return <LineartProcessor onChange={onChange} config={config} />;
}
if (config.type === 'content_shuffle_image_processor') {
return <ContentShuffleProcessor onChange={onChange} config={config} />;
}
if (config.type === 'lineart_anime_image_processor') {
// No configurable options for this processor
return null;
}
if (config.type === 'mediapipe_face_processor') {
return <MediapipeFaceProcessor onChange={onChange} config={config} />;
}
if (config.type === 'midas_depth_image_processor') {
return <MidasDepthProcessor onChange={onChange} config={config} />;
}
if (config.type === 'mlsd_image_processor') {
return <MlsdImageProcessor onChange={onChange} config={config} />;
}
if (config.type === 'normalbae_image_processor') {
// No configurable options for this processor
return null;
}
if (config.type === 'dw_openpose_image_processor') {
return <DWOpenposeProcessor onChange={onChange} config={config} />;
}
if (config.type === 'pidi_image_processor') {
return <PidiProcessor onChange={onChange} config={config} />;
}
if (config.type === 'zoe_depth_image_processor') {
return null;
}
});
ControlAdapterProcessorConfig.displayName = 'ControlAdapterProcessorConfig';

View File

@ -1,70 +0,0 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, Flex, FormControl, FormLabel, IconButton } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import type {FilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { configSelector } from 'features/system/store/configSelectors';
import { includes, map } from 'lodash-es';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { assert } from 'tsafe';
type Props = {
config: FilterConfig | null;
onChange: (config: FilterConfig | null) => void;
};
const selectDisabledProcessors = createMemoizedSelector(
configSelector,
(config) => config.sd.disabledControlNetProcessors
);
export const ControlAdapterProcessorTypeSelect = memo(({ config, onChange }: Props) => {
const { t } = useTranslation();
const disabledProcessors = useAppSelector(selectDisabledProcessors);
const options = useMemo(() => {
return map(IMAGE_FILTERS, ({ labelTKey }, type) => ({ value: type, label: t(labelTKey) })).filter(
(o) => !includes(disabledProcessors, o.value)
);
}, [disabledProcessors, t]);
const _onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!v) {
onChange(null);
} else {
assert(isFilterType(v.value));
onChange(IMAGE_FILTERS[v.value].buildDefaults());
}
},
[onChange]
);
const clearProcessor = useCallback(() => {
onChange(null);
}, [onChange]);
const value = useMemo(() => options.find((o) => o.value === config?.type) ?? null, [options, config?.type]);
return (
<Flex gap={2}>
<FormControl>
<InformationalPopover feature="controlNetProcessor">
<FormLabel m={0}>{t('controlnet.processor')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} onChange={_onChange} isSearchable={false} isClearable={false} />
</FormControl>
<IconButton
aria-label={t('controlLayers.clearProcessor')}
onClick={clearProcessor}
isDisabled={!config}
icon={<PiXBold />}
variant="ghost"
size="sm"
/>
</Flex>
);
});
ControlAdapterProcessorTypeSelect.displayName = 'ControlAdapterProcessorTypeSelect';

View File

@ -1,154 +0,0 @@
import { Box, Divider, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { CanvasEntitySettings } from 'features/controlLayers/components/common/CanvasEntitySettings';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { ControlAdapterControlModeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterControlModeSelect';
import { ControlAdapterImagePreview } from 'features/controlLayers/components/ControlAdapter/ControlAdapterImagePreview';
import { ControlAdapterModel } from 'features/controlLayers/components/ControlAdapter/ControlAdapterModel';
import { ControlAdapterProcessorConfig } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorConfig';
import { ControlAdapterProcessorTypeSelect } from 'features/controlLayers/components/ControlAdapter/ControlAdapterProcessorTypeSelect';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
caBeginEndStepPctChanged,
caControlModeChanged,
caImageChanged,
caModelChanged,
caProcessedImageChanged,
caProcessorConfigChanged,
caWeightChanged,
} from 'features/controlLayers/store/canvasV2Slice';
import { selectCAOrThrow } from 'features/controlLayers/store/controlAdaptersReducers';
import type { ControlModeV2, FilterConfig } from 'features/controlLayers/store/types';
import type { CAImageDropData } from 'features/dnd/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useToggle } from 'react-use';
import type {
CAImagePostUploadAction,
ControlNetModelConfig,
ImageDTO,
T2IAdapterModelConfig,
} from 'services/api/types';
export const ControlAdapterSettings = memo(() => {
const { id } = useEntityIdentifierContext();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [isExpanded, toggleIsExpanded] = useToggle(false);
const controlAdapter = useAppSelector((s) => selectCAOrThrow(s.canvasV2, id));
const onChangeBeginEndStepPct = useCallback(
(beginEndStepPct: [number, number]) => {
dispatch(caBeginEndStepPctChanged({ id, beginEndStepPct }));
},
[dispatch, id]
);
const onChangeControlMode = useCallback(
(controlMode: ControlModeV2) => {
dispatch(caControlModeChanged({ id, controlMode }));
},
[dispatch, id]
);
const onChangeWeight = useCallback(
(weight: number) => {
dispatch(caWeightChanged({ id, weight }));
},
[dispatch, id]
);
const onChangeProcessorConfig = useCallback(
(processorConfig: FilterConfig | null) => {
dispatch(caProcessorConfigChanged({ id, processorConfig }));
},
[dispatch, id]
);
const onChangeModel = useCallback(
(modelConfig: ControlNetModelConfig | T2IAdapterModelConfig) => {
dispatch(caModelChanged({ id, modelConfig }));
},
[dispatch, id]
);
const onChangeImage = useCallback(
(imageDTO: ImageDTO | null) => {
dispatch(caImageChanged({ id, imageDTO }));
},
[dispatch, id]
);
const onErrorLoadingImage = useCallback(() => {
dispatch(caImageChanged({ id, imageDTO: null }));
}, [dispatch, id]);
const onErrorLoadingProcessedImage = useCallback(() => {
dispatch(caProcessedImageChanged({ id, imageDTO: null }));
}, [dispatch, id]);
const droppableData = useMemo<CAImageDropData>(() => ({ actionType: 'SET_CA_IMAGE', context: { id }, id }), [id]);
const postUploadAction = useMemo<CAImagePostUploadAction>(() => ({ id, type: 'SET_CA_IMAGE' }), [id]);
return (
<CanvasEntitySettings>
<Flex flexDir="column" gap={3} position="relative" w="full">
<Flex gap={3} alignItems="center" w="full">
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
<ControlAdapterModel modelKey={controlAdapter.model?.key ?? null} onChange={onChangeModel} />
</Box>
<IconButton
size="sm"
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
onClick={toggleIsExpanded}
variant="ghost"
icon={
<Icon
boxSize={4}
as={PiCaretUpBold}
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
/>
}
/>
</Flex>
<Flex gap={3} w="full">
<Flex flexDir="column" gap={3} w="full" h="full">
{controlAdapter.adapterType === 'controlnet' && (
<ControlAdapterControlModeSelect controlMode={controlAdapter.controlMode} onChange={onChangeControlMode} />
)}
<Weight weight={controlAdapter.weight} onChange={onChangeWeight} />
<BeginEndStepPct beginEndStepPct={controlAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
<ControlAdapterImagePreview
controlAdapter={controlAdapter}
onChangeImage={onChangeImage}
droppableData={droppableData}
postUploadAction={postUploadAction}
onErrorLoadingImage={onErrorLoadingImage}
onErrorLoadingProcessedImage={onErrorLoadingProcessedImage}
/>
</Flex>
</Flex>
{isExpanded && (
<>
<Divider />
<Flex flexDir="column" gap={3} w="full">
<ControlAdapterProcessorTypeSelect config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
<ControlAdapterProcessorConfig config={controlAdapter.processorConfig} onChange={onChangeProcessorConfig} />
</Flex>
</>
)}
</Flex>
</CanvasEntitySettings>
);
});
ControlAdapterSettings.displayName = 'ControlAdapterSettings';

View File

@ -1,68 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { CannyProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<CannyProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['canny_image_processor'].buildDefaults();
export const CannyProcessor = ({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleLowThresholdChanged = useCallback(
(v: number) => {
onChange({ ...config, low_threshold: v });
},
[onChange, config]
);
const handleHighThresholdChanged = useCallback(
(v: number) => {
onChange({ ...config, high_threshold: v });
},
[onChange, config]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.lowThreshold')}</FormLabel>
<CompositeSlider
value={config.low_threshold}
onChange={handleLowThresholdChanged}
defaultValue={DEFAULTS.low_threshold}
min={0}
max={255}
/>
<CompositeNumberInput
value={config.low_threshold}
onChange={handleLowThresholdChanged}
defaultValue={DEFAULTS.low_threshold}
min={0}
max={255}
/>
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.highThreshold')}</FormLabel>
<CompositeSlider
value={config.high_threshold}
onChange={handleHighThresholdChanged}
defaultValue={DEFAULTS.high_threshold}
min={0}
max={255}
/>
<CompositeNumberInput
value={config.high_threshold}
onChange={handleHighThresholdChanged}
defaultValue={DEFAULTS.high_threshold}
min={0}
max={255}
/>
</FormControl>
</ProcessorWrapper>
);
};
CannyProcessor.displayName = 'CannyProcessor';

View File

@ -1,48 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { ColorMapProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ColorMapProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['color_map_image_processor'].buildDefaults();
export const ColorMapProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleColorMapTileSizeChanged = useCallback(
(v: number) => {
onChange({ ...config, color_map_tile_size: v });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.colorMapTileSize')}</FormLabel>
<CompositeSlider
value={config.color_map_tile_size}
defaultValue={DEFAULTS.color_map_tile_size}
onChange={handleColorMapTileSizeChanged}
min={1}
max={256}
step={1}
marks
/>
<CompositeNumberInput
value={config.color_map_tile_size}
defaultValue={DEFAULTS.color_map_tile_size}
onChange={handleColorMapTileSizeChanged}
min={1}
max={4096}
step={1}
/>
</FormControl>
</ProcessorWrapper>
);
});
ColorMapProcessor.displayName = 'ColorMapProcessor';

View File

@ -1,79 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { ContentShuffleProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<ContentShuffleProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['content_shuffle_image_processor'].buildDefaults();
export const ContentShuffleProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleWChanged = useCallback(
(v: number) => {
onChange({ ...config, w: v });
},
[config, onChange]
);
const handleHChanged = useCallback(
(v: number) => {
onChange({ ...config, h: v });
},
[config, onChange]
);
const handleFChanged = useCallback(
(v: number) => {
onChange({ ...config, f: v });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.w')}</FormLabel>
<CompositeSlider
value={config.w}
defaultValue={DEFAULTS.w}
onChange={handleWChanged}
min={0}
max={4096}
marks
/>
<CompositeNumberInput value={config.w} defaultValue={DEFAULTS.w} onChange={handleWChanged} min={0} max={4096} />
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.h')}</FormLabel>
<CompositeSlider
value={config.h}
defaultValue={DEFAULTS.h}
onChange={handleHChanged}
min={0}
max={4096}
marks
/>
<CompositeNumberInput value={config.h} defaultValue={DEFAULTS.h} onChange={handleHChanged} min={0} max={4096} />
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.f')}</FormLabel>
<CompositeSlider
value={config.f}
defaultValue={DEFAULTS.f}
onChange={handleFChanged}
min={0}
max={4096}
marks
/>
<CompositeNumberInput value={config.f} defaultValue={DEFAULTS.f} onChange={handleFChanged} min={0} max={4096} />
</FormControl>
</ProcessorWrapper>
);
});
ContentShuffleProcessor.displayName = 'ContentShuffleProcessor';

View File

@ -1,62 +0,0 @@
import { Flex, FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { DWOpenposeProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DWOpenposeProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['dw_openpose_image_processor'].buildDefaults();
export const DWOpenposeProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleDrawBodyChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, draw_body: e.target.checked });
},
[config, onChange]
);
const handleDrawFaceChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, draw_face: e.target.checked });
},
[config, onChange]
);
const handleDrawHandsChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, draw_hands: e.target.checked });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<Flex sx={{ flexDir: 'row', gap: 6 }}>
<FormControl w="max-content">
<FormLabel m={0}>{t('controlnet.body')}</FormLabel>
<Switch defaultChecked={DEFAULTS.draw_body} isChecked={config.draw_body} onChange={handleDrawBodyChanged} />
</FormControl>
<FormControl w="max-content">
<FormLabel m={0}>{t('controlnet.face')}</FormLabel>
<Switch defaultChecked={DEFAULTS.draw_face} isChecked={config.draw_face} onChange={handleDrawFaceChanged} />
</FormControl>
<FormControl w="max-content">
<FormLabel m={0}>{t('controlnet.hands')}</FormLabel>
<Switch
defaultChecked={DEFAULTS.draw_hands}
isChecked={config.draw_hands}
onChange={handleDrawHandsChanged}
/>
</FormControl>
</Flex>
</ProcessorWrapper>
);
});
DWOpenposeProcessor.displayName = 'DWOpenposeProcessor';

View File

@ -1,47 +0,0 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { DepthAnythingModelSize, DepthAnythingProcessorConfig } from 'features/controlLayers/store/types';
import { isDepthAnythingModelSize } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<DepthAnythingProcessorConfig>;
export const DepthAnythingProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleModelSizeChange = useCallback<ComboboxOnChange>(
(v) => {
if (!isDepthAnythingModelSize(v?.value)) {
return;
}
onChange({ ...config, model_size: v.value });
},
[config, onChange]
);
const options: { label: string; value: DepthAnythingModelSize }[] = useMemo(
() => [
{ label: t('controlnet.depthAnythingSmallV2'), value: 'small_v2' },
{ label: t('controlnet.small'), value: 'small' },
{ label: t('controlnet.base'), value: 'base' },
{ label: t('controlnet.large'), value: 'large' },
],
[t]
);
const value = useMemo(() => options.filter((o) => o.value === config.model_size)[0], [options, config.model_size]);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.modelSize')}</FormLabel>
<Combobox value={value} options={options} onChange={handleModelSizeChange} isSearchable={false} />
</FormControl>
</ProcessorWrapper>
);
});
DepthAnythingProcessor.displayName = 'DepthAnythingProcessor';

View File

@ -1,32 +0,0 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { HedProcessorConfig } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<HedProcessorConfig>;
export const HedProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleScribbleChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, scribble: e.target.checked });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.scribble')}</FormLabel>
<Switch isChecked={config.scribble} onChange={handleScribbleChanged} />
</FormControl>
</ProcessorWrapper>
);
});
HedProcessor.displayName = 'HedProcessor';

View File

@ -1,32 +0,0 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { LineartProcessorConfig } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<LineartProcessorConfig>;
export const LineartProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleCoarseChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, coarse: e.target.checked });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.coarse')}</FormLabel>
<Switch isChecked={config.coarse} onChange={handleCoarseChanged} />
</FormControl>
</ProcessorWrapper>
);
});
LineartProcessor.displayName = 'LineartProcessor';

View File

@ -1,74 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { MediapipeFaceProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MediapipeFaceProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['mediapipe_face_processor'].buildDefaults();
export const MediapipeFaceProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleMaxFacesChanged = useCallback(
(v: number) => {
onChange({ ...config, max_faces: v });
},
[config, onChange]
);
const handleMinConfidenceChanged = useCallback(
(v: number) => {
onChange({ ...config, min_confidence: v });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.maxFaces')}</FormLabel>
<CompositeSlider
value={config.max_faces}
onChange={handleMaxFacesChanged}
defaultValue={DEFAULTS.max_faces}
min={1}
max={20}
marks
/>
<CompositeNumberInput
value={config.max_faces}
onChange={handleMaxFacesChanged}
defaultValue={DEFAULTS.max_faces}
min={1}
max={20}
/>
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.minConfidence')}</FormLabel>
<CompositeSlider
value={config.min_confidence}
onChange={handleMinConfidenceChanged}
defaultValue={DEFAULTS.min_confidence}
min={0}
max={1}
step={0.01}
marks
/>
<CompositeNumberInput
value={config.min_confidence}
onChange={handleMinConfidenceChanged}
defaultValue={DEFAULTS.min_confidence}
min={0}
max={1}
step={0.01}
/>
</FormControl>
</ProcessorWrapper>
);
});
MediapipeFaceProcessor.displayName = 'MediapipeFaceProcessor';

View File

@ -1,76 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { MidasDepthProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MidasDepthProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['midas_depth_image_processor'].buildDefaults();
export const MidasDepthProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleAMultChanged = useCallback(
(v: number) => {
onChange({ ...config, a_mult: v });
},
[config, onChange]
);
const handleBgThChanged = useCallback(
(v: number) => {
onChange({ ...config, bg_th: v });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.amult')}</FormLabel>
<CompositeSlider
value={config.a_mult}
onChange={handleAMultChanged}
defaultValue={DEFAULTS.a_mult}
min={0}
max={20}
step={0.01}
marks
/>
<CompositeNumberInput
value={config.a_mult}
onChange={handleAMultChanged}
defaultValue={DEFAULTS.a_mult}
min={0}
max={20}
step={0.01}
/>
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.bgth')}</FormLabel>
<CompositeSlider
value={config.bg_th}
onChange={handleBgThChanged}
defaultValue={DEFAULTS.bg_th}
min={0}
max={20}
step={0.01}
marks
/>
<CompositeNumberInput
value={config.bg_th}
onChange={handleBgThChanged}
defaultValue={DEFAULTS.bg_th}
min={0}
max={20}
step={0.01}
/>
</FormControl>
</ProcessorWrapper>
);
});
MidasDepthProcessor.displayName = 'MidasDepthProcessor';

View File

@ -1,76 +0,0 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { MlsdProcessorConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<MlsdProcessorConfig>;
const DEFAULTS = IMAGE_FILTERS['mlsd_image_processor'].buildDefaults();
export const MlsdImageProcessor = memo(({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleThrDChanged = useCallback(
(v: number) => {
onChange({ ...config, thr_d: v });
},
[config, onChange]
);
const handleThrVChanged = useCallback(
(v: number) => {
onChange({ ...config, thr_v: v });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.w')} </FormLabel>
<CompositeSlider
value={config.thr_d}
onChange={handleThrDChanged}
defaultValue={DEFAULTS.thr_d}
min={0}
max={1}
step={0.01}
marks
/>
<CompositeNumberInput
value={config.thr_d}
onChange={handleThrDChanged}
defaultValue={DEFAULTS.thr_d}
min={0}
max={1}
step={0.01}
/>
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.h')} </FormLabel>
<CompositeSlider
value={config.thr_v}
onChange={handleThrVChanged}
defaultValue={DEFAULTS.thr_v}
min={0}
max={1}
step={0.01}
marks
/>
<CompositeNumberInput
value={config.thr_v}
onChange={handleThrVChanged}
defaultValue={DEFAULTS.thr_v}
min={0}
max={1}
step={0.01}
/>
</FormControl>
</ProcessorWrapper>
);
});
MlsdImageProcessor.displayName = 'MlsdImageProcessor';

View File

@ -1,43 +0,0 @@
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
import type { ProcessorComponentProps } from 'features/controlLayers/components/ControlAdapter/processors/types';
import type { PidiProcessorConfig } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ProcessorWrapper from './ProcessorWrapper';
type Props = ProcessorComponentProps<PidiProcessorConfig>;
export const PidiProcessor = ({ onChange, config }: Props) => {
const { t } = useTranslation();
const handleScribbleChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, scribble: e.target.checked });
},
[config, onChange]
);
const handleSafeChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, safe: e.target.checked });
},
[config, onChange]
);
return (
<ProcessorWrapper>
<FormControl>
<FormLabel m={0}>{t('controlnet.scribble')}</FormLabel>
<Switch isChecked={config.scribble} onChange={handleScribbleChanged} />
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlnet.safe')}</FormLabel>
<Switch isChecked={config.safe} onChange={handleSafeChanged} />
</FormControl>
</ProcessorWrapper>
);
};
PidiProcessor.displayName = 'PidiProcessor';

View File

@ -1,15 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
type Props = PropsWithChildren;
const ProcessorWrapper = (props: Props) => {
return (
<Flex flexDir="column" gap={3}>
{props.children}
</Flex>
);
};
export default memo(ProcessorWrapper);

View File

@ -1,6 +0,0 @@
import type { FilterConfig } from 'features/controlLayers/store/types';
export type ProcessorComponentProps<T extends FilterConfig> = {
onChange: (config: T) => void;
config: T;
};

View File

@ -1,4 +1,5 @@
import {
Button,
Checkbox,
Flex,
FormControl,
@ -11,7 +12,11 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { MaskOpacity } from 'features/controlLayers/components/MaskOpacity';
import { clipToBboxChanged, invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
import {
clipToBboxChanged,
invertScrollChanged,
rasterizationCachesInvalidated,
} from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -30,6 +35,9 @@ const ControlLayersSettingsPopover = () => {
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
[dispatch]
);
const invalidateRasterizationCaches = useCallback(() => {
dispatch(rasterizationCachesInvalidated());
}, [dispatch]);
return (
<Popover isLazy>
<PopoverTrigger>
@ -47,6 +55,9 @@ const ControlLayersSettingsPopover = () => {
<FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel>
<Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} />
</FormControl>
<Button onClick={invalidateRasterizationCaches} size="sm">
Invalidate Rasterization Caches
</Button>
</Flex>
</PopoverBody>
</PopoverContent>

View File

@ -11,7 +11,7 @@ export const DeleteAllLayersButton = memo(() => {
const entityCount = useAppSelector((s) => {
return (
s.canvasV2.regions.entities.length +
s.canvasV2.controlAdapters.entities.length +
// s.canvasV2.controlAdapters.entities.length +
s.canvasV2.ipAdapters.entities.length +
s.canvasV2.layers.entities.length
);

View File

@ -10,8 +10,6 @@ import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityI
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react';
import { LayerOpacity } from './LayerOpacity';
type Props = {
id: string;
};
@ -26,7 +24,6 @@ export const Layer = memo(({ id }: Props) => {
<CanvasEntityEnabledToggle />
<CanvasEntityTitle />
<Spacer />
<LayerOpacity />
<LayerActionsMenu />
<CanvasEntityDeleteButton />
</CanvasEntityHeader>

View File

@ -1,82 +0,0 @@
import {
CompositeNumberInput,
CompositeSlider,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { stopPropagation } from 'common/util/stopPropagation';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { layerOpacityChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectLayerOrThrow } from 'features/controlLayers/store/layersReducers';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDropHalfFill } from 'react-icons/pi';
const marks = [0, 25, 50, 75, 100];
const formatPct = (v: number | string) => `${v} %`;
export const LayerOpacity = memo(() => {
const entityIdentifier = useEntityIdentifierContext();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => Math.round(selectLayerOrThrow(s.canvasV2, entityIdentifier.id).opacity * 100));
const onChangeOpacity = useCallback(
(v: number) => {
dispatch(layerOpacityChanged({ id: entityIdentifier.id, opacity: v / 100 }));
},
[dispatch, entityIdentifier.id]
);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton
aria-label={t('controlLayers.opacity')}
size="sm"
icon={<PiDropHalfFill size={16} />}
variant="ghost"
onDoubleClick={stopPropagation}
/>
</PopoverTrigger>
<PopoverContent onDoubleClick={stopPropagation}>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl orientation="horizontal">
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
<CompositeSlider
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
marks={marks}
w={48}
/>
<CompositeNumberInput
min={0}
max={100}
step={1}
value={opacity}
defaultValue={100}
onChange={onChangeOpacity}
w={24}
format={formatPct}
/>
</FormControl>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
LayerOpacity.displayName = 'LayerOpacity';

View File

@ -40,11 +40,6 @@ const getIndexAndCount = (
index: canvasV2.layers.entities.findIndex((entity) => entity.id === id),
count: canvasV2.layers.entities.length,
};
} else if (type === 'control_adapter') {
return {
index: canvasV2.controlAdapters.entities.findIndex((entity) => entity.id === id),
count: canvasV2.controlAdapters.entities.length,
};
} else if (type === 'regional_guidance') {
return {
index: canvasV2.regions.entities.findIndex((entity) => entity.id === id),

View File

@ -1,6 +1,6 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import { caAdded, ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice';
import { ipaAdded, rgIPAdapterAdded } from 'features/controlLayers/store/canvasV2Slice';
import {
IMAGE_FILTERS,
initialControlNetV2,
@ -37,7 +37,7 @@ export const useAddCALayer = () => {
const initialConfig = deepClone(model.type === 'controlnet' ? initialControlNetV2 : initialT2IAdapterV2);
const config = { ...initialConfig, model: zModelIdentifierField.parse(model), processorConfig };
dispatch(caAdded({ config }));
// dispatch(caAdded({ config }));
}, [dispatch, model, baseModel]);
return [addCALayer, isDisabled] as const;

View File

@ -100,7 +100,12 @@ export class CanvasFilter {
this.manager.stateApi.rasterizeEntity({
entityIdentifier: this.parent.getEntityIdentifier(),
imageObject: this.imageState,
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
rect: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: this.imageState.image.height,
height: this.imageState.image.width,
},
});
this.parent.renderer.showObjects();
this.manager.stateApi.$filteringEntity.set(null);

View File

@ -9,20 +9,27 @@ import {
konvaNodeToBlob,
konvaNodeToImageData,
nanoid,
previewBlob,
} from 'features/controlLayers/konva/util';
import type { Extents, ExtentsResult, GetBboxTask, WorkerLogMessage } from 'features/controlLayers/konva/worker';
import type { CanvasV2State, Coordinate, Dimensions, GenerationMode, Rect } from 'features/controlLayers/store/types';
import type {
CanvasV2State,
Coordinate,
Dimensions,
GenerationMode,
ImageCache,
Rect,
} from 'features/controlLayers/store/types';
import { isValidLayerWithoutControlAdapter } from 'features/nodes/util/graph/generation/addLayers';
import type Konva from 'konva';
import { clamp } from 'lodash-es';
import { clamp, isEqual } from 'lodash-es';
import { atom } from 'nanostores';
import type { Logger } from 'roarr';
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
import { CanvasBackground } from './CanvasBackground';
import { CanvasControlAdapter } from './CanvasControlAdapter';
import type { CanvasControlAdapter } from './CanvasControlAdapter';
import { CanvasLayerAdapter } from './CanvasLayerAdapter';
import { CanvasMaskAdapter } from './CanvasMaskAdapter';
import { CanvasPreview } from './CanvasPreview';
@ -144,40 +151,15 @@ export class CanvasManager {
this._worker.postMessage(task, [data.buffer]);
}
async renderControlAdapters() {
const { entities } = this.stateApi.getControlAdaptersState();
for (const canvasControlAdapter of this.controlAdapters.values()) {
if (!entities.find((ca) => ca.id === canvasControlAdapter.id)) {
canvasControlAdapter.destroy();
this.controlAdapters.delete(canvasControlAdapter.id);
}
}
for (const entity of entities) {
let adapter = this.controlAdapters.get(entity.id);
if (!adapter) {
adapter = new CanvasControlAdapter(entity, this);
this.controlAdapters.set(adapter.id, adapter);
this.stage.add(adapter.konva.layer);
}
await adapter.render(entity);
}
}
arrangeEntities() {
const { getLayersState, getControlAdaptersState, getRegionsState } = this.stateApi;
const { getLayersState, getRegionsState } = this.stateApi;
const layers = getLayersState().entities;
const controlAdapters = getControlAdaptersState().entities;
const regions = getRegionsState().entities;
let zIndex = 0;
this.background.konva.layer.zIndex(++zIndex);
for (const layer of layers) {
this.layers.get(layer.id)?.konva.layer.zIndex(++zIndex);
}
for (const ca of controlAdapters) {
this.controlAdapters.get(ca.id)?.konva.layer.zIndex(++zIndex);
}
for (const rg of regions) {
this.regions.get(rg.id)?.konva.layer.zIndex(++zIndex);
}
@ -358,16 +340,6 @@ export class CanvasManager {
});
}
if (
this._isFirstRender ||
state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
state.tool.selected !== this._prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
) {
this.log.debug('Rendering control adapters');
await this.renderControlAdapters();
}
this.stateApi.$toolState.set(state.tool);
this.stateApi.$selectedEntityIdentifier.set(state.selectedEntityIdentifier);
this.stateApi.$selectedEntity.set(this.stateApi.getSelectedEntity());
@ -382,12 +354,7 @@ export class CanvasManager {
await this.preview.bbox.render();
}
if (
this._isFirstRender ||
state.layers !== this._prevState.layers ||
state.controlAdapters !== this._prevState.controlAdapters ||
state.regions !== this._prevState.regions
) {
if (this._isFirstRender || state.layers !== this._prevState.layers || state.regions !== this._prevState.regions) {
// this.log.debug('Updating entity bboxes');
// debouncedUpdateBboxes(stage, canvasV2.layers, canvasV2.controlAdapters, canvasV2.regions, onBboxChanged);
}
@ -400,7 +367,6 @@ export class CanvasManager {
if (
this._isFirstRender ||
state.layers.entities !== this._prevState.layers.entities ||
state.controlAdapters.entities !== this._prevState.controlAdapters.entities ||
state.regions.entities !== this._prevState.regions.entities ||
state.inpaintMask !== this._prevState.inpaintMask ||
state.selectedEntityIdentifier?.id !== this._prevState.selectedEntityIdentifier?.id
@ -569,45 +535,43 @@ export class CanvasManager {
return konvaNodeToImageData(this.getCompositeLayerStageClone(), rect);
};
getCompositeLayerImageDTO = async (rect?: Rect): Promise<ImageDTO> => {
getCompositeRasterizedImageCache = (rect: Rect): ImageCache | null => {
const layerState = this.stateApi.getLayersState();
const imageCache = layerState.compositeRasterizationCache.find((cache) => isEqual(cache.rect, rect));
return imageCache ?? null;
};
getCompositeLayerImageDTO = async (rect: Rect): Promise<ImageDTO> => {
let imageDTO: ImageDTO | null = null;
const compositeRasterizedImageCache = this.getCompositeRasterizedImageCache(rect);
if (compositeRasterizedImageCache) {
imageDTO = await getImageDTO(compositeRasterizedImageCache.imageName);
if (imageDTO) {
this.log.trace({ rect, compositeRasterizedImageCache, imageDTO }, 'Using cached composite rasterized image');
return imageDTO;
}
}
this.log.trace({ rect }, 'Rasterizing composite layer');
const blob = await this.getCompositeLayerBlob(rect);
const imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true);
this.stateApi.setLayerImageCache(imageDTO);
if (this._isDebugging) {
previewBlob(blob, 'Rasterized entity');
}
imageDTO = await uploadImage(blob, 'composite-layer.png', 'general', true);
this.stateApi.compositeLayerRasterized({ imageName: imageDTO.image_name, rect });
return imageDTO;
};
getInpaintMaskBlob = (rect?: Rect): Promise<Blob> => {
return this.inpaintMask.renderer.getBlob({ rect });
return this.inpaintMask.renderer.getBlob(rect);
};
getInpaintMaskImageData = (rect?: Rect): ImageData => {
return this.inpaintMask.renderer.getImageData({ rect });
};
getInpaintMaskImageDTO = async (rect?: Rect): Promise<ImageDTO> => {
const blob = await this.inpaintMask.renderer.getBlob({ rect });
const imageDTO = await uploadImage(blob, 'inpaint-mask.png', 'mask', true);
this.stateApi.setInpaintMaskImageCache(imageDTO);
return imageDTO;
};
getRegionMaskImageDTO = async (id: string, rect?: Rect): Promise<ImageDTO> => {
const region = this.stateApi.getEntity({ id, type: 'regional_guidance' });
assert(region?.type === 'regional_guidance');
if (region.state.imageCache) {
const imageDTO = await getImageDTO(region.state.imageCache);
if (imageDTO) {
return imageDTO;
}
}
return region.adapter.renderer.getImageDTO({
rect,
category: 'other',
is_intermediate: true,
onUploaded: (imageDTO) => {
this.stateApi.setRegionMaskImageCache(region.state.id, imageDTO);
},
});
return this.inpaintMask.renderer.getImageData(rect);
};
getGenerationMode(): GenerationMode {

View File

@ -14,14 +14,16 @@ import type {
CanvasEraserLineState,
CanvasImageState,
CanvasRectState,
ImageCache,
Rect,
RgbColor,
} from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import Konva from 'konva';
import { isEqual } from 'lodash-es';
import type { Logger } from 'roarr';
import { getImageDTO, uploadImage } from 'services/api/endpoints/images';
import type { ImageCategory, ImageDTO } from 'services/api/types';
import type { ImageDTO } from 'services/api/types';
import { assert } from 'tsafe';
/**
@ -62,12 +64,6 @@ export class CanvasObjectRenderer {
*/
renderers: Map<string, AnyObjectRenderer> = new Map();
/**
* A cache of the rasterized image data URL. If the cache is null, the parent has not been rasterized since its last
* change.
*/
rasterizedImageCache: string | null = null;
/**
* A object containing singleton Konva nodes.
*/
@ -168,19 +164,6 @@ export class CanvasObjectRenderer {
didRender = (await this.renderObject(this.buffer)) || didRender;
}
if (didRender && this.rasterizedImageCache) {
const hasOneObject = this.renderers.size === 1;
const firstObject = Array.from(this.renderers.values())[0];
if (
hasOneObject &&
firstObject &&
firstObject.state.type === 'image' &&
firstObject.state.image.image_name !== this.rasterizedImageCache
) {
this.rasterizedImageCache = null;
}
}
return didRender;
};
@ -376,29 +359,37 @@ export class CanvasObjectRenderer {
return this.renderers.size > 0 || this.buffer !== null;
};
getRasterizedImageCache = (rect: Rect): ImageCache | null => {
const imageCache = this.parent.state.rasterizationCache.find((cache) => isEqual(cache.rect, rect));
return imageCache ?? null;
};
/**
* Rasterizes the parent entity. If the entity has a rasterization cache, the cached image is returned after
* validating that it exists on the server.
* Rasterizes the parent entity. If the entity has a rasterization cache for the given rect, the cached image is
* returned. Otherwise, the entity is rasterized and the image is uploaded to the server.
*
* The rasterization cache is reset when the entity's objects change. The buffer object is not considered part of the
* entity's objects for this purpose.
* The rasterization cache is reset when the entity's state changes. The buffer object is not considered part of the
* entity state for this purpose as it is a temporary object.
*
* @param rect The rect to rasterize. If omitted, the entity's full rect will be used.
* @returns A promise that resolves to the rasterized image DTO.
*/
rasterize = async (): Promise<ImageDTO> => {
this.log.debug('Rasterizing entity');
rasterize = async (rect?: Rect): Promise<ImageDTO> => {
rect = rect ?? this.parent.transformer.getRelativeRect();
let imageDTO: ImageDTO | null = null;
if (this.rasterizedImageCache) {
imageDTO = await getImageDTO(this.rasterizedImageCache);
const rasterizedImageCache = this.getRasterizedImageCache(rect);
if (rasterizedImageCache) {
imageDTO = await getImageDTO(rasterizedImageCache.imageName);
if (imageDTO) {
this.log.trace({ rect, rasterizedImageCache, imageDTO }, 'Using cached rasterized image');
return imageDTO;
}
}
if (imageDTO) {
return imageDTO;
}
this.log.trace({ rect }, 'Rasterizing entity');
const rect = this.parent.transformer.getRelativeRect();
const blob = await this.getBlob({ rect });
const blob = await this.getBlob(rect);
if (this.manager._isDebugging) {
previewBlob(blob, 'Rasterized entity');
}
@ -408,41 +399,20 @@ export class CanvasObjectRenderer {
this.manager.stateApi.rasterizeEntity({
entityIdentifier: this.parent.getEntityIdentifier(),
imageObject,
position: { x: Math.round(rect.x), y: Math.round(rect.y) },
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: imageDTO.width, height: imageDTO.height },
});
this.rasterizedImageCache = imageDTO.image_name;
return imageDTO;
};
getBlob = ({ rect }: { rect?: Rect }): Promise<Blob> => {
getBlob = (rect?: Rect): Promise<Blob> => {
return konvaNodeToBlob(this.konva.objectGroup.clone(), rect);
};
getImageData = ({ rect }: { rect?: Rect }): ImageData => {
getImageData = (rect?: Rect): ImageData => {
return konvaNodeToImageData(this.konva.objectGroup.clone(), rect);
};
getImageDTO = async ({
rect,
category,
is_intermediate,
onUploaded,
}: {
rect?: Rect;
category: ImageCategory;
is_intermediate: boolean;
onUploaded?: (imageDTO: ImageDTO) => void;
}): Promise<ImageDTO> => {
const blob = await this.getBlob({ rect });
const imageDTO = await uploadImage(blob, `${this.id}.png`, category, is_intermediate);
if (onUploaded) {
onUploaded(imageDTO);
}
return imageDTO;
};
/**
* Destroys this renderer and all of its object renderers.
*/

View File

@ -26,9 +26,7 @@ import {
entityReset,
entitySelected,
eraserWidthChanged,
imImageCacheChanged,
layerImageCacheChanged,
rgImageCacheChanged,
layerCompositeRasterized,
toolBufferChanged,
toolChanged,
} from 'features/controlLayers/store/canvasV2Slice';
@ -51,7 +49,6 @@ import type {
import { RGBA_RED } from 'features/controlLayers/store/types';
import type { WritableAtom } from 'nanostores';
import { atom } from 'nanostores';
import type { ImageDTO } from 'services/api/types';
type EntityStateAndAdapter =
| {
@ -118,6 +115,10 @@ export class CanvasStateApi {
log.trace(arg, 'Rasterizing entity');
this._store.dispatch(entityRasterized(arg));
};
compositeLayerRasterized = (arg: { imageName: string; rect: Rect }) => {
log.trace(arg, 'Composite layer rasterized');
this._store.dispatch(layerCompositeRasterized(arg));
};
setSelectedEntity = (arg: EntityIdentifierPayload) => {
log.trace({ arg }, 'Setting selected entity');
this._store.dispatch(entitySelected(arg));
@ -134,18 +135,6 @@ export class CanvasStateApi {
log.trace({ width }, 'Setting eraser width');
this._store.dispatch(eraserWidthChanged(width));
};
setRegionMaskImageCache = (id: string, imageDTO: ImageDTO) => {
log.trace({ id, imageDTO }, 'Setting region mask image cache');
this._store.dispatch(rgImageCacheChanged({ id, imageDTO }));
};
setInpaintMaskImageCache = (imageDTO: ImageDTO) => {
log.trace({ imageDTO }, 'Setting inpaint mask image cache');
this._store.dispatch(imImageCacheChanged({ imageDTO }));
};
setLayerImageCache = (imageDTO: ImageDTO) => {
log.trace({ imageDTO }, 'Setting layer image cache');
this._store.dispatch(layerImageCacheChanged({ imageDTO }));
};
setTool = (tool: Tool) => {
log.trace({ tool }, 'Setting tool');
this._store.dispatch(toolChanged(tool));
@ -171,9 +160,6 @@ export class CanvasStateApi {
getLayersState = () => {
return this.getState().layers;
};
getControlAdaptersState = () => {
return this.getState().controlAdapters;
};
getInpaintMaskState = () => {
return this.getState().inpaintMask;
};
@ -202,9 +188,6 @@ export class CanvasStateApi {
if (identifier.type === 'layer') {
entityState = state.layers.entities.find((i) => i.id === identifier.id) ?? null;
entityAdapter = this.manager.layers.get(identifier.id) ?? null;
} else if (identifier.type === 'control_adapter') {
entityState = state.controlAdapters.entities.find((i) => i.id === identifier.id) ?? null;
entityAdapter = this.manager.controlAdapters.get(identifier.id) ?? null;
} else if (identifier.type === 'regional_guidance') {
entityState = state.regions.entities.find((i) => i.id === identifier.id) ?? null;
entityAdapter = this.manager.regions.get(identifier.id) ?? null;

View File

@ -5,7 +5,6 @@ import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/uti
import { deepClone } from 'common/util/deepClone';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers';
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
import { layersReducers } from 'features/controlLayers/store/layersReducers';
@ -18,13 +17,16 @@ import { toolReducers } from 'features/controlLayers/store/toolReducers';
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import { pick } from 'lodash-es';
import { isEqual, pick } from 'lodash-es';
import { atom } from 'nanostores';
import type { InvocationDenoiseProgressEvent } from 'services/events/types';
import { assert } from 'tsafe';
import type {
CanvasEntityIdentifier,
CanvasInpaintMaskState,
CanvasLayerState,
CanvasRegionalGuidanceState,
CanvasV2State,
Coordinate,
EntityBrushLineAddedPayload,
@ -41,8 +43,7 @@ import { IMAGE_FILTERS, RGBA_RED } from './types';
const initialState: CanvasV2State = {
_version: 3,
selectedEntityIdentifier: null,
layers: { entities: [], imageCache: null },
controlAdapters: { entities: [] },
layers: { entities: [], compositeRasterizationCache: [] },
ipAdapters: { entities: [] },
regions: { entities: [] },
loras: [],
@ -50,7 +51,7 @@ const initialState: CanvasV2State = {
id: 'inpaint_mask',
type: 'inpaint_mask',
fill: RGBA_RED,
imageCache: null,
rasterizationCache: [],
isEnabled: true,
objects: [],
position: {
@ -144,8 +145,6 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde
switch (type) {
case 'layer':
return state.layers.entities.find((layer) => layer.id === id);
case 'control_adapter':
return state.controlAdapters.entities.find((ca) => ca.id === id);
case 'inpaint_mask':
return state.inpaintMask;
case 'regional_guidance':
@ -157,13 +156,37 @@ export function selectEntity(state: CanvasV2State, { id, type }: CanvasEntityIde
}
}
const invalidateCompositeRasterizationCache = (entity: CanvasLayerState, state: CanvasV2State) => {
if (entity.controlAdapter === null) {
state.layers.compositeRasterizationCache = [];
}
};
const invalidateRasterizationCaches = (
entity: CanvasLayerState | CanvasInpaintMaskState | CanvasRegionalGuidanceState,
state: CanvasV2State
) => {
// TODO(psyche): We can be more efficient and only invalidate caches when the entity's changes intersect with the
// cached rect.
// Reset the entity's rasterization cache
entity.rasterizationCache = [];
// When an individual layer has its cache reset, we must also reset the composite rasterization cache because the
// layer's image data will contribute to the composite layer's image data.
// If the layer is used as a control layer, it will not contribute to the composite layer, so we do not need to reset
// its cache.
if (entity.type === 'layer') {
invalidateCompositeRasterizationCache(entity, state);
}
};
export const canvasV2Slice = createSlice({
name: 'canvasV2',
initialState,
reducers: {
...layersReducers,
...ipAdaptersReducers,
...controlAdaptersReducers,
...regionsReducers,
...lorasReducers,
...paramsReducers,
@ -182,16 +205,11 @@ export const canvasV2Slice = createSlice({
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
} else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.isEnabled = true;
entity.objects = [];
entity.position = { x: 0, y: 0 };
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.isEnabled = true;
entity.objects = [];
entity.position = { x: 0, y: 0 };
entity.imageCache = null;
invalidateRasterizationCaches(entity, state);
} else {
assert(false, 'Not implemented');
}
@ -209,32 +227,28 @@ export const canvasV2Slice = createSlice({
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
}
if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.position = position;
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.position = position;
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
// When an entity is moved, we need to invalidate the rasterization caches.
invalidateRasterizationCaches(entity, state);
}
},
entityRasterized: (state, action: PayloadAction<EntityRasterizedPayload>) => {
const { entityIdentifier, imageObject, position } = action.payload;
const { entityIdentifier, imageObject, rect } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
}
if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects = [imageObject];
entity.position = position;
entity.imageCache = imageObject.image.image_name;
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects = [imageObject];
entity.position = position;
entity.imageCache = imageObject.image.image_name;
} else {
assert(false, 'Not implemented');
entity.position = { x: rect.x, y: rect.y };
// Remove the cache for the given rect. This should never happen, because we should never rasterize the same
// rect twice. Just in case, we remove the old cache.
entity.rasterizationCache = entity.rasterizationCache.filter((cache) => !isEqual(cache.rect, rect));
entity.rasterizationCache.push({ imageName: imageObject.image.image_name, rect });
}
},
entityBrushLineAdded: (state, action: PayloadAction<EntityBrushLineAddedPayload>) => {
@ -242,14 +256,12 @@ export const canvasV2Slice = createSlice({
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
}
if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(brushLine);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(brushLine);
entity.imageCache = null;
} else {
assert(false, 'Not implemented');
// When adding a brush line, we need to invalidate the rasterization caches.
invalidateRasterizationCaches(entity, state);
}
},
entityEraserLineAdded: (state, action: PayloadAction<EntityEraserLineAddedPayload>) => {
@ -257,12 +269,10 @@ export const canvasV2Slice = createSlice({
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
} else if (entity.type === 'layer') {
} else if (entity.type === 'layer' || entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(eraserLine);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(eraserLine);
entity.imageCache = null;
// When adding an eraser line, we need to invalidate the rasterization caches.
invalidateRasterizationCaches(entity, state);
} else {
assert(false, 'Not implemented');
}
@ -274,19 +284,21 @@ export const canvasV2Slice = createSlice({
return;
} else if (entity.type === 'layer') {
entity.objects.push(rect);
state.layers.imageCache = null;
} else if (entity.type === 'inpaint_mask' || entity.type === 'regional_guidance') {
entity.objects.push(rect);
entity.imageCache = null;
// When adding an eraser line, we need to invalidate the rasterization caches.
invalidateRasterizationCaches(entity, state);
} else {
assert(false, 'Not implemented');
}
},
entityDeleted: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (entity?.type === 'layer') {
// When a layer is deleted, we may need to invalidate the composite rasterization cache.
invalidateCompositeRasterizationCache(entity, state);
}
if (entityIdentifier.type === 'layer') {
state.layers.entities = state.layers.entities.filter((layer) => layer.id !== entityIdentifier.id);
state.layers.imageCache = null;
} else if (entityIdentifier.type === 'regional_guidance') {
state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id);
} else {
@ -301,11 +313,10 @@ export const canvasV2Slice = createSlice({
}
if (entity.type === 'layer') {
moveOneToEnd(state.layers.entities, entity);
state.layers.imageCache = null;
// When arranging an entity, we may need to invalidate the composite rasterization cache.
invalidateCompositeRasterizationCache(entity, state);
} else if (entity.type === 'regional_guidance') {
moveOneToEnd(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveOneToEnd(state.controlAdapters.entities, entity);
}
},
entityArrangedToFront: (state, action: PayloadAction<EntityIdentifierPayload>) => {
@ -316,11 +327,10 @@ export const canvasV2Slice = createSlice({
}
if (entity.type === 'layer') {
moveToEnd(state.layers.entities, entity);
state.layers.imageCache = null;
// When arranging an entity, we may need to invalidate the composite rasterization cache.
invalidateCompositeRasterizationCache(entity, state);
} else if (entity.type === 'regional_guidance') {
moveToEnd(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveToEnd(state.controlAdapters.entities, entity);
}
},
entityArrangedBackwardOne: (state, action: PayloadAction<EntityIdentifierPayload>) => {
@ -331,11 +341,10 @@ export const canvasV2Slice = createSlice({
}
if (entity.type === 'layer') {
moveOneToStart(state.layers.entities, entity);
state.layers.imageCache = null;
// When arranging an entity, we may need to invalidate the composite rasterization cache.
invalidateCompositeRasterizationCache(entity, state);
} else if (entity.type === 'regional_guidance') {
moveOneToStart(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveOneToStart(state.controlAdapters.entities, entity);
}
},
entityArrangedToBack: (state, action: PayloadAction<EntityIdentifierPayload>) => {
@ -346,19 +355,17 @@ export const canvasV2Slice = createSlice({
}
if (entity.type === 'layer') {
moveToStart(state.layers.entities, entity);
state.layers.imageCache = null;
// When arranging an entity, we may need to invalidate the composite rasterization cache.
invalidateCompositeRasterizationCache(entity, state);
} else if (entity.type === 'regional_guidance') {
moveToStart(state.regions.entities, entity);
} else if (entity.type === 'control_adapter') {
moveToStart(state.controlAdapters.entities, entity);
}
},
allEntitiesDeleted: (state) => {
state.regions.entities = [];
state.layers.entities = [];
state.layers.imageCache = null;
state.layers.compositeRasterizationCache = [];
state.ipAdapters.entities = [];
state.controlAdapters.entities = [];
},
filterSelected: (state, action: PayloadAction<{ type: FilterConfig['type'] }>) => {
state.filter.config = IMAGE_FILTERS[action.payload.type].buildDefaults();
@ -366,6 +373,23 @@ export const canvasV2Slice = createSlice({
filterConfigChanged: (state, action: PayloadAction<{ config: FilterConfig }>) => {
state.filter.config = action.payload.config;
},
rasterizationCachesInvalidated: (state) => {
// Invalidate the rasterization caches for all entities.
// Layers & composite layer
state.layers.compositeRasterizationCache = [];
for (const layer of state.layers.entities) {
layer.rasterizationCache = [];
}
// Regions
for (const region of state.regions.entities) {
region.rasterizationCache = [];
}
// Inpaint mask
state.inpaintMask.rasterizationCache = [];
},
canvasReset: (state) => {
state.bbox = deepClone(initialState.bbox);
const optimalDimension = getOptimalDimension(state.params.model);
@ -374,7 +398,6 @@ export const canvasV2Slice = createSlice({
const size = pick(state.bbox.rect, 'width', 'height');
state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension);
state.controlAdapters = deepClone(initialState.controlAdapters);
state.ipAdapters = deepClone(initialState.ipAdapters);
state.layers = deepClone(initialState.layers);
state.regions = deepClone(initialState.regions);
@ -397,6 +420,7 @@ export const {
allEntitiesDeleted,
clipToBboxChanged,
canvasReset,
rasterizationCachesInvalidated,
// All entities
entitySelected,
entityReset,
@ -424,14 +448,13 @@ export const {
// layers
layerAdded,
layerRecalled,
layerOpacityChanged,
layerAllDeleted,
layerImageCacheChanged,
layerUsedAsControlChanged,
layerControlAdapterModelChanged,
layerControlAdapterControlModeChanged,
layerControlAdapterWeightChanged,
layerControlAdapterBeginEndStepPctChanged,
layerCompositeRasterized,
// IP Adapters
ipaAdded,
ipaRecalled,
@ -444,20 +467,6 @@ export const {
ipaCLIPVisionModelChanged,
ipaWeightChanged,
ipaBeginEndStepPctChanged,
// Control Adapters
caAdded,
caAllDeleted,
caOpacityChanged,
caRecalled,
caImageChanged,
caProcessedImageChanged,
caModelChanged,
caControlModeChanged,
caProcessorConfigChanged,
caFilterChanged,
caProcessorPendingBatchIdChanged,
caWeightChanged,
caBeginEndStepPctChanged,
// Regions
rgAdded,
rgRecalled,
@ -465,7 +474,6 @@ export const {
rgPositivePromptChanged,
rgNegativePromptChanged,
rgFillChanged,
rgImageCacheChanged,
rgAutoNegativeChanged,
rgIPAdapterAdded,
rgIPAdapterDeleted,
@ -522,7 +530,6 @@ export const {
// Inpaint mask
imRecalled,
imFillChanged,
imImageCacheChanged,
// Staging
sessionStartedStaging,
sessionImageStaged,

View File

@ -1,197 +0,0 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { isEqual } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
import type {
CanvasControlAdapterState,
CanvasControlNetState,
CanvasT2IAdapterState,
CanvasV2State,
ControlModeV2,
ControlNetConfig,
Filter,
FilterConfig,
T2IAdapterConfig,
} from './types';
import { buildControlAdapterProcessorV2, imageDTOToImageObject } from './types';
export const selectCA = (state: CanvasV2State, id: string) => state.controlAdapters.entities.find((ca) => ca.id === id);
export const selectCAOrThrow = (state: CanvasV2State, id: string) => {
const ca = selectCA(state, id);
assert(ca, `Control Adapter with id ${id} not found`);
return ca;
};
export const controlAdaptersReducers = {
caAdded: {
reducer: (state, action: PayloadAction<{ id: string; config: ControlNetConfig | T2IAdapterConfig }>) => {
const { id, config } = action.payload;
state.controlAdapters.entities.push({
id,
type: 'control_adapter',
position: { x: 0, y: 0 },
bbox: null,
bboxNeedsUpdate: false,
isEnabled: true,
opacity: 1,
filters: ['LightnessToAlphaFilter'],
processorPendingBatchId: null,
...config,
});
state.selectedEntityIdentifier = { type: 'control_adapter', id };
},
prepare: (payload: { config: ControlNetConfig | T2IAdapterConfig }) => ({
payload: { id: uuidv4(), ...payload },
}),
},
caRecalled: (state, action: PayloadAction<{ data: CanvasControlAdapterState }>) => {
const { data } = action.payload;
state.controlAdapters.entities.push(data);
state.selectedEntityIdentifier = { type: 'control_adapter', id: data.id };
},
caAllDeleted: (state) => {
state.controlAdapters.entities = [];
},
caOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.opacity = opacity;
},
caImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => {
const { id, imageDTO, objectId } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
if (imageDTO) {
const newImageObject = imageDTOToImageObject(imageDTO, { filters: ca.filters });
if (isEqual(newImageObject, ca.imageObject)) {
return;
}
ca.imageObject = newImageObject;
ca.processedImageObject = null;
} else {
ca.imageObject = null;
ca.processedImageObject = null;
}
},
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
},
caProcessedImageChanged: {
reducer: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO | null; objectId: string }>) => {
const { id, imageDTO, objectId } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.bbox = null;
ca.bboxNeedsUpdate = true;
ca.isEnabled = true;
ca.processedImageObject = imageDTO ? imageDTOToImageObject(imageDTO, { filters: ca.filters }) : null;
},
prepare: (payload: { id: string; imageDTO: ImageDTO | null }) => ({ payload: { ...payload, objectId: uuidv4() } }),
},
caModelChanged: (
state,
action: PayloadAction<{
id: string;
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | null;
}>
) => {
const { id, modelConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
if (!modelConfig) {
ca.model = null;
return;
}
ca.model = zModelIdentifierField.parse(modelConfig);
const candidateProcessorConfig = buildControlAdapterProcessorV2(modelConfig);
if (candidateProcessorConfig?.type !== ca.processorConfig?.type) {
// The processor has changed. For example, the previous model was a Canny model and the new model is a Depth
// model. We need to use the new processor.
ca.processedImageObject = null;
ca.processorConfig = candidateProcessorConfig;
}
// We may need to convert the CA to match the model
if (ca.adapterType === 't2i_adapter' && ca.model.type === 'controlnet') {
const convertedCA: CanvasControlNetState = { ...ca, adapterType: 'controlnet', controlMode: 'balanced' };
state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA);
} else if (ca.adapterType === 'controlnet' && ca.model.type === 't2i_adapter') {
const { controlMode: _, ...rest } = ca;
const convertedCA: CanvasT2IAdapterState = { ...rest, adapterType: 't2i_adapter' };
state.controlAdapters.entities.splice(state.controlAdapters.entities.indexOf(ca), 1, convertedCA);
}
},
caControlModeChanged: (state, action: PayloadAction<{ id: string; controlMode: ControlModeV2 }>) => {
const { id, controlMode } = action.payload;
const ca = selectCA(state, id);
if (!ca || ca.adapterType !== 'controlnet') {
return;
}
ca.controlMode = controlMode;
},
caProcessorConfigChanged: (state, action: PayloadAction<{ id: string; processorConfig: FilterConfig | null }>) => {
const { id, processorConfig } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorConfig = processorConfig;
if (!processorConfig) {
ca.processedImageObject = null;
}
},
caFilterChanged: (state, action: PayloadAction<{ id: string; filters: Filter[] }>) => {
const { id, filters } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.filters = filters;
if (ca.imageObject) {
ca.imageObject.filters = filters;
}
if (ca.processedImageObject) {
ca.processedImageObject.filters = filters;
}
},
caProcessorPendingBatchIdChanged: (state, action: PayloadAction<{ id: string; batchId: string | null }>) => {
const { id, batchId } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.processorPendingBatchId = batchId;
},
caWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
const { id, weight } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.weight = weight;
},
caBeginEndStepPctChanged: (state, action: PayloadAction<{ id: string; beginEndStepPct: [number, number] }>) => {
const { id, beginEndStepPct } = action.payload;
const ca = selectCA(state, id);
if (!ca) {
return;
}
ca.beginEndStepPct = beginEndStepPct;
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,7 +1,5 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import type { CanvasInpaintMaskState, CanvasV2State } from 'features/controlLayers/store/types';
import { imageDTOToImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDTO } from 'services/api/types';
import type { RgbColor } from './types';
@ -15,8 +13,4 @@ export const inpaintMaskReducers = {
const { fill } = action.payload;
state.inpaintMask.fill = fill;
},
imImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => {
const { imageDTO } = action.payload;
state.inpaintMask.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -1,12 +1,11 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { merge } from 'lodash-es';
import type { ControlNetModelConfig, ImageDTO, T2IAdapterModelConfig } from 'services/api/types';
import { isEqual, merge } from 'lodash-es';
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, T2IAdapterConfig } from './types';
import { imageDTOToImageWithDims } from './types';
import type { CanvasLayerState, CanvasV2State, ControlModeV2, ControlNetConfig, Rect, T2IAdapterConfig } from './types';
export const selectLayer = (state: CanvasV2State, id: string) => state.layers.entities.find((layer) => layer.id === id);
export const selectLayerOrThrow = (state: CanvasV2State, id: string) => {
@ -29,7 +28,7 @@ export const layersReducers = {
objects: [],
opacity: 1,
position: { x: 0, y: 0 },
imageCache: null,
rasterizationCache: [],
controlAdapter: null,
};
merge(layer, overrides);
@ -37,7 +36,11 @@ export const layersReducers = {
if (isSelected) {
state.selectedEntityIdentifier = { type: 'layer', id };
}
state.layers.imageCache = null;
if (layer.objects.length > 0) {
// This new layer will change the composite layer's image data. Invalidate the cache.
state.layers.compositeRasterizationCache = [];
}
},
prepare: (payload: { overrides?: Partial<CanvasLayerState>; isSelected?: boolean }) => ({
payload: { ...payload, id: getPrefixedId('layer') },
@ -47,24 +50,20 @@ export const layersReducers = {
const { data } = action.payload;
state.layers.entities.push(data);
state.selectedEntityIdentifier = { type: 'layer', id: data.id };
state.layers.imageCache = null;
if (data.objects.length > 0) {
// This new layer will change the composite layer's image data. Invalidate the cache.
state.layers.compositeRasterizationCache = [];
}
},
layerAllDeleted: (state) => {
state.layers.entities = [];
state.layers.imageCache = null;
state.layers.compositeRasterizationCache = [];
},
layerOpacityChanged: (state, action: PayloadAction<{ id: string; opacity: number }>) => {
const { id, opacity } = action.payload;
const layer = selectLayer(state, id);
if (!layer) {
return;
}
layer.opacity = opacity;
state.layers.imageCache = null;
},
layerImageCacheChanged: (state, action: PayloadAction<{ imageDTO: ImageDTO | null }>) => {
const { imageDTO } = action.payload;
state.layers.imageCache = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
layerCompositeRasterized: (state, action: PayloadAction<{ imageName: string; rect: Rect }>) => {
state.layers.compositeRasterizationCache = state.layers.compositeRasterizationCache.filter(
(cache) => !isEqual(cache.rect, action.payload.rect)
);
state.layers.compositeRasterizationCache.push(action.payload);
},
layerUsedAsControlChanged: (
state,
@ -76,6 +75,8 @@ export const layersReducers = {
return;
}
layer.controlAdapter = controlAdapter;
// The composite layer's image data will change when the layer is used as control (or not). Invalidate the cache.
state.layers.compositeRasterizationCache = [];
},
layerControlAdapterModelChanged: (
state,

View File

@ -54,7 +54,7 @@ export const regionsReducers = {
positivePrompt: '',
negativePrompt: null,
ipAdapters: [],
imageCache: null,
rasterizationCache: [],
};
state.regions.entities.push(rg);
state.selectedEntityIdentifier = { type: 'regional_guidance', id };
@ -93,14 +93,6 @@ export const regionsReducers = {
}
rg.fill = fill;
},
rgImageCacheChanged: (state, action: PayloadAction<{ id: string; imageDTO: ImageDTO }>) => {
const { id, imageDTO } = action.payload;
const rg = selectRG(state, id);
if (!rg) {
return;
}
rg.imageCache = imageDTO.image_name;
},
rgAutoNegativeChanged: (state, action: PayloadAction<{ id: string; autoNegative: ParameterAutoNegative }>) => {
const { id, autoNegative } = action.payload;
const rg = selectRG(state, id);

View File

@ -5,7 +5,7 @@ import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
export const selectEntityCount = createSelector(selectCanvasV2Slice, (canvasV2) => {
return (
canvasV2.regions.entities.length +
canvasV2.controlAdapters.entities.length +
// canvasV2.controlAdapters.entities.length +
canvasV2.ipAdapters.entities.length +
canvasV2.layers.entities.length
);

View File

@ -639,6 +639,12 @@ const zMaskObject = z
})
.pipe(z.discriminatedUnion('type', [zCanvasBrushLineState, zCanvasEraserLineState, zCanvasRectState]));
const zImageCache = z.object({
imageName: z.string(),
rect: zRect,
});
export type ImageCache = z.infer<typeof zImageCache>;
export const zCanvasRegionalGuidanceState = z.object({
id: zId,
type: z.literal('regional_guidance'),
@ -650,7 +656,7 @@ export const zCanvasRegionalGuidanceState = z.object({
negativePrompt: zParameterNegativePrompt.nullable(),
ipAdapters: z.array(zCanvasIPAdapterState),
autoNegative: zAutoNegative,
imageCache: z.string().min(1).nullable(),
rasterizationCache: z.array(zImageCache),
});
export type CanvasRegionalGuidanceState = z.infer<typeof zCanvasRegionalGuidanceState>;
@ -670,7 +676,7 @@ const zCanvasInpaintMaskState = z.object({
position: zCoordinate,
fill: zRgbColor,
objects: z.array(zCanvasObjectState),
imageCache: z.string().min(1).nullable(),
rasterizationCache: z.array(zImageCache),
});
export type CanvasInpaintMaskState = z.infer<typeof zCanvasInpaintMaskState>;
@ -729,7 +735,7 @@ export const zCanvasLayerState = z.object({
position: zCoordinate,
opacity: zOpacity,
objects: z.array(zCanvasObjectState),
imageCache: z.string().min(1).nullable(),
rasterizationCache: z.array(zImageCache),
controlAdapter: z.discriminatedUnion('type', [zControlNetConfig, zT2IAdapterConfig]).nullable(),
});
export type CanvasLayerState = z.infer<typeof zCanvasLayerState>;
@ -826,11 +832,7 @@ export type CanvasV2State = {
_version: 3;
selectedEntityIdentifier: CanvasEntityIdentifier | null;
inpaintMask: CanvasInpaintMaskState;
layers: {
imageCache: ImageWithDims | null;
entities: CanvasLayerState[];
};
controlAdapters: { entities: CanvasControlAdapterState[] };
layers: { entities: CanvasLayerState[]; compositeRasterizationCache: ImageCache[] };
ipAdapters: { entities: CanvasIPAdapterState[] };
regions: { entities: CanvasRegionalGuidanceState[] };
loras: LoRA[];
@ -938,7 +940,7 @@ export type EntityRectAddedPayload = { entityIdentifier: CanvasEntityIdentifier;
export type EntityRasterizedPayload = {
entityIdentifier: CanvasEntityIdentifier;
imageObject: CanvasImageState;
position: Coordinate;
rect: Rect;
};
export type ImageObjectAddedArg = { id: string; imageDTO: ImageDTO; position?: Coordinate };

View File

@ -13,7 +13,7 @@ import {
import {
bboxHeightChanged,
bboxWidthChanged,
caRecalled,
// caRecalled,
ipaRecalled,
layerAllDeleted,
layerRecalled,
@ -43,8 +43,8 @@ import type {
CanvasControlAdapterState,
CanvasIPAdapterState,
CanvasLayerState,
LoRA,
CanvasRegionalGuidanceState,
LoRA,
} from 'features/controlLayers/store/types';
import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice';
import type {
@ -271,7 +271,7 @@ const recallCA: MetadataRecallFunc<CanvasControlAdapterState> = async (ca) => {
}
// No clobber
clone.id = getCAId(uuidv4());
dispatch(caRecalled({ data: clone }));
// dispatch(caRecalled({ data: clone }));
return;
};

View File

@ -26,10 +26,11 @@ export const addControlAdapters = async (
const layersWithValidControlAdapters = layers
.filter((layer) => layer.isEnabled)
.filter((layer) => doesLayerHaveValidControlAdapter(layer, base));
for (const layer of layersWithValidControlAdapters) {
const adapter = manager.layers.get(layer.id);
assert(adapter, 'Adapter not found');
const imageDTO = await adapter.renderer.getImageDTO({ rect: bbox, is_intermediate: true, category: 'control' });
const imageDTO = await adapter.renderer.rasterize(bbox);
if (layer.controlAdapter.type === 'controlnet') {
await addControlNetToGraph(g, layer, imageDTO, denoise);
} else {

View File

@ -22,7 +22,7 @@ export const addInpaint = async (
denoise.denoising_start = denoising_start;
const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect);
const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect);
const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect);
if (!isEqual(scaledSize, originalSize)) {
// Scale before processing requires some resizing

View File

@ -23,7 +23,7 @@ export const addOutpaint = async (
denoise.denoising_start = denoising_start;
const initialImage = await manager.getCompositeLayerImageDTO(bbox.rect);
const maskImage = await manager.getInpaintMaskImageDTO(bbox.rect);
const maskImage = await manager.inpaintMask.renderer.rasterize(bbox.rect);
const infill = getInfill(g, compositing);
if (!isEqual(scaledSize, originalSize)) {

View File

@ -43,15 +43,16 @@ export const addRegions = async (
const validRegions = regions.filter((rg) => isValidRegion(rg, base));
for (const region of validRegions) {
// Upload the mask image, or get the cached image if it exists
const { image_name } = await manager.getRegionMaskImageDTO(region.id, bbox);
const adapter = manager.regions.get(region.id);
assert(adapter, 'Adapter not found');
const imageDTO = await adapter.renderer.rasterize(bbox);
// The main mask-to-tensor node
const maskToTensor = g.addNode({
id: `${PROMPT_REGION_MASK_TO_TENSOR_PREFIX}_${region.id}`,
type: 'alpha_mask_to_tensor',
image: {
image_name,
image_name: imageDTO.image_name,
},
});