mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-01 03:01:13 -04:00
Merge branch 'main' into ryan/flux-lora-quantized
This commit is contained in:
@@ -15,6 +15,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
PruneResult,
|
||||
SessionQueueCountsByDestination,
|
||||
SessionQueueItem,
|
||||
SessionQueueItemDTO,
|
||||
SessionQueueStatus,
|
||||
@@ -242,3 +243,18 @@ async def cancel_queue_item(
|
||||
"""Deletes a queue item"""
|
||||
|
||||
return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id)
|
||||
|
||||
|
||||
@session_queue_router.get(
|
||||
"/{queue_id}/counts_by_destination",
|
||||
operation_id="counts_by_destination",
|
||||
responses={200: {"model": SessionQueueCountsByDestination}},
|
||||
)
|
||||
async def counts_by_destination(
|
||||
queue_id: str = Path(description="The queue id to query"),
|
||||
destination: str = Query(description="The destination to query"),
|
||||
) -> SessionQueueCountsByDestination:
|
||||
"""Gets the counts of queue items by destination"""
|
||||
return ApiDependencies.invoker.services.session_queue.get_counts_by_destination(
|
||||
queue_id=queue_id, destination=destination
|
||||
)
|
||||
|
||||
@@ -129,7 +129,18 @@ class MergeMetadataInvocation(BaseInvocation):
|
||||
|
||||
|
||||
GENERATION_MODES = Literal[
|
||||
"txt2img", "img2img", "inpaint", "outpaint", "sdxl_txt2img", "sdxl_img2img", "sdxl_inpaint", "sdxl_outpaint"
|
||||
"txt2img",
|
||||
"img2img",
|
||||
"inpaint",
|
||||
"outpaint",
|
||||
"sdxl_txt2img",
|
||||
"sdxl_img2img",
|
||||
"sdxl_inpaint",
|
||||
"sdxl_outpaint",
|
||||
"flux_txt2img",
|
||||
"flux_img2img",
|
||||
"flux_inpaint",
|
||||
"flux_outpaint",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
IsEmptyResult,
|
||||
IsFullResult,
|
||||
PruneResult,
|
||||
SessionQueueCountsByDestination,
|
||||
SessionQueueItem,
|
||||
SessionQueueItemDTO,
|
||||
SessionQueueStatus,
|
||||
@@ -69,6 +70,11 @@ class SessionQueueBase(ABC):
|
||||
"""Gets the status of the queue"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_counts_by_destination(self, queue_id: str, destination: str) -> SessionQueueCountsByDestination:
|
||||
"""Gets the counts of queue items by destination"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_batch_status(self, queue_id: str, batch_id: str) -> BatchStatus:
|
||||
"""Gets the status of a batch"""
|
||||
|
||||
@@ -307,6 +307,17 @@ class SessionQueueStatus(BaseModel):
|
||||
total: int = Field(..., description="Total number of queue items")
|
||||
|
||||
|
||||
class SessionQueueCountsByDestination(BaseModel):
|
||||
queue_id: str = Field(..., description="The ID of the queue")
|
||||
destination: str = Field(..., description="The destination of queue items included in this status")
|
||||
pending: int = Field(..., description="Number of queue items with status 'pending' for the destination")
|
||||
in_progress: int = Field(..., description="Number of queue items with status 'in_progress' for the destination")
|
||||
completed: int = Field(..., description="Number of queue items with status 'complete' for the destination")
|
||||
failed: int = Field(..., description="Number of queue items with status 'error' for the destination")
|
||||
canceled: int = Field(..., description="Number of queue items with status 'canceled' for the destination")
|
||||
total: int = Field(..., description="Total number of queue items for the destination")
|
||||
|
||||
|
||||
class BatchStatus(BaseModel):
|
||||
queue_id: str = Field(..., description="The ID of the queue")
|
||||
batch_id: str = Field(..., description="The ID of the batch")
|
||||
|
||||
@@ -17,6 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
IsEmptyResult,
|
||||
IsFullResult,
|
||||
PruneResult,
|
||||
SessionQueueCountsByDestination,
|
||||
SessionQueueItem,
|
||||
SessionQueueItemDTO,
|
||||
SessionQueueItemNotFoundError,
|
||||
@@ -692,3 +693,37 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
canceled=counts.get("canceled", 0),
|
||||
total=total,
|
||||
)
|
||||
|
||||
def get_counts_by_destination(self, queue_id: str, destination: str) -> SessionQueueCountsByDestination:
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
self.__cursor.execute(
|
||||
"""--sql
|
||||
SELECT status, count(*)
|
||||
FROM session_queue
|
||||
WHERE queue_id = ?
|
||||
AND destination = ?
|
||||
GROUP BY status
|
||||
""",
|
||||
(queue_id, destination),
|
||||
)
|
||||
counts_result = cast(list[sqlite3.Row], self.__cursor.fetchall())
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
total = sum(row[1] for row in counts_result)
|
||||
counts: dict[str, int] = {row[0]: row[1] for row in counts_result}
|
||||
|
||||
return SessionQueueCountsByDestination(
|
||||
queue_id=queue_id,
|
||||
destination=destination,
|
||||
pending=counts.get("pending", 0),
|
||||
in_progress=counts.get("in_progress", 0),
|
||||
completed=counts.get("completed", 0),
|
||||
failed=counts.get("failed", 0),
|
||||
canceled=counts.get("canceled", 0),
|
||||
total=total,
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ def denoise(
|
||||
|
||||
if inpaint_extension is not None:
|
||||
img = inpaint_extension.merge_intermediate_latents_with_init_latents(img, t_prev)
|
||||
preview_img = inpaint_extension.merge_intermediate_latents_with_init_latents(preview_img, 0.0)
|
||||
|
||||
step_callback(
|
||||
PipelineIntermediateState(
|
||||
|
||||
@@ -214,8 +214,14 @@ class LineartEdgeDetector:
|
||||
line = line.cpu().numpy()
|
||||
line = (line * 255.0).clip(0, 255).astype(np.uint8)
|
||||
|
||||
detected_map = line
|
||||
detected_map = 255 - line
|
||||
|
||||
detected_map = 255 - detected_map
|
||||
# The lineart model often outputs a lot of almost-black noise. SD1.5 ControlNets seem to be OK with this, but
|
||||
# SDXL ControlNets are not - they need a cleaner map. 12 was experimentally determined to be a good threshold,
|
||||
# eliminating all the noise while keeping the actual edges. Other approaches to thresholding may be better,
|
||||
# for example stretching the contrast or removing noise.
|
||||
detected_map[detected_map < 12] = 0
|
||||
|
||||
return np_to_pil(detected_map)
|
||||
output = np_to_pil(detected_map)
|
||||
|
||||
return output
|
||||
|
||||
@@ -260,8 +260,14 @@ class LineartAnimeEdgeDetector:
|
||||
line = cv2.resize(line, (width, height), interpolation=cv2.INTER_CUBIC)
|
||||
line = line.clip(0, 255).astype(np.uint8)
|
||||
|
||||
detected_map = line
|
||||
detected_map = 255 - detected_map
|
||||
detected_map = 255 - line
|
||||
|
||||
# The lineart model often outputs a lot of almost-black noise. SD1.5 ControlNets seem to be OK with this, but
|
||||
# SDXL ControlNets are not - they need a cleaner map. 12 was experimentally determined to be a good threshold,
|
||||
# eliminating all the noise while keeping the actual edges. Other approaches to thresholding may be better,
|
||||
# for example stretching the contrast or removing noise.
|
||||
detected_map[detected_map < 12] = 0
|
||||
|
||||
output = np_to_pil(detected_map)
|
||||
|
||||
return output
|
||||
|
||||
@@ -173,102 +173,11 @@
|
||||
"comparing": "Comparing",
|
||||
"comparingDesc": "Comparing two images",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"controlnet": {
|
||||
"controlAdapter_one": "Control Adapter",
|
||||
"controlAdapter_other": "Control Adapters",
|
||||
"controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))",
|
||||
"ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))",
|
||||
"t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))",
|
||||
"addControlNet": "Add $t(common.controlNet)",
|
||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||
"addT2IAdapter": "Add $t(common.t2iAdapter)",
|
||||
"amult": "a_mult",
|
||||
"autoConfigure": "Auto configure processor",
|
||||
"balanced": "Balanced",
|
||||
"base": "Base",
|
||||
"beginEndStepPercent": "Begin / End Step Percentage",
|
||||
"beginEndStepPercentShort": "Begin/End %",
|
||||
"bgth": "bg_th",
|
||||
"canny": "Canny",
|
||||
"cannyDescription": "Canny edge detection",
|
||||
"colorMap": "Color",
|
||||
"colorMapDescription": "Generates a color map from the image",
|
||||
"coarse": "Coarse",
|
||||
"contentShuffle": "Content Shuffle",
|
||||
"contentShuffleDescription": "Shuffles the content in an image",
|
||||
"control": "Control",
|
||||
"controlMode": "Control Mode",
|
||||
"crop": "Crop",
|
||||
"delete": "Delete",
|
||||
"depthAnything": "Depth Anything",
|
||||
"depthAnythingDescription": "Depth map generation using the Depth Anything technique",
|
||||
"depthAnythingSmallV2": "Small V2",
|
||||
"depthMidas": "Depth (Midas)",
|
||||
"depthMidasDescription": "Depth map generation using Midas",
|
||||
"depthZoe": "Depth (Zoe)",
|
||||
"depthZoeDescription": "Depth map generation using Zoe",
|
||||
"detectResolution": "Detect Resolution",
|
||||
"duplicate": "Duplicate",
|
||||
"f": "F",
|
||||
"fill": "Fill",
|
||||
"h": "H",
|
||||
"face": "Face",
|
||||
"body": "Body",
|
||||
"hands": "Hands",
|
||||
"hed": "HED",
|
||||
"hedDescription": "Holistically-Nested Edge Detection",
|
||||
"hideAdvanced": "Hide Advanced",
|
||||
"highThreshold": "High Threshold",
|
||||
"imageResolution": "Image Resolution",
|
||||
"colorMapTileSize": "Tile Size",
|
||||
"importImageFromCanvas": "Import Image From Canvas",
|
||||
"importMaskFromCanvas": "Import Mask From Canvas",
|
||||
"large": "Large",
|
||||
"lineart": "Lineart",
|
||||
"lineartAnime": "Lineart Anime",
|
||||
"lineartAnimeDescription": "Anime-style lineart processing",
|
||||
"lineartDescription": "Converts image to lineart",
|
||||
"lowThreshold": "Low Threshold",
|
||||
"maxFaces": "Max Faces",
|
||||
"mediapipeFace": "Mediapipe Face",
|
||||
"mediapipeFaceDescription": "Face detection using Mediapipe",
|
||||
"megaControl": "Mega Control",
|
||||
"minConfidence": "Min Confidence",
|
||||
"mlsd": "M-LSD",
|
||||
"mlsdDescription": "Minimalist Line Segment Detector",
|
||||
"modelSize": "Model Size",
|
||||
"disabled": "Disabled",
|
||||
"placeholderSelectAModel": "Select a model",
|
||||
"reset": "Reset",
|
||||
"none": "None",
|
||||
"noneDescription": "No processing applied",
|
||||
"normalBae": "Normal BAE",
|
||||
"normalBaeDescription": "Normal BAE processing",
|
||||
"dwOpenpose": "DW Openpose",
|
||||
"dwOpenposeDescription": "Human pose estimation using DW Openpose",
|
||||
"pidi": "PIDI",
|
||||
"pidiDescription": "PIDI image processing",
|
||||
"processor": "Processor",
|
||||
"prompt": "Prompt",
|
||||
"resetControlImage": "Reset Control Image",
|
||||
"resize": "Resize",
|
||||
"resizeSimple": "Resize (Simple)",
|
||||
"resizeMode": "Resize Mode",
|
||||
"ipAdapterMethod": "Method",
|
||||
"full": "Full",
|
||||
"style": "Style Only",
|
||||
"composition": "Composition Only",
|
||||
"safe": "Safe",
|
||||
"saveControlImage": "Save Control Image",
|
||||
"scribble": "Scribble",
|
||||
"selectModel": "Select a model",
|
||||
"selectCLIPVisionModel": "Select a CLIP Vision model",
|
||||
"setControlImageDimensions": "Copy size to W/H (optimize for model)",
|
||||
"setControlImageDimensionsForce": "Copy size to W/H (ignore model)",
|
||||
"showAdvanced": "Show Advanced",
|
||||
"small": "Small",
|
||||
"toggleControlNet": "Toggle this ControlNet",
|
||||
"w": "W",
|
||||
"weight": "Weight"
|
||||
"new": "New"
|
||||
},
|
||||
"hrf": {
|
||||
"hrf": "High Resolution Fix",
|
||||
@@ -315,7 +224,7 @@
|
||||
"cancelItem": "Cancel Item",
|
||||
"cancelBatchSucceeded": "Batch Canceled",
|
||||
"cancelBatchFailed": "Problem Canceling Batch",
|
||||
"clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely.",
|
||||
"clearQueueAlertDialog": "Clearing the queue immediately cancels any processing items and clears the queue entirely. Pending filters will be canceled.",
|
||||
"clearQueueAlertDialog2": "Are you sure you want to clear the queue?",
|
||||
"current": "Current",
|
||||
"next": "Next",
|
||||
@@ -998,6 +907,7 @@
|
||||
"downloadImage": "Download Image",
|
||||
"general": "General",
|
||||
"globalSettings": "Global Settings",
|
||||
"guidance": "Guidance",
|
||||
"height": "Height",
|
||||
"imageFit": "Fit Initial Image To Output Size",
|
||||
"images": "Images",
|
||||
@@ -1020,6 +930,9 @@
|
||||
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
|
||||
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
|
||||
"noModelSelected": "No model selected",
|
||||
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
|
||||
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
|
||||
"noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation",
|
||||
"canvasManagerNotLoaded": "Canvas Manager not loaded",
|
||||
"canvasIsFiltering": "Canvas is filtering",
|
||||
"canvasIsTransforming": "Canvas is transforming",
|
||||
@@ -1168,6 +1081,8 @@
|
||||
"importFailed": "Import Failed",
|
||||
"importSuccessful": "Import Successful",
|
||||
"invalidUpload": "Invalid Upload",
|
||||
"layerCopiedToClipboard": "Layer Copied to Clipboard",
|
||||
"layerSavedToAssets": "Layer Saved to Assets",
|
||||
"loadedWithWarnings": "Workflow Loaded with Warnings",
|
||||
"maskSavedAssets": "Mask Saved to Assets",
|
||||
"maskSentControlnetAssets": "Mask Sent to ControlNet & Assets",
|
||||
@@ -1188,6 +1103,8 @@
|
||||
"problemCopyingCanvas": "Problem Copying Canvas",
|
||||
"problemCopyingCanvasDesc": "Unable to export base layer",
|
||||
"problemCopyingImage": "Unable to Copy Image",
|
||||
"problemCopyingLayer": "Unable to Copy Layer",
|
||||
"problemSavingLayer": "Unable to Save Layer",
|
||||
"problemDownloadingImage": "Unable to Download Image",
|
||||
"problemDownloadingCanvas": "Problem Downloading Canvas",
|
||||
"problemDownloadingCanvasDesc": "Unable to export base layer",
|
||||
@@ -1387,6 +1304,13 @@
|
||||
"High CFG Scale values can result in over-saturation and distorted generation results. "
|
||||
]
|
||||
},
|
||||
"paramGuidance": {
|
||||
"heading": "Guidance",
|
||||
"paragraphs": [
|
||||
"Controls how much the prompt influences the generation process.",
|
||||
"High guidance values can result in over-saturation and high or low guidance may result in distorted generation results. Guidance only applies to FLUX DEV models."
|
||||
]
|
||||
},
|
||||
"paramCFGRescaleMultiplier": {
|
||||
"heading": "CFG Rescale Multiplier",
|
||||
"paragraphs": [
|
||||
@@ -1664,18 +1588,31 @@
|
||||
"storeNotInitialized": "Store is not initialized"
|
||||
},
|
||||
"controlLayers": {
|
||||
"regional": "Regional",
|
||||
"global": "Global",
|
||||
"canvas": "Canvas",
|
||||
"bookmark": "Bookmark for Quick Switch",
|
||||
"fitBboxToLayers": "Fit Bbox To Layers",
|
||||
"removeBookmark": "Remove Bookmark",
|
||||
"saveCanvasToGallery": "Save Canvas to Gallery",
|
||||
"saveBboxToGallery": "Save Bbox to Gallery",
|
||||
"newRegionalIPAdapterFromBbox": "New Regional IP Adapter from Bbox",
|
||||
"newGlobalIPAdapterFromBbox": "New Global IP Adapter from Bbox",
|
||||
"saveLayerToAssets": "Save Layer to Assets",
|
||||
"newControlLayerFromBbox": "New Control Layer from Bbox",
|
||||
"newRasterLayerFromBbox": "New Raster Layer from Bbox",
|
||||
"savedToGalleryOk": "Saved to Gallery",
|
||||
"savedToGalleryError": "Error saving to gallery",
|
||||
"newGlobalReferenceImageOk": "Created Global Reference Image",
|
||||
"newGlobalReferenceImageError": "Problem Creating Global Reference Image",
|
||||
"newRegionalReferenceImageOk": "Created Regional Reference Image",
|
||||
"newRegionalReferenceImageError": "Problem Creating Regional Reference Image",
|
||||
"newControlLayerOk": "Created Control Layer",
|
||||
"newControlLayerError": "Problem Creating Control Layer",
|
||||
"newRasterLayerOk": "Created Raster Layer",
|
||||
"newRasterLayerError": "Problem Creating Raster Layer",
|
||||
"pullBboxIntoLayerOk": "Bbox Pulled Into Layer",
|
||||
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
|
||||
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
|
||||
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
|
||||
"regionIsEmpty": "Selected region is empty",
|
||||
"mergeVisible": "Merge Visible",
|
||||
"mergeVisibleOk": "Merged visible layers",
|
||||
@@ -1709,30 +1646,35 @@
|
||||
"enableAutoNegative": "Enable Auto Negative",
|
||||
"disableAutoNegative": "Disable Auto Negative",
|
||||
"deletePrompt": "Delete Prompt",
|
||||
"deleteReferenceImage": "Delete Reference Image",
|
||||
"resetRegion": "Reset Region",
|
||||
"debugLayers": "Debug Layers",
|
||||
"showHUD": "Show HUD",
|
||||
"rectangle": "Rectangle",
|
||||
"maskFill": "Mask Fill",
|
||||
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||
"addPositivePrompt": "Add $t(controlLayers.prompt)",
|
||||
"addNegativePrompt": "Add $t(controlLayers.negativePrompt)",
|
||||
"addReferenceImage": "Add $t(controlLayers.referenceImage)",
|
||||
"addRasterLayer": "Add $t(controlLayers.rasterLayer)",
|
||||
"addControlLayer": "Add $t(controlLayers.controlLayer)",
|
||||
"addInpaintMask": "Add $t(controlLayers.inpaintMask)",
|
||||
"addRegionalGuidance": "Add $t(controlLayers.regionalGuidance)",
|
||||
"addGlobalReferenceImage": "Add $t(controlLayers.globalReferenceImage)",
|
||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||
"raster": "Raster",
|
||||
"rasterLayer": "Raster Layer",
|
||||
"controlLayer": "Control Layer",
|
||||
"inpaintMask": "Inpaint Mask",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"referenceImage": "Reference Image",
|
||||
"regionalReferenceImage": "Regional Reference Image",
|
||||
"globalReferenceImage": "Global Reference Image",
|
||||
"sendingToCanvas": "Sending to Canvas",
|
||||
"sendingToGallery": "Sending to Gallery",
|
||||
"sendToGallery": "Send To Gallery",
|
||||
"sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.",
|
||||
"sendToCanvas": "Send To Canvas",
|
||||
"copyToClipboard": "Copy to Clipboard",
|
||||
"sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.",
|
||||
"viewProgressInViewer": "View progress and outputs in the <Btn>Image Viewer</Btn>.",
|
||||
"viewProgressOnCanvas": "View progress and stage outputs on the <Btn>Canvas</Btn>.",
|
||||
@@ -1740,29 +1682,23 @@
|
||||
"controlLayer_withCount_one": "$t(controlLayers.controlLayer)",
|
||||
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
|
||||
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
|
||||
"ipAdapter_withCount_one": "$t(controlLayers.ipAdapter)",
|
||||
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
|
||||
"rasterLayer_withCount_other": "Raster Layers",
|
||||
"controlLayer_withCount_other": "Control Layers",
|
||||
"inpaintMask_withCount_other": "Inpaint Masks",
|
||||
"regionalGuidance_withCount_other": "Regional Guidance",
|
||||
"ipAdapter_withCount_other": "IP Adapters",
|
||||
"globalReferenceImage_withCount_other": "Global Reference Images",
|
||||
"opacity": "Opacity",
|
||||
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
||||
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
||||
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
||||
"globalIPAdapters_withCount_hidden": "Global IP Adapters ({{count}} hidden)",
|
||||
"globalReferenceImages_withCount_hidden": "Global Reference Images ({{count}} hidden)",
|
||||
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
||||
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
||||
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||
"globalIPAdapters_withCount_visible": "Global IP Adapters ({{count}})",
|
||||
"globalReferenceImages_withCount_visible": "Global Reference Images ({{count}})",
|
||||
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
|
||||
"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)",
|
||||
"globalInitialImage": "Global Initial Image",
|
||||
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
|
||||
"layer": "Layer",
|
||||
"opacityFilter": "Opacity Filter",
|
||||
"clearProcessor": "Clear Processor",
|
||||
@@ -1793,8 +1729,27 @@
|
||||
"stagingOnCanvas": "Staging images on",
|
||||
"replaceLayer": "Replace Layer",
|
||||
"pullBboxIntoLayer": "Pull Bbox into Layer",
|
||||
"pullBboxIntoIPAdapter": "Pull Bbox into IP Adapter",
|
||||
"pullBboxIntoReferenceImage": "Pull Bbox into Reference Image",
|
||||
"showProgressOnCanvas": "Show Progress on Canvas",
|
||||
"prompt": "Prompt",
|
||||
"negativePrompt": "Negative Prompt",
|
||||
"beginEndStepPercentShort": "Begin/End %",
|
||||
"weight": "Weight",
|
||||
"controlMode": {
|
||||
"controlMode": "Control Mode",
|
||||
"balanced": "Balanced",
|
||||
"prompt": "Prompt",
|
||||
"control": "Control",
|
||||
"megaControl": "Mega Control"
|
||||
},
|
||||
"ipAdapterMethod": {
|
||||
"ipAdapterMethod": "IP Adapter Method",
|
||||
"full": "Full",
|
||||
"style": "Style Only",
|
||||
"composition": "Composition Only"
|
||||
},
|
||||
"useSizeOptimizeForModel": "Copy size to W/H (optimize for model)",
|
||||
"useSizeIgnoreModel": "Copy size to W/H (ignore model)",
|
||||
"fill": {
|
||||
"fillColor": "Fill Color",
|
||||
"fillStyle": "Fill Style",
|
||||
@@ -1914,7 +1869,7 @@
|
||||
"off": "Off"
|
||||
},
|
||||
"preserveMask": {
|
||||
"label": "Preserve Mask Region",
|
||||
"label": "Preserve Masked Region",
|
||||
"alert": "Preserving Masked Region"
|
||||
}
|
||||
},
|
||||
@@ -1930,6 +1885,16 @@
|
||||
"isDisabled": "{{title}} is disabled",
|
||||
"isEmpty": "{{title}} is empty"
|
||||
}
|
||||
},
|
||||
"canvasContextMenu": {
|
||||
"saveToGalleryGroup": "Save To Gallery",
|
||||
"saveCanvasToGallery": "Save Canvas To Gallery",
|
||||
"saveBboxToGallery": "Save Bbox To Gallery",
|
||||
"bboxGroup": "Create From Bbox",
|
||||
"newGlobalReferenceImage": "New Global Reference Image",
|
||||
"newRegionalReferenceImage": "New Regional Reference Image",
|
||||
"newControlLayer": "New Control Layer",
|
||||
"newRasterLayer": "New Raster Layer"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
import { useSocketIO } from 'app/hooks/useSocketIO';
|
||||
import { useSyncQueueStatus } from 'app/hooks/useSyncQueueStatus';
|
||||
import { useLogger } from 'app/logging/useLogger';
|
||||
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
@@ -19,7 +18,6 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
|
||||
import SettingsModal from 'features/system/components/SettingsModal/SettingsModal';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
import { AppContent } from 'features/ui/components/AppContent';
|
||||
@@ -32,6 +30,7 @@ import { size } from 'lodash-es';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
|
||||
import { useSocketIO } from 'services/events/useSocketIO';
|
||||
|
||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||
import PreselectedImage from './PreselectedImage';
|
||||
@@ -138,7 +137,6 @@ const App = ({
|
||||
<StylePresetModal />
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<PreselectedImage selectedImage={selectedImage} />
|
||||
<SettingsModal />
|
||||
<RefreshAfterResetModal />
|
||||
<DeleteBoardModal />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'i18n';
|
||||
|
||||
import type { Middleware } from '@reduxjs/toolkit';
|
||||
import { $socketOptions } from 'app/hooks/useSocketIO';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { $baseUrl } from 'app/store/nanostores/baseUrl';
|
||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||
@@ -24,6 +23,7 @@ import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { lazy, memo, useEffect, useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
|
||||
import { $socketOptions } from 'services/events/stores';
|
||||
import type { ManagerOptions, SocketOptions } from 'socket.io-client';
|
||||
|
||||
const App = lazy(() => import('./App'));
|
||||
|
||||
@@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
|
||||
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
|
||||
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
|
||||
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
|
||||
import { addCancellationsListeners } from 'app/store/middleware/listenerMiddleware/listeners/cancellationsListeners';
|
||||
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
||||
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
|
||||
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
@@ -73,15 +72,6 @@ addAnyEnqueuedListener(startAppListening);
|
||||
addBatchEnqueuedListener(startAppListening);
|
||||
|
||||
// Canvas actions
|
||||
// addCanvasSavedToGalleryListener(startAppListening);
|
||||
// addCanvasMaskSavedToGalleryListener(startAppListening);
|
||||
// addCanvasImageToControlNetListener(startAppListening);
|
||||
// addCanvasMaskToControlNetListener(startAppListening);
|
||||
// addCanvasDownloadedAsImageListener(startAppListening);
|
||||
// addCanvasCopiedToClipboardListener(startAppListening);
|
||||
// addCanvasMergedListener(startAppListening);
|
||||
// addStagingAreaImageSavedListener(startAppListening);
|
||||
// addCommitStagingAreaImageListener(startAppListening);
|
||||
addStagingListeners(startAppListening);
|
||||
|
||||
// Socket.IO
|
||||
@@ -121,6 +111,3 @@ addAdHocPostProcessingRequestedListener(startAppListening);
|
||||
addDynamicPromptsListener(startAppListening);
|
||||
|
||||
addSetDefaultSettingsListener(startAppListening);
|
||||
// addControlAdapterPreprocessor(startAppListening);
|
||||
|
||||
addCancellationsListeners(startAppListening);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { canvasReset, rasterLayerAdded } from 'features/controlLayers/store/canv
|
||||
import { stagingAreaImageAccepted, stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
|
||||
@@ -18,9 +19,10 @@ export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppS
|
||||
const state = getState();
|
||||
const nodes = selectNodesSlice(state);
|
||||
const canvas = selectCanvasSlice(state);
|
||||
const upscale = selectUpscaleSlice(state);
|
||||
|
||||
deleted_images.forEach((image_name) => {
|
||||
const imageUsage = getImageUsage(nodes, canvas, image_name);
|
||||
const imageUsage = getImageUsage(nodes, canvas, upscale, image_name);
|
||||
|
||||
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
|
||||
dispatch(nodeEditorReset());
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { $lastCanvasProgressEvent } from 'features/controlLayers/store/canvasSlice';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
/**
|
||||
* To prevent a race condition where a progress event arrives after a successful cancellation, we need to keep track of
|
||||
* cancellations:
|
||||
* - In the route handlers above, we track and update the cancellations object
|
||||
* - When the user queues a, we should reset the cancellations, also handled int he route handlers above
|
||||
* - When we get a progress event, we should check if the event is cancelled before setting the event
|
||||
*
|
||||
* We have a few ways that cancellations are effected, so we need to track them all:
|
||||
* - by queue item id (in this case, we will compare the session_id and not the item_id)
|
||||
* - by batch id
|
||||
* - by destination
|
||||
* - by clearing the queue
|
||||
*/
|
||||
type Cancellations = {
|
||||
sessionIds: Set<string>;
|
||||
batchIds: Set<string>;
|
||||
destinations: Set<string>;
|
||||
clearQueue: boolean;
|
||||
};
|
||||
|
||||
const resetCancellations = (): void => {
|
||||
cancellations.clearQueue = false;
|
||||
cancellations.sessionIds.clear();
|
||||
cancellations.batchIds.clear();
|
||||
cancellations.destinations.clear();
|
||||
};
|
||||
|
||||
const cancellations: Cancellations = {
|
||||
sessionIds: new Set(),
|
||||
batchIds: new Set(),
|
||||
destinations: new Set(),
|
||||
clearQueue: false,
|
||||
} as Readonly<Cancellations>;
|
||||
|
||||
/**
|
||||
* Checks if an item is cancelled, used to prevent race conditions with event handling.
|
||||
*
|
||||
* To use this, provide the session_id, batch_id and destination from the event payload.
|
||||
*/
|
||||
export const getIsCancelled = (item: {
|
||||
session_id: string;
|
||||
batch_id: string;
|
||||
destination?: string | null;
|
||||
}): boolean => {
|
||||
if (cancellations.clearQueue) {
|
||||
return true;
|
||||
}
|
||||
if (cancellations.sessionIds.has(item.session_id)) {
|
||||
return true;
|
||||
}
|
||||
if (cancellations.batchIds.has(item.batch_id)) {
|
||||
return true;
|
||||
}
|
||||
if (item.destination && cancellations.destinations.has(item.destination)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const addCancellationsListeners = (startAppListening: AppStartListening) => {
|
||||
// When we get a cancellation, we may need to clear the last progress event - next few listeners handle those cases.
|
||||
// Maybe we could use the `getIsCancelled` util here, but I think that could introduce _another_ race condition...
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
|
||||
effect: () => {
|
||||
resetCancellations();
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.cancelByBatchDestination.matchFulfilled,
|
||||
effect: (action) => {
|
||||
cancellations.destinations.add(action.meta.arg.originalArgs.destination);
|
||||
|
||||
const event = $lastCanvasProgressEvent.get();
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const { session_id, batch_id, destination } = event;
|
||||
if (getIsCancelled({ session_id, batch_id, destination })) {
|
||||
$lastCanvasProgressEvent.set(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.cancelQueueItem.matchFulfilled,
|
||||
effect: (action) => {
|
||||
cancellations.sessionIds.add(action.payload.session_id);
|
||||
|
||||
const event = $lastCanvasProgressEvent.get();
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const { session_id, batch_id, destination } = event;
|
||||
if (getIsCancelled({ session_id, batch_id, destination })) {
|
||||
$lastCanvasProgressEvent.set(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.cancelByBatchIds.matchFulfilled,
|
||||
effect: (action) => {
|
||||
for (const batch_id of action.meta.arg.originalArgs.batch_ids) {
|
||||
cancellations.batchIds.add(batch_id);
|
||||
}
|
||||
const event = $lastCanvasProgressEvent.get();
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const { session_id, batch_id, destination } = event;
|
||||
if (getIsCancelled({ session_id, batch_id, destination })) {
|
||||
$lastCanvasProgressEvent.set(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.clearQueue.matchFulfilled,
|
||||
effect: () => {
|
||||
cancellations.clearQueue = true;
|
||||
const event = $lastCanvasProgressEvent.get();
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const { session_id, batch_id, destination } = event;
|
||||
if (getIsCancelled({ session_id, batch_id, destination })) {
|
||||
$lastCanvasProgressEvent.set(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -5,12 +5,8 @@ import type { SerializableObject } from 'common/types';
|
||||
import type { Result } from 'common/util/result';
|
||||
import { withResult, withResultAsync } from 'common/util/result';
|
||||
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
|
||||
import {
|
||||
selectIsStaging,
|
||||
stagingAreaReset,
|
||||
stagingAreaStartedStaging,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildFLUXGraph } from 'features/nodes/util/graph/generation/buildFLUXGraph';
|
||||
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
|
||||
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
@@ -33,21 +29,12 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
const manager = $canvasManager.get();
|
||||
assert(manager, 'No model found in state');
|
||||
|
||||
let didStartStaging = false;
|
||||
|
||||
if (!selectIsStaging(state) && state.canvasSettings.sendToCanvas) {
|
||||
dispatch(stagingAreaStartedStaging());
|
||||
didStartStaging = true;
|
||||
}
|
||||
|
||||
const abortStaging = () => {
|
||||
if (didStartStaging && selectIsStaging(getState())) {
|
||||
dispatch(stagingAreaReset());
|
||||
}
|
||||
};
|
||||
|
||||
let buildGraphResult: Result<
|
||||
{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> },
|
||||
{
|
||||
g: Graph;
|
||||
noise: Invocation<'noise' | 'flux_denoise'>;
|
||||
posCond: Invocation<'compel' | 'sdxl_compel_prompt' | 'flux_text_encoder'>;
|
||||
},
|
||||
Error
|
||||
>;
|
||||
|
||||
@@ -62,13 +49,15 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
case `sd-2`:
|
||||
buildGraphResult = await withResultAsync(() => buildSD1Graph(state, manager));
|
||||
break;
|
||||
case `flux`:
|
||||
buildGraphResult = await withResultAsync(() => buildFLUXGraph(state, manager));
|
||||
break;
|
||||
default:
|
||||
assert(false, `No graph builders for base ${base}`);
|
||||
}
|
||||
|
||||
if (buildGraphResult.isErr()) {
|
||||
log.error({ error: serializeError(buildGraphResult.error) }, 'Failed to build graph');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,12 +66,11 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
const destination = state.canvasSettings.sendToCanvas ? 'canvas' : 'gallery';
|
||||
|
||||
const prepareBatchResult = withResult(() =>
|
||||
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
|
||||
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'canvas', destination)
|
||||
);
|
||||
|
||||
if (prepareBatchResult.isErr()) {
|
||||
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -97,7 +85,6 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
|
||||
if (enqueueResult.isErr()) {
|
||||
log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
@@ -53,9 +53,9 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
|
||||
// };
|
||||
|
||||
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => {
|
||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
||||
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
|
||||
dispatch(ipaImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
entitySelected,
|
||||
ipaImageChanged,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
@@ -56,7 +65,10 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
) {
|
||||
const { id } = overData.context;
|
||||
dispatch(
|
||||
ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO: activeData.payload.imageDTO })
|
||||
referenceImageIPAdapterImageChanged({
|
||||
entityIdentifier: { id, type: 'reference_image' },
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -69,11 +81,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { id, ipAdapterId } = overData.context;
|
||||
const { id, referenceImageId } = overData.context;
|
||||
dispatch(
|
||||
rgIPAdapterImageChanged({
|
||||
entityIdentifier: { id, type: 'regional_guidance' },
|
||||
ipAdapterId,
|
||||
referenceImageId,
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
@@ -119,6 +131,36 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
overData.actionType === 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const state = getState();
|
||||
const ipAdapter = deepClone(selectDefaultIPAdapter(state));
|
||||
ipAdapter.image = imageDTOToImageWithDims(activeData.payload.imageDTO);
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter }],
|
||||
};
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
overData.actionType === 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const state = getState();
|
||||
const ipAdapter = deepClone(selectDefaultIPAdapter(state));
|
||||
ipAdapter.image = imageDTOToImageWithDims(activeData.payload.imageDTO);
|
||||
const overrides: Partial<CanvasReferenceImageState> = {
|
||||
ipAdapter,
|
||||
};
|
||||
dispatch(referenceImageAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
|
||||
@@ -13,10 +13,13 @@ export const addImageToDeleteSelectedListener = (startAppListening: AppStartList
|
||||
const imagesUsage = selectImageUsage(getState());
|
||||
|
||||
const isImageInUse =
|
||||
imagesUsage.some((i) => i.isLayerImage) ||
|
||||
imagesUsage.some((i) => i.isControlAdapterImage) ||
|
||||
imagesUsage.some((i) => i.isIPAdapterImage) ||
|
||||
imagesUsage.some((i) => i.isLayerImage);
|
||||
imagesUsage.some((i) => i.isRasterLayerImage) ||
|
||||
imagesUsage.some((i) => i.isControlLayerImage) ||
|
||||
imagesUsage.some((i) => i.isReferenceImage) ||
|
||||
imagesUsage.some((i) => i.isInpaintMaskImage) ||
|
||||
imagesUsage.some((i) => i.isUpscaleImage) ||
|
||||
imagesUsage.some((i) => i.isNodesImage) ||
|
||||
imagesUsage.some((i) => i.isRegionalGuidanceImage);
|
||||
|
||||
if (shouldConfirmOnDelete || isImageInUse) {
|
||||
dispatch(isModalOpenChanged(true));
|
||||
|
||||
@@ -3,11 +3,11 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
|
||||
import {
|
||||
entityRasterized,
|
||||
entitySelected,
|
||||
ipaImageChanged,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
@@ -101,15 +101,15 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
|
||||
|
||||
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
|
||||
const { id } = postUploadAction;
|
||||
dispatch(ipaImageChanged({ entityIdentifier: { id, type: 'ip_adapter' }, imageDTO }));
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: { id, type: 'reference_image' }, imageDTO }));
|
||||
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
|
||||
const { id, ipAdapterId } = postUploadAction;
|
||||
const { id, referenceImageId } = postUploadAction;
|
||||
dispatch(
|
||||
rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, ipAdapterId, imageDTO })
|
||||
rgIPAdapterImageChanged({ entityIdentifier: { id, type: 'regional_guidance' }, referenceImageId, imageDTO })
|
||||
);
|
||||
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
|
||||
return;
|
||||
|
||||
@@ -6,11 +6,18 @@ import {
|
||||
bboxHeightChanged,
|
||||
bboxWidthChanged,
|
||||
controlLayerModelChanged,
|
||||
ipaModelChanged,
|
||||
referenceImageIPAdapterModelChanged,
|
||||
rgIPAdapterModelChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
|
||||
import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
|
||||
import {
|
||||
clipEmbedModelSelected,
|
||||
fluxVAESelected,
|
||||
modelChanged,
|
||||
refinerModelChanged,
|
||||
t5EncoderModelSelected,
|
||||
vaeSelected,
|
||||
} from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
|
||||
@@ -21,13 +28,16 @@ import type { Logger } from 'roarr';
|
||||
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
import {
|
||||
isCLIPEmbedModelConfig,
|
||||
isControlNetOrT2IAdapterModelConfig,
|
||||
isFluxVAEModelConfig,
|
||||
isIPAdapterModelConfig,
|
||||
isLoRAModelConfig,
|
||||
isNonFluxVAEModelConfig,
|
||||
isNonRefinerMainModelConfig,
|
||||
isRefinerMainModelModelConfig,
|
||||
isSpandrelImageToImageModelConfig,
|
||||
isVAEModelConfig,
|
||||
isT5EncoderModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
const log = logger('models');
|
||||
@@ -50,6 +60,9 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>
|
||||
handleControlAdapterModels(models, state, dispatch, log);
|
||||
handleSpandrelImageToImageModels(models, state, dispatch, log);
|
||||
handleIPAdapterModels(models, state, dispatch, log);
|
||||
handleT5EncoderModels(models, state, dispatch, log);
|
||||
handleCLIPEmbedModels(models, state, dispatch, log);
|
||||
handleFLUXVAEModels(models, state, dispatch, log);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -131,7 +144,7 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
// null is a valid VAE! it means "use the default with the main model"
|
||||
return;
|
||||
}
|
||||
const vaeModels = models.filter(isVAEModelConfig);
|
||||
const vaeModels = models.filter(isNonFluxVAEModelConfig);
|
||||
|
||||
const isCurrentVAEAvailable = vaeModels.some((m) => m.key === currentVae.key);
|
||||
|
||||
@@ -181,22 +194,22 @@ const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log)
|
||||
|
||||
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const ipaModels = models.filter(isIPAdapterModelConfig);
|
||||
selectCanvasSlice(state).ipAdapters.entities.forEach((entity) => {
|
||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
||||
const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key);
|
||||
if (isModelAvailable) {
|
||||
return;
|
||||
}
|
||||
dispatch(ipaModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
|
||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), modelConfig: null }));
|
||||
});
|
||||
|
||||
selectCanvasSlice(state).regions.entities.forEach((entity) => {
|
||||
entity.ipAdapters.forEach(({ id: ipAdapterId, model }) => {
|
||||
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
|
||||
selectCanvasSlice(state).regionalGuidance.entities.forEach((entity) => {
|
||||
entity.referenceImages.forEach(({ id: referenceImageId, ipAdapter }) => {
|
||||
const isModelAvailable = ipaModels.some((m) => m.key === ipAdapter.model?.key);
|
||||
if (isModelAvailable) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), ipAdapterId, modelConfig: null })
|
||||
rgIPAdapterModelChanged({ entityIdentifier: getEntityIdentifier(entity), referenceImageId, modelConfig: null })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -223,3 +236,45 @@ const handleSpandrelImageToImageModels: ModelHandler = (models, state, dispatch,
|
||||
dispatch(postProcessingModelChanged(firstModel));
|
||||
}
|
||||
};
|
||||
|
||||
const handleT5EncoderModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const { t5EncoderModel: currentT5EncoderModel } = state.params;
|
||||
const t5EncoderModels = models.filter(isT5EncoderModelConfig);
|
||||
const firstModel = t5EncoderModels[0] || null;
|
||||
|
||||
const isCurrentT5EncoderModelAvailable = currentT5EncoderModel
|
||||
? t5EncoderModels.some((m) => m.key === currentT5EncoderModel.key)
|
||||
: false;
|
||||
|
||||
if (!isCurrentT5EncoderModelAvailable) {
|
||||
dispatch(t5EncoderModelSelected(firstModel));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCLIPEmbedModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const { clipEmbedModel: currentCLIPEmbedModel } = state.params;
|
||||
const CLIPEmbedModels = models.filter(isCLIPEmbedModelConfig);
|
||||
const firstModel = CLIPEmbedModels[0] || null;
|
||||
|
||||
const isCurrentCLIPEmbedModelAvailable = currentCLIPEmbedModel
|
||||
? CLIPEmbedModels.some((m) => m.key === currentCLIPEmbedModel.key)
|
||||
: false;
|
||||
|
||||
if (!isCurrentCLIPEmbedModelAvailable) {
|
||||
dispatch(clipEmbedModelSelected(firstModel));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFLUXVAEModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const { fluxVAE: currentFLUXVAEModel } = state.params;
|
||||
const fluxVAEModels = models.filter(isFluxVAEModelConfig);
|
||||
const firstModel = fluxVAEModels[0] || null;
|
||||
|
||||
const isCurrentFLUXVAEModelAvailable = currentFLUXVAEModel
|
||||
? fluxVAEModels.some((m) => m.key === currentFLUXVAEModel.key)
|
||||
: false;
|
||||
|
||||
if (!isCurrentFLUXVAEModelAvailable) {
|
||||
dispatch(fluxVAESelected(firstModel));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,8 @@ import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilder
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||
import { socketConnected } from 'services/events/setEventListeners';
|
||||
|
||||
import { socketConnected } from './socketConnected';
|
||||
|
||||
const matcher = isAnyOf(
|
||||
positivePromptChanged,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { $baseUrl } from 'app/store/nanostores/baseUrl';
|
||||
@@ -6,11 +7,11 @@ import { atom } from 'nanostores';
|
||||
import { api } from 'services/api';
|
||||
import { modelsApi } from 'services/api/endpoints/models';
|
||||
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
|
||||
import { socketConnected } from 'services/events/setEventListeners';
|
||||
|
||||
const log = logger('events');
|
||||
|
||||
const $isFirstConnection = atom(true);
|
||||
export const socketConnected = createAction('socket/connected');
|
||||
|
||||
export const addSocketConnectedEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
|
||||
@@ -22,7 +22,6 @@ export type AppFeature =
|
||||
| 'multiselect'
|
||||
| 'pauseQueue'
|
||||
| 'resumeQueue'
|
||||
| 'prependQueue'
|
||||
| 'invocationCache'
|
||||
| 'bulkDownload'
|
||||
| 'starterModels'
|
||||
@@ -114,6 +113,9 @@ export type AppConfig = {
|
||||
weight: NumericalParameterConfig;
|
||||
};
|
||||
};
|
||||
flux: {
|
||||
guidance: NumericalParameterConfig;
|
||||
};
|
||||
};
|
||||
|
||||
export type PartialAppConfig = O.Partial<AppConfig, 'deep'>;
|
||||
|
||||
@@ -66,7 +66,7 @@ type IAIDndImageProps = FlexProps & {
|
||||
fitContainer?: boolean;
|
||||
droppableData?: TypesafeDroppableData;
|
||||
draggableData?: TypesafeDraggableData;
|
||||
dropLabel?: ReactNode;
|
||||
dropLabel?: string;
|
||||
isSelected?: boolean;
|
||||
isSelectedForCompare?: boolean;
|
||||
thumbnail?: boolean;
|
||||
@@ -155,12 +155,18 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
||||
return styles;
|
||||
}, [isUploadDisabled, minSize]);
|
||||
|
||||
const openInNewTab = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
window.open(imageDTO.image_url, '_blank');
|
||||
}, [imageDTO]);
|
||||
const openInNewTab = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
if (e.button !== 1) {
|
||||
return;
|
||||
}
|
||||
window.open(imageDTO.image_url, '_blank');
|
||||
},
|
||||
[imageDTO]
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageContextMenu imageDTO={imageDTO}>
|
||||
|
||||
@@ -10,7 +10,9 @@ const sx: SystemStyleObject = {
|
||||
transitionDuration: 'normal',
|
||||
fill: 'base.100',
|
||||
_hover: { fill: 'base.50' },
|
||||
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
|
||||
filter: `drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-900))
|
||||
drop-shadow(0px 0px 0.3rem var(--invoke-colors-base-900))
|
||||
drop-shadow(0px 0px 0.3rem var(--invoke-colors-base-900))`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,7 +29,6 @@ const IAIDndImageIcon = (props: Props) => {
|
||||
onClick={onClick}
|
||||
aria-label={tooltip}
|
||||
icon={icon}
|
||||
size="sm"
|
||||
variant="link"
|
||||
sx={sx}
|
||||
data-testid={tooltip}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
type Props = {
|
||||
isOver: boolean;
|
||||
label?: ReactNode;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const initial: AnimationProps['initial'] = {
|
||||
@@ -28,11 +27,13 @@ const IAIDropOverlay = (props: Props) => {
|
||||
const motionId = useRef(uuidv4());
|
||||
return (
|
||||
<motion.div key={motionId.current} initial={initial} animate={animate} exit={exit}>
|
||||
<Flex position="absolute" top={0} insetInlineStart={0} w="full" h="full">
|
||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0}>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineStart={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
w="full"
|
||||
h="full"
|
||||
bg="base.900"
|
||||
@@ -47,29 +48,30 @@ const IAIDropOverlay = (props: Props) => {
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={0.5}
|
||||
insetInlineStart={0.5}
|
||||
insetInlineEnd={0.5}
|
||||
right={0.5}
|
||||
bottom={0.5}
|
||||
left={0.5}
|
||||
opacity={1}
|
||||
borderWidth={2}
|
||||
borderColor={isOver ? 'base.300' : 'base.500'}
|
||||
borderWidth={1.5}
|
||||
borderColor={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
borderRadius="base"
|
||||
borderStyle="dashed"
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
p={4}
|
||||
>
|
||||
<Box
|
||||
fontSize="2xl"
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="semibold"
|
||||
transform={isOver ? 'scale(1.1)' : 'scale(1)'}
|
||||
color={isOver ? 'base.50' : 'base.300'}
|
||||
color={isOver ? 'invokeYellow.300' : 'base.500'}
|
||||
transitionProperty="common"
|
||||
transitionDuration="0.1s"
|
||||
textAlign="center"
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
|
||||
import type { TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import IAIDropOverlay from './IAIDropOverlay';
|
||||
|
||||
type IAIDroppableProps = {
|
||||
dropLabel?: ReactNode;
|
||||
dropLabel?: string;
|
||||
disabled?: boolean;
|
||||
data?: TypesafeDroppableData;
|
||||
};
|
||||
@@ -30,7 +29,9 @@ const IAIDroppable = (props: IAIDroppableProps) => {
|
||||
ref={setNodeRef}
|
||||
position="absolute"
|
||||
top={0}
|
||||
insetInlineStart={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
w="full"
|
||||
h="full"
|
||||
pointerEvents={active ? 'auto' : 'none'}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type Feature =
|
||||
| 'noiseUseCPU'
|
||||
| 'paramAspect'
|
||||
| 'paramCFGScale'
|
||||
| 'paramGuidance'
|
||||
| 'paramCFGRescaleMultiplier'
|
||||
| 'paramDenoisingStrength'
|
||||
| 'paramHeight'
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useClearQueue } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
import { useQueueFront } from 'features/queue/hooks/useQueueFront';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@@ -11,30 +10,28 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
export const useGlobalHotkeys = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isModelManagerEnabled = useFeatureStatus('modelManager');
|
||||
const { queueBack, isDisabled: isDisabledQueueBack, isLoading: isLoadingQueueBack } = useQueueBack();
|
||||
const queue = useInvoke();
|
||||
|
||||
useHotkeys(
|
||||
['ctrl+enter', 'meta+enter'],
|
||||
queueBack,
|
||||
queue.queueBack,
|
||||
{
|
||||
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
|
||||
enabled: !queue.isDisabled && !queue.isLoading,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[queueBack, isDisabledQueueBack, isLoadingQueueBack]
|
||||
[queue]
|
||||
);
|
||||
|
||||
const { queueFront, isDisabled: isDisabledQueueFront, isLoading: isLoadingQueueFront } = useQueueFront();
|
||||
|
||||
useHotkeys(
|
||||
['ctrl+shift+enter', 'meta+shift+enter'],
|
||||
queueFront,
|
||||
queue.queueFront,
|
||||
{
|
||||
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
|
||||
enabled: !queue.isDisabled && !queue.isLoading,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
[queueFront, isDisabledQueueFront, isLoadingQueueFront]
|
||||
[queue]
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { $true } from 'app/store/nanostores/util';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
@@ -13,7 +12,7 @@ import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { Templates } from 'features/nodes/store/types';
|
||||
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { selectUpscalelice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
@@ -21,13 +20,14 @@ import i18n from 'i18next';
|
||||
import { forEach, upperFirst } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { getConnectedEdges } from 'reactflow';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
const LAYER_TYPE_TO_TKEY = {
|
||||
ip_adapter: 'controlLayers.ipAdapter',
|
||||
reference_image: 'controlLayers.referenceImage',
|
||||
inpaint_mask: 'controlLayers.inpaintMask',
|
||||
regional_guidance: 'controlLayers.regionalGuidance',
|
||||
raster_layer: 'controlLayers.raster',
|
||||
control_layer: 'controlLayers.globalControlAdapter',
|
||||
raster_layer: 'controlLayers.rasterLayer',
|
||||
control_layer: 'controlLayers.controlLayer',
|
||||
} as const;
|
||||
|
||||
const createSelector = (
|
||||
@@ -46,7 +46,7 @@ const createSelector = (
|
||||
selectDynamicPromptsSlice,
|
||||
selectCanvasSlice,
|
||||
selectParamsSlice,
|
||||
selectUpscalelice,
|
||||
selectUpscaleSlice,
|
||||
selectConfigSlice,
|
||||
selectActiveTab,
|
||||
],
|
||||
@@ -147,6 +147,18 @@ const createSelector = (
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||
}
|
||||
|
||||
if (model?.base === 'flux') {
|
||||
if (!params.t5EncoderModel) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noT5EncoderModelSelected') });
|
||||
}
|
||||
if (!params.clipEmbedModel) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noCLIPEmbedModelSelected') });
|
||||
}
|
||||
if (!params.fluxVAE) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noFLUXVAEModelSelected') });
|
||||
}
|
||||
}
|
||||
|
||||
canvas.controlLayers.entities
|
||||
.filter((controlLayer) => controlLayer.isEnabled)
|
||||
.forEach((controlLayer, i) => {
|
||||
@@ -177,7 +189,7 @@ const createSelector = (
|
||||
}
|
||||
});
|
||||
|
||||
canvas.ipAdapters.entities
|
||||
canvas.referenceImages.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
||||
@@ -205,7 +217,7 @@ const createSelector = (
|
||||
}
|
||||
});
|
||||
|
||||
canvas.regions.entities
|
||||
canvas.regionalGuidance.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layer_one');
|
||||
@@ -218,10 +230,14 @@ const createSelector = (
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||
}
|
||||
// Must have at least 1 prompt or IP Adapter
|
||||
if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) {
|
||||
if (
|
||||
entity.positivePrompt === null &&
|
||||
entity.negativePrompt === null &&
|
||||
entity.referenceImages.length === 0
|
||||
) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
||||
}
|
||||
entity.ipAdapters.forEach((ipAdapter) => {
|
||||
entity.referenceImages.forEach(({ ipAdapter }) => {
|
||||
// Must have model
|
||||
if (!ipAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||
|
||||
14
invokeai/frontend/web/src/common/hooks/useNanoid.ts
Normal file
14
invokeai/frontend/web/src/common/hooks/useNanoid.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getPrefixedId, nanoid } from 'features/controlLayers/konva/util';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNanoid = (prefix?: string) => {
|
||||
const id = useMemo(() => {
|
||||
if (prefix) {
|
||||
return getPrefixedId(prefix);
|
||||
} else {
|
||||
return nanoid();
|
||||
}
|
||||
}, [prefix]);
|
||||
|
||||
return id;
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
|
||||
import { Button, Flex, Heading } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
useAddControlLayer,
|
||||
useAddGlobalReferenceImage,
|
||||
useAddInpaintMask,
|
||||
useAddIPAdapter,
|
||||
useAddRasterLayer,
|
||||
useAddRegionalGuidance,
|
||||
useAddRegionalReferenceImage,
|
||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
@@ -16,27 +19,82 @@ export const CanvasAddEntityButtons = memo(() => {
|
||||
const addRegionalGuidance = useAddRegionalGuidance();
|
||||
const addRasterLayer = useAddRasterLayer();
|
||||
const addControlLayer = useAddControlLayer();
|
||||
const addIPAdapter = useAddIPAdapter();
|
||||
const addGlobalReferenceImage = useAddGlobalReferenceImage();
|
||||
const addRegionalReferenceImage = useAddRegionalReferenceImage();
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" alignItems="center">
|
||||
<ButtonGroup position="relative" orientation="vertical" isAttached={false} top="20%">
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</Button>
|
||||
<Button variant="ghost" justifyContent="flex-start" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Flex w="full" h="full" justifyContent="center" gap={4}>
|
||||
<Flex position="relative" flexDir="column" gap={4} top="20%">
|
||||
<Flex flexDir="column" justifyContent="flex-start" gap={2}>
|
||||
<Heading size="xs">{t('controlLayers.global')}</Heading>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addGlobalReferenceImage}
|
||||
isDisabled={isFLUX}
|
||||
>
|
||||
{t('controlLayers.globalReferenceImage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Heading size="xs">{t('controlLayers.regional')}</Heading>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addInpaintMask}
|
||||
>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addRegionalGuidance}
|
||||
isDisabled={isFLUX}
|
||||
>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addRegionalReferenceImage}
|
||||
isDisabled={isFLUX}
|
||||
>
|
||||
{t('controlLayers.regionalReferenceImage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex flexDir="column" justifyContent="flex-start" gap={2}>
|
||||
<Heading size="xs">{t('controlLayers.layer_other')}</Heading>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addControlLayer}
|
||||
isDisabled={isFLUX}
|
||||
>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
leftIcon={<PiPlusBold />}
|
||||
onClick={addRasterLayer}
|
||||
>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MenuGroup, MenuItem } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
useNewControlLayerFromBbox,
|
||||
useNewGlobalIPAdapterFromBbox,
|
||||
useNewGlobalReferenceImageFromBbox,
|
||||
useNewRasterLayerFromBbox,
|
||||
useNewRegionalIPAdapterFromBbox,
|
||||
useNewRegionalReferenceImageFromBbox,
|
||||
useSaveBboxToGallery,
|
||||
useSaveCanvasToGallery,
|
||||
} from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
@@ -17,32 +17,36 @@ export const CanvasContextMenuGlobalMenuItems = memo(() => {
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const saveCanvasToGallery = useSaveCanvasToGallery();
|
||||
const saveBboxToGallery = useSaveBboxToGallery();
|
||||
const saveBboxAsRegionalGuidanceIPAdapter = useNewRegionalIPAdapterFromBbox();
|
||||
const saveBboxAsIPAdapter = useNewGlobalIPAdapterFromBbox();
|
||||
const saveBboxAsRasterLayer = useNewRasterLayerFromBbox();
|
||||
const saveBboxAsControlLayer = useNewControlLayerFromBbox();
|
||||
const newRegionalReferenceImageFromBbox = useNewRegionalReferenceImageFromBbox();
|
||||
const newGlobalReferenceImageFromBbox = useNewGlobalReferenceImageFromBbox();
|
||||
const newRasterLayerFromBbox = useNewRasterLayerFromBbox();
|
||||
const newControlLayerFromBbox = useNewControlLayerFromBbox();
|
||||
|
||||
return (
|
||||
<MenuGroup title={t('controlLayers.canvas')}>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
|
||||
{t('controlLayers.saveCanvasToGallery')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
|
||||
{t('controlLayers.saveBboxToGallery')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={saveBboxAsIPAdapter}>
|
||||
{t('controlLayers.newGlobalIPAdapterFromBbox')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={saveBboxAsRegionalGuidanceIPAdapter}>
|
||||
{t('controlLayers.newRegionalIPAdapterFromBbox')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={saveBboxAsControlLayer}>
|
||||
{t('controlLayers.newControlLayerFromBbox')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={saveBboxAsRasterLayer}>
|
||||
{t('controlLayers.newRasterLayerFromBbox')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.saveToGalleryGroup')}>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveCanvasToGallery}>
|
||||
{t('controlLayers.canvasContextMenu.saveCanvasToGallery')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiFloppyDiskBold />} isDisabled={isBusy} onClick={saveBboxToGallery}>
|
||||
{t('controlLayers.canvasContextMenu.saveBboxToGallery')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('controlLayers.canvasContextMenu.bboxGroup')}>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={newGlobalReferenceImageFromBbox}>
|
||||
{t('controlLayers.canvasContextMenu.newGlobalReferenceImage')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={newRegionalReferenceImageFromBbox}>
|
||||
{t('controlLayers.canvasContextMenu.newRegionalReferenceImage')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={newControlLayerFromBbox}>
|
||||
{t('controlLayers.canvasContextMenu.newControlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiStackPlusFill />} isDisabled={isBusy} onClick={newRasterLayerFromBbox}>
|
||||
{t('controlLayers.canvasContextMenu.newRasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { MenuGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import {
|
||||
EntityIdentifierContext,
|
||||
@@ -9,7 +11,11 @@ import {
|
||||
} from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { isFilterableEntityIdentifier, isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import {
|
||||
isFilterableEntityIdentifier,
|
||||
isSaveableEntityIdentifier,
|
||||
isTransformableEntityIdentifier,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { memo } from 'react';
|
||||
|
||||
const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
|
||||
@@ -20,6 +26,8 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => {
|
||||
<MenuGroup title={title}>
|
||||
{isFilterableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsFilter />}
|
||||
{isTransformableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsTransform />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsCopyToClipboard />}
|
||||
{isSaveableEntityIdentifier(entityIdentifier) && <CanvasEntityMenuItemsSave />}
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</MenuGroup>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { Grid, GridItem } from '@invoke-ai/ui-library';
|
||||
import IAIDroppable from 'common/components/IAIDroppable';
|
||||
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
|
||||
import type {
|
||||
AddControlLayerFromImageDropData,
|
||||
AddGlobalReferenceImageFromImageDropData,
|
||||
AddRasterLayerFromImageDropData,
|
||||
AddRegionalReferenceImageFromImageDropData,
|
||||
} from 'features/dnd/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -14,6 +19,16 @@ const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
|
||||
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addRegionalReferenceImageFromImageDropData: AddRegionalReferenceImageFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE',
|
||||
};
|
||||
|
||||
const addGlobalReferenceImageFromImageDropData: AddGlobalReferenceImageFromImageDropData = {
|
||||
id: 'add-control-layer-from-image-drop-data',
|
||||
actionType: 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE',
|
||||
};
|
||||
|
||||
export const CanvasDropArea = memo(() => {
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
@@ -23,12 +38,29 @@ export const CanvasDropArea = memo(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Raster Layer" data={addRasterLayerFromImageDropData} />
|
||||
</Flex>
|
||||
<Flex position="absolute" top="50%" right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||
<IAIDroppable dropLabel="Create Control Layer" data={addControlLayerFromImageDropData} />
|
||||
</Flex>
|
||||
<Grid
|
||||
gridTemplateRows="1fr 1fr"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Raster Layer" data={addRasterLayerFromImageDropData} />
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Control Layer" data={addControlLayerFromImageDropData} />
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Regional Reference Image" data={addRegionalReferenceImageFromImageDropData} />
|
||||
</GridItem>
|
||||
<GridItem position="relative">
|
||||
<IAIDroppable dropLabel="New Global Reference Image" data={addGlobalReferenceImageFromImageDropData} />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,9 +11,9 @@ export const CanvasEntityList = memo(() => {
|
||||
return (
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list" w="full" h="full">
|
||||
<IPAdapterList />
|
||||
<InpaintMaskList />
|
||||
<RegionalGuidanceEntityList />
|
||||
<IPAdapterList />
|
||||
<ControlLayerEntityList />
|
||||
<RasterLayerEntityList />
|
||||
</Flex>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
useAddControlLayer,
|
||||
useAddGlobalReferenceImage,
|
||||
useAddInpaintMask,
|
||||
useAddIPAdapter,
|
||||
useAddRasterLayer,
|
||||
useAddRegionalGuidance,
|
||||
useAddRegionalReferenceImage,
|
||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const addGlobalReferenceImage = useAddGlobalReferenceImage();
|
||||
const addInpaintMask = useAddInpaintMask();
|
||||
const addRegionalGuidance = useAddRegionalGuidance();
|
||||
const addRegionalReferenceImage = useAddRegionalReferenceImage();
|
||||
const addRasterLayer = useAddRasterLayer();
|
||||
const addControlLayer = useAddControlLayer();
|
||||
const addIPAdapter = useAddIPAdapter();
|
||||
const isFLUX = useAppSelector(selectIsFLUX);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
@@ -31,21 +36,30 @@ export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
|
||||
data-testid="control-layers-add-layer-menu-button"
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||
{t('controlLayers.globalIPAdapter')}
|
||||
</MenuItem>
|
||||
<MenuGroup title={t('controlLayers.global')}>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addGlobalReferenceImage} isDisabled={isFLUX}>
|
||||
{t('controlLayers.globalReferenceImage')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('controlLayers.regional')}>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
|
||||
{t('controlLayers.inpaintMask')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance} isDisabled={isFLUX}>
|
||||
{t('controlLayers.regionalGuidance')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRegionalReferenceImage} isDisabled={isFLUX}>
|
||||
{t('controlLayers.regionalReferenceImage')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
<MenuGroup title={t('controlLayers.layer_other')}>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer} isDisabled={isFLUX}>
|
||||
{t('controlLayers.controlLayer')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
|
||||
{t('controlLayers.rasterLayer')}
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers
|
||||
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { EntityListSelectedEntityActionBarSaveToAssetsButton } from './EntityListSelectedEntityActionBarSaveToAssetsButton';
|
||||
|
||||
export const EntityListSelectedEntityActionBar = memo(() => {
|
||||
return (
|
||||
<Flex w="full" gap={2} alignItems="center" ps={1}>
|
||||
@@ -16,6 +18,7 @@ export const EntityListSelectedEntityActionBar = memo(() => {
|
||||
<Flex h="full">
|
||||
<EntityListSelectedEntityActionBarFilterButton />
|
||||
<EntityListSelectedEntityActionBarTransformButton />
|
||||
<EntityListSelectedEntityActionBarSaveToAssetsButton />
|
||||
<EntityListSelectedEntityActionBarDuplicateButton />
|
||||
<EntityListGlobalActionBarAddLayerMenu />
|
||||
</Flex>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
||||
<FormControl
|
||||
w="min-content"
|
||||
gap={2}
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
||||
>
|
||||
<FormLabel m={0}>{t('controlLayers.opacity')}</FormLabel>
|
||||
<PopoverAnchor>
|
||||
@@ -167,7 +167,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
||||
position="absolute"
|
||||
insetInlineEnd={0}
|
||||
h="full"
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</NumberInput>
|
||||
@@ -185,7 +185,7 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
|
||||
marks={marks}
|
||||
formatValue={formatSliderValue}
|
||||
alwaysShowMarks
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'reference_image'}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { isSaveableEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
|
||||
export const EntityListSelectedEntityActionBarSaveToAssetsButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isBusy = useCanvasIsBusy();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
|
||||
const saveLayerToAssets = useSaveLayerToAssets();
|
||||
const onClick = useCallback(() => {
|
||||
saveLayerToAssets(adapter);
|
||||
}, [saveLayerToAssets, adapter]);
|
||||
|
||||
if (!selectedEntityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isSaveableEntityIdentifier(selectedEntityIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
isDisabled={!selectedEntityIdentifier || isBusy}
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('controlLayers.saveLayerToAssets')}
|
||||
tooltip={t('controlLayers.saveLayerToAssets')}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
EntityListSelectedEntityActionBarSaveToAssetsButton.displayName = 'EntityListSelectedEntityActionBarSaveToAssetsButton';
|
||||
@@ -16,10 +16,10 @@ export const ControlLayerControlAdapterControlMode = memo(({ controlMode, onChan
|
||||
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' },
|
||||
{ label: t('controlLayers.controlMode.balanced'), value: 'balanced' },
|
||||
{ label: t('controlLayers.controlMode.prompt'), value: 'more_prompt' },
|
||||
{ label: t('controlLayers.controlMode.control'), value: 'more_control' },
|
||||
{ label: t('controlLayers.controlMode.megaControl'), value: 'unbalanced' },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
@@ -44,7 +44,7 @@ export const ControlLayerControlAdapterControlMode = memo(({ controlMode, onChan
|
||||
return (
|
||||
<FormControl>
|
||||
<InformationalPopover feature="controlNetControlMode">
|
||||
<FormLabel m={0}>{t('controlnet.control')}</FormLabel>
|
||||
<FormLabel m={0}>{t('controlLayers.controlMode.controlMode')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox
|
||||
value={value}
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
|
||||
<Combobox
|
||||
options={options}
|
||||
placeholder={t('controlnet.selectModel')}
|
||||
placeholder={t('common.placeholderSelectAModel')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { ControlLayerMenuItemsConvertControlToRaster } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsConvertControlToRaster';
|
||||
import { ControlLayerMenuItemsTransparencyEffect } from 'features/controlLayers/components/ControlLayer/ControlLayerMenuItemsTransparencyEffect';
|
||||
@@ -19,6 +21,8 @@ export const ControlLayerMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const IPAdapter = memo(({ id }: Props) => {
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'ip_adapter' }), [id]);
|
||||
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'reference_image' }), [id]);
|
||||
|
||||
return (
|
||||
<EntityIdentifierContext.Provider value={entityIdentifier}>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Flex, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
|
||||
import { useNanoid } from 'common/hooks/useNanoid';
|
||||
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
|
||||
import type { ImageWithDims } from 'features/controlLayers/store/types';
|
||||
@@ -15,94 +15,91 @@ import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO, PostUploadAction } from 'services/api/types';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type Props = {
|
||||
image: ImageWithDims | null;
|
||||
onChangeImage: (imageDTO: ImageDTO | null) => void;
|
||||
ipAdapterId: string; // required for the dnd/upload interactions
|
||||
droppableData: TypesafeDroppableData;
|
||||
postUploadAction: PostUploadAction;
|
||||
};
|
||||
|
||||
export const IPAdapterImagePreview = memo(
|
||||
({ image, onChangeImage, ipAdapterId, droppableData, postUploadAction }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useStore($isConnected);
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
const shift = useShiftModifier();
|
||||
export const IPAdapterImagePreview = memo(({ image, onChangeImage, droppableData, postUploadAction }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const isConnected = useStore($isConnected);
|
||||
const optimalDimension = useAppSelector(selectOptimalDimension);
|
||||
const shift = useShiftModifier();
|
||||
const dndId = useNanoid('ip_adapter_image_preview');
|
||||
|
||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||
image?.image_name ?? skipToken
|
||||
);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
const { currentData: controlImage, isError: isErrorControlImage } = useGetImageDTOQuery(
|
||||
image?.image_name ?? skipToken
|
||||
);
|
||||
const handleResetControlImage = useCallback(() => {
|
||||
onChangeImage(null);
|
||||
}, [onChangeImage]);
|
||||
|
||||
const handleSetControlImageToDimensions = useCallback(() => {
|
||||
if (!controlImage) {
|
||||
return;
|
||||
}
|
||||
const handleSetControlImageToDimensions = useCallback(() => {
|
||||
if (!controlImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { updateAspectRatio: true, clamp: true };
|
||||
if (shift) {
|
||||
const { width, height } = controlImage;
|
||||
dispatch(bboxWidthChanged({ width, ...options }));
|
||||
dispatch(bboxHeightChanged({ height, ...options }));
|
||||
} else {
|
||||
const { width, height } = calculateNewSize(
|
||||
controlImage.width / controlImage.height,
|
||||
optimalDimension * optimalDimension
|
||||
);
|
||||
dispatch(bboxWidthChanged({ width, ...options }));
|
||||
dispatch(bboxHeightChanged({ height, ...options }));
|
||||
}
|
||||
}, [controlImage, dispatch, optimalDimension, shift]);
|
||||
const options = { updateAspectRatio: true, clamp: true };
|
||||
if (shift) {
|
||||
const { width, height } = controlImage;
|
||||
dispatch(bboxWidthChanged({ width, ...options }));
|
||||
dispatch(bboxHeightChanged({ height, ...options }));
|
||||
} else {
|
||||
const { width, height } = calculateNewSize(
|
||||
controlImage.width / controlImage.height,
|
||||
optimalDimension * optimalDimension
|
||||
);
|
||||
dispatch(bboxWidthChanged({ width, ...options }));
|
||||
dispatch(bboxHeightChanged({ height, ...options }));
|
||||
}
|
||||
}, [controlImage, dispatch, optimalDimension, shift]);
|
||||
|
||||
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
|
||||
if (controlImage) {
|
||||
return {
|
||||
id: ipAdapterId,
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO: controlImage },
|
||||
};
|
||||
}
|
||||
}, [controlImage, ipAdapterId]);
|
||||
const draggableData = useMemo<ImageDraggableData | undefined>(() => {
|
||||
if (controlImage) {
|
||||
return {
|
||||
id: dndId,
|
||||
payloadType: 'IMAGE_DTO',
|
||||
payload: { imageDTO: controlImage },
|
||||
};
|
||||
}
|
||||
}, [controlImage, dndId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && isErrorControlImage) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isConnected, isErrorControlImage]);
|
||||
useEffect(() => {
|
||||
if (isConnected && isErrorControlImage) {
|
||||
handleResetControlImage();
|
||||
}
|
||||
}, [handleResetControlImage, isConnected, isErrorControlImage]);
|
||||
|
||||
return (
|
||||
<Flex position="relative" w={36} h={36} alignItems="center">
|
||||
<IAIDndImage
|
||||
draggableData={draggableData}
|
||||
droppableData={droppableData}
|
||||
imageDTO={controlImage}
|
||||
postUploadAction={postUploadAction}
|
||||
/>
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" alignItems="center">
|
||||
<IAIDndImage
|
||||
draggableData={draggableData}
|
||||
droppableData={droppableData}
|
||||
imageDTO={controlImage}
|
||||
postUploadAction={postUploadAction}
|
||||
/>
|
||||
|
||||
{controlImage && (
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('controlnet.resetControlImage')}
|
||||
/>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleSetControlImageToDimensions}
|
||||
icon={<PiRulerBold size={16} />}
|
||||
tooltip={
|
||||
shift ? t('controlnet.setControlImageDimensionsForce') : t('controlnet.setControlImageDimensions')
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
{controlImage && (
|
||||
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleResetControlImage}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('common.reset')}
|
||||
/>
|
||||
<IAIDndImageIcon
|
||||
onClick={handleSetControlImageToDimensions}
|
||||
icon={<PiRulerBold size={16} />}
|
||||
tooltip={shift ? t('controlLayers.useSizeIgnoreModel') : t('controlLayers.useSizeOptimizeForModel')}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
IPAdapterImagePreview.displayName = 'IPAdapterImagePreview';
|
||||
|
||||
@@ -9,10 +9,10 @@ import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/cont
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.ipAdapters.entities.map(mapId).reverse();
|
||||
return canvas.referenceImages.entities.map(mapId).reverse();
|
||||
});
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'ip_adapter';
|
||||
return selectedEntityIdentifier?.type === 'reference_image';
|
||||
});
|
||||
|
||||
export const IPAdapterList = memo(() => {
|
||||
@@ -25,7 +25,7 @@ export const IPAdapterList = memo(() => {
|
||||
|
||||
if (ipaIds.length > 0) {
|
||||
return (
|
||||
<CanvasEntityGroupList type="ip_adapter" isSelected={isSelected}>
|
||||
<CanvasEntityGroupList type="reference_image" isSelected={isSelected}>
|
||||
{ipaIds.map((id) => (
|
||||
<IPAdapter key={id} id={id} />
|
||||
))}
|
||||
|
||||
@@ -16,9 +16,9 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const options: { label: string; value: IPMethodV2 }[] = useMemo(
|
||||
() => [
|
||||
{ label: t('controlnet.full'), value: 'full' },
|
||||
{ label: `${t('controlnet.style')} (${t('common.beta')})`, value: 'style' },
|
||||
{ label: `${t('controlnet.composition')} (${t('common.beta')})`, value: 'composition' },
|
||||
{ label: t('controlLayers.ipAdapterMethod.full'), value: 'full' },
|
||||
{ label: `${t('controlLayers.ipAdapterMethod.style')} (${t('common.beta')})`, value: 'style' },
|
||||
{ label: `${t('controlLayers.ipAdapterMethod.composition')} (${t('common.beta')})`, value: 'composition' },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
@@ -34,7 +34,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
|
||||
return (
|
||||
<FormControl>
|
||||
<InformationalPopover feature="ipAdapterMethod">
|
||||
<FormLabel>{t('controlnet.ipAdapterMethod')}</FormLabel>
|
||||
<FormLabel>{t('controlLayers.ipAdapterMethod.ipAdapterMethod')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<Combobox value={value} options={options} onChange={_onChange} />
|
||||
</FormControl>
|
||||
|
||||
@@ -70,12 +70,12 @@ export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel,
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={4}>
|
||||
<Flex gap={2}>
|
||||
<Tooltip label={selectedModel?.description}>
|
||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} w="full">
|
||||
<Combobox
|
||||
options={options}
|
||||
placeholder={t('controlnet.selectModel')}
|
||||
placeholder={t('common.placeholderSelectAModel')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
@@ -86,7 +86,7 @@ export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel,
|
||||
<FormControl isInvalid={!value || currentBaseModel !== selectedModel?.base} width="max-content" minWidth={28}>
|
||||
<Combobox
|
||||
options={CLIP_VISION_OPTIONS}
|
||||
placeholder={t('controlnet.selectCLIPVisionModel')}
|
||||
placeholder={t('common.placeholderSelectAModel')}
|
||||
value={clipVisionModelValue}
|
||||
onChange={_onChangeCLIPVisionModel}
|
||||
/>
|
||||
|
||||
@@ -6,15 +6,15 @@ import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/c
|
||||
import { Weight } from 'features/controlLayers/components/common/Weight';
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { usePullBboxIntoIPAdapter } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { usePullBboxIntoGlobalReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
ipaBeginEndStepPctChanged,
|
||||
ipaCLIPVisionModelChanged,
|
||||
ipaImageChanged,
|
||||
ipaMethodChanged,
|
||||
ipaModelChanged,
|
||||
ipaWeightChanged,
|
||||
referenceImageIPAdapterBeginEndStepPctChanged,
|
||||
referenceImageIPAdapterCLIPVisionModelChanged,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
referenceImageIPAdapterMethodChanged,
|
||||
referenceImageIPAdapterModelChanged,
|
||||
referenceImageIPAdapterWeightChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
@@ -30,7 +30,7 @@ import { IPAdapterModel } from './IPAdapterModel';
|
||||
export const IPAdapterSettings = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext('ip_adapter');
|
||||
const entityIdentifier = useEntityIdentifierContext('reference_image');
|
||||
const selectIPAdapter = useMemo(
|
||||
() => createSelector(selectCanvasSlice, (s) => selectEntityOrThrow(s, entityIdentifier).ipAdapter),
|
||||
[entityIdentifier]
|
||||
@@ -39,42 +39,42 @@ export const IPAdapterSettings = memo(() => {
|
||||
|
||||
const onChangeBeginEndStepPct = useCallback(
|
||||
(beginEndStepPct: [number, number]) => {
|
||||
dispatch(ipaBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
|
||||
dispatch(referenceImageIPAdapterBeginEndStepPctChanged({ entityIdentifier, beginEndStepPct }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeWeight = useCallback(
|
||||
(weight: number) => {
|
||||
dispatch(ipaWeightChanged({ entityIdentifier, weight }));
|
||||
dispatch(referenceImageIPAdapterWeightChanged({ entityIdentifier, weight }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeIPMethod = useCallback(
|
||||
(method: IPMethodV2) => {
|
||||
dispatch(ipaMethodChanged({ entityIdentifier, method }));
|
||||
dispatch(referenceImageIPAdapterMethodChanged({ entityIdentifier, method }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(modelConfig: IPAdapterModelConfig) => {
|
||||
dispatch(ipaModelChanged({ entityIdentifier, modelConfig }));
|
||||
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier, modelConfig }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeCLIPVisionModel = useCallback(
|
||||
(clipVisionModel: CLIPVisionModelV2) => {
|
||||
dispatch(ipaCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
|
||||
dispatch(referenceImageIPAdapterCLIPVisionModelChanged({ entityIdentifier, clipVisionModel }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onChangeImage = useCallback(
|
||||
(imageDTO: ImageDTO | null) => {
|
||||
dispatch(ipaImageChanged({ entityIdentifier, imageDTO }));
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
||||
},
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
@@ -87,13 +87,13 @@ export const IPAdapterSettings = memo(() => {
|
||||
() => ({ type: 'SET_IPA_IMAGE', id: entityIdentifier.id }),
|
||||
[entityIdentifier.id]
|
||||
);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoIPAdapter(entityIdentifier);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoGlobalReferenceImage(entityIdentifier);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
return (
|
||||
<CanvasEntitySettingsWrapper>
|
||||
<Flex flexDir="column" gap={4} position="relative" w="full">
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
<Flex flexDir="column" gap={2} position="relative" w="full">
|
||||
<Flex gap={2} alignItems="center" w="full">
|
||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||
<IPAdapterModel
|
||||
modelKey={ipAdapter.model?.key ?? null}
|
||||
@@ -106,22 +106,21 @@ export const IPAdapterSettings = memo(() => {
|
||||
onClick={pullBboxIntoIPAdapter}
|
||||
isDisabled={isBusy}
|
||||
variant="ghost"
|
||||
aria-label={t('controlLayers.pullBboxIntoIPAdapter')}
|
||||
tooltip={t('controlLayers.pullBboxIntoIPAdapter')}
|
||||
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
tooltip={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={4} w="full" alignItems="center">
|
||||
<Flex flexDir="column" gap={3} w="full">
|
||||
<Flex gap={2} w="full" alignItems="center">
|
||||
<Flex flexDir="column" gap={2} w="full">
|
||||
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
|
||||
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
|
||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image ?? null}
|
||||
onChangeImage={onChangeImage}
|
||||
ipAdapterId={entityIdentifier.id}
|
||||
droppableData={droppableData}
|
||||
postUploadAction={postUploadAction}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MenuDivider } from '@invoke-ai/ui-library';
|
||||
import { CanvasEntityMenuItemsArrange } from 'features/controlLayers/components/common/CanvasEntityMenuItemsArrange';
|
||||
import { CanvasEntityMenuItemsCopyToClipboard } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopyToClipboard';
|
||||
import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete';
|
||||
import { CanvasEntityMenuItemsDuplicate } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDuplicate';
|
||||
import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter';
|
||||
import { CanvasEntityMenuItemsSave } from 'features/controlLayers/components/common/CanvasEntityMenuItemsSave';
|
||||
import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform';
|
||||
import { RasterLayerMenuItemsConvertRasterToControl } from 'features/controlLayers/components/RasterLayer/RasterLayerMenuItemsConvertRasterToControl';
|
||||
import { memo } from 'react';
|
||||
@@ -17,6 +19,8 @@ export const RasterLayerMenuItems = memo(() => {
|
||||
<CanvasEntityMenuItemsArrange />
|
||||
<MenuDivider />
|
||||
<CanvasEntityMenuItemsDuplicate />
|
||||
<CanvasEntityMenuItemsCopyToClipboard />
|
||||
<CanvasEntityMenuItemsSave />
|
||||
<CanvasEntityMenuItemsDelete />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
|
||||
onClick={addRegionalGuidancePositivePrompt}
|
||||
isDisabled={!validActions.canAddPositivePrompt}
|
||||
>
|
||||
{t('common.positivePrompt')}
|
||||
{t('controlLayers.prompt')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -42,10 +42,10 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
|
||||
onClick={addRegionalGuidanceNegativePrompt}
|
||||
isDisabled={!validActions.canAddNegativePrompt}
|
||||
>
|
||||
{t('common.negativePrompt')}
|
||||
{t('controlLayers.negativePrompt')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addRegionalGuidanceIPAdapter}>
|
||||
{t('common.ipAdapter')}
|
||||
{t('controlLayers.referenceImage')}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
@@ -14,11 +14,12 @@ export const RegionalGuidanceDeletePromptButton = memo(({ onDelete }: Props) =>
|
||||
<IconButton
|
||||
variant="link"
|
||||
aria-label={t('controlLayers.deletePrompt')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
icon={<PiTrashSimpleFill />}
|
||||
onClick={onDelete}
|
||||
flexGrow={0}
|
||||
size="sm"
|
||||
p={0}
|
||||
colorScheme="error"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { selectCanvasSlice, selectSelectedEntityIdentifier } from 'features/cont
|
||||
import { memo } from 'react';
|
||||
|
||||
const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.regions.entities.map(mapId).reverse();
|
||||
return canvas.regionalGuidance.entities.map(mapId).reverse();
|
||||
});
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'regional_guidance';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapt
|
||||
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
|
||||
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { usePullBboxIntoRegionalGuidanceIPAdapter } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { usePullBboxIntoRegionalGuidanceReferenceImage } from 'features/controlLayers/hooks/saveCanvasHooks';
|
||||
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import {
|
||||
rgIPAdapterBeginEndStepPctChanged,
|
||||
@@ -18,111 +18,114 @@ import {
|
||||
rgIPAdapterModelChanged,
|
||||
rgIPAdapterWeightChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectRegionalGuidanceIPAdapter } from 'features/controlLayers/store/selectors';
|
||||
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
|
||||
import type { CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
|
||||
import type { RGIPAdapterImageDropData } from 'features/dnd/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiBoundingBoxBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { PiBoundingBoxBold, PiTrashSimpleFill } from 'react-icons/pi';
|
||||
import type { ImageDTO, IPAdapterModelConfig, RGIPAdapterImagePostUploadAction } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type Props = {
|
||||
ipAdapterId: string;
|
||||
ipAdapterNumber: number;
|
||||
referenceImageId: string;
|
||||
};
|
||||
|
||||
export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterNumber }: Props) => {
|
||||
export const RegionalGuidanceIPAdapterSettings = memo(({ referenceImageId }: Props) => {
|
||||
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onDeleteIPAdapter = useCallback(() => {
|
||||
dispatch(rgIPAdapterDeleted({ entityIdentifier, ipAdapterId }));
|
||||
}, [dispatch, entityIdentifier, ipAdapterId]);
|
||||
dispatch(rgIPAdapterDeleted({ entityIdentifier, referenceImageId }));
|
||||
}, [dispatch, entityIdentifier, referenceImageId]);
|
||||
const selectIPAdapter = useMemo(
|
||||
() =>
|
||||
createSelector(selectCanvasSlice, (canvas) => {
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(canvas, entityIdentifier, ipAdapterId);
|
||||
assert(ipAdapter, `Regional GuidanceIP Adapter with id ${ipAdapterId} not found`);
|
||||
return ipAdapter;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(canvas, entityIdentifier, referenceImageId);
|
||||
assert(referenceImage, `Regional Guidance IP Adapter with id ${referenceImageId} not found`);
|
||||
return referenceImage.ipAdapter;
|
||||
}),
|
||||
[entityIdentifier, ipAdapterId]
|
||||
[entityIdentifier, referenceImageId]
|
||||
);
|
||||
const ipAdapter = useAppSelector(selectIPAdapter);
|
||||
|
||||
const onChangeBeginEndStepPct = useCallback(
|
||||
(beginEndStepPct: [number, number]) => {
|
||||
dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, ipAdapterId, beginEndStepPct }));
|
||||
dispatch(rgIPAdapterBeginEndStepPctChanged({ entityIdentifier, referenceImageId, beginEndStepPct }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeWeight = useCallback(
|
||||
(weight: number) => {
|
||||
dispatch(rgIPAdapterWeightChanged({ entityIdentifier, ipAdapterId, weight }));
|
||||
dispatch(rgIPAdapterWeightChanged({ entityIdentifier, referenceImageId, weight }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeIPMethod = useCallback(
|
||||
(method: IPMethodV2) => {
|
||||
dispatch(rgIPAdapterMethodChanged({ entityIdentifier, ipAdapterId, method }));
|
||||
dispatch(rgIPAdapterMethodChanged({ entityIdentifier, referenceImageId, method }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeModel = useCallback(
|
||||
(modelConfig: IPAdapterModelConfig) => {
|
||||
dispatch(rgIPAdapterModelChanged({ entityIdentifier, ipAdapterId, modelConfig }));
|
||||
dispatch(rgIPAdapterModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeCLIPVisionModel = useCallback(
|
||||
(clipVisionModel: CLIPVisionModelV2) => {
|
||||
dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, ipAdapterId, clipVisionModel }));
|
||||
dispatch(rgIPAdapterCLIPVisionModelChanged({ entityIdentifier, referenceImageId, clipVisionModel }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const onChangeImage = useCallback(
|
||||
(imageDTO: ImageDTO | null) => {
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, ipAdapterId, imageDTO }));
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||
},
|
||||
[dispatch, entityIdentifier, ipAdapterId]
|
||||
[dispatch, entityIdentifier, referenceImageId]
|
||||
);
|
||||
|
||||
const droppableData = useMemo<RGIPAdapterImageDropData>(
|
||||
() => ({
|
||||
actionType: 'SET_RG_IP_ADAPTER_IMAGE',
|
||||
context: { id: entityIdentifier.id, ipAdapterId },
|
||||
context: { id: entityIdentifier.id, referenceImageId: referenceImageId },
|
||||
id: entityIdentifier.id,
|
||||
}),
|
||||
[entityIdentifier.id, ipAdapterId]
|
||||
[entityIdentifier.id, referenceImageId]
|
||||
);
|
||||
const postUploadAction = useMemo<RGIPAdapterImagePostUploadAction>(
|
||||
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, ipAdapterId }),
|
||||
[entityIdentifier.id, ipAdapterId]
|
||||
() => ({ type: 'SET_RG_IP_ADAPTER_IMAGE', id: entityIdentifier.id, referenceImageId: referenceImageId }),
|
||||
[entityIdentifier.id, referenceImageId]
|
||||
);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceIPAdapter(entityIdentifier, ipAdapterId);
|
||||
const pullBboxIntoIPAdapter = usePullBboxIntoRegionalGuidanceReferenceImage(entityIdentifier, referenceImageId);
|
||||
const isBusy = useCanvasIsBusy();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={3}>
|
||||
<Flex alignItems="center" gap={3}>
|
||||
<Text fontWeight="semibold" color="base.400">{`IP Adapter ${ipAdapterNumber}`}</Text>
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Text fontWeight="semibold" color="base.400">
|
||||
{t('controlLayers.referenceImage')}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={<PiTrashSimpleBold />}
|
||||
aria-label="Delete IP Adapter"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
icon={<PiTrashSimpleFill />}
|
||||
tooltip={t('controlLayers.deleteReferenceImage')}
|
||||
aria-label={t('controlLayers.deleteReferenceImage')}
|
||||
onClick={onDeleteIPAdapter}
|
||||
variant="ghost"
|
||||
colorScheme="error"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir="column" gap={4} position="relative" w="full">
|
||||
<Flex gap={3} alignItems="center" w="full">
|
||||
<Flex flexDir="column" gap={2} position="relative" w="full">
|
||||
<Flex gap={2} alignItems="center" w="full">
|
||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||
<IPAdapterModel
|
||||
modelKey={ipAdapter.model?.key ?? null}
|
||||
@@ -135,22 +138,21 @@ export const RegionalGuidanceIPAdapterSettings = memo(({ ipAdapterId, ipAdapterN
|
||||
onClick={pullBboxIntoIPAdapter}
|
||||
isDisabled={isBusy}
|
||||
variant="ghost"
|
||||
aria-label={t('controlLayers.pullBboxIntoIPAdapter')}
|
||||
tooltip={t('controlLayers.pullBboxIntoIPAdapter')}
|
||||
aria-label={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
tooltip={t('controlLayers.pullBboxIntoReferenceImage')}
|
||||
icon={<PiBoundingBoxBold />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={4} w="full" alignItems="center">
|
||||
<Flex flexDir="column" gap={3} w="full">
|
||||
<Flex gap={2} w="full">
|
||||
<Flex flexDir="column" gap={2} w="full">
|
||||
<IPAdapterMethod method={ipAdapter.method} onChange={onChangeIPMethod} />
|
||||
<Weight weight={ipAdapter.weight} onChange={onChangeWeight} />
|
||||
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
|
||||
</Flex>
|
||||
<Flex alignItems="center" justifyContent="center" h={36} w={36} aspectRatio="1/1">
|
||||
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1">
|
||||
<IPAdapterImagePreview
|
||||
image={ipAdapter.image ?? null}
|
||||
onChangeImage={onChangeImage}
|
||||
ipAdapterId={ipAdapter.id}
|
||||
droppableData={droppableData}
|
||||
postUploadAction={postUploadAction}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const RegionalGuidanceIPAdapters = memo(() => {
|
||||
const selectIPAdapterIds = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
const ipAdapterIds = selectEntityOrThrow(canvas, entityIdentifier).ipAdapters.map(({ id }) => id);
|
||||
const ipAdapterIds = selectEntityOrThrow(canvas, entityIdentifier).referenceImages.map(({ id }) => id);
|
||||
if (ipAdapterIds.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export const RegionalGuidanceIPAdapters = memo(() => {
|
||||
{ipAdapterIds.map((ipAdapterId, index) => (
|
||||
<Fragment key={ipAdapterId}>
|
||||
{index > 0 && <Divider />}
|
||||
<RegionalGuidanceIPAdapterSettings ipAdapterId={ipAdapterId} ipAdapterNumber={index + 1} />
|
||||
<RegionalGuidanceIPAdapterSettings referenceImageId={ipAdapterId} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -33,7 +33,7 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
|
||||
{t('controlLayers.addNegativePrompt')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
|
||||
{t('controlLayers.addIPAdapter')}
|
||||
{t('controlLayers.addReferenceImage')}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ export const RegionalGuidanceSettings = memo(() => {
|
||||
return {
|
||||
hasPositivePrompt: entity.positivePrompt !== null,
|
||||
hasNegativePrompt: entity.negativePrompt !== null,
|
||||
hasIPAdapters: entity.ipAdapters.length > 0,
|
||||
hasIPAdapters: entity.referenceImages.length > 0,
|
||||
};
|
||||
}),
|
||||
[entityIdentifier]
|
||||
|
||||
@@ -2,8 +2,16 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useGetQueueCountsByDestinationQuery } from 'services/api/endpoints/queue';
|
||||
|
||||
// This hook just serves as a persistent subscriber for the queue count query.
|
||||
const queueCountArg = { destination: 'canvas' };
|
||||
const useCanvasQueueCountWatcher = () => {
|
||||
useGetQueueCountsByDestinationQuery(queueCountArg);
|
||||
};
|
||||
|
||||
export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => {
|
||||
useCanvasQueueCountWatcher();
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
if (!isStaging) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
PiEyeBold,
|
||||
PiEyeSlashBold,
|
||||
PiFloppyDiskBold,
|
||||
PiStackPlusBold,
|
||||
PiTrashSimpleBold,
|
||||
PiXBold,
|
||||
} from 'react-icons/pi';
|
||||
@@ -180,14 +179,6 @@ export const StagingAreaToolbar = memo(() => {
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
icon={<PiStackPlusBold />}
|
||||
onClick={onSaveStagingImage}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.discardCurrent')}`}
|
||||
aria-label={t('unifiedCanvas.discardCurrent')}
|
||||
|
||||
@@ -18,9 +18,9 @@ export const BeginEndStepPct = memo(({ beginEndStepPct, onChange }: Props) => {
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<FormControl orientation="horizontal" pe={1}>
|
||||
<FormControl orientation="horizontal" pe={2}>
|
||||
<InformationalPopover feature="controlNetBeginEnd">
|
||||
<FormLabel m={0}>{t('controlnet.beginEndStepPercentShort')}</FormLabel>
|
||||
<FormLabel m={0}>{t('controlLayers.beginEndStepPercentShort')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeRangeSlider
|
||||
aria-label={ariaLabel}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
useAddControlLayer,
|
||||
useAddGlobalReferenceImage,
|
||||
useAddInpaintMask,
|
||||
useAddIPAdapter,
|
||||
useAddRasterLayer,
|
||||
useAddRegionalGuidance,
|
||||
} from 'features/controlLayers/hooks/addLayerHooks';
|
||||
@@ -23,7 +23,7 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
|
||||
const addRegionalGuidance = useAddRegionalGuidance();
|
||||
const addRasterLayer = useAddRasterLayer();
|
||||
const addControlLayer = useAddControlLayer();
|
||||
const addIPAdapter = useAddIPAdapter();
|
||||
const addGlobalReferenceImage = useAddGlobalReferenceImage();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
switch (type) {
|
||||
@@ -39,11 +39,11 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
|
||||
case 'control_layer':
|
||||
addControlLayer();
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
addIPAdapter();
|
||||
case 'reference_image':
|
||||
addGlobalReferenceImage();
|
||||
break;
|
||||
}
|
||||
}, [addControlLayer, addIPAdapter, addInpaintMask, addRasterLayer, addRegionalGuidance, type]);
|
||||
}, [addControlLayer, addGlobalReferenceImage, addInpaintMask, addRasterLayer, addRegionalGuidance, type]);
|
||||
|
||||
const label = useMemo(() => {
|
||||
switch (type) {
|
||||
@@ -55,8 +55,8 @@ export const CanvasEntityAddOfTypeButton = memo(({ type }: Props) => {
|
||||
return t('controlLayers.addRasterLayer');
|
||||
case 'control_layer':
|
||||
return t('controlLayers.addControlLayer');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.addIPAdapter');
|
||||
case 'reference_image':
|
||||
return t('controlLayers.addGlobalReferenceImage');
|
||||
}
|
||||
}, [type, t]);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
||||
const title = useEntityTypeTitle(type);
|
||||
const collapse = useBoolean(true);
|
||||
const canMergeVisible = useMemo(() => type === 'raster_layer' || type === 'inpaint_mask', [type]);
|
||||
const canHideAll = useMemo(() => type !== 'ip_adapter', [type]);
|
||||
const canHideAll = useMemo(() => type !== 'reference_image', [type]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="full">
|
||||
|
||||
@@ -44,7 +44,7 @@ export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (entityIdentifier.type === 'ip_adapter') {
|
||||
if (entityIdentifier.type === 'reference_image') {
|
||||
return (
|
||||
<MenuList>
|
||||
<IPAdapterMenuItems />
|
||||
|
||||
@@ -12,7 +12,7 @@ export const CanvasEntityHeaderCommonActions = memo(() => {
|
||||
return (
|
||||
<Flex alignSelf="stretch">
|
||||
<CanvasEntityIsBookmarkedForQuickSwitchToggle />
|
||||
{entityIdentifier.type !== 'ip_adapter' && <CanvasEntityIsLockedToggle />}
|
||||
{entityIdentifier.type !== 'reference_image' && <CanvasEntityIsLockedToggle />}
|
||||
<CanvasEntityEnabledToggle />
|
||||
<CanvasEntityDeleteButton />
|
||||
</Flex>
|
||||
|
||||
@@ -31,18 +31,18 @@ const getIndexAndCount = (
|
||||
};
|
||||
} else if (type === 'regional_guidance') {
|
||||
return {
|
||||
index: canvas.regions.entities.findIndex((entity) => entity.id === id),
|
||||
count: canvas.regions.entities.length,
|
||||
index: canvas.regionalGuidance.entities.findIndex((entity) => entity.id === id),
|
||||
count: canvas.regionalGuidance.entities.length,
|
||||
};
|
||||
} else if (type === 'inpaint_mask') {
|
||||
return {
|
||||
index: canvas.inpaintMasks.entities.findIndex((entity) => entity.id === id),
|
||||
count: canvas.inpaintMasks.entities.length,
|
||||
};
|
||||
} else if (type === 'ip_adapter') {
|
||||
} else if (type === 'reference_image') {
|
||||
return {
|
||||
index: canvas.ipAdapters.entities.findIndex((entity) => entity.id === id),
|
||||
count: canvas.ipAdapters.entities.length,
|
||||
index: canvas.referenceImages.entities.findIndex((entity) => entity.id === id),
|
||||
count: canvas.referenceImages.entities.length,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCopyBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsCopyToClipboard = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const copyLayerToClipboard = useCopyLayerToClipboard();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
copyLayerToClipboard(adapter);
|
||||
}, [copyLayerToClipboard, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiCopyBold />} isDisabled={!isInteractable}>
|
||||
{t('controlLayers.copyToClipboard')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsCopyToClipboard.displayName = 'CanvasEntityMenuItemsCopyToClipboard';
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable';
|
||||
import { useSaveLayerToAssets } from 'features/controlLayers/hooks/useSaveLayerToAssets';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFloppyDiskBold } from 'react-icons/pi';
|
||||
|
||||
export const CanvasEntityMenuItemsSave = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const adapter = useEntityAdapterSafe(entityIdentifier);
|
||||
const isInteractable = useIsEntityInteractable(entityIdentifier);
|
||||
const saveLayerToAssets = useSaveLayerToAssets();
|
||||
const onClick = useCallback(() => {
|
||||
saveLayerToAssets(adapter);
|
||||
}, [saveLayerToAssets, adapter]);
|
||||
|
||||
return (
|
||||
<MenuItem onClick={onClick} icon={<PiFloppyDiskBold />} isDisabled={!isInteractable}>
|
||||
{t('controlLayers.saveLayerToAssets')}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
|
||||
CanvasEntityMenuItemsSave.displayName = 'CanvasEntityMenuItemsSave';
|
||||
@@ -7,7 +7,7 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
|
||||
import { useEntityTypeCount } from 'features/controlLayers/hooks/useEntityTypeCount';
|
||||
import { inpaintMaskAdded, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Weight = memo(({ weight, onChange }: Props) => {
|
||||
return (
|
||||
<FormControl orientation="horizontal">
|
||||
<InformationalPopover feature="controlNetWeight">
|
||||
<FormLabel m={0}>{t('controlnet.weight')}</FormLabel>
|
||||
<FormLabel m={0}>{t('controlLayers.weight')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeSlider
|
||||
value={weight}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
inpaintMaskAdded,
|
||||
ipaAdded,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
rgAdded,
|
||||
rgIPAdapterAdded,
|
||||
rgNegativePromptChanged,
|
||||
@@ -16,11 +17,12 @@ import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
CanvasRegionalGuidanceState,
|
||||
ControlNetConfig,
|
||||
IPAdapterConfig,
|
||||
T2IAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { initialControlNet, initialIPAdapter, initialT2IAdapter } from 'features/controlLayers/store/types';
|
||||
import { initialControlNet, initialIPAdapter, initialT2IAdapter } from 'features/controlLayers/store/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { useCallback } from 'react';
|
||||
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
|
||||
@@ -71,78 +73,92 @@ export const selectDefaultIPAdapter = createSelector(
|
||||
export const useAddControlLayer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
|
||||
const addControlLayer = useCallback(() => {
|
||||
const func = useCallback(() => {
|
||||
const overrides = { controlAdapter: defaultControlAdapter };
|
||||
dispatch(controlLayerAdded({ isSelected: true, overrides }));
|
||||
}, [defaultControlAdapter, dispatch]);
|
||||
|
||||
return addControlLayer;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddRasterLayer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const addRasterLayer = useCallback(() => {
|
||||
const func = useCallback(() => {
|
||||
dispatch(rasterLayerAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
return addRasterLayer;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddInpaintMask = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const addInpaintMask = useCallback(() => {
|
||||
const func = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
return addInpaintMask;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddRegionalGuidance = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const addRegionalGuidance = useCallback(() => {
|
||||
const func = useCallback(() => {
|
||||
dispatch(rgAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
return addRegionalGuidance;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddIPAdapter = () => {
|
||||
export const useAddRegionalReferenceImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
const addControlLayer = useCallback(() => {
|
||||
const overrides = { ipAdapter: defaultIPAdapter };
|
||||
dispatch(ipaAdded({ isSelected: true, overrides }));
|
||||
|
||||
const func = useCallback(() => {
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
referenceImages: [{ id: getPrefixedId('regional_guidance_reference_image'), ipAdapter: defaultIPAdapter }],
|
||||
};
|
||||
dispatch(rgAdded({ isSelected: true, overrides }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
|
||||
return addControlLayer;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddGlobalReferenceImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
const func = useCallback(() => {
|
||||
const overrides = { ipAdapter: defaultIPAdapter };
|
||||
dispatch(referenceImageAdded({ isSelected: true, overrides }));
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddRegionalGuidanceIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
const addRegionalGuidanceIPAdapter = useCallback(() => {
|
||||
dispatch(rgIPAdapterAdded({ entityIdentifier, overrides: defaultIPAdapter }));
|
||||
const func = useCallback(() => {
|
||||
dispatch(rgIPAdapterAdded({ entityIdentifier, overrides: { ipAdapter: defaultIPAdapter } }));
|
||||
}, [defaultIPAdapter, dispatch, entityIdentifier]);
|
||||
|
||||
return addRegionalGuidanceIPAdapter;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddRegionalGuidancePositivePrompt = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const addRegionalGuidancePositivePrompt = useCallback(() => {
|
||||
const func = useCallback(() => {
|
||||
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return addRegionalGuidancePositivePrompt;
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useAddRegionalGuidanceNegativePrompt = (entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const addRegionalGuidanceNegativePrompt = useCallback(() => {
|
||||
const runc = useCallback(() => {
|
||||
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
return addRegionalGuidanceNegativePrompt;
|
||||
return runc;
|
||||
};
|
||||
|
||||
export const buildSelectValidRegionalGuidanceActions = (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { withResultAsync } from 'common/util/result';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectDefaultControlAdapter, selectDefaultIPAdapter } from 'features/controlLayers/hooks/addLayerHooks';
|
||||
@@ -7,22 +8,22 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
controlLayerAdded,
|
||||
entityRasterized,
|
||||
ipaAdded,
|
||||
ipaImageChanged,
|
||||
rasterLayerAdded,
|
||||
referenceImageAdded,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
rgAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasIPAdapterState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
Rect,
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
RegionalGuidanceReferenceImageState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -34,10 +35,12 @@ const log = logger('canvas');
|
||||
type UseSaveCanvasArg = {
|
||||
region: 'canvas' | 'bbox';
|
||||
saveToGallery: boolean;
|
||||
toastOk: string;
|
||||
toastError: string;
|
||||
onSave?: (imageDTO: ImageDTO, rect: Rect) => void;
|
||||
};
|
||||
|
||||
const useSaveCanvas = ({ region, saveToGallery, onSave }: UseSaveCanvasArg) => {
|
||||
const useSaveCanvas = ({ region, saveToGallery, toastOk, toastError, onSave }: UseSaveCanvasArg) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const canvasManager = useCanvasManager();
|
||||
@@ -48,7 +51,7 @@ const useSaveCanvas = ({ region, saveToGallery, onSave }: UseSaveCanvasArg) => {
|
||||
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
toast({
|
||||
title: t('controlLayers.savedToGalleryError'),
|
||||
title: toastError,
|
||||
description: t('controlLayers.regionIsEmpty'),
|
||||
status: 'warning',
|
||||
});
|
||||
@@ -63,74 +66,119 @@ const useSaveCanvas = ({ region, saveToGallery, onSave }: UseSaveCanvasArg) => {
|
||||
if (onSave) {
|
||||
onSave(result.value, rect);
|
||||
}
|
||||
toast({ title: t('controlLayers.savedToGalleryOk') });
|
||||
toast({ title: toastOk });
|
||||
} else {
|
||||
log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery');
|
||||
toast({ title: t('controlLayers.savedToGalleryError'), status: 'error' });
|
||||
toast({ title: toastError, status: 'error' });
|
||||
}
|
||||
}, [canvasManager.compositor, canvasManager.stage, canvasManager.stateApi, onSave, region, saveToGallery, t]);
|
||||
}, [
|
||||
canvasManager.compositor,
|
||||
canvasManager.stage,
|
||||
canvasManager.stateApi,
|
||||
onSave,
|
||||
region,
|
||||
saveToGallery,
|
||||
t,
|
||||
toastError,
|
||||
toastOk,
|
||||
]);
|
||||
|
||||
return saveCanvas;
|
||||
};
|
||||
|
||||
const saveCanvasToGalleryArg: UseSaveCanvasArg = { region: 'canvas', saveToGallery: true };
|
||||
export const useSaveCanvasToGallery = () => {
|
||||
const saveCanvasToGallery = useSaveCanvas(saveCanvasToGalleryArg);
|
||||
return saveCanvasToGallery;
|
||||
const { t } = useTranslation();
|
||||
const arg: UseSaveCanvasArg = useMemo(
|
||||
() => ({
|
||||
region: 'canvas',
|
||||
saveToGallery: true,
|
||||
toastOk: t('controlLayers.savedToGalleryOk'),
|
||||
toastError: t('controlLayers.savedToGalleryError'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
const saveBboxToGalleryArg: UseSaveCanvasArg = { region: 'bbox', saveToGallery: true };
|
||||
export const useSaveBboxToGallery = () => {
|
||||
const saveBboxToGallery = useSaveCanvas(saveBboxToGalleryArg);
|
||||
return saveBboxToGallery;
|
||||
const { t } = useTranslation();
|
||||
const arg: UseSaveCanvasArg = useMemo(
|
||||
() => ({
|
||||
region: 'bbox',
|
||||
saveToGallery: true,
|
||||
toastOk: t('controlLayers.savedToGalleryOk'),
|
||||
toastError: t('controlLayers.savedToGalleryError'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewRegionalIPAdapterFromBbox = () => {
|
||||
export const useNewRegionalReferenceImageFromBbox = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO) => {
|
||||
const ipAdapter: RegionalGuidanceIPAdapterConfig = {
|
||||
...defaultIPAdapter,
|
||||
id: getPrefixedId('regional_guidance_ip_adapter'),
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
const ipAdapter: RegionalGuidanceReferenceImageState = {
|
||||
id: getPrefixedId('regional_guidance_reference_image'),
|
||||
ipAdapter: {
|
||||
...deepClone(defaultIPAdapter),
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
},
|
||||
};
|
||||
const overrides: Partial<CanvasRegionalGuidanceState> = {
|
||||
ipAdapters: [ipAdapter],
|
||||
referenceImages: [ipAdapter],
|
||||
};
|
||||
|
||||
dispatch(rgAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
const newRegionalIPAdapterFromBbox = useSaveCanvas(arg);
|
||||
return newRegionalIPAdapterFromBbox;
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.newRegionalReferenceImageOk'),
|
||||
toastError: t('controlLayers.newRegionalReferenceImageError'),
|
||||
};
|
||||
}, [defaultIPAdapter, dispatch, t]);
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewGlobalIPAdapterFromBbox = () => {
|
||||
export const useNewGlobalReferenceImageFromBbox = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultIPAdapter = useAppSelector(selectDefaultIPAdapter);
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO) => {
|
||||
const overrides: Partial<CanvasIPAdapterState> = {
|
||||
const overrides: Partial<CanvasReferenceImageState> = {
|
||||
ipAdapter: {
|
||||
...defaultIPAdapter,
|
||||
...deepClone(defaultIPAdapter),
|
||||
image: imageDTOToImageWithDims(imageDTO),
|
||||
},
|
||||
};
|
||||
dispatch(ipaAdded({ overrides, isSelected: true }));
|
||||
dispatch(referenceImageAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [defaultIPAdapter, dispatch]);
|
||||
const newGlobalIPAdapterFromBbox = useSaveCanvas(arg);
|
||||
return newGlobalIPAdapterFromBbox;
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.newGlobalReferenceImageOk'),
|
||||
toastError: t('controlLayers.newGlobalReferenceImageError'),
|
||||
};
|
||||
}, [defaultIPAdapter, dispatch, t]);
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const useNewRasterLayerFromBbox = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, rect: Rect) => {
|
||||
@@ -141,13 +189,20 @@ export const useNewRasterLayerFromBbox = () => {
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [dispatch]);
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.newRasterLayerOk'),
|
||||
toastError: t('controlLayers.newRasterLayerError'),
|
||||
};
|
||||
}, [dispatch, t]);
|
||||
const newRasterLayerFromBbox = useSaveCanvas(arg);
|
||||
return newRasterLayerFromBbox;
|
||||
};
|
||||
|
||||
export const useNewControlLayerFromBbox = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const defaultControlAdapter = useAppSelector(selectDefaultControlAdapter);
|
||||
|
||||
@@ -155,19 +210,26 @@ export const useNewControlLayerFromBbox = () => {
|
||||
const onSave = (imageDTO: ImageDTO, rect: Rect) => {
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageDTOToImageObject(imageDTO)],
|
||||
controlAdapter: defaultControlAdapter,
|
||||
controlAdapter: deepClone(defaultControlAdapter),
|
||||
position: { x: rect.x, y: rect.y },
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [defaultControlAdapter, dispatch]);
|
||||
const newControlLayerFromBbox = useSaveCanvas(arg);
|
||||
return newControlLayerFromBbox;
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.newControlLayerOk'),
|
||||
toastError: t('controlLayers.newControlLayerError'),
|
||||
};
|
||||
}, [defaultControlAdapter, dispatch, t]);
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const usePullBboxIntoLayer = (entityIdentifier: CanvasEntityIdentifier<'control_layer' | 'raster_layer'>) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
@@ -182,42 +244,62 @@ export const usePullBboxIntoLayer = (entityIdentifier: CanvasEntityIdentifier<'c
|
||||
);
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [dispatch, entityIdentifier]);
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.pullBboxIntoLayerOk'),
|
||||
toastError: t('controlLayers.pullBboxIntoLayerError'),
|
||||
};
|
||||
}, [dispatch, entityIdentifier, t]);
|
||||
|
||||
const pullBboxIntoLayer = useSaveCanvas(arg);
|
||||
return pullBboxIntoLayer;
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const usePullBboxIntoIPAdapter = (entityIdentifier: CanvasEntityIdentifier<'ip_adapter'>) => {
|
||||
export const usePullBboxIntoGlobalReferenceImage = (entityIdentifier: CanvasEntityIdentifier<'reference_image'>) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, _: Rect) => {
|
||||
dispatch(ipaImageChanged({ entityIdentifier, imageDTO }));
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier, imageDTO }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [dispatch, entityIdentifier]);
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.pullBboxIntoReferenceImageOk'),
|
||||
toastError: t('controlLayers.pullBboxIntoReferenceImageError'),
|
||||
};
|
||||
}, [dispatch, entityIdentifier, t]);
|
||||
|
||||
const pullBboxIntoIPAdapter = useSaveCanvas(arg);
|
||||
return pullBboxIntoIPAdapter;
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
export const usePullBboxIntoRegionalGuidanceIPAdapter = (
|
||||
export const usePullBboxIntoRegionalGuidanceReferenceImage = (
|
||||
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>,
|
||||
ipAdapterId: string
|
||||
referenceImageId: string
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const arg = useMemo<UseSaveCanvasArg>(() => {
|
||||
const onSave = (imageDTO: ImageDTO, _: Rect) => {
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, ipAdapterId, imageDTO }));
|
||||
dispatch(rgIPAdapterImageChanged({ entityIdentifier, referenceImageId, imageDTO }));
|
||||
};
|
||||
|
||||
return { region: 'bbox', saveToGallery: false, onSave };
|
||||
}, [dispatch, entityIdentifier, ipAdapterId]);
|
||||
return {
|
||||
region: 'bbox',
|
||||
saveToGallery: false,
|
||||
onSave,
|
||||
toastOk: t('controlLayers.pullBboxIntoReferenceImageOk'),
|
||||
toastError: t('controlLayers.pullBboxIntoReferenceImageError'),
|
||||
};
|
||||
}, [dispatch, entityIdentifier, referenceImageId, t]);
|
||||
|
||||
const pullBboxIntoRegionalGuidanceIPAdapter = useSaveCanvas(arg);
|
||||
return pullBboxIntoRegionalGuidanceIPAdapter;
|
||||
const func = useSaveCanvas(arg);
|
||||
return func;
|
||||
};
|
||||
|
||||
@@ -25,11 +25,12 @@ export function useCanvasResetLayerHotkey() {
|
||||
const isInteractable = useStore(adapter?.$isInteractable ?? $false);
|
||||
|
||||
const resetSelectedLayer = useCallback(() => {
|
||||
if (selectedEntityIdentifier === null) {
|
||||
if (selectedEntityIdentifier === null || adapter === null) {
|
||||
return;
|
||||
}
|
||||
adapter.bufferRenderer.clearBuffer();
|
||||
dispatch(entityReset({ entityIdentifier: selectedEntityIdentifier }));
|
||||
}, [dispatch, selectedEntityIdentifier]);
|
||||
}, [adapter, dispatch, selectedEntityIdentifier]);
|
||||
|
||||
const isResetEnabled = useMemo(
|
||||
() => selectedEntityIdentifier !== null && isMaskEntityIdentifier(selectedEntityIdentifier),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
|
||||
import { canvasToBlob } from 'features/controlLayers/konva/util';
|
||||
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useCopyLayerToClipboard = () => {
|
||||
const { t } = useTranslation();
|
||||
const copyLayerToCipboard = useCallback(
|
||||
async (
|
||||
adapter:
|
||||
| CanvasEntityAdapterRasterLayer
|
||||
| CanvasEntityAdapterControlLayer
|
||||
| CanvasEntityAdapterInpaintMask
|
||||
| CanvasEntityAdapterRegionalGuidance
|
||||
| null
|
||||
) => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const canvas = adapter.getCanvas();
|
||||
const blob = await canvasToBlob(canvas);
|
||||
copyBlobToClipboard(blob);
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('toast.layerCopiedToClipboard'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('toast.problemCopyingLayer'),
|
||||
});
|
||||
}
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
return copyLayerToCipboard;
|
||||
};
|
||||
@@ -32,8 +32,8 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||
return t('controlLayers.controlLayer');
|
||||
case 'raster_layer':
|
||||
return t('controlLayers.rasterLayer');
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.globalIPAdapter');
|
||||
case 'reference_image':
|
||||
return t('controlLayers.globalReferenceImage');
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.regionalGuidance');
|
||||
default:
|
||||
|
||||
@@ -16,9 +16,9 @@ export const useEntityTypeCount = (type: CanvasEntityIdentifier['type']): number
|
||||
case 'inpaint_mask':
|
||||
return canvas.inpaintMasks.entities.length;
|
||||
case 'regional_guidance':
|
||||
return canvas.regions.entities.length;
|
||||
case 'ip_adapter':
|
||||
return canvas.ipAdapters.entities.length;
|
||||
return canvas.regionalGuidance.entities.length;
|
||||
case 'reference_image':
|
||||
return canvas.referenceImages.entities.length;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export const useEntityTypeIsHidden = (type: CanvasEntityIdentifier['type']): boo
|
||||
case 'inpaint_mask':
|
||||
return canvas.inpaintMasks.isHidden;
|
||||
case 'regional_guidance':
|
||||
return canvas.regions.isHidden;
|
||||
case 'ip_adapter':
|
||||
return canvas.regionalGuidance.isHidden;
|
||||
case 'reference_image':
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ export const useEntityTypeString = (type: CanvasEntityIdentifier['type'], plural
|
||||
return plural ? t('controlLayers.inpaintMask_withCount_other') : t('controlLayers.inpaintMask');
|
||||
case 'regional_guidance':
|
||||
return plural ? t('controlLayers.regionalGuidance_withCount_other') : t('controlLayers.regionalGuidance');
|
||||
case 'ip_adapter':
|
||||
return plural ? t('controlLayers.globalIPAdapter_withCount_other') : t('controlLayers.globalIPAdapter');
|
||||
case 'reference_image':
|
||||
return plural
|
||||
? t('controlLayers.globalReferenceImage_withCount_other')
|
||||
: t('controlLayers.globalReferenceImage');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ export const useEntityTypeTitle = (type: CanvasEntityIdentifier['type']): string
|
||||
return t('controlLayers.inpaintMasks_withCount', { count, context });
|
||||
case 'regional_guidance':
|
||||
return t('controlLayers.regionalGuidance_withCount', { count, context });
|
||||
case 'ip_adapter':
|
||||
return t('controlLayers.globalIPAdapters_withCount', { count, context });
|
||||
case 'reference_image':
|
||||
return t('controlLayers.globalReferenceImages_withCount', { count, context });
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $socket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
@@ -7,6 +6,7 @@ import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
|
||||
import Konva from 'konva';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { $socket } from 'services/events/stores';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
|
||||
const log = logger('canvas');
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
|
||||
import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask';
|
||||
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
|
||||
import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance';
|
||||
import { canvasToBlob } from 'features/controlLayers/konva/util';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||
|
||||
export const useSaveLayerToAssets = () => {
|
||||
const { t } = useTranslation();
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
|
||||
const saveLayerToAssets = useCallback(
|
||||
async (
|
||||
adapter:
|
||||
| CanvasEntityAdapterRasterLayer
|
||||
| CanvasEntityAdapterControlLayer
|
||||
| CanvasEntityAdapterInpaintMask
|
||||
| CanvasEntityAdapterRegionalGuidance
|
||||
| null
|
||||
) => {
|
||||
if (!adapter) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const canvas = adapter.getCanvas();
|
||||
const blob = await canvasToBlob(canvas);
|
||||
const file = new File([blob], `layer-${adapter.id}.png`, { type: 'image/png' });
|
||||
await uploadImage({
|
||||
file,
|
||||
image_category: 'user',
|
||||
is_intermediate: false,
|
||||
postUploadAction: { type: 'TOAST' },
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
});
|
||||
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('toast.layerSavedToAssets'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: t('toast.problemSavingLayer'),
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, autoAddBoardId, uploadImage]
|
||||
);
|
||||
|
||||
return saveLayerToAssets;
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import { selectAutoProcessFilter } from 'features/controlLayers/store/canvasSett
|
||||
import type { FilterConfig } from 'features/controlLayers/store/filters';
|
||||
import { getFilterForModel, IMAGE_FILTERS } from 'features/controlLayers/store/filters';
|
||||
import type { CanvasImageState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
previewBlob,
|
||||
} from 'features/controlLayers/konva/util';
|
||||
import type { Rect } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/util';
|
||||
import Konva from 'konva';
|
||||
import type { GroupConfig } from 'konva/lib/Group';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
@@ -64,8 +64,8 @@ export class CanvasEntityRendererModule extends CanvasModuleBase {
|
||||
};
|
||||
|
||||
createNewRegionalGuidance = async (state: CanvasState, prevState: CanvasState | null) => {
|
||||
if (!prevState || state.regions.entities !== prevState.regions.entities) {
|
||||
for (const entityState of state.regions.entities) {
|
||||
if (!prevState || state.regionalGuidance.entities !== prevState.regionalGuidance.entities) {
|
||||
for (const entityState of state.regionalGuidance.entities) {
|
||||
if (!this.manager.adapters.regionMasks.has(entityState.id)) {
|
||||
const adapter = this.manager.createAdapter(getEntityIdentifier(entityState));
|
||||
await adapter.initialize();
|
||||
@@ -90,7 +90,7 @@ export class CanvasEntityRendererModule extends CanvasModuleBase {
|
||||
!prevState ||
|
||||
state.rasterLayers.entities !== prevState.rasterLayers.entities ||
|
||||
state.controlLayers.entities !== prevState.controlLayers.entities ||
|
||||
state.regions.entities !== prevState.regions.entities ||
|
||||
state.regionalGuidance.entities !== prevState.regionalGuidance.entities ||
|
||||
state.inpaintMasks.entities !== prevState.inpaintMasks.entities ||
|
||||
state.selectedEntityIdentifier?.id !== prevState.selectedEntityIdentifier?.id
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { AppSocket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStore } from 'app/store/store';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
@@ -31,6 +30,7 @@ import Konva from 'konva';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { computed } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import type { AppSocket } from 'services/events/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { CanvasBackgroundModule } from './CanvasBackgroundModule';
|
||||
|
||||
@@ -4,7 +4,10 @@ import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase'
|
||||
import { getPrefixedId, loadImage } from 'features/controlLayers/konva/util';
|
||||
import { selectShowProgressOnCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
readonly type = 'progress_image';
|
||||
@@ -23,7 +26,8 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
imageElement: HTMLImageElement | null = null;
|
||||
|
||||
subscriptions = new Set<() => void>();
|
||||
|
||||
$lastProgressEvent = atom<S['InvocationDenoiseProgressEvent'] | null>(null);
|
||||
hasActiveGeneration: boolean = false;
|
||||
mutex: Mutex = new Mutex();
|
||||
|
||||
constructor(manager: CanvasManager) {
|
||||
@@ -41,11 +45,50 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
image: null,
|
||||
};
|
||||
|
||||
this.subscriptions.add(this.manager.stateApi.$lastCanvasProgressEvent.listen(this.render));
|
||||
this.subscriptions.add(this.manager.stagingArea.$shouldShowStagedImage.listen(this.render));
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectShowProgressOnCanvas, this.render));
|
||||
this.subscriptions.add(this.setSocketEventListeners());
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.createStoreSubscription(selectCanvasQueueCounts, ({ data }) => {
|
||||
if (data && (data.in_progress > 0 || data.pending > 0)) {
|
||||
this.hasActiveGeneration = true;
|
||||
} else {
|
||||
this.hasActiveGeneration = false;
|
||||
this.$lastProgressEvent.set(null);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.subscriptions.add(this.$lastProgressEvent.listen(this.render));
|
||||
}
|
||||
|
||||
setSocketEventListeners = (): (() => void) => {
|
||||
const progressListener = (data: S['InvocationDenoiseProgressEvent']) => {
|
||||
if (data.destination !== 'canvas') {
|
||||
return;
|
||||
}
|
||||
if (!this.hasActiveGeneration) {
|
||||
return;
|
||||
}
|
||||
this.$lastProgressEvent.set(data);
|
||||
};
|
||||
|
||||
const clearProgress = () => {
|
||||
this.$lastProgressEvent.set(null);
|
||||
};
|
||||
|
||||
this.manager.socket.on('invocation_denoise_progress', progressListener);
|
||||
this.manager.socket.on('connect', clearProgress);
|
||||
this.manager.socket.on('connect_error', clearProgress);
|
||||
this.manager.socket.on('disconnect', clearProgress);
|
||||
|
||||
return () => {
|
||||
this.manager.socket.off('invocation_denoise_progress', progressListener);
|
||||
this.manager.socket.off('connect', clearProgress);
|
||||
this.manager.socket.off('connect_error', clearProgress);
|
||||
this.manager.socket.off('disconnect', clearProgress);
|
||||
};
|
||||
};
|
||||
|
||||
getNodes = () => {
|
||||
return [this.konva.group];
|
||||
};
|
||||
@@ -53,7 +96,7 @@ export class CanvasProgressImageModule extends CanvasModuleBase {
|
||||
render = async () => {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
const event = this.manager.stateApi.$lastCanvasProgressEvent.get();
|
||||
const event = this.$lastProgressEvent.get();
|
||||
const showProgressOnCanvas = this.manager.stateApi.runSelector(selectShowProgressOnCanvas);
|
||||
|
||||
if (!event || !showProgressOnCanvas) {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { addAppListener } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { CanvasModuleBase } from 'features/controlLayers/konva/CanvasModuleBase';
|
||||
import { CanvasObjectImage } from 'features/controlLayers/konva/CanvasObject/CanvasObjectImage';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import {
|
||||
selectCanvasStagingAreaSlice,
|
||||
stagingAreaStartedStaging,
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { imageDTOToImageWithDims, type StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasStagingAreaSlice, selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import Konva from 'konva';
|
||||
import { atom } from 'nanostores';
|
||||
import type { Logger } from 'roarr';
|
||||
@@ -42,17 +39,23 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
this.image = null;
|
||||
this.selectedImage = null;
|
||||
|
||||
/**
|
||||
* When we change this flag, we need to re-render the staging area, which hides or shows the staged image.
|
||||
*/
|
||||
this.subscriptions.add(this.$shouldShowStagedImage.listen(this.render));
|
||||
/**
|
||||
* When the staging redux state changes (i.e. when the selected staged image is changed, or we add/discard a staged
|
||||
* image), we need to re-render the staging area.
|
||||
*/
|
||||
this.subscriptions.add(this.manager.stateApi.createStoreSubscription(selectCanvasStagingAreaSlice, this.render));
|
||||
/**
|
||||
* Sync the $isStaging flag with the redux state. $isStaging is used by the manager to determine the global busy
|
||||
* state of the canvas.
|
||||
*/
|
||||
this.subscriptions.add(
|
||||
this.manager.stateApi.store.dispatch(
|
||||
addAppListener({
|
||||
actionCreator: stagingAreaStartedStaging,
|
||||
effect: () => {
|
||||
this.$shouldShowStagedImage.set(true);
|
||||
},
|
||||
})
|
||||
)
|
||||
this.manager.stateApi.createStoreSubscription(selectIsStaging, (isStaging) => {
|
||||
this.$isStaging.set(isStaging);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +67,6 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
render = async () => {
|
||||
this.log.trace('Rendering staging area');
|
||||
const stagingArea = this.manager.stateApi.runSelector(selectCanvasStagingAreaSlice);
|
||||
this.$isStaging.set(stagingArea.isStaging);
|
||||
|
||||
const { x, y, width, height } = this.manager.stateApi.getBbox().rect;
|
||||
const shouldShowStagedImage = this.$shouldShowStagedImage.get();
|
||||
@@ -94,7 +96,7 @@ export class CanvasStagingAreaModule extends CanvasModuleBase {
|
||||
|
||||
if (!this.image.isLoading && !this.image.isError) {
|
||||
await this.image.update({ ...this.image.state, image: imageDTOToImageWithDims(imageDTO) }, true);
|
||||
this.manager.stateApi.$lastCanvasProgressEvent.set(null);
|
||||
this.manager.progressImage.$lastProgressEvent.set(null);
|
||||
}
|
||||
this.image.konva.group.visible(shouldShowStagedImage);
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
settingsEraserWidthChanged,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
$lastCanvasProgressEvent,
|
||||
bboxChangedFromCanvas,
|
||||
entityBrushLineAdded,
|
||||
entityEraserLineAdded,
|
||||
@@ -228,7 +227,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
* Gets the regions state from redux.
|
||||
*/
|
||||
getRegionsState = () => {
|
||||
return this.getCanvasState().regions;
|
||||
return this.getCanvasState().regionalGuidance;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -382,12 +381,6 @@ export class CanvasStateApiModule extends CanvasModuleBase {
|
||||
*/
|
||||
$isRasterizing = computed(this.$rasterizingAdapter, (rasterizingAdapter) => Boolean(rasterizingAdapter));
|
||||
|
||||
/**
|
||||
* The last canvas progress event. This is set in a global event listener. The staging area may set it to null when it
|
||||
* consumes the event.
|
||||
*/
|
||||
$lastCanvasProgressEvent = $lastCanvasProgressEvent;
|
||||
|
||||
/**
|
||||
* Whether the space key is currently pressed.
|
||||
*/
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
selectAllEntities,
|
||||
selectAllEntitiesOfType,
|
||||
selectEntity,
|
||||
selectRegionalGuidanceIPAdapter,
|
||||
selectRegionalGuidanceReferenceImage,
|
||||
} from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasInpaintMaskState,
|
||||
FillStyle,
|
||||
RegionalGuidanceIPAdapterConfig,
|
||||
RegionalGuidanceReferenceImageState,
|
||||
RgbColor,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
@@ -27,24 +27,18 @@ import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/c
|
||||
import type { AspectRatioID } from 'features/parameters/components/Bbox/types';
|
||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { isEqual, merge, omit } from 'lodash-es';
|
||||
import { merge, omit } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import type { UndoableOptions } from 'redux-undo';
|
||||
import type {
|
||||
ControlNetModelConfig,
|
||||
ImageDTO,
|
||||
IPAdapterModelConfig,
|
||||
S,
|
||||
T2IAdapterModelConfig,
|
||||
} from 'services/api/types';
|
||||
import type { ControlNetModelConfig, ImageDTO, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import type {
|
||||
BoundingBoxScaleMethod,
|
||||
CanvasControlLayerState,
|
||||
CanvasEntityIdentifier,
|
||||
CanvasIPAdapterState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
CanvasState,
|
||||
CLIPVisionModelV2,
|
||||
@@ -59,84 +53,57 @@ import type {
|
||||
IPMethodV2,
|
||||
T2IAdapterConfig,
|
||||
} from './types';
|
||||
import { getEntityIdentifier, isRenderableEntity } from './types';
|
||||
import {
|
||||
getEntityIdentifier,
|
||||
getControlLayerState,
|
||||
getInpaintMaskState,
|
||||
getRasterLayerState,
|
||||
getReferenceImageState,
|
||||
getRegionalGuidanceState,
|
||||
imageDTOToImageWithDims,
|
||||
initialControlNet,
|
||||
initialIPAdapter,
|
||||
isRenderableEntity,
|
||||
} from './types';
|
||||
} from './util';
|
||||
|
||||
const DEFAULT_MASK_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)
|
||||
];
|
||||
|
||||
const getRGMaskFill = (state: CanvasState): RgbColor => {
|
||||
const lastFill = state.regions.entities.slice(-1)[0]?.fill.color;
|
||||
let i = DEFAULT_MASK_COLORS.findIndex((c) => isEqual(c, lastFill));
|
||||
if (i === -1) {
|
||||
i = 0;
|
||||
}
|
||||
i = (i + 1) % DEFAULT_MASK_COLORS.length;
|
||||
const fill = DEFAULT_MASK_COLORS[i];
|
||||
assert(fill, 'This should never happen');
|
||||
return fill;
|
||||
};
|
||||
|
||||
const initialInpaintMaskState: CanvasInpaintMaskState = {
|
||||
id: getPrefixedId('inpaint_mask'),
|
||||
name: null,
|
||||
type: 'inpaint_mask',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
fill: {
|
||||
style: 'diagonal',
|
||||
color: { r: 255, g: 122, b: 0 }, // some orange color
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: CanvasState = {
|
||||
_version: 3,
|
||||
selectedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
|
||||
bookmarkedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
|
||||
rasterLayers: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
controlLayers: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
inpaintMasks: {
|
||||
isHidden: false,
|
||||
entities: [deepClone(initialInpaintMaskState)],
|
||||
},
|
||||
regions: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
ipAdapters: { entities: [] },
|
||||
bbox: {
|
||||
rect: { x: 0, y: 0, width: 512, height: 512 },
|
||||
optimalDimension: 512,
|
||||
aspectRatio: deepClone(initialAspectRatioState),
|
||||
scaleMethod: 'auto',
|
||||
scaledSize: {
|
||||
width: 512,
|
||||
height: 512,
|
||||
const getInitialState = (): CanvasState => {
|
||||
const initialInpaintMaskState = getInpaintMaskState(getPrefixedId('inpaint_mask'));
|
||||
const initialState: CanvasState = {
|
||||
_version: 3,
|
||||
selectedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
|
||||
bookmarkedEntityIdentifier: getEntityIdentifier(initialInpaintMaskState),
|
||||
rasterLayers: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
},
|
||||
controlLayers: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
inpaintMasks: {
|
||||
isHidden: false,
|
||||
entities: [initialInpaintMaskState],
|
||||
},
|
||||
regionalGuidance: {
|
||||
isHidden: false,
|
||||
entities: [],
|
||||
},
|
||||
referenceImages: { entities: [] },
|
||||
bbox: {
|
||||
rect: { x: 0, y: 0, width: 512, height: 512 },
|
||||
optimalDimension: 512,
|
||||
aspectRatio: deepClone(initialAspectRatioState),
|
||||
scaleMethod: 'auto',
|
||||
scaledSize: {
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
},
|
||||
};
|
||||
return initialState;
|
||||
};
|
||||
|
||||
const initialState = getInitialState();
|
||||
|
||||
export const canvasSlice = createSlice({
|
||||
name: 'canvas',
|
||||
initialState,
|
||||
@@ -154,27 +121,17 @@ export const canvasSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected, isMergingVisible } = action.payload;
|
||||
const entity: CanvasRasterLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'raster_layer',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
merge(entity, overrides);
|
||||
const entityState = getRasterLayerState(id, overrides);
|
||||
|
||||
if (isMergingVisible) {
|
||||
// When merging visible, we delete all disabled layers
|
||||
state.rasterLayers.entities = state.rasterLayers.entities.filter((layer) => !layer.isEnabled);
|
||||
}
|
||||
|
||||
state.rasterLayers.entities.push(entity);
|
||||
state.rasterLayers.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload: {
|
||||
@@ -235,22 +192,13 @@ export const canvasSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasControlLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'control_layer',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
withTransparencyEffect: true,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
controlAdapter: deepClone(initialControlNet),
|
||||
};
|
||||
merge(entity, overrides);
|
||||
state.controlLayers.entities.push(entity);
|
||||
|
||||
const entityState = getControlLayerState(id, overrides);
|
||||
|
||||
state.controlLayers.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload: { overrides?: Partial<CanvasControlLayerState>; isSelected?: boolean }) => ({
|
||||
@@ -371,39 +319,33 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
layer.withTransparencyEffect = !layer.withTransparencyEffect;
|
||||
},
|
||||
//#region IP Adapters
|
||||
ipaAdded: {
|
||||
//#region Global Reference Images
|
||||
referenceImageAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }>
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasReferenceImageState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasIPAdapterState = {
|
||||
id,
|
||||
type: 'ip_adapter',
|
||||
name: null,
|
||||
isLocked: false,
|
||||
isEnabled: true,
|
||||
ipAdapter: deepClone(initialIPAdapter),
|
||||
};
|
||||
merge(entity, overrides);
|
||||
state.ipAdapters.entities.push(entity);
|
||||
const entityState = getReferenceImageState(id, overrides);
|
||||
|
||||
state.referenceImages.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: { overrides?: Partial<CanvasIPAdapterState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('ip_adapter') },
|
||||
prepare: (payload?: { overrides?: Partial<CanvasReferenceImageState>; isSelected?: boolean }) => ({
|
||||
payload: { ...payload, id: getPrefixedId('reference_image') },
|
||||
}),
|
||||
},
|
||||
ipaRecalled: (state, action: PayloadAction<{ data: CanvasIPAdapterState }>) => {
|
||||
referenceImageRecalled: (state, action: PayloadAction<{ data: CanvasReferenceImageState }>) => {
|
||||
const { data } = action.payload;
|
||||
state.ipAdapters.entities.push(data);
|
||||
state.selectedEntityIdentifier = { type: 'ip_adapter', id: data.id };
|
||||
state.referenceImages.entities.push(data);
|
||||
state.selectedEntityIdentifier = { type: 'reference_image', id: data.id };
|
||||
},
|
||||
ipaImageChanged: (
|
||||
referenceImageIPAdapterImageChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ imageDTO: ImageDTO | null }, 'ip_adapter'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ imageDTO: ImageDTO | null }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, imageDTO } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
@@ -412,7 +354,10 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||
},
|
||||
ipaMethodChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ method: IPMethodV2 }, 'ip_adapter'>>) => {
|
||||
referenceImageIPAdapterMethodChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ method: IPMethodV2 }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, method } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
@@ -420,9 +365,9 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.method = method;
|
||||
},
|
||||
ipaModelChanged: (
|
||||
referenceImageIPAdapterModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ modelConfig: IPAdapterModelConfig | null }, 'ip_adapter'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ modelConfig: IPAdapterModelConfig | null }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, modelConfig } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
@@ -431,9 +376,9 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
||||
},
|
||||
ipaCLIPVisionModelChanged: (
|
||||
referenceImageIPAdapterCLIPVisionModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ clipVisionModel: CLIPVisionModelV2 }, 'ip_adapter'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ clipVisionModel: CLIPVisionModelV2 }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, clipVisionModel } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
@@ -442,7 +387,10 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.clipVisionModel = clipVisionModel;
|
||||
},
|
||||
ipaWeightChanged: (state, action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'ip_adapter'>>) => {
|
||||
referenceImageIPAdapterWeightChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ weight: number }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, weight } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
@@ -450,9 +398,9 @@ export const canvasSlice = createSlice({
|
||||
}
|
||||
entity.ipAdapter.weight = weight;
|
||||
},
|
||||
ipaBeginEndStepPctChanged: (
|
||||
referenceImageIPAdapterBeginEndStepPctChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'ip_adapter'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ beginEndStepPct: [number, number] }, 'reference_image'>>
|
||||
) => {
|
||||
const { entityIdentifier, beginEndStepPct } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
@@ -468,28 +416,13 @@ export const canvasSlice = createSlice({
|
||||
action: PayloadAction<{ id: string; overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }>
|
||||
) => {
|
||||
const { id, overrides, isSelected } = action.payload;
|
||||
const entity: CanvasRegionalGuidanceState = {
|
||||
id,
|
||||
name: null,
|
||||
isLocked: false,
|
||||
type: 'regional_guidance',
|
||||
isEnabled: true,
|
||||
objects: [],
|
||||
fill: {
|
||||
style: 'solid',
|
||||
color: getRGMaskFill(state),
|
||||
},
|
||||
opacity: 0.5,
|
||||
position: { x: 0, y: 0 },
|
||||
autoNegative: false,
|
||||
positivePrompt: null,
|
||||
negativePrompt: null,
|
||||
ipAdapters: [],
|
||||
};
|
||||
merge(entity, overrides);
|
||||
state.regions.entities.push(entity);
|
||||
|
||||
const entityState = getRegionalGuidanceState(id, overrides);
|
||||
|
||||
state.regionalGuidance.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: { overrides?: Partial<CanvasRegionalGuidanceState>; isSelected?: boolean }) => ({
|
||||
@@ -498,7 +431,7 @@ export const canvasSlice = createSlice({
|
||||
},
|
||||
rgRecalled: (state, action: PayloadAction<{ data: CanvasRegionalGuidanceState }>) => {
|
||||
const { data } = action.payload;
|
||||
state.regions.entities.push(data);
|
||||
state.regionalGuidance.entities.push(data);
|
||||
state.selectedEntityIdentifier = { type: 'regional_guidance', id: data.id };
|
||||
},
|
||||
rgPositivePromptChanged: (
|
||||
@@ -536,116 +469,121 @@ export const canvasSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<
|
||||
{ ipAdapterId: string; overrides?: Partial<RegionalGuidanceIPAdapterConfig> },
|
||||
{ referenceImageId: string; overrides?: Partial<RegionalGuidanceReferenceImageState> },
|
||||
'regional_guidance'
|
||||
>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, overrides, ipAdapterId } = action.payload;
|
||||
const { entityIdentifier, overrides, referenceImageId } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
const ipAdapter = { ...deepClone(initialIPAdapter), id: ipAdapterId };
|
||||
const ipAdapter = { id: referenceImageId, ipAdapter: deepClone(initialIPAdapter) };
|
||||
merge(ipAdapter, overrides);
|
||||
entity.ipAdapters.push(ipAdapter);
|
||||
entity.referenceImages.push(ipAdapter);
|
||||
},
|
||||
prepare: (
|
||||
payload: EntityIdentifierPayload<{ overrides?: Partial<RegionalGuidanceIPAdapterConfig> }, 'regional_guidance'>
|
||||
payload: EntityIdentifierPayload<
|
||||
{ overrides?: Partial<RegionalGuidanceReferenceImageState> },
|
||||
'regional_guidance'
|
||||
>
|
||||
) => ({
|
||||
payload: { ...payload, ipAdapterId: getPrefixedId('regional_guidance_ip_adapter') },
|
||||
payload: { ...payload, referenceImageId: getPrefixedId('regional_guidance_ip_adapter') },
|
||||
}),
|
||||
},
|
||||
rgIPAdapterDeleted: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string }, 'regional_guidance'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ referenceImageId: string }, 'regional_guidance'>>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId } = action.payload;
|
||||
const { entityIdentifier, referenceImageId } = action.payload;
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
entity.ipAdapters = entity.ipAdapters.filter((ipAdapter) => ipAdapter.id !== ipAdapterId);
|
||||
entity.referenceImages = entity.referenceImages.filter((ipAdapter) => ipAdapter.id !== referenceImageId);
|
||||
},
|
||||
rgIPAdapterImageChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<{ ipAdapterId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
|
||||
EntityIdentifierPayload<{ referenceImageId: string; imageDTO: ImageDTO | null }, 'regional_guidance'>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, imageDTO } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, imageDTO } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||
referenceImage.ipAdapter.image = imageDTO ? imageDTOToImageWithDims(imageDTO) : null;
|
||||
},
|
||||
rgIPAdapterWeightChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; weight: number }, 'regional_guidance'>>
|
||||
action: PayloadAction<EntityIdentifierPayload<{ referenceImageId: string; weight: number }, 'regional_guidance'>>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, weight } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, weight } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.weight = weight;
|
||||
referenceImage.ipAdapter.weight = weight;
|
||||
},
|
||||
rgIPAdapterBeginEndStepPctChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<{ ipAdapterId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
|
||||
EntityIdentifierPayload<{ referenceImageId: string; beginEndStepPct: [number, number] }, 'regional_guidance'>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, beginEndStepPct } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, beginEndStepPct } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.beginEndStepPct = beginEndStepPct;
|
||||
referenceImage.ipAdapter.beginEndStepPct = beginEndStepPct;
|
||||
},
|
||||
rgIPAdapterMethodChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ ipAdapterId: string; method: IPMethodV2 }, 'regional_guidance'>>
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<{ referenceImageId: string; method: IPMethodV2 }, 'regional_guidance'>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, method } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, method } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.method = method;
|
||||
referenceImage.ipAdapter.method = method;
|
||||
},
|
||||
rgIPAdapterModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<
|
||||
{
|
||||
ipAdapterId: string;
|
||||
referenceImageId: string;
|
||||
modelConfig: IPAdapterModelConfig | null;
|
||||
},
|
||||
'regional_guidance'
|
||||
>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, modelConfig } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, modelConfig } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
||||
referenceImage.ipAdapter.model = modelConfig ? zModelIdentifierField.parse(modelConfig) : null;
|
||||
},
|
||||
rgIPAdapterCLIPVisionModelChanged: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
EntityIdentifierPayload<{ ipAdapterId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
|
||||
EntityIdentifierPayload<{ referenceImageId: string; clipVisionModel: CLIPVisionModelV2 }, 'regional_guidance'>
|
||||
>
|
||||
) => {
|
||||
const { entityIdentifier, ipAdapterId, clipVisionModel } = action.payload;
|
||||
const ipAdapter = selectRegionalGuidanceIPAdapter(state, entityIdentifier, ipAdapterId);
|
||||
if (!ipAdapter) {
|
||||
const { entityIdentifier, referenceImageId, clipVisionModel } = action.payload;
|
||||
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
|
||||
if (!referenceImage) {
|
||||
return;
|
||||
}
|
||||
ipAdapter.clipVisionModel = clipVisionModel;
|
||||
referenceImage.ipAdapter.clipVisionModel = clipVisionModel;
|
||||
},
|
||||
//#region Inpaint mask
|
||||
inpaintMaskAdded: {
|
||||
@@ -659,31 +597,18 @@ export const canvasSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
const { id, overrides, isSelected, isMergingVisible } = action.payload;
|
||||
const entity: CanvasInpaintMaskState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'inpaint_mask',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
fill: {
|
||||
style: 'diagonal',
|
||||
color: { r: 255, g: 122, b: 0 }, // some orange color
|
||||
},
|
||||
};
|
||||
merge(entity, overrides);
|
||||
|
||||
const entityState = getInpaintMaskState(id, overrides);
|
||||
|
||||
if (isMergingVisible) {
|
||||
// When merging visible, we delete all disabled layers
|
||||
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((layer) => !layer.isEnabled);
|
||||
}
|
||||
|
||||
state.inpaintMasks.entities.push(entity);
|
||||
state.inpaintMasks.entities.push(entityState);
|
||||
|
||||
if (isSelected) {
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entity);
|
||||
state.selectedEntityIdentifier = getEntityIdentifier(entityState);
|
||||
}
|
||||
},
|
||||
prepare: (payload?: {
|
||||
@@ -886,11 +811,11 @@ export const canvasSlice = createSlice({
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
newEntity.id = getPrefixedId('regional_guidance');
|
||||
state.regions.entities.push(newEntity);
|
||||
state.regionalGuidance.entities.push(newEntity);
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
newEntity.id = getPrefixedId('ip_adapter');
|
||||
state.ipAdapters.entities.push(newEntity);
|
||||
case 'reference_image':
|
||||
newEntity.id = getPrefixedId('reference_image');
|
||||
state.referenceImages.entities.push(newEntity);
|
||||
break;
|
||||
case 'inpaint_mask':
|
||||
newEntity.id = getPrefixedId('inpaint_mask');
|
||||
@@ -1030,10 +955,12 @@ export const canvasSlice = createSlice({
|
||||
state.controlLayers.entities = state.controlLayers.entities.filter((rg) => rg.id !== entityIdentifier.id);
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
state.regions.entities = state.regions.entities.filter((rg) => rg.id !== entityIdentifier.id);
|
||||
state.regionalGuidance.entities = state.regionalGuidance.entities.filter(
|
||||
(rg) => rg.id !== entityIdentifier.id
|
||||
);
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
state.ipAdapters.entities = state.ipAdapters.entities.filter((rg) => rg.id !== entityIdentifier.id);
|
||||
case 'reference_image':
|
||||
state.referenceImages.entities = state.referenceImages.entities.filter((rg) => rg.id !== entityIdentifier.id);
|
||||
break;
|
||||
case 'inpaint_mask':
|
||||
state.inpaintMasks.entities = state.inpaintMasks.entities.filter((rg) => rg.id !== entityIdentifier.id);
|
||||
@@ -1080,7 +1007,7 @@ export const canvasSlice = createSlice({
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
if (entity.type === 'ip_adapter') {
|
||||
if (entity.type === 'reference_image') {
|
||||
return;
|
||||
}
|
||||
entity.opacity = opacity;
|
||||
@@ -1099,24 +1026,15 @@ export const canvasSlice = createSlice({
|
||||
state.inpaintMasks.isHidden = !state.inpaintMasks.isHidden;
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
state.regions.isHidden = !state.regions.isHidden;
|
||||
state.regionalGuidance.isHidden = !state.regionalGuidance.isHidden;
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
case 'reference_image':
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
},
|
||||
allEntitiesDeleted: (state) => {
|
||||
state.ipAdapters = deepClone(initialState.ipAdapters);
|
||||
state.rasterLayers = deepClone(initialState.rasterLayers);
|
||||
state.controlLayers = deepClone(initialState.controlLayers);
|
||||
state.regions = deepClone(initialState.regions);
|
||||
state.inpaintMasks = deepClone(initialState.inpaintMasks);
|
||||
|
||||
state.selectedEntityIdentifier = deepClone(initialState.selectedEntityIdentifier);
|
||||
},
|
||||
canvasReset: (state) => {
|
||||
const newState = deepClone(initialState);
|
||||
const newState = getInitialState();
|
||||
|
||||
// We need to retain the optimal dimension across resets, as it is changed only when the model changes. Copy it
|
||||
// from the old state, then recalculate the bbox size & scaled size.
|
||||
@@ -1212,14 +1130,14 @@ export const {
|
||||
controlLayerBeginEndStepPctChanged,
|
||||
controlLayerWithTransparencyEffectToggled,
|
||||
// IP Adapters
|
||||
ipaAdded,
|
||||
// ipaRecalled,
|
||||
ipaImageChanged,
|
||||
ipaMethodChanged,
|
||||
ipaModelChanged,
|
||||
ipaCLIPVisionModelChanged,
|
||||
ipaWeightChanged,
|
||||
ipaBeginEndStepPctChanged,
|
||||
referenceImageAdded,
|
||||
// referenceImageRecalled,
|
||||
referenceImageIPAdapterImageChanged,
|
||||
referenceImageIPAdapterMethodChanged,
|
||||
referenceImageIPAdapterModelChanged,
|
||||
referenceImageIPAdapterCLIPVisionModelChanged,
|
||||
referenceImageIPAdapterWeightChanged,
|
||||
referenceImageIPAdapterBeginEndStepPctChanged,
|
||||
// Regions
|
||||
rgAdded,
|
||||
// rgRecalled,
|
||||
@@ -1312,7 +1230,6 @@ function actionsThrottlingFilter(action: UnknownAction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const $lastCanvasProgressEvent = atom<S['InvocationDenoiseProgressEvent'] | null>(null);
|
||||
/**
|
||||
* The global canvas manager instance.
|
||||
*/
|
||||
|
||||
@@ -3,15 +3,14 @@ import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { canvasReset } from 'features/controlLayers/store/canvasSlice';
|
||||
import type { StagingAreaImage } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasQueueCounts } from 'services/api/endpoints/queue';
|
||||
|
||||
type CanvasStagingAreaState = {
|
||||
isStaging: boolean;
|
||||
stagedImages: StagingAreaImage[];
|
||||
selectedStagedImageIndex: number;
|
||||
};
|
||||
|
||||
const initialState: CanvasStagingAreaState = {
|
||||
isStaging: false,
|
||||
stagedImages: [],
|
||||
selectedStagedImageIndex: 0,
|
||||
};
|
||||
@@ -20,13 +19,8 @@ export const canvasStagingAreaSlice = createSlice({
|
||||
name: 'canvasStagingArea',
|
||||
initialState,
|
||||
reducers: {
|
||||
stagingAreaStartedStaging: (state) => {
|
||||
state.isStaging = true;
|
||||
state.selectedStagedImageIndex = 0;
|
||||
},
|
||||
stagingAreaImageStaged: (state, action: PayloadAction<{ stagingAreaImage: StagingAreaImage }>) => {
|
||||
const { stagingAreaImage } = action.payload;
|
||||
state.isStaging = true;
|
||||
state.stagedImages.push(stagingAreaImage);
|
||||
state.selectedStagedImageIndex = state.stagedImages.length - 1;
|
||||
},
|
||||
@@ -41,12 +35,8 @@ export const canvasStagingAreaSlice = createSlice({
|
||||
const { index } = action.payload;
|
||||
state.stagedImages.splice(index, 1);
|
||||
state.selectedStagedImageIndex = Math.min(state.selectedStagedImageIndex, state.stagedImages.length - 1);
|
||||
if (state.stagedImages.length === 0) {
|
||||
state.isStaging = false;
|
||||
}
|
||||
},
|
||||
stagingAreaReset: (state) => {
|
||||
state.isStaging = false;
|
||||
state.stagedImages = [];
|
||||
state.selectedStagedImageIndex = 0;
|
||||
},
|
||||
@@ -60,7 +50,6 @@ export const canvasStagingAreaSlice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
stagingAreaStartedStaging,
|
||||
stagingAreaImageStaged,
|
||||
stagingAreaStagedImageDiscarded,
|
||||
stagingAreaReset,
|
||||
@@ -83,4 +72,21 @@ export const canvasStagingAreaPersistConfig: PersistConfig<CanvasStagingAreaStat
|
||||
|
||||
export const selectCanvasStagingAreaSlice = (s: RootState) => s.canvasStagingArea;
|
||||
|
||||
export const selectIsStaging = createSelector(selectCanvasStagingAreaSlice, (stagingaArea) => stagingaArea.isStaging);
|
||||
/**
|
||||
* Selects if we should be staging images. This is true if:
|
||||
* - There are staged images.
|
||||
* - There are any in-progress or pending canvas queue items.
|
||||
*/
|
||||
export const selectIsStaging = createSelector(
|
||||
selectCanvasQueueCounts,
|
||||
selectCanvasStagingAreaSlice,
|
||||
({ data }, staging) => {
|
||||
if (staging.stagedImages.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
return data.in_progress > 0 || data.pending > 0;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
ParameterCanvasCoherenceMode,
|
||||
ParameterCFGRescaleMultiplier,
|
||||
ParameterCFGScale,
|
||||
ParameterCLIPEmbedModel,
|
||||
ParameterGuidance,
|
||||
ParameterMaskBlurMethod,
|
||||
ParameterModel,
|
||||
ParameterNegativePrompt,
|
||||
@@ -19,6 +21,7 @@ import type {
|
||||
ParameterSeed,
|
||||
ParameterSteps,
|
||||
ParameterStrength,
|
||||
ParameterT5EncoderModel,
|
||||
ParameterVAEModel,
|
||||
} from 'features/parameters/types/parameterSchemas';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -35,6 +38,7 @@ export type ParamsState = {
|
||||
infillColorValue: RgbaColor;
|
||||
cfgScale: ParameterCFGScale;
|
||||
cfgRescaleMultiplier: ParameterCFGRescaleMultiplier;
|
||||
guidance: ParameterGuidance;
|
||||
img2imgStrength: ParameterStrength;
|
||||
iterations: number;
|
||||
scheduler: ParameterScheduler;
|
||||
@@ -44,6 +48,7 @@ export type ParamsState = {
|
||||
model: ParameterModel | null;
|
||||
vae: ParameterVAEModel | null;
|
||||
vaePrecision: ParameterPrecision;
|
||||
fluxVAE: ParameterVAEModel | null;
|
||||
seamlessXAxis: boolean;
|
||||
seamlessYAxis: boolean;
|
||||
clipSkip: number;
|
||||
@@ -60,6 +65,8 @@ export type ParamsState = {
|
||||
refinerPositiveAestheticScore: number;
|
||||
refinerNegativeAestheticScore: number;
|
||||
refinerStart: number;
|
||||
t5EncoderModel: ParameterT5EncoderModel | null;
|
||||
clipEmbedModel: ParameterCLIPEmbedModel | null;
|
||||
};
|
||||
|
||||
const initialState: ParamsState = {
|
||||
@@ -74,14 +81,16 @@ const initialState: ParamsState = {
|
||||
infillColorValue: { r: 0, g: 0, b: 0, a: 1 },
|
||||
cfgScale: 7.5,
|
||||
cfgRescaleMultiplier: 0,
|
||||
guidance: 4,
|
||||
img2imgStrength: 0.75,
|
||||
iterations: 1,
|
||||
scheduler: 'euler',
|
||||
seed: 0,
|
||||
shouldRandomizeSeed: true,
|
||||
steps: 50,
|
||||
steps: 30,
|
||||
model: null,
|
||||
vae: null,
|
||||
fluxVAE: null,
|
||||
vaePrecision: 'fp32',
|
||||
seamlessXAxis: false,
|
||||
seamlessYAxis: false,
|
||||
@@ -99,6 +108,8 @@ const initialState: ParamsState = {
|
||||
refinerPositiveAestheticScore: 6,
|
||||
refinerNegativeAestheticScore: 2.5,
|
||||
refinerStart: 0.8,
|
||||
t5EncoderModel: null,
|
||||
clipEmbedModel: null,
|
||||
};
|
||||
|
||||
export const paramsSlice = createSlice({
|
||||
@@ -114,6 +125,9 @@ export const paramsSlice = createSlice({
|
||||
setCfgScale: (state, action: PayloadAction<ParameterCFGScale>) => {
|
||||
state.cfgScale = action.payload;
|
||||
},
|
||||
setGuidance: (state, action: PayloadAction<ParameterGuidance>) => {
|
||||
state.guidance = action.payload;
|
||||
},
|
||||
setCfgRescaleMultiplier: (state, action: PayloadAction<ParameterCFGRescaleMultiplier>) => {
|
||||
state.cfgRescaleMultiplier = action.payload;
|
||||
},
|
||||
@@ -161,6 +175,15 @@ export const paramsSlice = createSlice({
|
||||
// null is a valid VAE!
|
||||
state.vae = action.payload;
|
||||
},
|
||||
fluxVAESelected: (state, action: PayloadAction<ParameterVAEModel | null>) => {
|
||||
state.fluxVAE = action.payload;
|
||||
},
|
||||
t5EncoderModelSelected: (state, action: PayloadAction<ParameterT5EncoderModel | null>) => {
|
||||
state.t5EncoderModel = action.payload;
|
||||
},
|
||||
clipEmbedModelSelected: (state, action: PayloadAction<ParameterCLIPEmbedModel | null>) => {
|
||||
state.clipEmbedModel = action.payload;
|
||||
},
|
||||
vaePrecisionChanged: (state, action: PayloadAction<ParameterPrecision>) => {
|
||||
state.vaePrecision = action.payload;
|
||||
},
|
||||
@@ -246,6 +269,7 @@ export const {
|
||||
setSteps,
|
||||
setCfgScale,
|
||||
setCfgRescaleMultiplier,
|
||||
setGuidance,
|
||||
setScheduler,
|
||||
setSeed,
|
||||
setImg2imgStrength,
|
||||
@@ -253,7 +277,10 @@ export const {
|
||||
setSeamlessYAxis,
|
||||
setShouldRandomizeSeed,
|
||||
vaeSelected,
|
||||
fluxVAESelected,
|
||||
vaePrecisionChanged,
|
||||
t5EncoderModelSelected,
|
||||
clipEmbedModelSelected,
|
||||
setClipSkip,
|
||||
shouldUseCpuNoiseChanged,
|
||||
positivePromptChanged,
|
||||
@@ -289,11 +316,17 @@ export const createParamsSelector = <T>(selector: Selector<ParamsState, T>) =>
|
||||
|
||||
export const selectBase = createParamsSelector((params) => params.model?.base);
|
||||
export const selectIsSDXL = createParamsSelector((params) => params.model?.base === 'sdxl');
|
||||
export const selectIsFLUX = createParamsSelector((params) => params.model?.base === 'flux');
|
||||
|
||||
export const selectModel = createParamsSelector((params) => params.model);
|
||||
export const selectModelKey = createParamsSelector((params) => params.model?.key);
|
||||
export const selectVAE = createParamsSelector((params) => params.vae);
|
||||
export const selectFLUXVAE = createParamsSelector((params) => params.fluxVAE);
|
||||
export const selectVAEKey = createParamsSelector((params) => params.vae?.key);
|
||||
export const selectT5EncoderModel = createParamsSelector((params) => params.t5EncoderModel);
|
||||
export const selectCLIPEmbedModel = createParamsSelector((params) => params.clipEmbedModel);
|
||||
export const selectCFGScale = createParamsSelector((params) => params.cfgScale);
|
||||
export const selectGuidance = createParamsSelector((params) => params.guidance);
|
||||
export const selectSteps = createParamsSelector((params) => params.steps);
|
||||
export const selectCFGRescaleMultiplier = createParamsSelector((params) => params.cfgRescaleMultiplier);
|
||||
export const selectCLIPSKip = createParamsSelector((params) => params.clipSkip);
|
||||
|
||||
@@ -31,8 +31,8 @@ export const selectCanvasSlice = (state: RootState) => state.canvas.present;
|
||||
*/
|
||||
const selectEntityCountAll = createSelector(selectCanvasSlice, (canvas) => {
|
||||
return (
|
||||
canvas.regions.entities.length +
|
||||
canvas.ipAdapters.entities.length +
|
||||
canvas.regionalGuidance.entities.length +
|
||||
canvas.referenceImages.entities.length +
|
||||
canvas.rasterLayers.entities.length +
|
||||
canvas.controlLayers.entities.length +
|
||||
canvas.inpaintMasks.entities.length
|
||||
@@ -52,11 +52,11 @@ const selectActiveInpaintMaskEntities = createSelector(selectCanvasSlice, (canva
|
||||
);
|
||||
|
||||
const selectActiveRegionalGuidanceEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.regions.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
canvas.regionalGuidance.entities.filter((e) => e.isEnabled && e.objects.length > 0)
|
||||
);
|
||||
|
||||
const selectActiveIPAdapterEntities = createSelector(selectCanvasSlice, (canvas) =>
|
||||
canvas.ipAdapters.entities.filter((e) => e.isEnabled)
|
||||
canvas.referenceImages.entities.filter((e) => e.isEnabled)
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -127,10 +127,10 @@ export function selectEntity<T extends CanvasEntityIdentifier>(
|
||||
entity = state.inpaintMasks.entities.find((entity) => entity.id === id);
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
entity = state.regions.entities.find((entity) => entity.id === id);
|
||||
entity = state.regionalGuidance.entities.find((entity) => entity.id === id);
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
entity = state.ipAdapters.entities.find((entity) => entity.id === id);
|
||||
case 'reference_image':
|
||||
entity = state.referenceImages.entities.find((entity) => entity.id === id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -171,10 +171,10 @@ export function selectAllEntitiesOfType<T extends CanvasEntityState['type']>(
|
||||
entities = state.inpaintMasks.entities;
|
||||
break;
|
||||
case 'regional_guidance':
|
||||
entities = state.regions.entities;
|
||||
entities = state.regionalGuidance.entities;
|
||||
break;
|
||||
case 'ip_adapter':
|
||||
entities = state.ipAdapters.entities;
|
||||
case 'reference_image':
|
||||
entities = state.referenceImages.entities;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ export function selectAllEntities(state: CanvasState): CanvasEntityState[] {
|
||||
// These are in the same order as they are displayed in the list!
|
||||
return [
|
||||
...state.inpaintMasks.entities.toReversed(),
|
||||
...state.regions.entities.toReversed(),
|
||||
...state.ipAdapters.entities.toReversed(),
|
||||
...state.regionalGuidance.entities.toReversed(),
|
||||
...state.referenceImages.entities.toReversed(),
|
||||
...state.controlLayers.entities.toReversed(),
|
||||
...state.rasterLayers.entities.toReversed(),
|
||||
];
|
||||
@@ -210,23 +210,23 @@ export function selectAllRenderableEntities(
|
||||
...state.rasterLayers.entities,
|
||||
...state.controlLayers.entities,
|
||||
...state.inpaintMasks.entities,
|
||||
...state.regions.entities,
|
||||
...state.regionalGuidance.entities,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the IP adapter for the specific Regional Guidance layer.
|
||||
*/
|
||||
export function selectRegionalGuidanceIPAdapter(
|
||||
export function selectRegionalGuidanceReferenceImage(
|
||||
state: CanvasState,
|
||||
entityIdentifier: CanvasEntityIdentifier<'regional_guidance'>,
|
||||
ipAdapterId: string
|
||||
referenceImageId: string
|
||||
) {
|
||||
const entity = selectEntity(state, entityIdentifier);
|
||||
if (!entity) {
|
||||
return undefined;
|
||||
}
|
||||
return entity.ipAdapters.find((ipAdapter) => ipAdapter.id === ipAdapterId);
|
||||
return entity.referenceImages.find(({ id }) => id === referenceImageId);
|
||||
}
|
||||
|
||||
export const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
|
||||
@@ -264,7 +264,7 @@ export const selectSelectedEntityFill = createSelector(
|
||||
const selectRasterLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.rasterLayers.isHidden);
|
||||
const selectControlLayersIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.controlLayers.isHidden);
|
||||
const selectInpaintMasksIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.inpaintMasks.isHidden);
|
||||
const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.regions.isHidden);
|
||||
const selectRegionalGuidanceIsHidden = createSelector(selectCanvasSlice, (canvas) => canvas.regionalGuidance.isHidden);
|
||||
|
||||
/**
|
||||
* Returns the hidden selector for the given entity type.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||
import type { AspectRatioState } from 'features/parameters/components/Bbox/types';
|
||||
import type { ParameterHeight, ParameterLoRAModel, ParameterWidth } from 'features/parameters/types/parameterSchemas';
|
||||
@@ -114,6 +113,7 @@ const zCanvasImageState = z.object({
|
||||
image: zImageWithDims,
|
||||
});
|
||||
export type CanvasImageState = z.infer<typeof zCanvasImageState>;
|
||||
export const isCanvasImageState = (v: unknown): v is CanvasImageState => zCanvasImageState.safeParse(v).success;
|
||||
|
||||
const zCanvasObjectState = z.discriminatedUnion('type', [
|
||||
zCanvasImageState,
|
||||
@@ -124,6 +124,7 @@ const zCanvasObjectState = z.discriminatedUnion('type', [
|
||||
export type CanvasObjectState = z.infer<typeof zCanvasObjectState>;
|
||||
|
||||
const zIPAdapterConfig = z.object({
|
||||
type: z.literal('ip_adapter'),
|
||||
image: zImageWithDims.nullable(),
|
||||
model: zModelIdentifierField.nullable(),
|
||||
weight: z.number().gte(-1).lte(2),
|
||||
@@ -140,21 +141,22 @@ const zCanvasEntityBase = z.object({
|
||||
isLocked: z.boolean(),
|
||||
});
|
||||
|
||||
const zCanvasIPAdapterState = zCanvasEntityBase.extend({
|
||||
type: z.literal('ip_adapter'),
|
||||
const zCanvasReferenceImageState = zCanvasEntityBase.extend({
|
||||
type: z.literal('reference_image'),
|
||||
ipAdapter: zIPAdapterConfig,
|
||||
});
|
||||
export type CanvasIPAdapterState = z.infer<typeof zCanvasIPAdapterState>;
|
||||
export type CanvasReferenceImageState = z.infer<typeof zCanvasReferenceImageState>;
|
||||
|
||||
const zFillStyle = z.enum(['solid', 'grid', 'crosshatch', 'diagonal', 'horizontal', 'vertical']);
|
||||
export type FillStyle = z.infer<typeof zFillStyle>;
|
||||
export const isFillStyle = (v: unknown): v is FillStyle => zFillStyle.safeParse(v).success;
|
||||
const zFill = z.object({ style: zFillStyle, color: zRgbColor });
|
||||
|
||||
const zRegionalGuidanceIPAdapterConfig = zIPAdapterConfig.extend({
|
||||
const zRegionalGuidanceReferenceImageState = z.object({
|
||||
id: zId,
|
||||
ipAdapter: zIPAdapterConfig,
|
||||
});
|
||||
export type RegionalGuidanceIPAdapterConfig = z.infer<typeof zRegionalGuidanceIPAdapterConfig>;
|
||||
export type RegionalGuidanceReferenceImageState = z.infer<typeof zRegionalGuidanceReferenceImageState>;
|
||||
|
||||
const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
|
||||
type: z.literal('regional_guidance'),
|
||||
@@ -164,7 +166,7 @@ const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
|
||||
fill: zFill,
|
||||
positivePrompt: zParameterPositivePrompt.nullable(),
|
||||
negativePrompt: zParameterNegativePrompt.nullable(),
|
||||
ipAdapters: z.array(zRegionalGuidanceIPAdapterConfig),
|
||||
referenceImages: z.array(zRegionalGuidanceReferenceImageState),
|
||||
autoNegative: z.boolean(),
|
||||
});
|
||||
export type CanvasRegionalGuidanceState = z.infer<typeof zCanvasRegionalGuidanceState>;
|
||||
@@ -210,50 +212,6 @@ const zCanvasControlLayerState = zCanvasRasterLayerState.extend({
|
||||
});
|
||||
export type CanvasControlLayerState = z.infer<typeof zCanvasControlLayerState>;
|
||||
|
||||
export const initialControlNet: ControlNetConfig = {
|
||||
type: 'controlnet',
|
||||
model: null,
|
||||
weight: 1,
|
||||
beginEndStepPct: [0, 1],
|
||||
controlMode: 'balanced',
|
||||
};
|
||||
|
||||
export const initialT2IAdapter: T2IAdapterConfig = {
|
||||
type: 't2i_adapter',
|
||||
model: null,
|
||||
weight: 1,
|
||||
beginEndStepPct: [0, 1],
|
||||
};
|
||||
|
||||
export const initialIPAdapter: IPAdapterConfig = {
|
||||
image: null,
|
||||
model: null,
|
||||
beginEndStepPct: [0, 1],
|
||||
method: 'full',
|
||||
clipVisionModel: 'ViT-H',
|
||||
weight: 1,
|
||||
};
|
||||
|
||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||
image_name,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<CanvasImageState>): CanvasImageState => {
|
||||
const { width, height, image_name } = imageDTO;
|
||||
return {
|
||||
id: getPrefixedId('image'),
|
||||
type: 'image',
|
||||
image: {
|
||||
image_name,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const zBoundingBoxScaleMethod = z.enum(['none', 'auto', 'manual']);
|
||||
export type BoundingBoxScaleMethod = z.infer<typeof zBoundingBoxScaleMethod>;
|
||||
export const isBoundingBoxScaleMethod = (v: unknown): v is BoundingBoxScaleMethod =>
|
||||
@@ -264,7 +222,7 @@ export type CanvasEntityState =
|
||||
| CanvasControlLayerState
|
||||
| CanvasRegionalGuidanceState
|
||||
| CanvasInpaintMaskState
|
||||
| CanvasIPAdapterState;
|
||||
| CanvasReferenceImageState;
|
||||
|
||||
export type CanvasRenderableEntityState =
|
||||
| CanvasRasterLayerState
|
||||
@@ -304,12 +262,12 @@ export type CanvasState = {
|
||||
isHidden: boolean;
|
||||
entities: CanvasControlLayerState[];
|
||||
};
|
||||
regions: {
|
||||
regionalGuidance: {
|
||||
isHidden: boolean;
|
||||
entities: CanvasRegionalGuidanceState[];
|
||||
};
|
||||
ipAdapters: {
|
||||
entities: CanvasIPAdapterState[];
|
||||
referenceImages: {
|
||||
entities: CanvasReferenceImageState[];
|
||||
};
|
||||
bbox: {
|
||||
rect: {
|
||||
@@ -426,6 +384,12 @@ export function isTransformableEntityIdentifier(
|
||||
);
|
||||
}
|
||||
|
||||
export function isSaveableEntityIdentifier(
|
||||
entityIdentifier: CanvasEntityIdentifier
|
||||
): entityIdentifier is CanvasEntityIdentifier<'raster_layer'> | CanvasEntityIdentifier<'control_layer'> {
|
||||
return isRasterLayerEntityIdentifier(entityIdentifier) || isControlLayerEntityIdentifier(entityIdentifier);
|
||||
}
|
||||
|
||||
export function isRenderableEntity(entity: CanvasEntityState): entity is CanvasRenderableEntityState {
|
||||
return isRenderableEntityType(entity.type);
|
||||
}
|
||||
|
||||
186
invokeai/frontend/web/src/features/controlLayers/store/util.ts
Normal file
186
invokeai/frontend/web/src/features/controlLayers/store/util.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import type {
|
||||
CanvasControlLayerState,
|
||||
CanvasImageState,
|
||||
CanvasInpaintMaskState,
|
||||
CanvasRasterLayerState,
|
||||
CanvasReferenceImageState,
|
||||
CanvasRegionalGuidanceState,
|
||||
ControlNetConfig,
|
||||
ImageWithDims,
|
||||
IPAdapterConfig,
|
||||
RgbColor,
|
||||
T2IAdapterConfig,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { merge } from 'lodash-es';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<CanvasImageState>): CanvasImageState => {
|
||||
const { width, height, image_name } = imageDTO;
|
||||
return {
|
||||
id: getPrefixedId('image'),
|
||||
type: 'image',
|
||||
image: {
|
||||
image_name,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO): ImageWithDims => ({
|
||||
image_name,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const DEFAULT_RG_MASK_FILL_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)
|
||||
];
|
||||
const buildMaskFillCycler = (initialIndex: number): (() => RgbColor) => {
|
||||
let lastFillIndex = initialIndex;
|
||||
|
||||
return () => {
|
||||
lastFillIndex = (lastFillIndex + 1) % DEFAULT_RG_MASK_FILL_COLORS.length;
|
||||
const fill = DEFAULT_RG_MASK_FILL_COLORS[lastFillIndex];
|
||||
assert(fill, 'This should never happen');
|
||||
return fill;
|
||||
};
|
||||
};
|
||||
|
||||
const getInpaintMaskFillColor = buildMaskFillCycler(3);
|
||||
const getRegionalGuidanceMaskFillColor = buildMaskFillCycler(0);
|
||||
|
||||
export const initialIPAdapter: IPAdapterConfig = {
|
||||
type: 'ip_adapter',
|
||||
image: null,
|
||||
model: null,
|
||||
beginEndStepPct: [0, 1],
|
||||
method: 'full',
|
||||
clipVisionModel: 'ViT-H',
|
||||
weight: 1,
|
||||
};
|
||||
export const initialT2IAdapter: T2IAdapterConfig = {
|
||||
type: 't2i_adapter',
|
||||
model: null,
|
||||
weight: 1,
|
||||
beginEndStepPct: [0, 1],
|
||||
};
|
||||
export const initialControlNet: ControlNetConfig = {
|
||||
type: 'controlnet',
|
||||
model: null,
|
||||
weight: 1,
|
||||
beginEndStepPct: [0, 1],
|
||||
controlMode: 'balanced',
|
||||
};
|
||||
|
||||
export const getReferenceImageState = (
|
||||
id: string,
|
||||
overrides?: Partial<CanvasReferenceImageState>
|
||||
): CanvasReferenceImageState => {
|
||||
const entityState: CanvasReferenceImageState = {
|
||||
id,
|
||||
type: 'reference_image',
|
||||
name: null,
|
||||
isLocked: false,
|
||||
isEnabled: true,
|
||||
ipAdapter: deepClone(initialIPAdapter),
|
||||
};
|
||||
merge(entityState, overrides);
|
||||
return entityState;
|
||||
};
|
||||
|
||||
export const getRegionalGuidanceState = (
|
||||
id: string,
|
||||
overrides?: Partial<CanvasRegionalGuidanceState>
|
||||
): CanvasRegionalGuidanceState => {
|
||||
const entityState: CanvasRegionalGuidanceState = {
|
||||
id,
|
||||
name: null,
|
||||
isLocked: false,
|
||||
type: 'regional_guidance',
|
||||
isEnabled: true,
|
||||
objects: [],
|
||||
fill: {
|
||||
style: 'solid',
|
||||
color: getRegionalGuidanceMaskFillColor(),
|
||||
},
|
||||
opacity: 0.5,
|
||||
position: { x: 0, y: 0 },
|
||||
autoNegative: false,
|
||||
positivePrompt: null,
|
||||
negativePrompt: null,
|
||||
referenceImages: [],
|
||||
};
|
||||
merge(entityState, overrides);
|
||||
return entityState;
|
||||
};
|
||||
|
||||
export const getControlLayerState = (
|
||||
id: string,
|
||||
overrides?: Partial<CanvasControlLayerState>
|
||||
): CanvasControlLayerState => {
|
||||
const entityState: CanvasControlLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'control_layer',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
withTransparencyEffect: true,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
controlAdapter: deepClone(initialControlNet),
|
||||
};
|
||||
merge(entityState, overrides);
|
||||
return entityState;
|
||||
};
|
||||
|
||||
export const getRasterLayerState = (
|
||||
id: string,
|
||||
overrides?: Partial<CanvasRasterLayerState>
|
||||
): CanvasRasterLayerState => {
|
||||
const entityState: CanvasRasterLayerState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'raster_layer',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
merge(entityState, overrides);
|
||||
return entityState;
|
||||
};
|
||||
|
||||
export const getInpaintMaskState = (
|
||||
id: string,
|
||||
overrides?: Partial<CanvasInpaintMaskState>
|
||||
): CanvasInpaintMaskState => {
|
||||
const entityState: CanvasInpaintMaskState = {
|
||||
id,
|
||||
name: null,
|
||||
type: 'inpaint_mask',
|
||||
isEnabled: true,
|
||||
isLocked: false,
|
||||
objects: [],
|
||||
opacity: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
fill: {
|
||||
style: 'diagonal',
|
||||
color: getInpaintMaskFillColor(),
|
||||
},
|
||||
};
|
||||
merge(entityState, overrides);
|
||||
return entityState;
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
import { $isConnected } from 'services/events/stores';
|
||||
|
||||
type DeleteImageButtonProps = Omit<IconButtonProps, 'aria-label'> & {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'features/deleteImageModal/store/slice';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectSystemSlice, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { some } from 'lodash-es';
|
||||
import type { ChangeEvent } from 'react';
|
||||
@@ -21,17 +22,22 @@ import { useTranslation } from 'react-i18next';
|
||||
import ImageUsageMessage from './ImageUsageMessage';
|
||||
|
||||
const selectImageUsages = createMemoizedSelector(
|
||||
[selectDeleteImageModalSlice, selectNodesSlice, selectCanvasSlice, selectImageUsage],
|
||||
(deleteImageModal, nodes, canvas, imagesUsage) => {
|
||||
[selectDeleteImageModalSlice, selectNodesSlice, selectCanvasSlice, selectImageUsage, selectUpscaleSlice],
|
||||
(deleteImageModal, nodes, canvas, imagesUsage, upscale) => {
|
||||
const { imagesToDelete } = deleteImageModal;
|
||||
|
||||
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) => getImageUsage(nodes, canvas, image_name));
|
||||
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
|
||||
getImageUsage(nodes, canvas, upscale, image_name)
|
||||
);
|
||||
|
||||
const imageUsageSummary: ImageUsage = {
|
||||
isLayerImage: some(allImageUsage, (i) => i.isLayerImage),
|
||||
isUpscaleImage: some(allImageUsage, (i) => i.isUpscaleImage),
|
||||
isRasterLayerImage: some(allImageUsage, (i) => i.isRasterLayerImage),
|
||||
isInpaintMaskImage: some(allImageUsage, (i) => i.isInpaintMaskImage),
|
||||
isRegionalGuidanceImage: some(allImageUsage, (i) => i.isRegionalGuidanceImage),
|
||||
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
|
||||
isControlAdapterImage: some(allImageUsage, (i) => i.isControlAdapterImage),
|
||||
isIPAdapterImage: some(allImageUsage, (i) => i.isIPAdapterImage),
|
||||
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
|
||||
isReferenceImage: some(allImageUsage, (i) => i.isReferenceImage),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -78,8 +84,8 @@ const DeleteImageModal = () => {
|
||||
title={t('gallery.deleteImage', { count: imagesToDelete.length })}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleClose}
|
||||
cancelButtonText={t('boards.cancel')}
|
||||
acceptButtonText={t('controlnet.delete')}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
acceptButtonText={t('common.delete')}
|
||||
acceptCallback={handleDelete}
|
||||
useInert={false}
|
||||
>
|
||||
|
||||
@@ -28,10 +28,13 @@ const ImageUsageMessage = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Text>{topMessage}</Text>
|
||||
<UnorderedList paddingInlineStart={6}>
|
||||
{imageUsage.isLayerImage && <ListItem>{t('controlLayers.layers')}</ListItem>}
|
||||
{imageUsage.isControlAdapterImage && <ListItem>{t('controlLayers.controlAdapters')}</ListItem>}
|
||||
{imageUsage.isIPAdapterImage && <ListItem>{t('controlLayers.ipAdapters')}</ListItem>}
|
||||
<UnorderedList paddingInlineStart={6} fontSize="sm">
|
||||
{imageUsage.isControlLayerImage && <ListItem>{t('controlLayers.controlLayer')}</ListItem>}
|
||||
{imageUsage.isReferenceImage && <ListItem>{t('controlLayers.referenceImage')}</ListItem>}
|
||||
{imageUsage.isInpaintMaskImage && <ListItem>{t('controlLayers.inpaintMask')}</ListItem>}
|
||||
{imageUsage.isRasterLayerImage && <ListItem>{t('controlLayers.rasterLayer')}</ListItem>}
|
||||
{imageUsage.isRegionalGuidanceImage && <ListItem>{t('controlLayers.regionalGuidance')}</ListItem>}
|
||||
{imageUsage.isUpscaleImage && <ListItem>{t('ui.tabs.upscalingTab')}</ListItem>}
|
||||
{imageUsage.isNodesImage && <ListItem>{t('ui.tabs.workflowsTab')}</ListItem>}
|
||||
</UnorderedList>
|
||||
<Text>{bottomMessage}</Text>
|
||||
|
||||
@@ -1,31 +1,54 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasState } from 'features/controlLayers/store/types';
|
||||
import { type CanvasState, isCanvasImageState } from 'features/controlLayers/store/types';
|
||||
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import type { UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { some } from 'lodash-es';
|
||||
|
||||
import type { ImageUsage } from './types';
|
||||
// TODO(psyche): handle image deletion (canvas staging area?)
|
||||
export const getImageUsage = (nodes: NodesState, canvas: CanvasState, image_name: string) => {
|
||||
export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
|
||||
const isNodesImage = nodes.nodes
|
||||
.filter(isInvocationNode)
|
||||
.some((node) =>
|
||||
some(node.data.inputs, (input) => isImageFieldInputInstance(input) && input.value?.image_name === image_name)
|
||||
);
|
||||
|
||||
const isIPAdapterImage = canvas.ipAdapters.entities.some(
|
||||
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
|
||||
|
||||
const isReferenceImage = canvas.referenceImages.entities.some(
|
||||
({ ipAdapter }) => ipAdapter.image?.image_name === image_name
|
||||
);
|
||||
|
||||
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => isCanvasImageState(obj) && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => isCanvasImageState(obj) && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
|
||||
objects.some((obj) => isCanvasImageState(obj) && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
|
||||
referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isLayerImage: false,
|
||||
isUpscaleImage,
|
||||
isRasterLayerImage,
|
||||
isInpaintMaskImage,
|
||||
isRegionalGuidanceImage,
|
||||
isNodesImage,
|
||||
isControlAdapterImage: false,
|
||||
isIPAdapterImage,
|
||||
isControlLayerImage,
|
||||
isReferenceImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
@@ -35,14 +58,15 @@ export const selectImageUsage = createMemoizedSelector(
|
||||
selectDeleteImageModalSlice,
|
||||
selectNodesSlice,
|
||||
selectCanvasSlice,
|
||||
(deleteImageModal, nodes, canvas) => {
|
||||
selectUpscaleSlice,
|
||||
(deleteImageModal, nodes, canvas, upscale) => {
|
||||
const { imagesToDelete } = deleteImageModal;
|
||||
|
||||
if (!imagesToDelete.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvas, i.image_name));
|
||||
const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvas, upscale, i.image_name));
|
||||
|
||||
return imagesUsage;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ export type DeleteImageState = {
|
||||
};
|
||||
|
||||
export type ImageUsage = {
|
||||
isUpscaleImage: boolean;
|
||||
isRasterLayerImage: boolean;
|
||||
isInpaintMaskImage: boolean;
|
||||
isRegionalGuidanceImage: boolean;
|
||||
isNodesImage: boolean;
|
||||
isControlAdapterImage: boolean;
|
||||
isIPAdapterImage: boolean;
|
||||
isLayerImage: boolean;
|
||||
isControlLayerImage: boolean;
|
||||
isReferenceImage: boolean;
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export type RGIPAdapterImageDropData = BaseDropData & {
|
||||
actionType: 'SET_RG_IP_ADAPTER_IMAGE';
|
||||
context: {
|
||||
id: string;
|
||||
ipAdapterId: string;
|
||||
referenceImageId: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,6 +42,14 @@ export type AddControlLayerFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_CONTROL_LAYER_FROM_IMAGE';
|
||||
};
|
||||
|
||||
export type AddRegionalReferenceImageFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_REGIONAL_REFERENCE_IMAGE_FROM_IMAGE';
|
||||
};
|
||||
|
||||
export type AddGlobalReferenceImageFromImageDropData = BaseDropData & {
|
||||
actionType: 'ADD_GLOBAL_REFERENCE_IMAGE_FROM_IMAGE';
|
||||
};
|
||||
|
||||
export type ReplaceLayerImageDropData = BaseDropData & {
|
||||
actionType: 'REPLACE_LAYER_WITH_IMAGE';
|
||||
context: {
|
||||
@@ -88,7 +96,9 @@ export type TypesafeDroppableData =
|
||||
| UpscaleInitialImageDropData
|
||||
| AddRasterLayerFromImageDropData
|
||||
| AddControlLayerFromImageDropData
|
||||
| ReplaceLayerImageDropData;
|
||||
| ReplaceLayerImageDropData
|
||||
| AddRegionalReferenceImageFromImageDropData
|
||||
| AddGlobalReferenceImageFromImageDropData;
|
||||
|
||||
type BaseDragData = {
|
||||
id: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user