mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'main' into lstein/feat/simple-mm2-api
This commit is contained in:
commit
49c84cd423
@ -36,7 +36,7 @@ from invokeai.app.invocations.model import ModelIdentifierField
|
|||||||
from invokeai.app.invocations.primitives import ImageOutput
|
from invokeai.app.invocations.primitives import ImageOutput
|
||||||
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
|
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
|
||||||
from invokeai.app.services.shared.invocation_context import InvocationContext
|
from invokeai.app.services.shared.invocation_context import InvocationContext
|
||||||
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
|
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES, heuristic_resize
|
||||||
from invokeai.backend.image_util.canny import get_canny_edges
|
from invokeai.backend.image_util.canny import get_canny_edges
|
||||||
from invokeai.backend.image_util.depth_anything import DEPTH_ANYTHING_MODELS, DepthAnythingDetector
|
from invokeai.backend.image_util.depth_anything import DEPTH_ANYTHING_MODELS, DepthAnythingDetector
|
||||||
from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector
|
from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector
|
||||||
@ -44,8 +44,9 @@ from invokeai.backend.image_util.hed import HEDProcessor
|
|||||||
from invokeai.backend.image_util.lineart import LineartProcessor
|
from invokeai.backend.image_util.lineart import LineartProcessor
|
||||||
from invokeai.backend.image_util.lineart_anime import LineartAnimeProcessor
|
from invokeai.backend.image_util.lineart_anime import LineartAnimeProcessor
|
||||||
from invokeai.backend.util.devices import TorchDevice
|
from invokeai.backend.util.devices import TorchDevice
|
||||||
|
from invokeai.backend.image_util.util import np_to_pil, pil_to_np
|
||||||
|
|
||||||
from .baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
|
from .baseinvocation import BaseInvocation, BaseInvocationOutput, Classification, invocation, invocation_output
|
||||||
|
|
||||||
|
|
||||||
class ControlField(BaseModel):
|
class ControlField(BaseModel):
|
||||||
@ -641,3 +642,27 @@ class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation):
|
|||||||
resolution=self.image_resolution,
|
resolution=self.image_resolution,
|
||||||
)
|
)
|
||||||
return processed_image
|
return processed_image
|
||||||
|
|
||||||
|
|
||||||
|
@invocation(
|
||||||
|
"heuristic_resize",
|
||||||
|
title="Heuristic Resize",
|
||||||
|
tags=["image, controlnet"],
|
||||||
|
category="image",
|
||||||
|
version="1.0.0",
|
||||||
|
classification=Classification.Prototype,
|
||||||
|
)
|
||||||
|
class HeuristicResizeInvocation(BaseInvocation):
|
||||||
|
"""Resize an image using a heuristic method. Preserves edge maps."""
|
||||||
|
|
||||||
|
image: ImageField = InputField(description="The image to resize")
|
||||||
|
width: int = InputField(default=512, gt=0, description="The width to resize to (px)")
|
||||||
|
height: int = InputField(default=512, gt=0, description="The height to resize to (px)")
|
||||||
|
|
||||||
|
def invoke(self, context: InvocationContext) -> ImageOutput:
|
||||||
|
image = context.images.get_pil(self.image.image_name, "RGB")
|
||||||
|
np_img = pil_to_np(image)
|
||||||
|
np_resized = heuristic_resize(np_img, (self.width, self.height))
|
||||||
|
resized = np_to_pil(np_resized)
|
||||||
|
image_dto = context.images.save(image=resized)
|
||||||
|
return ImageOutput.build(image_dto)
|
||||||
|
@ -318,10 +318,8 @@ class DownloadQueueService(DownloadQueueServiceBase):
|
|||||||
in_progress_path.rename(job.download_path)
|
in_progress_path.rename(job.download_path)
|
||||||
|
|
||||||
def _validate_filename(self, directory: str, filename: str) -> bool:
|
def _validate_filename(self, directory: str, filename: str) -> bool:
|
||||||
pc_name_max = os.pathconf(directory, "PC_NAME_MAX") if hasattr(os, "pathconf") else 260 # hardcoded for windows
|
pc_name_max = get_pc_name_max(directory)
|
||||||
pc_path_max = (
|
pc_path_max = get_pc_path_max(directory)
|
||||||
os.pathconf(directory, "PC_PATH_MAX") if hasattr(os, "pathconf") else 32767
|
|
||||||
) # hardcoded for windows with long names enabled
|
|
||||||
if "/" in filename:
|
if "/" in filename:
|
||||||
return False
|
return False
|
||||||
if filename.startswith(".."):
|
if filename.startswith(".."):
|
||||||
@ -419,6 +417,26 @@ class DownloadQueueService(DownloadQueueServiceBase):
|
|||||||
self._logger.warning(excp)
|
self._logger.warning(excp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pc_name_max(directory: str) -> int:
|
||||||
|
if hasattr(os, "pathconf"):
|
||||||
|
try:
|
||||||
|
return os.pathconf(directory, "PC_NAME_MAX")
|
||||||
|
except OSError:
|
||||||
|
# macOS w/ external drives raise OSError
|
||||||
|
pass
|
||||||
|
return 260 # hardcoded for windows
|
||||||
|
|
||||||
|
|
||||||
|
def get_pc_path_max(directory: str) -> int:
|
||||||
|
if hasattr(os, "pathconf"):
|
||||||
|
try:
|
||||||
|
return os.pathconf(directory, "PC_PATH_MAX")
|
||||||
|
except OSError:
|
||||||
|
# some platforms may not have this value
|
||||||
|
pass
|
||||||
|
return 32767 # hardcoded for windows with long names enabled
|
||||||
|
|
||||||
|
|
||||||
# Example on_progress event handler to display a TQDM status bar
|
# Example on_progress event handler to display a TQDM status bar
|
||||||
# Activate with:
|
# Activate with:
|
||||||
# download_service.download(DownloadJob('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().update))
|
# download_service.download(DownloadJob('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().update))
|
||||||
|
@ -144,10 +144,8 @@ def resize_image_to_resolution(input_image: np.ndarray, resolution: int) -> np.n
|
|||||||
h = float(input_image.shape[0])
|
h = float(input_image.shape[0])
|
||||||
w = float(input_image.shape[1])
|
w = float(input_image.shape[1])
|
||||||
scaling_factor = float(resolution) / min(h, w)
|
scaling_factor = float(resolution) / min(h, w)
|
||||||
h *= scaling_factor
|
h = int(h * scaling_factor)
|
||||||
w *= scaling_factor
|
w = int(w * scaling_factor)
|
||||||
h = int(np.round(h / 64.0)) * 64
|
|
||||||
w = int(np.round(w / 64.0)) * 64
|
|
||||||
if scaling_factor > 1:
|
if scaling_factor > 1:
|
||||||
return cv2.resize(input_image, (w, h), interpolation=cv2.INTER_LANCZOS4)
|
return cv2.resize(input_image, (w, h), interpolation=cv2.INTER_LANCZOS4)
|
||||||
else:
|
else:
|
||||||
|
@ -101,6 +101,7 @@
|
|||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
|
"use-device-pixel-ratio": "^1.1.2",
|
||||||
"use-image": "^1.1.1",
|
"use-image": "^1.1.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
|
@ -158,6 +158,9 @@ dependencies:
|
|||||||
use-debounce:
|
use-debounce:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0(react@18.2.0)
|
version: 10.0.0(react@18.2.0)
|
||||||
|
use-device-pixel-ratio:
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2(react@18.2.0)
|
||||||
use-image:
|
use-image:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -13324,6 +13327,14 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/use-device-pixel-ratio@1.1.2(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-nFxV0HwLdRUt20kvIgqHYZe6PK/v4mU1X8/eLsT1ti5ck0l2ob0HDRziaJPx+YWzBo6dMm4cTac3mcyk68Gh+A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/use-image@1.1.1(react-dom@18.2.0)(react@18.2.0):
|
/use-image@1.1.1(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==}
|
resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -156,6 +156,7 @@
|
|||||||
"balanced": "Balanced",
|
"balanced": "Balanced",
|
||||||
"base": "Base",
|
"base": "Base",
|
||||||
"beginEndStepPercent": "Begin / End Step Percentage",
|
"beginEndStepPercent": "Begin / End Step Percentage",
|
||||||
|
"beginEndStepPercentShort": "Begin/End %",
|
||||||
"bgth": "bg_th",
|
"bgth": "bg_th",
|
||||||
"canny": "Canny",
|
"canny": "Canny",
|
||||||
"cannyDescription": "Canny edge detection",
|
"cannyDescription": "Canny edge detection",
|
||||||
@ -227,7 +228,8 @@
|
|||||||
"scribble": "scribble",
|
"scribble": "scribble",
|
||||||
"selectModel": "Select a model",
|
"selectModel": "Select a model",
|
||||||
"selectCLIPVisionModel": "Select a CLIP Vision model",
|
"selectCLIPVisionModel": "Select a CLIP Vision model",
|
||||||
"setControlImageDimensions": "Set Control Image Dimensions To W/H",
|
"setControlImageDimensions": "Copy size to W/H (optimize for model)",
|
||||||
|
"setControlImageDimensionsForce": "Copy size to W/H (ignore model)",
|
||||||
"showAdvanced": "Show Advanced",
|
"showAdvanced": "Show Advanced",
|
||||||
"small": "Small",
|
"small": "Small",
|
||||||
"toggleControlNet": "Toggle this ControlNet",
|
"toggleControlNet": "Toggle this ControlNet",
|
||||||
@ -1511,7 +1513,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"storeNotInitialized": "Store is not initialized"
|
"storeNotInitialized": "Store is not initialized"
|
||||||
},
|
},
|
||||||
"regionalPrompts": {
|
"controlLayers": {
|
||||||
"deleteAll": "Delete All",
|
"deleteAll": "Delete All",
|
||||||
"addLayer": "Add Layer",
|
"addLayer": "Add Layer",
|
||||||
"moveToFront": "Move to Front",
|
"moveToFront": "Move to Front",
|
||||||
@ -1519,8 +1521,7 @@
|
|||||||
"moveForward": "Move Forward",
|
"moveForward": "Move Forward",
|
||||||
"moveBackward": "Move Backward",
|
"moveBackward": "Move Backward",
|
||||||
"brushSize": "Brush Size",
|
"brushSize": "Brush Size",
|
||||||
"regionalControl": "Regional Control (ALPHA)",
|
"controlLayers": "Control Layers (BETA)",
|
||||||
"enableRegionalPrompts": "Enable $t(regionalPrompts.regionalPrompts)",
|
|
||||||
"globalMaskOpacity": "Global Mask Opacity",
|
"globalMaskOpacity": "Global Mask Opacity",
|
||||||
"autoNegative": "Auto Negative",
|
"autoNegative": "Auto Negative",
|
||||||
"toggleVisibility": "Toggle Layer Visibility",
|
"toggleVisibility": "Toggle Layer Visibility",
|
||||||
@ -1531,6 +1532,16 @@
|
|||||||
"maskPreviewColor": "Mask Preview Color",
|
"maskPreviewColor": "Mask Preview Color",
|
||||||
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||||
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||||
"addIPAdapter": "Add $t(common.ipAdapter)"
|
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||||
|
"regionalGuidance": "Regional Guidance",
|
||||||
|
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||||
|
"controlNetLayer": "$t(common.controlNet) $t(unifiedCanvas.layer)",
|
||||||
|
"ipAdapterLayer": "$t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
||||||
|
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||||
|
"globalIPAdapter": "Global $t(common.ipAdapter)",
|
||||||
|
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
||||||
|
"opacityFilter": "Opacity Filter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export type LoggerNamespace =
|
|||||||
| 'session'
|
| 'session'
|
||||||
| 'queue'
|
| 'queue'
|
||||||
| 'dnd'
|
| 'dnd'
|
||||||
| 'regionalPrompts';
|
| 'controlLayers';
|
||||||
|
|
||||||
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
|
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listen
|
|||||||
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
|
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
|
||||||
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
|
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
|
||||||
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
|
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
|
||||||
|
import { addControlLayersToControlAdapterBridge } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
|
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
|
||||||
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
|
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
|
||||||
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
|
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
|
||||||
@ -157,3 +158,5 @@ addUpscaleRequestedListener(startAppListening);
|
|||||||
addDynamicPromptsListener(startAppListening);
|
addDynamicPromptsListener(startAppListening);
|
||||||
|
|
||||||
addSetDefaultSettingsListener(startAppListening);
|
addSetDefaultSettingsListener(startAppListening);
|
||||||
|
|
||||||
|
addControlLayersToControlAdapterBridge(startAppListening);
|
||||||
|
@ -48,12 +48,10 @@ export const addCanvasImageToControlNetListener = (startAppListening: AppStartLi
|
|||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
const { image_name } = imageDTO;
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
controlAdapterImageChanged({
|
controlAdapterImageChanged({
|
||||||
id,
|
id,
|
||||||
controlImage: image_name,
|
controlImage: imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -58,12 +58,10 @@ export const addCanvasMaskToControlNetListener = (startAppListening: AppStartLis
|
|||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
const { image_name } = imageDTO;
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
controlAdapterImageChanged({
|
controlAdapterImageChanged({
|
||||||
id,
|
id,
|
||||||
controlImage: image_name,
|
controlImage: imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
|
||||||
|
import { controlAdapterAdded, controlAdapterRemoved } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import type { ControlNetConfig, IPAdapterConfig } from 'features/controlAdapters/store/types';
|
||||||
|
import { isControlAdapterProcessorType } from 'features/controlAdapters/store/types';
|
||||||
|
import {
|
||||||
|
controlAdapterLayerAdded,
|
||||||
|
ipAdapterLayerAdded,
|
||||||
|
layerDeleted,
|
||||||
|
maskLayerIPAdapterAdded,
|
||||||
|
maskLayerIPAdapterDeleted,
|
||||||
|
regionalGuidanceLayerAdded,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
|
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
|
||||||
|
import { isControlNetModelConfig, isIPAdapterModelConfig } from 'services/api/types';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
export const guidanceLayerAdded = createAction<Layer['type']>('controlLayers/guidanceLayerAdded');
|
||||||
|
export const guidanceLayerDeleted = createAction<string>('controlLayers/guidanceLayerDeleted');
|
||||||
|
export const allLayersDeleted = createAction('controlLayers/allLayersDeleted');
|
||||||
|
export const guidanceLayerIPAdapterAdded = createAction<string>('controlLayers/guidanceLayerIPAdapterAdded');
|
||||||
|
export const guidanceLayerIPAdapterDeleted = createAction<{ layerId: string; ipAdapterId: string }>(
|
||||||
|
'controlLayers/guidanceLayerIPAdapterDeleted'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const addControlLayersToControlAdapterBridge = (startAppListening: AppStartListening) => {
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: guidanceLayerAdded,
|
||||||
|
effect: (action, { dispatch, getState }) => {
|
||||||
|
const type = action.payload;
|
||||||
|
const layerId = uuidv4();
|
||||||
|
if (type === 'regional_guidance_layer') {
|
||||||
|
dispatch(regionalGuidanceLayerAdded({ layerId }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const baseModel = state.generation.model?.base;
|
||||||
|
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;
|
||||||
|
|
||||||
|
if (type === 'ip_adapter_layer') {
|
||||||
|
const ipAdapterId = uuidv4();
|
||||||
|
const overrides: Partial<IPAdapterConfig> = {
|
||||||
|
id: ipAdapterId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find and select the first matching model
|
||||||
|
if (modelConfigs) {
|
||||||
|
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
|
||||||
|
overrides.model = models.find((m) => m.base === baseModel) ?? null;
|
||||||
|
}
|
||||||
|
dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
|
||||||
|
dispatch(ipAdapterLayerAdded({ layerId, ipAdapterId }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'control_adapter_layer') {
|
||||||
|
const controlNetId = uuidv4();
|
||||||
|
const overrides: Partial<ControlNetConfig> = {
|
||||||
|
id: controlNetId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find and select the first matching model
|
||||||
|
if (modelConfigs) {
|
||||||
|
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isControlNetModelConfig);
|
||||||
|
const model = models.find((m) => m.base === baseModel) ?? null;
|
||||||
|
overrides.model = model;
|
||||||
|
const defaultPreprocessor = model?.default_settings?.preprocessor;
|
||||||
|
overrides.processorType = isControlAdapterProcessorType(defaultPreprocessor) ? defaultPreprocessor : 'none';
|
||||||
|
overrides.processorNode = CONTROLNET_PROCESSORS[overrides.processorType].buildDefaults(baseModel);
|
||||||
|
}
|
||||||
|
dispatch(controlAdapterAdded({ type: 'controlnet', overrides }));
|
||||||
|
dispatch(controlAdapterLayerAdded({ layerId, controlNetId }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: guidanceLayerDeleted,
|
||||||
|
effect: (action, { getState, dispatch }) => {
|
||||||
|
const layerId = action.payload;
|
||||||
|
const state = getState();
|
||||||
|
const layer = state.controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
|
||||||
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
|
dispatch(controlAdapterRemoved({ id: layer.ipAdapterId }));
|
||||||
|
} else if (layer.type === 'control_adapter_layer') {
|
||||||
|
dispatch(controlAdapterRemoved({ id: layer.controlNetId }));
|
||||||
|
} else if (layer.type === 'regional_guidance_layer') {
|
||||||
|
for (const ipAdapterId of layer.ipAdapterIds) {
|
||||||
|
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch(layerDeleted(layerId));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: allLayersDeleted,
|
||||||
|
effect: (action, { dispatch, getOriginalState }) => {
|
||||||
|
const state = getOriginalState();
|
||||||
|
for (const layer of state.controlLayers.present.layers) {
|
||||||
|
dispatch(guidanceLayerDeleted(layer.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: guidanceLayerIPAdapterAdded,
|
||||||
|
effect: (action, { dispatch, getState }) => {
|
||||||
|
const layerId = action.payload;
|
||||||
|
const ipAdapterId = uuidv4();
|
||||||
|
const overrides: Partial<IPAdapterConfig> = {
|
||||||
|
id: ipAdapterId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find and select the first matching model
|
||||||
|
const state = getState();
|
||||||
|
const baseModel = state.generation.model?.base;
|
||||||
|
const modelConfigs = modelsApi.endpoints.getModelConfigs.select(undefined)(state).data;
|
||||||
|
if (modelConfigs) {
|
||||||
|
const models = modelConfigsAdapterSelectors.selectAll(modelConfigs).filter(isIPAdapterModelConfig);
|
||||||
|
overrides.model = models.find((m) => m.base === baseModel) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(controlAdapterAdded({ type: 'ip_adapter', overrides }));
|
||||||
|
dispatch(maskLayerIPAdapterAdded({ layerId, ipAdapterId }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startAppListening({
|
||||||
|
actionCreator: guidanceLayerIPAdapterDeleted,
|
||||||
|
effect: (action, { dispatch }) => {
|
||||||
|
const { layerId, ipAdapterId } = action.payload;
|
||||||
|
dispatch(controlAdapterRemoved({ id: ipAdapterId }));
|
||||||
|
dispatch(maskLayerIPAdapterDeleted({ layerId, ipAdapterId }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -12,6 +12,7 @@ import {
|
|||||||
selectControlAdapterById,
|
selectControlAdapterById,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
type AnyControlAdapterParamChangeAction =
|
type AnyControlAdapterParamChangeAction =
|
||||||
| ReturnType<typeof controlAdapterProcessorParamsChanged>
|
| ReturnType<typeof controlAdapterProcessorParamsChanged>
|
||||||
@ -52,6 +53,11 @@ const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) =>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevCA.controlImage === ca.controlImage && isEqual(prevCA.processorNode, ca.processorNode)) {
|
||||||
|
// Don't re-process if the processor hasn't changed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const isProcessorSelected = processorType !== 'none';
|
const isProcessorSelected = processorType !== 'none';
|
||||||
|
|
||||||
const hasControlImage = Boolean(controlImage);
|
const hasControlImage = Boolean(controlImage);
|
||||||
|
@ -91,7 +91,7 @@ export const addControlNetImageProcessedListener = (startAppListening: AppStartL
|
|||||||
dispatch(
|
dispatch(
|
||||||
controlAdapterProcessedImageChanged({
|
controlAdapterProcessedImageChanged({
|
||||||
id,
|
id,
|
||||||
processedControlImage: processedControlImage.image_name,
|
processedControlImage,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
|||||||
dispatch(
|
dispatch(
|
||||||
controlAdapterImageChanged({
|
controlAdapterImageChanged({
|
||||||
id,
|
id,
|
||||||
controlImage: activeData.payload.imageDTO.image_name,
|
controlImage: activeData.payload.imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -96,7 +96,7 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
|
|||||||
dispatch(
|
dispatch(
|
||||||
controlAdapterImageChanged({
|
controlAdapterImageChanged({
|
||||||
id,
|
id,
|
||||||
controlImage: imageDTO.image_name,
|
controlImage: imageDTO,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
import {
|
import {
|
||||||
controlAdapterIsEnabledChanged,
|
controlAdapterModelChanged,
|
||||||
selectControlAdapterAll,
|
selectControlAdapterAll,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { loraRemoved } from 'features/lora/store/loraSlice';
|
import { loraRemoved } from 'features/lora/store/loraSlice';
|
||||||
@ -54,7 +54,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
|||||||
// handle incompatible controlnets
|
// handle incompatible controlnets
|
||||||
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
|
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
|
||||||
if (ca.model?.base !== newBaseModel) {
|
if (ca.model?.base !== newBaseModel) {
|
||||||
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
|
dispatch(controlAdapterModelChanged({ id: ca.id, modelConfig: null }));
|
||||||
modelsCleared += 1;
|
modelsCleared += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,10 @@ import {
|
|||||||
controlAdapterModelCleared,
|
controlAdapterModelCleared,
|
||||||
selectControlAdapterAll,
|
selectControlAdapterAll,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { loraRemoved } from 'features/lora/store/loraSlice';
|
import { loraRemoved } from 'features/lora/store/loraSlice';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
import { heightChanged, modelChanged, vaeSelected, widthChanged } from 'features/parameters/store/generationSlice';
|
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
|
||||||
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
|
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
|
||||||
@ -69,16 +70,22 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
|
|||||||
dispatch(modelChanged(defaultModelInList, currentModel));
|
dispatch(modelChanged(defaultModelInList, currentModel));
|
||||||
|
|
||||||
const optimalDimension = getOptimalDimension(defaultModelInList);
|
const optimalDimension = getOptimalDimension(defaultModelInList);
|
||||||
if (getIsSizeOptimal(state.generation.width, state.generation.height, optimalDimension)) {
|
if (
|
||||||
|
getIsSizeOptimal(
|
||||||
|
state.controlLayers.present.size.width,
|
||||||
|
state.controlLayers.present.size.height,
|
||||||
|
optimalDimension
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { width, height } = calculateNewSize(
|
const { width, height } = calculateNewSize(
|
||||||
state.generation.aspectRatio.value,
|
state.controlLayers.present.size.aspectRatio.value,
|
||||||
optimalDimension * optimalDimension
|
optimalDimension * optimalDimension
|
||||||
);
|
);
|
||||||
|
|
||||||
dispatch(widthChanged(width));
|
dispatch(widthChanged({ width }));
|
||||||
dispatch(heightChanged(height));
|
dispatch(heightChanged({ height }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { isAnyOf } from '@reduxjs/toolkit';
|
import { isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import {
|
import {
|
||||||
combinatorialToggled,
|
combinatorialToggled,
|
||||||
isErrorChanged,
|
isErrorChanged,
|
||||||
@ -10,11 +11,16 @@ import {
|
|||||||
promptsChanged,
|
promptsChanged,
|
||||||
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
import { setPositivePrompt } from 'features/parameters/store/generationSlice';
|
|
||||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||||
import { socketConnected } from 'services/events/actions';
|
import { socketConnected } from 'services/events/actions';
|
||||||
|
|
||||||
const matcher = isAnyOf(setPositivePrompt, combinatorialToggled, maxPromptsChanged, maxPromptsReset, socketConnected);
|
const matcher = isAnyOf(
|
||||||
|
positivePromptChanged,
|
||||||
|
combinatorialToggled,
|
||||||
|
maxPromptsChanged,
|
||||||
|
maxPromptsReset,
|
||||||
|
socketConnected
|
||||||
|
);
|
||||||
|
|
||||||
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -22,7 +28,7 @@ export const addDynamicPromptsListener = (startAppListening: AppStartListening)
|
|||||||
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
|
effect: async (action, { dispatch, getState, cancelActiveListeners, delay }) => {
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { positivePrompt } = state.generation;
|
const { positivePrompt } = state.controlLayers.present;
|
||||||
const { maxPrompts } = state.dynamicPrompts;
|
const { maxPrompts } = state.dynamicPrompts;
|
||||||
|
|
||||||
if (state.config.disabledFeatures.includes('dynamicPrompting')) {
|
if (state.config.disabledFeatures.includes('dynamicPrompting')) {
|
||||||
@ -32,7 +38,7 @@ export const addDynamicPromptsListener = (startAppListening: AppStartListening)
|
|||||||
const cachedPrompts = utilitiesApi.endpoints.dynamicPrompts.select({
|
const cachedPrompts = utilitiesApi.endpoints.dynamicPrompts.select({
|
||||||
prompt: positivePrompt,
|
prompt: positivePrompt,
|
||||||
max_prompts: maxPrompts,
|
max_prompts: maxPrompts,
|
||||||
})(getState()).data;
|
})(state).data;
|
||||||
|
|
||||||
if (cachedPrompts) {
|
if (cachedPrompts) {
|
||||||
dispatch(promptsChanged(cachedPrompts.prompts));
|
dispatch(promptsChanged(cachedPrompts.prompts));
|
||||||
@ -40,8 +46,8 @@ export const addDynamicPromptsListener = (startAppListening: AppStartListening)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getShouldProcessPrompt(state.generation.positivePrompt)) {
|
if (!getShouldProcessPrompt(positivePrompt)) {
|
||||||
dispatch(promptsChanged([state.generation.positivePrompt]));
|
dispatch(promptsChanged([positivePrompt]));
|
||||||
dispatch(parsingErrorChanged(undefined));
|
dispatch(parsingErrorChanged(undefined));
|
||||||
dispatch(isErrorChanged(false));
|
dispatch(isErrorChanged(false));
|
||||||
return;
|
return;
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||||
|
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { setDefaultSettings } from 'features/parameters/store/actions';
|
import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||||
import {
|
import {
|
||||||
heightRecalled,
|
|
||||||
setCfgRescaleMultiplier,
|
setCfgRescaleMultiplier,
|
||||||
setCfgScale,
|
setCfgScale,
|
||||||
setScheduler,
|
setScheduler,
|
||||||
setSteps,
|
setSteps,
|
||||||
vaePrecisionChanged,
|
vaePrecisionChanged,
|
||||||
vaeSelected,
|
vaeSelected,
|
||||||
widthRecalled,
|
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import {
|
import {
|
||||||
isParameterCFGRescaleMultiplier,
|
isParameterCFGRescaleMultiplier,
|
||||||
@ -100,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
|||||||
|
|
||||||
if (width) {
|
if (width) {
|
||||||
if (isParameterWidth(width)) {
|
if (isParameterWidth(width)) {
|
||||||
dispatch(widthRecalled(width));
|
dispatch(widthChanged({ width, updateAspectRatio: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height) {
|
if (height) {
|
||||||
if (isParameterHeight(height)) {
|
if (isParameterHeight(height)) {
|
||||||
dispatch(heightRecalled(height));
|
dispatch(heightChanged({ height, updateAspectRatio: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,11 @@ import {
|
|||||||
controlAdaptersPersistConfig,
|
controlAdaptersPersistConfig,
|
||||||
controlAdaptersSlice,
|
controlAdaptersSlice,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import {
|
||||||
|
controlLayersPersistConfig,
|
||||||
|
controlLayersSlice,
|
||||||
|
controlLayersUndoableConfig,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||||
@ -21,11 +26,6 @@ import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workf
|
|||||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||||
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
|
||||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||||
import {
|
|
||||||
regionalPromptsPersistConfig,
|
|
||||||
regionalPromptsSlice,
|
|
||||||
regionalPromptsUndoableConfig,
|
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||||
import { configSlice } from 'features/system/store/configSlice';
|
import { configSlice } from 'features/system/store/configSlice';
|
||||||
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
||||||
@ -65,7 +65,7 @@ const allReducers = {
|
|||||||
[queueSlice.name]: queueSlice.reducer,
|
[queueSlice.name]: queueSlice.reducer,
|
||||||
[workflowSlice.name]: workflowSlice.reducer,
|
[workflowSlice.name]: workflowSlice.reducer,
|
||||||
[hrfSlice.name]: hrfSlice.reducer,
|
[hrfSlice.name]: hrfSlice.reducer,
|
||||||
[regionalPromptsSlice.name]: undoable(regionalPromptsSlice.reducer, regionalPromptsUndoableConfig),
|
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
|
||||||
[api.reducerPath]: api.reducer,
|
[api.reducerPath]: api.reducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
|||||||
[loraPersistConfig.name]: loraPersistConfig,
|
[loraPersistConfig.name]: loraPersistConfig,
|
||||||
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
||||||
[hrfPersistConfig.name]: hrfPersistConfig,
|
[hrfPersistConfig.name]: hrfPersistConfig,
|
||||||
[regionalPromptsPersistConfig.name]: regionalPromptsPersistConfig,
|
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const unserialize: UnserializeFunction = (data, key) => {
|
const unserialize: UnserializeFunction = (data, key) => {
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
selectControlAdaptersSlice,
|
selectControlAdaptersSlice,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||||
|
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||||
@ -23,10 +24,12 @@ const selector = createMemoizedSelector(
|
|||||||
selectSystemSlice,
|
selectSystemSlice,
|
||||||
selectNodesSlice,
|
selectNodesSlice,
|
||||||
selectDynamicPromptsSlice,
|
selectDynamicPromptsSlice,
|
||||||
|
selectControlLayersSlice,
|
||||||
activeTabNameSelector,
|
activeTabNameSelector,
|
||||||
],
|
],
|
||||||
(controlAdapters, generation, system, nodes, dynamicPrompts, activeTabName) => {
|
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
|
||||||
const { initialImage, model, positivePrompt } = generation;
|
const { initialImage, model } = generation;
|
||||||
|
const { positivePrompt } = controlLayers.present;
|
||||||
|
|
||||||
const { isConnected } = system;
|
const { isConnected } = system;
|
||||||
|
|
||||||
@ -94,7 +97,41 @@ const selector = createMemoizedSelector(
|
|||||||
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
|
reasons.push(i18n.t('parameters.invoke.noModelSelected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
selectControlAdapterAll(controlAdapters).forEach((ca, i) => {
|
let enabledControlAdapters = selectControlAdapterAll(controlAdapters).filter((ca) => ca.isEnabled);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
// Special handling for control layers on txt2img
|
||||||
|
const enabledControlLayersAdapterIds = controlLayers.present.layers
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
|
.flatMap((layer) => {
|
||||||
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
|
return layer.ipAdapterIds;
|
||||||
|
}
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
return [layer.controlNetId];
|
||||||
|
}
|
||||||
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
|
return [layer.ipAdapterId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
enabledControlAdapters = enabledControlAdapters.filter((ca) => enabledControlLayersAdapterIds.includes(ca.id));
|
||||||
|
} else {
|
||||||
|
const allControlLayerAdapterIds = controlLayers.present.layers.flatMap((layer) => {
|
||||||
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
|
return layer.ipAdapterIds;
|
||||||
|
}
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
return [layer.controlNetId];
|
||||||
|
}
|
||||||
|
if (layer.type === 'ip_adapter_layer') {
|
||||||
|
return [layer.ipAdapterId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
enabledControlAdapters = enabledControlAdapters.filter((ca) => !allControlLayerAdapterIds.includes(ca.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledControlAdapters.forEach((ca, i) => {
|
||||||
if (!ca.isEnabled) {
|
if (!ca.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
3
invokeai/frontend/web/src/common/util/stopPropagation.ts
Normal file
3
invokeai/frontend/web/src/common/util/stopPropagation.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const stopPropagation = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
@ -113,7 +113,7 @@ const ControlAdapterConfig = (props: { id: string; number: number }) => {
|
|||||||
<Flex w="full" flexDir="column" gap={4}>
|
<Flex w="full" flexDir="column" gap={4}>
|
||||||
<Flex gap={8} w="full" alignItems="center">
|
<Flex gap={8} w="full" alignItems="center">
|
||||||
<Flex flexDir="column" gap={4} h={controlAdapterType === 'ip_adapter' ? 40 : 32} w="full">
|
<Flex flexDir="column" gap={4} h={controlAdapterType === 'ip_adapter' ? 40 : 32} w="full">
|
||||||
<ParamControlAdapterIPMethod id={id} />
|
{controlAdapterType === 'ip_adapter' && <ParamControlAdapterIPMethod id={id} />}
|
||||||
<ParamControlAdapterWeight id={id} />
|
<ParamControlAdapterWeight id={id} />
|
||||||
<ParamControlAdapterBeginEnd id={id} />
|
<ParamControlAdapterBeginEnd id={id} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -13,9 +13,10 @@ import {
|
|||||||
controlAdapterImageChanged,
|
controlAdapterImageChanged,
|
||||||
selectControlAdaptersSlice,
|
selectControlAdaptersSlice,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
import { heightChanged, selectOptimalDimension, widthChanged } from 'features/parameters/store/generationSlice';
|
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -99,8 +100,8 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
|
|||||||
controlImage.width / controlImage.height,
|
controlImage.width / controlImage.height,
|
||||||
optimalDimension * optimalDimension
|
optimalDimension * optimalDimension
|
||||||
);
|
);
|
||||||
dispatch(widthChanged(width));
|
dispatch(widthChanged({ width, updateAspectRatio: true }));
|
||||||
dispatch(heightChanged(height));
|
dispatch(heightChanged({ height, updateAspectRatio: true }));
|
||||||
}
|
}
|
||||||
}, [controlImage, activeTabName, dispatch, optimalDimension]);
|
}, [controlImage, activeTabName, dispatch, optimalDimension]);
|
||||||
|
|
||||||
|
@ -46,10 +46,6 @@ const ParamControlAdapterIPMethod = ({ id }: Props) => {
|
|||||||
|
|
||||||
const value = useMemo(() => options.find((o) => o.value === method), [options, method]);
|
const value = useMemo(() => options.find((o) => o.value === method), [options, method]);
|
||||||
|
|
||||||
if (!method) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InformationalPopover feature="ipAdapterMethod">
|
<InformationalPopover feature="ipAdapterMethod">
|
||||||
|
@ -102,13 +102,9 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ gap: 2 }}>
|
<Flex gap={4}>
|
||||||
<Tooltip label={selectedModel?.description}>
|
<Tooltip label={selectedModel?.description}>
|
||||||
<FormControl
|
<FormControl isDisabled={!isEnabled} isInvalid={!value || mainModel?.base !== modelConfig?.base} w="full">
|
||||||
isDisabled={!isEnabled}
|
|
||||||
isInvalid={!value || mainModel?.base !== modelConfig?.base}
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Combobox
|
<Combobox
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={t('controlnet.selectModel')}
|
placeholder={t('controlnet.selectModel')}
|
||||||
@ -122,7 +118,8 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
|
|||||||
<FormControl
|
<FormControl
|
||||||
isDisabled={!isEnabled}
|
isDisabled={!isEnabled}
|
||||||
isInvalid={!value || mainModel?.base !== modelConfig?.base}
|
isInvalid={!value || mainModel?.base !== modelConfig?.base}
|
||||||
sx={{ width: 'max-content', minWidth: 28 }}
|
width="max-content"
|
||||||
|
minWidth={28}
|
||||||
>
|
>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={clipVisionOptions}
|
options={clipVisionOptions}
|
||||||
|
@ -5,15 +5,15 @@ import {
|
|||||||
selectControlAdaptersSlice,
|
selectControlAdaptersSlice,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const useControlAdapterIPMethod = (id: string) => {
|
export const useControlAdapterIPMethod = (id: string) => {
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => {
|
createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => {
|
||||||
const cn = selectControlAdapterById(controlAdapters, id);
|
const ca = selectControlAdapterById(controlAdapters, id);
|
||||||
if (cn && cn?.type === 'ip_adapter') {
|
assert(ca?.type === 'ip_adapter');
|
||||||
return cn.method;
|
return ca.method;
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
@ -6,9 +6,8 @@ import { deepClone } from 'common/util/deepClone';
|
|||||||
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
||||||
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import { maskLayerIPAdapterAdded } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { merge, uniq } from 'lodash-es';
|
import { merge, uniq } from 'lodash-es';
|
||||||
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
import { socketInvocationError } from 'services/events/actions';
|
import { socketInvocationError } from 'services/events/actions';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -135,23 +134,46 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
const { id, isEnabled } = action.payload;
|
const { id, isEnabled } = action.payload;
|
||||||
caAdapter.updateOne(state, { id, changes: { isEnabled } });
|
caAdapter.updateOne(state, { id, changes: { isEnabled } });
|
||||||
},
|
},
|
||||||
controlAdapterImageChanged: (
|
controlAdapterImageChanged: (state, action: PayloadAction<{ id: string; controlImage: ImageDTO | null }>) => {
|
||||||
state,
|
|
||||||
action: PayloadAction<{
|
|
||||||
id: string;
|
|
||||||
controlImage: string | null;
|
|
||||||
}>
|
|
||||||
) => {
|
|
||||||
const { id, controlImage } = action.payload;
|
const { id, controlImage } = action.payload;
|
||||||
const ca = selectControlAdapterById(state, id);
|
const ca = selectControlAdapterById(state, id);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
caAdapter.updateOne(state, {
|
if (isControlNetOrT2IAdapter(ca)) {
|
||||||
id,
|
if (controlImage) {
|
||||||
changes: { controlImage, processedControlImage: null },
|
const { image_name, width, height } = controlImage;
|
||||||
});
|
const processorNode = deepClone(ca.processorNode);
|
||||||
|
const minDim = Math.min(controlImage.width, controlImage.height);
|
||||||
|
if ('detect_resolution' in processorNode) {
|
||||||
|
processorNode.detect_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('image_resolution' in processorNode) {
|
||||||
|
processorNode.image_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('resolution' in processorNode) {
|
||||||
|
processorNode.resolution = minDim;
|
||||||
|
}
|
||||||
|
caAdapter.updateOne(state, {
|
||||||
|
id,
|
||||||
|
changes: {
|
||||||
|
processorNode,
|
||||||
|
controlImage: image_name,
|
||||||
|
controlImageDimensions: { width, height },
|
||||||
|
processedControlImage: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
caAdapter.updateOne(state, {
|
||||||
|
id,
|
||||||
|
changes: { controlImage: null, controlImageDimensions: null, processedControlImage: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ip adapter
|
||||||
|
caAdapter.updateOne(state, { id, changes: { controlImage: controlImage?.image_name ?? null } });
|
||||||
|
}
|
||||||
|
|
||||||
if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') {
|
if (controlImage !== null && isControlNetOrT2IAdapter(ca) && ca.processorType !== 'none') {
|
||||||
state.pendingControlImages.push(id);
|
state.pendingControlImages.push(id);
|
||||||
@ -161,7 +183,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
id: string;
|
id: string;
|
||||||
processedControlImage: string | null;
|
processedControlImage: ImageDTO | null;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { id, processedControlImage } = action.payload;
|
const { id, processedControlImage } = action.payload;
|
||||||
@ -174,12 +196,24 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
caAdapter.updateOne(state, {
|
if (processedControlImage) {
|
||||||
id,
|
const { image_name, width, height } = processedControlImage;
|
||||||
changes: {
|
caAdapter.updateOne(state, {
|
||||||
processedControlImage,
|
id,
|
||||||
},
|
changes: {
|
||||||
});
|
processedControlImage: image_name,
|
||||||
|
processedControlImageDimensions: { width, height },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
caAdapter.updateOne(state, {
|
||||||
|
id,
|
||||||
|
changes: {
|
||||||
|
processedControlImage: null,
|
||||||
|
processedControlImageDimensions: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id);
|
state.pendingControlImages = state.pendingControlImages.filter((pendingId) => pendingId !== id);
|
||||||
},
|
},
|
||||||
@ -193,7 +227,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
id: string;
|
id: string;
|
||||||
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig;
|
modelConfig: ControlNetModelConfig | T2IAdapterModelConfig | IPAdapterModelConfig | null;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const { id, modelConfig } = action.payload;
|
const { id, modelConfig } = action.payload;
|
||||||
@ -202,6 +236,11 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modelConfig === null) {
|
||||||
|
caAdapter.updateOne(state, { id, changes: { model: null } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const model = zModelIdentifierField.parse(modelConfig);
|
const model = zModelIdentifierField.parse(modelConfig);
|
||||||
|
|
||||||
if (!isControlNetOrT2IAdapter(cn)) {
|
if (!isControlNetOrT2IAdapter(cn)) {
|
||||||
@ -209,22 +248,36 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = {
|
|
||||||
id,
|
|
||||||
changes: { model, shouldAutoConfig: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
update.changes.processedControlImage = null;
|
|
||||||
|
|
||||||
if (modelConfig.type === 'ip_adapter') {
|
if (modelConfig.type === 'ip_adapter') {
|
||||||
// should never happen...
|
// should never happen...
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processor = buildControlAdapterProcessor(modelConfig);
|
// We always update the model
|
||||||
update.changes.processorType = processor.processorType;
|
const update: Update<ControlNetConfig | T2IAdapterConfig, string> = { id, changes: { model } };
|
||||||
update.changes.processorNode = processor.processorNode;
|
|
||||||
|
|
||||||
|
// Build the default processor for this model
|
||||||
|
const processor = buildControlAdapterProcessor(modelConfig);
|
||||||
|
if (processor.processorType !== cn.processorNode.type) {
|
||||||
|
// If the processor type has changed, update the processor node
|
||||||
|
update.changes.shouldAutoConfig = true;
|
||||||
|
update.changes.processedControlImage = null;
|
||||||
|
update.changes.processorType = processor.processorType;
|
||||||
|
update.changes.processorNode = processor.processorNode;
|
||||||
|
|
||||||
|
if (cn.controlImageDimensions) {
|
||||||
|
const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height);
|
||||||
|
if ('detect_resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.detect_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('image_resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.image_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.resolution = minDim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
caAdapter.updateOne(state, update);
|
caAdapter.updateOne(state, update);
|
||||||
},
|
},
|
||||||
controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
|
controlAdapterWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => {
|
||||||
@ -341,8 +394,23 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
|
|
||||||
if (update.changes.shouldAutoConfig && modelConfig) {
|
if (update.changes.shouldAutoConfig && modelConfig) {
|
||||||
const processor = buildControlAdapterProcessor(modelConfig);
|
const processor = buildControlAdapterProcessor(modelConfig);
|
||||||
update.changes.processorType = processor.processorType;
|
if (processor.processorType !== cn.processorNode.type) {
|
||||||
update.changes.processorNode = processor.processorNode;
|
update.changes.processorType = processor.processorType;
|
||||||
|
update.changes.processorNode = processor.processorNode;
|
||||||
|
// Copy image resolution settings, urgh
|
||||||
|
if (cn.controlImageDimensions) {
|
||||||
|
const minDim = Math.min(cn.controlImageDimensions.width, cn.controlImageDimensions.height);
|
||||||
|
if ('detect_resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.detect_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('image_resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.image_resolution = minDim;
|
||||||
|
}
|
||||||
|
if ('resolution' in update.changes.processorNode) {
|
||||||
|
update.changes.processorNode.resolution = minDim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
caAdapter.updateOne(state, update);
|
caAdapter.updateOne(state, update);
|
||||||
@ -383,10 +451,6 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
builder.addCase(socketInvocationError, (state) => {
|
builder.addCase(socketInvocationError, (state) => {
|
||||||
state.pendingControlImages = [];
|
state.pendingControlImages = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(maskLayerIPAdapterAdded, (state, action) => {
|
|
||||||
caAdapter.addOne(state, buildControlAdapter(action.meta.uuid, 'ip_adapter'));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -225,7 +225,9 @@ export type ControlNetConfig = {
|
|||||||
controlMode: ControlMode;
|
controlMode: ControlMode;
|
||||||
resizeMode: ResizeMode;
|
resizeMode: ResizeMode;
|
||||||
controlImage: string | null;
|
controlImage: string | null;
|
||||||
|
controlImageDimensions: { width: number; height: number } | null;
|
||||||
processedControlImage: string | null;
|
processedControlImage: string | null;
|
||||||
|
processedControlImageDimensions: { width: number; height: number } | null;
|
||||||
processorType: ControlAdapterProcessorType;
|
processorType: ControlAdapterProcessorType;
|
||||||
processorNode: RequiredControlAdapterProcessorNode;
|
processorNode: RequiredControlAdapterProcessorNode;
|
||||||
shouldAutoConfig: boolean;
|
shouldAutoConfig: boolean;
|
||||||
@ -241,7 +243,9 @@ export type T2IAdapterConfig = {
|
|||||||
endStepPct: number;
|
endStepPct: number;
|
||||||
resizeMode: ResizeMode;
|
resizeMode: ResizeMode;
|
||||||
controlImage: string | null;
|
controlImage: string | null;
|
||||||
|
controlImageDimensions: { width: number; height: number } | null;
|
||||||
processedControlImage: string | null;
|
processedControlImage: string | null;
|
||||||
|
processedControlImageDimensions: { width: number; height: number } | null;
|
||||||
processorType: ControlAdapterProcessorType;
|
processorType: ControlAdapterProcessorType;
|
||||||
processorNode: RequiredControlAdapterProcessorNode;
|
processorNode: RequiredControlAdapterProcessorNode;
|
||||||
shouldAutoConfig: boolean;
|
shouldAutoConfig: boolean;
|
||||||
|
@ -20,7 +20,9 @@ export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
|
|||||||
controlMode: 'balanced',
|
controlMode: 'balanced',
|
||||||
resizeMode: 'just_resize',
|
resizeMode: 'just_resize',
|
||||||
controlImage: null,
|
controlImage: null,
|
||||||
|
controlImageDimensions: null,
|
||||||
processedControlImage: null,
|
processedControlImage: null,
|
||||||
|
processedControlImageDimensions: null,
|
||||||
processorType: 'canny_image_processor',
|
processorType: 'canny_image_processor',
|
||||||
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
|
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
|
||||||
shouldAutoConfig: true,
|
shouldAutoConfig: true,
|
||||||
@ -35,7 +37,9 @@ export const initialT2IAdapter: Omit<T2IAdapterConfig, 'id'> = {
|
|||||||
endStepPct: 1,
|
endStepPct: 1,
|
||||||
resizeMode: 'just_resize',
|
resizeMode: 'just_resize',
|
||||||
controlImage: null,
|
controlImage: null,
|
||||||
|
controlImageDimensions: null,
|
||||||
processedControlImage: null,
|
processedControlImage: null,
|
||||||
|
processedControlImageDimensions: null,
|
||||||
processorType: 'canny_image_processor',
|
processorType: 'canny_image_processor',
|
||||||
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
|
processorNode: CONTROLNET_PROCESSORS.canny_image_processor.buildDefaults() as RequiredCannyImageProcessorInvocation,
|
||||||
shouldAutoConfig: true,
|
shouldAutoConfig: true,
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
|
import { guidanceLayerAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const AddLayerButton = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const addRegionalGuidanceLayer = useCallback(() => {
|
||||||
|
dispatch(guidanceLayerAdded('regional_guidance_layer'));
|
||||||
|
}, [dispatch]);
|
||||||
|
const addControlAdapterLayer = useCallback(() => {
|
||||||
|
dispatch(guidanceLayerAdded('control_adapter_layer'));
|
||||||
|
}, [dispatch]);
|
||||||
|
const addIPAdapterLayer = useCallback(() => {
|
||||||
|
dispatch(guidanceLayerAdded('ip_adapter_layer'));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} leftIcon={<PiPlusBold />} variant="ghost">
|
||||||
|
{t('controlLayers.addLayer')}
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidanceLayer}>
|
||||||
|
{t('controlLayers.regionalGuidanceLayer')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<PiPlusBold />} onClick={addControlAdapterLayer}>
|
||||||
|
{t('controlLayers.globalControlAdapterLayer')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapterLayer}>
|
||||||
|
{t('controlLayers.globalIPAdapterLayer')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddLayerButton.displayName = 'AddLayerButton';
|
@ -1,13 +1,13 @@
|
|||||||
import { Button, Flex } from '@invoke-ai/ui-library';
|
import { Button, Flex } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
isVectorMaskLayer,
|
isRegionalGuidanceLayer,
|
||||||
maskLayerIPAdapterAdded,
|
|
||||||
maskLayerNegativePromptChanged,
|
maskLayerNegativePromptChanged,
|
||||||
maskLayerPositivePromptChanged,
|
maskLayerPositivePromptChanged,
|
||||||
selectRegionalPromptsSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
@ -21,9 +21,9 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectValidActions = useMemo(
|
const selectValidActions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
return {
|
return {
|
||||||
canAddPositivePrompt: layer.positivePrompt === null,
|
canAddPositivePrompt: layer.positivePrompt === null,
|
||||||
canAddNegativePrompt: layer.negativePrompt === null,
|
canAddNegativePrompt: layer.negativePrompt === null,
|
||||||
@ -39,7 +39,7 @@ export const AddPromptButtons = ({ layerId }: AddPromptButtonProps) => {
|
|||||||
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' }));
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
const addIPAdapter = useCallback(() => {
|
const addIPAdapter = useCallback(() => {
|
||||||
dispatch(maskLayerIPAdapterAdded(layerId));
|
dispatch(guidanceLayerIPAdapterAdded(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
|
|
||||||
return (
|
return (
|
@ -10,7 +10,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { brushSizeChanged, initialRegionalPromptsState } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { brushSizeChanged, initialControlLayersState } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -20,22 +20,22 @@ const formatPx = (v: number | string) => `${v} px`;
|
|||||||
export const BrushSize = memo(() => {
|
export const BrushSize = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const brushSize = useAppSelector((s) => s.regionalPrompts.present.brushSize);
|
const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
dispatch(brushSizeChanged(v));
|
dispatch(brushSizeChanged(Math.round(v)));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<FormControl w="min-content">
|
<FormControl w="min-content">
|
||||||
<FormLabel m={0}>{t('regionalPrompts.brushSize')}</FormLabel>
|
<FormLabel m={0}>{t('controlLayers.brushSize')}</FormLabel>
|
||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<CompositeNumberInput
|
<CompositeNumberInput
|
||||||
min={1}
|
min={1}
|
||||||
max={600}
|
max={600}
|
||||||
defaultValue={initialRegionalPromptsState.brushSize}
|
defaultValue={initialControlLayersState.brushSize}
|
||||||
value={brushSize}
|
value={brushSize}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
w={24}
|
w={24}
|
||||||
@ -48,7 +48,7 @@ export const BrushSize = memo(() => {
|
|||||||
<CompositeSlider
|
<CompositeSlider
|
||||||
min={1}
|
min={1}
|
||||||
max={300}
|
max={300}
|
||||||
defaultValue={initialRegionalPromptsState.brushSize}
|
defaultValue={initialControlLayersState.brushSize}
|
||||||
value={brushSize}
|
value={brushSize}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
marks={marks}
|
marks={marks}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import CALayerOpacity from 'features/controlLayers/components/CALayerOpacity';
|
||||||
|
import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig';
|
||||||
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton';
|
||||||
|
import { LayerMenu } from 'features/controlLayers/components/LayerMenu';
|
||||||
|
import { LayerTitle } from 'features/controlLayers/components/LayerTitle';
|
||||||
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle';
|
||||||
|
import {
|
||||||
|
isControlAdapterLayer,
|
||||||
|
layerSelected,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CALayerListItem = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isControlAdapterLayer(layer), `Layer ${layerId} not found or not a ControlNet layer`);
|
||||||
|
return {
|
||||||
|
controlNetId: layer.controlNetId,
|
||||||
|
isSelected: layerId === controlLayers.present.selectedLayerId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const { controlNetId, isSelected } = useAppSelector(selector);
|
||||||
|
const onClickCapture = useCallback(() => {
|
||||||
|
// Must be capture so that the layer is selected before deleting/resetting/etc
|
||||||
|
dispatch(layerSelected(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={2}
|
||||||
|
onClickCapture={onClickCapture}
|
||||||
|
bg={isSelected ? 'base.400' : 'base.800'}
|
||||||
|
px={2}
|
||||||
|
borderRadius="base"
|
||||||
|
py="1px"
|
||||||
|
>
|
||||||
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
|
<LayerTitle type="control_adapter_layer" />
|
||||||
|
<Spacer />
|
||||||
|
<CALayerOpacity layerId={layerId} />
|
||||||
|
<LayerMenu layerId={layerId} />
|
||||||
|
<LayerDeleteButton layerId={layerId} />
|
||||||
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
|
<ControlAdapterLayerConfig id={controlNetId} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CALayerListItem.displayName = 'CALayerListItem';
|
@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
CompositeNumberInput,
|
||||||
|
CompositeSlider,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Switch,
|
||||||
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
|
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
|
import { isFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { ChangeEvent } from 'react';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiDropHalfFill } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const marks = [0, 25, 50, 75, 100];
|
||||||
|
const formatPct = (v: number | string) => `${v} %`;
|
||||||
|
|
||||||
|
const CALayerOpacity = ({ layerId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { opacity, isFilterEnabled } = useLayerOpacity(layerId);
|
||||||
|
const onChangeOpacity = useCallback(
|
||||||
|
(v: number) => {
|
||||||
|
dispatch(layerOpacityChanged({ layerId, opacity: v / 100 }));
|
||||||
|
},
|
||||||
|
[dispatch, layerId]
|
||||||
|
);
|
||||||
|
const onChangeFilter = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(isFilterEnabledChanged({ layerId, isFilterEnabled: e.target.checked }));
|
||||||
|
},
|
||||||
|
[dispatch, layerId]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('controlLayers.opacity')}
|
||||||
|
size="sm"
|
||||||
|
icon={<PiDropHalfFill size={16} />}
|
||||||
|
variant="ghost"
|
||||||
|
onDoubleClick={stopPropagation}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex direction="column" gap={2}>
|
||||||
|
<FormControl orientation="horizontal" w="full">
|
||||||
|
<FormLabel m={0} flexGrow={1} cursor="pointer">
|
||||||
|
{t('controlLayers.opacityFilter')}
|
||||||
|
</FormLabel>
|
||||||
|
<Switch isChecked={isFilterEnabled} onChange={onChangeFilter} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl orientation="horizontal">
|
||||||
|
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||||
|
<CompositeSlider
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={opacity}
|
||||||
|
defaultValue={100}
|
||||||
|
onChange={onChangeOpacity}
|
||||||
|
marks={marks}
|
||||||
|
w={48}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={opacity}
|
||||||
|
defaultValue={100}
|
||||||
|
onChange={onChangeOpacity}
|
||||||
|
w={24}
|
||||||
|
format={formatPct}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CALayerOpacity);
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
|
||||||
|
const meta: Meta<typeof ControlLayersEditor> = {
|
||||||
|
title: 'Feature/ControlLayers',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
component: ControlLayersEditor,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ControlLayersEditor>;
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
return (
|
||||||
|
<Flex w={1500} h={1500}>
|
||||||
|
<ControlLayersEditor />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: Component,
|
||||||
|
};
|
@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { RegionalPromptsToolbar } from 'features/regionalPrompts/components/RegionalPromptsToolbar';
|
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
||||||
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
|
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const RegionalPromptsEditor = memo(() => {
|
export const ControlLayersEditor = memo(() => {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
position="relative"
|
position="relative"
|
||||||
@ -15,10 +15,10 @@ export const RegionalPromptsEditor = memo(() => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<RegionalPromptsToolbar />
|
<ControlLayersToolbar />
|
||||||
<StageComponent />
|
<StageComponent />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RegionalPromptsEditor.displayName = 'RegionalPromptsEditor';
|
ControlLayersEditor.displayName = 'ControlLayersEditor';
|
@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable i18next/no-literal-string */
|
||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
|
import { AddLayerButton } from 'features/controlLayers/components/AddLayerButton';
|
||||||
|
import { CALayerListItem } from 'features/controlLayers/components/CALayerListItem';
|
||||||
|
import { DeleteAllLayersButton } from 'features/controlLayers/components/DeleteAllLayersButton';
|
||||||
|
import { IPLayerListItem } from 'features/controlLayers/components/IPLayerListItem';
|
||||||
|
import { RGLayerListItem } from 'features/controlLayers/components/RGLayerListItem';
|
||||||
|
import { isRenderableLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
|
import { partition } from 'lodash-es';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const selectLayerIdTypePairs = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const [renderableLayers, ipAdapterLayers] = partition(controlLayers.present.layers, isRenderableLayer);
|
||||||
|
return [...ipAdapterLayers, ...renderableLayers].map((l) => ({ id: l.id, type: l.type })).reverse();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ControlLayersPanelContent = memo(() => {
|
||||||
|
const layerIdTypePairs = useAppSelector(selectLayerIdTypePairs);
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={4} w="full" h="full">
|
||||||
|
<Flex justifyContent="space-around">
|
||||||
|
<AddLayerButton />
|
||||||
|
<DeleteAllLayersButton />
|
||||||
|
</Flex>
|
||||||
|
<ScrollableContent>
|
||||||
|
<Flex flexDir="column" gap={4}>
|
||||||
|
{layerIdTypePairs.map(({ id, type }) => (
|
||||||
|
<LayerWrapper key={id} id={id} type={type} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ScrollableContent>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ControlLayersPanelContent.displayName = 'ControlLayersPanelContent';
|
||||||
|
|
||||||
|
type LayerWrapperProps = {
|
||||||
|
id: string;
|
||||||
|
type: Layer['type'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const LayerWrapper = memo(({ id, type }: LayerWrapperProps) => {
|
||||||
|
if (type === 'regional_guidance_layer') {
|
||||||
|
return <RGLayerListItem key={id} layerId={id} />;
|
||||||
|
}
|
||||||
|
if (type === 'control_adapter_layer') {
|
||||||
|
return <CALayerListItem key={id} layerId={id} />;
|
||||||
|
}
|
||||||
|
if (type === 'ip_adapter_layer') {
|
||||||
|
return <IPLayerListItem key={id} layerId={id} />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LayerWrapper.displayName = 'LayerWrapper';
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Flex, IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||||
|
import { GlobalMaskLayerOpacity } from 'features/controlLayers/components/GlobalMaskLayerOpacity';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RiSettings4Fill } from 'react-icons/ri';
|
||||||
|
|
||||||
|
const ControlLayersSettingsPopover = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverBody>
|
||||||
|
<Flex direction="column" gap={2}>
|
||||||
|
<GlobalMaskLayerOpacity />
|
||||||
|
</Flex>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ControlLayersSettingsPopover);
|
@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable i18next/no-literal-string */
|
||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { BrushSize } from 'features/controlLayers/components/BrushSize';
|
||||||
|
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
|
||||||
|
import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
|
||||||
|
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const ControlLayersToolbar = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex gap={4}>
|
||||||
|
<BrushSize />
|
||||||
|
<ToolChooser />
|
||||||
|
<UndoRedoButtonGroup />
|
||||||
|
<ControlLayersSettingsPopover />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ControlLayersToolbar.displayName = 'ControlLayersToolbar';
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@invoke-ai/ui-library';
|
import { Button } from '@invoke-ai/ui-library';
|
||||||
|
import { allLayersDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { allLayersDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
@ -14,7 +14,7 @@ export const DeleteAllLayersButton = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
|
<Button onClick={onClick} leftIcon={<PiTrashSimpleBold />} variant="ghost" colorScheme="error">
|
||||||
{t('regionalPrompts.deleteAll')}
|
{t('controlLayers.deleteAll')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
@ -0,0 +1,54 @@
|
|||||||
|
import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
globalMaskLayerOpacityChanged,
|
||||||
|
initialControlLayersState,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const marks = [0, 25, 50, 75, 100];
|
||||||
|
const formatPct = (v: number | string) => `${v} %`;
|
||||||
|
|
||||||
|
export const GlobalMaskLayerOpacity = memo(() => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const globalMaskLayerOpacity = useAppSelector((s) =>
|
||||||
|
Math.round(s.controlLayers.present.globalMaskLayerOpacity * 100)
|
||||||
|
);
|
||||||
|
const onChange = useCallback(
|
||||||
|
(v: number) => {
|
||||||
|
dispatch(globalMaskLayerOpacityChanged(v / 100));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormControl orientation="vertical">
|
||||||
|
<FormLabel m={0}>{t('controlLayers.globalMaskOpacity')}</FormLabel>
|
||||||
|
<Flex gap={4}>
|
||||||
|
<CompositeSlider
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={globalMaskLayerOpacity}
|
||||||
|
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
minW={48}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={globalMaskLayerOpacity}
|
||||||
|
defaultValue={initialControlLayersState.globalMaskLayerOpacity * 100}
|
||||||
|
onChange={onChange}
|
||||||
|
w={28}
|
||||||
|
format={formatPct}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
GlobalMaskLayerOpacity.displayName = 'GlobalMaskLayerOpacity';
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig';
|
||||||
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton';
|
||||||
|
import { LayerTitle } from 'features/controlLayers/components/LayerTitle';
|
||||||
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle';
|
||||||
|
import { isIPAdapterLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IPLayerListItem = memo(({ layerId }: Props) => {
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isIPAdapterLayer(layer), `Layer ${layerId} not found or not an IP Adapter layer`);
|
||||||
|
return layer.ipAdapterId;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const ipAdapterId = useAppSelector(selector);
|
||||||
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
return (
|
||||||
|
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
|
||||||
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
|
<LayerTitle type="ip_adapter_layer" />
|
||||||
|
<Spacer />
|
||||||
|
<LayerDeleteButton layerId={layerId} />
|
||||||
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
|
<ControlAdapterLayerConfig id={ipAdapterId} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
IPLayerListItem.displayName = 'IPLayerListItem';
|
@ -1,17 +1,18 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { layerDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
type Props = { layerId: string };
|
type Props = { layerId: string };
|
||||||
|
|
||||||
export const RPLayerDeleteButton = memo(({ layerId }: Props) => {
|
export const LayerDeleteButton = memo(({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const deleteLayer = useCallback(() => {
|
const deleteLayer = useCallback(() => {
|
||||||
dispatch(layerDeleted(layerId));
|
dispatch(guidanceLayerDeleted(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -21,8 +22,9 @@ export const RPLayerDeleteButton = memo(({ layerId }: Props) => {
|
|||||||
tooltip={t('common.delete')}
|
tooltip={t('common.delete')}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={deleteLayer}
|
onClick={deleteLayer}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerDeleteButton.displayName = 'RPLayerDeleteButton';
|
LayerDeleteButton.displayName = 'LayerDeleteButton';
|
@ -0,0 +1,59 @@
|
|||||||
|
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
|
import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerMenuArrangeActions';
|
||||||
|
import { LayerMenuRGActions } from 'features/controlLayers/components/LayerMenuRGActions';
|
||||||
|
import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
|
import { layerDeleted, layerReset } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowCounterClockwiseBold, PiDotsThreeVerticalBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
type Props = { layerId: string };
|
||||||
|
|
||||||
|
export const LayerMenu = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const layerType = useLayerType(layerId);
|
||||||
|
const resetLayer = useCallback(() => {
|
||||||
|
dispatch(layerReset(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const deleteLayer = useCallback(() => {
|
||||||
|
dispatch(layerDeleted(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Layer menu"
|
||||||
|
size="sm"
|
||||||
|
icon={<PiDotsThreeVerticalBold />}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
{layerType === 'regional_guidance_layer' && (
|
||||||
|
<>
|
||||||
|
<LayerMenuRGActions layerId={layerId} />
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(layerType === 'regional_guidance_layer' || layerType === 'control_adapter_layer') && (
|
||||||
|
<>
|
||||||
|
<LayerMenuArrangeActions layerId={layerId} />
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{layerType === 'regional_guidance_layer' && (
|
||||||
|
<MenuItem onClick={resetLayer} icon={<PiArrowCounterClockwiseBold />}>
|
||||||
|
{t('accessibility.reset')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem onClick={deleteLayer} icon={<PiTrashSimpleBold />} color="error.300">
|
||||||
|
{t('common.delete')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LayerMenu.displayName = 'LayerMenu';
|
@ -0,0 +1,69 @@
|
|||||||
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
isRenderableLayer,
|
||||||
|
layerMovedBackward,
|
||||||
|
layerMovedForward,
|
||||||
|
layerMovedToBack,
|
||||||
|
layerMovedToFront,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowDownBold, PiArrowLineDownBold, PiArrowLineUpBold, PiArrowUpBold } from 'react-icons/pi';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = { layerId: string };
|
||||||
|
|
||||||
|
export const LayerMenuArrangeActions = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const selectValidActions = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isRenderableLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
const layerIndex = controlLayers.present.layers.findIndex((l) => l.id === layerId);
|
||||||
|
const layerCount = controlLayers.present.layers.length;
|
||||||
|
return {
|
||||||
|
canMoveForward: layerIndex < layerCount - 1,
|
||||||
|
canMoveBackward: layerIndex > 0,
|
||||||
|
canMoveToFront: layerIndex < layerCount - 1,
|
||||||
|
canMoveToBack: layerIndex > 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const validActions = useAppSelector(selectValidActions);
|
||||||
|
const moveForward = useCallback(() => {
|
||||||
|
dispatch(layerMovedForward(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const moveToFront = useCallback(() => {
|
||||||
|
dispatch(layerMovedToFront(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const moveBackward = useCallback(() => {
|
||||||
|
dispatch(layerMovedBackward(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const moveToBack = useCallback(() => {
|
||||||
|
dispatch(layerMovedToBack(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={moveToFront} isDisabled={!validActions.canMoveToFront} icon={<PiArrowLineUpBold />}>
|
||||||
|
{t('controlLayers.moveToFront')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveForward} isDisabled={!validActions.canMoveForward} icon={<PiArrowUpBold />}>
|
||||||
|
{t('controlLayers.moveForward')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveBackward} isDisabled={!validActions.canMoveBackward} icon={<PiArrowDownBold />}>
|
||||||
|
{t('controlLayers.moveBackward')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={moveToBack} isDisabled={!validActions.canMoveToBack} icon={<PiArrowLineDownBold />}>
|
||||||
|
{t('controlLayers.moveToBack')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LayerMenuArrangeActions.displayName = 'LayerMenuArrangeActions';
|
@ -0,0 +1,58 @@
|
|||||||
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { guidanceLayerIPAdapterAdded } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = { layerId: string };
|
||||||
|
|
||||||
|
export const LayerMenuRGActions = memo(({ layerId }: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const selectValidActions = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
return {
|
||||||
|
canAddPositivePrompt: layer.positivePrompt === null,
|
||||||
|
canAddNegativePrompt: layer.negativePrompt === null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const validActions = useAppSelector(selectValidActions);
|
||||||
|
const addPositivePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerPositivePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addNegativePrompt = useCallback(() => {
|
||||||
|
dispatch(maskLayerNegativePromptChanged({ layerId, prompt: '' }));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
const addIPAdapter = useCallback(() => {
|
||||||
|
dispatch(guidanceLayerIPAdapterAdded(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt} icon={<PiPlusBold />}>
|
||||||
|
{t('controlLayers.addPositivePrompt')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt} icon={<PiPlusBold />}>
|
||||||
|
{t('controlLayers.addNegativePrompt')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={addIPAdapter} icon={<PiPlusBold />}>
|
||||||
|
{t('controlLayers.addIPAdapter')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LayerMenuRGActions.displayName = 'LayerMenuRGActions';
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Text } from '@invoke-ai/ui-library';
|
||||||
|
import type { Layer } from 'features/controlLayers/store/types';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: Layer['type'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayerTitle = memo(({ type }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const title = useMemo(() => {
|
||||||
|
if (type === 'regional_guidance_layer') {
|
||||||
|
return t('controlLayers.regionalGuidance');
|
||||||
|
} else if (type === 'control_adapter_layer') {
|
||||||
|
return t('controlLayers.globalControlAdapter');
|
||||||
|
} else if (type === 'ip_adapter_layer') {
|
||||||
|
return t('controlLayers.globalIPAdapter');
|
||||||
|
}
|
||||||
|
}, [t, type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LayerTitle.displayName = 'LayerTitle';
|
@ -1,7 +1,8 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useLayerIsVisible } from 'features/regionalPrompts/hooks/layerStateHooks';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { layerVisibilityToggled } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { useLayerIsVisible } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
|
import { layerVisibilityToggled } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold } from 'react-icons/pi';
|
import { PiCheckBold } from 'react-icons/pi';
|
||||||
@ -10,7 +11,7 @@ type Props = {
|
|||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerVisibilityToggle = memo(({ layerId }: Props) => {
|
export const LayerVisibilityToggle = memo(({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isVisible = useLayerIsVisible(layerId);
|
const isVisible = useLayerIsVisible(layerId);
|
||||||
@ -21,14 +22,15 @@ export const RPLayerVisibilityToggle = memo(({ layerId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={t('regionalPrompts.toggleVisibility')}
|
aria-label={t('controlLayers.toggleVisibility')}
|
||||||
tooltip={t('regionalPrompts.toggleVisibility')}
|
tooltip={t('controlLayers.toggleVisibility')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon={isVisible ? <PiCheckBold /> : undefined}
|
icon={isVisible ? <PiCheckBold /> : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
colorScheme="base"
|
colorScheme="base"
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerVisibilityToggle.displayName = 'RPLayerVisibilityToggle';
|
LayerVisibilityToggle.displayName = 'LayerVisibilityToggle';
|
@ -2,10 +2,10 @@ import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
isVectorMaskLayer,
|
isRegionalGuidanceLayer,
|
||||||
maskLayerAutoNegativeChanged,
|
maskLayerAutoNegativeChanged,
|
||||||
selectRegionalPromptsSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -18,9 +18,9 @@ type Props = {
|
|||||||
const useAutoNegative = (layerId: string) => {
|
const useAutoNegative = (layerId: string) => {
|
||||||
const selectAutoNegative = useMemo(
|
const selectAutoNegative = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
return layer.autoNegative;
|
return layer.autoNegative;
|
||||||
}),
|
}),
|
||||||
[layerId]
|
[layerId]
|
||||||
@ -29,7 +29,7 @@ const useAutoNegative = (layerId: string) => {
|
|||||||
return autoNegative;
|
return autoNegative;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
|
export const RGLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const autoNegative = useAutoNegative(layerId);
|
const autoNegative = useAutoNegative(layerId);
|
||||||
@ -42,10 +42,10 @@ export const RPLayerAutoNegativeCheckbox = memo(({ layerId }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl gap={2}>
|
<FormControl gap={2}>
|
||||||
<FormLabel m={0}>{t('regionalPrompts.autoNegative')}</FormLabel>
|
<FormLabel m={0}>{t('controlLayers.autoNegative')}</FormLabel>
|
||||||
<Checkbox size="md" isChecked={autoNegative === 'invert'} onChange={onChange} />
|
<Checkbox size="md" isChecked={autoNegative === 'invert'} onChange={onChange} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerAutoNegativeCheckbox.displayName = 'RPLayerAutoNegativeCheckbox';
|
RGLayerAutoNegativeCheckbox.displayName = 'RGLayerAutoNegativeCheckbox';
|
@ -2,12 +2,13 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } f
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import {
|
import {
|
||||||
isVectorMaskLayer,
|
isRegionalGuidanceLayer,
|
||||||
maskLayerPreviewColorChanged,
|
maskLayerPreviewColorChanged,
|
||||||
selectRegionalPromptsSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -17,13 +18,13 @@ type Props = {
|
|||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
export const RGLayerColorPicker = memo(({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectColor = useMemo(
|
const selectColor = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = regionalPrompts.present.layers.find((l) => l.id === layerId);
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an vector mask layer`);
|
||||||
return layer.previewColor;
|
return layer.previewColor;
|
||||||
}),
|
}),
|
||||||
[layerId]
|
[layerId]
|
||||||
@ -40,10 +41,10 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
<Popover isLazy>
|
<Popover isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip label={t('regionalPrompts.maskPreviewColor')}>
|
<Tooltip label={t('controlLayers.maskPreviewColor')}>
|
||||||
<Flex
|
<Flex
|
||||||
as="button"
|
as="button"
|
||||||
aria-label={t('regionalPrompts.maskPreviewColor')}
|
aria-label={t('controlLayers.maskPreviewColor')}
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
bg={rgbColorToString(color)}
|
bg={rgbColorToString(color)}
|
||||||
@ -51,6 +52,7 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
h={8}
|
h={8}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
@ -64,4 +66,4 @@ export const RPLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerColorPicker.displayName = 'RPLayerColorPicker';
|
RGLayerColorPicker.displayName = 'RGLayerColorPicker';
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Divider, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { guidanceLayerIPAdapterDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig';
|
||||||
|
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RGLayerIPAdapterList = memo(({ layerId }: Props) => {
|
||||||
|
const selectIPAdapterIds = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.filter(isRegionalGuidanceLayer).find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
return layer.ipAdapterIds;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const ipAdapterIds = useAppSelector(selectIPAdapterIds);
|
||||||
|
|
||||||
|
if (ipAdapterIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ipAdapterIds.map((id, index) => (
|
||||||
|
<Flex flexDir="column" key={id}>
|
||||||
|
{index > 0 && (
|
||||||
|
<Flex pb={3}>
|
||||||
|
<Divider />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<RGLayerIPAdapterListItem layerId={layerId} ipAdapterId={id} ipAdapterNumber={index + 1} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RGLayerIPAdapterList.displayName = 'RGLayerIPAdapterList';
|
||||||
|
|
||||||
|
type IPAdapterListItemProps = {
|
||||||
|
layerId: string;
|
||||||
|
ipAdapterId: string;
|
||||||
|
ipAdapterNumber: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RGLayerIPAdapterListItem = memo(({ layerId, ipAdapterId, ipAdapterNumber }: IPAdapterListItemProps) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onDeleteIPAdapter = useCallback(() => {
|
||||||
|
dispatch(guidanceLayerIPAdapterDeleted({ layerId, ipAdapterId }));
|
||||||
|
}, [dispatch, ipAdapterId, layerId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={3}>
|
||||||
|
<Flex alignItems="center" gap={3}>
|
||||||
|
<Text fontWeight="semibold" color="base.400">{`IP Adapter ${ipAdapterNumber}`}</Text>
|
||||||
|
<Spacer />
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={<PiTrashSimpleBold />}
|
||||||
|
aria-label="Delete IP Adapter"
|
||||||
|
onClick={onDeleteIPAdapter}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="error"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<ControlAdapterLayerConfig id={ipAdapterId} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RGLayerIPAdapterListItem.displayName = 'RGLayerIPAdapterListItem';
|
@ -0,0 +1,84 @@
|
|||||||
|
import { Badge, Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
|
import { LayerDeleteButton } from 'features/controlLayers/components/LayerDeleteButton';
|
||||||
|
import { LayerMenu } from 'features/controlLayers/components/LayerMenu';
|
||||||
|
import { LayerTitle } from 'features/controlLayers/components/LayerTitle';
|
||||||
|
import { LayerVisibilityToggle } from 'features/controlLayers/components/LayerVisibilityToggle';
|
||||||
|
import { RGLayerColorPicker } from 'features/controlLayers/components/RGLayerColorPicker';
|
||||||
|
import { RGLayerIPAdapterList } from 'features/controlLayers/components/RGLayerIPAdapterList';
|
||||||
|
import { RGLayerNegativePrompt } from 'features/controlLayers/components/RGLayerNegativePrompt';
|
||||||
|
import { RGLayerPositivePrompt } from 'features/controlLayers/components/RGLayerPositivePrompt';
|
||||||
|
import RGLayerSettingsPopover from 'features/controlLayers/components/RGLayerSettingsPopover';
|
||||||
|
import {
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
layerSelected,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
import { AddPromptButtons } from './AddPromptButtons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
layerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RGLayerListItem = memo(({ layerId }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selector = useMemo(
|
||||||
|
() =>
|
||||||
|
createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
return {
|
||||||
|
color: rgbColorToString(layer.previewColor),
|
||||||
|
hasPositivePrompt: layer.positivePrompt !== null,
|
||||||
|
hasNegativePrompt: layer.negativePrompt !== null,
|
||||||
|
hasIPAdapters: layer.ipAdapterIds.length > 0,
|
||||||
|
isSelected: layerId === controlLayers.present.selectedLayerId,
|
||||||
|
autoNegative: layer.autoNegative,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(layerSelected(layerId));
|
||||||
|
}, [dispatch, layerId]);
|
||||||
|
return (
|
||||||
|
<Flex gap={2} onClick={onClick} bg={isSelected ? color : 'base.800'} px={2} borderRadius="base" py="1px">
|
||||||
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
|
<LayerTitle type="regional_guidance_layer" />
|
||||||
|
<Spacer />
|
||||||
|
{autoNegative === 'invert' && (
|
||||||
|
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
|
||||||
|
{t('controlLayers.autoNegative')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<RGLayerColorPicker layerId={layerId} />
|
||||||
|
<RGLayerSettingsPopover layerId={layerId} />
|
||||||
|
<LayerMenu layerId={layerId} />
|
||||||
|
<LayerDeleteButton layerId={layerId} />
|
||||||
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex flexDir="column" gap={3} px={3} pb={3}>
|
||||||
|
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
|
||||||
|
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
|
||||||
|
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
|
||||||
|
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RGLayerListItem.displayName = 'RGLayerListItem';
|
@ -1,12 +1,12 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayerPromptDeleteButton';
|
||||||
|
import { useLayerNegativePrompt } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
|
import { maskLayerNegativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { RPLayerPromptDeleteButton } from 'features/regionalPrompts/components/RPLayerPromptDeleteButton';
|
|
||||||
import { useLayerNegativePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
|
||||||
import { maskLayerNegativePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ type Props = {
|
|||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerNegativePrompt = memo(({ layerId }: Props) => {
|
export const RGLayerNegativePrompt = memo(({ layerId }: Props) => {
|
||||||
const prompt = useLayerNegativePrompt(layerId);
|
const prompt = useLayerNegativePrompt(layerId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@ -47,7 +47,7 @@ export const RPLayerNegativePrompt = memo(({ layerId }: Props) => {
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RPLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="negative" />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
</Box>
|
</Box>
|
||||||
@ -55,4 +55,4 @@ export const RPLayerNegativePrompt = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerNegativePrompt.displayName = 'RPLayerNegativePrompt';
|
RGLayerNegativePrompt.displayName = 'RGLayerNegativePrompt';
|
@ -1,12 +1,12 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { RGLayerPromptDeleteButton } from 'features/controlLayers/components/RGLayerPromptDeleteButton';
|
||||||
|
import { useLayerPositivePrompt } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
|
import { maskLayerPositivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
import { RPLayerPromptDeleteButton } from 'features/regionalPrompts/components/RPLayerPromptDeleteButton';
|
|
||||||
import { useLayerPositivePrompt } from 'features/regionalPrompts/hooks/layerStateHooks';
|
|
||||||
import { maskLayerPositivePromptChanged } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { memo, useCallback, useRef } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ type Props = {
|
|||||||
layerId: string;
|
layerId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerPositivePrompt = memo(({ layerId }: Props) => {
|
export const RGLayerPositivePrompt = memo(({ layerId }: Props) => {
|
||||||
const prompt = useLayerPositivePrompt(layerId);
|
const prompt = useLayerPositivePrompt(layerId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@ -47,7 +47,7 @@ export const RPLayerPositivePrompt = memo(({ layerId }: Props) => {
|
|||||||
minH={28}
|
minH={28}
|
||||||
/>
|
/>
|
||||||
<PromptOverlayButtonWrapper>
|
<PromptOverlayButtonWrapper>
|
||||||
<RPLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
<RGLayerPromptDeleteButton layerId={layerId} polarity="positive" />
|
||||||
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
|
||||||
</PromptOverlayButtonWrapper>
|
</PromptOverlayButtonWrapper>
|
||||||
</Box>
|
</Box>
|
||||||
@ -55,4 +55,4 @@ export const RPLayerPositivePrompt = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerPositivePrompt.displayName = 'RPLayerPositivePrompt';
|
RGLayerPositivePrompt.displayName = 'RGLayerPositivePrompt';
|
@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
|||||||
import {
|
import {
|
||||||
maskLayerNegativePromptChanged,
|
maskLayerNegativePromptChanged,
|
||||||
maskLayerPositivePromptChanged,
|
maskLayerPositivePromptChanged,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
@ -13,7 +13,7 @@ type Props = {
|
|||||||
polarity: 'positive' | 'negative';
|
polarity: 'positive' | 'negative';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => {
|
export const RGLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
@ -24,10 +24,10 @@ export const RPLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) =>
|
|||||||
}
|
}
|
||||||
}, [dispatch, layerId, polarity]);
|
}, [dispatch, layerId, polarity]);
|
||||||
return (
|
return (
|
||||||
<Tooltip label={t('regionalPrompts.deletePrompt')}>
|
<Tooltip label={t('controlLayers.deletePrompt')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="promptOverlay"
|
variant="promptOverlay"
|
||||||
aria-label={t('regionalPrompts.deletePrompt')}
|
aria-label={t('controlLayers.deletePrompt')}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
@ -35,4 +35,4 @@ export const RPLayerPromptDeleteButton = memo(({ layerId, polarity }: Props) =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RPLayerPromptDeleteButton.displayName = 'RPLayerPromptDeleteButton';
|
RGLayerPromptDeleteButton.displayName = 'RGLayerPromptDeleteButton';
|
@ -9,7 +9,8 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { RPLayerAutoNegativeCheckbox } from 'features/regionalPrompts/components/RPLayerAutoNegativeCheckbox';
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
|
import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayerAutoNegativeCheckbox';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiGearSixBold } from 'react-icons/pi';
|
import { PiGearSixBold } from 'react-icons/pi';
|
||||||
@ -23,7 +24,7 @@ const formLabelProps: FormLabelProps = {
|
|||||||
minW: 32,
|
minW: 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RPLayerSettingsPopover = ({ layerId }: Props) => {
|
const RGLayerSettingsPopover = ({ layerId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,6 +35,7 @@ const RPLayerSettingsPopover = ({ layerId }: Props) => {
|
|||||||
aria-label={t('common.settingsLabel')}
|
aria-label={t('common.settingsLabel')}
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<PiGearSixBold />}
|
icon={<PiGearSixBold />}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
@ -41,7 +43,7 @@ const RPLayerSettingsPopover = ({ layerId }: Props) => {
|
|||||||
<PopoverBody>
|
<PopoverBody>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
<FormControlGroup formLabelProps={formLabelProps}>
|
<FormControlGroup formLabelProps={formLabelProps}>
|
||||||
<RPLayerAutoNegativeCheckbox layerId={layerId} />
|
<RGLayerAutoNegativeCheckbox layerId={layerId} />
|
||||||
</FormControlGroup>
|
</FormControlGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverBody>
|
</PopoverBody>
|
||||||
@ -50,4 +52,4 @@ const RPLayerSettingsPopover = ({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(RPLayerSettingsPopover);
|
export default memo(RGLayerSettingsPopover);
|
@ -1,59 +1,63 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useMouseEvents } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
import { useMouseEvents } from 'features/controlLayers/hooks/mouseEventHooks';
|
||||||
import {
|
import {
|
||||||
$cursorPosition,
|
$cursorPosition,
|
||||||
$isMouseOver,
|
$isMouseOver,
|
||||||
$lastMouseDownPos,
|
$lastMouseDownPos,
|
||||||
$tool,
|
$tool,
|
||||||
isVectorMaskLayer,
|
isRegionalGuidanceLayer,
|
||||||
layerBboxChanged,
|
layerBboxChanged,
|
||||||
layerSelected,
|
|
||||||
layerTranslated,
|
layerTranslated,
|
||||||
selectRegionalPromptsSlice,
|
selectControlLayersSlice,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { debouncedRenderers, renderers as normalRenderers } from 'features/regionalPrompts/util/renderers';
|
import { debouncedRenderers, renderers as normalRenderers } from 'features/controlLayers/util/renderers';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import type { MutableRefObject } from 'react';
|
import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||||
import { assert } from 'tsafe';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
|
||||||
Konva.showWarnings = false;
|
Konva.showWarnings = false;
|
||||||
|
|
||||||
const log = logger('regionalPrompts');
|
const log = logger('controlLayers');
|
||||||
|
|
||||||
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
const selectSelectedLayerColor = createMemoizedSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
const layer = regionalPrompts.present.layers.find((l) => l.id === regionalPrompts.present.selectedLayerId);
|
const layer = controlLayers.present.layers
|
||||||
if (!layer) {
|
.filter(isRegionalGuidanceLayer)
|
||||||
return null;
|
.find((l) => l.id === controlLayers.present.selectedLayerId);
|
||||||
}
|
return layer?.previewColor ?? null;
|
||||||
assert(isVectorMaskLayer(layer), `Layer ${regionalPrompts.present.selectedLayerId} is not an RP layer`);
|
});
|
||||||
return layer.previewColor;
|
|
||||||
|
const selectSelectedLayerType = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
||||||
|
return selectedLayer?.type ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const useStageRenderer = (
|
const useStageRenderer = (
|
||||||
stageRef: MutableRefObject<Konva.Stage>,
|
stage: Konva.Stage,
|
||||||
container: HTMLDivElement | null,
|
container: HTMLDivElement | null,
|
||||||
wrapper: HTMLDivElement | null,
|
wrapper: HTMLDivElement | null,
|
||||||
asPreview: boolean
|
asPreview: boolean
|
||||||
) => {
|
) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const width = useAppSelector((s) => s.generation.width);
|
const state = useAppSelector((s) => s.controlLayers.present);
|
||||||
const height = useAppSelector((s) => s.generation.height);
|
|
||||||
const state = useAppSelector((s) => s.regionalPrompts.present);
|
|
||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
|
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel } = useMouseEvents();
|
||||||
const cursorPosition = useStore($cursorPosition);
|
const cursorPosition = useStore($cursorPosition);
|
||||||
const lastMouseDownPos = useStore($lastMouseDownPos);
|
const lastMouseDownPos = useStore($lastMouseDownPos);
|
||||||
const isMouseOver = useStore($isMouseOver);
|
const isMouseOver = useStore($isMouseOver);
|
||||||
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
|
||||||
|
const selectedLayerType = useAppSelector(selectSelectedLayerType);
|
||||||
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
|
const layerIds = useMemo(() => state.layers.map((l) => l.id), [state.layers]);
|
||||||
|
const layerCount = useMemo(() => state.layers.length, [state.layers]);
|
||||||
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
const renderers = useMemo(() => (asPreview ? debouncedRenderers : normalRenderers), [asPreview]);
|
||||||
|
const dpr = useDevicePixelRatio({ round: false });
|
||||||
|
|
||||||
const onLayerPosChanged = useCallback(
|
const onLayerPosChanged = useCallback(
|
||||||
(layerId: string, x: number, y: number) => {
|
(layerId: string, x: number, y: number) => {
|
||||||
@ -69,37 +73,29 @@ const useStageRenderer = (
|
|||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onBboxMouseDown = useCallback(
|
|
||||||
(layerId: string) => {
|
|
||||||
dispatch(layerSelected(layerId));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Initializing stage');
|
log.trace('Initializing stage');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stage = stageRef.current.container(container);
|
stage.container(container);
|
||||||
return () => {
|
return () => {
|
||||||
log.trace('Cleaning up stage');
|
log.trace('Cleaning up stage');
|
||||||
stage.destroy();
|
stage.destroy();
|
||||||
};
|
};
|
||||||
}, [container, stageRef]);
|
}, [container, stage]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Adding stage listeners');
|
log.trace('Adding stage listeners');
|
||||||
if (asPreview) {
|
if (asPreview) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stageRef.current.on('mousedown', onMouseDown);
|
stage.on('mousedown', onMouseDown);
|
||||||
stageRef.current.on('mouseup', onMouseUp);
|
stage.on('mouseup', onMouseUp);
|
||||||
stageRef.current.on('mousemove', onMouseMove);
|
stage.on('mousemove', onMouseMove);
|
||||||
stageRef.current.on('mouseenter', onMouseEnter);
|
stage.on('mouseenter', onMouseEnter);
|
||||||
stageRef.current.on('mouseleave', onMouseLeave);
|
stage.on('mouseleave', onMouseLeave);
|
||||||
stageRef.current.on('wheel', onMouseWheel);
|
stage.on('wheel', onMouseWheel);
|
||||||
const stage = stageRef.current;
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
log.trace('Cleaning up stage listeners');
|
log.trace('Cleaning up stage listeners');
|
||||||
@ -110,7 +106,7 @@ const useStageRenderer = (
|
|||||||
stage.off('mouseleave', onMouseLeave);
|
stage.off('mouseleave', onMouseLeave);
|
||||||
stage.off('wheel', onMouseWheel);
|
stage.off('wheel', onMouseWheel);
|
||||||
};
|
};
|
||||||
}, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]);
|
}, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave, onMouseWheel]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Updating stage dimensions');
|
log.trace('Updating stage dimensions');
|
||||||
@ -118,14 +114,12 @@ const useStageRenderer = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stage = stageRef.current;
|
|
||||||
|
|
||||||
const fitStageToContainer = () => {
|
const fitStageToContainer = () => {
|
||||||
const newXScale = wrapper.offsetWidth / width;
|
const newXScale = wrapper.offsetWidth / state.size.width;
|
||||||
const newYScale = wrapper.offsetHeight / height;
|
const newYScale = wrapper.offsetHeight / state.size.height;
|
||||||
const newScale = Math.min(newXScale, newYScale, 1);
|
const newScale = Math.min(newXScale, newYScale, 1);
|
||||||
stage.width(width * newScale);
|
stage.width(state.size.width * newScale);
|
||||||
stage.height(height * newScale);
|
stage.height(state.size.height * newScale);
|
||||||
stage.scaleX(newScale);
|
stage.scaleX(newScale);
|
||||||
stage.scaleY(newScale);
|
stage.scaleY(newScale);
|
||||||
};
|
};
|
||||||
@ -137,7 +131,7 @@ const useStageRenderer = (
|
|||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [stageRef, width, height, wrapper]);
|
}, [stage, state.size.width, state.size.height, wrapper]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering tool preview');
|
log.trace('Rendering tool preview');
|
||||||
@ -146,9 +140,10 @@ const useStageRenderer = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderers.renderToolPreview(
|
renderers.renderToolPreview(
|
||||||
stageRef.current,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
selectedLayerIdColor,
|
selectedLayerIdColor,
|
||||||
|
selectedLayerType,
|
||||||
state.globalMaskLayerOpacity,
|
state.globalMaskLayerOpacity,
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
lastMouseDownPos,
|
lastMouseDownPos,
|
||||||
@ -157,9 +152,10 @@ const useStageRenderer = (
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
asPreview,
|
asPreview,
|
||||||
stageRef,
|
stage,
|
||||||
tool,
|
tool,
|
||||||
selectedLayerIdColor,
|
selectedLayerIdColor,
|
||||||
|
selectedLayerType,
|
||||||
state.globalMaskLayerOpacity,
|
state.globalMaskLayerOpacity,
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
lastMouseDownPos,
|
lastMouseDownPos,
|
||||||
@ -170,8 +166,17 @@ const useStageRenderer = (
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering layers');
|
log.trace('Rendering layers');
|
||||||
renderers.renderLayers(stageRef.current, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged);
|
renderers.renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
}, [stageRef, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderers]);
|
}, [
|
||||||
|
stage,
|
||||||
|
state.layers,
|
||||||
|
state.globalMaskLayerOpacity,
|
||||||
|
tool,
|
||||||
|
onLayerPosChanged,
|
||||||
|
renderers,
|
||||||
|
state.size.width,
|
||||||
|
state.size.height,
|
||||||
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering bbox');
|
log.trace('Rendering bbox');
|
||||||
@ -179,8 +184,8 @@ const useStageRenderer = (
|
|||||||
// Preview should not display bboxes
|
// Preview should not display bboxes
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderers.renderBbox(stageRef.current, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown);
|
renderers.renderBbox(stage, state.layers, tool, onBboxChanged);
|
||||||
}, [stageRef, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown, renderers]);
|
}, [stage, asPreview, state.layers, tool, onBboxChanged, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Rendering background');
|
log.trace('Rendering background');
|
||||||
@ -188,13 +193,26 @@ const useStageRenderer = (
|
|||||||
// The preview should not have a background
|
// The preview should not have a background
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderers.renderBackground(stageRef.current, width, height);
|
renderers.renderBackground(stage, state.size.width, state.size.height);
|
||||||
}, [stageRef, asPreview, width, height, renderers]);
|
}, [stage, asPreview, state.size.width, state.size.height, renderers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
log.trace('Arranging layers');
|
log.trace('Arranging layers');
|
||||||
renderers.arrangeLayers(stageRef.current, layerIds);
|
renderers.arrangeLayers(stage, layerIds);
|
||||||
}, [stageRef, layerIds, renderers]);
|
}, [stage, layerIds, renderers]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
log.trace('Rendering no layers message');
|
||||||
|
if (asPreview) {
|
||||||
|
// The preview should not display the no layers message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderers.renderNoLayersMessage(stage, layerCount, state.size.width, state.size.height);
|
||||||
|
}, [stage, layerCount, renderers, asPreview, state.size.width, state.size.height]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
Konva.pixelRatio = dpr;
|
||||||
|
}, [dpr]);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -202,10 +220,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||||
const stageRef = useRef<Konva.Stage>(
|
const [stage] = useState(
|
||||||
new Konva.Stage({
|
() => new Konva.Stage({ id: uuidv4(), container: document.createElement('div'), listening: !asPreview })
|
||||||
container: document.createElement('div'), // We will overwrite this shortly...
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
||||||
@ -218,12 +234,12 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
setWrapper(el);
|
setWrapper(el);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useStageRenderer(stageRef, container, wrapper, asPreview);
|
useStageRenderer(stage, container, wrapper, asPreview);
|
||||||
|
|
||||||
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" />
|
<Flex ref={containerRef} tabIndex={-1} bg="base.850" borderRadius="base" overflow="hidden" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
@ -1,21 +1,27 @@
|
|||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
$tool,
|
$tool,
|
||||||
layerAdded,
|
selectControlLayersSlice,
|
||||||
selectedLayerDeleted,
|
selectedLayerDeleted,
|
||||||
selectedLayerReset,
|
selectedLayerReset,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi';
|
import { PiArrowsOutCardinalBold, PiEraserBold, PiPaintBrushBold, PiRectangleBold } from 'react-icons/pi';
|
||||||
|
|
||||||
|
const selectIsDisabled = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const selectedLayer = controlLayers.present.layers.find((l) => l.id === controlLayers.present.selectedLayerId);
|
||||||
|
return selectedLayer?.type !== 'regional_guidance_layer';
|
||||||
|
});
|
||||||
|
|
||||||
export const ToolChooser: React.FC = () => {
|
export const ToolChooser: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isDisabled = useAppSelector((s) => s.regionalPrompts.present.layers.length === 0);
|
const isDisabled = useAppSelector(selectIsDisabled);
|
||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
|
|
||||||
const setToolToBrush = useCallback(() => {
|
const setToolToBrush = useCallback(() => {
|
||||||
@ -40,11 +46,6 @@ export const ToolChooser: React.FC = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
useHotkeys('shift+c', resetSelectedLayer);
|
useHotkeys('shift+c', resetSelectedLayer);
|
||||||
|
|
||||||
const addLayer = useCallback(() => {
|
|
||||||
dispatch(layerAdded('vector_mask_layer'));
|
|
||||||
}, [dispatch]);
|
|
||||||
useHotkeys('shift+a', addLayer);
|
|
||||||
|
|
||||||
const deleteSelectedLayer = useCallback(() => {
|
const deleteSelectedLayer = useCallback(() => {
|
||||||
dispatch(selectedLayerDeleted());
|
dispatch(selectedLayerDeleted());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -69,8 +70,8 @@ export const ToolChooser: React.FC = () => {
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={`${t('regionalPrompts.rectangle')} (U)`}
|
aria-label={`${t('controlLayers.rectangle')} (U)`}
|
||||||
tooltip={`${t('regionalPrompts.rectangle')} (U)`}
|
tooltip={`${t('controlLayers.rectangle')} (U)`}
|
||||||
icon={<PiRectangleBold />}
|
icon={<PiRectangleBold />}
|
||||||
variant={tool === 'rect' ? 'solid' : 'outline'}
|
variant={tool === 'rect' ? 'solid' : 'outline'}
|
||||||
onClick={setToolToRect}
|
onClick={setToolToRect}
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { redo, undo } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { redo, undo } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -11,13 +11,13 @@ export const UndoRedoButtonGroup = memo(() => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const mayUndo = useAppSelector((s) => s.regionalPrompts.past.length > 0);
|
const mayUndo = useAppSelector((s) => s.controlLayers.past.length > 0);
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
dispatch(undo());
|
dispatch(undo());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, handleUndo]);
|
useHotkeys(['meta+z', 'ctrl+z'], handleUndo, { enabled: mayUndo, preventDefault: true }, [mayUndo, handleUndo]);
|
||||||
|
|
||||||
const mayRedo = useAppSelector((s) => s.regionalPrompts.future.length > 0);
|
const mayRedo = useAppSelector((s) => s.controlLayers.future.length > 0);
|
||||||
const handleRedo = useCallback(() => {
|
const handleRedo = useCallback(() => {
|
||||||
dispatch(redo());
|
dispatch(redo());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
@ -0,0 +1,237 @@
|
|||||||
|
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||||
|
import { Box, Flex, Spinner, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
|
import { skipToken } from '@reduxjs/toolkit/query';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import IAIDndImage from 'common/components/IAIDndImage';
|
||||||
|
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
||||||
|
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
|
||||||
|
import { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage';
|
||||||
|
import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage';
|
||||||
|
import { useControlAdapterProcessorType } from 'features/controlAdapters/hooks/useControlAdapterProcessorType';
|
||||||
|
import {
|
||||||
|
controlAdapterImageChanged,
|
||||||
|
selectControlAdaptersSlice,
|
||||||
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||||
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
|
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiArrowCounterClockwiseBold, PiFloppyDiskBold, PiRulerBold } from 'react-icons/pi';
|
||||||
|
import {
|
||||||
|
useAddImageToBoardMutation,
|
||||||
|
useChangeImageIsIntermediateMutation,
|
||||||
|
useGetImageDTOQuery,
|
||||||
|
useRemoveImageFromBoardMutation,
|
||||||
|
} from 'services/api/endpoints/images';
|
||||||
|
import type { PostUploadAction } from 'services/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
isSmall?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPendingControlImages = createMemoizedSelector(
|
||||||
|
selectControlAdaptersSlice,
|
||||||
|
(controlAdapters) => controlAdapters.pendingControlImages
|
||||||
|
);
|
||||||
|
|
||||||
|
const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const controlImageName = useControlAdapterControlImage(id);
|
||||||
|
const processedControlImageName = useControlAdapterProcessedControlImage(id);
|
||||||
|
const processorType = useControlAdapterProcessorType(id);
|
||||||
|
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||||
|
const isConnected = useAppSelector((s) => s.system.isConnected);
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||||
|
const pendingControlImages = useAppSelector(selectPendingControlImages);
|
||||||
|
const shift = useShiftModifier();
|
||||||
|
|
||||||
|
const [isMouseOverImage, setIsMouseOverImage] = useState(false);
|
||||||
|
|
||||||
|
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||||
|
controlImageName ?? skipToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const { currentData: processedControlImage, isError: isErrorProcessedControlImage } = useGetImageDTOQuery(
|
||||||
|
processedControlImageName ?? skipToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const [changeIsIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
|
const [addToBoard] = useAddImageToBoardMutation();
|
||||||
|
const [removeFromBoard] = useRemoveImageFromBoardMutation();
|
||||||
|
const handleResetControlImage = useCallback(() => {
|
||||||
|
dispatch(controlAdapterImageChanged({ id, controlImage: null }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleSaveControlImage = useCallback(async () => {
|
||||||
|
if (!processedControlImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await changeIsIntermediate({
|
||||||
|
imageDTO: processedControlImage,
|
||||||
|
is_intermediate: false,
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
if (autoAddBoardId !== 'none') {
|
||||||
|
addToBoard({
|
||||||
|
imageDTO: processedControlImage,
|
||||||
|
board_id: autoAddBoardId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
removeFromBoard({ imageDTO: processedControlImage });
|
||||||
|
}
|
||||||
|
}, [processedControlImage, changeIsIntermediate, autoAddBoardId, addToBoard, removeFromBoard]);
|
||||||
|
|
||||||
|
const handleSetControlImageToDimensions = useCallback(() => {
|
||||||
|
if (!controlImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTabName === 'unifiedCanvas') {
|
||||||
|
dispatch(setBoundingBoxDimensions({ width: controlImage.width, height: controlImage.height }, optimalDimension));
|
||||||
|
} else {
|
||||||
|
if (shift) {
|
||||||
|
const { width, height } = controlImage;
|
||||||
|
dispatch(widthChanged({ width, updateAspectRatio: true }));
|
||||||
|
dispatch(heightChanged({ height, updateAspectRatio: true }));
|
||||||
|
} else {
|
||||||
|
const { width, height } = calculateNewSize(
|
||||||
|
controlImage.width / controlImage.height,
|
||||||
|
optimalDimension * optimalDimension
|
||||||
|
);
|
||||||
|
dispatch(widthChanged({ width, updateAspectRatio: true }));
|
||||||
|
dispatch(heightChanged({ height, updateAspectRatio: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [controlImage, activeTabName, dispatch, optimalDimension, shift]);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
setIsMouseOverImage(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setIsMouseOverImage(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
|
||||||
|
if (controlImage) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
payloadType: 'IMAGE_DTO',
|
||||||
|
payload: { imageDTO: controlImage },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [controlImage, id]);
|
||||||
|
|
||||||
|
const droppableData = useMemo<TypesafeDroppableData | undefined>(
|
||||||
|
() => ({
|
||||||
|
id,
|
||||||
|
actionType: 'SET_CONTROL_ADAPTER_IMAGE',
|
||||||
|
context: { id },
|
||||||
|
}),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const postUploadAction = useMemo<PostUploadAction>(() => ({ type: 'SET_CONTROL_ADAPTER_IMAGE', id }), [id]);
|
||||||
|
|
||||||
|
const shouldShowProcessedImage =
|
||||||
|
controlImage &&
|
||||||
|
processedControlImage &&
|
||||||
|
!isMouseOverImage &&
|
||||||
|
!pendingControlImages.includes(id) &&
|
||||||
|
processorType !== 'none';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConnected && (isErrorControlImage || isErrorProcessedControlImage)) {
|
||||||
|
handleResetControlImage();
|
||||||
|
}
|
||||||
|
}, [handleResetControlImage, isConnected, isErrorControlImage, isErrorProcessedControlImage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
position="relative"
|
||||||
|
w="full"
|
||||||
|
h={isSmall ? 36 : 366} // magic no touch
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<IAIDndImage
|
||||||
|
draggableData={draggableData}
|
||||||
|
droppableData={droppableData}
|
||||||
|
imageDTO={controlImage}
|
||||||
|
isDropDisabled={shouldShowProcessedImage}
|
||||||
|
postUploadAction={postUploadAction}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
opacity={shouldShowProcessedImage ? 1 : 0}
|
||||||
|
transitionProperty="common"
|
||||||
|
transitionDuration="normal"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
<IAIDndImage
|
||||||
|
draggableData={draggableData}
|
||||||
|
droppableData={droppableData}
|
||||||
|
imageDTO={processedControlImage}
|
||||||
|
isUploadDisabled={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<IAIDndImageIcon
|
||||||
|
onClick={handleResetControlImage}
|
||||||
|
icon={controlImage ? <PiArrowCounterClockwiseBold size={16} /> : undefined}
|
||||||
|
tooltip={t('controlnet.resetControlImage')}
|
||||||
|
/>
|
||||||
|
<IAIDndImageIcon
|
||||||
|
onClick={handleSaveControlImage}
|
||||||
|
icon={controlImage ? <PiFloppyDiskBold size={16} /> : undefined}
|
||||||
|
tooltip={t('controlnet.saveControlImage')}
|
||||||
|
styleOverrides={saveControlImageStyleOverrides}
|
||||||
|
/>
|
||||||
|
<IAIDndImageIcon
|
||||||
|
onClick={handleSetControlImageToDimensions}
|
||||||
|
icon={controlImage ? <PiRulerBold size={16} /> : undefined}
|
||||||
|
tooltip={shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')}
|
||||||
|
styleOverrides={setControlImageDimensionsStyleOverrides}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{pendingControlImages.includes(id) && (
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
insetInlineStart={0}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
opacity={0.8}
|
||||||
|
borderRadius="base"
|
||||||
|
bg="base.900"
|
||||||
|
>
|
||||||
|
<Spinner size="xl" color="base.400" />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ControlAdapterImagePreview);
|
||||||
|
|
||||||
|
const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
|
||||||
|
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };
|
@ -0,0 +1,72 @@
|
|||||||
|
import { Box, Flex, Icon, IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import ControlAdapterProcessorComponent from 'features/controlAdapters/components/ControlAdapterProcessorComponent';
|
||||||
|
import ControlAdapterShouldAutoConfig from 'features/controlAdapters/components/ControlAdapterShouldAutoConfig';
|
||||||
|
import ParamControlAdapterIPMethod from 'features/controlAdapters/components/parameters/ParamControlAdapterIPMethod';
|
||||||
|
import ParamControlAdapterProcessorSelect from 'features/controlAdapters/components/parameters/ParamControlAdapterProcessorSelect';
|
||||||
|
import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiCaretUpBold } from 'react-icons/pi';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
|
import ControlAdapterImagePreview from './ControlAdapterImagePreview';
|
||||||
|
import { ParamControlAdapterBeginEnd } from './ParamControlAdapterBeginEnd';
|
||||||
|
import ParamControlAdapterControlMode from './ParamControlAdapterControlMode';
|
||||||
|
import ParamControlAdapterModel from './ParamControlAdapterModel';
|
||||||
|
import ParamControlAdapterWeight from './ParamControlAdapterWeight';
|
||||||
|
|
||||||
|
const ControlAdapterLayerConfig = (props: { id: string }) => {
|
||||||
|
const { id } = props;
|
||||||
|
const controlAdapterType = useControlAdapterType(id);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" gap={4} position="relative" w="full">
|
||||||
|
<Flex gap={3} alignItems="center" w="full">
|
||||||
|
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||||
|
<ParamControlAdapterModel id={id} />{' '}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{controlAdapterType !== 'ip_adapter' && (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
tooltip={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||||
|
aria-label={isExpanded ? t('controlnet.hideAdvanced') : t('controlnet.showAdvanced')}
|
||||||
|
onClick={toggleIsExpanded}
|
||||||
|
variant="ghost"
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
boxSize={4}
|
||||||
|
as={PiCaretUpBold}
|
||||||
|
transform={isExpanded ? 'rotate(0deg)' : 'rotate(180deg)'}
|
||||||
|
transitionProperty="common"
|
||||||
|
transitionDuration="normal"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={4} w="full" alignItems="center">
|
||||||
|
<Flex flexDir="column" gap={3} w="full">
|
||||||
|
{controlAdapterType === 'ip_adapter' && <ParamControlAdapterIPMethod id={id} />}
|
||||||
|
{controlAdapterType === 'controlnet' && <ParamControlAdapterControlMode id={id} />}
|
||||||
|
<ParamControlAdapterWeight id={id} />
|
||||||
|
<ParamControlAdapterBeginEnd id={id} />
|
||||||
|
</Flex>
|
||||||
|
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
|
||||||
|
<ControlAdapterImagePreview id={id} isSmall />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<ControlAdapterShouldAutoConfig id={id} />
|
||||||
|
<ParamControlAdapterProcessorSelect id={id} />
|
||||||
|
<ControlAdapterProcessorComponent id={id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ControlAdapterLayerConfig);
|
@ -0,0 +1,89 @@
|
|||||||
|
import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
|
import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct';
|
||||||
|
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
|
||||||
|
import {
|
||||||
|
controlAdapterBeginStepPctChanged,
|
||||||
|
controlAdapterEndStepPctChanged,
|
||||||
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
||||||
|
|
||||||
|
export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => {
|
||||||
|
const isEnabled = useControlAdapterIsEnabled(id);
|
||||||
|
const stepPcts = useControlAdapterBeginEndStepPct(id);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(v: [number, number]) => {
|
||||||
|
dispatch(
|
||||||
|
controlAdapterBeginStepPctChanged({
|
||||||
|
id,
|
||||||
|
beginStepPct: v[0],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
controlAdapterEndStepPctChanged({
|
||||||
|
id,
|
||||||
|
endStepPct: v[1],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onReset = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
controlAdapterBeginStepPctChanged({
|
||||||
|
id,
|
||||||
|
beginStepPct: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
controlAdapterEndStepPctChanged({
|
||||||
|
id,
|
||||||
|
endStepPct: 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const value = useMemo<[number, number]>(() => [stepPcts?.beginStepPct ?? 0, stepPcts?.endStepPct ?? 1], [stepPcts]);
|
||||||
|
|
||||||
|
if (!stepPcts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isDisabled={!isEnabled} orientation="horizontal">
|
||||||
|
<InformationalPopover feature="controlNetBeginEnd">
|
||||||
|
<FormLabel m={0}>{t('controlnet.beginEndStepPercentShort')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<CompositeRangeSlider
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onReset={onReset}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.05}
|
||||||
|
fineStep={0.01}
|
||||||
|
minStepsBetweenThumbs={1}
|
||||||
|
formatValue={formatPct}
|
||||||
|
marks
|
||||||
|
withThumbTooltip
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ParamControlAdapterBeginEnd.displayName = 'ParamControlAdapterBeginEnd';
|
||||||
|
|
||||||
|
const ariaLabel = ['Begin Step %', 'End Step %'];
|
@ -0,0 +1,66 @@
|
|||||||
|
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||||
|
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
|
import { useControlAdapterControlMode } from 'features/controlAdapters/hooks/useControlAdapterControlMode';
|
||||||
|
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
|
||||||
|
import { controlAdapterControlModeChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import type { ControlMode } from 'features/controlAdapters/store/types';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParamControlAdapterControlMode = ({ id }: Props) => {
|
||||||
|
const isEnabled = useControlAdapterIsEnabled(id);
|
||||||
|
const controlMode = useControlAdapterControlMode(id);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const CONTROL_MODE_DATA = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: t('controlnet.balanced'), value: 'balanced' },
|
||||||
|
{ label: t('controlnet.prompt'), value: 'more_prompt' },
|
||||||
|
{ label: t('controlnet.control'), value: 'more_control' },
|
||||||
|
{ label: t('controlnet.megaControl'), value: 'unbalanced' },
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleControlModeChange = useCallback<ComboboxOnChange>(
|
||||||
|
(v) => {
|
||||||
|
if (!v) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
controlAdapterControlModeChanged({
|
||||||
|
id,
|
||||||
|
controlMode: v.value as ControlMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[id, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => CONTROL_MODE_DATA.filter((o) => o.value === controlMode)[0],
|
||||||
|
[CONTROL_MODE_DATA, controlMode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!controlMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isDisabled={!isEnabled}>
|
||||||
|
<InformationalPopover feature="controlNetControlMode">
|
||||||
|
<FormLabel m={0}>{t('controlnet.control')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Combobox value={value} options={CONTROL_MODE_DATA} onChange={handleControlModeChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParamControlAdapterControlMode);
|
@ -0,0 +1,136 @@
|
|||||||
|
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||||
|
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||||
|
import { useControlAdapterCLIPVisionModel } from 'features/controlAdapters/hooks/useControlAdapterCLIPVisionModel';
|
||||||
|
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
|
||||||
|
import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel';
|
||||||
|
import { useControlAdapterModels } from 'features/controlAdapters/hooks/useControlAdapterModels';
|
||||||
|
import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType';
|
||||||
|
import {
|
||||||
|
controlAdapterCLIPVisionModelChanged,
|
||||||
|
controlAdapterModelChanged,
|
||||||
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import type { CLIPVisionModel } from 'features/controlAdapters/store/types';
|
||||||
|
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
|
||||||
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type {
|
||||||
|
AnyModelConfig,
|
||||||
|
ControlNetModelConfig,
|
||||||
|
IPAdapterModelConfig,
|
||||||
|
T2IAdapterModelConfig,
|
||||||
|
} from 'services/api/types';
|
||||||
|
|
||||||
|
type ParamControlAdapterModelProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model);
|
||||||
|
|
||||||
|
const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => {
|
||||||
|
const isEnabled = useControlAdapterIsEnabled(id);
|
||||||
|
const controlAdapterType = useControlAdapterType(id);
|
||||||
|
const { modelConfig } = useControlAdapterModel(id);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const currentBaseModel = useAppSelector((s) => s.generation.model?.base);
|
||||||
|
const currentCLIPVisionModel = useControlAdapterCLIPVisionModel(id);
|
||||||
|
const mainModel = useAppSelector(selectMainModel);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [modelConfigs, { isLoading }] = useControlAdapterModels(controlAdapterType);
|
||||||
|
|
||||||
|
const _onChange = useCallback(
|
||||||
|
(modelConfig: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => {
|
||||||
|
if (!modelConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
controlAdapterModelChanged({
|
||||||
|
id,
|
||||||
|
modelConfig,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCLIPVisionModelChange = useCallback<ComboboxOnChange>(
|
||||||
|
(v) => {
|
||||||
|
if (!v?.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(controlAdapterCLIPVisionModelChanged({ id, clipVisionModel: v.value as CLIPVisionModel }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedModel = useMemo(
|
||||||
|
() => (modelConfig && controlAdapterType ? { ...modelConfig, model_type: controlAdapterType } : null),
|
||||||
|
[controlAdapterType, modelConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getIsDisabled = useCallback(
|
||||||
|
(model: AnyModelConfig): boolean => {
|
||||||
|
const isCompatible = currentBaseModel === model.base;
|
||||||
|
const hasMainModel = Boolean(currentBaseModel);
|
||||||
|
return !hasMainModel || !isCompatible;
|
||||||
|
},
|
||||||
|
[currentBaseModel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({
|
||||||
|
modelConfigs,
|
||||||
|
onChange: _onChange,
|
||||||
|
selectedModel,
|
||||||
|
getIsDisabled,
|
||||||
|
isLoading,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clipVisionOptions = useMemo<ComboboxOption[]>(
|
||||||
|
() => [
|
||||||
|
{ label: 'ViT-H', value: 'ViT-H' },
|
||||||
|
{ label: 'ViT-G', value: 'ViT-G' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clipVisionModel = useMemo(
|
||||||
|
() => clipVisionOptions.find((o) => o.value === currentCLIPVisionModel),
|
||||||
|
[clipVisionOptions, currentCLIPVisionModel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap={4}>
|
||||||
|
<Tooltip label={selectedModel?.description}>
|
||||||
|
<FormControl isDisabled={!isEnabled} isInvalid={!value || mainModel?.base !== modelConfig?.base} w="full">
|
||||||
|
<Combobox
|
||||||
|
options={options}
|
||||||
|
placeholder={t('controlnet.selectModel')}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
noOptionsMessage={noOptionsMessage}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Tooltip>
|
||||||
|
{modelConfig?.type === 'ip_adapter' && modelConfig.format === 'checkpoint' && (
|
||||||
|
<FormControl
|
||||||
|
isDisabled={!isEnabled}
|
||||||
|
isInvalid={!value || mainModel?.base !== modelConfig?.base}
|
||||||
|
width="max-content"
|
||||||
|
minWidth={28}
|
||||||
|
>
|
||||||
|
<Combobox
|
||||||
|
options={clipVisionOptions}
|
||||||
|
placeholder={t('controlnet.selectCLIPVisionModel')}
|
||||||
|
value={clipVisionModel}
|
||||||
|
onChange={onCLIPVisionModelChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParamControlAdapterModel);
|
@ -0,0 +1,74 @@
|
|||||||
|
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||||
|
import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled';
|
||||||
|
import { useControlAdapterWeight } from 'features/controlAdapters/hooks/useControlAdapterWeight';
|
||||||
|
import { controlAdapterWeightChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type ParamControlAdapterWeightProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (v: number) => v.toFixed(2);
|
||||||
|
|
||||||
|
const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isEnabled = useControlAdapterIsEnabled(id);
|
||||||
|
const weight = useControlAdapterWeight(id);
|
||||||
|
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
|
||||||
|
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
|
||||||
|
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
|
||||||
|
const numberInputMin = useAppSelector((s) => s.config.sd.ca.weight.numberInputMin);
|
||||||
|
const numberInputMax = useAppSelector((s) => s.config.sd.ca.weight.numberInputMax);
|
||||||
|
const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
|
||||||
|
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(weight: number) => {
|
||||||
|
dispatch(controlAdapterWeightChanged({ id, weight }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNil(weight)) {
|
||||||
|
// should never happen
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl isDisabled={!isEnabled} orientation="horizontal">
|
||||||
|
<InformationalPopover feature="controlNetWeight">
|
||||||
|
<FormLabel m={0}>{t('controlnet.weight')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<CompositeSlider
|
||||||
|
value={weight}
|
||||||
|
onChange={onChange}
|
||||||
|
defaultValue={initial}
|
||||||
|
min={sliderMin}
|
||||||
|
max={sliderMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
marks={marks}
|
||||||
|
formatValue={formatValue}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
value={weight}
|
||||||
|
onChange={onChange}
|
||||||
|
min={numberInputMin}
|
||||||
|
max={numberInputMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
maxW={20}
|
||||||
|
defaultValue={initial}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParamControlAdapterWeight);
|
||||||
|
|
||||||
|
const marks = [0, 1, 2];
|
@ -0,0 +1,81 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import {
|
||||||
|
isControlAdapterLayer,
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
selectControlLayersSlice,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
|
export const useLayerPositivePrompt = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
assert(layer.positivePrompt !== null, `Layer ${layerId} does not have a positive prompt`);
|
||||||
|
return layer.positivePrompt;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const prompt = useAppSelector(selectLayer);
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerNegativePrompt = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(isRegionalGuidanceLayer(layer), `Layer ${layerId} not found or not an RP layer`);
|
||||||
|
assert(layer.negativePrompt !== null, `Layer ${layerId} does not have a negative prompt`);
|
||||||
|
return layer.negativePrompt;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const prompt = useAppSelector(selectLayer);
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerIsVisible = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
return layer.isEnabled;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const isVisible = useAppSelector(selectLayer);
|
||||||
|
return isVisible;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerType = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
return layer.type;
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const type = useAppSelector(selectLayer);
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayerOpacity = (layerId: string) => {
|
||||||
|
const selectLayer = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
|
const layer = controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
||||||
|
assert(layer, `Layer ${layerId} not found`);
|
||||||
|
return { opacity: Math.round(layer.opacity * 100), isFilterEnabled: layer.isFilterEnabled };
|
||||||
|
}),
|
||||||
|
[layerId]
|
||||||
|
);
|
||||||
|
const opacity = useAppSelector(selectLayer);
|
||||||
|
return opacity;
|
||||||
|
};
|
@ -12,7 +12,7 @@ import {
|
|||||||
maskLayerLineAdded,
|
maskLayerLineAdded,
|
||||||
maskLayerPointsAdded,
|
maskLayerPointsAdded,
|
||||||
maskLayerRectAdded,
|
maskLayerRectAdded,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import type { Vector2d } from 'konva/lib/types';
|
import type { Vector2d } from 'konva/lib/types';
|
||||||
@ -48,11 +48,11 @@ const BRUSH_SPACING = 20;
|
|||||||
|
|
||||||
export const useMouseEvents = () => {
|
export const useMouseEvents = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedLayerId = useAppSelector((s) => s.regionalPrompts.present.selectedLayerId);
|
const selectedLayerId = useAppSelector((s) => s.controlLayers.present.selectedLayerId);
|
||||||
const tool = useStore($tool);
|
const tool = useStore($tool);
|
||||||
const lastCursorPosRef = useRef<[number, number] | null>(null);
|
const lastCursorPosRef = useRef<[number, number] | null>(null);
|
||||||
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
const shouldInvertBrushSizeScrollDirection = useAppSelector((s) => s.canvas.shouldInvertBrushSizeScrollDirection);
|
||||||
const brushSize = useAppSelector((s) => s.regionalPrompts.present.brushSize);
|
const brushSize = useAppSelector((s) => s.controlLayers.present.brushSize);
|
||||||
|
|
||||||
const onMouseDown = useCallback(
|
const onMouseDown = useCallback(
|
||||||
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
(e: KonvaEventObject<MouseEvent | TouchEvent>) => {
|
@ -1,15 +1,16 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { isRegionalGuidanceLayer, selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
|
const selectValidLayerCount = createSelector(selectControlLayersSlice, (controlLayers) => {
|
||||||
if (!regionalPrompts.present.isEnabled) {
|
if (!controlLayers.present.isEnabled) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const validLayers = regionalPrompts.present.layers
|
const validLayers = controlLayers.present.layers
|
||||||
.filter((l) => l.isVisible)
|
.filter(isRegionalGuidanceLayer)
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||||
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
const hasAtLeastOneImagePrompt = l.ipAdapterIds.length > 0;
|
||||||
@ -19,12 +20,12 @@ const selectValidLayerCount = createSelector(selectRegionalPromptsSlice, (region
|
|||||||
return validLayers.length;
|
return validLayers.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useRegionalControlTitle = () => {
|
export const useControlLayersTitle = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const validLayerCount = useAppSelector(selectValidLayerCount);
|
const validLayerCount = useAppSelector(selectValidLayerCount);
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : '';
|
const suffix = validLayerCount > 0 ? ` (${validLayerCount})` : '';
|
||||||
return `${t('regionalPrompts.regionalControl')}${suffix}`;
|
return `${t('controlLayers.controlLayers')}${suffix}`;
|
||||||
}, [t, validLayerCount]);
|
}, [t, validLayerCount]);
|
||||||
return title;
|
return title;
|
||||||
};
|
};
|
@ -0,0 +1,676 @@
|
|||||||
|
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
|
||||||
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
|
import {
|
||||||
|
controlAdapterImageChanged,
|
||||||
|
controlAdapterProcessedImageChanged,
|
||||||
|
isAnyControlAdapterAdded,
|
||||||
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||||
|
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
|
import { modelChanged } from 'features/parameters/store/generationSlice';
|
||||||
|
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
|
||||||
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
|
import { isEqual, partition } from 'lodash-es';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
import type { RgbColor } from 'react-colorful';
|
||||||
|
import type { UndoableOptions } from 'redux-undo';
|
||||||
|
import { assert } from 'tsafe';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ControlAdapterLayer,
|
||||||
|
ControlLayersState,
|
||||||
|
DrawingTool,
|
||||||
|
IPAdapterLayer,
|
||||||
|
Layer,
|
||||||
|
RegionalGuidanceLayer,
|
||||||
|
Tool,
|
||||||
|
VectorMaskLine,
|
||||||
|
VectorMaskRect,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const initialControlLayersState: ControlLayersState = {
|
||||||
|
_version: 1,
|
||||||
|
selectedLayerId: null,
|
||||||
|
brushSize: 100,
|
||||||
|
layers: [],
|
||||||
|
globalMaskLayerOpacity: 0.3, // this globally changes all mask layers' opacity
|
||||||
|
isEnabled: true,
|
||||||
|
positivePrompt: '',
|
||||||
|
negativePrompt: '',
|
||||||
|
positivePrompt2: '',
|
||||||
|
negativePrompt2: '',
|
||||||
|
shouldConcatPrompts: true,
|
||||||
|
initialImage: null,
|
||||||
|
size: {
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
aspectRatio: deepClone(initialAspectRatioState),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLine = (obj: VectorMaskLine | VectorMaskRect): obj is VectorMaskLine => obj.type === 'vector_mask_line';
|
||||||
|
export const isRegionalGuidanceLayer = (layer?: Layer): layer is RegionalGuidanceLayer =>
|
||||||
|
layer?.type === 'regional_guidance_layer';
|
||||||
|
export const isControlAdapterLayer = (layer?: Layer): layer is ControlAdapterLayer =>
|
||||||
|
layer?.type === 'control_adapter_layer';
|
||||||
|
export const isIPAdapterLayer = (layer?: Layer): layer is IPAdapterLayer => layer?.type === 'ip_adapter_layer';
|
||||||
|
export const isRenderableLayer = (layer?: Layer): layer is RegionalGuidanceLayer | ControlAdapterLayer =>
|
||||||
|
layer?.type === 'regional_guidance_layer' || layer?.type === 'control_adapter_layer';
|
||||||
|
const resetLayer = (layer: Layer) => {
|
||||||
|
if (layer.type === 'regional_guidance_layer') {
|
||||||
|
layer.maskObjects = [];
|
||||||
|
layer.bbox = null;
|
||||||
|
layer.isEnabled = true;
|
||||||
|
layer.needsPixelBbox = false;
|
||||||
|
layer.bboxNeedsUpdate = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer.type === 'control_adapter_layer') {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getVectorMaskPreviewColor = (state: ControlLayersState): RgbColor => {
|
||||||
|
const vmLayers = state.layers.filter(isRegionalGuidanceLayer);
|
||||||
|
const lastColor = vmLayers[vmLayers.length - 1]?.previewColor;
|
||||||
|
return LayerColors.next(lastColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const controlLayersSlice = createSlice({
|
||||||
|
name: 'controlLayers',
|
||||||
|
initialState: initialControlLayersState,
|
||||||
|
reducers: {
|
||||||
|
//#region All Layers
|
||||||
|
regionalGuidanceLayerAdded: (state, action: PayloadAction<{ layerId: string }>) => {
|
||||||
|
const { layerId } = action.payload;
|
||||||
|
const layer: RegionalGuidanceLayer = {
|
||||||
|
id: getRegionalGuidanceLayerId(layerId),
|
||||||
|
type: 'regional_guidance_layer',
|
||||||
|
isEnabled: true,
|
||||||
|
bbox: null,
|
||||||
|
bboxNeedsUpdate: false,
|
||||||
|
maskObjects: [],
|
||||||
|
previewColor: getVectorMaskPreviewColor(state),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
autoNegative: 'invert',
|
||||||
|
needsPixelBbox: false,
|
||||||
|
positivePrompt: '',
|
||||||
|
negativePrompt: null,
|
||||||
|
ipAdapterIds: [],
|
||||||
|
isSelected: true,
|
||||||
|
};
|
||||||
|
state.layers.push(layer);
|
||||||
|
state.selectedLayerId = layer.id;
|
||||||
|
for (const layer of state.layers.filter(isRenderableLayer)) {
|
||||||
|
if (layer.id !== layerId) {
|
||||||
|
layer.isSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
ipAdapterLayerAdded: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
|
||||||
|
const { layerId, ipAdapterId } = action.payload;
|
||||||
|
const layer: IPAdapterLayer = {
|
||||||
|
id: getIPAdapterLayerId(layerId),
|
||||||
|
type: 'ip_adapter_layer',
|
||||||
|
isEnabled: true,
|
||||||
|
ipAdapterId,
|
||||||
|
};
|
||||||
|
state.layers.push(layer);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
controlAdapterLayerAdded: (state, action: PayloadAction<{ layerId: string; controlNetId: string }>) => {
|
||||||
|
const { layerId, controlNetId } = action.payload;
|
||||||
|
const layer: ControlAdapterLayer = {
|
||||||
|
id: getControlNetLayerId(layerId),
|
||||||
|
type: 'control_adapter_layer',
|
||||||
|
controlNetId,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
bbox: null,
|
||||||
|
bboxNeedsUpdate: false,
|
||||||
|
isEnabled: true,
|
||||||
|
imageName: null,
|
||||||
|
opacity: 1,
|
||||||
|
isSelected: true,
|
||||||
|
isFilterEnabled: true,
|
||||||
|
};
|
||||||
|
state.layers.push(layer);
|
||||||
|
state.selectedLayerId = layer.id;
|
||||||
|
for (const layer of state.layers.filter(isRenderableLayer)) {
|
||||||
|
if (layer.id !== layerId) {
|
||||||
|
layer.isSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
layerSelected: (state, action: PayloadAction<string>) => {
|
||||||
|
for (const layer of state.layers.filter(isRenderableLayer)) {
|
||||||
|
if (layer.id === action.payload) {
|
||||||
|
layer.isSelected = true;
|
||||||
|
state.selectedLayerId = action.payload;
|
||||||
|
} else {
|
||||||
|
layer.isSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layerVisibilityToggled: (state, action: PayloadAction<string>) => {
|
||||||
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
|
if (layer) {
|
||||||
|
layer.isEnabled = !layer.isEnabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => {
|
||||||
|
const { layerId, x, y } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (isRenderableLayer(layer)) {
|
||||||
|
layer.x = x;
|
||||||
|
layer.y = y;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layerBboxChanged: (state, action: PayloadAction<{ layerId: string; bbox: IRect | null }>) => {
|
||||||
|
const { layerId, bbox } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (isRenderableLayer(layer)) {
|
||||||
|
layer.bbox = bbox;
|
||||||
|
layer.bboxNeedsUpdate = false;
|
||||||
|
if (bbox === null && layer.type === 'regional_guidance_layer') {
|
||||||
|
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
||||||
|
layer.maskObjects = [];
|
||||||
|
layer.needsPixelBbox = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layerReset: (state, action: PayloadAction<string>) => {
|
||||||
|
const layer = state.layers.find((l) => l.id === action.payload);
|
||||||
|
if (layer) {
|
||||||
|
resetLayer(layer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layerDeleted: (state, action: PayloadAction<string>) => {
|
||||||
|
state.layers = state.layers.filter((l) => l.id !== action.payload);
|
||||||
|
state.selectedLayerId = state.layers[0]?.id ?? null;
|
||||||
|
},
|
||||||
|
layerMovedForward: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
||||||
|
moveForward(renderableLayers, cb);
|
||||||
|
state.layers = [...ipAdapterLayers, ...renderableLayers];
|
||||||
|
},
|
||||||
|
layerMovedToFront: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
||||||
|
// Because the layers are in reverse order, moving to the front is equivalent to moving to the back
|
||||||
|
moveToBack(renderableLayers, cb);
|
||||||
|
state.layers = [...ipAdapterLayers, ...renderableLayers];
|
||||||
|
},
|
||||||
|
layerMovedBackward: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
||||||
|
moveBackward(renderableLayers, cb);
|
||||||
|
state.layers = [...ipAdapterLayers, ...renderableLayers];
|
||||||
|
},
|
||||||
|
layerMovedToBack: (state, action: PayloadAction<string>) => {
|
||||||
|
const cb = (l: Layer) => l.id === action.payload;
|
||||||
|
const [renderableLayers, ipAdapterLayers] = partition(state.layers, isRenderableLayer);
|
||||||
|
// Because the layers are in reverse order, moving to the back is equivalent to moving to the front
|
||||||
|
moveToFront(renderableLayers, cb);
|
||||||
|
state.layers = [...ipAdapterLayers, ...renderableLayers];
|
||||||
|
},
|
||||||
|
selectedLayerReset: (state) => {
|
||||||
|
const layer = state.layers.find((l) => l.id === state.selectedLayerId);
|
||||||
|
if (layer) {
|
||||||
|
resetLayer(layer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedLayerDeleted: (state) => {
|
||||||
|
state.layers = state.layers.filter((l) => l.id !== state.selectedLayerId);
|
||||||
|
state.selectedLayerId = state.layers[0]?.id ?? null;
|
||||||
|
},
|
||||||
|
layerOpacityChanged: (state, action: PayloadAction<{ layerId: string; opacity: number }>) => {
|
||||||
|
const { layerId, opacity } = action.payload;
|
||||||
|
const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
||||||
|
if (layer) {
|
||||||
|
layer.opacity = opacity;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region CA Layers
|
||||||
|
isFilterEnabledChanged: (state, action: PayloadAction<{ layerId: string; isFilterEnabled: boolean }>) => {
|
||||||
|
const { layerId, isFilterEnabled } = action.payload;
|
||||||
|
const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
||||||
|
if (layer) {
|
||||||
|
layer.isFilterEnabled = isFilterEnabled;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Mask Layers
|
||||||
|
maskLayerPositivePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
|
const { layerId, prompt } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.positivePrompt = prompt;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerNegativePromptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string | null }>) => {
|
||||||
|
const { layerId, prompt } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.negativePrompt = prompt;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerIPAdapterAdded: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
|
||||||
|
const { layerId, ipAdapterId } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.ipAdapterIds.push(ipAdapterId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerIPAdapterDeleted: (state, action: PayloadAction<{ layerId: string; ipAdapterId: string }>) => {
|
||||||
|
const { layerId, ipAdapterId } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.ipAdapterIds = layer.ipAdapterIds.filter((id) => id !== ipAdapterId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerPreviewColorChanged: (state, action: PayloadAction<{ layerId: string; color: RgbColor }>) => {
|
||||||
|
const { layerId, color } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.previewColor = color;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerLineAdded: {
|
||||||
|
reducer: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<
|
||||||
|
{ layerId: string; points: [number, number, number, number]; tool: DrawingTool },
|
||||||
|
string,
|
||||||
|
{ uuid: string }
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const { layerId, points, tool } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
const lineId = getRegionalGuidanceLayerLineId(layer.id, action.meta.uuid);
|
||||||
|
layer.maskObjects.push({
|
||||||
|
type: 'vector_mask_line',
|
||||||
|
tool: tool,
|
||||||
|
id: lineId,
|
||||||
|
// Points must be offset by the layer's x and y coordinates
|
||||||
|
// TODO: Handle this in the event listener?
|
||||||
|
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||||
|
strokeWidth: state.brushSize,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
if (!layer.needsPixelBbox && tool === 'eraser') {
|
||||||
|
layer.needsPixelBbox = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
||||||
|
payload,
|
||||||
|
meta: { uuid: uuidv4() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
maskLayerPointsAdded: (state, action: PayloadAction<{ layerId: string; point: [number, number] }>) => {
|
||||||
|
const { layerId, point } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
const lastLine = layer.maskObjects.findLast(isLine);
|
||||||
|
if (!lastLine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Points must be offset by the layer's x and y coordinates
|
||||||
|
// TODO: Handle this in the event listener
|
||||||
|
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maskLayerRectAdded: {
|
||||||
|
reducer: (state, action: PayloadAction<{ layerId: string; rect: IRect }, string, { uuid: string }>) => {
|
||||||
|
const { layerId, rect } = action.payload;
|
||||||
|
if (rect.height === 0 || rect.width === 0) {
|
||||||
|
// Ignore zero-area rectangles
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
const id = getMaskedGuidnaceLayerRectId(layer.id, action.meta.uuid);
|
||||||
|
layer.maskObjects.push({
|
||||||
|
type: 'vector_mask_rect',
|
||||||
|
id,
|
||||||
|
x: rect.x - layer.x,
|
||||||
|
y: rect.y - layer.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
});
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prepare: (payload: { layerId: string; rect: IRect }) => ({ payload, meta: { uuid: uuidv4() } }),
|
||||||
|
},
|
||||||
|
maskLayerAutoNegativeChanged: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ layerId: string; autoNegative: ParameterAutoNegative }>
|
||||||
|
) => {
|
||||||
|
const { layerId, autoNegative } = action.payload;
|
||||||
|
const layer = state.layers.find((l) => l.id === layerId);
|
||||||
|
if (layer?.type === 'regional_guidance_layer') {
|
||||||
|
layer.autoNegative = autoNegative;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Base Layer
|
||||||
|
positivePromptChanged: (state, action: PayloadAction<string>) => {
|
||||||
|
state.positivePrompt = action.payload;
|
||||||
|
},
|
||||||
|
negativePromptChanged: (state, action: PayloadAction<string>) => {
|
||||||
|
state.negativePrompt = action.payload;
|
||||||
|
},
|
||||||
|
positivePrompt2Changed: (state, action: PayloadAction<string>) => {
|
||||||
|
state.positivePrompt2 = action.payload;
|
||||||
|
},
|
||||||
|
negativePrompt2Changed: (state, action: PayloadAction<string>) => {
|
||||||
|
state.negativePrompt2 = action.payload;
|
||||||
|
},
|
||||||
|
shouldConcatPromptsChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.shouldConcatPrompts = action.payload;
|
||||||
|
},
|
||||||
|
widthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean }>) => {
|
||||||
|
const { width, updateAspectRatio } = action.payload;
|
||||||
|
state.size.width = width;
|
||||||
|
if (updateAspectRatio) {
|
||||||
|
state.size.aspectRatio.value = width / state.size.height;
|
||||||
|
state.size.aspectRatio.id = 'Free';
|
||||||
|
state.size.aspectRatio.isLocked = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heightChanged: (state, action: PayloadAction<{ height: number; updateAspectRatio?: boolean }>) => {
|
||||||
|
const { height, updateAspectRatio } = action.payload;
|
||||||
|
state.size.height = height;
|
||||||
|
if (updateAspectRatio) {
|
||||||
|
state.size.aspectRatio.value = state.size.width / height;
|
||||||
|
state.size.aspectRatio.id = 'Free';
|
||||||
|
state.size.aspectRatio.isLocked = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
||||||
|
state.size.aspectRatio = action.payload;
|
||||||
|
},
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region General
|
||||||
|
brushSizeChanged: (state, action: PayloadAction<number>) => {
|
||||||
|
state.brushSize = Math.round(action.payload);
|
||||||
|
},
|
||||||
|
globalMaskLayerOpacityChanged: (state, action: PayloadAction<number>) => {
|
||||||
|
state.globalMaskLayerOpacity = action.payload;
|
||||||
|
},
|
||||||
|
isEnabledChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isEnabled = action.payload;
|
||||||
|
},
|
||||||
|
undo: (state) => {
|
||||||
|
// Invalidate the bbox for all layers to prevent stale bboxes
|
||||||
|
for (const layer of state.layers.filter(isRenderableLayer)) {
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redo: (state) => {
|
||||||
|
// Invalidate the bbox for all layers to prevent stale bboxes
|
||||||
|
for (const layer of state.layers.filter(isRenderableLayer)) {
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//#endregion
|
||||||
|
},
|
||||||
|
extraReducers(builder) {
|
||||||
|
builder.addCase(modelChanged, (state, action) => {
|
||||||
|
const newModel = action.payload;
|
||||||
|
if (!newModel || action.meta.previousModel?.base === newModel.base) {
|
||||||
|
// Model was cleared or the base didn't change
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const optimalDimension = getOptimalDimension(newModel);
|
||||||
|
if (getIsSizeOptimal(state.size.width, state.size.height, optimalDimension)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { width, height } = calculateNewSize(state.size.aspectRatio.value, optimalDimension * optimalDimension);
|
||||||
|
state.size.width = width;
|
||||||
|
state.size.height = height;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(controlAdapterImageChanged, (state, action) => {
|
||||||
|
const { id, controlImage } = action.payload;
|
||||||
|
const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.controlNetId === id);
|
||||||
|
if (layer) {
|
||||||
|
layer.bbox = null;
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
layer.isEnabled = true;
|
||||||
|
layer.imageName = controlImage?.image_name ?? null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(controlAdapterProcessedImageChanged, (state, action) => {
|
||||||
|
const { id, processedControlImage } = action.payload;
|
||||||
|
const layer = state.layers.filter(isControlAdapterLayer).find((l) => l.controlNetId === id);
|
||||||
|
if (layer) {
|
||||||
|
layer.bbox = null;
|
||||||
|
layer.bboxNeedsUpdate = true;
|
||||||
|
layer.isEnabled = true;
|
||||||
|
layer.imageName = processedControlImage?.image_name ?? null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling
|
||||||
|
// factor than the UNet. Hopefully we get an upstream fix in diffusers.
|
||||||
|
builder.addMatcher(isAnyControlAdapterAdded, (state, action) => {
|
||||||
|
if (action.payload.type === 't2i_adapter') {
|
||||||
|
state.size.width = roundToMultiple(state.size.width, 64);
|
||||||
|
state.size.height = roundToMultiple(state.size.height, 64);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to cycle through a set of colors for the prompt region layers.
|
||||||
|
*/
|
||||||
|
class LayerColors {
|
||||||
|
static COLORS: RgbColor[] = [
|
||||||
|
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
|
||||||
|
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)
|
||||||
|
{ r: 250, g: 225, b: 80 }, // rgb(250, 225, 80)
|
||||||
|
{ r: 220, g: 144, b: 101 }, // rgb(220, 144, 101)
|
||||||
|
{ r: 224, g: 117, b: 117 }, // rgb(224, 117, 117)
|
||||||
|
{ r: 213, g: 139, b: 202 }, // rgb(213, 139, 202)
|
||||||
|
{ r: 161, g: 120, b: 214 }, // rgb(161, 120, 214)
|
||||||
|
];
|
||||||
|
static i = this.COLORS.length - 1;
|
||||||
|
/**
|
||||||
|
* Get the next color in the sequence. If a known color is provided, the next color will be the one after it.
|
||||||
|
*/
|
||||||
|
static next(currentColor?: RgbColor): RgbColor {
|
||||||
|
if (currentColor) {
|
||||||
|
const i = this.COLORS.findIndex((c) => isEqual(c, currentColor));
|
||||||
|
if (i !== -1) {
|
||||||
|
this.i = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.i = (this.i + 1) % this.COLORS.length;
|
||||||
|
const color = this.COLORS[this.i];
|
||||||
|
assert(color);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
// All layer actions
|
||||||
|
layerDeleted,
|
||||||
|
layerMovedBackward,
|
||||||
|
layerMovedForward,
|
||||||
|
layerMovedToBack,
|
||||||
|
layerMovedToFront,
|
||||||
|
layerReset,
|
||||||
|
layerSelected,
|
||||||
|
layerTranslated,
|
||||||
|
layerBboxChanged,
|
||||||
|
layerVisibilityToggled,
|
||||||
|
selectedLayerReset,
|
||||||
|
selectedLayerDeleted,
|
||||||
|
regionalGuidanceLayerAdded,
|
||||||
|
ipAdapterLayerAdded,
|
||||||
|
controlAdapterLayerAdded,
|
||||||
|
layerOpacityChanged,
|
||||||
|
// CA layer actions
|
||||||
|
isFilterEnabledChanged,
|
||||||
|
// Mask layer actions
|
||||||
|
maskLayerLineAdded,
|
||||||
|
maskLayerPointsAdded,
|
||||||
|
maskLayerRectAdded,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
maskLayerIPAdapterAdded,
|
||||||
|
maskLayerIPAdapterDeleted,
|
||||||
|
maskLayerAutoNegativeChanged,
|
||||||
|
maskLayerPreviewColorChanged,
|
||||||
|
// Base layer actions
|
||||||
|
positivePromptChanged,
|
||||||
|
negativePromptChanged,
|
||||||
|
positivePrompt2Changed,
|
||||||
|
negativePrompt2Changed,
|
||||||
|
shouldConcatPromptsChanged,
|
||||||
|
widthChanged,
|
||||||
|
heightChanged,
|
||||||
|
aspectRatioChanged,
|
||||||
|
// General actions
|
||||||
|
brushSizeChanged,
|
||||||
|
globalMaskLayerOpacityChanged,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
} = controlLayersSlice.actions;
|
||||||
|
|
||||||
|
export const selectAllControlAdapterIds = (controlLayers: ControlLayersState) =>
|
||||||
|
controlLayers.layers.flatMap((l) => {
|
||||||
|
if (l.type === 'control_adapter_layer') {
|
||||||
|
return [l.controlNetId];
|
||||||
|
}
|
||||||
|
if (l.type === 'ip_adapter_layer') {
|
||||||
|
return [l.ipAdapterId];
|
||||||
|
}
|
||||||
|
if (l.type === 'regional_guidance_layer') {
|
||||||
|
return l.ipAdapterIds;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectControlLayersSlice = (state: RootState) => state.controlLayers;
|
||||||
|
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
const migrateControlLayersState = (state: any): any => {
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const $isMouseDown = atom(false);
|
||||||
|
export const $isMouseOver = atom(false);
|
||||||
|
export const $lastMouseDownPos = atom<Vector2d | null>(null);
|
||||||
|
export const $tool = atom<Tool>('brush');
|
||||||
|
export const $cursorPosition = atom<Vector2d | null>(null);
|
||||||
|
|
||||||
|
// IDs for singleton Konva layers and objects
|
||||||
|
export const TOOL_PREVIEW_LAYER_ID = 'tool_preview_layer';
|
||||||
|
export const TOOL_PREVIEW_BRUSH_GROUP_ID = 'tool_preview_layer.brush_group';
|
||||||
|
export const TOOL_PREVIEW_BRUSH_FILL_ID = 'tool_preview_layer.brush_fill';
|
||||||
|
export const TOOL_PREVIEW_BRUSH_BORDER_INNER_ID = 'tool_preview_layer.brush_border_inner';
|
||||||
|
export const TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID = 'tool_preview_layer.brush_border_outer';
|
||||||
|
export const TOOL_PREVIEW_RECT_ID = 'tool_preview_layer.rect';
|
||||||
|
export const BACKGROUND_LAYER_ID = 'background_layer';
|
||||||
|
export const BACKGROUND_RECT_ID = 'background_layer.rect';
|
||||||
|
export const NO_LAYERS_MESSAGE_LAYER_ID = 'no_layers_message';
|
||||||
|
|
||||||
|
// Names (aka classes) for Konva layers and objects
|
||||||
|
export const CONTROLNET_LAYER_NAME = 'control_adapter_layer';
|
||||||
|
export const CONTROLNET_LAYER_IMAGE_NAME = 'control_adapter_layer.image';
|
||||||
|
export const regional_guidance_layer_NAME = 'regional_guidance_layer';
|
||||||
|
export const regional_guidance_layer_LINE_NAME = 'regional_guidance_layer.line';
|
||||||
|
export const regional_guidance_layer_OBJECT_GROUP_NAME = 'regional_guidance_layer.object_group';
|
||||||
|
export const regional_guidance_layer_RECT_NAME = 'regional_guidance_layer.rect';
|
||||||
|
export const LAYER_BBOX_NAME = 'layer.bbox';
|
||||||
|
|
||||||
|
// Getters for non-singleton layer and object IDs
|
||||||
|
const getRegionalGuidanceLayerId = (layerId: string) => `${regional_guidance_layer_NAME}_${layerId}`;
|
||||||
|
const getRegionalGuidanceLayerLineId = (layerId: string, lineId: string) => `${layerId}.line_${lineId}`;
|
||||||
|
const getMaskedGuidnaceLayerRectId = (layerId: string, lineId: string) => `${layerId}.rect_${lineId}`;
|
||||||
|
export const getRegionalGuidanceLayerObjectGroupId = (layerId: string, groupId: string) =>
|
||||||
|
`${layerId}.objectGroup_${groupId}`;
|
||||||
|
export const getLayerBboxId = (layerId: string) => `${layerId}.bbox`;
|
||||||
|
const getControlNetLayerId = (layerId: string) => `control_adapter_layer_${layerId}`;
|
||||||
|
export const getControlNetLayerImageId = (layerId: string, imageName: string) => `${layerId}.image_${imageName}`;
|
||||||
|
const getIPAdapterLayerId = (layerId: string) => `ip_adapter_layer_${layerId}`;
|
||||||
|
|
||||||
|
export const controlLayersPersistConfig: PersistConfig<ControlLayersState> = {
|
||||||
|
name: controlLayersSlice.name,
|
||||||
|
initialState: initialControlLayersState,
|
||||||
|
migrate: migrateControlLayersState,
|
||||||
|
persistDenylist: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// These actions are _individually_ grouped together as single undoable actions
|
||||||
|
const undoableGroupByMatcher = isAnyOf(
|
||||||
|
layerTranslated,
|
||||||
|
brushSizeChanged,
|
||||||
|
globalMaskLayerOpacityChanged,
|
||||||
|
maskLayerPositivePromptChanged,
|
||||||
|
maskLayerNegativePromptChanged,
|
||||||
|
maskLayerPreviewColorChanged
|
||||||
|
);
|
||||||
|
|
||||||
|
// These are used to group actions into logical lines below (hate typos)
|
||||||
|
const LINE_1 = 'LINE_1';
|
||||||
|
const LINE_2 = 'LINE_2';
|
||||||
|
|
||||||
|
export const controlLayersUndoableConfig: UndoableOptions<ControlLayersState, UnknownAction> = {
|
||||||
|
limit: 64,
|
||||||
|
undoType: controlLayersSlice.actions.undo.type,
|
||||||
|
redoType: controlLayersSlice.actions.redo.type,
|
||||||
|
groupBy: (action, state, history) => {
|
||||||
|
// Lines are started with `maskLayerLineAdded` and may have any number of subsequent `maskLayerPointsAdded` events.
|
||||||
|
// We can use a double-buffer-esque trick to group each "logical" line as a single undoable action, without grouping
|
||||||
|
// separate logical lines as a single undo action.
|
||||||
|
if (maskLayerLineAdded.match(action)) {
|
||||||
|
return history.group === LINE_1 ? LINE_2 : LINE_1;
|
||||||
|
}
|
||||||
|
if (maskLayerPointsAdded.match(action)) {
|
||||||
|
if (history.group === LINE_1 || history.group === LINE_2) {
|
||||||
|
return history.group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (undoableGroupByMatcher(action)) {
|
||||||
|
return action.type;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
filter: (action, _state, _history) => {
|
||||||
|
// Ignore all actions from other slices
|
||||||
|
if (!action.type.startsWith(controlLayersSlice.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// This action is triggered on state changes, including when we undo. If we do not ignore this action, when we
|
||||||
|
// undo, this action triggers and empties the future states array. Therefore, we must ignore this action.
|
||||||
|
if (layerBboxChanged.match(action)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
||||||
|
import type {
|
||||||
|
ParameterAutoNegative,
|
||||||
|
ParameterHeight,
|
||||||
|
ParameterNegativePrompt,
|
||||||
|
ParameterNegativeStylePromptSDXL,
|
||||||
|
ParameterPositivePrompt,
|
||||||
|
ParameterPositiveStylePromptSDXL,
|
||||||
|
ParameterWidth,
|
||||||
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
|
import type { IRect } from 'konva/lib/types';
|
||||||
|
import type { RgbColor } from 'react-colorful';
|
||||||
|
|
||||||
|
export type DrawingTool = 'brush' | 'eraser';
|
||||||
|
|
||||||
|
export type Tool = DrawingTool | 'move' | 'rect';
|
||||||
|
|
||||||
|
export type VectorMaskLine = {
|
||||||
|
id: string;
|
||||||
|
type: 'vector_mask_line';
|
||||||
|
tool: DrawingTool;
|
||||||
|
strokeWidth: number;
|
||||||
|
points: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VectorMaskRect = {
|
||||||
|
id: string;
|
||||||
|
type: 'vector_mask_rect';
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LayerBase = {
|
||||||
|
id: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RenderableLayerBase = LayerBase & {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
bbox: IRect | null;
|
||||||
|
bboxNeedsUpdate: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ControlAdapterLayer = RenderableLayerBase & {
|
||||||
|
type: 'control_adapter_layer'; // technically, also t2i adapter layer
|
||||||
|
controlNetId: string;
|
||||||
|
imageName: string | null;
|
||||||
|
opacity: number;
|
||||||
|
isFilterEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPAdapterLayer = LayerBase & {
|
||||||
|
type: 'ip_adapter_layer'; // technically, also t2i adapter layer
|
||||||
|
ipAdapterId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionalGuidanceLayer = RenderableLayerBase & {
|
||||||
|
type: 'regional_guidance_layer';
|
||||||
|
maskObjects: (VectorMaskLine | VectorMaskRect)[];
|
||||||
|
positivePrompt: ParameterPositivePrompt | null;
|
||||||
|
negativePrompt: ParameterNegativePrompt | null; // Up to one text prompt per mask
|
||||||
|
ipAdapterIds: string[]; // Any number of image prompts
|
||||||
|
previewColor: RgbColor;
|
||||||
|
autoNegative: ParameterAutoNegative;
|
||||||
|
needsPixelBbox: boolean; // Needs the slower pixel-based bbox calculation - set to true when an there is an eraser object
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Layer = RegionalGuidanceLayer | ControlAdapterLayer | IPAdapterLayer;
|
||||||
|
|
||||||
|
export type ControlLayersState = {
|
||||||
|
_version: 1;
|
||||||
|
selectedLayerId: string | null;
|
||||||
|
layers: Layer[];
|
||||||
|
brushSize: number;
|
||||||
|
globalMaskLayerOpacity: number;
|
||||||
|
isEnabled: boolean;
|
||||||
|
positivePrompt: ParameterPositivePrompt;
|
||||||
|
negativePrompt: ParameterNegativePrompt;
|
||||||
|
positivePrompt2: ParameterPositiveStylePromptSDXL;
|
||||||
|
negativePrompt2: ParameterNegativeStylePromptSDXL;
|
||||||
|
shouldConcatPrompts: boolean;
|
||||||
|
initialImage: string | null;
|
||||||
|
size: {
|
||||||
|
width: ParameterWidth;
|
||||||
|
height: ParameterHeight;
|
||||||
|
aspectRatio: AspectRatioState;
|
||||||
|
};
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { VECTOR_MASK_LAYER_OBJECT_GROUP_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { regional_guidance_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 { Layer as KonvaLayerType } from 'konva/lib/Layer';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
@ -81,7 +81,7 @@ export const getLayerBboxPixels = (layer: KonvaLayerType, preview: boolean = fal
|
|||||||
offscreenStage.add(layerClone);
|
offscreenStage.add(layerClone);
|
||||||
|
|
||||||
for (const child of layerClone.getChildren()) {
|
for (const child of layerClone.getChildren()) {
|
||||||
if (child.name() === VECTOR_MASK_LAYER_OBJECT_GROUP_NAME) {
|
if (child.name() === regional_guidance_layer_OBJECT_GROUP_NAME) {
|
||||||
// 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();
|
@ -1,8 +1,8 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { VECTOR_MASK_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
import { isRegionalGuidanceLayer, regional_guidance_layer_NAME } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { renderers } from 'features/regionalPrompts/util/renderers';
|
import { renderers } from 'features/controlLayers/util/renderers';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
@ -17,12 +17,14 @@ export const getRegionalPromptLayerBlobs = async (
|
|||||||
preview: boolean = false
|
preview: boolean = false
|
||||||
): Promise<Record<string, Blob>> => {
|
): Promise<Record<string, Blob>> => {
|
||||||
const state = getStore().getState();
|
const state = getStore().getState();
|
||||||
const reduxLayers = state.regionalPrompts.present.layers;
|
const { layers } = state.controlLayers.present;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
const reduxLayers = layers.filter(isRegionalGuidanceLayer);
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height });
|
const stage = new Konva.Stage({ container, width, height });
|
||||||
renderers.renderLayers(stage, reduxLayers, 1, 'brush');
|
renderers.renderLayers(stage, reduxLayers, 1, 'brush');
|
||||||
|
|
||||||
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);
|
const konvaLayers = stage.find<Konva.Layer>(`.${regional_guidance_layer_NAME}`);
|
||||||
const blobs: Record<string, Blob> = {};
|
const blobs: Record<string, Blob> = {};
|
||||||
|
|
||||||
// First remove all layers
|
// First remove all layers
|
@ -1,43 +1,49 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbaColorToString, rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import { getScaledFlooredCursorPosition } from 'features/regionalPrompts/hooks/mouseEventHooks';
|
import { getScaledFlooredCursorPosition } from 'features/controlLayers/hooks/mouseEventHooks';
|
||||||
import type {
|
|
||||||
Layer,
|
|
||||||
Tool,
|
|
||||||
VectorMaskLayer,
|
|
||||||
VectorMaskLine,
|
|
||||||
VectorMaskRect,
|
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import {
|
import {
|
||||||
$tool,
|
$tool,
|
||||||
BACKGROUND_LAYER_ID,
|
BACKGROUND_LAYER_ID,
|
||||||
BACKGROUND_RECT_ID,
|
BACKGROUND_RECT_ID,
|
||||||
|
CONTROLNET_LAYER_IMAGE_NAME,
|
||||||
|
CONTROLNET_LAYER_NAME,
|
||||||
|
getControlNetLayerImageId,
|
||||||
getLayerBboxId,
|
getLayerBboxId,
|
||||||
getVectorMaskLayerObjectGroupId,
|
getRegionalGuidanceLayerObjectGroupId,
|
||||||
isVectorMaskLayer,
|
isControlAdapterLayer,
|
||||||
|
isRegionalGuidanceLayer,
|
||||||
|
isRenderableLayer,
|
||||||
LAYER_BBOX_NAME,
|
LAYER_BBOX_NAME,
|
||||||
|
NO_LAYERS_MESSAGE_LAYER_ID,
|
||||||
|
regional_guidance_layer_LINE_NAME,
|
||||||
|
regional_guidance_layer_NAME,
|
||||||
|
regional_guidance_layer_OBJECT_GROUP_NAME,
|
||||||
|
regional_guidance_layer_RECT_NAME,
|
||||||
TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
TOOL_PREVIEW_BRUSH_BORDER_INNER_ID,
|
||||||
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
TOOL_PREVIEW_BRUSH_BORDER_OUTER_ID,
|
||||||
TOOL_PREVIEW_BRUSH_FILL_ID,
|
TOOL_PREVIEW_BRUSH_FILL_ID,
|
||||||
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
TOOL_PREVIEW_BRUSH_GROUP_ID,
|
||||||
TOOL_PREVIEW_LAYER_ID,
|
TOOL_PREVIEW_LAYER_ID,
|
||||||
TOOL_PREVIEW_RECT_ID,
|
TOOL_PREVIEW_RECT_ID,
|
||||||
VECTOR_MASK_LAYER_LINE_NAME,
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
VECTOR_MASK_LAYER_NAME,
|
import type {
|
||||||
VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
ControlAdapterLayer,
|
||||||
VECTOR_MASK_LAYER_RECT_NAME,
|
Layer,
|
||||||
} from 'features/regionalPrompts/store/regionalPromptsSlice';
|
RegionalGuidanceLayer,
|
||||||
import { getLayerBboxFast, getLayerBboxPixels } from 'features/regionalPrompts/util/bbox';
|
Tool,
|
||||||
|
VectorMaskLine,
|
||||||
|
VectorMaskRect,
|
||||||
|
} from 'features/controlLayers/store/types';
|
||||||
|
import { getLayerBboxFast, getLayerBboxPixels } from 'features/controlLayers/util/bbox';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import type { RgbColor } from 'react-colorful';
|
import type { RgbColor } from 'react-colorful';
|
||||||
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const BBOX_SELECTED_STROKE = 'rgba(78, 190, 255, 1)';
|
const BBOX_SELECTED_STROKE = 'rgba(78, 190, 255, 1)';
|
||||||
const BBOX_NOT_SELECTED_STROKE = 'rgba(255, 255, 255, 0.353)';
|
|
||||||
const BBOX_NOT_SELECTED_MOUSEOVER_STROKE = 'rgba(255, 255, 255, 0.661)';
|
|
||||||
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
|
||||||
const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
|
const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
|
||||||
// This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL
|
// This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL
|
||||||
@ -46,15 +52,11 @@ const STAGE_BG_DATAURL =
|
|||||||
|
|
||||||
const mapId = (object: { id: string }) => object.id;
|
const mapId = (object: { id: string }) => object.id;
|
||||||
|
|
||||||
const getIsSelected = (layerId?: string | null) => {
|
const selectRenderableLayers = (n: Konva.Node) =>
|
||||||
if (!layerId) {
|
n.name() === regional_guidance_layer_NAME || n.name() === CONTROLNET_LAYER_NAME;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return layerId === getStore().getState().regionalPrompts.present.selectedLayerId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectVectorMaskObjects = (node: Konva.Node) => {
|
const selectVectorMaskObjects = (node: Konva.Node) => {
|
||||||
return node.name() === VECTOR_MASK_LAYER_LINE_NAME || node.name() === VECTOR_MASK_LAYER_RECT_NAME;
|
return node.name() === regional_guidance_layer_LINE_NAME || node.name() === regional_guidance_layer_RECT_NAME;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,17 +134,21 @@ const renderToolPreview = (
|
|||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
color: RgbColor | null,
|
color: RgbColor | null,
|
||||||
|
selectedLayerType: Layer['type'] | null,
|
||||||
globalMaskLayerOpacity: number,
|
globalMaskLayerOpacity: number,
|
||||||
cursorPos: Vector2d | null,
|
cursorPos: Vector2d | null,
|
||||||
lastMouseDownPos: Vector2d | null,
|
lastMouseDownPos: Vector2d | null,
|
||||||
isMouseOver: boolean,
|
isMouseOver: boolean,
|
||||||
brushSize: number
|
brushSize: number
|
||||||
) => {
|
) => {
|
||||||
const layerCount = stage.find(`.${VECTOR_MASK_LAYER_NAME}`).length;
|
const layerCount = stage.find(`.${regional_guidance_layer_NAME}`).length;
|
||||||
// Update the stage's pointer style
|
// Update the stage's pointer style
|
||||||
if (layerCount === 0) {
|
if (layerCount === 0) {
|
||||||
// We have no layers, so we should not render any tool
|
// We have no layers, so we should not render any tool
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
|
} else if (selectedLayerType !== 'regional_guidance_layer') {
|
||||||
|
// Non-mask-guidance layers don't have tools
|
||||||
|
stage.container().style.cursor = 'not-allowed';
|
||||||
} else if (tool === 'move') {
|
} else if (tool === 'move') {
|
||||||
// Move tool gets a pointer
|
// Move tool gets a pointer
|
||||||
stage.container().style.cursor = 'default';
|
stage.container().style.cursor = 'default';
|
||||||
@ -219,15 +225,15 @@ const renderToolPreview = (
|
|||||||
* @param reduxLayer The redux layer to create the konva layer from.
|
* @param reduxLayer The redux layer to create the konva layer from.
|
||||||
* @param onLayerPosChanged Callback for when the layer's position changes.
|
* @param onLayerPosChanged Callback for when the layer's position changes.
|
||||||
*/
|
*/
|
||||||
const createVectorMaskLayer = (
|
const createRegionalGuidanceLayer = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayer: VectorMaskLayer,
|
reduxLayer: RegionalGuidanceLayer,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
// This layer hasn't been added to the konva state yet
|
// This layer hasn't been added to the konva state yet
|
||||||
const konvaLayer = new Konva.Layer({
|
const konvaLayer = new Konva.Layer({
|
||||||
id: reduxLayer.id,
|
id: reduxLayer.id,
|
||||||
name: VECTOR_MASK_LAYER_NAME,
|
name: regional_guidance_layer_NAME,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
dragDistance: 0,
|
dragDistance: 0,
|
||||||
});
|
});
|
||||||
@ -259,8 +265,8 @@ const createVectorMaskLayer = (
|
|||||||
|
|
||||||
// The object group holds all of the layer's objects (e.g. lines and rects)
|
// The object group holds all of the layer's objects (e.g. lines and rects)
|
||||||
const konvaObjectGroup = new Konva.Group({
|
const konvaObjectGroup = new Konva.Group({
|
||||||
id: getVectorMaskLayerObjectGroupId(reduxLayer.id, uuidv4()),
|
id: getRegionalGuidanceLayerObjectGroupId(reduxLayer.id, uuidv4()),
|
||||||
name: VECTOR_MASK_LAYER_OBJECT_GROUP_NAME,
|
name: regional_guidance_layer_OBJECT_GROUP_NAME,
|
||||||
listening: false,
|
listening: false,
|
||||||
});
|
});
|
||||||
konvaLayer.add(konvaObjectGroup);
|
konvaLayer.add(konvaObjectGroup);
|
||||||
@ -279,7 +285,7 @@ const createVectorMaskLine = (reduxObject: VectorMaskLine, konvaGroup: Konva.Gro
|
|||||||
const vectorMaskLine = new Konva.Line({
|
const vectorMaskLine = new Konva.Line({
|
||||||
id: reduxObject.id,
|
id: reduxObject.id,
|
||||||
key: reduxObject.id,
|
key: reduxObject.id,
|
||||||
name: VECTOR_MASK_LAYER_LINE_NAME,
|
name: regional_guidance_layer_LINE_NAME,
|
||||||
strokeWidth: reduxObject.strokeWidth,
|
strokeWidth: reduxObject.strokeWidth,
|
||||||
tension: 0,
|
tension: 0,
|
||||||
lineCap: 'round',
|
lineCap: 'round',
|
||||||
@ -301,7 +307,7 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro
|
|||||||
const vectorMaskRect = new Konva.Rect({
|
const vectorMaskRect = new Konva.Rect({
|
||||||
id: reduxObject.id,
|
id: reduxObject.id,
|
||||||
key: reduxObject.id,
|
key: reduxObject.id,
|
||||||
name: VECTOR_MASK_LAYER_RECT_NAME,
|
name: regional_guidance_layer_RECT_NAME,
|
||||||
x: reduxObject.x,
|
x: reduxObject.x,
|
||||||
y: reduxObject.y,
|
y: reduxObject.y,
|
||||||
width: reduxObject.width,
|
width: reduxObject.width,
|
||||||
@ -320,15 +326,16 @@ const createVectorMaskRect = (reduxObject: VectorMaskRect, konvaGroup: Konva.Gro
|
|||||||
* @param globalMaskLayerOpacity The opacity of the global mask layer.
|
* @param globalMaskLayerOpacity The opacity of the global mask layer.
|
||||||
* @param tool The current tool.
|
* @param tool The current tool.
|
||||||
*/
|
*/
|
||||||
const renderVectorMaskLayer = (
|
const renderRegionalGuidanceLayer = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayer: VectorMaskLayer,
|
reduxLayer: RegionalGuidanceLayer,
|
||||||
globalMaskLayerOpacity: number,
|
globalMaskLayerOpacity: number,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
): void => {
|
): void => {
|
||||||
const konvaLayer =
|
const konvaLayer =
|
||||||
stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? createVectorMaskLayer(stage, reduxLayer, onLayerPosChanged);
|
stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ??
|
||||||
|
createRegionalGuidanceLayer(stage, reduxLayer, onLayerPosChanged);
|
||||||
|
|
||||||
// Update the layer's position and listening state
|
// Update the layer's position and listening state
|
||||||
konvaLayer.setAttrs({
|
konvaLayer.setAttrs({
|
||||||
@ -340,13 +347,13 @@ const renderVectorMaskLayer = (
|
|||||||
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
// Convert the color to a string, stripping the alpha - the object group will handle opacity.
|
||||||
const rgbColor = rgbColorToString(reduxLayer.previewColor);
|
const rgbColor = rgbColorToString(reduxLayer.previewColor);
|
||||||
|
|
||||||
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${VECTOR_MASK_LAYER_OBJECT_GROUP_NAME}`);
|
const konvaObjectGroup = konvaLayer.findOne<Konva.Group>(`.${regional_guidance_layer_OBJECT_GROUP_NAME}`);
|
||||||
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`);
|
assert(konvaObjectGroup, `Object group not found for layer ${reduxLayer.id}`);
|
||||||
|
|
||||||
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
// We use caching to handle "global" layer opacity, but caching is expensive and we should only do it when required.
|
||||||
let groupNeedsCache = false;
|
let groupNeedsCache = false;
|
||||||
|
|
||||||
const objectIds = reduxLayer.objects.map(mapId);
|
const objectIds = reduxLayer.maskObjects.map(mapId);
|
||||||
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
for (const objectNode of konvaObjectGroup.find(selectVectorMaskObjects)) {
|
||||||
if (!objectIds.includes(objectNode.id())) {
|
if (!objectIds.includes(objectNode.id())) {
|
||||||
objectNode.destroy();
|
objectNode.destroy();
|
||||||
@ -354,7 +361,7 @@ const renderVectorMaskLayer = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxObject of reduxLayer.objects) {
|
for (const reduxObject of reduxLayer.maskObjects) {
|
||||||
if (reduxObject.type === 'vector_mask_line') {
|
if (reduxObject.type === 'vector_mask_line') {
|
||||||
const vectorMaskLine =
|
const vectorMaskLine =
|
||||||
stage.findOne<Konva.Line>(`#${reduxObject.id}`) ?? createVectorMaskLine(reduxObject, konvaObjectGroup);
|
stage.findOne<Konva.Line>(`#${reduxObject.id}`) ?? createVectorMaskLine(reduxObject, konvaObjectGroup);
|
||||||
@ -383,8 +390,8 @@ const renderVectorMaskLayer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only update layer visibility if it has changed.
|
// Only update layer visibility if it has changed.
|
||||||
if (konvaLayer.visible() !== reduxLayer.isVisible) {
|
if (konvaLayer.visible() !== reduxLayer.isEnabled) {
|
||||||
konvaLayer.visible(reduxLayer.isVisible);
|
konvaLayer.visible(reduxLayer.isEnabled);
|
||||||
groupNeedsCache = true;
|
groupNeedsCache = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -401,6 +408,118 @@ const renderVectorMaskLayer = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer): Konva.Layer => {
|
||||||
|
const konvaLayer = new Konva.Layer({
|
||||||
|
id: reduxLayer.id,
|
||||||
|
name: CONTROLNET_LAYER_NAME,
|
||||||
|
imageSmoothingEnabled: true,
|
||||||
|
});
|
||||||
|
stage.add(konvaLayer);
|
||||||
|
return konvaLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createControlNetLayerImage = (konvaLayer: Konva.Layer, image: HTMLImageElement): Konva.Image => {
|
||||||
|
const konvaImage = new Konva.Image({
|
||||||
|
name: CONTROLNET_LAYER_IMAGE_NAME,
|
||||||
|
image,
|
||||||
|
});
|
||||||
|
konvaLayer.add(konvaImage);
|
||||||
|
return konvaImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateControlNetLayerImageSource = async (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
konvaLayer: Konva.Layer,
|
||||||
|
reduxLayer: ControlAdapterLayer
|
||||||
|
) => {
|
||||||
|
if (reduxLayer.imageName) {
|
||||||
|
const imageName = reduxLayer.imageName;
|
||||||
|
const req = getStore().dispatch(imagesApi.endpoints.getImageDTO.initiate(reduxLayer.imageName));
|
||||||
|
const imageDTO = await req.unwrap();
|
||||||
|
req.unsubscribe();
|
||||||
|
const image = new Image();
|
||||||
|
const imageId = getControlNetLayerImageId(reduxLayer.id, imageName);
|
||||||
|
image.onload = () => {
|
||||||
|
// Find the existing image or create a new one - must find using the name, bc the id may have just changed
|
||||||
|
const konvaImage =
|
||||||
|
konvaLayer.findOne<Konva.Image>(`.${CONTROLNET_LAYER_IMAGE_NAME}`) ??
|
||||||
|
createControlNetLayerImage(konvaLayer, image);
|
||||||
|
|
||||||
|
// Update the image's attributes
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
id: imageId,
|
||||||
|
image,
|
||||||
|
});
|
||||||
|
updateControlNetLayerImageAttrs(stage, konvaImage, reduxLayer);
|
||||||
|
// Must cache after this to apply the filters
|
||||||
|
konvaImage.cache();
|
||||||
|
image.id = imageId;
|
||||||
|
};
|
||||||
|
image.src = imageDTO.image_url;
|
||||||
|
} else {
|
||||||
|
konvaLayer.findOne(`.${CONTROLNET_LAYER_IMAGE_NAME}`)?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateControlNetLayerImageAttrs = (
|
||||||
|
stage: Konva.Stage,
|
||||||
|
konvaImage: Konva.Image,
|
||||||
|
reduxLayer: ControlAdapterLayer
|
||||||
|
) => {
|
||||||
|
let needsCache = false;
|
||||||
|
const newWidth = stage.width() / stage.scaleX();
|
||||||
|
const newHeight = stage.height() / stage.scaleY();
|
||||||
|
const hasFilter = konvaImage.filters() !== null && konvaImage.filters().length > 0;
|
||||||
|
if (
|
||||||
|
konvaImage.width() !== newWidth ||
|
||||||
|
konvaImage.height() !== newHeight ||
|
||||||
|
konvaImage.visible() !== reduxLayer.isEnabled ||
|
||||||
|
hasFilter !== reduxLayer.isFilterEnabled
|
||||||
|
) {
|
||||||
|
konvaImage.setAttrs({
|
||||||
|
opacity: reduxLayer.opacity,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
width: stage.width() / stage.scaleX(),
|
||||||
|
height: stage.height() / stage.scaleY(),
|
||||||
|
visible: reduxLayer.isEnabled,
|
||||||
|
filters: reduxLayer.isFilterEnabled ? [LightnessToAlphaFilter] : [],
|
||||||
|
});
|
||||||
|
needsCache = true;
|
||||||
|
}
|
||||||
|
if (konvaImage.opacity() !== reduxLayer.opacity) {
|
||||||
|
konvaImage.opacity(reduxLayer.opacity);
|
||||||
|
}
|
||||||
|
if (needsCache) {
|
||||||
|
konvaImage.cache();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderControlNetLayer = (stage: Konva.Stage, reduxLayer: ControlAdapterLayer) => {
|
||||||
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`) ?? createControlNetLayer(stage, reduxLayer);
|
||||||
|
const konvaImage = konvaLayer.findOne<Konva.Image>(`.${CONTROLNET_LAYER_IMAGE_NAME}`);
|
||||||
|
const canvasImageSource = konvaImage?.image();
|
||||||
|
let imageSourceNeedsUpdate = false;
|
||||||
|
if (canvasImageSource instanceof HTMLImageElement) {
|
||||||
|
if (
|
||||||
|
reduxLayer.imageName &&
|
||||||
|
canvasImageSource.id !== getControlNetLayerImageId(reduxLayer.id, reduxLayer.imageName)
|
||||||
|
) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
} else if (!reduxLayer.imageName) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
} else if (!canvasImageSource) {
|
||||||
|
imageSourceNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSourceNeedsUpdate) {
|
||||||
|
updateControlNetLayerImageSource(stage, konvaLayer, reduxLayer);
|
||||||
|
} else if (konvaImage) {
|
||||||
|
updateControlNetLayerImageAttrs(stage, konvaImage, reduxLayer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the layers on the stage.
|
* Renders the layers on the stage.
|
||||||
* @param stage The konva stage to render on.
|
* @param stage The konva stage to render on.
|
||||||
@ -416,18 +535,20 @@ const renderLayers = (
|
|||||||
tool: Tool,
|
tool: Tool,
|
||||||
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
onLayerPosChanged?: (layerId: string, x: number, y: number) => void
|
||||||
) => {
|
) => {
|
||||||
const reduxLayerIds = reduxLayers.map(mapId);
|
const reduxLayerIds = reduxLayers.filter(isRenderableLayer).map(mapId);
|
||||||
|
|
||||||
// Remove un-rendered layers
|
// Remove un-rendered layers
|
||||||
for (const konvaLayer of stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`)) {
|
for (const konvaLayer of stage.find<Konva.Layer>(selectRenderableLayers)) {
|
||||||
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
if (!reduxLayerIds.includes(konvaLayer.id())) {
|
||||||
konvaLayer.destroy();
|
konvaLayer.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxLayer of reduxLayers) {
|
for (const reduxLayer of reduxLayers) {
|
||||||
if (isVectorMaskLayer(reduxLayer)) {
|
if (isRegionalGuidanceLayer(reduxLayer)) {
|
||||||
renderVectorMaskLayer(stage, reduxLayer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
renderRegionalGuidanceLayer(stage, reduxLayer, globalMaskLayerOpacity, tool, onLayerPosChanged);
|
||||||
|
}
|
||||||
|
if (isControlAdapterLayer(reduxLayer)) {
|
||||||
|
renderControlNetLayer(stage, reduxLayer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -438,29 +559,12 @@ const renderLayers = (
|
|||||||
* @param konvaLayer The konva layer to attach the bounding box to.
|
* @param konvaLayer The konva layer to attach the bounding box to.
|
||||||
* @param onBboxMouseDown Callback for when the bounding box is clicked.
|
* @param onBboxMouseDown Callback for when the bounding box is clicked.
|
||||||
*/
|
*/
|
||||||
const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer, onBboxMouseDown: (layerId: string) => void) => {
|
const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer) => {
|
||||||
const rect = new Konva.Rect({
|
const rect = new Konva.Rect({
|
||||||
id: getLayerBboxId(reduxLayer.id),
|
id: getLayerBboxId(reduxLayer.id),
|
||||||
name: LAYER_BBOX_NAME,
|
name: LAYER_BBOX_NAME,
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
});
|
});
|
||||||
rect.on('mousedown', function () {
|
|
||||||
onBboxMouseDown(reduxLayer.id);
|
|
||||||
});
|
|
||||||
rect.on('mouseover', function (e) {
|
|
||||||
if (getIsSelected(e.target.getLayer()?.id())) {
|
|
||||||
this.stroke(BBOX_SELECTED_STROKE);
|
|
||||||
} else {
|
|
||||||
this.stroke(BBOX_NOT_SELECTED_MOUSEOVER_STROKE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
rect.on('mouseout', function (e) {
|
|
||||||
if (getIsSelected(e.target.getLayer()?.id())) {
|
|
||||||
this.stroke(BBOX_SELECTED_STROKE);
|
|
||||||
} else {
|
|
||||||
this.stroke(BBOX_NOT_SELECTED_STROKE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
konvaLayer.add(rect);
|
konvaLayer.add(rect);
|
||||||
return rect;
|
return rect;
|
||||||
};
|
};
|
||||||
@ -478,10 +582,8 @@ const createBboxRect = (reduxLayer: Layer, konvaLayer: Konva.Layer, onBboxMouseD
|
|||||||
const renderBbox = (
|
const renderBbox = (
|
||||||
stage: Konva.Stage,
|
stage: Konva.Stage,
|
||||||
reduxLayers: Layer[],
|
reduxLayers: Layer[],
|
||||||
selectedLayerId: string | null,
|
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void,
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||||
onBboxMouseDown: (layerId: string) => 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}`)) {
|
||||||
@ -494,35 +596,36 @@ const renderBbox = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const reduxLayer of reduxLayers) {
|
for (const reduxLayer of reduxLayers) {
|
||||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${reduxLayer.id}`);
|
if (reduxLayer.type === 'regional_guidance_layer') {
|
||||||
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;
|
let bbox = reduxLayer.bbox;
|
||||||
|
|
||||||
// We only need to recalculate the bbox if the layer has changed and it has objects
|
// We only need to recalculate the bbox if the layer has changed and it has objects
|
||||||
if (reduxLayer.bboxNeedsUpdate && reduxLayer.objects.length) {
|
if (reduxLayer.bboxNeedsUpdate && reduxLayer.maskObjects.length) {
|
||||||
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
||||||
bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer);
|
bbox = reduxLayer.needsPixelBbox ? getLayerBboxPixels(konvaLayer) : getLayerBboxFast(konvaLayer);
|
||||||
// Update the layer's bbox in the redux store
|
// Update the layer's bbox in the redux store
|
||||||
onBboxChanged(reduxLayer.id, bbox);
|
onBboxChanged(reduxLayer.id, bbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bbox) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bbox) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rect =
|
|
||||||
konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(reduxLayer, konvaLayer, onBboxMouseDown);
|
|
||||||
|
|
||||||
rect.setAttrs({
|
|
||||||
visible: true,
|
|
||||||
listening: true,
|
|
||||||
x: bbox.x,
|
|
||||||
y: bbox.y,
|
|
||||||
width: bbox.width,
|
|
||||||
height: bbox.height,
|
|
||||||
stroke: reduxLayer.id === selectedLayerId ? BBOX_SELECTED_STROKE : BBOX_NOT_SELECTED_STROKE,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -600,11 +703,47 @@ const arrangeLayers = (stage: Konva.Stage, layerIds: string[]): void => {
|
|||||||
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++);
|
stage.findOne<Konva.Layer>(`#${TOOL_PREVIEW_LAYER_ID}`)?.zIndex(nextZIndex++);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createNoLayersMessageLayer = (stage: Konva.Stage): Konva.Layer => {
|
||||||
|
const noLayersMessageLayer = new Konva.Layer({
|
||||||
|
id: NO_LAYERS_MESSAGE_LAYER_ID,
|
||||||
|
opacity: 0.7,
|
||||||
|
listening: false,
|
||||||
|
});
|
||||||
|
const text = new Konva.Text({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
text: 'No Layers Added',
|
||||||
|
fontFamily: '"Inter Variable", sans-serif',
|
||||||
|
fontStyle: '600',
|
||||||
|
fill: 'white',
|
||||||
|
});
|
||||||
|
noLayersMessageLayer.add(text);
|
||||||
|
stage.add(noLayersMessageLayer);
|
||||||
|
return noLayersMessageLayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNoLayersMessage = (stage: Konva.Stage, layerCount: number, width: number, height: number) => {
|
||||||
|
const noLayersMessageLayer =
|
||||||
|
stage.findOne<Konva.Layer>(`#${NO_LAYERS_MESSAGE_LAYER_ID}`) ?? createNoLayersMessageLayer(stage);
|
||||||
|
if (layerCount === 0) {
|
||||||
|
noLayersMessageLayer.findOne<Konva.Text>('Text')?.setAttrs({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fontSize: 32 / stage.scaleX(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
noLayersMessageLayer?.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const renderers = {
|
export const renderers = {
|
||||||
renderToolPreview,
|
renderToolPreview,
|
||||||
renderLayers,
|
renderLayers,
|
||||||
renderBbox,
|
renderBbox,
|
||||||
renderBackground,
|
renderBackground,
|
||||||
|
renderNoLayersMessage,
|
||||||
arrangeLayers,
|
arrangeLayers,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -615,5 +754,23 @@ export const debouncedRenderers = {
|
|||||||
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
renderLayers: debounce(renderLayers, DEBOUNCE_MS),
|
||||||
renderBbox: debounce(renderBbox, DEBOUNCE_MS),
|
renderBbox: debounce(renderBbox, DEBOUNCE_MS),
|
||||||
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
renderBackground: debounce(renderBackground, DEBOUNCE_MS),
|
||||||
|
renderNoLayersMessage: debounce(renderNoLayersMessage, DEBOUNCE_MS),
|
||||||
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
arrangeLayers: debounce(arrangeLayers, DEBOUNCE_MS),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the lightness (HSL) of a given pixel and sets the alpha channel to that value.
|
||||||
|
* This is useful for edge maps and other masks, to make the black areas transparent.
|
||||||
|
* @param imageData The image data to apply the filter to
|
||||||
|
*/
|
||||||
|
const LightnessToAlphaFilter = (imageData: ImageData) => {
|
||||||
|
const len = imageData.data.length / 4;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const r = imageData.data[i * 4 + 0] as number;
|
||||||
|
const g = imageData.data[i * 4 + 1] as number;
|
||||||
|
const b = imageData.data[i * 4 + 2] as number;
|
||||||
|
const cMin = Math.min(r, g, b);
|
||||||
|
const cMax = Math.max(r, g, b);
|
||||||
|
imageData.data[i * 4 + 3] = (cMin + cMax) / 2;
|
||||||
|
}
|
||||||
|
};
|
@ -286,7 +286,9 @@ const parseControlNet: MetadataParseFunc<ControlNetConfigMetadata> = async (meta
|
|||||||
controlMode: control_mode ?? initialControlNet.controlMode,
|
controlMode: control_mode ?? initialControlNet.controlMode,
|
||||||
resizeMode: resize_mode ?? initialControlNet.resizeMode,
|
resizeMode: resize_mode ?? initialControlNet.resizeMode,
|
||||||
controlImage: image?.image_name ?? null,
|
controlImage: image?.image_name ?? null,
|
||||||
|
controlImageDimensions: null,
|
||||||
processedControlImage: processedImage?.image_name ?? null,
|
processedControlImage: processedImage?.image_name ?? null,
|
||||||
|
processedControlImageDimensions: null,
|
||||||
processorType,
|
processorType,
|
||||||
processorNode,
|
processorNode,
|
||||||
shouldAutoConfig: true,
|
shouldAutoConfig: true,
|
||||||
@ -350,9 +352,11 @@ const parseT2IAdapter: MetadataParseFunc<T2IAdapterConfigMetadata> = async (meta
|
|||||||
endStepPct: end_step_percent ?? initialT2IAdapter.endStepPct,
|
endStepPct: end_step_percent ?? initialT2IAdapter.endStepPct,
|
||||||
resizeMode: resize_mode ?? initialT2IAdapter.resizeMode,
|
resizeMode: resize_mode ?? initialT2IAdapter.resizeMode,
|
||||||
controlImage: image?.image_name ?? null,
|
controlImage: image?.image_name ?? null,
|
||||||
|
controlImageDimensions: null,
|
||||||
processedControlImage: processedImage?.image_name ?? null,
|
processedControlImage: processedImage?.image_name ?? null,
|
||||||
processorType,
|
processedControlImageDimensions: null,
|
||||||
processorNode,
|
processorNode,
|
||||||
|
processorType,
|
||||||
shouldAutoConfig: true,
|
shouldAutoConfig: true,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,14 @@ import {
|
|||||||
ipAdaptersReset,
|
ipAdaptersReset,
|
||||||
t2iAdaptersReset,
|
t2iAdaptersReset,
|
||||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import {
|
||||||
|
heightChanged,
|
||||||
|
negativePrompt2Changed,
|
||||||
|
negativePromptChanged,
|
||||||
|
positivePrompt2Changed,
|
||||||
|
positivePromptChanged,
|
||||||
|
widthChanged,
|
||||||
|
} from 'features/controlLayers/store/controlLayersSlice';
|
||||||
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';
|
||||||
@ -16,18 +24,14 @@ import type {
|
|||||||
} from 'features/metadata/types';
|
} from 'features/metadata/types';
|
||||||
import { modelSelected } from 'features/parameters/store/actions';
|
import { modelSelected } from 'features/parameters/store/actions';
|
||||||
import {
|
import {
|
||||||
heightRecalled,
|
|
||||||
initialImageChanged,
|
initialImageChanged,
|
||||||
setCfgRescaleMultiplier,
|
setCfgRescaleMultiplier,
|
||||||
setCfgScale,
|
setCfgScale,
|
||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setNegativePrompt,
|
|
||||||
setPositivePrompt,
|
|
||||||
setScheduler,
|
setScheduler,
|
||||||
setSeed,
|
setSeed,
|
||||||
setSteps,
|
setSteps,
|
||||||
vaeSelected,
|
vaeSelected,
|
||||||
widthRecalled,
|
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import type {
|
import type {
|
||||||
ParameterCFGRescaleMultiplier,
|
ParameterCFGRescaleMultiplier,
|
||||||
@ -53,8 +57,6 @@ import type {
|
|||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import {
|
import {
|
||||||
refinerModelChanged,
|
refinerModelChanged,
|
||||||
setNegativeStylePromptSDXL,
|
|
||||||
setPositiveStylePromptSDXL,
|
|
||||||
setRefinerCFGScale,
|
setRefinerCFGScale,
|
||||||
setRefinerNegativeAestheticScore,
|
setRefinerNegativeAestheticScore,
|
||||||
setRefinerPositiveAestheticScore,
|
setRefinerPositiveAestheticScore,
|
||||||
@ -65,19 +67,19 @@ import {
|
|||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
|
|
||||||
const recallPositivePrompt: MetadataRecallFunc<ParameterPositivePrompt> = (positivePrompt) => {
|
const recallPositivePrompt: MetadataRecallFunc<ParameterPositivePrompt> = (positivePrompt) => {
|
||||||
getStore().dispatch(setPositivePrompt(positivePrompt));
|
getStore().dispatch(positivePromptChanged(positivePrompt));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallNegativePrompt: MetadataRecallFunc<ParameterNegativePrompt> = (negativePrompt) => {
|
const recallNegativePrompt: MetadataRecallFunc<ParameterNegativePrompt> = (negativePrompt) => {
|
||||||
getStore().dispatch(setNegativePrompt(negativePrompt));
|
getStore().dispatch(negativePromptChanged(negativePrompt));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallSDXLPositiveStylePrompt: MetadataRecallFunc<ParameterPositiveStylePromptSDXL> = (positiveStylePrompt) => {
|
const recallSDXLPositiveStylePrompt: MetadataRecallFunc<ParameterPositiveStylePromptSDXL> = (positiveStylePrompt) => {
|
||||||
getStore().dispatch(setPositiveStylePromptSDXL(positiveStylePrompt));
|
getStore().dispatch(positivePrompt2Changed(positiveStylePrompt));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallSDXLNegativeStylePrompt: MetadataRecallFunc<ParameterNegativeStylePromptSDXL> = (negativeStylePrompt) => {
|
const recallSDXLNegativeStylePrompt: MetadataRecallFunc<ParameterNegativeStylePromptSDXL> = (negativeStylePrompt) => {
|
||||||
getStore().dispatch(setNegativeStylePromptSDXL(negativeStylePrompt));
|
getStore().dispatch(negativePrompt2Changed(negativeStylePrompt));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallSeed: MetadataRecallFunc<ParameterSeed> = (seed) => {
|
const recallSeed: MetadataRecallFunc<ParameterSeed> = (seed) => {
|
||||||
@ -101,11 +103,11 @@ const recallInitialImage: MetadataRecallFunc<ImageDTO> = async (imageDTO) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
|
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
|
||||||
getStore().dispatch(widthRecalled(width));
|
getStore().dispatch(widthChanged({ width, updateAspectRatio: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => {
|
const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => {
|
||||||
getStore().dispatch(heightRecalled(height));
|
getStore().dispatch(heightChanged({ height, updateAspectRatio: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => {
|
const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { getStore } from 'app/store/nanostores/store';
|
import { getStore } from 'app/store/nanostores/store';
|
||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { selectAllIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { selectAllIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
|
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
|
import { getRegionalPromptLayerBlobs } from 'features/controlLayers/util/getLayerBlobs';
|
||||||
import {
|
import {
|
||||||
IP_ADAPTER_COLLECT,
|
IP_ADAPTER_COLLECT,
|
||||||
NEGATIVE_CONDITIONING,
|
NEGATIVE_CONDITIONING,
|
||||||
@ -13,25 +15,23 @@ import {
|
|||||||
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
|
PROMPT_REGION_POSITIVE_COND_INVERTED_PREFIX,
|
||||||
PROMPT_REGION_POSITIVE_COND_PREFIX,
|
PROMPT_REGION_POSITIVE_COND_PREFIX,
|
||||||
} from 'features/nodes/util/graph/constants';
|
} from 'features/nodes/util/graph/constants';
|
||||||
import { isVectorMaskLayer } from 'features/regionalPrompts/store/regionalPromptsSlice';
|
|
||||||
import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs';
|
|
||||||
import { size } from 'lodash-es';
|
import { size } from 'lodash-es';
|
||||||
import { imagesApi } from 'services/api/endpoints/images';
|
import { imagesApi } from 'services/api/endpoints/images';
|
||||||
import type { CollectInvocation, Edge, IPAdapterInvocation, NonNullableGraph, S } from 'services/api/types';
|
import type { CollectInvocation, Edge, IPAdapterInvocation, NonNullableGraph, S } from 'services/api/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
|
|
||||||
export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
|
export const addControlLayersToGraph = async (state: RootState, graph: NonNullableGraph, denoiseNodeId: string) => {
|
||||||
if (!state.regionalPrompts.present.isEnabled) {
|
if (!state.controlLayers.present.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { dispatch } = getStore();
|
const { dispatch } = getStore();
|
||||||
const isSDXL = state.generation.model?.base === 'sdxl';
|
const isSDXL = state.generation.model?.base === 'sdxl';
|
||||||
const layers = state.regionalPrompts.present.layers
|
const layers = state.controlLayers.present.layers
|
||||||
// Only support vector mask layers now
|
// Only support vector mask layers now
|
||||||
// TODO: Image masks
|
// TODO: Image masks
|
||||||
.filter(isVectorMaskLayer)
|
.filter(isRegionalGuidanceLayer)
|
||||||
// Only visible layers are rendered on the canvas
|
// Only visible layers are rendered on the canvas
|
||||||
.filter((l) => l.isVisible)
|
.filter((l) => l.isEnabled)
|
||||||
// Only layers with prompts get added to the graph
|
// Only layers with prompts get added to the graph
|
||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
const hasTextPrompt = Boolean(l.positivePrompt || l.negativePrompt);
|
||||||
@ -39,12 +39,15 @@ export const addRegionalPromptsToGraph = async (state: RootState, graph: NonNull
|
|||||||
return hasTextPrompt || hasIPAdapter;
|
return hasTextPrompt || hasIPAdapter;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collect all IP Adapter ids for IP adapter layers
|
||||||
|
const layerIPAdapterIds = layers.flatMap((l) => l.ipAdapterIds);
|
||||||
|
|
||||||
const regionalIPAdapters = selectAllIPAdapters(state.controlAdapters).filter(
|
const regionalIPAdapters = selectAllIPAdapters(state.controlAdapters).filter(
|
||||||
({ id, model, controlImage, isEnabled }) => {
|
({ id, model, controlImage, isEnabled }) => {
|
||||||
const hasModel = Boolean(model);
|
const hasModel = Boolean(model);
|
||||||
const doesBaseMatch = model?.base === state.generation.model?.base;
|
const doesBaseMatch = model?.base === state.generation.model?.base;
|
||||||
const hasControlImage = controlImage;
|
const hasControlImage = controlImage;
|
||||||
const isRegional = layers.some((l) => l.ipAdapterIds.includes(id));
|
const isRegional = layerIPAdapterIds.includes(id);
|
||||||
return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional;
|
return isEnabled && hasModel && doesBaseMatch && hasControlImage && isRegional;
|
||||||
}
|
}
|
||||||
);
|
);
|
@ -1,7 +1,10 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { selectValidControlNets } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types';
|
import type { ControlAdapterProcessorType, ControlNetConfig } from 'features/controlAdapters/store/types';
|
||||||
|
import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ImageField } from 'features/nodes/types/common';
|
import type { ImageField } from 'features/nodes/types/common';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { differenceWith, intersectionWith } from 'lodash-es';
|
||||||
import type {
|
import type {
|
||||||
CollectInvocation,
|
CollectInvocation,
|
||||||
ControlNetInvocation,
|
ControlNetInvocation,
|
||||||
@ -14,11 +17,8 @@ import { assert } from 'tsafe';
|
|||||||
import { CONTROL_NET_COLLECT } from './constants';
|
import { CONTROL_NET_COLLECT } from './constants';
|
||||||
import { upsertMetadata } from './metadata';
|
import { upsertMetadata } from './metadata';
|
||||||
|
|
||||||
export const addControlNetToLinearGraph = async (
|
const getControlNets = (state: RootState) => {
|
||||||
state: RootState,
|
// Start with the valid controlnets
|
||||||
graph: NonNullableGraph,
|
|
||||||
baseNodeId: string
|
|
||||||
): Promise<void> => {
|
|
||||||
const validControlNets = selectValidControlNets(state.controlAdapters).filter(
|
const validControlNets = selectValidControlNets(state.controlAdapters).filter(
|
||||||
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
|
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
|
||||||
const hasModel = Boolean(model);
|
const hasModel = Boolean(model);
|
||||||
@ -29,9 +29,37 @@ export const addControlNetToLinearGraph = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
|
||||||
|
// accordion. We need to filter the list of valid T2I adapters according to the tab.
|
||||||
|
const activeTabName = activeTabNameSelector(state);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
// Add only the cnets that are used in control layers
|
||||||
|
// Collect all ControlNet ids for enabled ControlNet layers
|
||||||
|
const layerControlNetIds = state.controlLayers.present.layers
|
||||||
|
.filter(isControlAdapterLayer)
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
|
.map((l) => l.controlNetId);
|
||||||
|
return intersectionWith(validControlNets, layerControlNetIds, (a, b) => a.id === b);
|
||||||
|
} else {
|
||||||
|
// Else, we want to exclude the cnets that are used in control layers
|
||||||
|
// Collect all ControlNet ids for all ControlNet layers
|
||||||
|
const layerControlNetIds = state.controlLayers.present.layers
|
||||||
|
.filter(isControlAdapterLayer)
|
||||||
|
.map((l) => l.controlNetId);
|
||||||
|
return differenceWith(validControlNets, layerControlNetIds, (a, b) => a.id === b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addControlNetToLinearGraph = async (
|
||||||
|
state: RootState,
|
||||||
|
graph: NonNullableGraph,
|
||||||
|
baseNodeId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const controlNets = getControlNets(state);
|
||||||
const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
|
const controlNetMetadata: CoreMetadataInvocation['controlnets'] = [];
|
||||||
|
|
||||||
if (validControlNets.length) {
|
if (controlNets.length) {
|
||||||
// Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect
|
// Even though denoise_latents' control input is collection or scalar, keep it simple and always use a collect
|
||||||
const controlNetIterateNode: CollectInvocation = {
|
const controlNetIterateNode: CollectInvocation = {
|
||||||
id: CONTROL_NET_COLLECT,
|
id: CONTROL_NET_COLLECT,
|
||||||
@ -47,7 +75,7 @@ export const addControlNetToLinearGraph = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const controlNet of validControlNets) {
|
for (const controlNet of controlNets) {
|
||||||
if (!controlNet.model) {
|
if (!controlNet.model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -110,10 +110,9 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void =
|
|||||||
|
|
||||||
const { vae, seamlessXAxis, seamlessYAxis } = state.generation;
|
const { vae, seamlessXAxis, seamlessYAxis } = state.generation;
|
||||||
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
|
const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
const isAutoVae = !vae;
|
const isAutoVae = !vae;
|
||||||
const isSeamlessEnabled = seamlessXAxis || seamlessYAxis;
|
const isSeamlessEnabled = seamlessXAxis || seamlessYAxis;
|
||||||
const width = state.generation.width;
|
|
||||||
const height = state.generation.height;
|
|
||||||
const optimalDimension = selectOptimalDimension(state);
|
const optimalDimension = selectOptimalDimension(state);
|
||||||
const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height);
|
const { newWidth: hrfWidth, newHeight: hrfHeight } = calculateHrfRes(optimalDimension, width, height);
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { selectValidIPAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { IPAdapterConfig } from 'features/controlAdapters/store/types';
|
import type { IPAdapterConfig } from 'features/controlAdapters/store/types';
|
||||||
|
import { isIPAdapterLayer, isRegionalGuidanceLayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ImageField } from 'features/nodes/types/common';
|
import type { ImageField } from 'features/nodes/types/common';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { differenceWith, intersectionWith } from 'lodash-es';
|
||||||
import type {
|
import type {
|
||||||
CollectInvocation,
|
CollectInvocation,
|
||||||
CoreMetadataInvocation,
|
CoreMetadataInvocation,
|
||||||
@ -14,21 +17,50 @@ import { assert } from 'tsafe';
|
|||||||
import { IP_ADAPTER_COLLECT } from './constants';
|
import { IP_ADAPTER_COLLECT } from './constants';
|
||||||
import { upsertMetadata } from './metadata';
|
import { upsertMetadata } from './metadata';
|
||||||
|
|
||||||
|
const getIPAdapters = (state: RootState) => {
|
||||||
|
// Start with the valid IP adapters
|
||||||
|
const validIPAdapters = selectValidIPAdapters(state.controlAdapters).filter(({ model, controlImage, isEnabled }) => {
|
||||||
|
const hasModel = Boolean(model);
|
||||||
|
const doesBaseMatch = model?.base === state.generation.model?.base;
|
||||||
|
const hasControlImage = controlImage;
|
||||||
|
return isEnabled && hasModel && doesBaseMatch && hasControlImage;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masked IP adapters are handled in the graph helper for regional control - skip them here
|
||||||
|
const maskedIPAdapterIds = state.controlLayers.present.layers
|
||||||
|
.filter(isRegionalGuidanceLayer)
|
||||||
|
.map((l) => l.ipAdapterIds)
|
||||||
|
.flat();
|
||||||
|
const nonMaskedIPAdapters = differenceWith(validIPAdapters, maskedIPAdapterIds, (a, b) => a.id === b);
|
||||||
|
|
||||||
|
// txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
|
||||||
|
// accordion. We need to filter the list of valid IP adapters according to the tab.
|
||||||
|
const activeTabName = activeTabNameSelector(state);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
// If we are on the t2i tab, we only want to add the IP adapters that are used in unmasked IP Adapter layers
|
||||||
|
// Collect all IP Adapter ids for enabled IP adapter layers
|
||||||
|
const layerIPAdapterIds = state.controlLayers.present.layers
|
||||||
|
.filter(isIPAdapterLayer)
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
|
.map((l) => l.ipAdapterId);
|
||||||
|
return intersectionWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b);
|
||||||
|
} else {
|
||||||
|
// Else, we want to exclude the IP adapters that are used in IP Adapter layers
|
||||||
|
// Collect all IP Adapter ids for enabled IP adapter layers
|
||||||
|
const layerIPAdapterIds = state.controlLayers.present.layers.filter(isIPAdapterLayer).map((l) => l.ipAdapterId);
|
||||||
|
return differenceWith(nonMaskedIPAdapters, layerIPAdapterIds, (a, b) => a.id === b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const addIPAdapterToLinearGraph = async (
|
export const addIPAdapterToLinearGraph = async (
|
||||||
state: RootState,
|
state: RootState,
|
||||||
graph: NonNullableGraph,
|
graph: NonNullableGraph,
|
||||||
baseNodeId: string
|
baseNodeId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const validIPAdapters = selectValidIPAdapters(state.controlAdapters)
|
const ipAdapters = getIPAdapters(state);
|
||||||
.filter(({ model, controlImage, isEnabled }) => {
|
|
||||||
const hasModel = Boolean(model);
|
|
||||||
const doesBaseMatch = model?.base === state.generation.model?.base;
|
|
||||||
const hasControlImage = controlImage;
|
|
||||||
return isEnabled && hasModel && doesBaseMatch && hasControlImage;
|
|
||||||
})
|
|
||||||
.filter((ca) => !state.regionalPrompts.present.layers.some((l) => l.ipAdapterIds.includes(ca.id)));
|
|
||||||
|
|
||||||
if (validIPAdapters.length) {
|
if (ipAdapters.length) {
|
||||||
// Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect
|
// Even though denoise_latents' ip adapter input is collection or scalar, keep it simple and always use a collect
|
||||||
const ipAdapterCollectNode: CollectInvocation = {
|
const ipAdapterCollectNode: CollectInvocation = {
|
||||||
id: IP_ADAPTER_COLLECT,
|
id: IP_ADAPTER_COLLECT,
|
||||||
@ -46,7 +78,7 @@ export const addIPAdapterToLinearGraph = async (
|
|||||||
|
|
||||||
const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = [];
|
const ipAdapterMetdata: CoreMetadataInvocation['ipAdapters'] = [];
|
||||||
|
|
||||||
for (const ipAdapter of validIPAdapters) {
|
for (const ipAdapter of ipAdapters) {
|
||||||
if (!ipAdapter.model) {
|
if (!ipAdapter.model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import type { RootState } from 'app/store/store';
|
import type { RootState } from 'app/store/store';
|
||||||
import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
import { selectValidT2IAdapters } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||||
import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
import type { ControlAdapterProcessorType, T2IAdapterConfig } from 'features/controlAdapters/store/types';
|
||||||
|
import { isControlAdapterLayer } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ImageField } from 'features/nodes/types/common';
|
import type { ImageField } from 'features/nodes/types/common';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { differenceWith, intersectionWith } from 'lodash-es';
|
||||||
import type {
|
import type {
|
||||||
CollectInvocation,
|
CollectInvocation,
|
||||||
CoreMetadataInvocation,
|
CoreMetadataInvocation,
|
||||||
@ -14,11 +17,8 @@ import { assert } from 'tsafe';
|
|||||||
import { T2I_ADAPTER_COLLECT } from './constants';
|
import { T2I_ADAPTER_COLLECT } from './constants';
|
||||||
import { upsertMetadata } from './metadata';
|
import { upsertMetadata } from './metadata';
|
||||||
|
|
||||||
export const addT2IAdaptersToLinearGraph = async (
|
const getT2IAdapters = (state: RootState) => {
|
||||||
state: RootState,
|
// Start with the valid controlnets
|
||||||
graph: NonNullableGraph,
|
|
||||||
baseNodeId: string
|
|
||||||
): Promise<void> => {
|
|
||||||
const validT2IAdapters = selectValidT2IAdapters(state.controlAdapters).filter(
|
const validT2IAdapters = selectValidT2IAdapters(state.controlAdapters).filter(
|
||||||
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
|
({ model, processedControlImage, processorType, controlImage, isEnabled }) => {
|
||||||
const hasModel = Boolean(model);
|
const hasModel = Boolean(model);
|
||||||
@ -29,7 +29,35 @@ export const addT2IAdaptersToLinearGraph = async (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validT2IAdapters.length) {
|
// txt2img tab has special handling - it uses layers exclusively, while the other tabs use the older control adapters
|
||||||
|
// accordion. We need to filter the list of valid T2I adapters according to the tab.
|
||||||
|
const activeTabName = activeTabNameSelector(state);
|
||||||
|
|
||||||
|
if (activeTabName === 'txt2img') {
|
||||||
|
// Add only the T2Is that are used in control layers
|
||||||
|
// Collect all ids for enabled control adapter layers
|
||||||
|
const layerControlAdapterIds = state.controlLayers.present.layers
|
||||||
|
.filter(isControlAdapterLayer)
|
||||||
|
.filter((l) => l.isEnabled)
|
||||||
|
.map((l) => l.controlNetId);
|
||||||
|
return intersectionWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b);
|
||||||
|
} else {
|
||||||
|
// Else, we want to exclude the T2Is that are used in control layers
|
||||||
|
const layerControlAdapterIds = state.controlLayers.present.layers
|
||||||
|
.filter(isControlAdapterLayer)
|
||||||
|
.map((l) => l.controlNetId);
|
||||||
|
return differenceWith(validT2IAdapters, layerControlAdapterIds, (a, b) => a.id === b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addT2IAdaptersToLinearGraph = async (
|
||||||
|
state: RootState,
|
||||||
|
graph: NonNullableGraph,
|
||||||
|
baseNodeId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const t2iAdapters = getT2IAdapters(state);
|
||||||
|
|
||||||
|
if (t2iAdapters.length) {
|
||||||
// Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
|
// Even though denoise_latents' t2i adapter input is collection or scalar, keep it simple and always use a collect
|
||||||
const t2iAdapterCollectNode: CollectInvocation = {
|
const t2iAdapterCollectNode: CollectInvocation = {
|
||||||
id: T2I_ADAPTER_COLLECT,
|
id: T2I_ADAPTER_COLLECT,
|
||||||
@ -47,7 +75,7 @@ export const addT2IAdaptersToLinearGraph = async (
|
|||||||
|
|
||||||
const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = [];
|
const t2iAdapterMetadata: CoreMetadataInvocation['t2iAdapters'] = [];
|
||||||
|
|
||||||
for (const t2iAdapter of validT2IAdapters) {
|
for (const t2iAdapter of t2iAdapters) {
|
||||||
if (!t2iAdapter.model) {
|
if (!t2iAdapter.model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,6 @@ export const buildCanvasImageToImageGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -57,6 +55,7 @@ export const buildCanvasImageToImageGraph = async (
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
@ -47,8 +47,6 @@ export const buildCanvasInpaintGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -66,6 +64,7 @@ export const buildCanvasInpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
log.error('No model found in state');
|
log.error('No model found in state');
|
||||||
|
@ -51,8 +51,6 @@ export const buildCanvasOutpaintGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -78,6 +76,7 @@ export const buildCanvasOutpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
log.error('No model found in state');
|
log.error('No model found in state');
|
||||||
|
@ -43,8 +43,6 @@ export const buildCanvasSDXLImageToImageGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -57,6 +55,7 @@ export const buildCanvasSDXLImageToImageGraph = async (
|
|||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
img2imgStrength: strength,
|
img2imgStrength: strength,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
|
@ -48,8 +48,6 @@ export const buildCanvasSDXLInpaintGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -66,6 +64,7 @@ export const buildCanvasSDXLInpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
|
@ -52,8 +52,6 @@ export const buildCanvasSDXLOutpaintGraph = async (
|
|||||||
): Promise<NonNullableGraph> => {
|
): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -78,6 +76,7 @@ export const buildCanvasSDXLOutpaintGraph = async (
|
|||||||
canvasCoherenceEdgeSize,
|
canvasCoherenceEdgeSize,
|
||||||
maskBlur,
|
maskBlur,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
|
@ -33,8 +33,6 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -46,6 +44,7 @@ export const buildCanvasSDXLTextToImageGraph = async (state: RootState): Promise
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
@ -32,8 +32,6 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildCanvasTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildCanvasTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -46,6 +44,7 @@ export const buildCanvasTextToImageGraph = async (state: RootState): Promise<Non
|
|||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
|
||||||
// The bounding box determines width and height, not the width and height params
|
// The bounding box determines width and height, not the width and height params
|
||||||
const { width, height } = state.canvas.boundingBoxDimensions;
|
const { width, height } = state.canvas.boundingBoxDimensions;
|
||||||
|
@ -10,7 +10,7 @@ import { getHasMetadata, removeMetadata } from './metadata';
|
|||||||
|
|
||||||
export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => {
|
export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, prepend: boolean): BatchConfig => {
|
||||||
const { iterations, model, shouldRandomizeSeed, seed } = state.generation;
|
const { iterations, model, shouldRandomizeSeed, seed } = state.generation;
|
||||||
const { shouldConcatSDXLStylePrompt } = state.sdxl;
|
const { shouldConcatPrompts } = state.controlLayers.present;
|
||||||
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
const { prompts, seedBehaviour } = state.dynamicPrompts;
|
||||||
|
|
||||||
const data: Batch['data'] = [];
|
const data: Batch['data'] = [];
|
||||||
@ -105,7 +105,7 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldConcatSDXLStylePrompt && model?.base === 'sdxl') {
|
if (shouldConcatPrompts && model?.base === 'sdxl') {
|
||||||
if (graph.nodes[POSITIVE_CONDITIONING]) {
|
if (graph.nodes[POSITIVE_CONDITIONING]) {
|
||||||
firstBatchDatumList.push({
|
firstBatchDatumList.push({
|
||||||
node_path: POSITIVE_CONDITIONING,
|
node_path: POSITIVE_CONDITIONING,
|
||||||
|
@ -38,8 +38,6 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildLinearImageToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildLinearImageToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -49,14 +47,14 @@ export const buildLinearImageToImageGraph = async (state: RootState): Promise<No
|
|||||||
initialImage,
|
initialImage,
|
||||||
img2imgStrength: strength,
|
img2imgStrength: strength,
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
width,
|
|
||||||
height,
|
|
||||||
clipSkip,
|
clipSkip,
|
||||||
shouldUseCpuNoise,
|
shouldUseCpuNoise,
|
||||||
vaePrecision,
|
vaePrecision,
|
||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
* The easiest way to build linear graphs is to do it in the node editor, then copy and paste the
|
||||||
|
@ -39,8 +39,6 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
@ -49,14 +47,14 @@ export const buildLinearSDXLImageToImageGraph = async (state: RootState): Promis
|
|||||||
steps,
|
steps,
|
||||||
initialImage,
|
initialImage,
|
||||||
shouldFitToWidthHeight,
|
shouldFitToWidthHeight,
|
||||||
width,
|
|
||||||
height,
|
|
||||||
shouldUseCpuNoise,
|
shouldUseCpuNoise,
|
||||||
vaePrecision,
|
vaePrecision,
|
||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
img2imgStrength: strength,
|
img2imgStrength: strength,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { addRegionalPromptsToGraph } from 'features/nodes/util/graph/addRegionalPromptsToGraph';
|
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
||||||
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
import { isNonRefinerMainModelConfig, type NonNullableGraph } from 'services/api/types';
|
||||||
|
|
||||||
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
import { addControlNetToLinearGraph } from './addControlNetToLinearGraph';
|
||||||
@ -30,21 +30,19 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
scheduler,
|
scheduler,
|
||||||
seed,
|
seed,
|
||||||
steps,
|
steps,
|
||||||
width,
|
|
||||||
height,
|
|
||||||
shouldUseCpuNoise,
|
shouldUseCpuNoise,
|
||||||
vaePrecision,
|
vaePrecision,
|
||||||
seamlessXAxis,
|
seamlessXAxis,
|
||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
const { refinerModel, refinerStart } = state.sdxl;
|
const { refinerModel, refinerStart } = state.sdxl;
|
||||||
|
|
||||||
@ -274,7 +272,7 @@ export const buildLinearSDXLTextToImageGraph = async (state: RootState): Promise
|
|||||||
|
|
||||||
await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
|
await addT2IAdaptersToLinearGraph(state, graph, SDXL_DENOISE_LATENTS);
|
||||||
|
|
||||||
await addRegionalPromptsToGraph(state, graph, SDXL_DENOISE_LATENTS);
|
await addControlLayersToGraph(state, graph, SDXL_DENOISE_LATENTS);
|
||||||
|
|
||||||
// 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) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
import { fetchModelConfigWithTypeGuard } from 'features/metadata/util/modelFetchingHelpers';
|
||||||
import { addRegionalPromptsToGraph } from 'features/nodes/util/graph/addRegionalPromptsToGraph';
|
import { addControlLayersToGraph } from 'features/nodes/util/graph/addControlLayersToGraph';
|
||||||
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';
|
||||||
|
|
||||||
@ -30,15 +30,11 @@ import { addCoreMetadataNode, getModelMetadataField } from './metadata';
|
|||||||
export const buildLinearTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
export const buildLinearTextToImageGraph = async (state: RootState): Promise<NonNullableGraph> => {
|
||||||
const log = logger('nodes');
|
const log = logger('nodes');
|
||||||
const {
|
const {
|
||||||
positivePrompt,
|
|
||||||
negativePrompt,
|
|
||||||
model,
|
model,
|
||||||
cfgScale: cfg_scale,
|
cfgScale: cfg_scale,
|
||||||
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
cfgRescaleMultiplier: cfg_rescale_multiplier,
|
||||||
scheduler,
|
scheduler,
|
||||||
steps,
|
steps,
|
||||||
width,
|
|
||||||
height,
|
|
||||||
clipSkip,
|
clipSkip,
|
||||||
shouldUseCpuNoise,
|
shouldUseCpuNoise,
|
||||||
vaePrecision,
|
vaePrecision,
|
||||||
@ -46,6 +42,8 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise<Non
|
|||||||
seamlessYAxis,
|
seamlessYAxis,
|
||||||
seed,
|
seed,
|
||||||
} = state.generation;
|
} = state.generation;
|
||||||
|
const { positivePrompt, negativePrompt } = state.controlLayers.present;
|
||||||
|
const { width, height } = state.controlLayers.present.size;
|
||||||
|
|
||||||
const use_cpu = shouldUseCpuNoise;
|
const use_cpu = shouldUseCpuNoise;
|
||||||
|
|
||||||
@ -256,7 +254,7 @@ export const buildLinearTextToImageGraph = async (state: RootState): Promise<Non
|
|||||||
|
|
||||||
await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS);
|
await addT2IAdaptersToLinearGraph(state, graph, DENOISE_LATENTS);
|
||||||
|
|
||||||
await addRegionalPromptsToGraph(state, graph, DENOISE_LATENTS);
|
await addControlLayersToGraph(state, graph, DENOISE_LATENTS);
|
||||||
|
|
||||||
// High resolution fix.
|
// High resolution fix.
|
||||||
if (state.hrf.hrfEnabled) {
|
if (state.hrf.hrfEnabled) {
|
||||||
|
@ -17,12 +17,12 @@ export const getBoardField = (state: RootState): BoardField | undefined => {
|
|||||||
* Gets the SDXL style prompts, based on the concat setting.
|
* Gets the SDXL style prompts, based on the concat setting.
|
||||||
*/
|
*/
|
||||||
export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => {
|
export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => {
|
||||||
const { positivePrompt, negativePrompt } = state.generation;
|
const { positivePrompt, negativePrompt, positivePrompt2, negativePrompt2, shouldConcatPrompts } =
|
||||||
const { positiveStylePrompt, negativeStylePrompt, shouldConcatSDXLStylePrompt } = state.sdxl;
|
state.controlLayers.present;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
positiveStylePrompt: shouldConcatSDXLStylePrompt ? positivePrompt : positiveStylePrompt,
|
positiveStylePrompt: shouldConcatPrompts ? positivePrompt : positivePrompt2,
|
||||||
negativeStylePrompt: shouldConcatSDXLStylePrompt ? negativePrompt : negativeStylePrompt,
|
negativeStylePrompt: shouldConcatPrompts ? negativePrompt : negativePrompt2,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { negativePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { setNegativePrompt } from 'features/parameters/store/generationSlice';
|
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
@ -10,12 +10,12 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
export const ParamNegativePrompt = memo(() => {
|
export const ParamNegativePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.generation.negativePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.negativePrompt);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const _onChange = useCallback(
|
const _onChange = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
dispatch(setNegativePrompt(v));
|
dispatch(negativePromptChanged(v));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Box, Textarea } from '@invoke-ai/ui-library';
|
import { Box, Textarea } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
|
import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton';
|
||||||
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
|
||||||
import { setPositivePrompt } from 'features/parameters/store/generationSlice';
|
|
||||||
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
|
||||||
import { PromptPopover } from 'features/prompt/PromptPopover';
|
import { PromptPopover } from 'features/prompt/PromptPopover';
|
||||||
import { usePrompt } from 'features/prompt/usePrompt';
|
import { usePrompt } from 'features/prompt/usePrompt';
|
||||||
@ -14,14 +14,14 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
export const ParamPositivePrompt = memo(() => {
|
export const ParamPositivePrompt = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const prompt = useAppSelector((s) => s.generation.positivePrompt);
|
const prompt = useAppSelector((s) => s.controlLayers.present.positivePrompt);
|
||||||
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
const baseModel = useAppSelector((s) => s.generation.model)?.base;
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
dispatch(setPositivePrompt(v));
|
dispatch(positivePromptChanged(v));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
|
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const AspectRatioCanvasPreview = memo(() => {
|
export const AspectRatioCanvasPreview = memo(() => {
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { roundToMultiple } from 'common/util/roundDownToMultiple';
|
|
||||||
import { isAnyControlAdapterAdded } from 'features/controlAdapters/store/controlAdaptersSlice';
|
|
||||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
|
||||||
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
import { initialAspectRatioState } from 'features/parameters/components/ImageSize/constants';
|
||||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
|
||||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||||
import type {
|
import type {
|
||||||
ParameterCanvasCoherenceMode,
|
ParameterCanvasCoherenceMode,
|
||||||
@ -16,7 +12,7 @@ import type {
|
|||||||
ParameterScheduler,
|
ParameterScheduler,
|
||||||
ParameterVAEModel,
|
ParameterVAEModel,
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
@ -28,12 +24,9 @@ const initialGenerationState: GenerationState = {
|
|||||||
_version: 2,
|
_version: 2,
|
||||||
cfgScale: 7.5,
|
cfgScale: 7.5,
|
||||||
cfgRescaleMultiplier: 0,
|
cfgRescaleMultiplier: 0,
|
||||||
height: 512,
|
|
||||||
img2imgStrength: 0.75,
|
img2imgStrength: 0.75,
|
||||||
infillMethod: 'patchmatch',
|
infillMethod: 'patchmatch',
|
||||||
iterations: 1,
|
iterations: 1,
|
||||||
positivePrompt: '',
|
|
||||||
negativePrompt: '',
|
|
||||||
scheduler: 'euler',
|
scheduler: 'euler',
|
||||||
maskBlur: 16,
|
maskBlur: 16,
|
||||||
maskBlurMethod: 'box',
|
maskBlurMethod: 'box',
|
||||||
@ -44,7 +37,6 @@ const initialGenerationState: GenerationState = {
|
|||||||
shouldFitToWidthHeight: true,
|
shouldFitToWidthHeight: true,
|
||||||
shouldRandomizeSeed: true,
|
shouldRandomizeSeed: true,
|
||||||
steps: 50,
|
steps: 50,
|
||||||
width: 512,
|
|
||||||
model: null,
|
model: null,
|
||||||
vae: null,
|
vae: null,
|
||||||
vaePrecision: 'fp32',
|
vaePrecision: 'fp32',
|
||||||
@ -53,7 +45,6 @@ const initialGenerationState: GenerationState = {
|
|||||||
clipSkip: 0,
|
clipSkip: 0,
|
||||||
shouldUseCpuNoise: true,
|
shouldUseCpuNoise: true,
|
||||||
shouldShowAdvancedOptions: false,
|
shouldShowAdvancedOptions: false,
|
||||||
aspectRatio: { ...initialAspectRatioState },
|
|
||||||
infillTileSize: 32,
|
infillTileSize: 32,
|
||||||
infillPatchmatchDownscaleSize: 1,
|
infillPatchmatchDownscaleSize: 1,
|
||||||
infillMosaicTileWidth: 64,
|
infillMosaicTileWidth: 64,
|
||||||
@ -67,12 +58,6 @@ export const generationSlice = createSlice({
|
|||||||
name: 'generation',
|
name: 'generation',
|
||||||
initialState: initialGenerationState,
|
initialState: initialGenerationState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setPositivePrompt: (state, action: PayloadAction<string>) => {
|
|
||||||
state.positivePrompt = action.payload;
|
|
||||||
},
|
|
||||||
setNegativePrompt: (state, action: PayloadAction<string>) => {
|
|
||||||
state.negativePrompt = action.payload;
|
|
||||||
},
|
|
||||||
setIterations: (state, action: PayloadAction<number>) => {
|
setIterations: (state, action: PayloadAction<number>) => {
|
||||||
state.iterations = action.payload;
|
state.iterations = action.payload;
|
||||||
},
|
},
|
||||||
@ -148,19 +133,6 @@ export const generationSlice = createSlice({
|
|||||||
const { maxClip } = CLIP_SKIP_MAP[newModel.base];
|
const { maxClip } = CLIP_SKIP_MAP[newModel.base];
|
||||||
state.clipSkip = clamp(state.clipSkip, 0, maxClip);
|
state.clipSkip = clamp(state.clipSkip, 0, maxClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.meta.previousModel?.base === newModel.base) {
|
|
||||||
// The base model hasn't changed, we don't need to optimize the size
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const optimalDimension = getOptimalDimension(newModel);
|
|
||||||
if (getIsSizeOptimal(state.width, state.height, optimalDimension)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { width, height } = calculateNewSize(state.aspectRatio.value, optimalDimension * optimalDimension);
|
|
||||||
state.width = width;
|
|
||||||
state.height = height;
|
|
||||||
},
|
},
|
||||||
prepare: (payload: ParameterModel | null, previousModel?: ParameterModel | null) => ({
|
prepare: (payload: ParameterModel | null, previousModel?: ParameterModel | null) => ({
|
||||||
payload,
|
payload,
|
||||||
@ -182,27 +154,6 @@ export const generationSlice = createSlice({
|
|||||||
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
shouldUseCpuNoiseChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldUseCpuNoise = action.payload;
|
state.shouldUseCpuNoise = action.payload;
|
||||||
},
|
},
|
||||||
widthChanged: (state, action: PayloadAction<number>) => {
|
|
||||||
state.width = action.payload;
|
|
||||||
},
|
|
||||||
heightChanged: (state, action: PayloadAction<number>) => {
|
|
||||||
state.height = action.payload;
|
|
||||||
},
|
|
||||||
widthRecalled: (state, action: PayloadAction<number>) => {
|
|
||||||
state.width = action.payload;
|
|
||||||
state.aspectRatio.value = action.payload / state.height;
|
|
||||||
state.aspectRatio.id = 'Free';
|
|
||||||
state.aspectRatio.isLocked = false;
|
|
||||||
},
|
|
||||||
heightRecalled: (state, action: PayloadAction<number>) => {
|
|
||||||
state.height = action.payload;
|
|
||||||
state.aspectRatio.value = state.width / action.payload;
|
|
||||||
state.aspectRatio.id = 'Free';
|
|
||||||
state.aspectRatio.isLocked = false;
|
|
||||||
},
|
|
||||||
aspectRatioChanged: (state, action: PayloadAction<AspectRatioState>) => {
|
|
||||||
state.aspectRatio = action.payload;
|
|
||||||
},
|
|
||||||
setInfillMethod: (state, action: PayloadAction<string>) => {
|
setInfillMethod: (state, action: PayloadAction<string>) => {
|
||||||
state.infillMethod = action.payload;
|
state.infillMethod = action.payload;
|
||||||
},
|
},
|
||||||
@ -237,15 +188,6 @@ export const generationSlice = createSlice({
|
|||||||
state.vaePrecision = action.payload.sd.vaePrecision;
|
state.vaePrecision = action.payload.sd.vaePrecision;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling
|
|
||||||
// factor than the UNet. Hopefully we get an upstream fix in diffusers.
|
|
||||||
builder.addMatcher(isAnyControlAdapterAdded, (state, action) => {
|
|
||||||
if (action.payload.type === 't2i_adapter') {
|
|
||||||
state.width = roundToMultiple(state.width, 64);
|
|
||||||
state.height = roundToMultiple(state.height, 64);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
selectOptimalDimension: (slice) => getOptimalDimension(slice.model),
|
selectOptimalDimension: (slice) => getOptimalDimension(slice.model),
|
||||||
@ -259,8 +201,6 @@ export const {
|
|||||||
setImg2imgStrength,
|
setImg2imgStrength,
|
||||||
setInfillMethod,
|
setInfillMethod,
|
||||||
setIterations,
|
setIterations,
|
||||||
setPositivePrompt,
|
|
||||||
setNegativePrompt,
|
|
||||||
setScheduler,
|
setScheduler,
|
||||||
setMaskBlur,
|
setMaskBlur,
|
||||||
setCanvasCoherenceMode,
|
setCanvasCoherenceMode,
|
||||||
@ -278,11 +218,6 @@ export const {
|
|||||||
setClipSkip,
|
setClipSkip,
|
||||||
shouldUseCpuNoiseChanged,
|
shouldUseCpuNoiseChanged,
|
||||||
vaePrecisionChanged,
|
vaePrecisionChanged,
|
||||||
aspectRatioChanged,
|
|
||||||
widthChanged,
|
|
||||||
heightChanged,
|
|
||||||
widthRecalled,
|
|
||||||
heightRecalled,
|
|
||||||
setInfillTileSize,
|
setInfillTileSize,
|
||||||
setInfillPatchmatchDownscaleSize,
|
setInfillPatchmatchDownscaleSize,
|
||||||
setInfillMosaicTileWidth,
|
setInfillMosaicTileWidth,
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
|
|
||||||
import type {
|
import type {
|
||||||
ParameterCanvasCoherenceMode,
|
ParameterCanvasCoherenceMode,
|
||||||
ParameterCFGRescaleMultiplier,
|
ParameterCFGRescaleMultiplier,
|
||||||
ParameterCFGScale,
|
ParameterCFGScale,
|
||||||
ParameterHeight,
|
|
||||||
ParameterMaskBlurMethod,
|
ParameterMaskBlurMethod,
|
||||||
ParameterModel,
|
ParameterModel,
|
||||||
ParameterNegativePrompt,
|
|
||||||
ParameterPositivePrompt,
|
|
||||||
ParameterPrecision,
|
ParameterPrecision,
|
||||||
ParameterScheduler,
|
ParameterScheduler,
|
||||||
ParameterSeed,
|
ParameterSeed,
|
||||||
ParameterSteps,
|
ParameterSteps,
|
||||||
ParameterStrength,
|
ParameterStrength,
|
||||||
ParameterVAEModel,
|
ParameterVAEModel,
|
||||||
ParameterWidth,
|
|
||||||
} from 'features/parameters/types/parameterSchemas';
|
} from 'features/parameters/types/parameterSchemas';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
|
|
||||||
@ -23,13 +18,10 @@ export interface GenerationState {
|
|||||||
_version: 2;
|
_version: 2;
|
||||||
cfgScale: ParameterCFGScale;
|
cfgScale: ParameterCFGScale;
|
||||||
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
||||||
height: ParameterHeight;
|
|
||||||
img2imgStrength: ParameterStrength;
|
img2imgStrength: ParameterStrength;
|
||||||
infillMethod: string;
|
infillMethod: string;
|
||||||
initialImage?: { imageName: string; width: number; height: number };
|
initialImage?: { imageName: string; width: number; height: number };
|
||||||
iterations: number;
|
iterations: number;
|
||||||
positivePrompt: ParameterPositivePrompt;
|
|
||||||
negativePrompt: ParameterNegativePrompt;
|
|
||||||
scheduler: ParameterScheduler;
|
scheduler: ParameterScheduler;
|
||||||
maskBlur: number;
|
maskBlur: number;
|
||||||
maskBlurMethod: ParameterMaskBlurMethod;
|
maskBlurMethod: ParameterMaskBlurMethod;
|
||||||
@ -40,7 +32,6 @@ export interface GenerationState {
|
|||||||
shouldFitToWidthHeight: boolean;
|
shouldFitToWidthHeight: boolean;
|
||||||
shouldRandomizeSeed: boolean;
|
shouldRandomizeSeed: boolean;
|
||||||
steps: ParameterSteps;
|
steps: ParameterSteps;
|
||||||
width: ParameterWidth;
|
|
||||||
model: ParameterModel | null;
|
model: ParameterModel | null;
|
||||||
vae: ParameterVAEModel | null;
|
vae: ParameterVAEModel | null;
|
||||||
vaePrecision: ParameterPrecision;
|
vaePrecision: ParameterPrecision;
|
||||||
@ -49,7 +40,6 @@ export interface GenerationState {
|
|||||||
clipSkip: number;
|
clipSkip: number;
|
||||||
shouldUseCpuNoise: boolean;
|
shouldUseCpuNoise: boolean;
|
||||||
shouldShowAdvancedOptions: boolean;
|
shouldShowAdvancedOptions: boolean;
|
||||||
aspectRatio: AspectRatioState;
|
|
||||||
infillTileSize: number;
|
infillTileSize: number;
|
||||||
infillPatchmatchDownscaleSize: number;
|
infillPatchmatchDownscaleSize: number;
|
||||||
infillMosaicTileWidth: number;
|
infillMosaicTileWidth: number;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user