mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0848cb8ebd | ||
|
|
1b777bb972 | ||
|
|
029ee90351 | ||
|
|
2f9a064d48 | ||
|
|
b180666497 | ||
|
|
4740cd4f64 | ||
|
|
8b51298ba1 | ||
|
|
1533429e54 | ||
|
|
fc000214a5 | ||
|
|
f631aea4ee | ||
|
|
32f4c1f966 | ||
|
|
adebe639e3 | ||
|
|
44280ed472 | ||
|
|
cec8840038 | ||
|
|
fc7f484935 | ||
|
|
1aa7cd57c2 | ||
|
|
722a91aedb | ||
|
|
03c24ca9cb | ||
|
|
5820579237 | ||
|
|
6c768bfe7e | ||
|
|
5ca794b94f | ||
|
|
d20695260d | ||
|
|
d8557d573b | ||
|
|
6c1fd584d2 | ||
|
|
e8e764be20 | ||
|
|
e8023c44b0 | ||
|
|
a3a6449786 | ||
|
|
e9d2ffe3d7 | ||
|
|
23ad6fb730 | ||
|
|
00f36cb491 | ||
|
|
3f489c92c8 | ||
|
|
f147f99bef | ||
|
|
6107e3d281 | ||
|
|
de33d6e647 | ||
|
|
e36e5871a1 | ||
|
|
8b25c1a62e | ||
|
|
dfbd7eb1cf | ||
|
|
b43b2714cc | ||
|
|
e537de2f6d | ||
|
|
ccd399e277 | ||
|
|
bfad814862 | ||
|
|
6e8b7f9421 | ||
|
|
e47629cbe7 | ||
|
|
e840de27ed | ||
|
|
8342f32f2e | ||
|
|
a7aa529b99 | ||
|
|
4adc592657 | ||
|
|
e8d60e8d83 | ||
|
|
886f5c90a3 |
@@ -65,6 +65,7 @@
|
|||||||
"chakra-react-select": "^4.7.6",
|
"chakra-react-select": "^4.7.6",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"dateformat": "^5.0.3",
|
"dateformat": "^5.0.3",
|
||||||
|
"fracturedjsonjs": "^4.0.1",
|
||||||
"framer-motion": "^11.1.8",
|
"framer-motion": "^11.1.8",
|
||||||
"i18next": "^23.11.3",
|
"i18next": "^23.11.3",
|
||||||
"i18next-http-backend": "^2.5.1",
|
"i18next-http-backend": "^2.5.1",
|
||||||
|
|||||||
7
invokeai/frontend/web/pnpm-lock.yaml
generated
7
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ dependencies:
|
|||||||
dateformat:
|
dateformat:
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
|
fracturedjsonjs:
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^11.1.8
|
specifier: ^11.1.8
|
||||||
version: 11.1.8(react-dom@18.3.1)(react@18.3.1)
|
version: 11.1.8(react-dom@18.3.1)(react@18.3.1)
|
||||||
@@ -8691,6 +8694,10 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fracturedjsonjs@4.0.1:
|
||||||
|
resolution: {integrity: sha512-KMhSx7o45aPVj4w27dwdQyKJkNU8oBqw8UiK/s3VzsQB3+pKQ/3AqG/YOEQblV2BDuYE5dKp0OMf8RDsshrjTA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1):
|
/framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==}
|
resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -142,9 +142,11 @@
|
|||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
"alpha": "Alpha",
|
"alpha": "Alpha",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"viewer": "Viewer",
|
|
||||||
"tab": "Tab",
|
"tab": "Tab",
|
||||||
"close": "Close"
|
"viewing": "Viewing",
|
||||||
|
"viewingDesc": "Review images in a large gallery view",
|
||||||
|
"editing": "Editing",
|
||||||
|
"editingDesc": "Edit on the Control Layers canvas"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
@@ -365,10 +367,7 @@
|
|||||||
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
"bulkDownloadRequestFailed": "Problem Preparing Download",
|
||||||
"bulkDownloadFailed": "Download Failed",
|
"bulkDownloadFailed": "Download Failed",
|
||||||
"problemDeletingImages": "Problem Deleting Images",
|
"problemDeletingImages": "Problem Deleting Images",
|
||||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
"problemDeletingImagesDesc": "One or more images could not be deleted"
|
||||||
"switchTo": "Switch to {{ tab }} (Z)",
|
|
||||||
"openFloatingViewer": "Open Floating Viewer",
|
|
||||||
"closeFloatingViewer": "Close Floating Viewer"
|
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
@@ -939,10 +938,12 @@
|
|||||||
},
|
},
|
||||||
"maskBlur": "Mask Blur",
|
"maskBlur": "Mask Blur",
|
||||||
"negativePromptPlaceholder": "Negative Prompt",
|
"negativePromptPlaceholder": "Negative Prompt",
|
||||||
|
"globalNegativePromptPlaceholder": "Global Negative Prompt",
|
||||||
"noiseThreshold": "Noise Threshold",
|
"noiseThreshold": "Noise Threshold",
|
||||||
"patchmatchDownScaleSize": "Downscale",
|
"patchmatchDownScaleSize": "Downscale",
|
||||||
"perlinNoise": "Perlin Noise",
|
"perlinNoise": "Perlin Noise",
|
||||||
"positivePromptPlaceholder": "Positive Prompt",
|
"positivePromptPlaceholder": "Positive Prompt",
|
||||||
|
"globalPositivePromptPlaceholder": "Global Positive Prompt",
|
||||||
"iterations": "Iterations",
|
"iterations": "Iterations",
|
||||||
"iterationsWithCount_one": "{{count}} Iteration",
|
"iterationsWithCount_one": "{{count}} Iteration",
|
||||||
"iterationsWithCount_other": "{{count}} Iterations",
|
"iterationsWithCount_other": "{{count}} Iterations",
|
||||||
@@ -1547,8 +1548,6 @@
|
|||||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||||
"regionalGuidance": "Regional Guidance",
|
"regionalGuidance": "Regional Guidance",
|
||||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||||
"controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)",
|
|
||||||
"ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
||||||
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||||
@@ -1559,7 +1558,9 @@
|
|||||||
"opacityFilter": "Opacity Filter",
|
"opacityFilter": "Opacity Filter",
|
||||||
"clearProcessor": "Clear Processor",
|
"clearProcessor": "Clear Processor",
|
||||||
"resetProcessor": "Reset Processor to Defaults",
|
"resetProcessor": "Reset Processor to Defaults",
|
||||||
"noLayersAdded": "No Layers Added"
|
"noLayersAdded": "No Layers Added",
|
||||||
|
"layers_one": "Layer",
|
||||||
|
"layers_other": "Layers"
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"tabs": {
|
"tabs": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
caLayerModelChanged,
|
caLayerModelChanged,
|
||||||
caLayerProcessedImageChanged,
|
caLayerProcessedImageChanged,
|
||||||
caLayerProcessorConfigChanged,
|
caLayerProcessorConfigChanged,
|
||||||
|
caLayerRecalled,
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
||||||
@@ -20,7 +21,7 @@ import { queueApi } from 'services/api/endpoints/queue';
|
|||||||
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
||||||
import { socketInvocationComplete } from 'services/events/actions';
|
import { socketInvocationComplete } from 'services/events/actions';
|
||||||
|
|
||||||
const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged);
|
const matcher = isAnyOf(caLayerImageChanged, caLayerProcessorConfigChanged, caLayerModelChanged, caLayerRecalled);
|
||||||
|
|
||||||
const DEBOUNCE_MS = 300;
|
const DEBOUNCE_MS = 300;
|
||||||
const log = logger('session');
|
const log = logger('session');
|
||||||
@@ -29,7 +30,7 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
startAppListening({
|
startAppListening({
|
||||||
matcher,
|
matcher,
|
||||||
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => {
|
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take }) => {
|
||||||
const { layerId } = action.payload;
|
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
|
||||||
const precheckLayerOriginal = getOriginalState()
|
const precheckLayerOriginal = getOriginalState()
|
||||||
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
||||||
.find((l) => l.id === layerId);
|
.find((l) => l.id === layerId);
|
||||||
@@ -131,7 +132,6 @@ export const addControlAdapterPreprocessor = (startAppListening: AppStartListeni
|
|||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||||
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
dispatch(caLayerIsProcessingImageChanged({ layerId, isProcessingImage: false }));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { enqueueRequested } from 'app/store/actions';
|
import { enqueueRequested } from 'app/store/actions';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph';
|
import { buildGenerationTabGraph } from 'features/nodes/util/graph/buildGenerationTabGraph';
|
||||||
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph';
|
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/buildGenerationTabSDXLGraph';
|
||||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||||
@@ -11,6 +12,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
enqueueRequested.match(action) && action.payload.tabName === 'generation',
|
enqueueRequested.match(action) && action.payload.tabName === 'generation',
|
||||||
effect: async (action, { getState, dispatch }) => {
|
effect: async (action, { getState, dispatch }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const { shouldShowProgressInViewer } = state.ui;
|
||||||
const model = state.generation.model;
|
const model = state.generation.model;
|
||||||
const { prepend } = action.payload;
|
const { prepend } = action.payload;
|
||||||
|
|
||||||
@@ -29,7 +31,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
|||||||
fixedCacheKey: 'enqueueBatch',
|
fixedCacheKey: 'enqueueBatch',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
req.reset();
|
try {
|
||||||
|
req.unwrap();
|
||||||
|
if (shouldShowProgressInViewer) {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
req.reset();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
import { isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
import { imagesSelectors } from 'services/api/util';
|
import { imagesSelectors } from 'services/api/util';
|
||||||
@@ -62,7 +62,6 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
|
|||||||
} else {
|
} else {
|
||||||
dispatch(selectionChanged([imageDTO]));
|
dispatch(selectionChanged([imageDTO]));
|
||||||
}
|
}
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,25 +73,25 @@ const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, ima
|
|||||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||||
state.controlLayers.present.layers.forEach((l) => {
|
state.controlLayers.present.layers.forEach((l) => {
|
||||||
if (isRegionalGuidanceLayer(l)) {
|
if (isRegionalGuidanceLayer(l)) {
|
||||||
if (l.ipAdapters.some((ipa) => ipa.image?.imageName === imageDTO.image_name)) {
|
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isControlAdapterLayer(l)) {
|
if (isControlAdapterLayer(l)) {
|
||||||
if (
|
if (
|
||||||
l.controlAdapter.image?.imageName === imageDTO.image_name ||
|
l.controlAdapter.image?.name === imageDTO.image_name ||
|
||||||
l.controlAdapter.processedImage?.imageName === imageDTO.image_name
|
l.controlAdapter.processedImage?.name === imageDTO.image_name
|
||||||
) {
|
) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isIPAdapterLayer(l)) {
|
if (isIPAdapterLayer(l)) {
|
||||||
if (l.ipAdapter.image?.imageName === imageDTO.image_name) {
|
if (l.ipAdapter.image?.name === imageDTO.image_name) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInitialImageLayer(l)) {
|
if (isInitialImageLayer(l)) {
|
||||||
if (l.image?.imageName === imageDTO.image_name) {
|
if (l.image?.name === imageDTO.image_name) {
|
||||||
dispatch(layerDeleted(l.id));
|
dispatch(layerDeleted(l.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
onMouseOver,
|
onMouseOver,
|
||||||
onMouseOut,
|
onMouseOut,
|
||||||
dataTestId,
|
dataTestId,
|
||||||
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@@ -138,6 +139,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
minH={minSize ? minSize : undefined}
|
minH={minSize ? minSize : undefined}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{imageDTO && (
|
{imageDTO && (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
} from 'features/canvas/store/canvasSlice';
|
} from 'features/canvas/store/canvasSlice';
|
||||||
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
|
||||||
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -220,107 +219,97 @@ const IAICanvasToolbar = () => {
|
|||||||
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
|
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" gap={2} alignItems="center">
|
<Flex alignItems="center" gap={2} flexWrap="wrap">
|
||||||
<Flex flex={1} justifyContent="center">
|
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
||||||
<Flex gap={2} marginInlineEnd="auto" />
|
<FormControl isDisabled={isStaging} w="5rem">
|
||||||
</Flex>
|
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
</FormControl>
|
||||||
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
|
</Tooltip>
|
||||||
<FormControl isDisabled={isStaging} w="5rem">
|
|
||||||
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
|
|
||||||
</FormControl>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<IAICanvasMaskOptions />
|
<IAICanvasMaskOptions />
|
||||||
<IAICanvasToolChooserOptions />
|
<IAICanvasToolChooserOptions />
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
aria-label={`${t('unifiedCanvas.move')} (V)`}
|
||||||
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
tooltip={`${t('unifiedCanvas.move')} (V)`}
|
||||||
icon={<PiHandGrabbingBold />}
|
icon={<PiHandGrabbingBold />}
|
||||||
isChecked={tool === 'move' || isStaging}
|
isChecked={tool === 'move' || isStaging}
|
||||||
onClick={handleSelectMoveTool}
|
onClick={handleSelectMoveTool}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
aria-label={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
tooltip={`${shouldShowBoundingBox ? t('unifiedCanvas.hideBoundingBox') : t('unifiedCanvas.showBoundingBox')} (Shift + H)`}
|
||||||
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
icon={shouldShowBoundingBox ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||||
onClick={handleSetShouldShowBoundingBox}
|
onClick={handleSetShouldShowBoundingBox}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
aria-label={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
tooltip={`${t('unifiedCanvas.resetView')} (R)`}
|
||||||
icon={<PiCrosshairSimpleBold />}
|
icon={<PiCrosshairSimpleBold />}
|
||||||
onClick={handleClickResetCanvasView}
|
onClick={handleClickResetCanvasView}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
||||||
|
icon={<PiStackBold />}
|
||||||
|
onClick={handleMergeVisible}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||||
|
icon={<PiFloppyDiskBold />}
|
||||||
|
onClick={handleSaveToGallery}
|
||||||
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
tooltip={`${t('unifiedCanvas.mergeVisible')} (Shift+M)`}
|
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
icon={<PiStackBold />}
|
icon={<PiCopyBold />}
|
||||||
onClick={handleMergeVisible}
|
onClick={handleCopyImageToClipboard}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
)}
|
||||||
aria-label={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
<IconButton
|
||||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
icon={<PiFloppyDiskBold />}
|
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
onClick={handleSaveToGallery}
|
icon={<PiDownloadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
onClick={handleDownloadAsImage}
|
||||||
/>
|
isDisabled={isStaging}
|
||||||
{isClipboardAPIAvailable && (
|
/>
|
||||||
<IconButton
|
</ButtonGroup>
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<ButtonGroup>
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<IAICanvasUndoButton />
|
||||||
icon={<PiCopyBold />}
|
<IAICanvasRedoButton />
|
||||||
onClick={handleCopyImageToClipboard}
|
</ButtonGroup>
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
|
||||||
icon={<PiDownloadSimpleBold />}
|
|
||||||
onClick={handleDownloadAsImage}
|
|
||||||
isDisabled={isStaging}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
<ButtonGroup>
|
|
||||||
<IAICanvasUndoButton />
|
|
||||||
<IAICanvasRedoButton />
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('common.upload')}`}
|
aria-label={`${t('common.upload')}`}
|
||||||
tooltip={`${t('common.upload')}`}
|
tooltip={`${t('common.upload')}`}
|
||||||
icon={<PiUploadSimpleBold />}
|
icon={<PiUploadSimpleBold />}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
{...getUploadButtonProps()}
|
{...getUploadButtonProps()}
|
||||||
/>
|
/>
|
||||||
<input {...getUploadInputProps()} />
|
<input {...getUploadInputProps()} />
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
aria-label={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
tooltip={`${t('unifiedCanvas.clearCanvas')}`}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={handleResetCanvas}
|
onClick={handleResetCanvas}
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IAICanvasSettingsButtonPopover />
|
<IAICanvasSettingsButtonPopover />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
|
||||||
<Flex flex={1} justifyContent="center">
|
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
|
||||||
<ViewerButton />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ export const AddLayerButton = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={Button} leftIcon={<PiPlusBold />} variant="ghost">
|
<MenuButton
|
||||||
|
as={Button}
|
||||||
|
leftIcon={<PiPlusBold />}
|
||||||
|
variant="ghost"
|
||||||
|
data-testid="control-layers-add-layer-menu-button"
|
||||||
|
>
|
||||||
{t('controlLayers.addLayer')}
|
{t('controlLayers.addLayer')}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export const CALayer = memo(({ layerId }: Props) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
const isSelected = useAppSelector((s) => selectCALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export const ControlAdapterImagePreview = memo(
|
|||||||
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
||||||
|
|
||||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||||
controlAdapter.image?.imageName ?? skipToken
|
controlAdapter.image?.name ?? skipToken
|
||||||
);
|
);
|
||||||
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
||||||
controlAdapter.processedImage?.imageName ?? skipToken
|
controlAdapter.processedImage?.name ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ export const IPAdapterImagePreview = memo(
|
|||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken);
|
||||||
image?.imageName ?? skipToken
|
|
||||||
);
|
|
||||||
const handleResetControlImage = useCallback(() => {
|
const handleResetControlImage = useCallback(() => {
|
||||||
onChangeImage(null);
|
onChangeImage(null);
|
||||||
}, [onChangeImage]);
|
}, [onChangeImage]);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const ControlLayersPanelContent = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{layerIdTypePairs.length > 0 && (
|
{layerIdTypePairs.length > 0 && (
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2}>
|
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list">
|
||||||
{layerIdTypePairs.map(({ id, type }) => (
|
{layerIdTypePairs.map(({ id, type }) => (
|
||||||
<LayerWrapper key={id} id={id} type={type} />
|
<LayerWrapper key={id} id={id} type={type} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
import { Flex, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { setShouldInvertBrushSizeScrollDirection } from 'features/canvas/store/canvasSlice';
|
||||||
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity';
|
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity';
|
||||||
import { memo } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiSettings4Fill } from 'react-icons/ri';
|
import { RiSettings4Fill } from 'react-icons/ri';
|
||||||
|
|
||||||
const ControlLayersSettingsPopover = () => {
|
const ControlLayersSettingsPopover = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
||||||
|
const handleChangeShouldInvertBrushSizeScrollDirection = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldInvertBrushSizeScrollDirection(e.target.checked)),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
@@ -16,6 +34,13 @@ const ControlLayersSettingsPopover = () => {
|
|||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
<GlobalMaskLayerOpacity />
|
<GlobalMaskLayerOpacity />
|
||||||
|
<FormControl w="full">
|
||||||
|
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={shouldInvertBrushSizeScrollDirection}
|
||||||
|
onChange={handleChangeShouldInvertBrushSizeScrollDirection}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
|||||||
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||||
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const ControlLayersToolbar = memo(() => {
|
export const ControlLayersToolbar = memo(() => {
|
||||||
return (
|
return (
|
||||||
<Flex w="full" gap={2}>
|
<Flex w="full" gap={2}>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineEnd="auto" />
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} gap={2} justifyContent="center">
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
<BrushSize />
|
<BrushSize />
|
||||||
@@ -21,7 +24,7 @@ export const ControlLayersToolbar = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
<ViewerButton />
|
<ViewerToggleMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const DeleteAllLayersButton = memo(() => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
data-testid="control-layers-delete-all-layers-button"
|
||||||
>
|
>
|
||||||
{t('controlLayers.deleteAll')}
|
{t('controlLayers.deleteAll')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerT
|
|||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import {
|
import {
|
||||||
|
iiLayerDenoisingStrengthChanged,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
layerSelected,
|
layerSelected,
|
||||||
selectIILayerOrThrow,
|
selectIILayerOrThrow,
|
||||||
@@ -36,6 +37,13 @@ export const IILayer = memo(({ layerId }: Props) => {
|
|||||||
[dispatch, layerId]
|
[dispatch, layerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onChangeDenoisingStrength = useCallback(
|
||||||
|
(denoisingStrength: number) => {
|
||||||
|
dispatch(iiLayerDenoisingStrengthChanged({ layerId, denoisingStrength }));
|
||||||
|
},
|
||||||
|
[dispatch, layerId]
|
||||||
|
);
|
||||||
|
|
||||||
const droppableData = useMemo<IILayerImageDropData>(
|
const droppableData = useMemo<IILayerImageDropData>(
|
||||||
() => ({
|
() => ({
|
||||||
actionType: 'SET_II_LAYER_IMAGE',
|
actionType: 'SET_II_LAYER_IMAGE',
|
||||||
@@ -67,7 +75,7 @@ export const IILayer = memo(({ layerId }: Props) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
<ImageToImageStrength />
|
<ImageToImageStrength value={layer.denoisingStrength} onChange={onChangeDenoisingStrength} />
|
||||||
<InitialImagePreview
|
<InitialImagePreview
|
||||||
image={layer.image}
|
image={layer.image}
|
||||||
onChangeImage={onChangeImage}
|
onChangeImage={onChangeImage}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const InitialImagePreview = memo(({ image, onChangeImage, droppableData,
|
|||||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.imageName ?? skipToken);
|
const { currentData: imageDTO, isError: isErrorControlImage } = useGetImageDTOQuery(image?.name ?? skipToken);
|
||||||
|
|
||||||
const onReset = useCallback(() => {
|
const onReset = useCallback(() => {
|
||||||
onChangeImage(null);
|
onChangeImage(null);
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
|
import { IPALayerIPAdapterWrapper } from 'features/controlLayers/components/IPALayer/IPALayerIPAdapterWrapper';
|
||||||
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerCommon/LayerDeleteButton';
|
||||||
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
import { LayerTitle } from 'features/controlLayers/components/LayerCommon/LayerTitle';
|
||||||
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerCommon/LayerVisibilityToggle';
|
||||||
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
import { LayerWrapper } from 'features/controlLayers/components/LayerCommon/LayerWrapper';
|
||||||
import { memo } from 'react';
|
import { layerSelected, selectIPALayerOrThrow } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IPALayer = memo(({ layerId }: Props) => {
|
export const IPALayer = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isSelected = useAppSelector((s) => selectIPALayerOrThrow(s.controlLayers.present, layerId).isSelected);
|
||||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(layerSelected(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<LayerWrapper borderColor="base.800">
|
<LayerWrapper onClick={onClick} borderColor={isSelected ? 'base.400' : 'base.800'}>
|
||||||
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
<LayerVisibilityToggle layerId={layerId} />
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
<LayerTitle type="ip_adapter_layer" />
|
<LayerTitle type="ip_adapter_layer" />
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
|||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
|||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
minH={28}
|
minH={28}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ const useStageRenderer = (
|
|||||||
}, [stage, state.size.width, state.size.height, wrapper]);
|
}, [stage, state.size.width, state.size.height, wrapper]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering tool preview');
|
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// Preview should not display tool
|
// Preview should not display tool
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering tool preview');
|
||||||
renderers.renderToolPreview(
|
renderers.renderToolPreview(
|
||||||
stage,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
@@ -178,15 +178,24 @@ const useStageRenderer = (
|
|||||||
// Preview should not display bboxes
|
// Preview should not display bboxes
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderers.renderBbox(stage, state.layers, tool, onBboxChanged);
|
renderers.renderBboxes(stage, state.layers, tool);
|
||||||
}, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]);
|
}, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering background');
|
if (asPreview) {
|
||||||
|
// Preview should not check for transparency
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.trace('Updating bboxes');
|
||||||
|
debouncedRenderers.updateBboxes(stage, state.layers, onBboxChanged);
|
||||||
|
}, [stage, asPreview, state.layers, onBboxChanged]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// The preview should not have a background
|
// The preview should not have a background
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering background');
|
||||||
renderers.renderBackground(stage, state.size.width, state.size.height);
|
renderers.renderBackground(stage, state.size.width, state.size.height);
|
||||||
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
||||||
|
|
||||||
@@ -196,11 +205,11 @@ const useStageRenderer = (
|
|||||||
}, [stage, layerIds, renderers]);
|
}, [stage, layerIds, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering no layers message');
|
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
// The preview should not display the no layers message
|
// The preview should not display the no layers message
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.trace('Rendering no layers message');
|
||||||
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
||||||
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
||||||
|
|
||||||
@@ -233,7 +242,14 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Flex overflow="hidden" w="full" h="full">
|
<Flex overflow="hidden" w="full" h="full">
|
||||||
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
|
||||||
<Flex ref={containerRef} tabIndex={-1} bg="base.850" borderRadius="base" overflow="hidden" />
|
<Flex
|
||||||
|
ref={containerRef}
|
||||||
|
tabIndex={-1}
|
||||||
|
bg="base.850"
|
||||||
|
borderRadius="base"
|
||||||
|
overflow="hidden"
|
||||||
|
data-testid="control-layers-canvas"
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -224,5 +224,10 @@ export const useMouseEvents = () => {
|
|||||||
[selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize]
|
[selectedLayerType, tool, shouldInvertBrushSizeScrollDirection, dispatch, brushSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel };
|
const handlers = useMemo(
|
||||||
|
() => ({ onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel }),
|
||||||
|
[onMouseDown, onMouseUp, onMouseMove, onMouseLeave, onMouseWheel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return handlers;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const initialControlLayersState: ControlLayersState = {
|
export const initialControlLayersState: ControlLayersState = {
|
||||||
_version: 1,
|
_version: 2,
|
||||||
selectedLayerId: null,
|
selectedLayerId: null,
|
||||||
brushSize: 100,
|
brushSize: 100,
|
||||||
layers: [],
|
layers: [],
|
||||||
@@ -124,6 +124,12 @@ const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
|
|||||||
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
const lastColor = rgLayers[rgLayers.length - 1]?.previewColor;
|
||||||
return LayerColors.next(lastColor);
|
return LayerColors.next(lastColor);
|
||||||
};
|
};
|
||||||
|
const exclusivelySelectLayer = (state: ControlLayersState, layerId: string) => {
|
||||||
|
for (const layer of state.layers) {
|
||||||
|
layer.isSelected = layer.id === layerId;
|
||||||
|
}
|
||||||
|
state.selectedLayerId = layerId;
|
||||||
|
};
|
||||||
|
|
||||||
export const controlLayersSlice = createSlice({
|
export const controlLayersSlice = createSlice({
|
||||||
name: 'controlLayers',
|
name: 'controlLayers',
|
||||||
@@ -131,14 +137,7 @@ export const controlLayersSlice = createSlice({
|
|||||||
reducers: {
|
reducers: {
|
||||||
//#region Any Layer Type
|
//#region Any Layer Type
|
||||||
layerSelected: (state, action: PayloadAction<string>) => {
|
layerSelected: (state, action: PayloadAction<string>) => {
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
exclusivelySelectLayer(state, action.payload);
|
||||||
if (layer.id === action.payload) {
|
|
||||||
layer.isSelected = true;
|
|
||||||
state.selectedLayerId = action.payload;
|
|
||||||
} else {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
||||||
const layer = state.layers.find((l) => l.id === action.payload);
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
@@ -167,7 +166,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
||||||
layer.maskObjects = [];
|
layer.maskObjects = [];
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
layer.needsPixelBbox = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -178,7 +176,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
layer.maskObjects = [];
|
layer.maskObjects = [];
|
||||||
layer.bbox = null;
|
layer.bbox = null;
|
||||||
layer.isEnabled = true;
|
layer.isEnabled = true;
|
||||||
layer.needsPixelBbox = false;
|
|
||||||
layer.bboxNeedsUpdate = false;
|
layer.bboxNeedsUpdate = false;
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
}
|
}
|
||||||
@@ -244,17 +241,16 @@ export const controlLayersSlice = createSlice({
|
|||||||
controlAdapter,
|
controlAdapter,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
|
prepare: (controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2) => ({
|
||||||
payload: { layerId: uuidv4(), controlAdapter },
|
payload: { layerId: uuidv4(), controlAdapter },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
caLayerRecalled: (state, action: PayloadAction<ControlAdapterLayer>) => {
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
|
},
|
||||||
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
caLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectCALayerOrThrow(state, layerId);
|
||||||
@@ -346,12 +342,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectCALayerOrThrow(state, layerId);
|
const layer = selectCALayerOrThrow(state, layerId);
|
||||||
layer.controlAdapter.isProcessingImage = isProcessingImage;
|
layer.controlAdapter.isProcessingImage = isProcessingImage;
|
||||||
},
|
},
|
||||||
caLayerControlNetsDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 'controlnet');
|
|
||||||
},
|
|
||||||
caLayerT2IAdaptersDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isControlAdapterLayer(l) || l.controlAdapter.type !== 't2i_adapter');
|
|
||||||
},
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region IP Adapter Layers
|
//#region IP Adapter Layers
|
||||||
@@ -362,12 +352,17 @@ export const controlLayersSlice = createSlice({
|
|||||||
id: getIPALayerId(layerId),
|
id: getIPALayerId(layerId),
|
||||||
type: 'ip_adapter_layer',
|
type: 'ip_adapter_layer',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isSelected: true,
|
||||||
ipAdapter,
|
ipAdapter,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
|
exclusivelySelectLayer(state, layer.id);
|
||||||
},
|
},
|
||||||
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
|
prepare: (ipAdapter: IPAdapterConfigV2) => ({ payload: { layerId: uuidv4(), ipAdapter } }),
|
||||||
},
|
},
|
||||||
|
ipaLayerRecalled: (state, action: PayloadAction<IPAdapterLayer>) => {
|
||||||
|
state.layers.push(action.payload);
|
||||||
|
},
|
||||||
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
ipaLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectIPALayerOrThrow(state, layerId);
|
||||||
@@ -401,9 +396,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectIPALayerOrThrow(state, layerId);
|
const layer = selectIPALayerOrThrow(state, layerId);
|
||||||
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
layer.ipAdapter.clipVisionModel = clipVisionModel;
|
||||||
},
|
},
|
||||||
ipaLayersDeleted: (state) => {
|
|
||||||
state.layers = state.layers.filter((l) => !isIPAdapterLayer(l));
|
|
||||||
},
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region CA or IPA Layers
|
//#region CA or IPA Layers
|
||||||
@@ -445,7 +437,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
autoNegative: 'invert',
|
autoNegative: 'invert',
|
||||||
needsPixelBbox: false,
|
|
||||||
positivePrompt: '',
|
positivePrompt: '',
|
||||||
negativePrompt: null,
|
negativePrompt: null,
|
||||||
ipAdapters: [],
|
ipAdapters: [],
|
||||||
@@ -453,15 +444,14 @@ export const controlLayersSlice = createSlice({
|
|||||||
uploadedMaskImage: null,
|
uploadedMaskImage: null,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
prepare: () => ({ payload: { layerId: uuidv4() } }),
|
||||||
},
|
},
|
||||||
|
rgLayerRecalled: (state, action: PayloadAction<RegionalGuidanceLayer>) => {
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
|
},
|
||||||
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
rgLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
const { layerId, prompt } = action.payload;
|
const { layerId, prompt } = action.payload;
|
||||||
const layer = selectRGLayerOrThrow(state, layerId);
|
const layer = selectRGLayerOrThrow(state, layerId);
|
||||||
@@ -501,9 +491,6 @@ export const controlLayersSlice = createSlice({
|
|||||||
});
|
});
|
||||||
layer.bboxNeedsUpdate = true;
|
layer.bboxNeedsUpdate = true;
|
||||||
layer.uploadedMaskImage = null;
|
layer.uploadedMaskImage = null;
|
||||||
if (!layer.needsPixelBbox && tool === 'eraser') {
|
|
||||||
layer.needsPixelBbox = true;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
||||||
payload: { ...payload, lineUuid: uuidv4() },
|
payload: { ...payload, lineUuid: uuidv4() },
|
||||||
@@ -642,16 +629,17 @@ export const controlLayersSlice = createSlice({
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
|
denoisingStrength: 0.75,
|
||||||
};
|
};
|
||||||
state.layers.push(layer);
|
state.layers.push(layer);
|
||||||
state.selectedLayerId = layer.id;
|
exclusivelySelectLayer(state, layer.id);
|
||||||
for (const layer of state.layers.filter(isRenderableLayer)) {
|
|
||||||
if (layer.id !== layerId) {
|
|
||||||
layer.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: 'initial_image_layer', imageDTO } }),
|
prepare: (imageDTO: ImageDTO | null) => ({ payload: { layerId: INITIAL_IMAGE_LAYER_ID, imageDTO } }),
|
||||||
|
},
|
||||||
|
iiLayerRecalled: (state, action: PayloadAction<InitialImageLayer>) => {
|
||||||
|
state.layers = state.layers.filter((l) => (isInitialImageLayer(l) ? false : true));
|
||||||
|
state.layers.push({ ...action.payload, isSelected: true });
|
||||||
|
exclusivelySelectLayer(state, action.payload.id);
|
||||||
},
|
},
|
||||||
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
iiLayerImageChanged: (state, action: PayloadAction<{ layerId: string; imageDTO: ImageDTO | null }>) => {
|
||||||
const { layerId, imageDTO } = action.payload;
|
const { layerId, imageDTO } = action.payload;
|
||||||
@@ -666,6 +654,11 @@ export const controlLayersSlice = createSlice({
|
|||||||
const layer = selectIILayerOrThrow(state, layerId);
|
const layer = selectIILayerOrThrow(state, layerId);
|
||||||
layer.opacity = opacity;
|
layer.opacity = opacity;
|
||||||
},
|
},
|
||||||
|
iiLayerDenoisingStrengthChanged: (state, action: PayloadAction<{ layerId: string; denoisingStrength: number }>) => {
|
||||||
|
const { layerId, denoisingStrength } = action.payload;
|
||||||
|
const layer = selectIILayerOrThrow(state, layerId);
|
||||||
|
layer.denoisingStrength = denoisingStrength;
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Globals
|
//#region Globals
|
||||||
@@ -799,6 +792,7 @@ export const {
|
|||||||
allLayersDeleted,
|
allLayersDeleted,
|
||||||
// CA Layers
|
// CA Layers
|
||||||
caLayerAdded,
|
caLayerAdded,
|
||||||
|
caLayerRecalled,
|
||||||
caLayerImageChanged,
|
caLayerImageChanged,
|
||||||
caLayerProcessedImageChanged,
|
caLayerProcessedImageChanged,
|
||||||
caLayerModelChanged,
|
caLayerModelChanged,
|
||||||
@@ -807,20 +801,19 @@ export const {
|
|||||||
caLayerIsFilterEnabledChanged,
|
caLayerIsFilterEnabledChanged,
|
||||||
caLayerOpacityChanged,
|
caLayerOpacityChanged,
|
||||||
caLayerIsProcessingImageChanged,
|
caLayerIsProcessingImageChanged,
|
||||||
caLayerControlNetsDeleted,
|
|
||||||
caLayerT2IAdaptersDeleted,
|
|
||||||
// IPA Layers
|
// IPA Layers
|
||||||
ipaLayerAdded,
|
ipaLayerAdded,
|
||||||
|
ipaLayerRecalled,
|
||||||
ipaLayerImageChanged,
|
ipaLayerImageChanged,
|
||||||
ipaLayerMethodChanged,
|
ipaLayerMethodChanged,
|
||||||
ipaLayerModelChanged,
|
ipaLayerModelChanged,
|
||||||
ipaLayerCLIPVisionModelChanged,
|
ipaLayerCLIPVisionModelChanged,
|
||||||
ipaLayersDeleted,
|
|
||||||
// CA or IPA Layers
|
// CA or IPA Layers
|
||||||
caOrIPALayerWeightChanged,
|
caOrIPALayerWeightChanged,
|
||||||
caOrIPALayerBeginEndStepPctChanged,
|
caOrIPALayerBeginEndStepPctChanged,
|
||||||
// RG Layers
|
// RG Layers
|
||||||
rgLayerAdded,
|
rgLayerAdded,
|
||||||
|
rgLayerRecalled,
|
||||||
rgLayerPositivePromptChanged,
|
rgLayerPositivePromptChanged,
|
||||||
rgLayerNegativePromptChanged,
|
rgLayerNegativePromptChanged,
|
||||||
rgLayerPreviewColorChanged,
|
rgLayerPreviewColorChanged,
|
||||||
@@ -839,8 +832,10 @@ export const {
|
|||||||
rgLayerIPAdapterCLIPVisionModelChanged,
|
rgLayerIPAdapterCLIPVisionModelChanged,
|
||||||
// II Layer
|
// II Layer
|
||||||
iiLayerAdded,
|
iiLayerAdded,
|
||||||
|
iiLayerRecalled,
|
||||||
iiLayerImageChanged,
|
iiLayerImageChanged,
|
||||||
iiLayerOpacityChanged,
|
iiLayerOpacityChanged,
|
||||||
|
iiLayerDenoisingStrengthChanged,
|
||||||
// Globals
|
// Globals
|
||||||
positivePromptChanged,
|
positivePromptChanged,
|
||||||
negativePromptChanged,
|
negativePromptChanged,
|
||||||
@@ -860,6 +855,10 @@ export const selectControlLayersSlice = (state: RootState) => state.controlLayer
|
|||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const migrateControlLayersState = (state: any): any => {
|
const migrateControlLayersState = (state: any): any => {
|
||||||
|
if (state._version === 1) {
|
||||||
|
// Reset state for users on v1 (e.g. beta users), some changes could cause
|
||||||
|
return deepClone(initialControlLayersState);
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -886,21 +885,22 @@ export const RG_LAYER_NAME = 'regional_guidance_layer';
|
|||||||
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
|
export const RG_LAYER_LINE_NAME = 'regional_guidance_layer.line';
|
||||||
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
|
export const RG_LAYER_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
|
||||||
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
|
export const RG_LAYER_RECT_NAME = 'regional_guidance_layer.rect';
|
||||||
|
export const INITIAL_IMAGE_LAYER_ID = 'singleton_initial_image_layer';
|
||||||
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
|
export const INITIAL_IMAGE_LAYER_NAME = 'initial_image_layer';
|
||||||
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
|
export const INITIAL_IMAGE_LAYER_IMAGE_NAME = 'initial_image_layer.image';
|
||||||
export const LAYER_BBOX_NAME = 'layer.bbox';
|
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
export const COMPOSITING_RECT_NAME = 'compositing-rect';
|
||||||
|
|
||||||
// Getters for non-singleton layer and object IDs
|
// Getters for non-singleton layer and object IDs
|
||||||
const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
export const getRGLayerId = (layerId: string) => `${RG_LAYER_NAME}_${layerId}`;
|
||||||
const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
const getRGLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||||
const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
const getRGLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
||||||
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
export const getRGLayerObjectGroupId = (layerId: string, groupId: string) => `${layerId}.objectGroup_${groupId}`;
|
||||||
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||||
const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
export const getCALayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
||||||
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
export const getCALayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
export const getIILayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
|
export const getIPALayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
|
||||||
|
|
||||||
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
||||||
name: controlLayersSlice.name,
|
name: controlLayersSlice.name,
|
||||||
|
|||||||
@@ -1,90 +1,119 @@
|
|||||||
import type {
|
import {
|
||||||
ControlNetConfigV2,
|
zControlNetConfigV2,
|
||||||
ImageWithDims,
|
zImageWithDims,
|
||||||
IPAdapterConfigV2,
|
zIPAdapterConfigV2,
|
||||||
T2IAdapterConfigV2,
|
zT2IAdapterConfigV2,
|
||||||
} from 'features/controlLayers/util/controlAdapters';
|
} from 'features/controlLayers/util/controlAdapters';
|
||||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
import type {
|
import {
|
||||||
ParameterAutoNegative,
|
type ParameterHeight,
|
||||||
ParameterHeight,
|
type ParameterNegativePrompt,
|
||||||
ParameterNegativePrompt,
|
type ParameterNegativeStylePromptSDXL,
|
||||||
ParameterNegativeStylePromptSDXL,
|
type ParameterPositivePrompt,
|
||||||
ParameterPositivePrompt,
|
type ParameterPositiveStylePromptSDXL,
|
||||||
ParameterPositiveStylePromptSDXL,
|
type ParameterWidth,
|
||||||
ParameterWidth,
|
zAutoNegative,
|
||||||
|
zParameterNegativePrompt,
|
||||||
|
zParameterPositivePrompt,
|
||||||
|
zParameterStrength,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import { z } from 'zod';
|
||||||
import type { RgbColor } from 'react-colorful';
|
|
||||||
|
|
||||||
export type DrawingTool = 'brush' | 'eraser';
|
const zTool = z.enum(['brush', 'eraser', 'move', 'rect']);
|
||||||
|
export type Tool = z.infer<typeof zTool>;
|
||||||
|
const zDrawingTool = zTool.extract(['brush', 'eraser']);
|
||||||
|
export type DrawingTool = z.infer<typeof zDrawingTool>;
|
||||||
|
|
||||||
export type Tool = DrawingTool | 'move' | 'rect';
|
const zPoints = z.array(z.number()).refine((points) => points.length % 2 === 0, {
|
||||||
|
message: 'Must have an even number of points',
|
||||||
|
});
|
||||||
|
const zVectorMaskLine = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.literal('vector_mask_line'),
|
||||||
|
tool: zDrawingTool,
|
||||||
|
strokeWidth: z.number().min(1),
|
||||||
|
points: zPoints,
|
||||||
|
});
|
||||||
|
export type VectorMaskLine = z.infer<typeof zVectorMaskLine>;
|
||||||
|
|
||||||
export type VectorMaskLine = {
|
const zVectorMaskRect = z.object({
|
||||||
id: string;
|
id: z.string(),
|
||||||
type: 'vector_mask_line';
|
type: z.literal('vector_mask_rect'),
|
||||||
tool: DrawingTool;
|
x: z.number(),
|
||||||
strokeWidth: number;
|
y: z.number(),
|
||||||
points: number[];
|
width: z.number().min(1),
|
||||||
};
|
height: z.number().min(1),
|
||||||
|
});
|
||||||
|
export type VectorMaskRect = z.infer<typeof zVectorMaskRect>;
|
||||||
|
|
||||||
export type VectorMaskRect = {
|
const zLayerBase = z.object({
|
||||||
id: string;
|
id: z.string(),
|
||||||
type: 'vector_mask_rect';
|
isEnabled: z.boolean().default(true),
|
||||||
x: number;
|
isSelected: z.boolean().default(true),
|
||||||
y: number;
|
});
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LayerBase = {
|
const zRect = z.object({
|
||||||
id: string;
|
x: z.number(),
|
||||||
isEnabled: boolean;
|
y: z.number(),
|
||||||
};
|
width: z.number().min(1),
|
||||||
|
height: z.number().min(1),
|
||||||
|
});
|
||||||
|
const zRenderableLayerBase = zLayerBase.extend({
|
||||||
|
x: z.number(),
|
||||||
|
y: z.number(),
|
||||||
|
bbox: zRect.nullable(),
|
||||||
|
bboxNeedsUpdate: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
type RenderableLayerBase = LayerBase & {
|
const zControlAdapterLayer = zRenderableLayerBase.extend({
|
||||||
x: number;
|
type: z.literal('control_adapter_layer'),
|
||||||
y: number;
|
opacity: z.number().gte(0).lte(1),
|
||||||
bbox: IRect | null;
|
isFilterEnabled: z.boolean(),
|
||||||
bboxNeedsUpdate: boolean;
|
controlAdapter: z.discriminatedUnion('type', [zControlNetConfigV2, zT2IAdapterConfigV2]),
|
||||||
isSelected: boolean;
|
});
|
||||||
};
|
export type ControlAdapterLayer = z.infer<typeof zControlAdapterLayer>;
|
||||||
|
|
||||||
export type ControlAdapterLayer = RenderableLayerBase & {
|
const zIPAdapterLayer = zLayerBase.extend({
|
||||||
type: 'control_adapter_layer'; // technically, also t2i adapter layer
|
type: z.literal('ip_adapter_layer'),
|
||||||
opacity: number;
|
ipAdapter: zIPAdapterConfigV2,
|
||||||
isFilterEnabled: boolean;
|
});
|
||||||
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2;
|
export type IPAdapterLayer = z.infer<typeof zIPAdapterLayer>;
|
||||||
};
|
|
||||||
|
|
||||||
export type IPAdapterLayer = LayerBase & {
|
const zRgbColor = z.object({
|
||||||
type: 'ip_adapter_layer';
|
r: z.number().int().min(0).max(255),
|
||||||
ipAdapter: IPAdapterConfigV2;
|
g: z.number().int().min(0).max(255),
|
||||||
};
|
b: z.number().int().min(0).max(255),
|
||||||
|
});
|
||||||
|
const zRegionalGuidanceLayer = zRenderableLayerBase.extend({
|
||||||
|
type: z.literal('regional_guidance_layer'),
|
||||||
|
maskObjects: z.array(z.discriminatedUnion('type', [zVectorMaskLine, zVectorMaskRect])),
|
||||||
|
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||||
|
negativePrompt: zParameterNegativePrompt.nullable(),
|
||||||
|
ipAdapters: z.array(zIPAdapterConfigV2),
|
||||||
|
previewColor: zRgbColor,
|
||||||
|
autoNegative: zAutoNegative,
|
||||||
|
uploadedMaskImage: zImageWithDims.nullable(),
|
||||||
|
});
|
||||||
|
export type RegionalGuidanceLayer = z.infer<typeof zRegionalGuidanceLayer>;
|
||||||
|
|
||||||
export type RegionalGuidanceLayer = RenderableLayerBase & {
|
const zInitialImageLayer = zRenderableLayerBase.extend({
|
||||||
type: 'regional_guidance_layer';
|
type: z.literal('initial_image_layer'),
|
||||||
maskObjects: (VectorMaskLine | VectorMaskRect)[];
|
opacity: z.number().gte(0).lte(1),
|
||||||
positivePrompt: ParameterPositivePrompt | null;
|
image: zImageWithDims.nullable(),
|
||||||
negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask
|
denoisingStrength: zParameterStrength,
|
||||||
ipAdapters: IPAdapterConfigV2[]; // Any number of image prompts
|
});
|
||||||
previewColor: RgbColor;
|
export type InitialImageLayer = z.infer<typeof zInitialImageLayer>;
|
||||||
autoNegative: ParameterAutoNegative;
|
|
||||||
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
|
||||||
uploadedMaskImage: ImageWithDims | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InitialImageLayer = RenderableLayerBase & {
|
export const zLayer = z.discriminatedUnion('type', [
|
||||||
type: 'initial_image_layer';
|
zRegionalGuidanceLayer,
|
||||||
opacity: number;
|
zControlAdapterLayer,
|
||||||
image: ImageWithDims | null;
|
zIPAdapterLayer,
|
||||||
};
|
zInitialImageLayer,
|
||||||
|
]);
|
||||||
export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer | InitialImageLayer;
|
export type Layer = z.infer<typeof zLayer>;
|
||||||
|
|
||||||
export type ControlLayersState = {
|
export type ControlLayersState = {
|
||||||
_version: 1;
|
_version: 2;
|
||||||
selectedLayerId: string | null;
|
selectedLayerId: string | null;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
brushSize: number;
|
brushSize: number;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
|||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice';
|
import { RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { Layer as KonvaLayerType } from 'konva/lib/Layer';
|
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@@ -54,34 +53,30 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
||||||
* @param layer The konva layer to get the bounding box of.
|
* to be captured, manipulated or analyzed without interference from other layers.
|
||||||
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
* @param layer The konva layer to clone.
|
||||||
|
* @returns The cloned stage and layer.
|
||||||
*/
|
*/
|
||||||
export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = false): IRect | null => {
|
const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; layerClone: Konva.Layer } => {
|
||||||
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
|
||||||
//
|
|
||||||
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
|
||||||
// by calculating the extents of individual shapes from their "vector" shape data.
|
|
||||||
//
|
|
||||||
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
|
||||||
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
|
||||||
const stage = layer.getStage();
|
const stage = layer.getStage();
|
||||||
|
|
||||||
// Construct and offscreen canvas on which we will do the bbox calculations.
|
// Construct an offscreen canvas with the same dimensions as the layer's stage.
|
||||||
const offscreenStageContainer = document.createElement('div');
|
const offscreenStageContainer = document.createElement('div');
|
||||||
const offscreenStage = new Konva.Stage({
|
const stageClone = new Konva.Stage({
|
||||||
container: offscreenStageContainer,
|
container: offscreenStageContainer,
|
||||||
|
x: stage.x(),
|
||||||
|
y: stage.y(),
|
||||||
width: stage.width(),
|
width: stage.width(),
|
||||||
height: stage.height(),
|
height: stage.height(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clone the layer and filter out unwanted children.
|
// Clone the layer and filter out unwanted children.
|
||||||
const layerClone = layer.clone();
|
const layerClone = layer.clone();
|
||||||
offscreenStage.add(layerClone);
|
stageClone.add(layerClone);
|
||||||
|
|
||||||
for (const child of layerClone.getChildren()) {
|
for (const child of layerClone.getChildren()) {
|
||||||
if (child.name() === RG_LAYER_OBJECT_GROUP_NAME) {
|
if (child.name() === RG_LAYER_OBJECT_GROUP_NAME && child.hasChildren()) {
|
||||||
// We need to cache the group to ensure it composites out eraser strokes correctly
|
// We need to cache the group to ensure it composites out eraser strokes correctly
|
||||||
child.opacity(1);
|
child.opacity(1);
|
||||||
child.cache();
|
child.cache();
|
||||||
@@ -91,11 +86,31 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { stageClone, layerClone };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the bounding box of a regional prompt konva layer. This function has special handling for regional prompt layers.
|
||||||
|
* @param layer The konva layer to get the bounding box of.
|
||||||
|
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
||||||
|
*/
|
||||||
|
export const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => {
|
||||||
|
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
||||||
|
//
|
||||||
|
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
||||||
|
// by calculating the extents of individual shapes from their "vector" shape data.
|
||||||
|
//
|
||||||
|
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
||||||
|
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
||||||
|
const { stageClone, layerClone } = getIsolatedRGLayerClone(layer);
|
||||||
|
|
||||||
// Get a worst-case rect using the relatively fast `getClientRect`.
|
// Get a worst-case rect using the relatively fast `getClientRect`.
|
||||||
const layerRect = layerClone.getClientRect();
|
const layerRect = layerClone.getClientRect();
|
||||||
|
if (layerRect.width === 0 || layerRect.height === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Capture the image data with the above rect.
|
// Capture the image data with the above rect.
|
||||||
const layerImageData = offscreenStage
|
const layerImageData = stageClone
|
||||||
.toCanvas(layerRect)
|
.toCanvas(layerRect)
|
||||||
.getContext('2d')
|
.getContext('2d')
|
||||||
?.getImageData(0, 0, layerRect.width, layerRect.height);
|
?.getImageData(0, 0, layerRect.width, layerRect.height);
|
||||||
@@ -114,8 +129,8 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
|
|
||||||
// Correct the bounding box to be relative to the layer's position.
|
// Correct the bounding box to be relative to the layer's position.
|
||||||
const correctedLayerBbox = {
|
const correctedLayerBbox = {
|
||||||
x: layerBbox.minX - Math.floor(stage.x()) + layerRect.x - Math.floor(layer.x()),
|
x: layerBbox.minX - Math.floor(stageClone.x()) + layerRect.x - Math.floor(layer.x()),
|
||||||
y: layerBbox.minY - Math.floor(stage.y()) + layerRect.y - Math.floor(layer.y()),
|
y: layerBbox.minY - Math.floor(stageClone.y()) + layerRect.y - Math.floor(layer.y()),
|
||||||
width: layerBbox.maxX - layerBbox.minX,
|
width: layerBbox.maxX - layerBbox.minX,
|
||||||
height: layerBbox.maxY - layerBbox.minY,
|
height: layerBbox.maxY - layerBbox.minY,
|
||||||
};
|
};
|
||||||
@@ -123,7 +138,13 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
return correctedLayerBbox;
|
return correctedLayerBbox;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLayerBboxFast = (layer: KonvaLayerType): IRect => {
|
/**
|
||||||
|
* Get the bounding box of a konva layer. This function is faster than `getLayerBboxPixels` but less accurate. It
|
||||||
|
* should only be used when there are no eraser strokes or shapes in the layer.
|
||||||
|
* @param layer The konva layer to get the bounding box of.
|
||||||
|
* @returns The bounding box of the layer.
|
||||||
|
*/
|
||||||
|
export const getLayerBboxFast = (layer: Konva.Layer): IRect => {
|
||||||
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
const bbox = layer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||||
return {
|
return {
|
||||||
x: Math.floor(bbox.x),
|
x: Math.floor(bbox.x),
|
||||||
|
|||||||
@@ -4,20 +4,74 @@ import { assert } from 'tsafe';
|
|||||||
import { describe, test } from 'vitest';
|
import { describe, test } from 'vitest';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
_CannyProcessorConfig,
|
||||||
|
_ColorMapProcessorConfig,
|
||||||
|
_ContentShuffleProcessorConfig,
|
||||||
|
_DepthAnythingProcessorConfig,
|
||||||
|
_DWOpenposeProcessorConfig,
|
||||||
|
_HedProcessorConfig,
|
||||||
|
_LineartAnimeProcessorConfig,
|
||||||
|
_LineartProcessorConfig,
|
||||||
|
_MediapipeFaceProcessorConfig,
|
||||||
|
_MidasDepthProcessorConfig,
|
||||||
|
_MlsdProcessorConfig,
|
||||||
|
_NormalbaeProcessorConfig,
|
||||||
|
_PidiProcessorConfig,
|
||||||
|
_ZoeDepthProcessorConfig,
|
||||||
|
CannyProcessorConfig,
|
||||||
CLIPVisionModelV2,
|
CLIPVisionModelV2,
|
||||||
|
ColorMapProcessorConfig,
|
||||||
|
ContentShuffleProcessorConfig,
|
||||||
ControlModeV2,
|
ControlModeV2,
|
||||||
DepthAnythingModelSize,
|
DepthAnythingModelSize,
|
||||||
|
DepthAnythingProcessorConfig,
|
||||||
|
DWOpenposeProcessorConfig,
|
||||||
|
HedProcessorConfig,
|
||||||
IPMethodV2,
|
IPMethodV2,
|
||||||
|
LineartAnimeProcessorConfig,
|
||||||
|
LineartProcessorConfig,
|
||||||
|
MediapipeFaceProcessorConfig,
|
||||||
|
MidasDepthProcessorConfig,
|
||||||
|
MlsdProcessorConfig,
|
||||||
|
NormalbaeProcessorConfig,
|
||||||
|
PidiProcessorConfig,
|
||||||
ProcessorConfig,
|
ProcessorConfig,
|
||||||
ProcessorTypeV2,
|
ProcessorTypeV2,
|
||||||
|
ZoeDepthProcessorConfig,
|
||||||
} from './controlAdapters';
|
} from './controlAdapters';
|
||||||
|
|
||||||
describe('Control Adapter Types', () => {
|
describe('Control Adapter Types', () => {
|
||||||
test('ProcessorType', () => assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>());
|
test('ProcessorType', () => {
|
||||||
test('IP Adapter Method', () => assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>());
|
assert<Equals<ProcessorConfig['type'], ProcessorTypeV2>>();
|
||||||
test('CLIP Vision Model', () =>
|
});
|
||||||
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>());
|
test('IP Adapter Method', () => {
|
||||||
test('Control Mode', () => assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>());
|
assert<Equals<NonNullable<S['IPAdapterInvocation']['method']>, IPMethodV2>>();
|
||||||
test('DepthAnything Model Size', () =>
|
});
|
||||||
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>());
|
test('CLIP Vision Model', () => {
|
||||||
|
assert<Equals<NonNullable<S['IPAdapterInvocation']['clip_vision_model']>, CLIPVisionModelV2>>();
|
||||||
|
});
|
||||||
|
test('Control Mode', () => {
|
||||||
|
assert<Equals<NonNullable<S['ControlNetInvocation']['control_mode']>, ControlModeV2>>();
|
||||||
|
});
|
||||||
|
test('DepthAnything Model Size', () => {
|
||||||
|
assert<Equals<NonNullable<S['DepthAnythingImageProcessorInvocation']['model_size']>, DepthAnythingModelSize>>();
|
||||||
|
});
|
||||||
|
test('Processor Configs', () => {
|
||||||
|
// The processor configs are manually modeled zod schemas. This test ensures that the inferred types are correct.
|
||||||
|
// The types prefixed with `_` are types generated from OpenAPI, while the types without the prefix are manually modeled.
|
||||||
|
assert<Equals<_CannyProcessorConfig, CannyProcessorConfig>>();
|
||||||
|
assert<Equals<_ColorMapProcessorConfig, ColorMapProcessorConfig>>();
|
||||||
|
assert<Equals<_ContentShuffleProcessorConfig, ContentShuffleProcessorConfig>>();
|
||||||
|
assert<Equals<_DepthAnythingProcessorConfig, DepthAnythingProcessorConfig>>();
|
||||||
|
assert<Equals<_HedProcessorConfig, HedProcessorConfig>>();
|
||||||
|
assert<Equals<_LineartAnimeProcessorConfig, LineartAnimeProcessorConfig>>();
|
||||||
|
assert<Equals<_LineartProcessorConfig, LineartProcessorConfig>>();
|
||||||
|
assert<Equals<_MediapipeFaceProcessorConfig, MediapipeFaceProcessorConfig>>();
|
||||||
|
assert<Equals<_MidasDepthProcessorConfig, MidasDepthProcessorConfig>>();
|
||||||
|
assert<Equals<_MlsdProcessorConfig, MlsdProcessorConfig>>();
|
||||||
|
assert<Equals<_NormalbaeProcessorConfig, NormalbaeProcessorConfig>>();
|
||||||
|
assert<Equals<_DWOpenposeProcessorConfig, DWOpenposeProcessorConfig>>();
|
||||||
|
assert<Equals<_PidiProcessorConfig, PidiProcessorConfig>>();
|
||||||
|
assert<Equals<_ZoeDepthProcessorConfig, ZoeDepthProcessorConfig>>();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { deepClone } from 'common/util/deepClone';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import type {
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
ParameterControlNetModel,
|
|
||||||
ParameterIPAdapterModel,
|
|
||||||
ParameterT2IAdapterModel,
|
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
|
||||||
import { merge, omit } from 'lodash-es';
|
import { merge, omit } from 'lodash-es';
|
||||||
import type {
|
import type {
|
||||||
BaseModelType,
|
BaseModelType,
|
||||||
@@ -28,90 +24,201 @@ import type {
|
|||||||
} from 'services/api/types';
|
} from 'services/api/types';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const zId = z.string().min(1);
|
||||||
|
|
||||||
|
const zCannyProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('canny_image_processor'),
|
||||||
|
low_threshold: z.number().int().gte(0).lte(255),
|
||||||
|
high_threshold: z.number().int().gte(0).lte(255),
|
||||||
|
});
|
||||||
|
export type _CannyProcessorConfig = Required<
|
||||||
|
Pick<CannyImageProcessorInvocation, 'id' | 'type' | 'low_threshold' | 'high_threshold'>
|
||||||
|
>;
|
||||||
|
export type CannyProcessorConfig = z.infer<typeof zCannyProcessorConfig>;
|
||||||
|
|
||||||
|
const zColorMapProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('color_map_image_processor'),
|
||||||
|
color_map_tile_size: z.number().int().gte(1),
|
||||||
|
});
|
||||||
|
export type _ColorMapProcessorConfig = Required<
|
||||||
|
Pick<ColorMapImageProcessorInvocation, 'id' | 'type' | 'color_map_tile_size'>
|
||||||
|
>;
|
||||||
|
export type ColorMapProcessorConfig = z.infer<typeof zColorMapProcessorConfig>;
|
||||||
|
|
||||||
|
const zContentShuffleProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('content_shuffle_image_processor'),
|
||||||
|
w: z.number().int().gte(0),
|
||||||
|
h: z.number().int().gte(0),
|
||||||
|
f: z.number().int().gte(0),
|
||||||
|
});
|
||||||
|
export type _ContentShuffleProcessorConfig = Required<
|
||||||
|
Pick<ContentShuffleImageProcessorInvocation, 'id' | 'type' | 'w' | 'h' | 'f'>
|
||||||
|
>;
|
||||||
|
export type ContentShuffleProcessorConfig = z.infer<typeof zContentShuffleProcessorConfig>;
|
||||||
|
|
||||||
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
const zDepthAnythingModelSize = z.enum(['large', 'base', 'small']);
|
||||||
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
|
export type DepthAnythingModelSize = z.infer<typeof zDepthAnythingModelSize>;
|
||||||
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
|
export const isDepthAnythingModelSize = (v: unknown): v is DepthAnythingModelSize =>
|
||||||
zDepthAnythingModelSize.safeParse(v).success;
|
zDepthAnythingModelSize.safeParse(v).success;
|
||||||
|
const zDepthAnythingProcessorConfig = z.object({
|
||||||
export type CannyProcessorConfig = Required<
|
id: zId,
|
||||||
Pick<CannyImageProcessorInvocation, 'id' | 'type' | 'low_threshold' | 'high_threshold'>
|
type: z.literal('depth_anything_image_processor'),
|
||||||
>;
|
model_size: zDepthAnythingModelSize,
|
||||||
export type ColorMapProcessorConfig = Required<
|
});
|
||||||
Pick<ColorMapImageProcessorInvocation, 'id' | 'type' | 'color_map_tile_size'>
|
export type _DepthAnythingProcessorConfig = Required<
|
||||||
>;
|
|
||||||
export type ContentShuffleProcessorConfig = Required<
|
|
||||||
Pick<ContentShuffleImageProcessorInvocation, 'id' | 'type' | 'w' | 'h' | 'f'>
|
|
||||||
>;
|
|
||||||
export type DepthAnythingProcessorConfig = Required<
|
|
||||||
Pick<DepthAnythingImageProcessorInvocation, 'id' | 'type' | 'model_size'>
|
Pick<DepthAnythingImageProcessorInvocation, 'id' | 'type' | 'model_size'>
|
||||||
>;
|
>;
|
||||||
export type HedProcessorConfig = Required<Pick<HedImageProcessorInvocation, 'id' | 'type' | 'scribble'>>;
|
export type DepthAnythingProcessorConfig = z.infer<typeof zDepthAnythingProcessorConfig>;
|
||||||
type LineartAnimeProcessorConfig = Required<Pick<LineartAnimeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type LineartProcessorConfig = Required<Pick<LineartImageProcessorInvocation, 'id' | 'type' | 'coarse'>>;
|
const zHedProcessorConfig = z.object({
|
||||||
export type MediapipeFaceProcessorConfig = Required<
|
id: zId,
|
||||||
|
type: z.literal('hed_image_processor'),
|
||||||
|
scribble: z.boolean(),
|
||||||
|
});
|
||||||
|
export type _HedProcessorConfig = Required<Pick<HedImageProcessorInvocation, 'id' | 'type' | 'scribble'>>;
|
||||||
|
export type HedProcessorConfig = z.infer<typeof zHedProcessorConfig>;
|
||||||
|
|
||||||
|
const zLineartAnimeProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('lineart_anime_image_processor'),
|
||||||
|
});
|
||||||
|
export type _LineartAnimeProcessorConfig = Required<Pick<LineartAnimeImageProcessorInvocation, 'id' | 'type'>>;
|
||||||
|
export type LineartAnimeProcessorConfig = z.infer<typeof zLineartAnimeProcessorConfig>;
|
||||||
|
|
||||||
|
const zLineartProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('lineart_image_processor'),
|
||||||
|
coarse: z.boolean(),
|
||||||
|
});
|
||||||
|
export type _LineartProcessorConfig = Required<Pick<LineartImageProcessorInvocation, 'id' | 'type' | 'coarse'>>;
|
||||||
|
export type LineartProcessorConfig = z.infer<typeof zLineartProcessorConfig>;
|
||||||
|
|
||||||
|
const zMediapipeFaceProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('mediapipe_face_processor'),
|
||||||
|
max_faces: z.number().int().gte(1),
|
||||||
|
min_confidence: z.number().gte(0).lte(1),
|
||||||
|
});
|
||||||
|
export type _MediapipeFaceProcessorConfig = Required<
|
||||||
Pick<MediapipeFaceProcessorInvocation, 'id' | 'type' | 'max_faces' | 'min_confidence'>
|
Pick<MediapipeFaceProcessorInvocation, 'id' | 'type' | 'max_faces' | 'min_confidence'>
|
||||||
>;
|
>;
|
||||||
export type MidasDepthProcessorConfig = Required<
|
export type MediapipeFaceProcessorConfig = z.infer<typeof zMediapipeFaceProcessorConfig>;
|
||||||
|
|
||||||
|
const zMidasDepthProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('midas_depth_image_processor'),
|
||||||
|
a_mult: z.number().gte(0),
|
||||||
|
bg_th: z.number().gte(0),
|
||||||
|
});
|
||||||
|
export type _MidasDepthProcessorConfig = Required<
|
||||||
Pick<MidasDepthImageProcessorInvocation, 'id' | 'type' | 'a_mult' | 'bg_th'>
|
Pick<MidasDepthImageProcessorInvocation, 'id' | 'type' | 'a_mult' | 'bg_th'>
|
||||||
>;
|
>;
|
||||||
export type MlsdProcessorConfig = Required<Pick<MlsdImageProcessorInvocation, 'id' | 'type' | 'thr_v' | 'thr_d'>>;
|
export type MidasDepthProcessorConfig = z.infer<typeof zMidasDepthProcessorConfig>;
|
||||||
type NormalbaeProcessorConfig = Required<Pick<NormalbaeImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
export type DWOpenposeProcessorConfig = Required<
|
const zMlsdProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('mlsd_image_processor'),
|
||||||
|
thr_v: z.number().gte(0),
|
||||||
|
thr_d: z.number().gte(0),
|
||||||
|
});
|
||||||
|
export type _MlsdProcessorConfig = Required<Pick<MlsdImageProcessorInvocation, 'id' | 'type' | 'thr_v' | 'thr_d'>>;
|
||||||
|
export type MlsdProcessorConfig = z.infer<typeof zMlsdProcessorConfig>;
|
||||||
|
|
||||||
|
const zNormalbaeProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('normalbae_image_processor'),
|
||||||
|
});
|
||||||
|
export type _NormalbaeProcessorConfig = Required<Pick<NormalbaeImageProcessorInvocation, 'id' | 'type'>>;
|
||||||
|
export type NormalbaeProcessorConfig = z.infer<typeof zNormalbaeProcessorConfig>;
|
||||||
|
|
||||||
|
const zDWOpenposeProcessorConfig = z.object({
|
||||||
|
id: zId,
|
||||||
|
type: z.literal('dw_openpose_image_processor'),
|
||||||
|
draw_body: z.boolean(),
|
||||||
|
draw_face: z.boolean(),
|
||||||
|
draw_hands: z.boolean(),
|
||||||
|
});
|
||||||
|
export type _DWOpenposeProcessorConfig = Required<
|
||||||
Pick<DWOpenposeImageProcessorInvocation, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
|
Pick<DWOpenposeImageProcessorInvocation, 'id' | 'type' | 'draw_body' | 'draw_face' | 'draw_hands'>
|
||||||
>;
|
>;
|
||||||
export type PidiProcessorConfig = Required<Pick<PidiImageProcessorInvocation, 'id' | 'type' | 'safe' | 'scribble'>>;
|
export type DWOpenposeProcessorConfig = z.infer<typeof zDWOpenposeProcessorConfig>;
|
||||||
type ZoeDepthProcessorConfig = Required<Pick<ZoeDepthImageProcessorInvocation, 'id' | 'type'>>;
|
|
||||||
|
|
||||||
export type ProcessorConfig =
|
const zPidiProcessorConfig = z.object({
|
||||||
| CannyProcessorConfig
|
id: zId,
|
||||||
| ColorMapProcessorConfig
|
type: z.literal('pidi_image_processor'),
|
||||||
| ContentShuffleProcessorConfig
|
safe: z.boolean(),
|
||||||
| DepthAnythingProcessorConfig
|
scribble: z.boolean(),
|
||||||
| HedProcessorConfig
|
});
|
||||||
| LineartAnimeProcessorConfig
|
export type _PidiProcessorConfig = Required<Pick<PidiImageProcessorInvocation, 'id' | 'type' | 'safe' | 'scribble'>>;
|
||||||
| LineartProcessorConfig
|
export type PidiProcessorConfig = z.infer<typeof zPidiProcessorConfig>;
|
||||||
| MediapipeFaceProcessorConfig
|
|
||||||
| MidasDepthProcessorConfig
|
|
||||||
| MlsdProcessorConfig
|
|
||||||
| NormalbaeProcessorConfig
|
|
||||||
| DWOpenposeProcessorConfig
|
|
||||||
| PidiProcessorConfig
|
|
||||||
| ZoeDepthProcessorConfig;
|
|
||||||
|
|
||||||
export type ImageWithDims = {
|
const zZoeDepthProcessorConfig = z.object({
|
||||||
imageName: string;
|
id: zId,
|
||||||
width: number;
|
type: z.literal('zoe_depth_image_processor'),
|
||||||
height: number;
|
});
|
||||||
};
|
export type _ZoeDepthProcessorConfig = Required<Pick<ZoeDepthImageProcessorInvocation, 'id' | 'type'>>;
|
||||||
|
export type ZoeDepthProcessorConfig = z.infer<typeof zZoeDepthProcessorConfig>;
|
||||||
|
|
||||||
type ControlAdapterBase = {
|
const zProcessorConfig = z.discriminatedUnion('type', [
|
||||||
id: string;
|
zCannyProcessorConfig,
|
||||||
weight: number;
|
zColorMapProcessorConfig,
|
||||||
image: ImageWithDims | null;
|
zContentShuffleProcessorConfig,
|
||||||
processedImage: ImageWithDims | null;
|
zDepthAnythingProcessorConfig,
|
||||||
isProcessingImage: boolean;
|
zHedProcessorConfig,
|
||||||
processorConfig: ProcessorConfig | null;
|
zLineartAnimeProcessorConfig,
|
||||||
beginEndStepPct: [number, number];
|
zLineartProcessorConfig,
|
||||||
};
|
zMediapipeFaceProcessorConfig,
|
||||||
|
zMidasDepthProcessorConfig,
|
||||||
|
zMlsdProcessorConfig,
|
||||||
|
zNormalbaeProcessorConfig,
|
||||||
|
zDWOpenposeProcessorConfig,
|
||||||
|
zPidiProcessorConfig,
|
||||||
|
zZoeDepthProcessorConfig,
|
||||||
|
]);
|
||||||
|
export type ProcessorConfig = z.infer<typeof zProcessorConfig>;
|
||||||
|
|
||||||
|
export const zImageWithDims = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
width: z.number().int().positive(),
|
||||||
|
height: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
export type ImageWithDims = z.infer<typeof zImageWithDims>;
|
||||||
|
|
||||||
|
const zBeginEndStepPct = z
|
||||||
|
.tuple([z.number().gte(0).lte(1), z.number().gte(0).lte(1)])
|
||||||
|
.refine(([begin, end]) => begin < end, {
|
||||||
|
message: 'Begin must be less than end',
|
||||||
|
});
|
||||||
|
|
||||||
|
const zControlAdapterBase = z.object({
|
||||||
|
id: zId,
|
||||||
|
weight: z.number().gte(0).lte(1),
|
||||||
|
image: zImageWithDims.nullable(),
|
||||||
|
processedImage: zImageWithDims.nullable(),
|
||||||
|
isProcessingImage: z.boolean(),
|
||||||
|
processorConfig: zProcessorConfig.nullable(),
|
||||||
|
beginEndStepPct: zBeginEndStepPct,
|
||||||
|
});
|
||||||
|
|
||||||
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
|
const zControlModeV2 = z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']);
|
||||||
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
|
export type ControlModeV2 = z.infer<typeof zControlModeV2>;
|
||||||
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
|
export const isControlModeV2 = (v: unknown): v is ControlModeV2 => zControlModeV2.safeParse(v).success;
|
||||||
|
|
||||||
export type ControlNetConfigV2 = ControlAdapterBase & {
|
export const zControlNetConfigV2 = zControlAdapterBase.extend({
|
||||||
type: 'controlnet';
|
type: z.literal('controlnet'),
|
||||||
model: ParameterControlNetModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
controlMode: ControlModeV2;
|
controlMode: zControlModeV2,
|
||||||
};
|
});
|
||||||
export const isControlNetConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is ControlNetConfigV2 =>
|
export type ControlNetConfigV2 = z.infer<typeof zControlNetConfigV2>;
|
||||||
ca.type === 'controlnet';
|
|
||||||
|
|
||||||
export type T2IAdapterConfigV2 = ControlAdapterBase & {
|
export const zT2IAdapterConfigV2 = zControlAdapterBase.extend({
|
||||||
type: 't2i_adapter';
|
type: z.literal('t2i_adapter'),
|
||||||
model: ParameterT2IAdapterModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
};
|
});
|
||||||
export const isT2IAdapterConfigV2 = (ca: ControlNetConfigV2 | T2IAdapterConfigV2): ca is T2IAdapterConfigV2 =>
|
export type T2IAdapterConfigV2 = z.infer<typeof zT2IAdapterConfigV2>;
|
||||||
ca.type === 't2i_adapter';
|
|
||||||
|
|
||||||
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
|
const zCLIPVisionModelV2 = z.enum(['ViT-H', 'ViT-G']);
|
||||||
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
|
export type CLIPVisionModelV2 = z.infer<typeof zCLIPVisionModelV2>;
|
||||||
@@ -121,16 +228,17 @@ const zIPMethodV2 = z.enum(['full', 'style', 'composition']);
|
|||||||
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
|
export type IPMethodV2 = z.infer<typeof zIPMethodV2>;
|
||||||
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
|
export const isIPMethodV2 = (v: unknown): v is IPMethodV2 => zIPMethodV2.safeParse(v).success;
|
||||||
|
|
||||||
export type IPAdapterConfigV2 = {
|
export const zIPAdapterConfigV2 = z.object({
|
||||||
id: string;
|
id: zId,
|
||||||
type: 'ip_adapter';
|
type: z.literal('ip_adapter'),
|
||||||
weight: number;
|
weight: z.number().gte(0).lte(1),
|
||||||
method: IPMethodV2;
|
method: zIPMethodV2,
|
||||||
image: ImageWithDims | null;
|
image: zImageWithDims.nullable(),
|
||||||
model: ParameterIPAdapterModel | null;
|
model: zModelIdentifierField.nullable(),
|
||||||
clipVisionModel: CLIPVisionModelV2;
|
clipVisionModel: zCLIPVisionModelV2,
|
||||||
beginEndStepPct: [number, number];
|
beginEndStepPct: zBeginEndStepPct,
|
||||||
};
|
});
|
||||||
|
export type IPAdapterConfigV2 = z.infer<typeof zIPAdapterConfigV2>;
|
||||||
|
|
||||||
const zProcessorTypeV2 = z.enum([
|
const zProcessorTypeV2 = z.enum([
|
||||||
'canny_image_processor',
|
'canny_image_processor',
|
||||||
@@ -190,7 +298,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
type: 'canny_image_processor',
|
type: 'canny_image_processor',
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -207,7 +315,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
type: 'color_map_image_processor',
|
type: 'color_map_image_processor',
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
content_shuffle_image_processor: {
|
content_shuffle_image_processor: {
|
||||||
@@ -223,7 +331,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -239,7 +347,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
resolution: minDim(image),
|
resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -254,7 +362,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -269,7 +377,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -285,7 +393,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -302,7 +410,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -319,7 +427,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -336,7 +444,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -351,7 +459,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -369,7 +477,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -385,7 +493,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
detect_resolution: minDim(image),
|
detect_resolution: minDim(image),
|
||||||
image_resolution: minDim(image),
|
image_resolution: minDim(image),
|
||||||
}),
|
}),
|
||||||
@@ -400,7 +508,7 @@ export const CA_PROCESSOR_DATA: CAProcessorsData = {
|
|||||||
}),
|
}),
|
||||||
buildNode: (image, config) => ({
|
buildNode: (image, config) => ({
|
||||||
...config,
|
...config,
|
||||||
image: { image_name: image.imageName },
|
image: { image_name: image.name },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -462,7 +570,7 @@ export const buildControlAdapterProcessorV2 = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||||
imageName: image_name,
|
name: image_name,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -437,8 +437,8 @@ const renderRegionalGuidanceLayer = (
|
|||||||
konvaObjectGroup.opacity(1);
|
konvaObjectGroup.opacity(1);
|
||||||
|
|
||||||
compositingRect.setAttrs({
|
compositingRect.setAttrs({
|
||||||
// The rect should be the size of the layer - use the fast method bc it's OK if the rect is larger
|
// The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already
|
||||||
...getLayerBboxFast(konvaLayer),
|
...(!reduxLayer.bboxNeedsUpdate && reduxLayer.bbox ? reduxLayer.bbox : getLayerBboxFast(konvaLayer)),
|
||||||
fill: rgbColor,
|
fill: rgbColor,
|
||||||
opacity: globalMaskLayerOpacity,
|
opacity: globalMaskLayerOpacity,
|
||||||
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
// Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes)
|
||||||
@@ -483,6 +483,9 @@ const updateInitialImageLayerImageAttrs = (
|
|||||||
konvaImage: Konva.Image,
|
konvaImage: Konva.Image,
|
||||||
reduxLayer: InitialImageLayer
|
reduxLayer: InitialImageLayer
|
||||||
) => {
|
) => {
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
const newWidth = stage.width() / stage.scaleX();
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
const newHeight = stage.height() / stage.scaleY();
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
if (
|
if (
|
||||||
@@ -510,7 +513,7 @@ const updateInitialImageLayerImageSource = async (
|
|||||||
reduxLayer: InitialImageLayer
|
reduxLayer: InitialImageLayer
|
||||||
) => {
|
) => {
|
||||||
if (reduxLayer.image) {
|
if (reduxLayer.image) {
|
||||||
const { imageName } = reduxLayer.image;
|
const imageName = reduxLayer.image.name;
|
||||||
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
||||||
const imageDTO = await req.unwrap();
|
const imageDTO = await req.unwrap();
|
||||||
req.unsubscribe();
|
req.unsubscribe();
|
||||||
@@ -543,7 +546,7 @@ const renderInitialImageLayer = (stage: Konva.Stage, reduxLayer: InitialImageLay
|
|||||||
let imageSourceNeedsUpdate = false;
|
let imageSourceNeedsUpdate = false;
|
||||||
if (canvasImageSource instanceof HTMLImageElement) {
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
const image = reduxLayer.image;
|
const image = reduxLayer.image;
|
||||||
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) {
|
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
} else if (!image) {
|
} else if (!image) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
@@ -585,7 +588,7 @@ const updateControlNetLayerImageSource = async (
|
|||||||
) => {
|
) => {
|
||||||
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
||||||
if (image) {
|
if (image) {
|
||||||
const { imageName } = image;
|
const imageName = image.name;
|
||||||
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
||||||
const imageDTO = await req.unwrap();
|
const imageDTO = await req.unwrap();
|
||||||
req.unsubscribe();
|
req.unsubscribe();
|
||||||
@@ -618,6 +621,9 @@ const updateControlNetLayerImageAttrs = (
|
|||||||
reduxLayer: ControlAdapterLayer
|
reduxLayer: ControlAdapterLayer
|
||||||
) => {
|
) => {
|
||||||
let needsCache = false;
|
let needsCache = false;
|
||||||
|
// Konva erroneously reports NaN for width and height when the stage is hidden. This causes errors when caching,
|
||||||
|
// but it doesn't seem to break anything.
|
||||||
|
// TODO(psyche): Investigate and report upstream.
|
||||||
const newWidth = stage.width() / stage.scaleX();
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
const newHeight = stage.height() / stage.scaleY();
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
||||||
@@ -653,7 +659,7 @@ const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLay
|
|||||||
let imageSourceNeedsUpdate = false;
|
let imageSourceNeedsUpdate = false;
|
||||||
if (canvasImageSource instanceof HTMLImageElement) {
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
const image = reduxLayer.controlAdapter.processedImage ?? reduxLayer.controlAdapter.image;
|
||||||
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.imageName)) {
|
if (image && canvasImageSource.id !== getCALayerImageId(reduxLayer.id, image.name)) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
} else if (!image) {
|
} else if (!image) {
|
||||||
imageSourceNeedsUpdate = true;
|
imageSourceNeedsUpdate = true;
|
||||||
@@ -702,6 +708,7 @@ const renderLayers = (
|
|||||||
if (isInitialImageLayer(reduxLayer)) {
|
if (isInitialImageLayer(reduxLayer)) {
|
||||||
renderInitialImageLayer(stage, reduxLayer);
|
renderInitialImageLayer(stage, reduxLayer);
|
||||||
}
|
}
|
||||||
|
// IP Adapter layers are not rendered
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -716,6 +723,7 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
|
|||||||
id: getLayerBboxId(reduxLayer.id),
|
id: getLayerBboxId(reduxLayer.id),
|
||||||
name: LAYER_BBOX_NAME,
|
name: LAYER_BBOX_NAME,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
|
visible: false,
|
||||||
});
|
});
|
||||||
konvaLayer.add(rect);
|
konvaLayer.add(rect);
|
||||||
return rect;
|
return rect;
|
||||||
@@ -725,18 +733,10 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
|
|||||||
* Renders the bounding boxes for the layers.
|
* Renders the bounding boxes for the layers.
|
||||||
* @param stage The konva stage to render on
|
* @param stage The konva stage to render on
|
||||||
* @param reduxLayers An array of all redux layers to draw bboxes for
|
* @param reduxLayers An array of all redux layers to draw bboxes for
|
||||||
* @param selectedLayerId The selected layer's id
|
|
||||||
* @param tool The current tool
|
* @param tool The current tool
|
||||||
* @param onBboxChanged Callback for when the bbox is changed
|
|
||||||
* @param onBboxMouseDown Callback for when the bbox is clicked
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const renderBbox = (
|
const renderBboxes = (stage: Konva.Stage, reduxLayers: Layer[], tool: Tool) => {
|
||||||
stage: Konva.Stage,
|
|
||||||
reduxLayers: Layer[],
|
|
||||||
tool: Tool,
|
|
||||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
|
||||||
) => {
|
|
||||||
// Hide all bboxes so they don't interfere with getClientRect
|
// Hide all bboxes so they don't interfere with getClientRect
|
||||||
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
for (const bboxRect of stage.find<Konva.Rect>(`.${LAYER_BBOX_NAME}`)) {
|
||||||
bboxRect.visible(false);
|
bboxRect.visible(false);
|
||||||
@@ -747,36 +747,59 @@ const renderBbox = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxLayer of reduxLayers) {
|
for (const reduxLayer of reduxLayers.filter(isRegionalGuidanceLayer)) {
|
||||||
if (reduxLayer.type === 'regional_guidance_layer') {
|
if (!reduxLayer.bbox) {
|
||||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
continue;
|
||||||
assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`);
|
}
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${reduxLayer.id} not found in stage`);
|
||||||
|
|
||||||
let bbox = reduxLayer.bbox;
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer);
|
||||||
|
|
||||||
// We only need to recalculate the bbox if the layer has changed and it has objects
|
bboxRect.setAttrs({
|
||||||
if (reduxLayer.bboxNeedsUpdate && reduxLayer.maskObjects.length) {
|
visible: !reduxLayer.bboxNeedsUpdate,
|
||||||
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
listening: reduxLayer.isSelected,
|
||||||
bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer);
|
x: reduxLayer.bbox.x,
|
||||||
// Update the layer's bbox in the redux store
|
y: reduxLayer.bbox.y,
|
||||||
onBboxChanged(reduxLayer.id, bbox);
|
width: reduxLayer.bbox.width,
|
||||||
|
height: reduxLayer.bbox.height,
|
||||||
|
stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
|
||||||
|
* @param stage The konva stage to render on.
|
||||||
|
* @param reduxLayers An array of redux layers to calculate bboxes for
|
||||||
|
* @param onBboxChanged Callback for when the bounding box changes
|
||||||
|
*/
|
||||||
|
const updateBboxes = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
reduxLayers: Layer[],
|
||||||
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||||
|
) => {
|
||||||
|
for (const rgLayer of reduxLayers.filter(isRegionalGuidanceLayer)) {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`);
|
||||||
|
assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`);
|
||||||
|
// We only need to recalculate the bbox if the layer has changed
|
||||||
|
if (rgLayer.bboxNeedsUpdate) {
|
||||||
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer);
|
||||||
|
|
||||||
|
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
|
||||||
|
const visible = bboxRect.visible();
|
||||||
|
bboxRect.visible(false);
|
||||||
|
|
||||||
|
if (rgLayer.maskObjects.length === 0) {
|
||||||
|
// No objects - no bbox to calculate
|
||||||
|
onBboxChanged(rgLayer.id, null);
|
||||||
|
} else {
|
||||||
|
// Calculate the bbox by rendering the layer and checking its pixels
|
||||||
|
onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bbox) {
|
// Restore the visibility of the bbox
|
||||||
continue;
|
bboxRect.visible(visible);
|
||||||
}
|
|
||||||
|
|
||||||
const rect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer);
|
|
||||||
|
|
||||||
rect.setAttrs({
|
|
||||||
visible: true,
|
|
||||||
listening: reduxLayer.isSelected,
|
|
||||||
x: bbox.x,
|
|
||||||
y: bbox.y,
|
|
||||||
width: bbox.width,
|
|
||||||
height: bbox.height,
|
|
||||||
stroke: reduxLayer.isSelected ? BBOX_SELECTED_STROKE : '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -893,10 +916,11 @@ const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: nu
|
|||||||
export const renderers = {
|
export const renderers = {
|
||||||
renderToolPreview,
|
renderToolPreview,
|
||||||
renderLayers,
|
renderLayers,
|
||||||
renderBbox,
|
renderBboxes,
|
||||||
renderBackground,
|
renderBackground,
|
||||||
renderNoLayersMessage,
|
renderNoLayersMessage,
|
||||||
arrangeLayers,
|
arrangeLayers,
|
||||||
|
updateBboxes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEBOUNCE_MS = 300;
|
const DEBOUNCE_MS = 300;
|
||||||
@@ -904,10 +928,11 @@ const DEBOUNCE_MS = 300;
|
|||||||
export const debouncedRenderers = {
|
export const debouncedRenderers = {
|
||||||
renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS),
|
renderToolPreview: debounce(renderToolPreview, DEBOUNCE_MS),
|
||||||
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
||||||
renderBbox: debounce(renderBbox, DEBOUNCE_MS),
|
renderBboxes: debounce(renderBboxes, DEBOUNCE_MS),
|
||||||
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
||||||
renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS),
|
renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS),
|
||||||
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
||||||
|
updateBboxes: debounce(updateBboxes, DEBOUNCE_MS),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,18 +46,16 @@ export const getImageUsage = (
|
|||||||
|
|
||||||
const isControlLayerImage = controlLayers.layers.some((l) => {
|
const isControlLayerImage = controlLayers.layers.some((l) => {
|
||||||
if (isRegionalGuidanceLayer(l)) {
|
if (isRegionalGuidanceLayer(l)) {
|
||||||
return l.ipAdapters.some((ipa) => ipa.image?.imageName === image_name);
|
return l.ipAdapters.some((ipa) => ipa.image?.name === image_name);
|
||||||
}
|
}
|
||||||
if (isControlAdapterLayer(l)) {
|
if (isControlAdapterLayer(l)) {
|
||||||
return (
|
return l.controlAdapter.image?.name === image_name || l.controlAdapter.processedImage?.name === image_name;
|
||||||
l.controlAdapter.image?.imageName === image_name || l.controlAdapter.processedImage?.imageName === image_name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isIPAdapterLayer(l)) {
|
if (isIPAdapterLayer(l)) {
|
||||||
return l.ipAdapter.image?.imageName === image_name;
|
return l.ipAdapter.image?.name === image_name;
|
||||||
}
|
}
|
||||||
if (isInitialImageLayer(l)) {
|
if (isInitialImageLayer(l)) {
|
||||||
return l.image?.imageName === image_name;
|
return l.image?.name === image_name;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(iiLayerAdded(imageDTO));
|
dispatch(iiLayerAdded(imageDTO));
|
||||||
|
dispatch(setActiveTab('generation'));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
|
|||||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -102,6 +103,10 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
setIsHovered(true);
|
setIsHovered(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDoubleClick = useCallback(() => {
|
||||||
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleMouseOut = useCallback(() => {
|
const handleMouseOut = useCallback(() => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -143,6 +148,7 @@ const GalleryImage = (props: HoverableImageProps) => {
|
|||||||
>
|
>
|
||||||
<IAIDndImage
|
<IAIDndImage
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
|
import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
|
import { Formatter } from 'fracturedjsonjs';
|
||||||
import { isString } from 'lodash-es';
|
import { isString } from 'lodash-es';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
@@ -7,6 +8,8 @@ import { memo, useCallback, useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi';
|
import { PiCopyBold, PiDownloadSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const formatter = new Formatter();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
data: unknown;
|
data: unknown;
|
||||||
@@ -20,7 +23,7 @@ const overlayscrollbarsOptions = getOverlayScrollbarsParams('scroll', 'scroll').
|
|||||||
|
|
||||||
const DataViewer = (props: Props) => {
|
const DataViewer = (props: Props) => {
|
||||||
const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props;
|
const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props;
|
||||||
const dataString = useMemo(() => (isString(data) ? data : JSON.stringify(data, null, 2)), [data]);
|
const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]);
|
||||||
const shift = useShiftModifier();
|
const shift = useShiftModifier();
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(dataString);
|
navigator.clipboard.writeText(dataString);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
|
import { MetadataControlNets } from 'features/metadata/components/MetadataControlNets';
|
||||||
import { MetadataControlNetsV2 } from 'features/metadata/components/MetadataControlNetsV2';
|
|
||||||
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
|
import { MetadataIPAdapters } from 'features/metadata/components/MetadataIPAdapters';
|
||||||
import { MetadataIPAdaptersV2 } from 'features/metadata/components/MetadataIPAdaptersV2';
|
|
||||||
import { MetadataItem } from 'features/metadata/components/MetadataItem';
|
import { MetadataItem } from 'features/metadata/components/MetadataItem';
|
||||||
|
import { MetadataLayers } from 'features/metadata/components/MetadataLayers';
|
||||||
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
|
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
|
||||||
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
|
import { MetadataT2IAdapters } from 'features/metadata/components/MetadataT2IAdapters';
|
||||||
import { MetadataT2IAdaptersV2 } from 'features/metadata/components/MetadataT2IAdaptersV2';
|
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
import { handlers } from 'features/metadata/util/handlers';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
@@ -39,8 +37,7 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
<MetadataItem metadata={metadata} handlers={handlers.scheduler} />
|
<MetadataItem metadata={metadata} handlers={handlers.scheduler} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.cfgScale} />
|
<MetadataItem metadata={metadata} handlers={handlers.cfgScale} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.cfgRescaleMultiplier} />
|
<MetadataItem metadata={metadata} handlers={handlers.cfgRescaleMultiplier} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.initialImage} />
|
{activeTabName !== 'generation' && <MetadataItem metadata={metadata} handlers={handlers.strength} />}
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.strength} />
|
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfEnabled} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfEnabled} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfMethod} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfMethod} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.hrfStrength} />
|
<MetadataItem metadata={metadata} handlers={handlers.hrfStrength} />
|
||||||
@@ -52,12 +49,10 @@ const ImageMetadataActions = (props: Props) => {
|
|||||||
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
|
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
|
||||||
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
|
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
|
||||||
<MetadataLoRAs metadata={metadata} />
|
<MetadataLoRAs metadata={metadata} />
|
||||||
|
{activeTabName === 'generation' && <MetadataLayers metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataControlNets metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataT2IAdapters metadata={metadata} />}
|
||||||
{activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
|
{activeTabName !== 'generation' && <MetadataIPAdapters metadata={metadata} />}
|
||||||
{activeTabName === 'generation' && <MetadataControlNetsV2 metadata={metadata} />}
|
|
||||||
{activeTabName === 'generation' && <MetadataT2IAdaptersV2 metadata={metadata} />}
|
|
||||||
{activeTabName === 'generation' && <MetadataIPAdaptersV2 metadata={metadata} />}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import ParamUpscalePopover from 'features/parameters/components/Upscale/ParamUps
|
|||||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||||
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@@ -84,6 +85,7 @@ const CurrentImageButtons = () => {
|
|||||||
}
|
}
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(iiLayerAdded(imageDTO));
|
dispatch(iiLayerAdded(imageDTO));
|
||||||
|
dispatch(setActiveTab('generation'));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||||
|
|||||||
@@ -103,24 +103,11 @@ const CurrentImagePreview = ({
|
|||||||
dataTestId="image-preview"
|
dataTestId="image-preview"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AnimatePresence>
|
{shouldShowImageDetails && imageDTO && withMetadata && (
|
||||||
{shouldShowImageDetails && imageDTO && withMetadata && (
|
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||||
<Box
|
<ImageMetadataViewer image={imageDTO} />
|
||||||
as={motion.div}
|
</Box>
|
||||||
key="metadataViewer"
|
)}
|
||||||
initial={initial}
|
|
||||||
animate={animateMetadata}
|
|
||||||
exit={exit}
|
|
||||||
position="absolute"
|
|
||||||
top={0}
|
|
||||||
width="full"
|
|
||||||
height="full"
|
|
||||||
borderRadius="base"
|
|
||||||
>
|
|
||||||
<ImageMetadataViewer image={imageDTO} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && (
|
{withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && (
|
||||||
<Box
|
<Box
|
||||||
@@ -152,10 +139,6 @@ const animateArrows: AnimationProps['animate'] = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { duration: 0.07 },
|
transition: { duration: 0.07 },
|
||||||
};
|
};
|
||||||
const animateMetadata: AnimationProps['animate'] = {
|
|
||||||
opacity: 0.8,
|
|
||||||
transition: { duration: 0.07 },
|
|
||||||
};
|
|
||||||
const exit: AnimationProps['exit'] = {
|
const exit: AnimationProps['exit'] = {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: { duration: 0.07 },
|
transition: { duration: 0.07 },
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
|
||||||
|
|
||||||
const TAB_NAME_TO_TKEY_SHORT: Record<InvokeTabName, string> = {
|
|
||||||
generation: 'controlLayers.controlLayers',
|
|
||||||
canvas: 'ui.tabs.canvas',
|
|
||||||
workflows: 'ui.tabs.workflows',
|
|
||||||
models: 'ui.tabs.models',
|
|
||||||
queue: 'ui.tabs.queue',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditorButton = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { onClose } = useImageViewer();
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
const tooltip = useMemo(
|
|
||||||
() => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY_SHORT[activeTabName]) }),
|
|
||||||
[t, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
onClick={onClose}
|
|
||||||
variant="outline"
|
|
||||||
leftIcon={<PiArrowsDownUpBold />}
|
|
||||||
>
|
|
||||||
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -10,7 +10,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
|
|
||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import { EditorButton } from './EditorButton';
|
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||||
|
|
||||||
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export const ImageViewer = memo(() => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justifyContent="center">
|
<Flex flex={1} justifyContent="center">
|
||||||
<Flex gap={2} marginInlineStart="auto">
|
<Flex gap={2} marginInlineStart="auto">
|
||||||
<EditorButton />
|
<ViewerToggleMenu />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||||
|
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
|
|
||||||
|
export const ImageViewerWorkflows = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
layerStyle="first"
|
||||||
|
borderRadius="base"
|
||||||
|
position="absolute"
|
||||||
|
flexDirection="column"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
p={2}
|
||||||
|
rowGap={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
zIndex={10} // reactflow puts its minimap at 5, so we need to be above that
|
||||||
|
>
|
||||||
|
<Flex w="full" gap={2}>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineEnd="auto">
|
||||||
|
<ToggleProgressButton />
|
||||||
|
<ToggleMetadataViewerButton />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} gap={2} justifyContent="center">
|
||||||
|
<CurrentImageButtons />
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1} justifyContent="center">
|
||||||
|
<Flex gap={2} marginInlineStart="auto" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<CurrentImagePreview />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ImageViewerWorkflows.displayName = 'ImageViewerWorkflows';
|
||||||
@@ -35,6 +35,7 @@ export const ToggleMetadataViewerButton = memo(() => {
|
|||||||
isDisabled={!imageDTO}
|
isDisabled={!imageDTO}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
|
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
|
||||||
|
data-testid="toggle-show-metadata-button"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ToggleProgressButton = memo(() => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'}
|
colorScheme={shouldShowProgressInViewer ? 'invokeBlue' : 'base'}
|
||||||
|
data-testid="toggle-show-progress-button"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PiArrowsDownUpBold } from 'react-icons/pi';
|
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
|
||||||
|
|
||||||
export const ViewerButton = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { onOpen } = useImageViewer();
|
|
||||||
const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
aria-label={tooltip}
|
|
||||||
tooltip={tooltip}
|
|
||||||
onClick={onOpen}
|
|
||||||
variant="outline"
|
|
||||||
pointerEvents="auto"
|
|
||||||
leftIcon={<PiArrowsDownUpBold />}
|
|
||||||
>
|
|
||||||
{t('common.viewer')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Text,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
import { useImageViewer } from './useImageViewer';
|
||||||
|
|
||||||
|
export const ViewerToggleMenu = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isOpen, onClose, onOpen } = useImageViewer();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button variant="outline" data-testid="toggle-viewer-menu-button">
|
||||||
|
<Flex gap={3} w="full" alignItems="center">
|
||||||
|
{isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
|
||||||
|
<Text fontSize="md">{isOpen ? t('common.viewing') : t('common.editing')}</Text>
|
||||||
|
<Icon as={PiCaretDownBold} />
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent p={2}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex flexDir="column">
|
||||||
|
<Button onClick={onOpen} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
<Icon as={PiCheckBold} visibility={isOpen ? 'visible' : 'hidden'} />
|
||||||
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
|
<Text fontWeight="semibold" color="base.100">
|
||||||
|
{t('common.viewing')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="normal" color="base.300">
|
||||||
|
{t('common.viewingDesc')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose} variant="ghost" h="auto" w="auto" p={2}>
|
||||||
|
<Flex gap={2} w="full">
|
||||||
|
<Icon as={PiCheckBold} visibility={isOpen ? 'hidden' : 'visible'} />
|
||||||
|
<Flex flexDir="column" gap={2} alignItems="flex-start">
|
||||||
|
<Text fontWeight="semibold" color="base.100">
|
||||||
|
{t('common.editing')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="normal" color="base.300">
|
||||||
|
{t('common.editingDesc')}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { rgLayerAdded } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
|
||||||
import { uniqBy } from 'lodash-es';
|
import { uniqBy } from 'lodash-es';
|
||||||
import { boardsApi } from 'services/api/endpoints/boards';
|
import { boardsApi } from 'services/api/endpoints/boards';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
@@ -23,7 +21,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
boardSearchText: '',
|
boardSearchText: '',
|
||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isImageViewerOpen: false,
|
isImageViewerOpen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@@ -83,12 +81,6 @@ export const gallerySlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(setActiveTab, (state) => {
|
|
||||||
state.isImageViewerOpen = false;
|
|
||||||
});
|
|
||||||
builder.addCase(rgLayerAdded, (state) => {
|
|
||||||
state.isImageViewerOpen = false;
|
|
||||||
});
|
|
||||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||||
const deletedBoardId = action.meta.arg.originalArgs;
|
const deletedBoardId = action.meta.arg.originalArgs;
|
||||||
if (deletedBoardId === state.selectedBoardId) {
|
if (deletedBoardId === state.selectedBoardId) {
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
|
|
||||||
import type { ControlNetConfigV2Metadata, MetadataHandlers } from 'features/metadata/types';
|
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
metadata: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MetadataControlNetsV2 = ({ metadata }: Props) => {
|
|
||||||
const [controlNets, setControlNets] = useState<ControlNetConfigV2Metadata[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const parse = async () => {
|
|
||||||
try {
|
|
||||||
const parsed = await handlers.controlNetsV2.parse(metadata);
|
|
||||||
setControlNets(parsed);
|
|
||||||
} catch (e) {
|
|
||||||
setControlNets([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
parse();
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const label = useMemo(() => handlers.controlNetsV2.getLabel(), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{controlNets.map((controlNet) => (
|
|
||||||
<MetadataViewControlNet
|
|
||||||
key={controlNet.id}
|
|
||||||
label={label}
|
|
||||||
controlNet={controlNet}
|
|
||||||
handlers={handlers.controlNetsV2}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MetadataViewControlNet = ({
|
|
||||||
label,
|
|
||||||
controlNet,
|
|
||||||
handlers,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
controlNet: ControlNetConfigV2Metadata;
|
|
||||||
handlers: MetadataHandlers<ControlNetConfigV2Metadata[], ControlNetConfigV2Metadata>;
|
|
||||||
}) => {
|
|
||||||
const onRecall = useCallback(() => {
|
|
||||||
if (!handlers.recallItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlers.recallItem(controlNet, true);
|
|
||||||
}, [handlers, controlNet]);
|
|
||||||
|
|
||||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const _renderValue = async () => {
|
|
||||||
if (!handlers.renderItemValue) {
|
|
||||||
setRenderedValue(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rendered = await handlers.renderItemValue(controlNet);
|
|
||||||
setRenderedValue(rendered);
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderValue();
|
|
||||||
}, [handlers, controlNet]);
|
|
||||||
|
|
||||||
return <MetadataItemView label={label} isDisabled={false} onRecall={onRecall} renderedValue={renderedValue} />;
|
|
||||||
};
|
|
||||||
@@ -3,6 +3,7 @@ import { MetadataItemView } from 'features/metadata/components/MetadataItemView'
|
|||||||
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
import { useMetadataItem } from 'features/metadata/hooks/useMetadataItem';
|
||||||
import type { MetadataHandlers } from 'features/metadata/types';
|
import type { MetadataHandlers } from 'features/metadata/types';
|
||||||
import { MetadataParseFailedToken } from 'features/metadata/util/parsers';
|
import { MetadataParseFailedToken } from 'features/metadata/util/parsers';
|
||||||
|
import { isSymbol } from 'lodash-es';
|
||||||
|
|
||||||
type MetadataItemProps<T> = {
|
type MetadataItemProps<T> = {
|
||||||
metadata: unknown;
|
metadata: unknown;
|
||||||
@@ -17,6 +18,10 @@ const _MetadataItem = typedMemo(<T,>({ metadata, handlers, direction = 'row' }:
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (handlers.getIsVisible && !isSymbol(value) && !handlers.getIsVisible(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataItemView
|
<MetadataItemView
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
|
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
|
||||||
import type { IPAdapterConfigV2Metadata, MetadataHandlers } from 'features/metadata/types';
|
import type { MetadataHandlers } from 'features/metadata/types';
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
import { handlers } from 'features/metadata/util/handlers';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
@@ -7,52 +8,47 @@ type Props = {
|
|||||||
metadata: unknown;
|
metadata: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MetadataIPAdaptersV2 = ({ metadata }: Props) => {
|
export const MetadataLayers = ({ metadata }: Props) => {
|
||||||
const [ipAdapters, setIPAdapters] = useState<IPAdapterConfigV2Metadata[]>([]);
|
const [layers, setLayers] = useState<Layer[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parse = async () => {
|
const parse = async () => {
|
||||||
try {
|
try {
|
||||||
const parsed = await handlers.ipAdaptersV2.parse(metadata);
|
const parsed = await handlers.layers.parse(metadata);
|
||||||
setIPAdapters(parsed);
|
setLayers(parsed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIPAdapters([]);
|
setLayers([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
parse();
|
parse();
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
const label = useMemo(() => handlers.ipAdaptersV2.getLabel(), []);
|
const label = useMemo(() => handlers.layers.getLabel(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ipAdapters.map((ipAdapter) => (
|
{layers.map((layer) => (
|
||||||
<MetadataViewIPAdapter
|
<MetadataViewLayer key={layer.id} label={label} layer={layer} handlers={handlers.layers} />
|
||||||
key={ipAdapter.id}
|
|
||||||
label={label}
|
|
||||||
ipAdapter={ipAdapter}
|
|
||||||
handlers={handlers.ipAdaptersV2}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetadataViewIPAdapter = ({
|
const MetadataViewLayer = ({
|
||||||
label,
|
label,
|
||||||
ipAdapter,
|
layer,
|
||||||
handlers,
|
handlers,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
ipAdapter: IPAdapterConfigV2Metadata;
|
layer: Layer;
|
||||||
handlers: MetadataHandlers<IPAdapterConfigV2Metadata[], IPAdapterConfigV2Metadata>;
|
handlers: MetadataHandlers<Layer[], Layer>;
|
||||||
}) => {
|
}) => {
|
||||||
const onRecall = useCallback(() => {
|
const onRecall = useCallback(() => {
|
||||||
if (!handlers.recallItem) {
|
if (!handlers.recallItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handlers.recallItem(ipAdapter, true);
|
handlers.recallItem(layer, true);
|
||||||
}, [handlers, ipAdapter]);
|
}, [handlers, layer]);
|
||||||
|
|
||||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,12 +57,12 @@ const MetadataViewIPAdapter = ({
|
|||||||
setRenderedValue(null);
|
setRenderedValue(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rendered = await handlers.renderItemValue(ipAdapter);
|
const rendered = await handlers.renderItemValue(layer);
|
||||||
setRenderedValue(rendered);
|
setRenderedValue(rendered);
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderValue();
|
_renderValue();
|
||||||
}, [handlers, ipAdapter]);
|
}, [handlers, layer]);
|
||||||
|
|
||||||
return <MetadataItemView label={label} isDisabled={false} onRecall={onRecall} renderedValue={renderedValue} />;
|
return <MetadataItemView label={label} isDisabled={false} onRecall={onRecall} renderedValue={renderedValue} />;
|
||||||
};
|
};
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { MetadataItemView } from 'features/metadata/components/MetadataItemView';
|
|
||||||
import type { MetadataHandlers, T2IAdapterConfigV2Metadata } from 'features/metadata/types';
|
|
||||||
import { handlers } from 'features/metadata/util/handlers';
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
metadata: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MetadataT2IAdaptersV2 = ({ metadata }: Props) => {
|
|
||||||
const [t2iAdapters, setT2IAdapters] = useState<T2IAdapterConfigV2Metadata[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const parse = async () => {
|
|
||||||
try {
|
|
||||||
const parsed = await handlers.t2iAdaptersV2.parse(metadata);
|
|
||||||
setT2IAdapters(parsed);
|
|
||||||
} catch (e) {
|
|
||||||
setT2IAdapters([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
parse();
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const label = useMemo(() => handlers.t2iAdaptersV2.getLabel(), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{t2iAdapters.map((t2iAdapter) => (
|
|
||||||
<MetadataViewT2IAdapter
|
|
||||||
key={t2iAdapter.id}
|
|
||||||
label={label}
|
|
||||||
t2iAdapter={t2iAdapter}
|
|
||||||
handlers={handlers.t2iAdaptersV2}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MetadataViewT2IAdapter = ({
|
|
||||||
label,
|
|
||||||
t2iAdapter,
|
|
||||||
handlers,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
t2iAdapter: T2IAdapterConfigV2Metadata;
|
|
||||||
handlers: MetadataHandlers<T2IAdapterConfigV2Metadata[], T2IAdapterConfigV2Metadata>;
|
|
||||||
}) => {
|
|
||||||
const onRecall = useCallback(() => {
|
|
||||||
if (!handlers.recallItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlers.recallItem(t2iAdapter, true);
|
|
||||||
}, [handlers, t2iAdapter]);
|
|
||||||
|
|
||||||
const [renderedValue, setRenderedValue] = useState<React.ReactNode>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const _renderValue = async () => {
|
|
||||||
if (!handlers.renderItemValue) {
|
|
||||||
setRenderedValue(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rendered = await handlers.renderItemValue(t2iAdapter);
|
|
||||||
setRenderedValue(rendered);
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderValue();
|
|
||||||
}, [handlers, t2iAdapter]);
|
|
||||||
|
|
||||||
return <MetadataItemView label={label} isDisabled={false} onRecall={onRecall} renderedValue={renderedValue} />;
|
|
||||||
};
|
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
||||||
import type {
|
|
||||||
ControlNetConfigV2,
|
|
||||||
IPAdapterConfigV2,
|
|
||||||
T2IAdapterConfigV2,
|
|
||||||
} from 'features/controlLayers/util/controlAdapters';
|
|
||||||
import type { O } from 'ts-toolbelt';
|
import type { O } from 'ts-toolbelt';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +45,14 @@ export type MetadataParseFunc<T = unknown> = (metadata: unknown) => Promise<T>;
|
|||||||
*/
|
*/
|
||||||
export type MetadataValidateFunc<T> = (value: T) => Promise<T>;
|
export type MetadataValidateFunc<T> = (value: T) => Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that determines whether a metadata item should be visible.
|
||||||
|
*
|
||||||
|
* @param value The value to check.
|
||||||
|
* @returns True if the item should be visible, false otherwise.
|
||||||
|
*/
|
||||||
|
type MetadataGetIsVisibleFunc<T> = (value: T) => boolean;
|
||||||
|
|
||||||
export type MetadataHandlers<TValue = unknown, TItem = unknown> = {
|
export type MetadataHandlers<TValue = unknown, TItem = unknown> = {
|
||||||
/**
|
/**
|
||||||
* Gets the label of the current metadata item as a string.
|
* Gets the label of the current metadata item as a string.
|
||||||
@@ -111,6 +114,14 @@ export type MetadataHandlers<TValue = unknown, TItem = unknown> = {
|
|||||||
* @returns The rendered item.
|
* @returns The rendered item.
|
||||||
*/
|
*/
|
||||||
renderItemValue?: MetadataRenderValueFunc<TItem>;
|
renderItemValue?: MetadataRenderValueFunc<TItem>;
|
||||||
|
/**
|
||||||
|
* Checks if a parsed metadata value should be visible.
|
||||||
|
* If not provided, the item is always visible.
|
||||||
|
*
|
||||||
|
* @param value The value to check.
|
||||||
|
* @returns True if the item should be visible, false otherwise.
|
||||||
|
*/
|
||||||
|
getIsVisible?: MetadataGetIsVisibleFunc<TValue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(psyche): The types for item handlers should be able to be inferred from the type of the value:
|
// TODO(psyche): The types for item handlers should be able to be inferred from the type of the value:
|
||||||
@@ -127,6 +138,7 @@ type BuildMetadataHandlersArg<TValue, TItem> = {
|
|||||||
getLabel: MetadataGetLabelFunc;
|
getLabel: MetadataGetLabelFunc;
|
||||||
renderValue?: MetadataRenderValueFunc<TValue>;
|
renderValue?: MetadataRenderValueFunc<TValue>;
|
||||||
renderItemValue?: MetadataRenderValueFunc<TItem>;
|
renderItemValue?: MetadataRenderValueFunc<TItem>;
|
||||||
|
getIsVisible?: MetadataGetIsVisibleFunc<TValue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BuildMetadataHandlers = <TValue, TItem>(
|
export type BuildMetadataHandlers = <TValue, TItem>(
|
||||||
@@ -140,11 +152,3 @@ export type AnyControlAdapterConfigMetadata =
|
|||||||
| ControlNetConfigMetadata
|
| ControlNetConfigMetadata
|
||||||
| T2IAdapterConfigMetadata
|
| T2IAdapterConfigMetadata
|
||||||
| IPAdapterConfigMetadata;
|
| IPAdapterConfigMetadata;
|
||||||
|
|
||||||
export type ControlNetConfigV2Metadata = O.NonNullable<ControlNetConfigV2, 'model'>;
|
|
||||||
export type T2IAdapterConfigV2Metadata = O.NonNullable<T2IAdapterConfigV2, 'model'>;
|
|
||||||
export type IPAdapterConfigV2Metadata = O.NonNullable<IPAdapterConfigV2, 'model'>;
|
|
||||||
export type AnyControlAdapterConfigV2Metadata =
|
|
||||||
| ControlNetConfigV2Metadata
|
|
||||||
| T2IAdapterConfigV2Metadata
|
|
||||||
| IPAdapterConfigV2Metadata;
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { objectKeys } from 'common/util/objectKeys';
|
import { objectKeys } from 'common/util/objectKeys';
|
||||||
import { toast } from 'common/util/toast';
|
import { toast } from 'common/util/toast';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import type { LoRA } from 'features/lora/store/loraSlice';
|
import type { LoRA } from 'features/lora/store/loraSlice';
|
||||||
import type {
|
import type {
|
||||||
AnyControlAdapterConfigMetadata,
|
AnyControlAdapterConfigMetadata,
|
||||||
AnyControlAdapterConfigV2Metadata,
|
|
||||||
BuildMetadataHandlers,
|
BuildMetadataHandlers,
|
||||||
MetadataGetLabelFunc,
|
MetadataGetLabelFunc,
|
||||||
MetadataHandlers,
|
MetadataHandlers,
|
||||||
@@ -16,6 +16,7 @@ import { fetchModelConfig } from 'features/metadata/util/modelFetchingHelpers';
|
|||||||
import { validators } from 'features/metadata/util/validators';
|
import { validators } from 'features/metadata/util/validators';
|
||||||
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
import { parsers } from './parsers';
|
import { parsers } from './parsers';
|
||||||
import { recallers } from './recallers';
|
import { recallers } from './recallers';
|
||||||
@@ -44,13 +45,48 @@ const renderControlAdapterValue: MetadataRenderValueFunc<AnyControlAdapterConfig
|
|||||||
return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`;
|
return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const renderControlAdapterValueV2: MetadataRenderValueFunc<AnyControlAdapterConfigV2Metadata> = async (value) => {
|
const renderLayerValue: MetadataRenderValueFunc<Layer> = async (layer) => {
|
||||||
try {
|
if (layer.type === 'initial_image_layer') {
|
||||||
const modelConfig = await fetchModelConfig(value.model.key ?? 'none');
|
let rendered = t('controlLayers.globalInitialImageLayer');
|
||||||
return `${modelConfig.name} (${modelConfig.base.toUpperCase()}) - ${value.weight}`;
|
if (layer.image) {
|
||||||
} catch {
|
rendered += ` (${layer.image})`;
|
||||||
return `${value.model.key} (${value.model.base.toUpperCase()}) - ${value.weight}`;
|
}
|
||||||
|
return rendered;
|
||||||
}
|
}
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
let rendered = t('controlLayers.globalControlAdapterLayer');
|
||||||
|
const model = layer.controlAdapter.model;
|
||||||
|
if (model) {
|
||||||
|
rendered += ` (${model.name} - ${model.base.toUpperCase()})`;
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
|
let rendered = t('controlLayers.globalIPAdapterLayer');
|
||||||
|
const model = layer.ipAdapter.model;
|
||||||
|
if (model) {
|
||||||
|
rendered += ` (${model.name} - ${model.base.toUpperCase()})`;
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
|
const rendered = t('controlLayers.regionalGuidanceLayer');
|
||||||
|
const items: string[] = [];
|
||||||
|
if (layer.positivePrompt) {
|
||||||
|
items.push(`Positive: ${layer.positivePrompt}`);
|
||||||
|
}
|
||||||
|
if (layer.negativePrompt) {
|
||||||
|
items.push(`Negative: ${layer.negativePrompt}`);
|
||||||
|
}
|
||||||
|
if (layer.ipAdapters.length > 0) {
|
||||||
|
items.push(`${layer.ipAdapters.length} IP Adapters`);
|
||||||
|
}
|
||||||
|
return `${rendered} (${items.join(', ')})`;
|
||||||
|
}
|
||||||
|
assert(false, 'Unknown layer type');
|
||||||
|
};
|
||||||
|
const renderLayersValue: MetadataRenderValueFunc<Layer[]> = async (layers) => {
|
||||||
|
return `${layers.length} ${t('controlLayers.layers', { count: layers.length })}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parameterSetToast = (parameter: string, description?: string) => {
|
const parameterSetToast = (parameter: string, description?: string) => {
|
||||||
@@ -73,26 +109,6 @@ const parameterNotSetToast = (parameter: string, description?: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const allParameterSetToast = (description?: string) => {
|
|
||||||
// toast({
|
|
||||||
// title: t('toast.parametersSet'),
|
|
||||||
// status: 'info',
|
|
||||||
// description,
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const allParameterNotSetToast = (description?: string) => {
|
|
||||||
// toast({
|
|
||||||
// title: t('toast.parametersNotSet'),
|
|
||||||
// status: 'warning',
|
|
||||||
// description,
|
|
||||||
// duration: 2500,
|
|
||||||
// isClosable: true,
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
const buildParse =
|
const buildParse =
|
||||||
<TValue, TItem>(arg: {
|
<TValue, TItem>(arg: {
|
||||||
parser: MetadataParseFunc<TValue>;
|
parser: MetadataParseFunc<TValue>;
|
||||||
@@ -171,6 +187,7 @@ const buildHandlers: BuildMetadataHandlers = ({
|
|||||||
itemValidator,
|
itemValidator,
|
||||||
renderValue,
|
renderValue,
|
||||||
renderItemValue,
|
renderItemValue,
|
||||||
|
getIsVisible,
|
||||||
}) => ({
|
}) => ({
|
||||||
parse: buildParse({ parser, getLabel }),
|
parse: buildParse({ parser, getLabel }),
|
||||||
parseItem: itemParser ? buildParseItem({ itemParser, getLabel }) : undefined,
|
parseItem: itemParser ? buildParseItem({ itemParser, getLabel }) : undefined,
|
||||||
@@ -179,6 +196,7 @@ const buildHandlers: BuildMetadataHandlers = ({
|
|||||||
getLabel,
|
getLabel,
|
||||||
renderValue: renderValue ?? resolveToString,
|
renderValue: renderValue ?? resolveToString,
|
||||||
renderItemValue: renderItemValue ?? resolveToString,
|
renderItemValue: renderItemValue ?? resolveToString,
|
||||||
|
getIsVisible,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const handlers = {
|
export const handlers = {
|
||||||
@@ -198,12 +216,6 @@ export const handlers = {
|
|||||||
recaller: recallers.cfgScale,
|
recaller: recallers.cfgScale,
|
||||||
}),
|
}),
|
||||||
height: buildHandlers({ getLabel: () => t('metadata.height'), parser: parsers.height, recaller: recallers.height }),
|
height: buildHandlers({ getLabel: () => t('metadata.height'), parser: parsers.height, recaller: recallers.height }),
|
||||||
initialImage: buildHandlers({
|
|
||||||
getLabel: () => t('metadata.initImage'),
|
|
||||||
parser: parsers.initialImage,
|
|
||||||
recaller: recallers.initialImage,
|
|
||||||
renderValue: async (imageDTO) => imageDTO.image_name,
|
|
||||||
}),
|
|
||||||
negativePrompt: buildHandlers({
|
negativePrompt: buildHandlers({
|
||||||
getLabel: () => t('metadata.negativePrompt'),
|
getLabel: () => t('metadata.negativePrompt'),
|
||||||
parser: parsers.negativePrompt,
|
parser: parsers.negativePrompt,
|
||||||
@@ -350,35 +362,17 @@ export const handlers = {
|
|||||||
itemValidator: validators.t2iAdapter,
|
itemValidator: validators.t2iAdapter,
|
||||||
renderItemValue: renderControlAdapterValue,
|
renderItemValue: renderControlAdapterValue,
|
||||||
}),
|
}),
|
||||||
controlNetsV2: buildHandlers({
|
layers: buildHandlers({
|
||||||
getLabel: () => t('common.controlNet'),
|
getLabel: () => t('controlLayers.layers_one'),
|
||||||
parser: parsers.controlNetsV2,
|
parser: parsers.layers,
|
||||||
itemParser: parsers.controlNetV2,
|
itemParser: parsers.layer,
|
||||||
recaller: recallers.controlNetsV2,
|
recaller: recallers.layers,
|
||||||
itemRecaller: recallers.controlNetV2,
|
itemRecaller: recallers.layer,
|
||||||
validator: validators.controlNetsV2,
|
validator: validators.layers,
|
||||||
itemValidator: validators.controlNetV2,
|
itemValidator: validators.layer,
|
||||||
renderItemValue: renderControlAdapterValueV2,
|
renderItemValue: renderLayerValue,
|
||||||
}),
|
renderValue: renderLayersValue,
|
||||||
ipAdaptersV2: buildHandlers({
|
getIsVisible: (value) => value.length > 0,
|
||||||
getLabel: () => t('common.ipAdapter'),
|
|
||||||
parser: parsers.ipAdaptersV2,
|
|
||||||
itemParser: parsers.ipAdapterV2,
|
|
||||||
recaller: recallers.ipAdaptersV2,
|
|
||||||
itemRecaller: recallers.ipAdapterV2,
|
|
||||||
validator: validators.ipAdaptersV2,
|
|
||||||
itemValidator: validators.ipAdapterV2,
|
|
||||||
renderItemValue: renderControlAdapterValueV2,
|
|
||||||
}),
|
|
||||||
t2iAdaptersV2: buildHandlers({
|
|
||||||
getLabel: () => t('common.t2iAdapter'),
|
|
||||||
parser: parsers.t2iAdaptersV2,
|
|
||||||
itemParser: parsers.t2iAdapterV2,
|
|
||||||
recaller: recallers.t2iAdaptersV2,
|
|
||||||
itemRecaller: recallers.t2iAdapterV2,
|
|
||||||
validator: validators.t2iAdaptersV2,
|
|
||||||
itemValidator: validators.t2iAdapterV2,
|
|
||||||
renderItemValue: renderControlAdapterValueV2,
|
|
||||||
}),
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -435,9 +429,9 @@ export const parseAndRecallImageDimensions = async (metadata: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// These handlers should be omitted when recalling to control layers
|
// These handlers should be omitted when recalling to control layers
|
||||||
const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters'];
|
const TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNets', 'ipAdapters', 't2iAdapters', 'strength'];
|
||||||
// These handlers should be omitted when recalling to the rest of the app
|
// These handlers should be omitted when recalling to the rest of the app
|
||||||
const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['controlNetsV2', 'ipAdaptersV2', 't2iAdaptersV2'];
|
const NOT_TO_CONTROL_LAYERS_SKIP_KEYS: (keyof typeof handlers)[] = ['layers'];
|
||||||
|
|
||||||
export const parseAndRecallAllMetadata = async (
|
export const parseAndRecallAllMetadata = async (
|
||||||
metadata: unknown,
|
metadata: unknown,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
|
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import { isModelIdentifier, isModelIdentifierV2 } from 'features/nodes/types/common';
|
import { isModelIdentifier, isModelIdentifierV2 } from 'features/nodes/types/common';
|
||||||
import { modelsApi } from 'services/api/endpoints/models';
|
import { modelsApi } from 'services/api/endpoints/models';
|
||||||
import type { AnyModelConfig, BaseModelType, ModelType } from 'services/api/types';
|
import type { AnyModelConfig, BaseModelType, ModelType } from 'services/api/types';
|
||||||
@@ -68,6 +69,24 @@ const fetchModelConfigByAttrs = async (name: string, base: BaseModelType, type:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the model config given an identifier. First attempts to fetch by key, then falls back to fetching by attrs.
|
||||||
|
* @param identifier The model identifier.
|
||||||
|
* @returns A promise that resolves to the model config.
|
||||||
|
* @throws {ModelConfigNotFoundError} If the model config is unable to be fetched.
|
||||||
|
*/
|
||||||
|
export const fetchModelConfigByIdentifier = async (identifier: ModelIdentifierField): Promise<AnyModelConfig> => {
|
||||||
|
try {
|
||||||
|
return await fetchModelConfig(identifier.key);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
return await fetchModelConfigByAttrs(identifier.name, identifier.base, identifier.type);
|
||||||
|
} catch {
|
||||||
|
throw new ModelConfigNotFoundError(`Unable to retrieve model config for identifier ${identifier}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the model config for a given model key and type, and ensures that the model config is of a specific type.
|
* Fetches the model config for a given model key and type, and ensures that the model config is of a specific type.
|
||||||
* @param key The model key.
|
* @param key The model key.
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
|
||||||
import {
|
import {
|
||||||
initialControlNet,
|
initialControlNet,
|
||||||
initialIPAdapter,
|
initialIPAdapter,
|
||||||
initialT2IAdapter,
|
initialT2IAdapter,
|
||||||
} from 'features/controlAdapters/util/buildControlAdapter';
|
} from 'features/controlAdapters/util/buildControlAdapter';
|
||||||
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
||||||
|
import { getCALayerId, getIPALayerId, INITIAL_IMAGE_LAYER_ID } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { ControlAdapterLayer, InitialImageLayer, IPAdapterLayer, Layer } from 'features/controlLayers/store/types';
|
||||||
|
import { zLayer } from 'features/controlLayers/store/types';
|
||||||
import {
|
import {
|
||||||
CA_PROCESSOR_DATA,
|
CA_PROCESSOR_DATA,
|
||||||
imageDTOToImageWithDims,
|
imageDTOToImageWithDims,
|
||||||
@@ -17,12 +19,9 @@ import type { LoRA } from 'features/lora/store/loraSlice';
|
|||||||
import { defaultLoRAConfig } from 'features/lora/store/loraSlice';
|
import { defaultLoRAConfig } from 'features/lora/store/loraSlice';
|
||||||
import type {
|
import type {
|
||||||
ControlNetConfigMetadata,
|
ControlNetConfigMetadata,
|
||||||
ControlNetConfigV2Metadata,
|
|
||||||
IPAdapterConfigMetadata,
|
IPAdapterConfigMetadata,
|
||||||
IPAdapterConfigV2Metadata,
|
|
||||||
MetadataParseFunc,
|
MetadataParseFunc,
|
||||||
T2IAdapterConfigMetadata,
|
T2IAdapterConfigMetadata,
|
||||||
T2IAdapterConfigV2Metadata,
|
|
||||||
} from 'features/metadata/types';
|
} from 'features/metadata/types';
|
||||||
import { fetchModelConfigWithTypeGuard, getModelKey } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard, getModelKey } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { zControlField, zIPAdapterField, zModelIdentifierField, zT2IAdapterField } from 'features/nodes/types/common';
|
import { zControlField, zIPAdapterField, zModelIdentifierField, zT2IAdapterField } from 'features/nodes/types/common';
|
||||||
@@ -69,8 +68,7 @@ import {
|
|||||||
isParameterWidth,
|
isParameterWidth,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import { get, isArray, isString } from 'lodash-es';
|
import { get, isArray, isString } from 'lodash-es';
|
||||||
import { getImageDTO, imagesApi } from 'services/api/endpoints/images';
|
import { getImageDTO } from 'services/api/endpoints/images';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
|
||||||
import {
|
import {
|
||||||
isControlNetModelConfig,
|
isControlNetModelConfig,
|
||||||
isIPAdapterModelConfig,
|
isIPAdapterModelConfig,
|
||||||
@@ -80,6 +78,7 @@ import {
|
|||||||
isT2IAdapterModelConfig,
|
isT2IAdapterModelConfig,
|
||||||
isVAEModelConfig,
|
isVAEModelConfig,
|
||||||
} from 'services/api/types';
|
} from 'services/api/types';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export const MetadataParsePendingToken = Symbol('pending');
|
export const MetadataParsePendingToken = Symbol('pending');
|
||||||
@@ -149,14 +148,6 @@ const parseCFGRescaleMultiplier: MetadataParseFunc<ParameterCFGRescaleMultiplier
|
|||||||
const parseScheduler: MetadataParseFunc<ParameterScheduler> = (metadata) =>
|
const parseScheduler: MetadataParseFunc<ParameterScheduler> = (metadata) =>
|
||||||
getProperty(metadata, 'scheduler', isParameterScheduler);
|
getProperty(metadata, 'scheduler', isParameterScheduler);
|
||||||
|
|
||||||
const parseInitialImage: MetadataParseFunc<ImageDTO> = async (metadata) => {
|
|
||||||
const imageName = await getProperty(metadata, 'init_image', isString);
|
|
||||||
const imageDTORequest = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(imageName));
|
|
||||||
const imageDTO = await imageDTORequest.unwrap();
|
|
||||||
imageDTORequest.unsubscribe();
|
|
||||||
return imageDTO;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseWidth: MetadataParseFunc<ParameterWidth> = (metadata) => getProperty(metadata, 'width', isParameterWidth);
|
const parseWidth: MetadataParseFunc<ParameterWidth> = (metadata) => getProperty(metadata, 'width', isParameterWidth);
|
||||||
|
|
||||||
const parseHeight: MetadataParseFunc<ParameterHeight> = (metadata) =>
|
const parseHeight: MetadataParseFunc<ParameterHeight> = (metadata) =>
|
||||||
@@ -309,7 +300,7 @@ const parseControlNet: MetadataParseFunc<ControlNetConfigMetadata> = async (meta
|
|||||||
|
|
||||||
const parseAllControlNets: MetadataParseFunc<ControlNetConfigMetadata[]> = async (metadata) => {
|
const parseAllControlNets: MetadataParseFunc<ControlNetConfigMetadata[]> = async (metadata) => {
|
||||||
try {
|
try {
|
||||||
const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined);
|
const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray);
|
||||||
const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNet(cn)));
|
const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNet(cn)));
|
||||||
const controlNets = parseResults
|
const controlNets = parseResults
|
||||||
.filter((result): result is PromiseFulfilledResult<ControlNetConfigMetadata> => result.status === 'fulfilled')
|
.filter((result): result is PromiseFulfilledResult<ControlNetConfigMetadata> => result.status === 'fulfilled')
|
||||||
@@ -439,8 +430,103 @@ const parseAllIPAdapters: MetadataParseFunc<IPAdapterConfigMetadata[]> = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//#region V2/Control Layers
|
//#region Control Layers
|
||||||
const parseControlNetV2: MetadataParseFunc<ControlNetConfigV2Metadata> = async (metadataItem) => {
|
const parseLayer: MetadataParseFunc<Layer> = async (metadataItem) => zLayer.parseAsync(metadataItem);
|
||||||
|
|
||||||
|
const parseLayers: MetadataParseFunc<Layer[]> = async (metadata) => {
|
||||||
|
// We need to support recalling pre-Control Layers metadata into Control Layers. A separate set of parsers handles
|
||||||
|
// taking pre-CL metadata and parsing it into layers. It doesn't always map 1-to-1, so this is best-effort. For
|
||||||
|
// example, CL Control Adapters don't support resize mode, so we simply omit that property.
|
||||||
|
|
||||||
|
try {
|
||||||
|
const layers: Layer[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const control_layers = await getProperty(metadata, 'control_layers');
|
||||||
|
const controlLayersRaw = await getProperty(control_layers, 'layers', isArray);
|
||||||
|
const controlLayersParseResults = await Promise.allSettled(controlLayersRaw.map(parseLayer));
|
||||||
|
const controlLayers = controlLayersParseResults
|
||||||
|
.filter((result): result is PromiseFulfilledResult<Layer> => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
layers.push(...controlLayers);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray);
|
||||||
|
const controlNetsParseResults = await Promise.allSettled(
|
||||||
|
controlNetsRaw.map(async (cn) => await parseControlNetToControlAdapterLayer(cn))
|
||||||
|
);
|
||||||
|
const controlNetsAsLayers = controlNetsParseResults
|
||||||
|
.filter((result): result is PromiseFulfilledResult<ControlAdapterLayer> => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
layers.push(...controlNetsAsLayers);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray);
|
||||||
|
const t2iAdaptersParseResults = await Promise.allSettled(
|
||||||
|
t2iAdaptersRaw.map(async (cn) => await parseT2IAdapterToControlAdapterLayer(cn))
|
||||||
|
);
|
||||||
|
const t2iAdaptersAsLayers = t2iAdaptersParseResults
|
||||||
|
.filter((result): result is PromiseFulfilledResult<ControlAdapterLayer> => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
layers.push(...t2iAdaptersAsLayers);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray);
|
||||||
|
const ipAdaptersParseResults = await Promise.allSettled(
|
||||||
|
ipAdaptersRaw.map(async (cn) => await parseIPAdapterToIPAdapterLayer(cn))
|
||||||
|
);
|
||||||
|
const ipAdaptersAsLayers = ipAdaptersParseResults
|
||||||
|
.filter((result): result is PromiseFulfilledResult<IPAdapterLayer> => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
layers.push(...ipAdaptersAsLayers);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const initialImageLayer = await parseInitialImageToInitialImageLayer(metadata);
|
||||||
|
layers.push(initialImageLayer);
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
return layers;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseInitialImageToInitialImageLayer: MetadataParseFunc<InitialImageLayer> = async (metadata) => {
|
||||||
|
const denoisingStrength = await getProperty(metadata, 'strength', isParameterStrength);
|
||||||
|
const imageName = await getProperty(metadata, 'init_image', isString);
|
||||||
|
const imageDTO = await getImageDTO(imageName);
|
||||||
|
assert(imageDTO, 'ImageDTO is null');
|
||||||
|
const layer: InitialImageLayer = {
|
||||||
|
id: INITIAL_IMAGE_LAYER_ID,
|
||||||
|
type: 'initial_image_layer',
|
||||||
|
bbox: null,
|
||||||
|
bboxNeedsUpdate: true,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
isEnabled: true,
|
||||||
|
opacity: 1,
|
||||||
|
image: imageDTOToImageWithDims(imageDTO),
|
||||||
|
isSelected: true,
|
||||||
|
denoisingStrength,
|
||||||
|
};
|
||||||
|
return layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseControlNetToControlAdapterLayer: MetadataParseFunc<ControlAdapterLayer> = async (metadataItem) => {
|
||||||
const control_model = await getProperty(metadataItem, 'control_model');
|
const control_model = await getProperty(metadataItem, 'control_model');
|
||||||
const key = await getModelKey(control_model, 'controlnet');
|
const key = await getModelKey(control_model, 'controlnet');
|
||||||
const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig);
|
const controlNetModel = await fetchModelConfigWithTypeGuard(key, isControlNetModelConfig);
|
||||||
@@ -469,7 +555,6 @@ const parseControlNetV2: MetadataParseFunc<ControlNetConfigV2Metadata> = async (
|
|||||||
.catch(null)
|
.catch(null)
|
||||||
.parse(await getProperty(metadataItem, 'control_mode'));
|
.parse(await getProperty(metadataItem, 'control_mode'));
|
||||||
|
|
||||||
const id = uuidv4();
|
|
||||||
const defaultPreprocessor = controlNetModel.default_settings?.preprocessor;
|
const defaultPreprocessor = controlNetModel.default_settings?.preprocessor;
|
||||||
const processorConfig = isProcessorTypeV2(defaultPreprocessor)
|
const processorConfig = isProcessorTypeV2(defaultPreprocessor)
|
||||||
? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults()
|
? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults()
|
||||||
@@ -481,36 +566,35 @@ const parseControlNetV2: MetadataParseFunc<ControlNetConfigV2Metadata> = async (
|
|||||||
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
||||||
const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null;
|
const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null;
|
||||||
|
|
||||||
const controlNet: ControlNetConfigV2Metadata = {
|
const layer: ControlAdapterLayer = {
|
||||||
id,
|
id: getCALayerId(uuidv4()),
|
||||||
type: 'controlnet',
|
bbox: null,
|
||||||
model: zModelIdentifierField.parse(controlNetModel),
|
bboxNeedsUpdate: true,
|
||||||
weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight,
|
isEnabled: true,
|
||||||
beginEndStepPct,
|
isFilterEnabled: true,
|
||||||
controlMode: control_mode ?? initialControlNetV2.controlMode,
|
isSelected: true,
|
||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
opacity: 1,
|
||||||
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
type: 'control_adapter_layer',
|
||||||
processorConfig,
|
x: 0,
|
||||||
isProcessingImage: false,
|
y: 0,
|
||||||
|
controlAdapter: {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'controlnet',
|
||||||
|
model: zModelIdentifierField.parse(controlNetModel),
|
||||||
|
weight: typeof control_weight === 'number' ? control_weight : initialControlNetV2.weight,
|
||||||
|
beginEndStepPct,
|
||||||
|
controlMode: control_mode ?? initialControlNetV2.controlMode,
|
||||||
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
|
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
||||||
|
processorConfig,
|
||||||
|
isProcessingImage: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return controlNet;
|
return layer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseAllControlNetsV2: MetadataParseFunc<ControlNetConfigV2Metadata[]> = async (metadata) => {
|
const parseT2IAdapterToControlAdapterLayer: MetadataParseFunc<ControlAdapterLayer> = async (metadataItem) => {
|
||||||
try {
|
|
||||||
const controlNetsRaw = await getProperty(metadata, 'controlnets', isArray || undefined);
|
|
||||||
const parseResults = await Promise.allSettled(controlNetsRaw.map((cn) => parseControlNetV2(cn)));
|
|
||||||
const controlNets = parseResults
|
|
||||||
.filter((result): result is PromiseFulfilledResult<ControlNetConfigV2Metadata> => result.status === 'fulfilled')
|
|
||||||
.map((result) => result.value);
|
|
||||||
return controlNets;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseT2IAdapterV2: MetadataParseFunc<T2IAdapterConfigV2Metadata> = async (metadataItem) => {
|
|
||||||
const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model');
|
const t2i_adapter_model = await getProperty(metadataItem, 't2i_adapter_model');
|
||||||
const key = await getModelKey(t2i_adapter_model, 't2i_adapter');
|
const key = await getModelKey(t2i_adapter_model, 't2i_adapter');
|
||||||
const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig);
|
const t2iAdapterModel = await fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig);
|
||||||
@@ -536,7 +620,6 @@ const parseT2IAdapterV2: MetadataParseFunc<T2IAdapterConfigV2Metadata> = async (
|
|||||||
.catch(null)
|
.catch(null)
|
||||||
.parse(await getProperty(metadataItem, 'end_step_percent'));
|
.parse(await getProperty(metadataItem, 'end_step_percent'));
|
||||||
|
|
||||||
const id = uuidv4();
|
|
||||||
const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor;
|
const defaultPreprocessor = t2iAdapterModel.default_settings?.preprocessor;
|
||||||
const processorConfig = isProcessorTypeV2(defaultPreprocessor)
|
const processorConfig = isProcessorTypeV2(defaultPreprocessor)
|
||||||
? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults()
|
? CA_PROCESSOR_DATA[defaultPreprocessor].buildDefaults()
|
||||||
@@ -548,35 +631,34 @@ const parseT2IAdapterV2: MetadataParseFunc<T2IAdapterConfigV2Metadata> = async (
|
|||||||
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
||||||
const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null;
|
const processedImageDTO = processedImage ? await getImageDTO(processedImage.image_name) : null;
|
||||||
|
|
||||||
const t2iAdapter: T2IAdapterConfigV2Metadata = {
|
const layer: ControlAdapterLayer = {
|
||||||
id,
|
id: getCALayerId(uuidv4()),
|
||||||
type: 't2i_adapter',
|
bbox: null,
|
||||||
model: zModelIdentifierField.parse(t2iAdapterModel),
|
bboxNeedsUpdate: true,
|
||||||
weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight,
|
isEnabled: true,
|
||||||
beginEndStepPct,
|
isFilterEnabled: true,
|
||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
isSelected: true,
|
||||||
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
opacity: 1,
|
||||||
processorConfig,
|
type: 'control_adapter_layer',
|
||||||
isProcessingImage: false,
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
controlAdapter: {
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 't2i_adapter',
|
||||||
|
model: zModelIdentifierField.parse(t2iAdapterModel),
|
||||||
|
weight: typeof weight === 'number' ? weight : initialT2IAdapterV2.weight,
|
||||||
|
beginEndStepPct,
|
||||||
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
|
processedImage: processedImageDTO ? imageDTOToImageWithDims(processedImageDTO) : null,
|
||||||
|
processorConfig,
|
||||||
|
isProcessingImage: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return t2iAdapter;
|
return layer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseAllT2IAdaptersV2: MetadataParseFunc<T2IAdapterConfigV2Metadata[]> = async (metadata) => {
|
const parseIPAdapterToIPAdapterLayer: MetadataParseFunc<IPAdapterLayer> = async (metadataItem) => {
|
||||||
try {
|
|
||||||
const t2iAdaptersRaw = await getProperty(metadata, 't2iAdapters', isArray);
|
|
||||||
const parseResults = await Promise.allSettled(t2iAdaptersRaw.map((t2iAdapter) => parseT2IAdapterV2(t2iAdapter)));
|
|
||||||
const t2iAdapters = parseResults
|
|
||||||
.filter((result): result is PromiseFulfilledResult<T2IAdapterConfigV2Metadata> => result.status === 'fulfilled')
|
|
||||||
.map((result) => result.value);
|
|
||||||
return t2iAdapters;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseIPAdapterV2: MetadataParseFunc<IPAdapterConfigV2Metadata> = async (metadataItem) => {
|
|
||||||
const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model');
|
const ip_adapter_model = await getProperty(metadataItem, 'ip_adapter_model');
|
||||||
const key = await getModelKey(ip_adapter_model, 'ip_adapter');
|
const key = await getModelKey(ip_adapter_model, 'ip_adapter');
|
||||||
const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig);
|
const ipAdapterModel = await fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig);
|
||||||
@@ -602,39 +684,32 @@ const parseIPAdapterV2: MetadataParseFunc<IPAdapterConfigV2Metadata> = async (me
|
|||||||
.catch(null)
|
.catch(null)
|
||||||
.parse(await getProperty(metadataItem, 'end_step_percent'));
|
.parse(await getProperty(metadataItem, 'end_step_percent'));
|
||||||
|
|
||||||
const id = uuidv4();
|
|
||||||
const beginEndStepPct: [number, number] = [
|
const beginEndStepPct: [number, number] = [
|
||||||
begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0],
|
begin_step_percent ?? initialIPAdapterV2.beginEndStepPct[0],
|
||||||
end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1],
|
end_step_percent ?? initialIPAdapterV2.beginEndStepPct[1],
|
||||||
];
|
];
|
||||||
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
const imageDTO = image ? await getImageDTO(image.image_name) : null;
|
||||||
|
|
||||||
const ipAdapter: IPAdapterConfigV2Metadata = {
|
const layer: IPAdapterLayer = {
|
||||||
id,
|
id: getIPALayerId(uuidv4()),
|
||||||
type: 'ip_adapter',
|
type: 'ip_adapter_layer',
|
||||||
model: zModelIdentifierField.parse(ipAdapterModel),
|
isEnabled: true,
|
||||||
weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight,
|
isSelected: true,
|
||||||
beginEndStepPct,
|
ipAdapter: {
|
||||||
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
id: uuidv4(),
|
||||||
clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField...
|
type: 'ip_adapter',
|
||||||
method: method ?? initialIPAdapterV2.method,
|
model: zModelIdentifierField.parse(ipAdapterModel),
|
||||||
|
weight: typeof weight === 'number' ? weight : initialIPAdapterV2.weight,
|
||||||
|
beginEndStepPct,
|
||||||
|
image: imageDTO ? imageDTOToImageWithDims(imageDTO) : null,
|
||||||
|
clipVisionModel: initialIPAdapterV2.clipVisionModel, // TODO: This needs to be added to the zIPAdapterField...
|
||||||
|
method: method ?? initialIPAdapterV2.method,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return ipAdapter;
|
return layer;
|
||||||
};
|
|
||||||
|
|
||||||
const parseAllIPAdaptersV2: MetadataParseFunc<IPAdapterConfigV2Metadata[]> = async (metadata) => {
|
|
||||||
try {
|
|
||||||
const ipAdaptersRaw = await getProperty(metadata, 'ipAdapters', isArray);
|
|
||||||
const parseResults = await Promise.allSettled(ipAdaptersRaw.map((ipAdapter) => parseIPAdapterV2(ipAdapter)));
|
|
||||||
const ipAdapters = parseResults
|
|
||||||
.filter((result): result is PromiseFulfilledResult<IPAdapterConfigV2Metadata> => result.status === 'fulfilled')
|
|
||||||
.map((result) => result.value);
|
|
||||||
return ipAdapters;
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
export const parsers = {
|
export const parsers = {
|
||||||
createdBy: parseCreatedBy,
|
createdBy: parseCreatedBy,
|
||||||
@@ -647,7 +722,6 @@ export const parsers = {
|
|||||||
cfgScale: parseCFGScale,
|
cfgScale: parseCFGScale,
|
||||||
cfgRescaleMultiplier: parseCFGRescaleMultiplier,
|
cfgRescaleMultiplier: parseCFGRescaleMultiplier,
|
||||||
scheduler: parseScheduler,
|
scheduler: parseScheduler,
|
||||||
initialImage: parseInitialImage,
|
|
||||||
width: parseWidth,
|
width: parseWidth,
|
||||||
height: parseHeight,
|
height: parseHeight,
|
||||||
steps: parseSteps,
|
steps: parseSteps,
|
||||||
@@ -672,10 +746,9 @@ export const parsers = {
|
|||||||
t2iAdapters: parseAllT2IAdapters,
|
t2iAdapters: parseAllT2IAdapters,
|
||||||
ipAdapter: parseIPAdapter,
|
ipAdapter: parseIPAdapter,
|
||||||
ipAdapters: parseAllIPAdapters,
|
ipAdapters: parseAllIPAdapters,
|
||||||
controlNetV2: parseControlNetV2,
|
controlNetToControlLayer: parseControlNetToControlAdapterLayer,
|
||||||
controlNetsV2: parseAllControlNetsV2,
|
t2iAdapterToControlAdapterLayer: parseT2IAdapterToControlAdapterLayer,
|
||||||
t2iAdapterV2: parseT2IAdapterV2,
|
ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer,
|
||||||
t2iAdaptersV2: parseAllT2IAdaptersV2,
|
layer: parseLayer,
|
||||||
ipAdapterV2: parseIPAdapterV2,
|
layers: parseLayers,
|
||||||
ipAdaptersV2: parseAllIPAdaptersV2,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import {
|
import {
|
||||||
controlAdapterRecalled,
|
controlAdapterRecalled,
|
||||||
controlNetsReset,
|
controlNetsReset,
|
||||||
@@ -6,31 +7,32 @@ import {
|
|||||||
t2iAdaptersReset,
|
t2iAdaptersReset,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import {
|
import {
|
||||||
caLayerAdded,
|
allLayersDeleted,
|
||||||
caLayerControlNetsDeleted,
|
caLayerRecalled,
|
||||||
caLayerT2IAdaptersDeleted,
|
getCALayerId,
|
||||||
|
getIPALayerId,
|
||||||
|
getRGLayerId,
|
||||||
heightChanged,
|
heightChanged,
|
||||||
iiLayerAdded,
|
iiLayerRecalled,
|
||||||
ipaLayerAdded,
|
ipaLayerRecalled,
|
||||||
ipaLayersDeleted,
|
|
||||||
negativePrompt2Changed,
|
negativePrompt2Changed,
|
||||||
negativePromptChanged,
|
negativePromptChanged,
|
||||||
positivePrompt2Changed,
|
positivePrompt2Changed,
|
||||||
positivePromptChanged,
|
positivePromptChanged,
|
||||||
|
rgLayerRecalled,
|
||||||
widthChanged,
|
widthChanged,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice';
|
import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice';
|
||||||
import type { LoRA } from 'features/lora/store/loraSlice';
|
import type { LoRA } from 'features/lora/store/loraSlice';
|
||||||
import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice';
|
import { loraRecalled, lorasReset } from 'features/lora/store/loraSlice';
|
||||||
import type {
|
import type {
|
||||||
ControlNetConfigMetadata,
|
ControlNetConfigMetadata,
|
||||||
ControlNetConfigV2Metadata,
|
|
||||||
IPAdapterConfigMetadata,
|
IPAdapterConfigMetadata,
|
||||||
IPAdapterConfigV2Metadata,
|
|
||||||
MetadataRecallFunc,
|
MetadataRecallFunc,
|
||||||
T2IAdapterConfigMetadata,
|
T2IAdapterConfigMetadata,
|
||||||
T2IAdapterConfigV2Metadata,
|
|
||||||
} from 'features/metadata/types';
|
} from 'features/metadata/types';
|
||||||
|
import { fetchModelConfigByIdentifier } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { modelSelected } from 'features/parameters/store/actions';
|
import { modelSelected } from 'features/parameters/store/actions';
|
||||||
import {
|
import {
|
||||||
setCfgRescaleMultiplier,
|
setCfgRescaleMultiplier,
|
||||||
@@ -72,7 +74,8 @@ import {
|
|||||||
setRefinerStart,
|
setRefinerStart,
|
||||||
setRefinerSteps,
|
setRefinerSteps,
|
||||||
} from 'features/sdxl/store/sdxlSlice';
|
} from 'features/sdxl/store/sdxlSlice';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import { getImageDTO } from 'services/api/endpoints/images';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const recallPositivePrompt: MetadataRecallFunc<ParameterPositivePrompt> = (positivePrompt) => {
|
const recallPositivePrompt: MetadataRecallFunc<ParameterPositivePrompt> = (positivePrompt) => {
|
||||||
getStore().dispatch(positivePromptChanged(positivePrompt));
|
getStore().dispatch(positivePromptChanged(positivePrompt));
|
||||||
@@ -106,10 +109,6 @@ const recallScheduler: MetadataRecallFunc<ParameterScheduler> = (scheduler) => {
|
|||||||
getStore().dispatch(setScheduler(scheduler));
|
getStore().dispatch(setScheduler(scheduler));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallInitialImage: MetadataRecallFunc<ImageDTO> = async (imageDTO) => {
|
|
||||||
getStore().dispatch(iiLayerAdded(imageDTO));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSizeOptions = { updateAspectRatio: true, clamp: true };
|
const setSizeOptions = { updateAspectRatio: true, clamp: true };
|
||||||
|
|
||||||
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
|
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
|
||||||
@@ -244,50 +243,96 @@ const recallIPAdapters: MetadataRecallFunc<IPAdapterConfigMetadata[]> = (ipAdapt
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
//#region V2/Control Layer
|
//#region Control Layers
|
||||||
const recallControlNetV2: MetadataRecallFunc<ControlNetConfigV2Metadata> = (controlNet) => {
|
const recallLayer: MetadataRecallFunc<Layer> = async (layer) => {
|
||||||
getStore().dispatch(caLayerAdded(controlNet));
|
|
||||||
};
|
|
||||||
|
|
||||||
const recallControlNetsV2: MetadataRecallFunc<ControlNetConfigV2Metadata[]> = (controlNets) => {
|
|
||||||
const { dispatch } = getStore();
|
const { dispatch } = getStore();
|
||||||
dispatch(caLayerControlNetsDeleted());
|
// We need to check for the existence of all images and models when recalling. If they do not exist, SMITE THEM!
|
||||||
if (!controlNets.length) {
|
// Also, we need fresh IDs for all objects when recalling, to prevent multiple layers with the same ID.
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
const clone = deepClone(layer);
|
||||||
|
if (clone.controlAdapter.image) {
|
||||||
|
const imageDTO = await getImageDTO(clone.controlAdapter.image.name);
|
||||||
|
if (!imageDTO) {
|
||||||
|
clone.controlAdapter.image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clone.controlAdapter.processedImage) {
|
||||||
|
const imageDTO = await getImageDTO(clone.controlAdapter.processedImage.name);
|
||||||
|
if (!imageDTO) {
|
||||||
|
clone.controlAdapter.processedImage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clone.controlAdapter.model) {
|
||||||
|
try {
|
||||||
|
await fetchModelConfigByIdentifier(clone.controlAdapter.model);
|
||||||
|
} catch {
|
||||||
|
clone.controlAdapter.model = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clone.id = getCALayerId(uuidv4());
|
||||||
|
clone.controlAdapter.id = uuidv4();
|
||||||
|
dispatch(caLayerRecalled(clone));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controlNets.forEach((controlNet) => {
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
dispatch(caLayerAdded(controlNet));
|
const clone = deepClone(layer);
|
||||||
});
|
if (clone.ipAdapter.image) {
|
||||||
};
|
const imageDTO = await getImageDTO(clone.ipAdapter.image.name);
|
||||||
|
if (!imageDTO) {
|
||||||
const recallT2IAdapterV2: MetadataRecallFunc<T2IAdapterConfigV2Metadata> = (t2iAdapter) => {
|
clone.ipAdapter.image = null;
|
||||||
getStore().dispatch(caLayerAdded(t2iAdapter));
|
}
|
||||||
};
|
}
|
||||||
|
if (clone.ipAdapter.model) {
|
||||||
const recallT2IAdaptersV2: MetadataRecallFunc<T2IAdapterConfigV2Metadata[]> = (t2iAdapters) => {
|
try {
|
||||||
const { dispatch } = getStore();
|
await fetchModelConfigByIdentifier(clone.ipAdapter.model);
|
||||||
dispatch(caLayerT2IAdaptersDeleted());
|
} catch {
|
||||||
if (!t2iAdapters.length) {
|
clone.ipAdapter.model = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clone.id = getIPALayerId(uuidv4());
|
||||||
|
clone.ipAdapter.id = uuidv4();
|
||||||
|
dispatch(ipaLayerRecalled(clone));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
t2iAdapters.forEach((t2iAdapters) => {
|
|
||||||
dispatch(caLayerAdded(t2iAdapters));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const recallIPAdapterV2: MetadataRecallFunc<IPAdapterConfigV2Metadata> = (ipAdapter) => {
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
getStore().dispatch(ipaLayerAdded(ipAdapter));
|
const clone = deepClone(layer);
|
||||||
};
|
// Strip out the uploaded mask image property - this is an intermediate image
|
||||||
|
clone.uploadedMaskImage = null;
|
||||||
|
|
||||||
const recallIPAdaptersV2: MetadataRecallFunc<IPAdapterConfigV2Metadata[]> = (ipAdapters) => {
|
for (const ipAdapter of clone.ipAdapters) {
|
||||||
const { dispatch } = getStore();
|
if (ipAdapter.image) {
|
||||||
dispatch(ipaLayersDeleted());
|
const imageDTO = await getImageDTO(ipAdapter.image.name);
|
||||||
if (!ipAdapters.length) {
|
if (!imageDTO) {
|
||||||
|
ipAdapter.image = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ipAdapter.model) {
|
||||||
|
try {
|
||||||
|
await fetchModelConfigByIdentifier(ipAdapter.model);
|
||||||
|
} catch {
|
||||||
|
ipAdapter.model = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipAdapter.id = uuidv4();
|
||||||
|
}
|
||||||
|
clone.id = getRGLayerId(uuidv4());
|
||||||
|
dispatch(rgLayerRecalled(clone));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ipAdapters.forEach((ipAdapter) => {
|
|
||||||
dispatch(ipaLayerAdded(ipAdapter));
|
if (layer.type === 'initial_image_layer') {
|
||||||
});
|
dispatch(iiLayerRecalled(layer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recallLayers: MetadataRecallFunc<Layer[]> = (layers) => {
|
||||||
|
const { dispatch } = getStore();
|
||||||
|
dispatch(allLayersDeleted());
|
||||||
|
for (const l of layers) {
|
||||||
|
recallLayer(l);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recallers = {
|
export const recallers = {
|
||||||
@@ -299,7 +344,6 @@ export const recallers = {
|
|||||||
cfgScale: recallCFGScale,
|
cfgScale: recallCFGScale,
|
||||||
cfgRescaleMultiplier: recallCFGRescaleMultiplier,
|
cfgRescaleMultiplier: recallCFGRescaleMultiplier,
|
||||||
scheduler: recallScheduler,
|
scheduler: recallScheduler,
|
||||||
initialImage: recallInitialImage,
|
|
||||||
width: recallWidth,
|
width: recallWidth,
|
||||||
height: recallHeight,
|
height: recallHeight,
|
||||||
steps: recallSteps,
|
steps: recallSteps,
|
||||||
@@ -324,10 +368,6 @@ export const recallers = {
|
|||||||
t2iAdapter: recallT2IAdapter,
|
t2iAdapter: recallT2IAdapter,
|
||||||
ipAdapters: recallIPAdapters,
|
ipAdapters: recallIPAdapters,
|
||||||
ipAdapter: recallIPAdapter,
|
ipAdapter: recallIPAdapter,
|
||||||
controlNetV2: recallControlNetV2,
|
layer: recallLayer,
|
||||||
controlNetsV2: recallControlNetsV2,
|
layers: recallLayers,
|
||||||
t2iAdapterV2: recallT2IAdapterV2,
|
|
||||||
t2iAdaptersV2: recallT2IAdaptersV2,
|
|
||||||
ipAdapterV2: recallIPAdapterV2,
|
|
||||||
ipAdaptersV2: recallIPAdaptersV2,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
import type { LoRA } from 'features/lora/store/loraSlice';
|
import type { LoRA } from 'features/lora/store/loraSlice';
|
||||||
import type {
|
import type {
|
||||||
ControlNetConfigMetadata,
|
ControlNetConfigMetadata,
|
||||||
ControlNetConfigV2Metadata,
|
|
||||||
IPAdapterConfigMetadata,
|
IPAdapterConfigMetadata,
|
||||||
IPAdapterConfigV2Metadata,
|
|
||||||
MetadataValidateFunc,
|
MetadataValidateFunc,
|
||||||
T2IAdapterConfigMetadata,
|
T2IAdapterConfigMetadata,
|
||||||
T2IAdapterConfigV2Metadata,
|
|
||||||
} from 'features/metadata/types';
|
} from 'features/metadata/types';
|
||||||
import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers';
|
import { InvalidModelConfigError } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterSDXLRefinerModel, ParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import type { BaseModelType } from 'services/api/types';
|
import type { BaseModelType } from 'services/api/types';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the given base model type against the currently-selected model's base type and throws an error if they are
|
* Checks the given base model type against the currently-selected model's base type and throws an error if they are
|
||||||
@@ -111,58 +110,39 @@ const validateIPAdapters: MetadataValidateFunc<IPAdapterConfigMetadata[]> = (ipA
|
|||||||
return new Promise((resolve) => resolve(validatedIPAdapters));
|
return new Promise((resolve) => resolve(validatedIPAdapters));
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateControlNetV2: MetadataValidateFunc<ControlNetConfigV2Metadata> = (controlNet) => {
|
const validateLayer: MetadataValidateFunc<Layer> = async (layer) => {
|
||||||
validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model');
|
if (layer.type === 'control_adapter_layer') {
|
||||||
return new Promise((resolve) => resolve(controlNet));
|
const model = layer.controlAdapter.model;
|
||||||
};
|
assert(model, 'Control Adapter layer missing model');
|
||||||
|
validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model');
|
||||||
const validateControlNetsV2: MetadataValidateFunc<ControlNetConfigV2Metadata[]> = (controlNets) => {
|
}
|
||||||
const validatedControlNets: ControlNetConfigV2Metadata[] = [];
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
controlNets.forEach((controlNet) => {
|
const model = layer.ipAdapter.model;
|
||||||
try {
|
assert(model, 'IP Adapter layer missing model');
|
||||||
validateBaseCompatibility(controlNet.model?.base, 'ControlNet incompatible with currently-selected model');
|
validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model');
|
||||||
validatedControlNets.push(controlNet);
|
}
|
||||||
} catch {
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
// This is a no-op - we want to continue validating the rest of the ControlNets, and an empty list is valid.
|
for (const ipa of layer.ipAdapters) {
|
||||||
|
const model = ipa.model;
|
||||||
|
assert(model, 'IP Adapter layer missing model');
|
||||||
|
validateBaseCompatibility(model.base, 'Layer incompatible with currently-selected model');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return new Promise((resolve) => resolve(validatedControlNets));
|
|
||||||
|
return layer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateT2IAdapterV2: MetadataValidateFunc<T2IAdapterConfigV2Metadata> = (t2iAdapter) => {
|
const validateLayers: MetadataValidateFunc<Layer[]> = async (layers) => {
|
||||||
validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model');
|
const validatedLayers: Layer[] = [];
|
||||||
return new Promise((resolve) => resolve(t2iAdapter));
|
for (const l of layers) {
|
||||||
};
|
|
||||||
|
|
||||||
const validateT2IAdaptersV2: MetadataValidateFunc<T2IAdapterConfigV2Metadata[]> = (t2iAdapters) => {
|
|
||||||
const validatedT2IAdapters: T2IAdapterConfigV2Metadata[] = [];
|
|
||||||
t2iAdapters.forEach((t2iAdapter) => {
|
|
||||||
try {
|
try {
|
||||||
validateBaseCompatibility(t2iAdapter.model?.base, 'T2I Adapter incompatible with currently-selected model');
|
const validated = await validateLayer(l);
|
||||||
validatedT2IAdapters.push(t2iAdapter);
|
validatedLayers.push(validated);
|
||||||
} catch {
|
} catch {
|
||||||
// This is a no-op - we want to continue validating the rest of the T2I Adapters, and an empty list is valid.
|
// This is a no-op - we want to continue validating the rest of the layers, and an empty list is valid.
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return new Promise((resolve) => resolve(validatedT2IAdapters));
|
return new Promise((resolve) => resolve(validatedLayers));
|
||||||
};
|
|
||||||
|
|
||||||
const validateIPAdapterV2: MetadataValidateFunc<IPAdapterConfigV2Metadata> = (ipAdapter) => {
|
|
||||||
validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model');
|
|
||||||
return new Promise((resolve) => resolve(ipAdapter));
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateIPAdaptersV2: MetadataValidateFunc<IPAdapterConfigV2Metadata[]> = (ipAdapters) => {
|
|
||||||
const validatedIPAdapters: IPAdapterConfigV2Metadata[] = [];
|
|
||||||
ipAdapters.forEach((ipAdapter) => {
|
|
||||||
try {
|
|
||||||
validateBaseCompatibility(ipAdapter.model?.base, 'IP Adapter incompatible with currently-selected model');
|
|
||||||
validatedIPAdapters.push(ipAdapter);
|
|
||||||
} catch {
|
|
||||||
// This is a no-op - we want to continue validating the rest of the IP Adapters, and an empty list is valid.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return new Promise((resolve) => resolve(validatedIPAdapters));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validators = {
|
export const validators = {
|
||||||
@@ -176,10 +156,6 @@ export const validators = {
|
|||||||
t2iAdapters: validateT2IAdapters,
|
t2iAdapters: validateT2IAdapters,
|
||||||
ipAdapter: validateIPAdapter,
|
ipAdapter: validateIPAdapter,
|
||||||
ipAdapters: validateIPAdapters,
|
ipAdapters: validateIPAdapters,
|
||||||
controlNetV2: validateControlNetV2,
|
layer: validateLayer,
|
||||||
controlNetsV2: validateControlNetsV2,
|
layers: validateLayers,
|
||||||
t2iAdapterV2: validateT2IAdapterV2,
|
|
||||||
t2iAdaptersV2: validateT2IAdaptersV2,
|
|
||||||
ipAdapterV2: validateIPAdapterV2,
|
|
||||||
ipAdaptersV2: validateIPAdaptersV2,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
|
|
||||||
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
|
||||||
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
|
||||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
|
||||||
@@ -23,7 +22,6 @@ const TopCenterPanel = () => {
|
|||||||
<ClearFlowButton />
|
<ClearFlowButton />
|
||||||
<SaveWorkflowButton />
|
<SaveWorkflowButton />
|
||||||
<WorkflowLibraryMenu />
|
<WorkflowLibraryMenu />
|
||||||
<ViewerButton />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,27 +2,28 @@ import { getStore } from 'app/store/nanostores/store';
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import {
|
import {
|
||||||
isControlAdapterLayer,
|
isControlAdapterLayer,
|
||||||
|
isInitialImageLayer,
|
||||||
isIPAdapterLayer,
|
isIPAdapterLayer,
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
rgLayerMaskImageUploaded,
|
rgLayerMaskImageUploaded,
|
||||||
} from 'features/controlLayers/store/controlLayersSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { RegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
import type { InitialImageLayer, Layer, RegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
||||||
import {
|
import type {
|
||||||
type ControlNetConfigV2,
|
ControlNetConfigV2,
|
||||||
type ImageWithDims,
|
ImageWithDims,
|
||||||
type IPAdapterConfigV2,
|
IPAdapterConfigV2,
|
||||||
isControlNetConfigV2,
|
ProcessorConfig,
|
||||||
isT2IAdapterConfigV2,
|
T2IAdapterConfigV2,
|
||||||
type ProcessorConfig,
|
|
||||||
type T2IAdapterConfigV2,
|
|
||||||
} from 'features/controlLayers/util/controlAdapters';
|
} from 'features/controlLayers/util/controlAdapters';
|
||||||
import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs';
|
import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs';
|
||||||
import type { ImageField } from 'features/nodes/types/common';
|
import type { ImageField } from 'features/nodes/types/common';
|
||||||
import {
|
import {
|
||||||
CONTROL_NET_COLLECT,
|
CONTROL_NET_COLLECT,
|
||||||
|
IMAGE_TO_LATENTS,
|
||||||
IP_ADAPTER_COLLECT,
|
IP_ADAPTER_COLLECT,
|
||||||
NEGATIVE_CONDITIONING,
|
NEGATIVE_CONDITIONING,
|
||||||
NEGATIVE_CONDITIONING_COLLECT,
|
NEGATIVE_CONDITIONING_COLLECT,
|
||||||
|
NOISE,
|
||||||
POSITIVE_CONDITIONING,
|
POSITIVE_CONDITIONING,
|
||||||
POSITIVE_CONDITIONING_COLLECT,
|
POSITIVE_CONDITIONING_COLLECT,
|
||||||
PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX,
|
PROMPT_REGION_INVERT_TENSOR_MASK_PREFIX,
|
||||||
@@ -30,17 +31,20 @@ import {
|
|||||||
PROMPT_REGION_NEGATIVE_COND_PREFIX,
|
PROMPT_REGION_NEGATIVE_COND_PREFIX,
|
||||||
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
|
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
|
||||||
PROMPT_REGION_POSITIVE_COND_PREFIX,
|
PROMPT_REGION_POSITIVE_COND_PREFIX,
|
||||||
|
RESIZE,
|
||||||
T2I_ADAPTER_COLLECT,
|
T2I_ADAPTER_COLLECT,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { upsertMetadata } from 'features/nodes/util/graph/metadata';
|
import { upsertMetadata } from 'features/nodes/util/graph/metadata';
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { getImageDTO, imagesApi } from 'services/api/endpoints/images';
|
import { getImageDTO, imagesApi } from 'services/api/endpoints/images';
|
||||||
import type {
|
import type {
|
||||||
|
BaseModelType,
|
||||||
CollectInvocation,
|
CollectInvocation,
|
||||||
ControlNetInvocation,
|
ControlNetInvocation,
|
||||||
CoreMetadataInvocation,
|
|
||||||
Edge,
|
Edge,
|
||||||
ImageDTO,
|
ImageDTO,
|
||||||
|
ImageResizeInvocation,
|
||||||
|
ImageToLatentsInvocation,
|
||||||
IPAdapterInvocation,
|
IPAdapterInvocation,
|
||||||
NonNullableGraph,
|
NonNullableGraph,
|
||||||
S,
|
S,
|
||||||
@@ -48,364 +52,33 @@ import type {
|
|||||||
} from 'services/api/types';
|
} from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
const buildControlImage = (
|
export const addControlLayersToGraph = async (
|
||||||
image: ImageWithDims | null,
|
state: RootState,
|
||||||
processedImage: ImageWithDims | null,
|
|
||||||
processorConfig: ProcessorConfig | null
|
|
||||||
): ImageField => {
|
|
||||||
if (processedImage && processorConfig) {
|
|
||||||
// We've processed the image in the app - use it for the control image.
|
|
||||||
return {
|
|
||||||
image_name: processedImage.imageName,
|
|
||||||
};
|
|
||||||
} else if (image) {
|
|
||||||
// No processor selected, and we have an image - the user provided a processed image, use it for the control image.
|
|
||||||
return {
|
|
||||||
image_name: image.imageName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
assert(false, 'Attempted to add unprocessed control image');
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildControlNetMetadata = (controlNet: ControlNetConfigV2): S['ControlNetMetadataField'] => {
|
|
||||||
const { beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet;
|
|
||||||
|
|
||||||
assert(model, 'ControlNet model is required');
|
|
||||||
assert(image, 'ControlNet image is required');
|
|
||||||
|
|
||||||
const processed_image =
|
|
||||||
processedImage && processorConfig
|
|
||||||
? {
|
|
||||||
image_name: processedImage.imageName,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
control_model: model,
|
|
||||||
control_weight: weight,
|
|
||||||
control_mode: controlMode,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
resize_mode: 'just_resize',
|
|
||||||
image: {
|
|
||||||
image_name: image.imageName,
|
|
||||||
},
|
|
||||||
processed_image,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
|
||||||
if (graph.nodes[CONTROL_NET_COLLECT]) {
|
|
||||||
// You see, we've already got one!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Add the ControlNet collector
|
|
||||||
const controlNetIterateNode: CollectInvocation = {
|
|
||||||
id: CONTROL_NET_COLLECT,
|
|
||||||
type: 'collect',
|
|
||||||
is_intermediate: true,
|
|
||||||
};
|
|
||||||
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: CONTROL_NET_COLLECT, field: 'collection' },
|
|
||||||
destination: {
|
|
||||||
node_id: denoiseNodeId,
|
|
||||||
field: 'control',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addGlobalControlNetsToGraph = async (
|
|
||||||
controlNets: ControlNetConfigV2[],
|
|
||||||
graph: NonNullableGraph,
|
graph: NonNullableGraph,
|
||||||
denoiseNodeId: string
|
denoiseNodeId: string
|
||||||
) => {
|
): Promise<Layer[]> => {
|
||||||
if (controlNets.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
|
|
||||||
addControlNetCollectorSafe(graph, denoiseNodeId);
|
|
||||||
|
|
||||||
for (const controlNet of controlNets) {
|
|
||||||
if (!controlNet.model) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet;
|
|
||||||
|
|
||||||
const controlNetNode: ControlNetInvocation = {
|
|
||||||
id: `control_net_${id}`,
|
|
||||||
type: 'controlnet',
|
|
||||||
is_intermediate: true,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
control_mode: controlMode,
|
|
||||||
resize_mode: 'just_resize',
|
|
||||||
control_model: model,
|
|
||||||
control_weight: weight,
|
|
||||||
image: buildControlImage(image, processedImage, processorConfig),
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.nodes[controlNetNode.id] = controlNetNode;
|
|
||||||
|
|
||||||
controlNetMetadata.push(buildControlNetMetadata(controlNet));
|
|
||||||
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: controlNetNode.id, field: 'control' },
|
|
||||||
destination: {
|
|
||||||
node_id: CONTROL_NET_COLLECT,
|
|
||||||
field: 'item',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
upsertMetadata(graph, { controlnets: controlNetMetadata });
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildT2IAdapterMetadata = (t2iAdapter: T2IAdapterConfigV2): S['T2IAdapterMetadataField'] => {
|
|
||||||
const { beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter;
|
|
||||||
|
|
||||||
assert(model, 'T2I Adapter model is required');
|
|
||||||
assert(image, 'T2I Adapter image is required');
|
|
||||||
|
|
||||||
const processed_image =
|
|
||||||
processedImage && processorConfig
|
|
||||||
? {
|
|
||||||
image_name: processedImage.imageName,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
t2i_adapter_model: model,
|
|
||||||
weight,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
resize_mode: 'just_resize',
|
|
||||||
image: {
|
|
||||||
image_name: image.imageName,
|
|
||||||
},
|
|
||||||
processed_image,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
|
||||||
if (graph.nodes[T2I_ADAPTER_COLLECT]) {
|
|
||||||
// You see, we've already got one!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
|
|
||||||
const t2iAdapterCollectNode: CollectInvocation = {
|
|
||||||
id: T2I_ADAPTER_COLLECT,
|
|
||||||
type: 'collect',
|
|
||||||
is_intermediate: true,
|
|
||||||
};
|
|
||||||
graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode;
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' },
|
|
||||||
destination: {
|
|
||||||
node_id: denoiseNodeId,
|
|
||||||
field: 't2i_adapter',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addGlobalT2IAdaptersToGraph = async (
|
|
||||||
t2iAdapters: T2IAdapterConfigV2[],
|
|
||||||
graph: NonNullableGraph,
|
|
||||||
denoiseNodeId: string
|
|
||||||
) => {
|
|
||||||
if (t2iAdapters.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = [];
|
|
||||||
addT2IAdapterCollectorSafe(graph, denoiseNodeId);
|
|
||||||
|
|
||||||
for (const t2iAdapter of t2iAdapters) {
|
|
||||||
if (!t2iAdapter.model) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter;
|
|
||||||
|
|
||||||
const t2iAdapterNode: T2IAdapterInvocation = {
|
|
||||||
id: `t2i_adapter_${id}`,
|
|
||||||
type: 't2i_adapter',
|
|
||||||
is_intermediate: true,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
resize_mode: 'just_resize',
|
|
||||||
t2i_adapter_model: model,
|
|
||||||
weight: weight,
|
|
||||||
image: buildControlImage(image, processedImage, processorConfig),
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.nodes[t2iAdapterNode.id] = t2iAdapterNode;
|
|
||||||
|
|
||||||
t2iAdapterMetadata.push(buildT2IAdapterMetadata(t2iAdapter));
|
|
||||||
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' },
|
|
||||||
destination: {
|
|
||||||
node_id: T2I_ADAPTER_COLLECT,
|
|
||||||
field: 'item',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
upsertMetadata(graph, { t2iAdapters: t2iAdapterMetadata });
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildIPAdapterMetadata = (ipAdapter: IPAdapterConfigV2): S['IPAdapterMetadataField'] => {
|
|
||||||
const { weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
|
|
||||||
|
|
||||||
assert(model, 'IP Adapter model is required');
|
|
||||||
assert(image, 'IP Adapter image is required');
|
|
||||||
|
|
||||||
return {
|
|
||||||
ip_adapter_model: model,
|
|
||||||
clip_vision_model: clipVisionModel,
|
|
||||||
weight,
|
|
||||||
method,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
image: {
|
|
||||||
image_name: image.imageName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
|
||||||
if (graph.nodes[IP_ADAPTER_COLLECT]) {
|
|
||||||
// You see, we've already got one!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipAdapterCollectNode: CollectInvocation = {
|
|
||||||
id: IP_ADAPTER_COLLECT,
|
|
||||||
type: 'collect',
|
|
||||||
is_intermediate: true,
|
|
||||||
};
|
|
||||||
graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode;
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' },
|
|
||||||
destination: {
|
|
||||||
node_id: denoiseNodeId,
|
|
||||||
field: 'ip_adapter',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addGlobalIPAdaptersToGraph = async (
|
|
||||||
ipAdapters: IPAdapterConfigV2[],
|
|
||||||
graph: NonNullableGraph,
|
|
||||||
denoiseNodeId: string
|
|
||||||
) => {
|
|
||||||
if (ipAdapters.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = [];
|
|
||||||
addIPAdapterCollectorSafe(graph, denoiseNodeId);
|
|
||||||
|
|
||||||
for (const ipAdapter of ipAdapters) {
|
|
||||||
const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
|
|
||||||
assert(image, 'IP Adapter image is required');
|
|
||||||
assert(model, 'IP Adapter model is required');
|
|
||||||
|
|
||||||
const ipAdapterNode: IPAdapterInvocation = {
|
|
||||||
id: `ip_adapter_${id}`,
|
|
||||||
type: 'ip_adapter',
|
|
||||||
is_intermediate: true,
|
|
||||||
weight,
|
|
||||||
method,
|
|
||||||
ip_adapter_model: model,
|
|
||||||
clip_vision_model: clipVisionModel,
|
|
||||||
begin_step_percent: beginEndStepPct[0],
|
|
||||||
end_step_percent: beginEndStepPct[1],
|
|
||||||
image: {
|
|
||||||
image_name: image.imageName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.nodes[ipAdapterNode.id] = ipAdapterNode;
|
|
||||||
|
|
||||||
ipAdapterMetdata.push(buildIPAdapterMetadata(ipAdapter));
|
|
||||||
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: ipAdapterNode.id, field: 'ip_adapter' },
|
|
||||||
destination: {
|
|
||||||
node_id: IP_ADAPTER_COLLECT,
|
|
||||||
field: 'item',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
upsertMetadata(graph, { ipAdapters: ipAdapterMetdata });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
|
|
||||||
const mainModel = state.generation.model;
|
const mainModel = state.generation.model;
|
||||||
assert(mainModel, 'Missing main model when building graph');
|
assert(mainModel, 'Missing main model when building graph');
|
||||||
const isSDXL = mainModel.base === 'sdxl';
|
const isSDXL = mainModel.base === 'sdxl';
|
||||||
|
|
||||||
// Add global control adapters
|
// Filter out layers with incompatible base model, missing control image
|
||||||
const globalControlNets = state.controlLayers.present.layers
|
const validLayers = state.controlLayers.present.layers.filter((l) => isValidLayer(l, mainModel.base));
|
||||||
// Must be a CA layer
|
|
||||||
.filter(isControlAdapterLayer)
|
|
||||||
// Must be enabled
|
|
||||||
.filter((l) => l.isEnabled)
|
|
||||||
// We want the CAs themselves
|
|
||||||
.map((l) => l.controlAdapter)
|
|
||||||
// Must be a ControlNet
|
|
||||||
.filter(isControlNetConfigV2)
|
|
||||||
.filter((ca) => {
|
|
||||||
const hasModel = Boolean(ca.model);
|
|
||||||
const modelMatchesBase = ca.model?.base === mainModel.base;
|
|
||||||
const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig);
|
|
||||||
return hasModel && modelMatchesBase && hasControlImage;
|
|
||||||
});
|
|
||||||
addGlobalControlNetsToGraph(globalControlNets, graph, denoiseNodeId);
|
|
||||||
|
|
||||||
const globalT2IAdapters = state.controlLayers.present.layers
|
const validControlAdapters = validLayers.filter(isControlAdapterLayer).map((l) => l.controlAdapter);
|
||||||
// Must be a CA layer
|
for (const ca of validControlAdapters) {
|
||||||
.filter(isControlAdapterLayer)
|
addGlobalControlAdapterToGraph(ca, graph, denoiseNodeId);
|
||||||
// Must be enabled
|
}
|
||||||
.filter((l) => l.isEnabled)
|
|
||||||
// We want the CAs themselves
|
|
||||||
.map((l) => l.controlAdapter)
|
|
||||||
// Must have a ControlNet CA
|
|
||||||
.filter(isT2IAdapterConfigV2)
|
|
||||||
.filter((ca) => {
|
|
||||||
const hasModel = Boolean(ca.model);
|
|
||||||
const modelMatchesBase = ca.model?.base === mainModel.base;
|
|
||||||
const hasControlImage = ca.image || (ca.processedImage && ca.processorConfig);
|
|
||||||
return hasModel && modelMatchesBase && hasControlImage;
|
|
||||||
});
|
|
||||||
addGlobalT2IAdaptersToGraph(globalT2IAdapters, graph, denoiseNodeId);
|
|
||||||
|
|
||||||
const globalIPAdapters = state.controlLayers.present.layers
|
const validIPAdapters = validLayers.filter(isIPAdapterLayer).map((l) => l.ipAdapter);
|
||||||
// Must be an IP Adapter layer
|
for (const ipAdapter of validIPAdapters) {
|
||||||
.filter(isIPAdapterLayer)
|
addGlobalIPAdapterToGraph(ipAdapter, graph, denoiseNodeId);
|
||||||
// Must be enabled
|
}
|
||||||
.filter((l) => l.isEnabled)
|
|
||||||
// We want the IP Adapters themselves
|
|
||||||
.map((l) => l.ipAdapter)
|
|
||||||
.filter((ca) => {
|
|
||||||
const hasModel = Boolean(ca.model);
|
|
||||||
const modelMatchesBase = ca.model?.base === mainModel.base;
|
|
||||||
const hasControlImage = Boolean(ca.image);
|
|
||||||
return hasModel && modelMatchesBase && hasControlImage;
|
|
||||||
});
|
|
||||||
addGlobalIPAdaptersToGraph(globalIPAdapters, graph, denoiseNodeId);
|
|
||||||
|
|
||||||
const rgLayers = state.controlLayers.present.layers
|
|
||||||
// Only RG layers are get masks
|
|
||||||
.filter(isRegionalGuidanceLayer)
|
|
||||||
// Only visible layers are rendered on the canvas
|
|
||||||
.filter((l) => l.isEnabled)
|
|
||||||
// Only layers with prompts get added to the graph
|
|
||||||
.filter((l) => {
|
|
||||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
|
||||||
const hasIPAdapter = l.ipAdapters.length !== 0;
|
|
||||||
return hasTextPrompt || hasIPAdapter;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const initialImageLayers = validLayers.filter(isInitialImageLayer);
|
||||||
|
assert(initialImageLayers.length <= 1, 'Only one initial image layer allowed');
|
||||||
|
if (initialImageLayers[0]) {
|
||||||
|
addInitialImageLayerToGraph(state, graph, denoiseNodeId, initialImageLayers[0]);
|
||||||
|
}
|
||||||
// TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing
|
// TODO: We should probably just use conditioning collectors by default, and skip all this fanagling with re-routing
|
||||||
// the existing conditioning nodes.
|
// the existing conditioning nodes.
|
||||||
|
|
||||||
@@ -468,11 +141,12 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const layerIds = rgLayers.map((l) => l.id);
|
const validRGLayers = validLayers.filter(isRegionalGuidanceLayer);
|
||||||
|
const layerIds = validRGLayers.map((l) => l.id);
|
||||||
const blobs = await getRegionalPromptLayerBlobs(layerIds);
|
const blobs = await getRegionalPromptLayerBlobs(layerIds);
|
||||||
assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs');
|
assert(size(blobs) === size(layerIds), 'Mismatch between layer IDs and blobs');
|
||||||
|
|
||||||
for (const layer of rgLayers) {
|
for (const layer of validRGLayers) {
|
||||||
const blob = blobs[layer.id];
|
const blob = blobs[layer.id];
|
||||||
assert(blob, `Blob for layer ${layer.id} not found`);
|
assert(blob, `Blob for layer ${layer.id} not found`);
|
||||||
// Upload the mask image, or get the cached image if it exists
|
// Upload the mask image, or get the cached image if it exists
|
||||||
@@ -623,15 +297,11 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(psyche): For some reason, I have to explicitly annotate regionalIPAdapters here. Not sure why.
|
const validRegionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipa) =>
|
||||||
const regionalIPAdapters: IPAdapterConfigV2[] = layer.ipAdapters.filter((ipAdapter) => {
|
isValidIPAdapter(ipa, mainModel.base)
|
||||||
const hasModel = Boolean(ipAdapter.model);
|
);
|
||||||
const modelMatchesBase = ipAdapter.model?.base === mainModel.base;
|
|
||||||
const hasControlImage = Boolean(ipAdapter.image);
|
|
||||||
return hasModel && modelMatchesBase && hasControlImage;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const ipAdapter of regionalIPAdapters) {
|
for (const ipAdapter of validRegionalIPAdapters) {
|
||||||
addIPAdapterCollectorSafe(graph, denoiseNodeId);
|
addIPAdapterCollectorSafe(graph, denoiseNodeId);
|
||||||
const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
|
const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
|
||||||
assert(model, 'IP Adapter model is required');
|
assert(model, 'IP Adapter model is required');
|
||||||
@@ -648,7 +318,7 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
|
|||||||
begin_step_percent: beginEndStepPct[0],
|
begin_step_percent: beginEndStepPct[0],
|
||||||
end_step_percent: beginEndStepPct[1],
|
end_step_percent: beginEndStepPct[1],
|
||||||
image: {
|
image: {
|
||||||
image_name: image.imageName,
|
image_name: image.name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -669,11 +339,14 @@ export const addControlLayersToGraph = async (state: RootState, graph: NonNullab
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upsertMetadata(graph, { control_layers: { layers: validLayers, version: state.controlLayers.present._version } });
|
||||||
|
return validLayers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise<ImageDTO> => {
|
const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise<ImageDTO> => {
|
||||||
if (layer.uploadedMaskImage) {
|
if (layer.uploadedMaskImage) {
|
||||||
const imageDTO = await getImageDTO(layer.uploadedMaskImage.imageName);
|
const imageDTO = await getImageDTO(layer.uploadedMaskImage.name);
|
||||||
if (imageDTO) {
|
if (imageDTO) {
|
||||||
return imageDTO;
|
return imageDTO;
|
||||||
}
|
}
|
||||||
@@ -690,3 +363,345 @@ const getMaskImage = async (layer: RegionalGuidanceLayer, blob: Blob): Promise<I
|
|||||||
dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO }));
|
dispatch(rgLayerMaskImageUploaded({ layerId: layer.id, imageDTO }));
|
||||||
return imageDTO;
|
return imageDTO;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildControlImage = (
|
||||||
|
image: ImageWithDims | null,
|
||||||
|
processedImage: ImageWithDims | null,
|
||||||
|
processorConfig: ProcessorConfig | null
|
||||||
|
): ImageField => {
|
||||||
|
if (processedImage && processorConfig) {
|
||||||
|
// We've processed the image in the app - use it for the control image.
|
||||||
|
return {
|
||||||
|
image_name: processedImage.name,
|
||||||
|
};
|
||||||
|
} else if (image) {
|
||||||
|
// No processor selected, and we have an image - the user provided a processed image, use it for the control image.
|
||||||
|
return {
|
||||||
|
image_name: image.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
assert(false, 'Attempted to add unprocessed control image');
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGlobalControlAdapterToGraph = (
|
||||||
|
controlAdapter: ControlNetConfigV2 | T2IAdapterConfigV2,
|
||||||
|
graph: NonNullableGraph,
|
||||||
|
denoiseNodeId: string
|
||||||
|
) => {
|
||||||
|
if (controlAdapter.type === 'controlnet') {
|
||||||
|
addGlobalControlNetToGraph(controlAdapter, graph, denoiseNodeId);
|
||||||
|
}
|
||||||
|
if (controlAdapter.type === 't2i_adapter') {
|
||||||
|
addGlobalT2IAdapterToGraph(controlAdapter, graph, denoiseNodeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addControlNetCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
if (graph.nodes[CONTROL_NET_COLLECT]) {
|
||||||
|
// You see, we've already got one!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Add the ControlNet collector
|
||||||
|
const controlNetIterateNode: CollectInvocation = {
|
||||||
|
id: CONTROL_NET_COLLECT,
|
||||||
|
type: 'collect',
|
||||||
|
is_intermediate: true,
|
||||||
|
};
|
||||||
|
graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode;
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: CONTROL_NET_COLLECT, field: 'collection' },
|
||||||
|
destination: {
|
||||||
|
node_id: denoiseNodeId,
|
||||||
|
field: 'control',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGlobalControlNetToGraph = (controlNet: ControlNetConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
const { id, beginEndStepPct, controlMode, image, model, processedImage, processorConfig, weight } = controlNet;
|
||||||
|
assert(model, 'ControlNet model is required');
|
||||||
|
const controlImage = buildControlImage(image, processedImage, processorConfig);
|
||||||
|
addControlNetCollectorSafe(graph, denoiseNodeId);
|
||||||
|
|
||||||
|
const controlNetNode: ControlNetInvocation = {
|
||||||
|
id: `control_net_${id}`,
|
||||||
|
type: 'controlnet',
|
||||||
|
is_intermediate: true,
|
||||||
|
begin_step_percent: beginEndStepPct[0],
|
||||||
|
end_step_percent: beginEndStepPct[1],
|
||||||
|
control_mode: controlMode,
|
||||||
|
resize_mode: 'just_resize',
|
||||||
|
control_model: model,
|
||||||
|
control_weight: weight,
|
||||||
|
image: controlImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[controlNetNode.id] = controlNetNode;
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: controlNetNode.id, field: 'control' },
|
||||||
|
destination: {
|
||||||
|
node_id: CONTROL_NET_COLLECT,
|
||||||
|
field: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addT2IAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
if (graph.nodes[T2I_ADAPTER_COLLECT]) {
|
||||||
|
// You see, we've already got one!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
|
||||||
|
const t2iAdapterCollectNode: CollectInvocation = {
|
||||||
|
id: T2I_ADAPTER_COLLECT,
|
||||||
|
type: 'collect',
|
||||||
|
is_intermediate: true,
|
||||||
|
};
|
||||||
|
graph.nodes[T2I_ADAPTER_COLLECT] = t2iAdapterCollectNode;
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: T2I_ADAPTER_COLLECT, field: 'collection' },
|
||||||
|
destination: {
|
||||||
|
node_id: denoiseNodeId,
|
||||||
|
field: 't2i_adapter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGlobalT2IAdapterToGraph = (t2iAdapter: T2IAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
const { id, beginEndStepPct, image, model, processedImage, processorConfig, weight } = t2iAdapter;
|
||||||
|
assert(model, 'T2I Adapter model is required');
|
||||||
|
const controlImage = buildControlImage(image, processedImage, processorConfig);
|
||||||
|
addT2IAdapterCollectorSafe(graph, denoiseNodeId);
|
||||||
|
|
||||||
|
const t2iAdapterNode: T2IAdapterInvocation = {
|
||||||
|
id: `t2i_adapter_${id}`,
|
||||||
|
type: 't2i_adapter',
|
||||||
|
is_intermediate: true,
|
||||||
|
begin_step_percent: beginEndStepPct[0],
|
||||||
|
end_step_percent: beginEndStepPct[1],
|
||||||
|
resize_mode: 'just_resize',
|
||||||
|
t2i_adapter_model: model,
|
||||||
|
weight: weight,
|
||||||
|
image: controlImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[t2iAdapterNode.id] = t2iAdapterNode;
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: t2iAdapterNode.id, field: 't2i_adapter' },
|
||||||
|
destination: {
|
||||||
|
node_id: T2I_ADAPTER_COLLECT,
|
||||||
|
field: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addIPAdapterCollectorSafe = (graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
if (graph.nodes[IP_ADAPTER_COLLECT]) {
|
||||||
|
// You see, we've already got one!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipAdapterCollectNode: CollectInvocation = {
|
||||||
|
id: IP_ADAPTER_COLLECT,
|
||||||
|
type: 'collect',
|
||||||
|
is_intermediate: true,
|
||||||
|
};
|
||||||
|
graph.nodes[IP_ADAPTER_COLLECT] = ipAdapterCollectNode;
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: IP_ADAPTER_COLLECT, field: 'collection' },
|
||||||
|
destination: {
|
||||||
|
node_id: denoiseNodeId,
|
||||||
|
field: 'ip_adapter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addGlobalIPAdapterToGraph = (ipAdapter: IPAdapterConfigV2, graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
|
addIPAdapterCollectorSafe(graph, denoiseNodeId);
|
||||||
|
const { id, weight, model, clipVisionModel, method, beginEndStepPct, image } = ipAdapter;
|
||||||
|
assert(image, 'IP Adapter image is required');
|
||||||
|
assert(model, 'IP Adapter model is required');
|
||||||
|
|
||||||
|
const ipAdapterNode: IPAdapterInvocation = {
|
||||||
|
id: `ip_adapter_${id}`,
|
||||||
|
type: 'ip_adapter',
|
||||||
|
is_intermediate: true,
|
||||||
|
weight,
|
||||||
|
method,
|
||||||
|
ip_adapter_model: model,
|
||||||
|
clip_vision_model: clipVisionModel,
|
||||||
|
begin_step_percent: beginEndStepPct[0],
|
||||||
|
end_step_percent: beginEndStepPct[1],
|
||||||
|
image: {
|
||||||
|
image_name: image.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[ipAdapterNode.id] = ipAdapterNode;
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: ipAdapterNode.id, field: 'ip_adapter' },
|
||||||
|
destination: {
|
||||||
|
node_id: IP_ADAPTER_COLLECT,
|
||||||
|
field: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addInitialImageLayerToGraph = (
|
||||||
|
state: RootState,
|
||||||
|
graph: NonNullableGraph,
|
||||||
|
denoiseNodeId: string,
|
||||||
|
layer: InitialImageLayer
|
||||||
|
) => {
|
||||||
|
const { vaePrecision, model } = state.generation;
|
||||||
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
assert(layer.isEnabled, 'Initial image layer is not enabled');
|
||||||
|
assert(layer.image, 'Initial image layer has no image');
|
||||||
|
|
||||||
|
const isSDXL = model?.base === 'sdxl';
|
||||||
|
const useRefinerStartEnd = isSDXL && Boolean(refinerModel);
|
||||||
|
|
||||||
|
const denoiseNode = graph.nodes[denoiseNodeId];
|
||||||
|
assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`);
|
||||||
|
|
||||||
|
const { denoisingStrength } = layer;
|
||||||
|
denoiseNode.denoising_start = useRefinerStartEnd
|
||||||
|
? Math.min(refinerStart, 1 - denoisingStrength)
|
||||||
|
: 1 - denoisingStrength;
|
||||||
|
denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1;
|
||||||
|
|
||||||
|
const i2lNode: ImageToLatentsInvocation = {
|
||||||
|
type: 'i2l',
|
||||||
|
id: IMAGE_TO_LATENTS,
|
||||||
|
is_intermediate: true,
|
||||||
|
use_cache: true,
|
||||||
|
fp32: vaePrecision === 'fp32',
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[i2lNode.id] = i2lNode;
|
||||||
|
graph.edges.push({
|
||||||
|
source: {
|
||||||
|
node_id: IMAGE_TO_LATENTS,
|
||||||
|
field: 'latents',
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
node_id: denoiseNode.id,
|
||||||
|
field: 'latents',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (layer.image.width !== width || layer.image.height !== height) {
|
||||||
|
// The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS`
|
||||||
|
|
||||||
|
// Create a resize node, explicitly setting its image
|
||||||
|
const resizeNode: ImageResizeInvocation = {
|
||||||
|
id: RESIZE,
|
||||||
|
type: 'img_resize',
|
||||||
|
image: {
|
||||||
|
image_name: layer.image.name,
|
||||||
|
},
|
||||||
|
is_intermediate: true,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.nodes[RESIZE] = resizeNode;
|
||||||
|
|
||||||
|
// The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS`
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: RESIZE, field: 'image' },
|
||||||
|
destination: {
|
||||||
|
node_id: IMAGE_TO_LATENTS,
|
||||||
|
field: 'image',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// The `RESIZE` node also passes its width and height to `NOISE`
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: RESIZE, field: 'width' },
|
||||||
|
destination: {
|
||||||
|
node_id: NOISE,
|
||||||
|
field: 'width',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: RESIZE, field: 'height' },
|
||||||
|
destination: {
|
||||||
|
node_id: NOISE,
|
||||||
|
field: 'height',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly
|
||||||
|
i2lNode.image = {
|
||||||
|
image_name: layer.image.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass the image's dimensions to the `NOISE` node
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: IMAGE_TO_LATENTS, field: 'width' },
|
||||||
|
destination: {
|
||||||
|
node_id: NOISE,
|
||||||
|
field: 'width',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
graph.edges.push({
|
||||||
|
source: { node_id: IMAGE_TO_LATENTS, field: 'height' },
|
||||||
|
destination: {
|
||||||
|
node_id: NOISE,
|
||||||
|
field: 'height',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertMetadata(graph, { generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidControlAdapter = (ca: ControlNetConfigV2 | T2IAdapterConfigV2, base: BaseModelType): boolean => {
|
||||||
|
// Must be have a model that matches the current base and must have a control image
|
||||||
|
const hasModel = Boolean(ca.model);
|
||||||
|
const modelMatchesBase = ca.model?.base === base;
|
||||||
|
const hasControlImage = Boolean(ca.image || (ca.processedImage && ca.processorConfig));
|
||||||
|
return hasModel && modelMatchesBase && hasControlImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidIPAdapter = (ipa: IPAdapterConfigV2, base: BaseModelType): boolean => {
|
||||||
|
// Must be have a model that matches the current base and must have a control image
|
||||||
|
const hasModel = Boolean(ipa.model);
|
||||||
|
const modelMatchesBase = ipa.model?.base === base;
|
||||||
|
const hasImage = Boolean(ipa.image);
|
||||||
|
return hasModel && modelMatchesBase && hasImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidLayer = (layer: Layer, base: BaseModelType) => {
|
||||||
|
if (!layer.isEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isControlAdapterLayer(layer)) {
|
||||||
|
return isValidControlAdapter(layer.controlAdapter, base);
|
||||||
|
}
|
||||||
|
if (isIPAdapterLayer(layer)) {
|
||||||
|
return isValidIPAdapter(layer.ipAdapter, base);
|
||||||
|
}
|
||||||
|
if (isInitialImageLayer(layer)) {
|
||||||
|
if (!layer.image) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
|
if (layer.maskObjects.length === 0) {
|
||||||
|
// Layer has no mask, meaning any guidance would be applied to an empty region.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasTextPrompt = Boolean(layer.positivePrompt) || Boolean(layer.negativePrompt);
|
||||||
|
const hasIPAdapter = layer.ipAdapters.filter((ipa) => isValidIPAdapter(ipa, base)).length > 0;
|
||||||
|
return hasTextPrompt || hasIPAdapter;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
|
||||||
import { isInitialImageLayer } from 'features/controlLayers/store/controlLayersSlice';
|
|
||||||
import { upsertMetadata } from 'features/nodes/util/graph/metadata';
|
|
||||||
import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types';
|
|
||||||
import { assert } from 'tsafe';
|
|
||||||
|
|
||||||
import { IMAGE_TO_LATENTS, NOISE, RESIZE } from './constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if an initial image was added, false if not.
|
|
||||||
*/
|
|
||||||
export const addInitialImageToLinearGraph = (
|
|
||||||
state: RootState,
|
|
||||||
graph: NonNullableGraph,
|
|
||||||
denoiseNodeId: string
|
|
||||||
): boolean => {
|
|
||||||
// Remove Existing UNet Connections
|
|
||||||
const { img2imgStrength, vaePrecision, model } = state.generation;
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
|
||||||
const { width, height } = state.controlLayers.present.size;
|
|
||||||
const initialImageLayer = state.controlLayers.present.layers.find(isInitialImageLayer);
|
|
||||||
const initialImage = initialImageLayer?.isEnabled ? initialImageLayer?.image : null;
|
|
||||||
|
|
||||||
if (!initialImage) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSDXL = model?.base === 'sdxl';
|
|
||||||
const useRefinerStartEnd = isSDXL && Boolean(refinerModel);
|
|
||||||
|
|
||||||
const denoiseNode = graph.nodes[denoiseNodeId];
|
|
||||||
assert(denoiseNode?.type === 'denoise_latents', `Missing denoise node or incorrect type: ${denoiseNode?.type}`);
|
|
||||||
|
|
||||||
denoiseNode.denoising_start = useRefinerStartEnd ? Math.min(refinerStart, 1 - img2imgStrength) : 1 - img2imgStrength;
|
|
||||||
denoiseNode.denoising_end = useRefinerStartEnd ? refinerStart : 1;
|
|
||||||
|
|
||||||
// We conditionally hook the image in depending on if a resize is needed
|
|
||||||
const i2lNode: ImageToLatentsInvocation = {
|
|
||||||
type: 'i2l',
|
|
||||||
id: IMAGE_TO_LATENTS,
|
|
||||||
is_intermediate: true,
|
|
||||||
use_cache: true,
|
|
||||||
fp32: vaePrecision === 'fp32',
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.nodes[i2lNode.id] = i2lNode;
|
|
||||||
graph.edges.push({
|
|
||||||
source: {
|
|
||||||
node_id: IMAGE_TO_LATENTS,
|
|
||||||
field: 'latents',
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
node_id: denoiseNode.id,
|
|
||||||
field: 'latents',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (initialImage.width !== width || initialImage.height !== height) {
|
|
||||||
// The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS`
|
|
||||||
|
|
||||||
// Create a resize node, explicitly setting its image
|
|
||||||
const resizeNode: ImageResizeInvocation = {
|
|
||||||
id: RESIZE,
|
|
||||||
type: 'img_resize',
|
|
||||||
image: {
|
|
||||||
image_name: initialImage.imageName,
|
|
||||||
},
|
|
||||||
is_intermediate: true,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
};
|
|
||||||
|
|
||||||
graph.nodes[RESIZE] = resizeNode;
|
|
||||||
|
|
||||||
// The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS`
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: RESIZE, field: 'image' },
|
|
||||||
destination: {
|
|
||||||
node_id: IMAGE_TO_LATENTS,
|
|
||||||
field: 'image',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// The `RESIZE` node also passes its width and height to `NOISE`
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: RESIZE, field: 'width' },
|
|
||||||
destination: {
|
|
||||||
node_id: NOISE,
|
|
||||||
field: 'width',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: RESIZE, field: 'height' },
|
|
||||||
destination: {
|
|
||||||
node_id: NOISE,
|
|
||||||
field: 'height',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// We are not resizing, so we need to set the image on the `IMAGE_TO_LATENTS` node explicitly
|
|
||||||
i2lNode.image = {
|
|
||||||
image_name: initialImage.imageName,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pass the image's dimensions to the `NOISE` node
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: IMAGE_TO_LATENTS, field: 'width' },
|
|
||||||
destination: {
|
|
||||||
node_id: NOISE,
|
|
||||||
field: 'width',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
graph.edges.push({
|
|
||||||
source: { node_id: IMAGE_TO_LATENTS, field: 'height' },
|
|
||||||
destination: {
|
|
||||||
node_id: NOISE,
|
|
||||||
field: 'height',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
upsertMetadata(graph, {
|
|
||||||
generation_mode: isSDXL ? 'sdxl_img2img' : 'img2img',
|
|
||||||
strength: img2imgStrength,
|
|
||||||
init_image: initialImage.imageName,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
|
import { isInitialImageLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
||||||
import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph';
|
|
||||||
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||||
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
@@ -232,24 +232,23 @@ export const buildGenerationTabGraph = async (state: RootState): Promise<NonNull
|
|||||||
LATENTS_TO_IMAGE
|
LATENTS_TO_IMAGE
|
||||||
);
|
);
|
||||||
|
|
||||||
const didAddInitialImage = addInitialImageToLinearGraph(state, graph, DENOISE_LATENTS);
|
|
||||||
|
|
||||||
// Add Seamless To Graph
|
// Add Seamless To Graph
|
||||||
if (seamlessXAxis || seamlessYAxis) {
|
if (seamlessXAxis || seamlessYAxis) {
|
||||||
addSeamlessToLinearGraph(state, graph, modelLoaderNodeId);
|
addSeamlessToLinearGraph(state, graph, modelLoaderNodeId);
|
||||||
modelLoaderNodeId = SEAMLESS;
|
modelLoaderNodeId = SEAMLESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionally add custom VAE
|
|
||||||
await addVAEToGraph(state, graph, modelLoaderNodeId);
|
|
||||||
|
|
||||||
// add LoRA support
|
// add LoRA support
|
||||||
await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId);
|
await addLoRAsToGraph(state, graph, DENOISE_LATENTS, modelLoaderNodeId);
|
||||||
|
|
||||||
await addControlLayersToGraph(state, graph, DENOISE_LATENTS);
|
const addedLayers = await addControlLayersToGraph(state, graph, DENOISE_LATENTS);
|
||||||
|
|
||||||
|
// optionally add custom VAE
|
||||||
|
await addVAEToGraph(state, graph, modelLoaderNodeId);
|
||||||
|
|
||||||
|
const shouldUseHRF = !addedLayers.some((l) => isInitialImageLayer(l) || isRegionalGuidanceLayer(l));
|
||||||
// High resolution fix.
|
// High resolution fix.
|
||||||
if (state.hrf.hrfEnabled && !didAddInitialImage) {
|
if (state.hrf.hrfEnabled && shouldUseHRF) {
|
||||||
addHrfToGraph(state, graph);
|
addHrfToGraph(state, graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
||||||
import { addInitialImageToLinearGraph } from 'features/nodes/util/graph/addInitialImageToLinearGraph';
|
|
||||||
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
|
import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph';
|
||||||
@@ -242,8 +241,6 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
|
|||||||
LATENTS_TO_IMAGE
|
LATENTS_TO_IMAGE
|
||||||
);
|
);
|
||||||
|
|
||||||
addInitialImageToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
|
|
||||||
|
|
||||||
// Add Seamless To Graph
|
// Add Seamless To Graph
|
||||||
if (seamlessXAxis || seamlessYAxis) {
|
if (seamlessXAxis || seamlessYAxis) {
|
||||||
addSeamlessToLinearGraph(state, graph, modelLoaderNodeId);
|
addSeamlessToLinearGraph(state, graph, modelLoaderNodeId);
|
||||||
@@ -258,14 +255,14 @@ export const buildGenerationTabSDXLGraph = async (state: RootState): Promise<Non
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionally add custom VAE
|
|
||||||
await addVAEToGraph(state, graph, modelLoaderNodeId);
|
|
||||||
|
|
||||||
// add LoRA support
|
// add LoRA support
|
||||||
await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId);
|
await addSDXLLoRAsToGraph(state, graph, SDXL_DENOISE_LATENTS, modelLoaderNodeId);
|
||||||
|
|
||||||
await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS);
|
await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS);
|
||||||
|
|
||||||
|
// optionally add custom VAE
|
||||||
|
await addVAEToGraph(state, graph, modelLoaderNodeId);
|
||||||
|
|
||||||
// NSFW & watermark - must be last thing added to graph
|
// NSFW & watermark - must be last thing added to graph
|
||||||
if (state.system.shouldUseNSFWChecker) {
|
if (state.system.shouldUseNSFWChecker) {
|
||||||
// must add before watermarker!
|
// must add before watermarker!
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
||||||
|
import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
const ParamImageToImageStrength = () => {
|
||||||
|
const img2imgStrength = useAppSelector((s) => s.generation.img2imgStrength);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(v: number) => {
|
||||||
|
dispatch(setImg2imgStrength(v));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ImageToImageStrength value={img2imgStrength} onChange={onChange} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParamImageToImageStrength);
|
||||||
@@ -33,12 +33,13 @@ export const ParamNegativePrompt = memo(() => {
|
|||||||
name="negativePrompt"
|
name="negativePrompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.negativePromptPlaceholder')}
|
placeholder={t('parameters.globalNegativePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
|
|||||||
@@ -49,12 +49,13 @@ export const ParamPositivePrompt = memo(() => {
|
|||||||
name="prompt"
|
name="prompt"
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
placeholder={t('parameters.positivePromptPlaceholder')}
|
placeholder={t('parameters.globalPositivePromptPlaceholder')}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
minH={28}
|
minH={28}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
|
import { memo } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const marks = [0, 0.5, 1];
|
const marks = [0, 0.5, 1];
|
||||||
|
|
||||||
const ImageToImageStrength = () => {
|
type Props = {
|
||||||
const img2imgStrength = useAppSelector((s) => s.generation.img2imgStrength);
|
value: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageToImageStrength = ({ value, onChange }: Props) => {
|
||||||
const initial = useAppSelector((s) => s.config.sd.img2imgStrength.initial);
|
const initial = useAppSelector((s) => s.config.sd.img2imgStrength.initial);
|
||||||
const sliderMin = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMin);
|
const sliderMin = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMin);
|
||||||
const sliderMax = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMax);
|
const sliderMax = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMax);
|
||||||
@@ -16,11 +19,8 @@ const ImageToImageStrength = () => {
|
|||||||
const numberInputMax = useAppSelector((s) => s.config.sd.img2imgStrength.numberInputMax);
|
const numberInputMax = useAppSelector((s) => s.config.sd.img2imgStrength.numberInputMax);
|
||||||
const coarseStep = useAppSelector((s) => s.config.sd.img2imgStrength.coarseStep);
|
const coarseStep = useAppSelector((s) => s.config.sd.img2imgStrength.coarseStep);
|
||||||
const fineStep = useAppSelector((s) => s.config.sd.img2imgStrength.fineStep);
|
const fineStep = useAppSelector((s) => s.config.sd.img2imgStrength.fineStep);
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleChange = useCallback((v: number) => dispatch(setImg2imgStrength(v)), [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InformationalPopover feature="paramDenoisingStrength">
|
<InformationalPopover feature="paramDenoisingStrength">
|
||||||
@@ -31,8 +31,8 @@ const ImageToImageStrength = () => {
|
|||||||
fineStep={fineStep}
|
fineStep={fineStep}
|
||||||
min={sliderMin}
|
min={sliderMin}
|
||||||
max={sliderMax}
|
max={sliderMax}
|
||||||
onChange={handleChange}
|
onChange={onChange}
|
||||||
value={img2imgStrength}
|
value={value}
|
||||||
defaultValue={initial}
|
defaultValue={initial}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
/>
|
/>
|
||||||
@@ -41,8 +41,8 @@ const ImageToImageStrength = () => {
|
|||||||
fineStep={fineStep}
|
fineStep={fineStep}
|
||||||
min={numberInputMin}
|
min={numberInputMin}
|
||||||
max={numberInputMax}
|
max={numberInputMax}
|
||||||
onChange={handleChange}
|
onChange={onChange}
|
||||||
value={img2imgStrength}
|
value={value}
|
||||||
defaultValue={initial}
|
defaultValue={initial}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const usePreselectedImage = (selectedImage?: {
|
|||||||
const handleSendToImg2Img = useCallback(() => {
|
const handleSendToImg2Img = useCallback(() => {
|
||||||
if (selectedImageDto) {
|
if (selectedImageDto) {
|
||||||
dispatch(iiLayerAdded(selectedImageDto));
|
dispatch(iiLayerAdded(selectedImageDto));
|
||||||
|
dispatch(setActiveTab('generation'));
|
||||||
}
|
}
|
||||||
}, [dispatch, selectedImageDto]);
|
}, [dispatch, selectedImageDto]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import { z } from 'zod';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// #region Positive prompt
|
// #region Positive prompt
|
||||||
const zParameterPositivePrompt = z.string();
|
export const zParameterPositivePrompt = z.string();
|
||||||
export type ParameterPositivePrompt = z.infer<typeof zParameterPositivePrompt>;
|
export type ParameterPositivePrompt = z.infer<typeof zParameterPositivePrompt>;
|
||||||
export const isParameterPositivePrompt = (val: unknown): val is ParameterPositivePrompt =>
|
export const isParameterPositivePrompt = (val: unknown): val is ParameterPositivePrompt =>
|
||||||
zParameterPositivePrompt.safeParse(val).success;
|
zParameterPositivePrompt.safeParse(val).success;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Negative prompt
|
// #region Negative prompt
|
||||||
const zParameterNegativePrompt = z.string();
|
export const zParameterNegativePrompt = z.string();
|
||||||
export type ParameterNegativePrompt = z.infer<typeof zParameterNegativePrompt>;
|
export type ParameterNegativePrompt = z.infer<typeof zParameterNegativePrompt>;
|
||||||
export const isParameterNegativePrompt = (val: unknown): val is ParameterNegativePrompt =>
|
export const isParameterNegativePrompt = (val: unknown): val is ParameterNegativePrompt =>
|
||||||
zParameterNegativePrompt.safeParse(val).success;
|
zParameterNegativePrompt.safeParse(val).success;
|
||||||
@@ -127,7 +127,7 @@ export type ParameterT2IAdapterModel = z.infer<typeof zParameterT2IAdapterModel>
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Strength (l2l strength)
|
// #region Strength (l2l strength)
|
||||||
const zParameterStrength = z.number().min(0).max(1);
|
export const zParameterStrength = z.number().min(0).max(1);
|
||||||
export type ParameterStrength = z.infer<typeof zParameterStrength>;
|
export type ParameterStrength = z.infer<typeof zParameterStrength>;
|
||||||
export const isParameterStrength = (val: unknown): val is ParameterStrength =>
|
export const isParameterStrength = (val: unknown): val is ParameterStrength =>
|
||||||
zParameterStrength.safeParse(val).success;
|
zParameterStrength.safeParse(val).success;
|
||||||
@@ -198,6 +198,6 @@ export const isParameterLoRAWeight = (val: unknown): val is ParameterLoRAWeight
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Regional Prompts AutoNegative
|
// #region Regional Prompts AutoNegative
|
||||||
const zAutoNegative = z.enum(['off', 'invert']);
|
export const zAutoNegative = z.enum(['off', 'invert']);
|
||||||
export type ParameterAutoNegative = z.infer<typeof zAutoNegative>;
|
export type ParameterAutoNegative = z.infer<typeof zAutoNegative>;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const ParamSDXLNegativeStylePrompt = memo(() => {
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const ParamSDXLPositiveStylePrompt = memo(() => {
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
variant="darkFilled"
|
variant="darkFilled"
|
||||||
paddingRight={30}
|
paddingRight={30}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { selectHrfSlice } from 'features/hrf/store/hrfSlice';
|
|||||||
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
import ParamScaleBeforeProcessing from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaleBeforeProcessing';
|
||||||
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
import ParamScaledHeight from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledHeight';
|
||||||
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
import ParamScaledWidth from 'features/parameters/components/Canvas/InfillAndScaling/ParamScaledWidth';
|
||||||
import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength';
|
import ParamImageToImageStrength from 'features/parameters/components/Canvas/ParamImageToImageStrength';
|
||||||
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
import { ParamSeedNumberInput } from 'features/parameters/components/Seed/ParamSeedNumberInput';
|
||||||
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
import { ParamSeedRandomize } from 'features/parameters/components/Seed/ParamSeedRandomize';
|
||||||
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
import { ParamSeedShuffle } from 'features/parameters/components/Seed/ParamSeedShuffle';
|
||||||
@@ -85,7 +85,10 @@ export const ImageSettingsAccordion = memo(() => {
|
|||||||
onToggle={onToggleAccordion}
|
onToggle={onToggleAccordion}
|
||||||
>
|
>
|
||||||
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
|
<Flex px={4} pt={4} w="full" h="full" flexDir="column" data-testid="image-settings-accordion">
|
||||||
{activeTabName === 'canvas' ? <ImageSizeCanvas /> : <ImageSizeLinear />}
|
<Flex flexDir="column" gap={4}>
|
||||||
|
{activeTabName === 'canvas' ? <ImageSizeCanvas /> : <ImageSizeLinear />}
|
||||||
|
{activeTabName === 'canvas' && <ParamImageToImageStrength />}
|
||||||
|
</Flex>
|
||||||
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
|
<Expander label={t('accordions.advanced.options')} isOpen={isOpenExpander} onToggle={onToggleExpander}>
|
||||||
<Flex gap={4} pb={4} flexDir="column">
|
<Flex gap={4} pb={4} flexDir="column">
|
||||||
<Flex gap={4} alignItems="center">
|
<Flex gap={4} alignItems="center">
|
||||||
@@ -93,7 +96,6 @@ export const ImageSettingsAccordion = memo(() => {
|
|||||||
<ParamSeedShuffle />
|
<ParamSeedShuffle />
|
||||||
<ParamSeedRandomize />
|
<ParamSeedRandomize />
|
||||||
</Flex>
|
</Flex>
|
||||||
{activeTabName === 'canvas' && <ImageToImageStrength />}
|
|
||||||
{activeTabName === 'generation' && !isSDXL && <HrfSettings />}
|
{activeTabName === 'generation' && !isSDXL && <HrfSettings />}
|
||||||
{activeTabName === 'canvas' && (
|
{activeTabName === 'canvas' && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
||||||
@@ -255,9 +254,8 @@ const InvokeTabs = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Panel id="main-panel" order={1} minSize={20}>
|
<Panel id="main-panel" order={1} minSize={20}>
|
||||||
<TabPanels w="full" h="full" position="relative">
|
<TabPanels w="full" h="full">
|
||||||
{tabPanels}
|
{tabPanels}
|
||||||
<ImageViewer />
|
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Panel>
|
</Panel>
|
||||||
{shouldShowGalleryPanel && (
|
{shouldShowGalleryPanel && (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
|
||||||
|
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
import { Prompts } from 'features/parameters/components/Prompts/Prompts';
|
||||||
import QueueControls from 'features/queue/components/QueueControls';
|
import QueueControls from 'features/queue/components/QueueControls';
|
||||||
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
|
||||||
@@ -15,7 +16,7 @@ import { RefinerSettingsAccordion } from 'features/settingsAccordions/components
|
|||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const overlayScrollbarsStyles: CSSProperties = {
|
const overlayScrollbarsStyles: CSSProperties = {
|
||||||
@@ -37,6 +38,7 @@ const selectedStyles: ChakraProps['sx'] = {
|
|||||||
|
|
||||||
const ParametersPanelTextToImage = () => {
|
const ParametersPanelTextToImage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length);
|
const controlLayersCount = useAppSelector((s) => s.controlLayers.present.layers.length);
|
||||||
const controlLayersTitle = useMemo(() => {
|
const controlLayersTitle = useMemo(() => {
|
||||||
@@ -46,6 +48,14 @@ const ParametersPanelTextToImage = () => {
|
|||||||
return `${t('controlLayers.controlLayers')} (${controlLayersCount})`;
|
return `${t('controlLayers.controlLayers')} (${controlLayersCount})`;
|
||||||
}, [controlLayersCount, t]);
|
}, [controlLayersCount, t]);
|
||||||
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl');
|
||||||
|
const onChangeTabs = useCallback(
|
||||||
|
(i: number) => {
|
||||||
|
if (i === 1) {
|
||||||
|
dispatch(isImageViewerOpenChanged(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||||
@@ -55,12 +65,24 @@ const ParametersPanelTextToImage = () => {
|
|||||||
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
|
||||||
<Flex gap={2} flexDirection="column" h="full" w="full">
|
<Flex gap={2} flexDirection="column" h="full" w="full">
|
||||||
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
{isSDXL ? <SDXLPrompts /> : <Prompts />}
|
||||||
<Tabs variant="enclosed" display="flex" flexDir="column" w="full" h="full" gap={2}>
|
<Tabs
|
||||||
|
variant="enclosed"
|
||||||
|
display="flex"
|
||||||
|
flexDir="column"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
gap={2}
|
||||||
|
onChange={onChangeTabs}
|
||||||
|
>
|
||||||
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
<TabList gap={2} fontSize="sm" borderColor="base.800">
|
||||||
<Tab sx={baseStyles} _selected={selectedStyles}>
|
<Tab sx={baseStyles} _selected={selectedStyles} data-testid="generation-tab-settings-tab-button">
|
||||||
{t('common.settingsLabel')}
|
{t('common.settingsLabel')}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab sx={baseStyles} _selected={selectedStyles}>
|
<Tab
|
||||||
|
sx={baseStyles}
|
||||||
|
_selected={selectedStyles}
|
||||||
|
data-testid="generation-tab-control-layers-tab-button"
|
||||||
|
>
|
||||||
{controlLayersTitle}
|
{controlLayersTitle}
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { ImageViewerWorkflows } from 'features/gallery/components/ImageViewer/ImageViewerWorkflows';
|
||||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
const NodesTab = () => {
|
const NodesTab = () => {
|
||||||
|
const mode = useAppSelector((s) => s.workflow.mode);
|
||||||
|
if (mode === 'view') {
|
||||||
|
return (
|
||||||
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
|
<ImageViewerWorkflows />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ControlLayersEditor />
|
<ControlLayersEditor />
|
||||||
|
<ImageViewer />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.2.0b2"
|
__version__ = "4.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user