Merge branch 'main' into ryan/flux-lora-quantized

This commit is contained in:
Ryan Dick
2024-09-18 13:22:39 -04:00
committed by GitHub
185 changed files with 2992 additions and 1681 deletions

View File

@@ -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
)

View File

@@ -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",
]

View File

@@ -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"""

View File

@@ -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")

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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'));

View File

@@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { 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);

View File

@@ -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';

View File

@@ -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());

View File

@@ -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);
}
},
});
};

View File

@@ -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;
}

View File

@@ -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 }));
}
});
};

View File

@@ -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
*/

View File

@@ -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));

View File

@@ -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;

View File

@@ -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));
}
};

View File

@@ -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,

View File

@@ -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({

View File

@@ -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'>;

View File

@@ -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}>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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'}

View File

@@ -30,6 +30,7 @@ export type Feature =
| 'noiseUseCPU'
| 'paramAspect'
| 'paramCFGScale'
| 'paramGuidance'
| 'paramCFGRescaleMultiplier'
| 'paramDenoisingStrength'
| 'paramHeight'

View File

@@ -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 {

View File

@@ -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'));

View 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;
};

View File

@@ -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>
);
});

View File

@@ -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>
</>
);
});

View File

@@ -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>
);

View File

@@ -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>
</>
);
});

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 />
</>
);

View File

@@ -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}>

View File

@@ -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';

View File

@@ -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} />
))}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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 />
</>
);

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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';

View File

@@ -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}
/>

View File

@@ -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>
))}
</>

View File

@@ -33,7 +33,7 @@ export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
{t('controlLayers.addIPAdapter')}
{t('controlLayers.addReferenceImage')}
</MenuItem>
</>
);

View File

@@ -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]

View File

@@ -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) {

View File

@@ -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')}

View File

@@ -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}

View File

@@ -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]);

View File

@@ -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">

View File

@@ -44,7 +44,7 @@ export const CanvasEntityHeader = memo(({ children, ...rest }: FlexProps) => {
);
}
if (entityIdentifier.type === 'ip_adapter') {
if (entityIdentifier.type === 'reference_image') {
return (
<MenuList>
<IPAdapterMenuItems />

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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}

View File

@@ -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 = (

View File

@@ -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;
};

View File

@@ -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),

View File

@@ -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;
};

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 '';
}

View File

@@ -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 '';
}

View File

@@ -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');

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
) {

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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.
*/

View File

@@ -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.
*/

View File

@@ -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;
}
);

View File

@@ -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);

View File

@@ -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.

View File

@@ -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);
}

View 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;
};

View File

@@ -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;

View File

@@ -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}
>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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