Showing preview only (7,211K chars total). Download the full file or copy to clipboard to get everything.
Repository: robertvoy/ComfyUI-Flux-Continuum
Branch: main
Commit: b9d6a614198b
Files: 25
Total size: 6.9 MB
Directory structure:
gitextract_si5rgbko/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── publish_action.yml
├── LICENSE
├── README.md
├── __init__.py
├── misc.py
├── pyproject.toml
├── web/
│ ├── getsetorder.js
│ ├── guidanceversions.js
│ ├── help.js
│ ├── hint.js
│ ├── imagedisplay.js
│ ├── imagetransfershortcut.js
│ ├── impactpackfix.js
│ ├── outputgetnode.js
│ ├── samplerversions.js
│ ├── tabs.js
│ └── textversions.js
└── workflow/
├── Flux+ 1.3_release.json
├── Flux+ 1.4.4_release.json
├── Flux+ 1.4.5_release.json
├── Flux+ 1.6.4_release.json
├── Flux+ 1.7.0_release.json
├── Flux+ 1.7.1_beta.json
└── Flux+ Light 1.0.0_release.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: robertvoy
buy_me_a_coffee: robertvoy
================================================
FILE: .github/workflows/publish_action.yml
================================================
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Robert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# ComfyUI Flux Continuum - Modular Interface

> A modular workflow that brings order to the chaos of image generation pipelines.
📺 [Watch the Tutorial](https://www.youtube.com/watch?v=cjWuPcRZ1j0)
## Updates
- **1.7.0:** Enhanced workflow and usability update 📺 [Watch Video Update](https://www.youtube.com/watch?v=e_7cYbBwjFc)
- **Image Transfer Shortcut**: Use `Ctrl+Shift+C` to copy images from Img Preview to Img Load (customizable in Settings > Keybinding > Image Transfer)
- **Configurable Model Router**: Dynamic model selection with customizable JSON mapping for flexible workflows
- **Hint System**: Interactive hint nodes provide contextual help throughout the workflow
- **Crop & Stitch**: Enhanced inpainting/outpainting with automatic crop and stitch functionality
- **Smart Guidance**: Automatic guidance value of 30 for inpainting, outpainting, canny, and depth operations
- **TeaCache Integration**: Optional speed boost for all outputs (trades some quality for performance)
- **Improved Preprocessor Preview Logic**: CN Input is used for previewing when ControlNet strength > 0, otherwise uses Img Load
- **Workflow Reorganization**: Modules reordered for more logical flow
- **Redux Naming**: IP Adapter renamed to Redux for consistency with BFL terminology
<details>
<summary><b>📋 Older Changelog</b> (Click to expand)</summary>
- **1.6.4:** ControlNet Union Pro v2 Update 📺 [Watch Video Update](https://www.youtube.com/watch?v=oh1P_4d9_HI)
- ControlNet Union Pro v2: Integrated the new Depth, Canny, OpenPose ControlNets
- New canny preprocessor control
- Removed the input preview tab
- Better upscaling controls
- New Redux (IPAdapter) implementation
- **Flux Continuum Light 1.0.0:**
- Light version of the workflow with all the basic functions that requires only the FLUX.1-dev model. [Download](https://github.com/robertvoy/ComfyUI-Flux-Continuum/blob/main/workflow/Flux%2B%20Light%201.0.0_release.json)
- **1.4.2:** Black Forest Labs tools update
- Black Forest Labs tools: Integrated the new Redux, Depth, Canny, Fill models
- Preview Panel: Preview all your image inputs and masks at a glance
- Mask Feather Control: Feather the mask using one control
- Text Versions: Add more tabs via properties
- New Nodes: *FluxContinuumModelRouter*, *OutputGet*, *OutputGetString*, *OutputTextDisplay*, *DrawTextConfig* and *ConfigurableDrawText*
</details>
## Overview
ComfyUI Flux Continuum revolutionises workflow management through a thoughtful dual-interface design:
- **Front-end**: A consistent control interface shared across all modules
- **Back-end**: Powerful, modular architecture for customisation and experimentation
## ✨ Core Features
> Perfect for creators who want a consistent, streamlined experience across all image generation tasks, while maintaining the power to customize when needed.
- **Unified Control Interface**
- Single set of controls affects all relevant modules
- Smart guidance adjustment based on operation type
- Consistent experience across all generation tasks
- **Smart Workflow Management**
- Only activates nodes and models required for current task
- Toggle between different output types seamlessly
- Efficiently handles resource allocation
- Optional TeaCache for speed optimization
- **Universal Model Integration**
- LoRAs, ControlNets and Redux work across all output modules
- Seamless Black Forest Labs model support
- Configurable model routing for custom workflows
- **Enhanced Usability**
- Interactive hint system for contextual help
- Quick image transfer with keyboard shortcut
- Intelligent preprocessing based on control values
- Crop & stitch for seamless inpainting/outpainting
---
## 🚀 Quick Start
📺 **New to Flux Continuum?** [Watch the tutorial first](https://www.youtube.com/watch?v=cjWuPcRZ1j0)
1. Clone repo to the custom nodes folder
```shell
git clone https://github.com/robertvoy/ComfyUI-Flux-Continuum
```
2. [Download](https://github.com/robertvoy/ComfyUI-Flux-Continuum/blob/main/workflow/Flux%2B%201.7.0_release.json) and import the workflow into ComfyUI
3. Install missing custom nodes using the ComfyUI Manager
4. Configure your models in the config panel (press `2` to access)
5. Download any missing models (see Model Downloads section below)
6. Return to the main interface (press `1`)
7. Select `txt2img` from the output selector (top left corner)
8. Run the workflow to generate your first image
---
## 🎯 Usage Guide
### Output Selection
The workflow is controlled by the **Output selector** in the top-left corner. Select your desired output and all relevant controls will automatically apply.
### Key Controls
**🎨 Main Generation**
- **Prompt**: Your text description for generation
- **Denoise**: Controls strength for img2img operations (0 = no change, 1 = completely new)
- **Steps**: Number of sampling steps (higher = more detail, slower)
- **Guidance**: How closely to follow the prompt (automatically set to 30 for inpainting/outpainting/canny/depth)
- **TeaCache**: Toggle for speed boost (some quality trade-off)
**🖼️ Input Images**
- **Img Load**: Primary image for all img2img operations (inpainting, outpainting, detailer, upscaling)
- **CN Input**: Source for ControlNet preprocessing
- **Redux 1-3**: Up to 3 reference images for style transfer (use very low strength values)
- **Tip**: Use `Ctrl+Shift+C` to quickly copy from Img Preview to Img Load
**🎛️ ControlNet & Redux**
- ControlNets activate when strength > 0
- When CN strength > 0, preprocessor uses CN Input; otherwise uses Img Load
- Preview preprocessor results by selecting corresponding output (e.g., "preprocessor canny")
- Redux sliders control each Redux input individually (1 = Redux 1, etc.)
**Recommended ControlNet Values:**
- **Canny**: Strength=0.7, End=0.8
- **Depth**: Strength=0.8, End=0.8
- **Pose**: Strength=0.9, End=0.65
**🔧 Image Processing**
- Resize, crop, sharpen, color correct, or pad images
- Preview results with "imgload prep" output
- Bypass nodes after processing to avoid reprocessing (`Ctrl+B`)
**⬆️ Upscaling**
- **Resolution Multiply**: Multiplies image resolution after any preprocessing
- **Upscale Model**: Choose your upscaling model (recommended: 4xNomos8kDAT)
- 📺 [Watch Upscaling Tutorial](https://www.youtube.com/watch?v=TmF3JK_1AAs)
---
## 🎯 Workflow Modules
### Main Modules
> All modules use the same unified control interface
| Module | Description |
|--------|-------------|
| **txt2img** | Standard text-to-image generation from prompts |
| **txt2img noise injection** | Enhanced detail generation ([Learn more](https://youtu.be/tned5bYOC08?si=qfP2Sv2VOTzDK-uL&t=1335)) |
| **img2img** | Transform existing images with text prompts |
| **inpainting** | Edit specific areas with automatic crop & stitch using BFL Fill model |
| **outpainting** | Expand images beyond boundaries with smart padding and BFL Fill model |
| **canny** | Edge-guided generation using BFL Canny model |
| **depth** | Depth-guided generation using BFL Depth model |
| **detailer** | Focused refinement using mask selection |
| **ultimate upscaler** | Advanced tiled upscaling with full control |
| **upscaler** | Simple model-based upscaling |
### Utility Modules
| Module | Description |
|--------|-------------|
| **imgload prep** | Preview processed images after crop/sharpen/resize/padding |
| **preprocessor canny** | Preview canny edge detection results |
| **preprocessor depth** | Preview depth map generation |
| **preprocessor openpose** | Preview pose detection results |
---
## 🔧 Custom Nodes
> *These custom nodes were made specifically for this workflow and are required for it to work*
### Interface Enhancement Nodes
- **Hint Node**:
- Interactive help system throughout the workflow
- Hover for contextual information
- Right-click to edit hint content
- Supports markdown formatting
- **Tabs**:
- Space-saving node organization
- Add tabs via properties panel
- Compatible with most nodes
- Special handling for unsupported nodes
- **Sliders**:
- Suite of pre-configured sliders
- Optimized ranges and defaults for common operations
- Includes: Denoise, Step, Guidance, Batch, GPU, ControlNet, Redux, and more
- **OutputGet System**:
- Filters set nodes with prefix `Output -`
- **OutputTextDisplay**: Visual display of selected output
- **OutputGetString**: String output for conditional routing
- **Text Versions**:
- Multi-tab text management (default 5 tabs)
- Add more tabs via properties panel
- Save different prompt versions
- Perfect for A/B testing
- **ImageDisplay**:
- Base64 image display on canvas
- Configurable via properties
- Useful for reference images
### Workflow Control Nodes
- **Image Transfer Shortcut**:
- `Ctrl+Shift+C` copies from Img Preview to Img Load
- Customizable in ComfyUI keybindings
- **Configurable Model Router**:
- Dynamic model selection with JSON mapping
- Flexible routing based on conditions
- Supports lazy loading for efficiency
- **Sampler Parameter Packer/Unpacker**:
- Consolidate sampler settings
- Tabbed interface for version control
- **Image Batch Boolean**:
- Conditional batch processing
- Smart second image loading
- **Configurable Draw Text**:
- Advanced text rendering on images
- Configurable fonts, colors, shadows, alignment
- **Pass Nodes**:
- Extended pass-through for Latent, Pipe, SEGS, and Int data types
- Maintains data flow integrity
---
## 📥 Model Downloads
### Required Models
**unet folder:**
- [flux1-dev.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors)
- [flux1-depth-dev.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev/resolve/main/flux1-depth-dev.safetensors)
- [flux1-canny-dev.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev/resolve/main/flux1-canny-dev.safetensors)
- [flux1-fill-dev.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev/resolve/main/flux1-fill-dev.safetensors)
> **Note**: If you don't use Canny or Depth models, you can bypass their load nodes and skip downloading them.
**vae folder:**
- [ae.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/ae.safetensors)
**clip folder:**
- [t5xxl_fp8_e4m3fn.safetensors](https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors)
- [clip_l.safetensors](https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors)
**style_models folder:**
- [flux1-redux-dev.safetensors](https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors)
**clip_vision folder:**
- [sigclip_vision_patch14_384.safetensors](https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors)
**controlnet/FLUX folder:**
- [FLUX.1-dev-ControlNet-Union-Pro-2.0.safetensors](https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro-2.0/resolve/main/diffusion_pytorch_model.safetensors) *(rename file)*
---
## 🔜 Coming Soon
- **Multi-GPU Support**: Distributed processing across multiple GPUs
---
## 🙏 Acknowledgments
Special thanks to the creators of these essential custom node packs:
- [rgthree ComfyUI Extensions](https://github.com/rgthree/rgthree-comfy)
- [ComfyUI Essentials](https://github.com/cubiq/ComfyUI_essentials)
- [ComfyUI Impact Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack)
- [ComfyUI KJNodes](https://github.com/kijai/ComfyUI-KJNodes)
================================================
FILE: __init__.py
================================================
from .misc import MISC_CLASS_MAPPINGS
# Merge both mappings into a single dictionary for the custom nodes
NODE_CLASS_MAPPINGS = {**MISC_CLASS_MAPPINGS}
WEB_DIRECTORY = "./web"
# Optional: You can also define NODE_DISPLAY_NAME_MAPPINGS if you want custom display names in the UI
NODE_DISPLAY_NAME_MAPPINGS = {
"StepSlider": "Step Slider",
"DenoiseSlider": "Denoise Slider",
"GuidanceSlider": "Guidance Slider",
"GPUSlider": "GPU Slider",
"BatchSlider": "Batch Slider",
"IPAdapterSlider": "IPAdapter Slider",
"MaxShiftSlider": "Max Shift Slider",
"ControlNetSlider": "ControlNet Slider",
"CannySlider": "CannySlider",
"SelectFromBatch": "Select From Batch",
"LatentPass": "LatentPass",
"SEGSPass": "SEGSPass",
"PipePass": "PipePass",
"IntPass": "IntPass",
"TextVersions": "Text Versions",
"ResolutionPicker": "Resolution Picker",
"ResolutionMultiplySlider": "ResolutionMultiplySlider",
"SamplerParameterPacker": "Sampler Parameter Packer",
"SamplerParameterUnpacker": "Sampler Parameter Unpacker",
"ImpactControlBridgeFix": "ImpactControlBridgeFix",
"BooleanToEnabled": "Boolean To Enabled",
"OutputGetString": "OutputGetString",
"SplitVec2": "SplitVec2",
"SplitVec3": "SplitVec3",
"SimpleTextTruncate": "Simple Text Truncate",
"FluxContinuumModelRouter": "Flux Continuum Model Router",
"ConfigurableModelRouter": "Configurable Model Router",
"ImageBatchBoolean": "Image Batch Boolean",
"DrawTextConfig": "DrawTextConfig",
"ConfigurableDrawText": "ConfigurableDrawText"
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
================================================
FILE: misc.py
================================================
import nodes
from server import PromptServer
import torch
import comfy.samplers
import os
import time
from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter
import torchvision.transforms.v2 as T
import numpy as np
import folder_paths
import numpy as np
import json
from typing import Any, Mapping, Tuple
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
any_typ = AnyType("*")
class DenoiseSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.001 }),
},
}
RETURN_TYPES = ("FLOAT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = """Control the **denoising strength** for img2img operations, including: inpainting, ultimate upscaler and detailer.
- A value of **1.0** means a completely new image.
- A value of **0.0** means no change to the latent image."""
def execute(self, value):
return (value, )
class StepSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 25.0, "min": 0.0, "max": 50.0, "step": 1.0 }),
},
}
RETURN_TYPES = ("INT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Set the number of **sampling steps**. Higher values can increase detail but take longer to process."
def execute(self, value):
# Use round() instead of int() to ensure proper integer conversion
return (int(round(value)), )
class BatchSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 1.0, "min": 1.0, "max": 10.0, "step": 1.0 }),
},
}
RETURN_TYPES = ("INT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Provides a slider for controlling batch size with range 1-10"
def execute(self, value):
# Use round() instead of int() to ensure proper integer conversion
return (int(round(value)), )
class ResolutionMultiplySlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 1.0, "min": 1.0, "max": 10.0, "step": 0.1 }),
},
}
RETURN_TYPES = ("FLOAT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Provides a slider for controlling resolution multiplication for upscaling, with range 1-10"
def execute(self, value):
return (value, )
class GPUSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 1.0, "min": 1.0, "max": 4.0, "step": 1.0 }),
},
}
RETURN_TYPES = ("INT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Provides a slider for selecting number of GPUs with range 1-4"
def execute(self, value):
# Use round() instead of int() to ensure proper integer conversion
return (int(round(value)), )
class SelectFromBatch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 0.0, "min": 0.0, "max": 24.0, "step": 1.0 }),
},
}
RETURN_TYPES = ("INT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Provides a slider for selecting specific images from a batch with range 0-24"
def execute(self, value):
# Use round() instead of int() to ensure proper integer conversion
return (int(round(value)), )
class GuidanceSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 2.5, "min": -1.0, "max": 30.0, "step": 0.1 }),
},
}
RETURN_TYPES = ("FLOAT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Higher values make the output adhere more strictly to the prompt. Select between different presets for convenience. NOTE: FLUX Continuum workflow automatically sets your guidance to 30 when you're doing inpainting, outpainting, canny, or depth operations."
def execute(self, value):
# Return the float value directly
return (value, )
class MaxShiftSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("FLOAT", { "display": "slider", "default": 1.15, "min": 0.0, "max": 4.0, "step": 0.05 }),
},
}
RETURN_TYPES = ("FLOAT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Control the **maximum pixel shift**, often used to introduce variation."
def execute(self, value):
# Return the float value directly
return (value, )
class ControlNetSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"Strength": ("FLOAT", { "display": "slider", "default": 1, "min": 0.0, "max": 1.0, "step": 0.05 }),
"Start": ("FLOAT", { "display": "slider", "default": 0, "min": 0.0, "max": 1.0, "step": 0.05 }),
"End": ("FLOAT", { "display": "slider", "default": 1, "min": 0.0, "max": 1.0, "step": 0.05 }),
},
}
RETURN_TYPES = ("VEC3", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = """- **Strength**: The overall influence of the ControlNet.
- **Start**: The step at which the ControlNet begins to apply (as a percentage).
- **End**: The step at which the ControlNet stops applying (as a percentage)."""
def execute(self, Strength, Start, End):
# Return the three values as a VEC3
return ((Strength, Start, End), )
class CannySlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"Low_Threshold": ("FLOAT", { "display": "slider", "default": 0.40, "min": 0.1, "max": 0.99, "step": 0.01 }),
"High_Threshold": ("FLOAT", { "display": "slider", "default": 0.80, "min": 0.1, "max": 0.99, "step": 0.01 })
},
}
RETURN_TYPES = ("VEC2", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Provides two sliders for canny preprocessor parameters"
def execute(self, Low_Threshold, High_Threshold):
# Return the two values as a VEC2
return ((Low_Threshold, High_Threshold), )
class IPAdapterSlider:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"IP1": ("FLOAT", { "display": "slider", "default": 0, "min": 0.0, "max": 1.0, "step": 0.05 }),
"IP2": ("FLOAT", { "display": "slider", "default": 0, "min": 0.0, "max": 1.0, "step": 0.05 }),
"IP3": ("FLOAT", { "display": "slider", "default": 0, "min": 0.0, "max": 1.0, "step": 0.05 }),
},
}
RETURN_TYPES = ("VEC3",)
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Sliders"
DESCRIPTION = "Control the strength of up to three different Redux inputs simultaneously."
def execute(self, IP1, IP2, IP3):
# Return the three values as a VEC3
return ((IP1, IP2, IP3),)
class SEGSPass:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"SEGS": ("SEGS",),
},
}
RETURN_TYPES = ("SEGS", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Utilities"
def execute(self, SEGS):
# Return the integer value directly
return (SEGS, )
class PipePass:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PIPE_LINE": ("PIPE_LINE",),
},
}
RETURN_TYPES = ("PIPE_LINE", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Utilities"
def execute(self, PIPE_LINE):
return (PIPE_LINE, )
class LatentPass:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"latent": ("LATENT",),
},
}
RETURN_TYPES = ("LATENT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Utilities"
def execute(self, latent):
# Simply pass through the latent data
return (latent, )
class IntPass:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"INT": ("INT",),
},
}
RETURN_TYPES = ("INT", )
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Utilities"
def execute(self, INT):
# Simply pass through an integer
return (INT, )
class ResolutionPicker:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"resolution": (["704x1408 (0.5)","704x1344 (0.52)","768x1344 (0.57)","768x1280 (0.6)","832x1216 (0.68)","832x1152 (0.72)","896x1152 (0.78)","896x1088 (0.82)","960x1088 (0.88)","960x1024 (0.94)","1024x1024 (1.0)","1024x960 (1.07)","1088x960 (1.13)","1088x896 (1.21)","1152x896 (1.29)","1152x832 (1.38)","1216x832 (1.46)","1280x768 (1.67)","1344x768 (1.75)","1344x704 (1.91)","1408x704 (2.0)","1472x704 (2.09)","1536x640 (2.4)","1600x640 (2.5)","1664x576 (2.89)","1728x576 (3.0)",], {"default": "1024x1024 (1.0)"}),
}}
RETURN_TYPES = (["704x1408 (0.5)","704x1344 (0.52)","768x1344 (0.57)","768x1280 (0.6)","832x1216 (0.68)","832x1152 (0.72)","896x1152 (0.78)","896x1088 (0.82)","960x1088 (0.88)","960x1024 (0.94)","1024x1024 (1.0)","1024x960 (1.07)","1088x960 (1.13)","1088x896 (1.21)","1152x896 (1.29)","1152x832 (1.38)","1216x832 (1.46)","1280x768 (1.67)","1344x768 (1.75)","1344x704 (1.91)","1408x704 (2.0)","1472x704 (2.09)","1536x640 (2.4)","1600x640 (2.5)","1664x576 (2.89)","1728x576 (3.0)",],)
RETURN_NAMES = ("resolution",)
FUNCTION = "execute"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = "Provides a convenient dropdown menu to select from a list of common, pre-calculated image **resolutions** and their aspect ratios. Perfect for FLUX."
def execute(self, resolution):
return (resolution,)
class SamplerParameterPacker:
CATEGORY = 'Flux-Continuum/Utilities'
RETURN_TYPES = ("SAMPLER_PARAMS",)
RETURN_NAMES = ("sampler_params",)
FUNCTION = "pack_parameters"
DESCRIPTION = "Packs sampler and scheduler selections into a single parameter object for efficient passing"
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"sampler": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
}}
def pack_parameters(self, sampler, scheduler):
return ((sampler, str(sampler), scheduler, str(scheduler)),)
class SamplerParameterUnpacker:
CATEGORY = 'Flux-Continuum/Utilities'
RETURN_TYPES = (comfy.samplers.KSampler.SAMPLERS, "STRING", any_typ, "STRING",)
RETURN_NAMES = ("sampler", "sampler_name", "scheduler", "scheduler_name",)
FUNCTION = "unpack_parameters"
DESCRIPTION = "Unpacks previously packed sampler parameters back into individual components"
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"sampler_params": ("SAMPLER_PARAMS",),
}}
def unpack_parameters(self, sampler_params):
sampler, sampler_name, scheduler, scheduler_name = sampler_params
return (sampler, sampler_name, scheduler, scheduler_name,)
class TextVersions:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": True}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "process_text"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = "Provides a multi-tab interface for managing different versions of text input"
def __init__(self):
self.order = 0
def process_text(self, text):
return (text,)
def workflow_to_map(workflow):
nodes_map = {}
links = {}
# Create a lookup table for links and nodes
for links_data in workflow['links']:
links[links_data[0]] = links_data[1:]
for node_data in workflow['nodes']:
nodes_map[str(node_data['id'])] = node_data
return nodes_map, links
def is_execution_model_version_supported():
try:
import comfy_execution
return True
except:
return False
class ImpactControlBridgeFix:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"value": (any_typ,),
"mode": ("BOOLEAN", {"default": True, "label_on": "Active", "label_off": "Stop/Mute/Bypass"}),
"behavior": (["Stop", "Mute", "Bypass"], ),
},
"hidden": {"unique_id": "UNIQUE_ID", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}
}
FUNCTION = "doit"
CATEGORY = "Flux-Continuum/Utilities"
RETURN_TYPES = (any_typ,)
RETURN_NAMES = ("value",)
OUTPUT_NODE = True
DESCRIPTION = ("When behavior is Stop and mode is active, the input value is passed directly to the output.\n"
"When behavior is Mute/Bypass and mode is active, the node connected to the output is changed to active state.\n"
"When behavior is Stop and mode is Stop/Mute/Bypass, the workflow execution of the current node is halted.\n"
"When behavior is Mute/Bypass and mode is Stop/Mute/Bypass, the node connected to the output is changed to Mute/Bypass state.")
@classmethod
def IS_CHANGED(self, value, mode, behavior="Stop", unique_id=None, prompt=None, extra_pnginfo=None):
if behavior == "Stop":
return value, mode, behavior
try:
if prompt and 'extra_data' in prompt and 'extra_pnginfo' in prompt['extra_data']:
workflow = prompt['extra_data']['extra_pnginfo'].get('workflow')
if workflow:
nodes_map, links = workflow_to_map(workflow)
next_nodes = []
for link in nodes_map[unique_id]['outputs'][0]['links']:
node_id = str(links[link][2])
if node_id in nodes_map:
next_nodes.append(node_id)
return next_nodes
except:
pass
return 0
def doit(self, value, mode, behavior="Stop", unique_id=None, prompt=None, extra_pnginfo=None):
# Check for execution model support
if is_execution_model_version_supported():
from comfy_execution.graph import ExecutionBlocker
else:
print("[Impact Pack] ImpactControlBridge: ComfyUI is outdated. The 'Stop' behavior cannot function properly.")
# Handle Stop behavior
if behavior == "Stop":
if mode:
return (value, )
else:
return (ExecutionBlocker(None), )
# Handle other behaviors
try:
# Validate extra_pnginfo
if not extra_pnginfo or not isinstance(extra_pnginfo, dict) or 'workflow' not in extra_pnginfo:
return (value, )
workflow_nodes, links = workflow_to_map(extra_pnginfo['workflow'])
# Initialize node lists
active_nodes = []
mute_nodes = []
bypass_nodes = []
node_outputs = workflow_nodes.get(unique_id, {}).get('outputs', [])
if not node_outputs:
return (value, )
output_links = node_outputs[0].get('links', [])
for link in output_links:
try:
node_id = str(links[link][2])
next_nodes = []
if node_id in workflow_nodes:
next_nodes.append(node_id)
for next_node_id in next_nodes:
node_mode = workflow_nodes[next_node_id].get('mode', 0)
if node_mode == 0:
active_nodes.append(next_node_id)
elif node_mode == 2:
mute_nodes.append(next_node_id)
elif node_mode == 4:
bypass_nodes.append(next_node_id)
except:
continue
# Handle mode-specific behavior
if mode:
# active
should_be_active_nodes = mute_nodes + bypass_nodes
if should_be_active_nodes:
PromptServer.instance.send_sync("impact-bridge-continue",
{"node_id": unique_id,
'actives': list(should_be_active_nodes)})
nodes.interrupt_processing()
elif behavior == "Mute" or behavior == True:
# mute
should_be_mute_nodes = active_nodes + bypass_nodes
if should_be_mute_nodes:
PromptServer.instance.send_sync("impact-bridge-continue",
{"node_id": unique_id,
'mutes': list(should_be_mute_nodes)})
nodes.interrupt_processing()
else:
# bypass
should_be_bypass_nodes = active_nodes + mute_nodes
if should_be_bypass_nodes:
PromptServer.instance.send_sync("impact-bridge-continue",
{"node_id": unique_id,
'bypasses': list(should_be_bypass_nodes)})
nodes.interrupt_processing()
except Exception as e:
print(f"[Impact Pack] Error in ImpactControlBridge: {str(e)}")
return (value, )
class BooleanToEnabled:
"""Convert boolean value to enabled string format"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"BOOLEAN": ("BOOLEAN",),
},
}
RETURN_TYPES = (["true", "false", "remote"],) # Match the exact format from RemoteQueueWorker
RETURN_NAMES = ("enabled",)
FUNCTION = "convert"
CATEGORY = "Flux-Continuum/Utilities"
TITLE = "Boolean to Enabled"
DESCRIPTION = "Converts boolean values to 'true'/'false'/'remote' strings for ComfyUI_NetDist"
def convert(self, BOOLEAN):
# Convert boolean to appropriate string value
return ("true" if BOOLEAN else "false",)
class OutputGetString:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
},
"hidden": {
"unique_id": "UNIQUE_ID",
"prompt": "PROMPT",
"title": ("STRING", {"default": ""})
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("STRING",)
FUNCTION = "process"
CATEGORY = "Flux-Continuum/Utilities"
OUTPUT_NODE = True
def process(self, title, unique_id, prompt):
title = title[len("Output - "):]
return (title,)
# Type definition for Vec3
Vec3 = Tuple[float, float, float]
Vec2 = Tuple[float, float]
# Zero vector constant
VEC3_ZERO = (0.0, 0.0, 0.0)
VEC2_ZERO = (0.0, 0.0)
class SplitVec3:
@classmethod
def INPUT_TYPES(cls) -> Mapping[str, Any]:
return {"required": {"a": ("VEC3", {"default": VEC3_ZERO})}}
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT")
FUNCTION = "op"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = "Splits a vector3 input into its three individual float components"
def op(self, a: Vec3) -> tuple[float, float, float]:
return (a[0], a[1], a[2])
class SplitVec2:
@classmethod
def INPUT_TYPES(cls) -> Mapping[str, Any]:
return {"required": {"a": ("VEC2", {"default": (0.0, 0.0)})}}
RETURN_TYPES = ("FLOAT", "FLOAT")
FUNCTION = "op"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = "Splits a vector2 input into its two individual float components"
def op(self, a) -> tuple[float, float]:
return (a[0], a[1])
class SimpleTextTruncate:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
"word_count": ("INT", {"default": 10, "min": 0, "max": 99999999, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("TEXT",)
FUNCTION = "truncate_words"
CATEGORY = "Text Operations"
DESCRIPTION = "Truncates input text to a specified number of words"
def truncate_words(self, text, word_count):
if text is None:
return ("",) # Return as a tuple
words = str(text).split()
result = ' '.join(words[:word_count])
# Return as a tuple since RETURN_TYPES is defined as a tuple
return (result,)
class FluxContinuumModelRouter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"condition": ("STRING", {"default": ""})
},
"optional": {
"flux_fill": ("MODEL", {"lazy": True}), # Lazy load for inpainting/outpainting
"flux_depth": ("MODEL", {"lazy": True}), # Lazy load for depth
"flux_canny": ("MODEL", {"lazy": True}), # Lazy load for canny
"flux_dev": ("MODEL", {"lazy": True}), # Lazy load for default case
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "route_model"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = "For Flux Continuum workflow only. Routes model selection based on conditional input for different tasks (fill, depth, canny, dev)"
def check_lazy_status(self, condition, flux_fill=None, flux_depth=None, flux_canny=None, flux_dev=None):
condition = condition.lower().strip()
needed = []
# Only request the model we actually need based on the condition
if condition in ["inpainting", "outpainting"]:
if flux_fill is None:
needed.append("flux_fill")
elif condition == "depth":
if flux_depth is None:
needed.append("flux_depth")
elif condition == "canny":
if flux_canny is None:
needed.append("flux_canny")
else:
if flux_dev is None:
needed.append("flux_dev")
return needed
def route_model(self, condition, flux_fill=None, flux_depth=None, flux_canny=None, flux_dev=None):
condition = condition.lower().strip()
if condition in ["inpainting", "outpainting"]:
print(f"ModelRouter: Condition '{condition}' matched - Selected flux_fill model")
return (flux_fill,)
elif condition == "depth":
print(f"ModelRouter: Condition '{condition}' matched - Selected flux_depth model")
return (flux_depth,)
elif condition == "canny":
print(f"ModelRouter: Condition '{condition}' matched - Selected flux_canny model")
return (flux_canny,)
else:
return (flux_dev,)
class ConfigurableModelRouter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# This will be a text box widget on the node for manual input
"condition": ("STRING", {"multiline": False, "default": "default"}),
# The JSON config is also a widget on the node
"routing_config": ("STRING", {
"multiline": True,
"default": '{\n "default": 1,\n "inpainting": 2,\n "depth": 3,\n "canny": 4\n}'
}),
},
"optional": {
"model_1": ("MODEL", {"lazy": True}),
"model_2": ("MODEL", {"lazy": True}),
"model_3": ("MODEL", {"lazy": True}),
"model_4": ("MODEL", {"lazy": True}),
"model_5": ("MODEL", {"lazy": True}),
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "route_model"
CATEGORY = "Flux-Continuum/Utilities"
DESCRIPTION = """
A dynamic model router that selects one of its inputs based on a configurable JSON mapping.
How to Use:
1. **Configure Logic:** Edit the `routing_config` JSON to map condition strings (e.g., `"inpainting"`) to an input index (e.g., `2`).
2. The `"default"` key is used if no other condition matches.
"""
# It's an instance method, so it can correctly read the widget values.
def check_lazy_status(self, condition, routing_config, **kwargs):
needed = []
try:
config = json.loads(routing_config)
# Use the values from the widgets to find the target index
target_index = config.get(condition.strip().lower(), config.get("default", 1))
# Construct the name of the model input we need to load
model_key = f"model_{target_index}"
# If the required model hasn't been loaded yet, request it by name
if kwargs.get(model_key) is None:
needed.append(model_key)
except:
# If the JSON is invalid, do nothing.
pass
print(f"[Model Router Check] Condition: '{condition}', Needing to load: {needed}")
return needed
def route_model(self, condition, routing_config, **kwargs):
# This logic runs after the needed model has been loaded.
config = json.loads(routing_config)
target_index = config.get(condition.strip().lower(), config.get("default", 1))
model_key = f"model_{target_index}"
# Check that the model exists and is connected
if model_key not in kwargs or kwargs.get(model_key) is None:
raise ValueError(f"Input '{model_key}' is required for condition '{condition}' but is not connected or loaded.")
print(f"Model Router: Successfully routed to '{model_key}'")
return (kwargs[model_key],)
class ImageBatchBoolean:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE", {"lazy": True}), # Make image2 lazy
"batch_enabled": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "batch"
CATEGORY = "Flux-Continuum/Utilities"
def check_lazy_status(self, image1, image2, batch_enabled):
needed = []
# Only need image2 if batching is enabled
if image2 is None and batch_enabled:
needed.append("image2")
return needed
def batch(self, image1, image2, batch_enabled):
# If batching is disabled, just return the first image
if not batch_enabled:
return (image1,)
# If batching is enabled, perform the normal batch operation
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(
image2.movedim(-1,1),
image1.shape[2],
image1.shape[1],
"bilinear",
"center"
).movedim(1,-1)
s = torch.cat((image1, image2), dim=0)
return (s,)
# based on ComfyUI Essentials: github.com/cubiq/ComfyUI_essentials
MAX_RESOLUTION = 2048
FONTS_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts")
def hex_to_rgba(hex_color):
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
return (r, g, b, 255)
elif len(hex_color) == 8:
r, g, b, a = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4, 6))
return (r, g, b, a)
else:
raise ValueError("Invalid hex color format")
class DrawTextConfig:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"font": (sorted([f for f in os.listdir(FONTS_DIR) if f.endswith('.ttf') or f.endswith('.otf')]), ),
"size": ("INT", { "default": 56, "min": 1, "max": 9999, "step": 1 }),
"color": ("STRING", { "multiline": False, "default": "#FFFFFF" }),
"background_color": ("STRING", { "multiline": False, "default": "#00000000" }),
"padding": ("INT", { "default": 20, "min": 0, "max": 500, "step": 1 }),
"shadow_distance": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }),
"shadow_blur": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }),
"shadow_color": ("STRING", { "multiline": False, "default": "#000000" }),
"horizontal_align": (["left", "center", "right"],),
"vertical_align": (["top", "center", "bottom"],),
"offset_x": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
"offset_y": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
"direction": (["ltr", "rtl"],),
}}
RETURN_TYPES = ("TEXT_STYLE",)
FUNCTION = "configure"
CATEGORY = "text"
DESCRIPTION = "Configures text rendering parameters including font, size, color, alignment, and effects"
def configure(self, font, size, color, background_color, padding, shadow_distance, shadow_blur,
shadow_color, horizontal_align, vertical_align, offset_x, offset_y, direction):
return ({
"font": font,
"size": size,
"color": color,
"background_color": background_color,
"padding": padding,
"shadow_distance": shadow_distance,
"shadow_blur": shadow_blur,
"shadow_color": shadow_color,
"horizontal_align": horizontal_align,
"vertical_align": vertical_align,
"offset_x": offset_x,
"offset_y": offset_y,
"direction": direction
},)
class ConfigurableDrawText:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"TEXT": ("STRING", {"multiline": True}),
"TEXT_STYLE": ("TEXT_STYLE",),
"IMAGE": ("IMAGE",),
}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "draw"
CATEGORY = "text"
DESCRIPTION = "Renders text onto images using previously configured text style parameters"
def draw(self, TEXT, TEXT_STYLE, IMAGE):
font = ImageFont.truetype(os.path.join(FONTS_DIR, TEXT_STYLE["font"]), TEXT_STYLE["size"])
lines = TEXT.split("\n")
if TEXT_STYLE["direction"] == "rtl":
lines = [line[::-1] for line in lines]
ascent, descent = font.getmetrics()
line_spacing = ascent + descent
text_width = max(font.getbbox(line)[2] - font.getbbox(line)[0] for line in lines)
text_height = line_spacing * (len(lines) - 1) + ascent + descent
IMAGE = T.ToPILImage()(IMAGE.permute([0,3,1,2])[0]).convert('RGBA')
width = IMAGE.width
height = IMAGE.height
image = Image.new('RGBA', (width, height), (0,0,0,0))
box_width = text_width + (TEXT_STYLE["padding"] * 2)
box_height = text_height + (TEXT_STYLE["padding"] * 2)
if TEXT_STYLE["horizontal_align"] == "left":
box_x = TEXT_STYLE["offset_x"]
elif TEXT_STYLE["horizontal_align"] == "center":
box_x = (width - box_width) // 2 + TEXT_STYLE["offset_x"]
else: # right
box_x = width - box_width + TEXT_STYLE["offset_x"]
if TEXT_STYLE["vertical_align"] == "top":
box_y = TEXT_STYLE["offset_y"]
elif TEXT_STYLE["vertical_align"] == "center":
box_y = (height - box_height) // 2 + TEXT_STYLE["offset_y"]
else: # bottom
box_y = height - box_height + TEXT_STYLE["offset_y"]
x = box_x + TEXT_STYLE["padding"]
y = box_y + TEXT_STYLE["padding"]
draw = ImageDraw.Draw(image)
draw.rectangle([box_x, box_y, box_x + box_width, box_y + box_height],
fill=hex_to_rgba(TEXT_STYLE["background_color"]))
image_shadow = None
if TEXT_STYLE["shadow_distance"] > 0:
image_shadow = image.copy()
for i, line in enumerate(lines):
current_y = y + (i * line_spacing)
draw = ImageDraw.Draw(image)
draw.text((x, current_y), line, font=font, fill=hex_to_rgba(TEXT_STYLE["color"]))
if image_shadow is not None:
draw = ImageDraw.Draw(image_shadow)
draw.text((x + TEXT_STYLE["shadow_distance"], current_y + TEXT_STYLE["shadow_distance"]),
line, font=font, fill=hex_to_rgba(TEXT_STYLE["shadow_color"]))
if image_shadow is not None:
image_shadow = image_shadow.filter(ImageFilter.GaussianBlur(TEXT_STYLE["shadow_blur"]))
image = Image.alpha_composite(image_shadow, image)
image = Image.alpha_composite(IMAGE, image)
image = T.ToTensor()(image).unsqueeze(0).permute([0,2,3,1])
return (image[:, :, :, :3],)
MISC_CLASS_MAPPINGS = {
"DenoiseSlider": DenoiseSlider,
"StepSlider": StepSlider,
"GuidanceSlider": GuidanceSlider,
"BatchSlider": BatchSlider,
"MaxShiftSlider": MaxShiftSlider,
"ControlNetSlider": ControlNetSlider,
"IPAdapterSlider": IPAdapterSlider,
"CannySlider": CannySlider,
"SelectFromBatch": SelectFromBatch,
"GPUSlider": GPUSlider,
"SEGSPass": SEGSPass,
"IntPass": IntPass,
"PipePass": PipePass,
"LatentPass": LatentPass,
"ResolutionPicker": ResolutionPicker,
"ResolutionMultiplySlider": ResolutionMultiplySlider,
"SamplerParameterPacker": SamplerParameterPacker,
"SamplerParameterUnpacker": SamplerParameterUnpacker,
"TextVersions": TextVersions,
"ImpactControlBridgeFix": ImpactControlBridgeFix,
"BooleanToEnabled": BooleanToEnabled,
"OutputGetString": OutputGetString,
"SplitVec2": SplitVec2,
"SplitVec3": SplitVec3,
"SimpleTextTruncate": SimpleTextTruncate,
"FluxContinuumModelRouter": FluxContinuumModelRouter,
"ConfigurableModelRouter": ConfigurableModelRouter,
"ImageBatchBoolean": ImageBatchBoolean,
"DrawTextConfig": DrawTextConfig,
"ConfigurableDrawText": ConfigurableDrawText
}
================================================
FILE: pyproject.toml
================================================
[project]
name = "comfyui-flux-continuum"
description = "Set of custom nodes to use with the ComfyUI Flux Continuum: Modular Interface. NODES: Text Versions, Image64 Display, Tabs, Step Slider, Denoise Slider, Guidance Slider, Batch Slider, Max Shift Slider, ControlNet Slider and more"
version = "1.7.1"
license = {file = "LICENSE"}
dependencies = []
[project.urls]
Repository = "https://github.com/robertvoy/ComfyUI-Flux-Continuum"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "robertvoy"
DisplayName = "ComfyUI-Flux-Continuum"
Icon = ""
================================================
FILE: web/getsetorder.js
================================================
import { app } from "../../../scripts/app.js";
const originalAlert = window.alert;
window.alert = (message) => {
if (message === "Error: Set node input undefined. Most likely you're missing custom nodes") {
return;
}
originalAlert(message);
};
// Configuration for nodes that should be forced to back
const BACKGROUND_NODE_CONFIG = {
nodes: {
"ImagePass": -200,
"GetNode": -100,
"SetNode": -150
}
};
app.registerExtension({
name: "FluxContinuum.GetSetNodeOrdering",
async setup() {
const originalLoadGraphData = app.loadGraphData;
app.loadGraphData = function(graph_data) {
const result = originalLoadGraphData.apply(this, arguments);
// Using requestAnimationFrame instead of setTimeout for better performance
requestAnimationFrame(() => {
if (!app.graph?._nodes?.length) return;
const nodes = app.graph._nodes;
// Batch z-index updates
for (let i = 0; i < nodes.length; i++) {
const zIndex = BACKGROUND_NODE_CONFIG.nodes[nodes[i].type];
if (zIndex !== undefined) {
nodes[i].z_index = zIndex;
}
}
// In-place sort
nodes.sort((a, b) => (a.z_index || 0) - (b.z_index || 0));
// Mark canvas as dirty
app.canvas.setDirty(true, true);
});
return result;
};
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const zIndex = BACKGROUND_NODE_CONFIG.nodes[nodeType.type];
if (zIndex === undefined) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
this.z_index = zIndex;
};
}
});
================================================
FILE: web/guidanceversions.js
================================================
import { app } from "../../../scripts/app.js";
// Configuration Constants
const TAB_CONFIG = {
width: 40,
height: 15,
fontSize: 10,
normalColor: "#0d0d0d",
selectedColor: "#666666",
textColor: "white",
borderRadius: 4,
spacing: 10,
offset: 14,
labels: ["1", "2", "3"], // 3 preset tabs
yPosition: 6
};
app.registerExtension({
name: "FluxContinuum.TabbedGuidanceSlider",
// Modern API uses nodeCreated instead of beforeRegisterNodeDef
nodeCreated(node) {
// Only apply to GuidanceSlider nodes
if (node.type !== "GuidanceSlider" && node.comfyClass !== "GuidanceSlider") return;
console.log("TabbedGuidanceSlider: Found GuidanceSlider node", node);
// Store original methods
const originalOnNodeCreated = node.onNodeCreated;
const originalOnDrawForeground = node.onDrawForeground;
const originalGetBounding = node.getBounding;
const originalOnSerialize = node.onSerialize;
const originalOnConfigure = node.onConfigure;
const originalOnMouseDown = node.onMouseDown; // IMPORTANT: Store the original onMouseDown
// Override methods
node.onNodeCreated = function() {
if (originalOnNodeCreated) {
originalOnNodeCreated.apply(this, arguments);
}
// Initialize tab state and content
this.activeTab = 0;
this.tabContents = TAB_CONFIG.labels.map(() => ({
value: this.widgets[0].value
}));
// Store widget reference
this.valueWidget = this.widgets[0];
// Add change listener to widget
this.valueWidget.callback = () => {
this.tabContents[this.activeTab].value = this.valueWidget.value;
};
};
node.onDrawForeground = function(ctx) {
if (originalOnDrawForeground) {
originalOnDrawForeground.apply(this, arguments);
}
if (this.flags.collapsed) return;
ctx.save();
// Draw tabs
TAB_CONFIG.labels.forEach((label, i) => {
const x = TAB_CONFIG.offset + (TAB_CONFIG.width + TAB_CONFIG.spacing) * i;
const y = TAB_CONFIG.yPosition;
// Draw tab background
ctx.fillStyle = i === this.activeTab ? TAB_CONFIG.selectedColor : TAB_CONFIG.normalColor;
ctx.beginPath();
ctx.roundRect(x, y, TAB_CONFIG.width, TAB_CONFIG.height, TAB_CONFIG.borderRadius);
ctx.fill();
// Draw tab text
ctx.fillStyle = TAB_CONFIG.textColor;
ctx.font = `${TAB_CONFIG.fontSize}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x + TAB_CONFIG.width / 2, y + TAB_CONFIG.height / 2);
});
ctx.restore();
};
node.onMouseDown = function(event, local_pos, graphCanvas) {
const [x, y] = local_pos;
const { yPosition, height, width, spacing, offset, labels } = TAB_CONFIG;
// Check if click is in tab area
if (y >= yPosition && y <= yPosition + height) {
for (let i = 0; i < labels.length; i++) {
const tabX = offset + (width + spacing) * i;
if (x >= tabX && x <= tabX + width) {
if (i === this.activeTab) return false;
// Save current widget value to current tab
this.tabContents[this.activeTab] = {
value: this.valueWidget.value
};
// Switch tab
this.activeTab = i;
// Load content from new tab
this.valueWidget.value = this.tabContents[i].value;
this.setDirtyCanvas(true);
return true;
}
}
}
// IMPORTANT: Call the original onMouseDown if we didn't handle the click
if (originalOnMouseDown) {
return originalOnMouseDown.apply(this, arguments);
}
return false;
};
node.getBounding = function() {
const bounds = originalGetBounding ? originalGetBounding.apply(this, arguments) : [0, 0, 200, 100];
const tabsHeight = Math.abs(TAB_CONFIG.yPosition) + TAB_CONFIG.height;
bounds[1] -= tabsHeight; // Extend top boundary to include tabs
bounds[3] += tabsHeight; // Add tab height to total height
return bounds;
};
node.onSerialize = function(o) {
if (originalOnSerialize) {
originalOnSerialize.apply(this, arguments);
}
o.tabContents = this.tabContents;
o.activeTab = this.activeTab;
};
node.onConfigure = function(o) {
if (originalOnConfigure) {
originalOnConfigure.apply(this, arguments);
}
if (o.tabContents && Array.isArray(o.tabContents)) {
// Ensure tabContents length matches number of tabs
this.tabContents = TAB_CONFIG.labels.map((_, i) =>
o.tabContents[i] || {
value: this.valueWidget.value
}
);
this.activeTab = o.activeTab >= 0 && o.activeTab < TAB_CONFIG.labels.length ? o.activeTab : 0;
// Load the active tab's content
if (this.valueWidget) {
this.valueWidget.value = this.tabContents[this.activeTab].value;
}
}
};
}
});
================================================
FILE: web/help.js
================================================
import { app } from "../../../scripts/app.js";
// Replace this with the category/categories used by your nodes.
const categories = ["Flux-Continuum/Utilities", "Flux-Continuum/Sliders"];
// --- MODIFICATION 3: Change the extension name to be unique ---
app.registerExtension({
name: "FluxContinuum.Help", // A unique name for your extension
async beforeRegisterNodeDef(nodeType, nodeData) {
if (app.ui.settings.getSettingValue("KJNodes.helpPopup") === false) {
return;
}
try {
// --- MODIFICATION 4: Corrected Logic ---
// Check if the node's category matches any in our list.
const hasMatchingCategory = categories.some(cat => nodeData?.category?.startsWith(cat));
if (hasMatchingCategory) {
addDocumentation(nodeData, nodeType);
}
} catch (error) {
console.error("Error in registering MyNodePack.HelpPopup", error);
}
},
});
const create_documentation_stylesheet = () => {
const tag = 'my-documentation-stylesheet' // Use a unique tag
let styleTag = document.head.querySelector(tag)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.id = tag
styleTag.innerHTML = `
.kj-documentation-popup {
background: var(--comfy-menu-bg);
position: absolute;
color: var(--fg-color);
font: 12px monospace;
line-height: 1.5em;
padding: 10px;
border-radius: 10px;
border-style: solid;
border-width: medium;
border-color: var(--border-color);
z-index: 50; /* Increased z-index */
overflow: hidden;
max-width: 450px; /* Set a maximum width */
min-width: 200px; /* Set a minimum width */
word-wrap: break-word; /* Ensure long words wrap */
}
.content-wrapper {
overflow: auto;
max-height: 100%;
/* Scrollbar styling for Chrome */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: var(--bg-color);
}
&::-webkit-scrollbar-thumb {
background-color: var(--fg-color);
border-radius: 6px;
border: 3px solid var(--bg-color);
}
/* Scrollbar styling for Firefox */
scrollbar-width: thin;
scrollbar-color: var(--fg-color) var(--bg-color);
a {
color: yellow;
}
a:visited {
color: orange;
}
a:hover {
color: red;
}
}
`
document.head.appendChild(styleTag)
}
}
/** Add documentation widget to the selected node */
export const addDocumentation = (
nodeData,
nodeType,
opts = { icon_size: 14, icon_margin: 4 },) => {
opts = opts || {}
const iconSize = opts.icon_size ? opts.icon_size : 14
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
let docElement = null
let contentWrapper = null
//if no description in the node python code, don't do anything
if (!nodeData.description) {
return
}
const drawFg = nodeType.prototype.onDrawForeground
nodeType.prototype.onDrawForeground = function (ctx) {
const r = drawFg ? drawFg.apply(this, arguments) : undefined
if (this.flags.collapsed) return r
// icon position
const x = this.size[0] - iconSize - iconMargin
// create the popup
if (this.show_doc && docElement === null) {
docElement = document.createElement('div')
contentWrapper = document.createElement('div');
docElement.appendChild(contentWrapper);
create_documentation_stylesheet()
contentWrapper.classList.add('content-wrapper');
docElement.classList.add('kj-documentation-popup')
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
// resize handle
const resizeHandle = document.createElement('div');
resizeHandle.style.width = '0';
resizeHandle.style.height = '0';
resizeHandle.style.position = 'absolute';
resizeHandle.style.bottom = '0';
resizeHandle.style.right = '0';
resizeHandle.style.cursor = 'se-resize';
// Add pseudo-elements to create a triangle shape
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
resizeHandle.style.borderTop = '10px solid transparent';
resizeHandle.style.borderLeft = '10px solid transparent';
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
docElement.appendChild(resizeHandle)
let isResizing = false
let startX, startY, startWidth, startHeight
resizeHandle.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
},
{ signal: this.docCtrl.signal },
);
// close button
const closeButton = document.createElement('div');
closeButton.textContent = '❌';
closeButton.style.position = 'absolute';
closeButton.style.top = '0';
closeButton.style.right = '0';
closeButton.style.cursor = 'pointer';
closeButton.style.padding = '5px';
closeButton.style.color = 'red';
closeButton.style.fontSize = '12px';
docElement.appendChild(closeButton)
closeButton.addEventListener('mousedown', (e) => {
e.stopPropagation();
this.show_doc = !this.show_doc
docElement.parentNode.removeChild(docElement)
docElement = null
if (contentWrapper) {
contentWrapper.remove()
contentWrapper = null
}
},
{ signal: this.docCtrl.signal },
);
document.addEventListener('mousemove', function (e) {
if (!isResizing) return;
const scale = app.canvas.ds.scale;
const newWidth = startWidth + (e.clientX - startX) / scale;
const newHeight = startHeight + (e.clientY - startY) / scale;;
docElement.style.width = `${newWidth}px`;
docElement.style.height = `${newHeight}px`;
},
{ signal: this.docCtrl.signal },
);
document.addEventListener('mouseup', function () {
isResizing = false
},
{ signal: this.docCtrl.signal },
)
document.body.appendChild(docElement)
}
// close the popup
else if (!this.show_doc && docElement !== null) {
docElement.parentNode.removeChild(docElement)
docElement = null
}
// update position of the popup
if (this.show_doc && docElement !== null) {
const rect = ctx.canvas.getBoundingClientRect()
const scaleX = rect.width / ctx.canvas.width
const scaleY = rect.height / ctx.canvas.height
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(this.size[0], 0) // Adjusted position slightly
.translateSelf(10, 0)
const scale = new DOMMatrix()
.scaleSelf(transform.a, transform.d);
const bcr = app.canvas.canvas.getBoundingClientRect()
const styleObject = {
transformOrigin: '0 0',
transform: scale,
left: `${bcr.x + transform.e}px`,
top: `${bcr.y + transform.f}px`,
};
Object.assign(docElement.style, styleObject);
}
ctx.save()
ctx.translate(x, iconSize - 38)
ctx.scale(iconSize / 24, iconSize / 24)
ctx.font = 'bold 24px monospace'
ctx.fillStyle = this.show_doc ? 'orange' : 'rgba(255, 255, 255, 0.4)';
ctx.fillText('?', 0, 24)
ctx.restore()
return r
}
// handle clicking of the icon
const mouseDown = nodeType.prototype.onMouseDown
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
const iconX = this.size[0] - iconSize - iconMargin
const iconY = iconSize - 34
if (
localPos[0] > iconX &&
localPos[0] < iconX + iconSize &&
localPos[1] > iconY &&
localPos[1] < iconY + iconSize
) {
if (this.show_doc === undefined) {
this.show_doc = true
} else {
this.show_doc = !this.show_doc
}
if (this.show_doc) {
this.docCtrl = new AbortController()
} else {
if (this.docCtrl) {
this.docCtrl.abort()
}
}
return true;
}
return r;
}
const onRem = nodeType.prototype.onRemoved
nodeType.prototype.onRemoved = function () {
const r = onRem ? onRem.apply(this, []) : undefined
if (docElement) {
docElement.remove()
docElement = null
}
if (contentWrapper) {
contentWrapper.remove()
contentWrapper = null
}
return r
}
}
================================================
FILE: web/hint.js
================================================
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "FluxContinuum.HintNode",
registerCustomNodes() {
const LiteGraph = window.LiteGraph;
// --- Stylesheet for the popup ---
const create_hint_stylesheet = () => {
const tagId = 'flux-hint-stylesheet-final';
if (document.head.querySelector(`#${tagId}`)) return;
const styleTag = document.createElement('style');
styleTag.id = tagId;
styleTag.innerHTML = `
.flux-hint-popup-final {
background: var(--comfy-menu-bg);
position: absolute;
color: var(--fg-color);
font: 12px monospace;
line-height: 1.5em;
padding: 25px;
border-radius: 10px;
border: 1px solid var(--border-color);
z-index: 101;
overflow: hidden;
min-width: 200px;
max-width: 500px;
min-height: 50px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.flux-hint-popup-final .content-wrapper {
overflow: auto;
max-height: 400px;
white-space: pre-wrap;
}
.flux-hint-popup-final .content-wrapper p,
.flux-hint-popup-final .content-wrapper h1,
.flux-hint-popup-final .content-wrapper h2,
.flux-hint-popup-final .content-wrapper h3,
.flux-hint-popup-final .content-wrapper h4,
.flux-hint-popup-final .content-wrapper h5,
.flux-hint-popup-final .content-wrapper h6 {
margin-top: 0.1em !important;
margin-bottom: 0.1em !important;
}
.flux-hint-edit-dialog textarea {
width: 500px;
height: 200px;
font-family: monospace;
}
.flux-hint-edit-dialog .buttons {
text-align: right;
margin-top: 10px;
}
`;
document.head.appendChild(styleTag);
};
// --- Define the custom Hint Node class ---
class FCHintNode extends LiteGraph.LGraphNode {
constructor() {
super("Hint");
this.isVirtualNode = true;
this.shape = LiteGraph.ROUND_SHAPE;
this.resizable = false;
this.size = [20, 20];
this.properties = {
hint_content: "Right-click me and choose 'Edit Hint' to add multi-line text!\n\n**Markdown** is supported.",
saved_size: null
};
this.popupElement = null;
this.contentWrapper = null;
this.docCtrl = null;
this.closeTimer = null;
create_hint_stylesheet();
}
// --- Core LiteGraph Methods ---
getExtraMenuOptions(canvas, options) {
options.unshift({
content: "Edit Hint...",
callback: () => {
const initialValue = this.properties.hint_content;
const dialogContent = `
<div class="flux-hint-edit-dialog">
<textarea autofocus>${initialValue}</textarea>
<div class="buttons">
<button id="hint-cancel-btn">Cancel</button>
<button id="hint-save-btn" style="margin-left: 5px;">Save</button>
</div>
</div>`;
const dialog = canvas.createDialog(dialogContent, {
title: "Edit Hint Content",
close_on_click_outside: true,
});
const textarea = dialog.querySelector("textarea");
const saveBtn = dialog.querySelector("#hint-save-btn");
const cancelBtn = dialog.querySelector("#hint-cancel-btn");
saveBtn.addEventListener('click', () => {
this.properties.hint_content = textarea.value;
if (this.popupElement) {
this.updatePopupContent();
}
dialog.close();
});
cancelBtn.addEventListener('click', () => {
dialog.close();
});
}
});
}
computeSize(out) {
if (this.properties.saved_size) return this.properties.saved_size;
return [20, 20];
}
onAdded(graph) {
if (this.properties.saved_size) this.size = this.properties.saved_size;
}
onResize(size) { }
onMouseEnter(e) {
if (this.closeTimer) clearTimeout(this.closeTimer);
this.createPopup();
}
onMouseLeave(e) {
this.closeTimer = setTimeout(() => this.removePopup(), 300);
}
onDrawBackground(ctx) {}
onDrawForeground(ctx) {
if (this.flags.collapsed) return;
ctx.save();
ctx.font = 'bold ' + (this.size[1] * 0.6) + 'px monospace';
ctx.fillStyle = this.popupElement ? 'orange' : 'rgba(255, 255, 255, 0.7)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('?', this.size[0] / 2, this.size[1] / 2 + 2);
ctx.restore();
if (this.popupElement) {
this.updatePopupPosition(ctx);
}
}
onRemoved() {
this.removePopup();
}
onSerialize(o) {
o.size = this.size;
o.hint_content = this.properties.hint_content;
}
onConfigure(o) {
if (o.size) {
this.properties.saved_size = o.size;
this.size = o.size;
}
if (o.hint_content) this.properties.hint_content = o.hint_content;
}
// --- Popup Management ---
updatePopupPosition(ctx) {
if (!this.popupElement || !ctx) return;
const canvas = app.canvas.canvas;
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width / canvas.width;
const scaleY = rect.height / canvas.height;
const transform = new DOMMatrix()
.scaleSelf(scaleX, scaleY)
.multiplySelf(ctx.getTransform())
.translateSelf(this.size[0], 0)
.translateSelf(10 / scaleX, 0);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
Object.assign(this.popupElement.style, {
transformOrigin: '0 0',
transform: scale,
left: `${rect.x + transform.e}px`,
top: `${rect.y + transform.f}px`,
});
}
createPopup() {
if (this.popupElement) return;
this.popupElement = document.createElement('div');
this.contentWrapper = document.createElement('div');
this.popupElement.appendChild(this.contentWrapper);
this.popupElement.className = 'flux-hint-popup-final';
this.contentWrapper.className = 'content-wrapper';
this.popupElement.addEventListener('mouseenter', () => {
if (this.closeTimer) clearTimeout(this.closeTimer);
});
this.popupElement.addEventListener('mouseleave', () => {
this.closeTimer = setTimeout(() => this.removePopup(), 300);
});
this.updatePopupContent();
this.addPopupControls();
document.body.appendChild(this.popupElement);
}
removePopup() {
if (this.closeTimer) clearTimeout(this.closeTimer);
if (this.popupElement) this.popupElement.remove();
if (this.docCtrl) this.docCtrl.abort();
this.popupElement = null;
this.docCtrl = null;
}
updatePopupContent() {
if (!this.contentWrapper) return;
const content = this.properties.hint_content || "";
if (window.marked && window.DOMPurify) {
const html = marked.parse(content, { breaks: true });
this.contentWrapper.innerHTML = DOMPurify.sanitize(html);
} else {
this.contentWrapper.textContent = content;
}
}
addPopupControls() {
const resizeHandle = document.createElement('div');
Object.assign(resizeHandle.style, {
width: '0', height: '0', position: 'absolute', bottom: '0', right: '0', cursor: 'se-resize',
borderTop: '10px solid transparent', borderLeft: '10px solid transparent',
borderBottom: '10px solid var(--border-color)', borderRight: '10px solid var(--border-color)'
});
this.popupElement.appendChild(resizeHandle);
// ** The close button creation logic has been removed. **
this.docCtrl = new AbortController();
const signal = this.docCtrl.signal;
let isResizing = false, startX, startY, startWidth, startHeight;
resizeHandle.addEventListener('mousedown', (e) => {
e.preventDefault(); e.stopPropagation();
isResizing = true;
startX = e.clientX; startY = e.clientY;
startWidth = this.popupElement.offsetWidth;
startHeight = this.popupElement.offsetHeight;
}, { signal });
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
this.popupElement.style.width = `${newWidth}px`;
this.popupElement.style.height = `${newHeight}px`;
}, { signal });
document.addEventListener('mouseup', () => { isResizing = false; }, { signal });
}
}
// --- Register the Node with LiteGraph ---
const nodeClassName = "Hint Node";
const nodeCategory = "Flux-Continuum/Utilities";
LiteGraph.registerNodeType(nodeClassName, Object.assign(FCHintNode, {
title: "Hint",
title_mode: LiteGraph.NO_TITLE,
category: nodeCategory
}));
FCHintNode.prototype.flags = { no_title: true };
}
});
================================================
FILE: web/imagedisplay.js
================================================
import { app } from "../../../scripts/app.js";
// Default white square image (200x200 pixels, white)
const DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAPoAAAD6CAIAAAAHjs1qAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKUklEQVR4nO3dS0hU7R/AcR3NLHOs1IoulEyBhRlZ0Y3uIGFBQRRSi8guVKuoLApatLA23RZRC7tRGxMEW1mmyFuRJiJUVmJlNyvTzDQnrVHnvzhQ/bvonJkzz/PMeb6f7Yvn95vp23lnzhynsDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7Cpe9wO9cLldGRkZycnJCQsLAgQNlrwPTPB7Px48f6+rqioqKamtrZa/zf1TJfdSoUXv37jVCDw9XZSsEwuv11tfXFxUVnTp16vnz57LXUUN0dPTZs2e/fPnihU253e5Lly7Fx8fLbk22jIyMV69eyf7jgAjv37/PzMyU21uExNl79uzJzc0dPny4xB0gzJAhQ1avXh0VFVVWViZrB2m5Hzp06MiRI5GRkbIWgHgOh2PhwoWxsbHFxcVSFpCT++bNm48dO+ZwOKRMh1xz587t6OgoLy8XP1rCNZCUlJTbt28PHTpU/Ggowu12p6en3717V/BcCbmXlJQsW7ZM/FwopaqqatasWYKHin4xs3bt2v379wseCgWNHj26qampqqpK5FDRZ/c7d+7Mnz9f8FCo6eHDh6mpqSInCn2zmJycPHv2bJETobKpU6cuXrxY5EShuW/cuJErj/iV4A+ehOYu/q0JFDdz5kyR44Tm7nK5RI6D+iZMmCBynNC3qm63e/DgwSInQn1xcXHt7e1iZgk9u9M6/jR27Fhhs1R/41hRUXHu3Lm2tjbZi6AfsbGxGzZs8OMDxMTExGDsI5/ZW0ZbW1ujo6Nlbw1fORyOhoYGs3/KixYtErehsEl+ePfuXVdXl+wt4Kve3t5nz57J3qIvSueOkNPT0yN7hb6QOzRC7tAIuUMj5A6NkDs0Qu7QCLlDI+QOjZA7NELu0Ijqd0QGLjk5OSsra86cOePHj09ISIiKivr+/XtLS8urV68qKysvXrxYU1Mje0fYkdl75R49ehTIuAULFty8edPj8fQxoru7u7S0lO+9sUppaanKd0QKJSx3h8Nx9uzZb9+++TjI4/FcuHAhIkLmF8Tag+K52/C1u9PpvH379vbt26Oionz8kcjIyE2bNpWXl48aNSqou0Euu+UeHR1dVlY2b948P3521qxZN27ccDqdlm8FRdgt97y8vLS0NL9/PDU19erVqxbuA6XYKvcdO3asWrUqwIMsX7589+7dluwD1dgn94iIiIMHD1pyqH379vE7srZkn9z37t1r1Vc4jBw58sCBA5YcCkqxT+5r1qyx8GiBvyiCgmySu9PpnDZtmoUHTElJUfOi5IgRIy5fvvzixYvm5ubq6uqdO3fK3gj/YPYDCN8/Zlq3bp3Zg/crKysrqM+GH1wuV11d3W975ubmyt7rJz5mEmHy5MmWH3PSpEmWHzMQLpfr+vXrf261ZcuW8+fPS1kp5Ngk97i4uJA4pt+M1idOnPjX/5qVlUXxvrBJ7l6vV/YKQdR36waK94VNcv/8+bPlx2xtbbX8mH7wpXUDxffLJrk/efLE8mPW1dVZfkyzfG/dQPEKMfue3fcrM9HR0Z2dnWaP3wePxxMfHx/UZ6NfLpfr6dOnfiwvsXiuzIjQ1dVVXV1t4QHv37/f0tJi4QHNMnte/xXn+H+xSe5hYWF5eXkWHq2goMDCo5kVSOsGipfP7P/mzP42U319vdkRf/XmzRuJv9nk92uYP4kvnhcz4hw+fNhrxRXJI0eOyPqe8sDP67/iHC+T2b/3fvyu6pUrV8xO+Y3ElzEWntd/JbJ4xc/uQpl9IvzI3eFwlJWVmR30Q3l5+YABA4Lx2PsVpNYNwoon95/MPhH+fRPBgAEDioqKzM7yer2lpaUxMTGWP2pfBLV1g5jiyf0ns09EIN8zk5OT09HR4eOgr1+/Hj9+3MJHaoqA1g0Ciif3n8w+EQF+rdKUKVPy8/PdbncfIzo7OwsLC6dPn27VYzRLWOuGYBeveO52/tK8x48fr1u3Lj4+fuvWrfPnz09KSkpISIiIiOjp6Wlpaamvr6+oqMjNzW1qapK1obXXYXxh3MS/efNmYRP1ZfbvfYBnd8UJPq//KnjneMXP7ra67h5CxJ/Xf6Xt9Xhyl0Bu6wY9iyd30VRo3aBh8eQulDqtG3QrntzFUa11g1bFk7sgarZu0Kd4chdB5dYNmhRP7kGnfusGHYrXPfdhw4YF9cvxQqV1g+2L1zf3DRs2PH78uKWl5f37969fv96zZ4/lI0KrdYPtixfH7MfLwbuJIDs7+89/ke/kyZMWjpB4j0Dg/C5e8ZsIhDL7RAQp9+zs7O7u7r9OtKr4kG7d4F/x5P6T2SciGLn30boh8OJt0Lrh9OnTZh87uf9k9omwPPd+WzcEUrxtWvd6vffu3TP78BXPXaO3qtnZ2UePHvXlGzV27drlX/Gh+N5UK7rk7nvrBj+Kp3X1aZG72dYNpoqn9ZBg/9z9a93gY/G0HipsnnsgrRv6LZ7WQ4idcw+8dUMfxdN6aLFt7la1bvhr8bQecuyZu7WtG34rntZDkQ1zD0brhh/F03qIstvXKu3evTtIrRt27do1ePDgpUuX0nooslvuK1asCPa/RLBt27agHh/BY8MXM8C/kDs0Qu7QCLlDI+QOjZA7NELu0Ai5QyPkDo2QOzRC7tAIuUMj5A6NkDs0Qu7QiN3ud6+urpa9gn08efJE9goWs1vu2dnZsleAungxA42QOzRC7tAIuUMj5A6NkDs0Qu7QCLlDI+QOjZA7NELu0Ai5QyPkDo0onfugQYNkrwBzhg0bJnuFviid+4QJE7Zv3y57C/gqMzMzJSVF9hZ9CRc5zOv1+vEjL1++7OzsDMY+sFBUVFRSUpIf/5bE7NmzKysrg7HSn4T+ekd3d3dkpLmJ4eHhSUlJQdoHKmhubhY2S+iLmc+fP4scB/V1dXW9ePFC2Dihub9580bkOKjv7du3IscJzf3Bgwcix0F9NTU1IscJzb24uFjkOKivrKxM5DihV2bCwsIaGhrGjBkjeCjU1NbWlpiY6PF4hE0Ufd09Pz9f8EQoq6CgQGTrYeLP7k6n8+nTpyNGjBA8F6ppb29PS0t7/vy5yKGiz+7t7e05OTmCh0JBp06dEtx6mPizu6GwsHDVqlVSRkMF//333+LFi8XPlZN7TEzMrVu30tLSpEyHXLW1tUuWLGlsbBQ/Ws4tYm63e8WKFXx9qYZqa2tXrlwppfUwiXdENjY2Lly48Nq1a7IWgHglJSVLliwR/5L9B9P3r1nI4/Hk5eW1trbOmDEjJiZG4iYItk+fPuXk5GzdurWjo0P2LrI5nc4TJ040NDR4YTsfPnw4c+aMIpee5bxV/Zf169enp6enpqaOGzdu6NChZu8Whgp6enra2trevn374MGDkpKSy5cv9/b2yl4KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA1v4He6Qy6Tvwx/oAAAAASUVORK5CYII=";
class ImageDisplay extends LGraphNode {
constructor() {
super();
this.isVirtualNode = true;
this.shape = LiteGraph.BOX_SHAPE;
this.size = [200, 200];
this.bgcolor = "#00000000";
this.color = "#00000000";
this.resizable = true;
// Add properties to store original dimensions and image data
this.properties = {
originalWidth: 200,
originalHeight: 200,
maintainAspectRatio: true,
imageBase64: DEFAULT_IMAGE, // Initialize with default image
};
this.addProperty("imageBase64", DEFAULT_IMAGE, "string"); // Add widget for image input
this.widgets_up = true;
this.img = new Image();
this.loaded = false;
this.setupImage();
}
setupImage() {
this.img.onload = () => {
console.log("Image loaded successfully!");
console.log("Image dimensions:", this.img.width, "x", this.img.height);
this.loaded = true;
// Store original dimensions
this.properties.originalWidth = this.img.width;
this.properties.originalHeight = this.img.height;
// Set initial size if not restored from previous session
if (!this.properties.lastWidth) {
this.size[0] = this.img.width;
this.size[1] = this.img.height;
} else {
// Restore previous size
this.size[0] = this.properties.lastWidth;
this.size[1] = this.properties.lastHeight;
}
this.setDirtyCanvas(true);
};
this.img.onerror = (err) => {
console.error("Error loading image:", err);
// If loading fails, revert to default image
if (this.properties.imageBase64 !== DEFAULT_IMAGE) {
console.log("Reverting to default image");
this.properties.imageBase64 = DEFAULT_IMAGE;
this.img.src = "data:image/png;base64," + DEFAULT_IMAGE;
}
};
// Load initial image
this.img.src = "data:image/png;base64," + this.properties.imageBase64;
}
// Method to update image when base64 property changes
onPropertyChanged(name, value) {
if (name === "imageBase64") {
this.loaded = false;
// If value is empty, use default image
const imageData = value || DEFAULT_IMAGE;
this.img.src = "data:image/png;base64," + imageData;
}
}
// Handle resizing with aspect ratio
onResize(size) {
if (this.properties.maintainAspectRatio && this.loaded) {
const aspectRatio = this.properties.originalWidth / this.properties.originalHeight;
const currentRatio = size[0] / size[1];
if (currentRatio > aspectRatio) {
size[0] = size[1] * aspectRatio;
} else {
size[1] = size[0] / aspectRatio;
}
}
this.properties.lastWidth = size[0];
this.properties.lastHeight = size[1];
return size;
}
// Serialize the node state
onSerialize(o) {
if (this.loaded) {
o.lastWidth = this.size[0];
o.lastHeight = this.size[1];
o.imageBase64 = this.properties.imageBase64; // Save image data
}
}
// Deserialize the node state
onConfigure(o) {
if (o.lastWidth !== undefined) {
this.properties.lastWidth = o.lastWidth;
this.properties.lastHeight = o.lastHeight;
}
if (o.imageBase64 !== undefined) {
this.properties.imageBase64 = o.imageBase64;
this.onPropertyChanged("imageBase64", o.imageBase64);
}
}
}
// Override the default node drawing
const oldDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function(node, ctx) {
if (node.constructor === ImageDisplay) {
const tmp_shape = node.shape;
const tmp_color = node.color;
const tmp_bgcolor = node.bgcolor;
node.shape = null;
node.color = "#00000000";
node.bgcolor = "#00000000";
const r = oldDrawNode.apply(this, arguments);
if (node.img && node.loaded) {
ctx.save();
ctx.drawImage(node.img, 0, 0, node.size[0], node.size[1]);
ctx.restore();
}
node.shape = tmp_shape;
node.color = tmp_color;
node.bgcolor = tmp_bgcolor;
return r;
}
return oldDrawNode.apply(this, arguments);
};
app.registerExtension({
name: "FluxContinuum.ImageDisplay",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "ImageDisplay") {
nodeType.prototype.execute = () => {};
}
},
registerCustomNodes() {
LiteGraph.registerNodeType(
"ImageDisplay",
Object.assign(ImageDisplay, {
title: "ImageDisplay",
title_mode: LiteGraph.NO_TITLE,
category: "Flux-Continuum/Utilities",
comfyClass: "ImageDisplay"
})
);
ImageDisplay.prototype.flags = { no_title: true };
}
});
================================================
FILE: web/imagetransfershortcut.js
================================================
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
app.registerExtension({
name: "FluxContinuum.ImageTransfer",
commands: [
{
id: "imageTransfer",
label: "Image Transfer",
function: async () => {
const sourceNodeTitle = "Img Preview";
const destNodeTitle = "Img Load";
try {
const sourceNode = app.graph.findNodesByTitle(sourceNodeTitle)?.[0];
const destNode = app.graph.findNodesByTitle(destNodeTitle)?.[0];
if (!sourceNode || !destNode) {
console.error(`Custom Command: Could not find nodes "${sourceNodeTitle}" and/or "${destNodeTitle}".`);
return;
}
if (!sourceNode.images || sourceNode.images.length === 0) {
console.warn(`Custom Command: Source node "${sourceNodeTitle}" has no image to copy.`);
return;
}
// Get the current image index
let currentIndex = sourceNode.imageIndex ?? 0;
// Ensure index is within bounds
currentIndex = Math.max(0, Math.min(currentIndex, sourceNode.images.length - 1));
// Use the selected image
const imageInfo = sourceNode.images[currentIndex];
const response = await api.fetchApi(`/view?filename=${encodeURIComponent(imageInfo.filename)}&type=${imageInfo.type}&subfolder=${encodeURIComponent(imageInfo.subfolder)}`);
const imageBlob = await response.blob();
const formData = new FormData();
formData.append("image", imageBlob, imageInfo.filename);
formData.append("overwrite", "true");
const uploadResponse = await api.fetchApi('/upload/image', {
method: 'POST',
body: formData
});
const uploadResult = await uploadResponse.json();
if (!uploadResult || !uploadResult.name) {
throw new Error("Upload failed or did not return a valid filename from the server.");
}
const newFilename = uploadResult.name;
const imageWidget = destNode.widgets.find((w) => w.name === "image");
if (imageWidget) {
imageWidget.value = newFilename;
if (imageWidget.callback) {
imageWidget.callback(imageWidget.value);
}
destNode.setDirtyCanvas(true, true);
} else {
console.error(`Custom Command: Could not find the 'image' widget on "${destNodeTitle}".`);
}
} catch (error) {
console.error("Custom Command: An error occurred:", error);
}
},
},
],
keybindings: [
{
combo: { key: "c", ctrl: true, shift: true },
commandId: "imageTransfer",
},
],
});
================================================
FILE: web/impactpackfix.js
================================================
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "FluxContinuum.ImpactControlBridgeFix",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData.name == "ImpactControlBridge") {
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
if(index != 0 || !link_info || this.inputs[0].type != '*')
return;
// assign type
let slot_type = '*';
if(type == 2) {
slot_type = link_info.type;
}
else {
const node = app.graph.getNodeById(link_info.origin_id);
slot_type = node.outputs[link_info.origin_slot].type;
}
this.inputs[0].type = slot_type;
this.outputs[0].type = slot_type;
this.outputs[0].label = slot_type;
}
}
}
});
================================================
FILE: web/outputgetnode.js
================================================
import { app } from "../../../scripts/app.js";
//based on KJNodes SetGet: https://github.com/kijai/ComfyUI-KJNodes
// Configuration
const CONFIG = {
NODE: {
name: "OutputGet",
category: "Flux-Continuum/Utilities",
title: "OutputGet",
defaultColor: "#FFF",
prefix: "Output -"
},
CACHE: {
timeout: 1000,
debounceWait: 100
},
ERRORS: {
singletonError: "Error: Only one OutputGet node is allowed.",
noSetNodeError: "No Output SetNode found for",
errorTimeout: 5000
}
};
// Helper functions
function debounce(func, wait = CONFIG.CACHE.debounceWait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function createCachedFunction(fn) {
let cache = null;
let lastUpdate = 0;
return function (...args) {
const now = Date.now();
if (!cache || (now - lastUpdate) > CONFIG.CACHE.timeout) {
cache = fn.apply(this, args);
lastUpdate = now;
}
return cache;
};
}
app.registerExtension({
name: "FluxContinuum.OutputGet",
registerCustomNodes() {
class OutputGetNode extends LGraphNode {
static instance = null;
constructor(title) {
super(title);
if (OutputGetNode.instance) {
alert(CONFIG.ERRORS.singletonError);
setTimeout(() => this.graph?.remove(this), 0);
return;
}
OutputGetNode.instance = this;
if (!this.properties) {
this.properties = {};
}
this.properties.showOutputText = true;
const node = this;
const widget = this.addWidget(
"combo",
"Selected",
"",
function (value, canvas, node, pos, event) {
try {
this.value = value;
node.widgets[0].value = value;
node.onRename();
node.graph._version++;
node.setDirtyCanvas(true, true);
node.updateGetStringTitles();
} catch (error) {
console.error("Error in combo widget callback:", error);
}
},
{
values: () => node.getSetterNodeTitles(node.graph),
}
);
this.addOutput("*", '*');
this.onConnectionsChange = function (slotType, slot, isChangeConnect, link_info, output) {
try {
this.validateLinks();
if (link_info && node.graph) {
const setter = node.findSetter(node.graph);
if (setter) {
let linkType = setter.inputs[0].type;
node.setType(linkType);
node.updateGetStringTitles();
}
}
node.setDirtyCanvas(true, true);
} catch (error) {
console.error("Error in onConnectionsChange:", error);
}
};
this.setName = function (name) {
try {
node.widgets[0].value = name;
node.onRename();
node.serialize();
node.updateGetStringTitles();
} catch (error) {
console.error("Error in setName:", error);
}
};
this.onRename = function () {
try {
const setter = node.findSetter(node.graph);
if (setter) {
let linkType = setter.inputs[0].type;
node.setType(linkType);
node.title = setter.widgets[0].value;
if (app.ui.settings.getSettingValue("KJNodes.nodeAutoColor")) {
setColorAndBgColor.call(node, linkType);
}
node.updateGetStringTitles();
} else {
node.setType('*');
}
} catch (error) {
console.error("Error in onRename:", error);
}
};
this.updateGetStringTitles = debounce(function () {
try {
const getStringNodes = this.graph._nodes.filter((n) => n.type === "OutputGetString");
getStringNodes.forEach((getStringNode) => {
getStringNode.title = "GetString_" + this.title;
});
} catch (error) {
console.error("Error updating GetString titles:", error);
}
}, CONFIG.CACHE.debounceWait);
this.getSetterNodeTitles = createCachedFunction(function (graph) {
return graph._nodes
.filter(node => node.type === 'SetNode' && node.widgets[0].value.startsWith(CONFIG.NODE.prefix))
.map(node => node.widgets[0].value)
.sort();
});
this.defaultVisibility = true;
this.serialize_widgets = true;
this.drawConnection = false;
this.slotColor = CONFIG.NODE.defaultColor;
this.currentSetter = null;
this.canvas = app.canvas;
this.isVirtualNode = true;
}
onRemoved() {
if (OutputGetNode.instance === this) {
OutputGetNode.instance = null;
}
this.drawConnection = false;
this.currentSetter = null;
}
validateLinks() {
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
this.outputs[0].links.filter((linkId) => {
const link = this.graph.links[linkId];
return link && (link.type !== this.outputs[0].type && link.type !== '*');
}).forEach((linkId) => {
this.graph.removeLink(linkId);
});
}
}
setType(type) {
this.outputs[0].name = type;
this.outputs[0].type = type;
this.validateLinks();
}
findSetter(graph) {
const name = this.widgets[0].value;
return graph._nodes.find(
(otherNode) =>
otherNode.type === 'SetNode' &&
otherNode.widgets[0].value === name &&
name !== '' &&
name.startsWith(CONFIG.NODE.prefix)
);
}
goToSetter() {
const setter = this.findSetter(this.graph);
if (setter) {
this.canvas.centerOnNode(setter);
this.canvas.selectNode(setter);
}
}
getInputLink(slot) {
try {
const setter = this.findSetter(this.graph);
if (setter) {
const slotInfo = setter.inputs[slot];
return this.graph.links[slotInfo.link];
} else {
const message = `${CONFIG.ERRORS.noSetNodeError} ${this.widgets[0].value}`;
if (!window.isAlertShown) {
window.isAlertShown = true;
alert(message);
setTimeout(() => (window.isAlertShown = false), CONFIG.ERRORS.errorTimeout);
}
}
} catch (error) {
console.error("Error in getInputLink:", error);
return null;
}
}
getExtraMenuOptions(_, options) {
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
options.unshift(
{
content: "Go to output setter",
callback: () => {
this.goToSetter();
},
},
{
content: menuEntry,
callback: () => {
try {
this.currentSetter = this.findSetter(this.graph);
if (!this.currentSetter) return;
let linkType = this.currentSetter.inputs[0].type;
this.drawConnection = !this.drawConnection;
this.slotColor = this.canvas.default_connection_color_byType[linkType];
this.canvas.setDirty(true, true);
} catch (error) {
console.error("Error in menu callback:", error);
}
},
}
);
}
onDrawForeground(ctx, lGraphCanvas) {
if (this.drawConnection) {
this._drawVirtualLink(lGraphCanvas, ctx);
}
}
_drawVirtualLink(lGraphCanvas, ctx) {
if (!this.currentSetter) return;
try {
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
start_node_slotpos = [
start_node_slotpos[0] - this.pos[0],
start_node_slotpos[1] - this.pos[1],
];
let end_node_slotpos = [0, -LiteGraph.NODE_TITLE_HEIGHT * 0.5];
lGraphCanvas.renderLink(
ctx,
start_node_slotpos,
end_node_slotpos,
null,
false,
null,
this.slotColor
);
} catch (error) {
console.error("Error drawing virtual link:", error);
}
}
}
LiteGraph.registerNodeType(CONFIG.NODE.name, OutputGetNode);
OutputGetNode.category = CONFIG.NODE.category;
const originalCheckNodeTypes = LGraphCanvas.prototype.checkNodeTypes;
LGraphCanvas.prototype.checkNodeTypes = function() {
const r = originalCheckNodeTypes.apply(this, arguments);
if (r && r.type === CONFIG.NODE.name && OutputGetNode.hasInstance()) {
r.disabled = true;
r.tooltip = CONFIG.ERRORS.singletonError;
}
return r;
};
},
});
app.registerExtension({
name: "FluxContinuum.OutputGetString",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "OutputGetString") {
return;
}
// Ensure the widget is hidden and updated
const ensureHiddenWidget = (node) => {
let widget = node.widgets?.find((w) => w.name === "title");
if (!widget) {
// If the widget does not exist, create it
widget = { name: "title", value: "", hidden: true, serialize: true };
node.widgets = node.widgets || [];
node.widgets.push(widget);
}
// Keep it hidden and serialized
widget.hidden = true;
return widget;
};
// Update title and widget values
const updateGetStringTitles = (graph) => {
const getStringNodes = graph._nodes.filter((n) => n.type === "OutputGetString");
const outputGetNode = graph._nodes.find((n) => n.type === CONFIG.NODE.name);
getStringNodes.forEach((node) => {
const titleWidget = ensureHiddenWidget(node);
if (outputGetNode) {
// Update the title and widget value
const newTitle = "GetString_" + outputGetNode.title;
node.title = newTitle;
titleWidget.value = newTitle.replace("GetString_", "");
} else {
// Default title if no OutputGet node
node.title = "GetString";
titleWidget.value = "";
}
});
};
// Hijack app.queuePrompt to update titles on execution
const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function () {
const graph = app.graph;
if (graph) {
updateGetStringTitles(graph);
}
return await originalQueuePrompt.apply(this, arguments);
};
// Initialize hidden widget on node addition
nodeType.prototype.onAdded = function (graph) {
ensureHiddenWidget(this);
this.updateTitle();
};
// Update title when called
nodeType.prototype.updateTitle = function () {
if (!this.graph) {
return;
}
const outputGetNode = this.findOutputGetNode(this.graph);
if (outputGetNode) {
this.title = "GetString_" + outputGetNode.title;
} else {
this.title = "GetString";
}
};
// Find the OutputGet node in the graph
nodeType.prototype.findOutputGetNode = function (graph) {
return graph._nodes.find((node) => node.type === CONFIG.NODE.name);
};
},
});
class TextDisplay extends LGraphNode {
constructor() {
super();
this.isVirtualNode = true;
this.shape = LiteGraph.BOX_SHAPE;
this.size = [200, 50];
this.resizable = true;
this.properties = {
fontSize: 24,
fontFamily: "Arial",
fontColor: "#ffffff",
textAlign: "center",
bgColor: "transparent",
padding: 10
};
this.widgets_up = true;
this.bgcolor = "#00000000";
}
onAdded(graph) {
// When added to a graph, move to end of nodes array
if (graph && graph._nodes) {
const index = graph._nodes.indexOf(this);
if (index !== -1) {
graph._nodes.splice(index, 1);
graph._nodes.push(this);
}
}
}
onDragFinished() {
// After dragging, ensure node stays at end of array
if (this.graph && this.graph._nodes) {
const index = this.graph._nodes.indexOf(this);
if (index !== -1) {
this.graph._nodes.splice(index, 1);
this.graph._nodes.push(this);
}
}
}
onDrawForeground(ctx) {
if (!this.graph) return;
// Always ensure this node is at the end of the array before drawing
const index = this.graph._nodes.indexOf(this);
if (index !== -1 && index !== this.graph._nodes.length - 1) {
this.graph._nodes.splice(index, 1);
this.graph._nodes.push(this);
}
const outputGetNode = this.findOutputGetNode(this.graph);
if (!outputGetNode || !outputGetNode.widgets[0]) {
this.displayText = "NO OUTPUTGET NODE FOUND";
} else {
this.displayText = outputGetNode.widgets[0].value || "NO SELECTION";
this.displayText = this.displayText.toUpperCase();
}
ctx.save();
ctx.textAlign = this.properties.textAlign;
ctx.textBaseline = "middle";
ctx.fillStyle = this.properties.fontColor || "#ffffff";
ctx.font = `${this.properties.fontSize}px ${this.properties.fontFamily}`;
const x = this.properties.textAlign === "center" ? this.size[0] / 2 :
this.properties.textAlign === "right" ? this.size[0] - 10 : 10;
const y = this.size[1] / 2;
ctx.fillText(this.displayText, x, y);
ctx.restore();
}
findOutputGetNode(graph) {
return graph._nodes.find((node) => node.type === "OutputGet");
}
onResize(size) {
size[0] = Math.max(50, size[0]);
size[1] = Math.max(20, size[1]);
return size;
}
onDblClick(event, pos, canvas) {
LGraphCanvas.active_canvas.showShowNodePanel(this);
}
}
// Node type registration with required static properties
TextDisplay.title = "OutputTextDisplay";
TextDisplay.title_mode = LiteGraph.NO_TITLE;
TextDisplay.collapsable = false;
const oldDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function(node, ctx) {
if (node.constructor === TextDisplay) {
const tmp_shape = node.shape;
node.shape = null;
const r = oldDrawNode.apply(this, arguments);
if (node.onDrawForeground) {
node.onDrawForeground(ctx);
}
node.shape = tmp_shape;
return r;
}
return oldDrawNode.apply(this, arguments);
};
app.registerExtension({
name: "FluxContinuum.OutputTextDisplay",
registerCustomNodes() {
LiteGraph.registerNodeType(
"OutputTextDisplay",
Object.assign(TextDisplay, {
title: "OutputTextDisplay",
title_mode: LiteGraph.NO_TITLE,
category: "Flux-Continuum/Utilities",
comfyClass: "OutputTextDisplay",
"@fontSize": { type: "number" },
"@fontFamily": { type: "string" },
"@fontColor": { type: "string", default: "#ffffff" },
"@bgColor": { type: "string", default: "#00000000" },
"@textAlign": { type: "combo", values: ["left", "center", "right"] },
"@padding": { type: "number" }
})
);
TextDisplay.prototype.flags = { no_title: true };
}
});
================================================
FILE: web/samplerversions.js
================================================
import { app } from "../../../scripts/app.js";
// Configuration Constants
const TAB_CONFIG = {
width: 40,
height: 15,
fontSize: 10,
normalColor: "#0d0d0d",
selectedColor: "#666666",
textColor: "white",
borderRadius: 4,
spacing: 10,
offset: 16,
labels: ["1", "2", "3"], // 3 tabs
yPosition: 6
};
app.registerExtension({
name: "FluxContinuum.SamplerParameterPacker",
nodeCreated(node) {
// Only apply to SamplerParameterPacker nodes
if (node.type !== "SamplerParameterPacker" && node.comfyClass !== "SamplerParameterPacker") return;
// Preserve original methods
const originalOnNodeCreated = node.onNodeCreated;
const originalOnDrawForeground = node.onDrawForeground;
const originalGetBounding = node.getBounding;
const originalOnSerialize = node.onSerialize;
const originalOnConfigure = node.onConfigure;
const originalOnMouseDown = node.onMouseDown; // IMPORTANT: Store the original onMouseDown
node.onNodeCreated = function() {
if (originalOnNodeCreated) {
originalOnNodeCreated.apply(this, arguments);
}
// Initialize tab state and content
this.activeTab = 0;
this.tabContents = TAB_CONFIG.labels.map(() => ({
sampler: this.widgets[0].value,
scheduler: this.widgets[1].value
}));
// Store widget references
this.samplerWidget = this.widgets[0];
this.schedulerWidget = this.widgets[1];
// Add change listeners to widgets
this.samplerWidget.callback = () => {
this.tabContents[this.activeTab].sampler = this.samplerWidget.value;
};
this.schedulerWidget.callback = () => {
this.tabContents[this.activeTab].scheduler = this.schedulerWidget.value;
};
};
node.onDrawForeground = function(ctx) {
if (originalOnDrawForeground) {
originalOnDrawForeground.apply(this, arguments);
}
if (this.flags.collapsed) return;
ctx.save();
// Draw tabs
TAB_CONFIG.labels.forEach((label, i) => {
const x = TAB_CONFIG.offset + (TAB_CONFIG.width + TAB_CONFIG.spacing) * i;
const y = TAB_CONFIG.yPosition;
// Draw tab background
ctx.fillStyle = i === this.activeTab ? TAB_CONFIG.selectedColor : TAB_CONFIG.normalColor;
ctx.beginPath();
ctx.roundRect(x, y, TAB_CONFIG.width, TAB_CONFIG.height, TAB_CONFIG.borderRadius);
ctx.fill();
// Draw tab text
ctx.fillStyle = TAB_CONFIG.textColor;
ctx.font = `${TAB_CONFIG.fontSize}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x + TAB_CONFIG.width / 2, y + TAB_CONFIG.height / 2);
});
ctx.restore();
};
node.onMouseDown = function(event, local_pos, graphCanvas) {
const [x, y] = local_pos;
const { yPosition, height, width, spacing, offset, labels } = TAB_CONFIG;
// Check if click is in tab area
if (y >= yPosition && y <= yPosition + height) {
for (let i = 0; i < labels.length; i++) {
const tabX = offset + (width + spacing) * i;
if (x >= tabX && x <= tabX + width) {
if (i === this.activeTab) return false;
// Save current widget values to current tab
this.tabContents[this.activeTab] = {
sampler: this.samplerWidget.value,
scheduler: this.schedulerWidget.value
};
// Switch tab
this.activeTab = i;
// Load content from new tab
this.samplerWidget.value = this.tabContents[i].sampler;
this.schedulerWidget.value = this.tabContents[i].scheduler;
this.setDirtyCanvas(true);
return true;
}
}
}
// IMPORTANT: Call the original onMouseDown if we didn't handle the click
if (originalOnMouseDown) {
return originalOnMouseDown.apply(this, arguments);
}
return false;
};
node.getBounding = function() {
const bounds = originalGetBounding ? originalGetBounding.apply(this, arguments) : [0, 0, 200, 100];
const tabsHeight = Math.abs(TAB_CONFIG.yPosition) + TAB_CONFIG.height;
bounds[1] -= tabsHeight; // Extend top boundary to include tabs
bounds[3] += tabsHeight; // Add tab height to total height
return bounds;
};
node.onSerialize = function(o) {
if (originalOnSerialize) {
originalOnSerialize.apply(this, arguments);
}
o.tabContents = this.tabContents;
o.activeTab = this.activeTab;
};
node.onConfigure = function(o) {
if (originalOnConfigure) {
originalOnConfigure.apply(this, arguments);
}
if (o.tabContents && Array.isArray(o.tabContents)) {
// Ensure tabContents length matches number of tabs
this.tabContents = TAB_CONFIG.labels.map((_, i) =>
o.tabContents[i] || {
sampler: this.samplerWidget.value,
scheduler: this.schedulerWidget.value
}
);
this.activeTab = o.activeTab >= 0 && o.activeTab < TAB_CONFIG.labels.length ? o.activeTab : 0;
// Load the active tab's content
if (this.samplerWidget && this.schedulerWidget) {
this.samplerWidget.value = this.tabContents[this.activeTab].sampler;
this.schedulerWidget.value = this.tabContents[this.activeTab].scheduler;
}
}
};
}
});
================================================
FILE: web/tabs.js
================================================
import { app } from "../../../scripts/app.js";
/**
* Constants for node and tab dimensions and appearance
* @readonly
*/
const NODE_METRICS = {
TITLE_HEIGHT: 30,
TAB_HEIGHT: 48,
DEFAULT_TAB_X_OFFSET: 10,
DEFAULT_SECOND_TAB_OFFSET: 80,
MIN_TAB_SPACING: 5 // Minimum spacing between tabs
};
/**
* Style configuration for tabs
* @readonly
*/
const TAB_STYLE = {
defaultWidth: 65,
minWidth: 30, // Minimum allowed tab width
maxWidth: 200, // Maximum allowed tab width
height: 18,
fontSize: 9,
normalColor: "#5a5a5a",
textColor: "white",
borderRadius: 4,
fontFamily: "'Segoe UI', Arial, sans-serif"
};
/**
* Utility class for bounding box calculations
* Optimizes memory usage by reusing a single Float32Array
*/
class BoundingBoxCalculator {
constructor() {
this.boundingBoxBuffer = new Float32Array(4);
}
/**
* Calculate the bounding box containing all provided rectangles
* @param {Array<{x: number, y: number, width: number, height: number}>} rects - Array of rectangles
* @returns {Float32Array} Bounding box as [x, y, width, height]
*/
calculate(rects) {
if (!rects || rects.length === 0) {
this.boundingBoxBuffer.set([0, 0, 0, 0]);
return this.boundingBoxBuffer;
}
let minX = rects[0].x;
let minY = rects[0].y;
let maxX = rects[0].x + rects[0].width;
let maxY = rects[0].y + rects[0].height;
for (let i = 1; i < rects.length; i++) {
const rect = rects[i];
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
this.boundingBoxBuffer.set([minX, minY, maxX - minX, maxY - minY]);
return this.boundingBoxBuffer;
}
}
// Create a single instance of the calculator for reuse
const boundingBoxCalculator = new BoundingBoxCalculator();
/**
* Class to handle tab interactions and rendering
*/
class TabManager {
/**
* Get the width of a tab, with validation
* @param {Object} config - Tab configuration
* @param {boolean} isSecondTab - Whether this is the second tab
* @returns {number} - The tab width
*/
static getTabWidth(config, isSecondTab = false) {
try {
if (!config) {
return TAB_STYLE.defaultWidth;
}
let width;
if (isSecondTab) {
width = config.secondTabWidth || TAB_STYLE.defaultWidth;
} else {
width = config.width || TAB_STYLE.defaultWidth;
}
// Validate width is within acceptable range
return Math.min(Math.max(width, TAB_STYLE.minWidth), TAB_STYLE.maxWidth);
} catch (error) {
console.warn("Error getting tab width:", error);
return TAB_STYLE.defaultWidth;
}
}
/**
* Check if tabs would overlap with current configuration
* @param {Object} properties - Node properties
* @returns {boolean} - True if tabs would overlap
*/
static wouldTabsOverlap(properties) {
if (!properties.hasSecondTab) return false;
const tab1End = properties.tabXOffset + properties.tabWidth;
const tab2Start = properties.secondTabOffset;
return tab2Start < tab1End + NODE_METRICS.MIN_TAB_SPACING;
}
/**
* Draw a tab on the canvas
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - X position
* @param {number} y - Y position
* @param {string} text - Tab text
* @param {number} width - Tab width
*/
static drawTab(ctx, x, y, text, width) {
const { height, fontSize, normalColor, textColor, borderRadius, fontFamily } = TAB_STYLE;
ctx.save();
ctx.fillStyle = normalColor;
// Draw tab background
ctx.beginPath();
ctx.moveTo(x + borderRadius, y);
ctx.lineTo(x + width - borderRadius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + borderRadius);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y + borderRadius);
ctx.quadraticCurveTo(x, y, x + borderRadius, y);
ctx.closePath();
ctx.fill();
// Text rendering with optimizations
ctx.fillStyle = textColor;
ctx.textRendering = 'optimizeLegibility';
ctx.imageSmoothingEnabled = true;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Truncate text if it's too long for the tab
let displayText = text;
const textWidth = ctx.measureText(text).width;
if (textWidth > width - 10) {
// Create ellipsis if text is too long
while (ctx.measureText(displayText + "...").width > width - 10 && displayText.length > 0) {
displayText = displayText.slice(0, -1);
}
displayText += "...";
}
ctx.fillText(displayText, x + width / 2, y + height / 2);
ctx.restore();
}
/**
* Handle tab click to reorder the node
* @param {LiteGraph.LGraphNode} node - The node being clicked
* @param {boolean} toFront - Whether to bring the node to front
* @returns {Function} - Cleanup function
*/
static handleNodeOrder(node, toFront = true) {
const { graph } = node;
if (!graph?._nodes) return null;
const index = graph._nodes.indexOf(node);
if (index === -1) return null;
let frameId;
const handleDeselection = () => {
frameId = requestAnimationFrame(() => {
app.canvas.deselectNode(node);
node.setDirtyCanvas(true, true);
});
};
// Reorder the node
graph._nodes.splice(index, 1);
if (toFront) {
graph._nodes.push(node);
} else {
graph._nodes.unshift(node);
}
handleDeselection();
// Return cleanup function
return () => {
if (frameId) {
cancelAnimationFrame(frameId);
}
};
}
}
/**
* Helper function to check if a point is inside a rectangle
* @param {number} x - Point X coordinate
* @param {number} y - Point Y coordinate
* @param {number} rx - Rectangle X coordinate
* @param {number} ry - Rectangle Y coordinate
* @param {number} rw - Rectangle width
* @param {number} rh - Rectangle height
* @returns {boolean} - True if point is inside rectangle
*/
const isInsideRect = (x, y, rx, ry, rw, rh) => (
x >= rx && x <= rx + rw && y >= ry && y <= ry + rh
);
/**
* Main class to handle node tab functionality
*/
class NodeWithTabs {
/**
* Initialize a node with tab properties
* @param {LiteGraph.LGraphNode} node - The node being created
*/
static onNodeCreated(node) {
// Add properties to the node when it's created
node.addProperty("enableTabs", false, "boolean", "Enable tabs above the node");
node.addProperty("tabWidth", TAB_STYLE.defaultWidth, "number", "Width of the main tab");
node.addProperty("tabXOffset", NODE_METRICS.DEFAULT_TAB_X_OFFSET, "number", "X offset for the main tab");
node.addProperty("hasSecondTab", false, "boolean", "Enable a second tab");
node.addProperty("secondTabText", "Send Back", "string", "Text for the second tab");
node.addProperty("secondTabOffset", NODE_METRICS.DEFAULT_SECOND_TAB_OFFSET, "number", "X offset for second tab");
node.addProperty("secondTabWidth", TAB_STYLE.defaultWidth, "number", "Width of the second tab");
// Add a method to validate tab configuration
node.validateTabConfiguration = function() {
// Ensure tabs don't overlap
if (TabManager.wouldTabsOverlap(this.properties)) {
this.properties.secondTabOffset = this.properties.tabXOffset +
this.properties.tabWidth + NODE_METRICS.MIN_TAB_SPACING;
}
// Validate tab widths
this.properties.tabWidth = TabManager.getTabWidth({ width: this.properties.tabWidth });
this.properties.secondTabWidth = TabManager.getTabWidth(
{ secondTabWidth: this.properties.secondTabWidth },
true
);
};
}
}
/**
* ComfyUI NodeTabExtension
*
* This extension adds customizable tabs to ComfyUI nodes.
* Features:
* - Add one or two tabs to a node
* - Customize tab text, width, and position
* - Click tabs to bring node to front or send to back
*
* Usage:
* 1. Enable tabs on a node by setting the "enableTabs" property to true
* 2. Customize the main tab width and position with "tabWidth" and "tabXOffset"
* 3. Enable a second tab with "hasSecondTab" and customize with "secondTabText",
* "secondTabOffset", and "secondTabWidth"
*/
app.registerExtension({
name: "FluxContinuum.NodeTabExtension",
// Using the modern nodeCreated hook that works with the latest ComfyUI version
nodeCreated(node) {
try {
// Store original methods to call them when appropriate
const originalMethods = {
onNodeCreated: node.onNodeCreated,
getBounding: node.getBounding,
isPointInside: node.isPointInside,
onMouseDown: node.onMouseDown,
onMouseUp: node.onMouseUp,
onDrawForeground: node.onDrawForeground,
onPropertyChanged: node.onPropertyChanged
};
// Override onNodeCreated method
node.onNodeCreated = function() {
if (originalMethods.onNodeCreated) {
originalMethods.onNodeCreated.apply(this, arguments);
}
NodeWithTabs.onNodeCreated(this);
};
// Call onNodeCreated immediately if the node is already created
if (node.flags && !node._tabsInitialized) {
NodeWithTabs.onNodeCreated(node);
node._tabsInitialized = true;
}
// Add support for property validation
node.onPropertyChanged = function(property, value) {
if (originalMethods.onPropertyChanged) {
originalMethods.onPropertyChanged.call(this, property, value);
}
// If a tab-related property changed, validate the configuration
const tabProperties = [
"tabWidth", "tabXOffset", "hasSecondTab",
"secondTabOffset", "secondTabWidth"
];
if (tabProperties.includes(property)) {
this.validateTabConfiguration();
}
};
/**
* Get the bounding box for the node including tabs
*/
node.getBounding = function(out) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.getBounding.call(this, out);
}
// Validate tab configuration
this.validateTabConfiguration();
out = out || new Float32Array(4);
const [width, height] = this.size;
const [nodeX, nodeY] = this.pos;
const rects = [
// Body and title
{
x: nodeX,
y: nodeY - NODE_METRICS.TITLE_HEIGHT,
width,
height: height + NODE_METRICS.TITLE_HEIGHT
},
// Main tab
{
x: nodeX + this.properties.tabXOffset,
y: nodeY - NODE_METRICS.TAB_HEIGHT,
width: this.properties.tabWidth,
height: TAB_STYLE.height
}
];
if (this.properties.hasSecondTab) {
rects.push({
x: nodeX + this.properties.secondTabOffset,
y: nodeY - NODE_METRICS.TAB_HEIGHT,
width: this.properties.secondTabWidth,
height: TAB_STYLE.height
});
}
// Calculate the bounding box
const result = boundingBoxCalculator.calculate(rects);
// Copy values to the out parameter
out.set(result);
return out;
};
/**
* Check if a point is inside the node or its tabs
*/
node.isPointInside = function(x, y, margin = 0) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.isPointInside.call(this, x, y, margin);
}
// Validate tab configuration
this.validateTabConfiguration();
const [nodeX, nodeY] = this.pos;
const [width, height] = this.size;
// During rectangle selection, use a simpler hit test that includes the margin
if (margin > 0) {
const boundingBox = this.getBounding();
return isInsideRect(
x, y,
boundingBox[0] - margin,
boundingBox[1] - margin,
boundingBox[2] + 2 * margin,
boundingBox[3] + 2 * margin
);
}
// For regular clicking, keep the detailed hit testing
const bodyAndTitle = isInsideRect(x, y,
nodeX, nodeY - NODE_METRICS.TITLE_HEIGHT,
width, height + NODE_METRICS.TITLE_HEIGHT
);
if (bodyAndTitle) return true;
const mainTabHit = isInsideRect(x, y,
nodeX + this.properties.tabXOffset,
nodeY - NODE_METRICS.TAB_HEIGHT,
this.properties.tabWidth,
TAB_STYLE.height
);
if (mainTabHit) return true;
if (this.properties.hasSecondTab) {
return isInsideRect(x, y,
nodeX + this.properties.secondTabOffset,
nodeY - NODE_METRICS.TAB_HEIGHT,
this.properties.secondTabWidth,
TAB_STYLE.height
);
}
return false;
};
/**
* Handle mouse down event on the node or its tabs
*/
node.onMouseDown = function(event, local_pos, graphCanvas) {
if (!this.properties?.enableTabs || this.flags.collapsed) {
return originalMethods.onMouseDown?.call(this, event, local_pos, graphCanvas) ?? false;
}
// Validate tab configuration
this.validateTabConfiguration();
const [localX, localY] = local_pos;
let tabHandled = false;
let cleanup = null;
// Check main tab click
const mainTabHit = isInsideRect(localX, localY,
this.properties.tabXOffset, -NODE_METRICS.TAB_HEIGHT,
this.properties.tabWidth, TAB_STYLE.height
);
if (mainTabHit) {
cleanup = TabManager.handleNodeOrder(this, true);
tabHandled = true;
}
// Check second tab click
if (!tabHandled && this.properties.hasSecondTab) {
const secondTabHit = isInsideRect(localX, localY,
this.properties.secondTabOffset, -NODE_METRICS.TAB_HEIGHT,
this.properties.secondTabWidth, TAB_STYLE.height
);
if (secondTabHit) {
cleanup = TabManager.handleNodeOrder(this, false);
tabHandled = true;
}
}
// If tab was handled, return early
if (tabHandled) {
// Store cleanup function to be called later
this._tabCleanup = cleanup;
return true;
}
// If no tab was clicked, delegate to original handler
const originalResult = originalMethods.onMouseDown?.call(this, event, local_pos, graphCanvas) ?? false;
return originalResult;
};
/**
* Clean up any pending tab operations when mouse is released
*/
node.onMouseUp = function(event) {
// Call any pending tab cleanup
if (this._tabCleanup) {
this._tabCleanup();
this._tabCleanup = null;
}
// Call original handler
if (originalMethods.onMouseUp) {
return originalMethods.onMouseUp.call(this, event);
}
};
/**
* Draw the tabs above the node
*/
node.onDrawForeground = function(ctx) {
// Call original method first
if (originalMethods.onDrawForeground) {
originalMethods.onDrawForeground.call(this, ctx);
}
if (!this.properties?.enableTabs || this.flags.collapsed) {
return;
}
// Validate tab configuration
this.validateTabConfiguration();
ctx.save();
const baseY = -NODE_METRICS.TAB_HEIGHT;
// Draw main tab
TabManager.drawTab(ctx, this.properties.tabXOffset, baseY, this.title, this.properties.tabWidth);
// Draw second tab if enabled
if (this.properties.hasSecondTab) {
TabManager.drawTab(ctx, this.properties.secondTabOffset, baseY,
this.properties.secondTabText, this.properties.secondTabWidth);
}
ctx.restore();
};
} catch (error) {
console.error("NodeTabExtension: Error initializing node", error);
}
}
});
================================================
FILE: web/textversions.js
================================================
import { app } from "../../../scripts/app.js";
/**
* Default and limit configurations
*/
const CONFIG = {
DEFAULT_VERSION_AMOUNT: 5,
MAX_VERSION_AMOUNT: 100,
MIN_VERSION_AMOUNT: 1
};
/**
* Visual style configuration for version tabs
*/
const TAB_STYLE = {
width: 40,
height: 18,
fontSize: 10,
normalColor: "#333333",
selectedColor: "#666666",
textColor: "white",
borderRadius: 4,
spacing: 10,
offset: 10,
yPosition: 10
};
function createTabConfig(numVersions) {
return {
...TAB_STYLE,
labels: Array.from({length: numVersions}, (_, i) => (i + 1).toString())
};
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
app.registerExtension({
name: "FluxContinuum.TextVersions",
// Modern API uses nodeCreated instead of beforeRegisterNodeDef
nodeCreated(node) {
// Only apply to TextVersions nodes
if (node.type !== "TextVersions" && node.comfyClass !== "TextVersions") return;
// Store original methods
const onNodeCreated = node.onNodeCreated;
const onDrawBackground = node.onDrawBackground;
const getBounding = node.getBounding;
const onSerialize = node.onSerialize;
const onConfigure = node.onConfigure;
const onMouseDown = node.onMouseDown; // IMPORTANT: Store the original onMouseDown
// Add onNodeCreated to the node
node.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Add properties
this.addProperty("versionAmount", CONFIG.DEFAULT_VERSION_AMOUNT, "number");
// Initialize node
if (!this.properties) {
this.properties = {};
}
this.properties.versionAmount = this.properties.versionAmount || CONFIG.DEFAULT_VERSION_AMOUNT;
// Create tab config based on number of versions
this.tabConfig = createTabConfig(this.properties.versionAmount);
// Initialize tab state and content
this.activeTab = 0;
this.tabContents = Array(this.properties.versionAmount).fill("");
// Store reference to the text widget
this.textWidget = this.widgets.find(w => w.name === "text");
// Initial content setup
if (this.textWidget) {
this.textWidget.value = this.tabContents[this.activeTab];
// Add debounced change event listener
if (this.textWidget.inputEl) {
const saveContent = debounce(() => {
this.tabContents[this.activeTab] = this.textWidget.value;
}, 300);
this.textWidget.inputEl.addEventListener("input", saveContent);
}
}
};
node.onPropertyChanged = function(name, value) {
if (name === "versionAmount") {
const newValue = Math.max(CONFIG.MIN_VERSION_AMOUNT,
Math.min(CONFIG.MAX_VERSION_AMOUNT, Math.floor(value)));
const oldContents = [...this.tabContents];
this.properties.versionAmount = newValue;
this.tabConfig = createTabConfig(newValue);
this.tabContents = Array(newValue).fill("").map((_, i) => oldContents[i] || "");
this.activeTab = Math.min(this.activeTab, newValue - 1);
if (this.textWidget) {
this.textWidget.value = this.tabContents[this.activeTab];
}
this.setDirtyCanvas(true);
}
};
node.onDrawBackground = function(ctx) {
if (onDrawBackground) {
onDrawBackground.apply(this, arguments);
}
if (this.flags.collapsed) return;
ctx.save();
// Create clipping region for overflow
const nodeWidth = this.size[0];
const clipPadding = 10;
ctx.beginPath();
ctx.rect(clipPadding, this.tabConfig.yPosition - 5,
nodeWidth - (2 * clipPadding),
this.tabConfig.height + 10);
ctx.clip();
// Draw tabs
this.tabConfig.labels.forEach((label, i) => {
const x = this.tabConfig.offset + (this.tabConfig.width + this.tabConfig.spacing) * i;
const y = this.tabConfig.yPosition;
ctx.fillStyle = i === this.activeTab ? this.tabConfig.selectedColor : this.tabConfig.normalColor;
ctx.beginPath();
ctx.roundRect(x, y, this.tabConfig.width, this.tabConfig.height, this.tabConfig.borderRadius);
ctx.fill();
ctx.fillStyle = this.tabConfig.textColor;
ctx.font = `${this.tabConfig.fontSize}px Arial`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x + this.tabConfig.width / 2, y + this.tabConfig.height / 2);
});
ctx.restore();
};
node.onMouseDown = function(event, local_pos, graphCanvas) {
const [x, y] = local_pos;
const { yPosition, height, width, spacing, offset, labels } = this.tabConfig;
if (y >= yPosition && y <= yPosition + height) {
for (let i = 0; i < labels.length; i++) {
const tabX = offset + (width + spacing) * i;
if (x >= tabX && x <= tabX + width) {
if (i === this.activeTab) return false;
if (this.textWidget) {
this.tabContents[this.activeTab] = this.textWidget.value;
}
this.activeTab = i;
if (this.textWidget) {
this.textWidget.value = this.tabContents[i];
}
this.setDirtyCanvas(true);
return true;
}
}
}
// IMPORTANT: Call the original onMouseDown if we didn't handle the click
if (onMouseDown) {
return onMouseDown.apply(this, arguments);
}
return false;
};
node.getBounding = function() {
const bounds = getBounding?.apply(this, arguments) || new Float32Array(4);
const tabsHeight = Math.abs(this.tabConfig.yPosition) + this.tabConfig.height;
bounds[1] -= tabsHeight;
bounds[3] += tabsHeight;
return bounds;
};
node.onSerialize = function(o) {
if (onSerialize) {
onSerialize.apply(this, arguments);
}
o.tabContents = this.tabContents;
o.activeTab = this.activeTab;
};
node.onConfigure = function(o) {
if (onConfigure) {
onConfigure.apply(this, arguments);
}
this.tabConfig = createTabConfig(this.properties.versionAmount);
if (o.tabContents && Array.isArray(o.tabContents)) {
this.tabContents = Array(this.properties.versionAmount).fill("").map((_, i) => o.tabContents[i] || "");
this.activeTab = o.activeTab >= 0 && o.activeTab < this.properties.versionAmount ? o.activeTab : 0;
if (this.textWidget) {
this.textWidget.value = this.tabContents[this.activeTab];
}
}
};
}
});
================================================
FILE: workflow/Flux+ 1.3_release.json
================================================
{
"last_node_id": 3156,
"last_link_id": 4684,
"nodes": [
{
"id": 1026,
"type": "ImagePass",
"pos": [
660,
220
],
"size": [
210,
30
],
"flags": {
"collapsed": true
},
"order": 154,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1744,
"shape": 7
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1745
],
"slot_index": 0,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ImagePass",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": []
},
{
"id": 1113,
"type": "ImagePass",
"pos": [
5567.8857421875,
6493.90966796875
],
"size": [
210,
30
],
"flags": {
"collapsed": true
},
"order": 158,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 4511,
"shape": 7
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1859
],
"slot_index": 0,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ImagePass",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": []
},
{
"id": 1346,
"type": "ImagePass",
"pos": [
1790,
1000
],
"size": [
140,
30
],
"flags": {
"collapsed": true
},
"order": 329,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 4607,
"shape": 7
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
2073
],
"slot_index": 0,
"shape": 3
}
],
"properties": {
"Node name for S&R": "ImagePass",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": []
},
{
"id": 644,
"type": "SetNode",
"pos": [
4493.2578125,
240.7759246826172
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 312,
"mode": 0,
"inputs": [
{
"name": "BASIC_PIPE",
"type": "BASIC_PIPE",
"link": 1187
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_pipe",
"properties": {
"previousName": "pipe"
},
"widgets_values": [
"pipe"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 712,
"type": "SetNode",
"pos": [
5569.60400390625,
1606.9547119140625
],
"size": [
210,
60
],
"flags": {
"collapsed": true
},
"order": 346,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 2030
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - inpaint",
"properties": {
"previousName": "Output - inpaint"
},
"widgets_values": [
"Output - inpaint"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 715,
"type": "SetNode",
"pos": [
5973.2578125,
220.7759552001953
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 337,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 1538
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - txt2img",
"properties": {
"previousName": "Output - txt2img"
},
"widgets_values": [
"Output - txt2img"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 716,
"type": "SetNode",
"pos": [
5776.8857421875,
6489.90966796875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 232,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 1859
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_final",
"properties": {
"previousName": "final"
},
"widgets_values": [
"final"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 723,
"type": "SetNode",
"pos": [
1431.1396484375,
1160.7176513671875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 223,
"mode": 0,
"inputs": [
{
"name": "STRING",
"type": "STRING",
"link": 3492
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_prompt",
"properties": {
"previousName": "prompt"
},
"widgets_values": [
"prompt"
]
},
{
"id": 729,
"type": "SetNode",
"pos": [
5428.25927734375,
3585.4765625
],
"size": [
252,
58
],
"flags": {
"collapsed": true
},
"order": 331,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 2177
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - ultimate upscaler",
"properties": {
"previousName": "Output - ultimate upscaler"
},
"widgets_values": [
"Output - ultimate upscaler"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 788,
"type": "SetNode",
"pos": [
1590.7579345703125,
1348.9764404296875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 224,
"mode": 0,
"inputs": [
{
"name": "STRING",
"type": "STRING",
"link": 3493
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": [],
"slot_index": 0
}
],
"title": "Set_tags",
"properties": {
"previousName": "tags"
},
"widgets_values": [
"tags"
]
},
{
"id": 832,
"type": "SetNode",
"pos": [
3613.2578125,
366.5325927734375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 205,
"mode": 0,
"inputs": [
{
"name": "VAE",
"type": "VAE",
"link": 4425
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_vae",
"properties": {
"previousName": "vae"
},
"widgets_values": [
"vae"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 873,
"type": "SetNode",
"pos": [
5448.25927734375,
4285.47607421875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 332,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 2206
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - detailer",
"properties": {
"previousName": "Output - detailer"
},
"widgets_values": [
"Output - detailer"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 925,
"type": "SetNode",
"pos": [
680,
1180
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 207,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 1589
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_seed",
"properties": {
"previousName": "seed"
},
"widgets_values": [
"seed"
]
},
{
"id": 1023,
"type": "SetNode",
"pos": [
670,
220
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 230,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 1745
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_send",
"properties": {
"previousName": "send"
},
"widgets_values": [
"send"
]
},
{
"id": 1061,
"type": "SetNode",
"pos": [
7663.2578125,
254.91954040527344
],
"size": [
302.3999938964844,
58
],
"flags": {
"collapsed": true
},
"order": 362,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 1783
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - txt2img_noise injection",
"properties": {
"previousName": "Output - txt2img_noise injection"
},
"widgets_values": [
"Output - txt2img_noise injection"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1075,
"type": "SetNode",
"pos": [
4695.26611328125,
999.5366821289062
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 313,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 1802
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - img2img",
"properties": {
"previousName": "Output - img2img"
},
"widgets_values": [
"Output - img2img"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1095,
"type": "SetNode",
"pos": [
480,
1070
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 211,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 2305
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_batch",
"properties": {
"previousName": "batch"
},
"widgets_values": [
"batch"
]
},
{
"id": 1105,
"type": "SetNode",
"pos": [
3620,
220
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 225,
"mode": 0,
"inputs": [
{
"name": "MODEL",
"type": "MODEL",
"link": 3494
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_model",
"properties": {
"previousName": "model"
},
"widgets_values": [
"model"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1114,
"type": "SetNode",
"pos": [
3620,
500
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 204,
"mode": 0,
"inputs": [
{
"name": "CLIP",
"type": "CLIP",
"link": 4424
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_clip",
"properties": {
"previousName": "clip"
},
"widgets_values": [
"clip"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1116,
"type": "SetNode",
"pos": [
650,
350
],
"size": [
285.375,
58
],
"flags": {
"collapsed": true
},
"order": 233,
"mode": 0,
"inputs": [
{
"name": "MODEL",
"type": "MODEL",
"link": 1863
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_loras",
"properties": {
"previousName": "loras"
},
"widgets_values": [
"loras"
]
},
{
"id": 1217,
"type": "SetNode",
"pos": [
1940,
650
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 208,
"mode": 0,
"inputs": [
{
"name": "FLOAT",
"type": "FLOAT",
"link": 2000
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_denoise",
"properties": {
"previousName": "denoise"
},
"widgets_values": [
"denoise"
]
},
{
"id": 1271,
"type": "SetNode",
"pos": [
4135.2470703125,
6430.484375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 254,
"mode": 0,
"inputs": [
{
"name": "COMBO",
"type": "COMBO",
"link": 3464
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_scheduler",
"properties": {
"previousName": "scheduler"
},
"widgets_values": [
"scheduler"
]
},
{
"id": 1272,
"type": "SetNode",
"pos": [
4135.2470703125,
6540.484375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 255,
"mode": 0,
"inputs": [
{
"name": "STRING",
"type": "STRING",
"link": 3466
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_scheduler_name",
"properties": {
"previousName": "scheduler_name"
},
"widgets_values": [
"scheduler_name"
]
},
{
"id": 1280,
"type": "SetNode",
"pos": [
670,
750
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 209,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 2002
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_steps",
"properties": {
"previousName": "steps"
},
"widgets_values": [
"steps"
]
},
{
"id": 1281,
"type": "SetNode",
"pos": [
650,
860
],
"size": [
210,
60
],
"flags": {
"collapsed": true
},
"order": 210,
"mode": 0,
"inputs": [
{
"name": "FLOAT",
"type": "FLOAT",
"link": 2001
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_guidance",
"properties": {
"previousName": "guidance"
},
"widgets_values": [
"guidance"
]
},
{
"id": 1320,
"type": "SetNode",
"pos": [
1920,
240
],
"size": [
210,
60
],
"flags": {
"collapsed": true,
"pinned": true
},
"order": 229,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 4446
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": [],
"slot_index": 0
}
],
"title": "Set_loadImage",
"properties": {
"previousName": "loadImage"
},
"widgets_values": [
"loadImage"
]
},
{
"id": 1321,
"type": "SetNode",
"pos": [
1940,
300
],
"size": [
210,
60
],
"flags": {
"collapsed": true,
"pinned": true
},
"order": 227,
"mode": 0,
"inputs": [
{
"name": "MASK",
"type": "MASK",
"link": 4515
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_mask",
"properties": {
"previousName": "mask"
},
"widgets_values": [
"mask"
]
},
{
"id": 1330,
"type": "SetNode",
"pos": [
1881.1695556640625,
1443.02587890625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 212,
"mode": 0,
"inputs": [
{
"name": "UPSCALE_MODEL",
"type": "UPSCALE_MODEL",
"link": 2361
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_upscale_model",
"properties": {
"previousName": ""
},
"widgets_values": [
"upscale_model"
]
},
{
"id": 1331,
"type": "SetNode",
"pos": [
1920,
1300
],
"size": [
210,
58
],
"flags": {
"collapsed": true,
"pinned": true
},
"order": 215,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 3215
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_res multiply",
"properties": {
"previousName": "res multiply"
},
"widgets_values": [
"res multiply"
]
},
{
"id": 1345,
"type": "SetNode",
"pos": [
1800,
1050
],
"size": [
268.79998779296875,
58
],
"flags": {
"collapsed": true
},
"order": 342,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 2073
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - imgload_prep",
"properties": {
"previousName": "Output - imgload_prep"
},
"widgets_values": [
"Output - imgload_prep"
]
},
{
"id": 1392,
"type": "SetNode",
"pos": [
4448.25927734375,
3765.4775390625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 294,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 3433
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - upscaler",
"properties": {
"previousName": "Output - upscaler"
},
"widgets_values": [
"Output - upscaler"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1508,
"type": "SetNode",
"pos": [
4135.2470703125,
6470.48388671875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 252,
"mode": 0,
"inputs": [
{
"name": "COMBO",
"type": "COMBO",
"link": 3463
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_sampler",
"properties": {
"previousName": "sampler"
},
"widgets_values": [
"sampler"
]
},
{
"id": 1510,
"type": "SetNode",
"pos": [
4135.2470703125,
6500.48388671875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 253,
"mode": 0,
"inputs": [
{
"name": "STRING",
"type": "STRING",
"link": 3465
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_sampler_name",
"properties": {
"previousName": "sampler_name"
},
"widgets_values": [
"sampler_name"
]
},
{
"id": 1518,
"type": "SetNode",
"pos": [
5603.2578125,
600.77587890625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 240,
"mode": 0,
"inputs": [
{
"name": "SAMPLER",
"type": "SAMPLER",
"link": 2335
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_sampler_convert",
"properties": {
"previousName": "sampler_convert"
},
"widgets_values": [
"sampler_convert"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 1617,
"type": "SetNode",
"pos": [
650,
940
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 213,
"mode": 0,
"inputs": [
{
"name": "FLOAT",
"type": "FLOAT",
"link": 2418
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_max shift",
"properties": {
"previousName": "max shift"
},
"widgets_values": [
"max shift"
]
},
{
"id": 2122,
"type": "SetNode",
"pos": [
6098.259765625,
6450.18798828125
],
"size": [
319.20001220703125,
58
],
"flags": {
"collapsed": true
},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "*",
"type": "*",
"link": null
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output END -----------------------",
"properties": {
"previousName": "Output END -----------------------"
},
"widgets_values": [
"Output END -----------------------"
]
},
{
"id": 2129,
"type": "SetNode",
"pos": [
6098.259765625,
6500.18798828125
],
"size": [
310.79998779296875,
58
],
"flags": {
"collapsed": true
},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "*",
"type": "*",
"link": null
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_--------- SELECT Output ---------",
"properties": {
"previousName": "--------- SELECT Output ---------"
},
"widgets_values": [
"--------- SELECT Output ---------"
]
},
{
"id": 2273,
"type": "SetNode",
"pos": [
7498.462890625,
5117.22998046875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 352,
"mode": 0,
"inputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"link": 3356
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_cond_cnApplied",
"properties": {
"previousName": "cond_cnApplied"
},
"widgets_values": [
"cond_cnApplied"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2285,
"type": "SetNode",
"pos": [
5773.65234375,
6065.1103515625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 350,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 3295
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - preprocessor",
"properties": {
"previousName": "Output - preprocessor"
},
"widgets_values": [
"Output - preprocessor"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2287,
"type": "SetNode",
"pos": [
5293.65283203125,
5635.1103515625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 325,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 3296
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4032
],
"slot_index": 0
}
],
"title": "Set_pre_OpenPose",
"properties": {
"previousName": "pre_OpenPose"
},
"widgets_values": [
"pre_OpenPose"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2288,
"type": "SetNode",
"pos": [
5273.65283203125,
5915.1103515625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 324,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 3297
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4031
],
"slot_index": 0
}
],
"title": "Set_pre_Depth",
"properties": {
"previousName": "pre_Depth"
},
"widgets_values": [
"pre_Depth"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2289,
"type": "SetNode",
"pos": [
5274.7216796875,
6064.35986328125
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 323,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 4026
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4030
],
"slot_index": 0
}
],
"title": "Set_pre_Lineart",
"properties": {
"previousName": "pre_Lineart"
},
"widgets_values": [
"pre_Lineart"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2290,
"type": "SetNode",
"pos": [
1831.615234375,
804.8087768554688
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 216,
"mode": 0,
"inputs": [
{
"name": "VEC3",
"type": "VEC3",
"link": 3299
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_cn_slider_depth",
"properties": {
"previousName": "cn_slider_depth"
},
"widgets_values": [
"cn_slider_depth"
]
},
{
"id": 2311,
"type": "SetNode",
"pos": [
1791.615234375,
804.8087768554688
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 218,
"mode": 0,
"inputs": [
{
"name": "VEC3",
"type": "VEC3",
"link": 3347
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_cn_slider_lineart",
"properties": {
"previousName": "cn_slider_lineart"
},
"widgets_values": [
"cn_slider_lineart"
]
},
{
"id": 2313,
"type": "SetNode",
"pos": [
1811.615234375,
804.8087768554688
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 219,
"mode": 0,
"inputs": [
{
"name": "VEC3",
"type": "VEC3",
"link": 3348
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_cn_slider_pose",
"properties": {
"previousName": "cn_slider_pose"
},
"widgets_values": [
"cn_slider_pose"
]
},
{
"id": 2315,
"type": "SetNode",
"pos": [
1900,
798.9000244140625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 220,
"mode": 0,
"inputs": [
{
"name": "VEC3",
"type": "VEC3",
"link": 3349
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_cn_slider_tile",
"properties": {
"previousName": "cn_slider_tile"
},
"widgets_values": [
"cn_slider_tile"
]
},
{
"id": 2342,
"type": "SetNode",
"pos": [
7490,
5368
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 353,
"mode": 0,
"inputs": [
{
"name": "SEGS",
"type": "SEGS",
"link": 3403
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_segs_cnApplied",
"properties": {
"previousName": "segs_cnApplied"
},
"widgets_values": [
"segs_cnApplied"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2428,
"type": "SetNode",
"pos": [
650,
1350
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 221,
"mode": 0,
"inputs": [
{
"name": "COMBO",
"type": "COMBO",
"link": 3447
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_resolution",
"properties": {
"previousName": "resolution"
},
"widgets_values": [
"resolution"
]
},
{
"id": 2437,
"type": "SetNode",
"pos": [
610,
620
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 222,
"mode": 0,
"inputs": [
{
"name": "SAMPLER_PARAMS",
"type": "SAMPLER_PARAMS",
"link": 3460
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_sampler params",
"properties": {
"previousName": "sampler params"
},
"widgets_values": [
"sampler params"
]
},
{
"id": 2475,
"type": "SetNode",
"pos": [
3610,
590
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 217,
"mode": 0,
"inputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"link": 3505
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_controlnet_model",
"properties": {
"previousName": "controlnet_model"
},
"widgets_values": [
"controlnet_model"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2507,
"type": "SetNode",
"pos": [
4623.2578125,
550.7760620117188
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 249,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 3543
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_width",
"properties": {
"previousName": "width"
},
"widgets_values": [
"width"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2508,
"type": "SetNode",
"pos": [
4623.2578125,
600.77587890625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 251,
"mode": 0,
"inputs": [
{
"name": "INT",
"type": "INT",
"link": 3544
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_height",
"properties": {
"previousName": "height"
},
"widgets_values": [
"height"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 2784,
"type": "SetNode",
"pos": [
1900,
350
],
"size": [
210,
60
],
"flags": {
"collapsed": true
},
"order": 228,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 4469
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_loadImage_cn",
"properties": {
"previousName": "loadImage_cn"
},
"widgets_values": [
"loadImage_cn"
]
},
{
"id": 3032,
"type": "SetNode",
"pos": [
4623.2578125,
500.77618408203125
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 248,
"mode": 0,
"inputs": [
{
"name": "LATENT",
"type": "LATENT",
"link": 4514
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_latent",
"properties": {
"previousName": "latent"
},
"widgets_values": [
"latent"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 3057,
"type": "SetNode",
"pos": [
5767.1435546875,
2728.443115234375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 339,
"mode": 0,
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"link": 4543
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_Output - outpaint",
"properties": {
"previousName": "Output - outpaint"
},
"widgets_values": [
"Output - outpaint"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 3094,
"type": "SetNode",
"pos": [
3613.2578125,
696.5328979492188
],
"size": [
260.3999938964844,
60
],
"flags": {
"collapsed": true
},
"order": 226,
"mode": 0,
"inputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"link": 4592
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_controlnet_model_inpainting",
"properties": {
"previousName": "controlnet_model_inpainting"
},
"widgets_values": [
"controlnet_model_inpainting"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 3103,
"type": "SetNode",
"pos": [
1830,
1120
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 330,
"mode": 0,
"inputs": [
{
"name": "MASK",
"type": "MASK",
"link": 4608
}
],
"outputs": [
{
"name": "*",
"type": "*",
"links": null
}
],
"title": "Set_mask_outpainting",
"properties": {
"previousName": "mask_outpainting"
},
"widgets_values": [
"mask_outpainting"
]
},
{
"id": 596,
"type": "GetNode",
"pos": [
5313.2578125,
370.7759704589844
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
1360
],
"slot_index": 0
}
],
"title": "Get_seed",
"properties": {},
"widgets_values": [
"seed"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 645,
"type": "GetNode",
"pos": [
4718.25927734375,
4355.474609375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "BASIC_PIPE",
"type": "BASIC_PIPE",
"links": [
3152
],
"slot_index": 0
}
],
"title": "Get_pipe",
"properties": {},
"widgets_values": [
"pipe"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 708,
"type": "GetNode",
"pos": [
4928.25927734375,
3715.477294921875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
2173
],
"slot_index": 0
}
],
"title": "Get_seed",
"properties": {},
"widgets_values": [
"seed"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 713,
"type": "GetNode",
"pos": [
480,
200
],
"size": [
310,
60
],
"flags": {
"collapsed": false,
"pinned": true
},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1744
],
"slot_index": 0
}
],
"title": "Get_Output - txt2img",
"properties": {},
"widgets_values": [
"Output - txt2img"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 717,
"type": "GetNode",
"pos": [
1000,
150
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 6,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1372,
3426,
4414
],
"slot_index": 0
}
],
"title": "Get_final",
"properties": {},
"widgets_values": [
"final"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 724,
"type": "GetNode",
"pos": [
3953.2578125,
590.7759399414062
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 7,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
1442
],
"slot_index": 0
}
],
"title": "Get_prompt",
"properties": {},
"widgets_values": [
"prompt"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 726,
"type": "GetNode",
"pos": [
4839.33056640625,
6439.1826171875
],
"size": [
313.42822265625,
58
],
"flags": {
"collapsed": true
},
"order": 8,
"mode": 4,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
2380
],
"slot_index": 0
}
],
"title": "Get_final",
"properties": {},
"widgets_values": [
"final"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 793,
"type": "GetNode",
"pos": [
3963.2578125,
550.7760620117188
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 9,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
1443
],
"slot_index": 0
}
],
"title": "Get_tags",
"properties": {},
"widgets_values": [
"tags"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 933,
"type": "GetNode",
"pos": [
4533.8857421875,
3681.912841796875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 10,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "BASIC_PIPE",
"type": "BASIC_PIPE",
"links": [
1599
]
}
],
"title": "Get_pipe",
"properties": {},
"widgets_values": [
"pipe"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 965,
"type": "GetNode",
"pos": [
3725.26708984375,
999.5366821289062
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 11,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
1680,
1804
],
"slot_index": 0
}
],
"title": "Get_vae",
"properties": {},
"widgets_values": [
"vae"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1022,
"type": "GetNode",
"pos": [
5396.8857421875,
6493.90966796875
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 12,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4511
],
"slot_index": 0
}
],
"title": "Get_send",
"properties": {},
"widgets_values": [
"send"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1057,
"type": "GetNode",
"pos": [
6263.2578125,
274.9195251464844
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 13,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
1775
],
"slot_index": 0
}
],
"title": "Get_seed",
"properties": {},
"widgets_values": [
"seed"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1064,
"type": "GetNode",
"pos": [
7333.2578125,
214.91949462890625
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 14,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
1786
]
}
],
"title": "Get_vae",
"properties": {},
"widgets_values": [
"vae"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1079,
"type": "GetNode",
"pos": [
4105.26611328125,
1049.53662109375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 15,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
3363
],
"slot_index": 0
}
],
"title": "Get_loras",
"properties": {},
"widgets_values": [
"loras"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1086,
"type": "GetNode",
"pos": [
3965.26611328125,
1219.53662109375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 16,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
1813
],
"slot_index": 0
}
],
"title": "Get_seed",
"properties": {},
"widgets_values": [
"seed"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1099,
"type": "GetNode",
"pos": [
6333.2578125,
404.919677734375
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 17,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2105
],
"slot_index": 0
}
],
"title": "Get_latent",
"properties": {},
"widgets_values": [
"latent"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 1106,
"type": "GetNode",
"pos": [
650,
350
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 18,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
3092
],
"slot_index": 0
}
],
"title": "Get_model",
"properties": {},
"widgets_values": [
"model"
]
},
{
"id": 1115,
"type": "GetNode",
"pos": [
660,
350
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 19,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CLIP",
"type": "CLIP",
"links": [
3432
],
"slot_index": 0
}
],
"title": "Get_clip",
"properties": {},
"widgets_values": [
"clip"
]
},
{
"id": 1117,
"type": "GetNode",
"pos": [
4143.2578125,
370.7759704589844
],
"size": [
210,
58
],
"flags": {
"collapsed": true
},
"order": 20,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1864,
3293
],
"slot_index": 0
}
],
"title": "Get_loras",
"properties": {},
"widgets_values": [
"loras"
],
gitextract_si5rgbko/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── publish_action.yml
├── LICENSE
├── README.md
├── __init__.py
├── misc.py
├── pyproject.toml
├── web/
│ ├── getsetorder.js
│ ├── guidanceversions.js
│ ├── help.js
│ ├── hint.js
│ ├── imagedisplay.js
│ ├── imagetransfershortcut.js
│ ├── impactpackfix.js
│ ├── outputgetnode.js
│ ├── samplerversions.js
│ ├── tabs.js
│ └── textversions.js
└── workflow/
├── Flux+ 1.3_release.json
├── Flux+ 1.4.4_release.json
├── Flux+ 1.4.5_release.json
├── Flux+ 1.6.4_release.json
├── Flux+ 1.7.0_release.json
├── Flux+ 1.7.1_beta.json
└── Flux+ Light 1.0.0_release.json
SYMBOL INDEX (154 symbols across 11 files)
FILE: misc.py
class AnyType (line 15) | class AnyType(str):
method __ne__ (line 16) | def __ne__(self, __value: object) -> bool:
class DenoiseSlider (line 21) | class DenoiseSlider:
method INPUT_TYPES (line 23) | def INPUT_TYPES(s):
method execute (line 37) | def execute(self, value):
class StepSlider (line 40) | class StepSlider:
method INPUT_TYPES (line 42) | def INPUT_TYPES(s):
method execute (line 52) | def execute(self, value):
class BatchSlider (line 56) | class BatchSlider:
method INPUT_TYPES (line 58) | def INPUT_TYPES(s):
method execute (line 68) | def execute(self, value):
class ResolutionMultiplySlider (line 72) | class ResolutionMultiplySlider:
method INPUT_TYPES (line 74) | def INPUT_TYPES(s):
method execute (line 84) | def execute(self, value):
class GPUSlider (line 87) | class GPUSlider:
method INPUT_TYPES (line 89) | def INPUT_TYPES(s):
method execute (line 99) | def execute(self, value):
class SelectFromBatch (line 103) | class SelectFromBatch:
method INPUT_TYPES (line 105) | def INPUT_TYPES(s):
method execute (line 115) | def execute(self, value):
class GuidanceSlider (line 119) | class GuidanceSlider:
method INPUT_TYPES (line 121) | def INPUT_TYPES(s):
method execute (line 133) | def execute(self, value):
class MaxShiftSlider (line 137) | class MaxShiftSlider:
method INPUT_TYPES (line 139) | def INPUT_TYPES(s):
method execute (line 151) | def execute(self, value):
class ControlNetSlider (line 155) | class ControlNetSlider:
method INPUT_TYPES (line 157) | def INPUT_TYPES(s):
method execute (line 173) | def execute(self, Strength, Start, End):
class CannySlider (line 177) | class CannySlider:
method INPUT_TYPES (line 179) | def INPUT_TYPES(s):
method execute (line 192) | def execute(self, Low_Threshold, High_Threshold):
class IPAdapterSlider (line 196) | class IPAdapterSlider:
method INPUT_TYPES (line 198) | def INPUT_TYPES(s):
method execute (line 212) | def execute(self, IP1, IP2, IP3):
class SEGSPass (line 216) | class SEGSPass:
method INPUT_TYPES (line 218) | def INPUT_TYPES(s):
method execute (line 229) | def execute(self, SEGS):
class PipePass (line 233) | class PipePass:
method INPUT_TYPES (line 235) | def INPUT_TYPES(s):
method execute (line 246) | def execute(self, PIPE_LINE):
class LatentPass (line 249) | class LatentPass:
method INPUT_TYPES (line 251) | def INPUT_TYPES(s):
method execute (line 261) | def execute(self, latent):
class IntPass (line 265) | class IntPass:
method INPUT_TYPES (line 267) | def INPUT_TYPES(s):
method execute (line 277) | def execute(self, INT):
class ResolutionPicker (line 281) | class ResolutionPicker:
method INPUT_TYPES (line 283) | def INPUT_TYPES(s):
method execute (line 293) | def execute(self, resolution):
class SamplerParameterPacker (line 296) | class SamplerParameterPacker:
method INPUT_TYPES (line 304) | def INPUT_TYPES(cls):
method pack_parameters (line 310) | def pack_parameters(self, sampler, scheduler):
class SamplerParameterUnpacker (line 313) | class SamplerParameterUnpacker:
method INPUT_TYPES (line 321) | def INPUT_TYPES(cls):
method unpack_parameters (line 326) | def unpack_parameters(self, sampler_params):
class TextVersions (line 330) | class TextVersions:
method INPUT_TYPES (line 332) | def INPUT_TYPES(s):
method __init__ (line 344) | def __init__(self):
method process_text (line 347) | def process_text(self, text):
function workflow_to_map (line 350) | def workflow_to_map(workflow):
function is_execution_model_version_supported (line 363) | def is_execution_model_version_supported():
class ImpactControlBridgeFix (line 371) | class ImpactControlBridgeFix:
method INPUT_TYPES (line 373) | def INPUT_TYPES(cls):
method IS_CHANGED (line 394) | def IS_CHANGED(self, value, mode, behavior="Stop", unique_id=None, pro...
method doit (line 414) | def doit(self, value, mode, behavior="Stop", unique_id=None, prompt=No...
class BooleanToEnabled (line 499) | class BooleanToEnabled:
method __init__ (line 501) | def __init__(self):
method INPUT_TYPES (line 505) | def INPUT_TYPES(cls):
method convert (line 519) | def convert(self, BOOLEAN):
class OutputGetString (line 523) | class OutputGetString:
method INPUT_TYPES (line 525) | def INPUT_TYPES(s):
method process (line 543) | def process(self, title, unique_id, prompt):
class SplitVec3 (line 557) | class SplitVec3:
method INPUT_TYPES (line 559) | def INPUT_TYPES(cls) -> Mapping[str, Any]:
method op (line 567) | def op(self, a: Vec3) -> tuple[float, float, float]:
class SplitVec2 (line 570) | class SplitVec2:
method INPUT_TYPES (line 572) | def INPUT_TYPES(cls) -> Mapping[str, Any]:
method op (line 578) | def op(self, a) -> tuple[float, float]:
class SimpleTextTruncate (line 581) | class SimpleTextTruncate:
method __init__ (line 582) | def __init__(self):
method INPUT_TYPES (line 586) | def INPUT_TYPES(cls):
method truncate_words (line 600) | def truncate_words(self, text, word_count):
class FluxContinuumModelRouter (line 610) | class FluxContinuumModelRouter:
method INPUT_TYPES (line 612) | def INPUT_TYPES(s):
method check_lazy_status (line 630) | def check_lazy_status(self, condition, flux_fill=None, flux_depth=None...
method route_model (line 650) | def route_model(self, condition, flux_fill=None, flux_depth=None, flux...
class ConfigurableModelRouter (line 665) | class ConfigurableModelRouter:
method INPUT_TYPES (line 667) | def INPUT_TYPES(cls):
method check_lazy_status (line 699) | def check_lazy_status(self, condition, routing_config, **kwargs):
method route_model (line 720) | def route_model(self, condition, routing_config, **kwargs):
class ImageBatchBoolean (line 733) | class ImageBatchBoolean:
method INPUT_TYPES (line 735) | def INPUT_TYPES(s):
method check_lazy_status (line 748) | def check_lazy_status(self, image1, image2, batch_enabled):
method batch (line 755) | def batch(self, image1, image2, batch_enabled):
function hex_to_rgba (line 778) | def hex_to_rgba(hex_color):
class DrawTextConfig (line 789) | class DrawTextConfig:
method INPUT_TYPES (line 791) | def INPUT_TYPES(s):
method configure (line 813) | def configure(self, font, size, color, background_color, padding, shad...
class ConfigurableDrawText (line 831) | class ConfigurableDrawText:
method INPUT_TYPES (line 833) | def INPUT_TYPES(s):
method draw (line 845) | def draw(self, TEXT, TEXT_STYLE, IMAGE):
FILE: web/getsetorder.js
constant BACKGROUND_NODE_CONFIG (line 12) | const BACKGROUND_NODE_CONFIG = {
method setup (line 22) | async setup() {
method beforeRegisterNodeDef (line 52) | async beforeRegisterNodeDef(nodeType, nodeData, app) {
FILE: web/guidanceversions.js
constant TAB_CONFIG (line 4) | const TAB_CONFIG = {
method nodeCreated (line 22) | nodeCreated(node) {
FILE: web/help.js
method beforeRegisterNodeDef (line 9) | async beforeRegisterNodeDef(nodeType, nodeData) {
FILE: web/hint.js
method registerCustomNodes (line 6) | registerCustomNodes() {
FILE: web/imagedisplay.js
constant DEFAULT_IMAGE (line 4) | const DEFAULT_IMAGE = "iVBORw0KGgoAAAANSUhEUgAAAPoAAAD6CAIAAAAHjs1qAAAAC...
class ImageDisplay (line 6) | class ImageDisplay extends LGraphNode {
method constructor (line 7) | constructor() {
method setupImage (line 34) | setupImage() {
method onPropertyChanged (line 72) | onPropertyChanged(name, value) {
method onResize (line 82) | onResize(size) {
method onSerialize (line 101) | onSerialize(o) {
method onConfigure (line 110) | onConfigure(o) {
method beforeRegisterNodeDef (line 153) | async beforeRegisterNodeDef(nodeType, nodeData, app) {
method registerCustomNodes (line 158) | registerCustomNodes() {
FILE: web/impactpackfix.js
method beforeRegisterNodeDef (line 5) | async beforeRegisterNodeDef(nodeType, nodeData, app) {
FILE: web/outputgetnode.js
constant CONFIG (line 6) | const CONFIG = {
function debounce (line 26) | function debounce(func, wait = CONFIG.CACHE.debounceWait) {
function createCachedFunction (line 38) | function createCachedFunction(fn) {
method registerCustomNodes (line 54) | registerCustomNodes() {
method beforeRegisterNodeDef (line 316) | async beforeRegisterNodeDef(nodeType, nodeData) {
class TextDisplay (line 391) | class TextDisplay extends LGraphNode {
method constructor (line 392) | constructor() {
method onAdded (line 410) | onAdded(graph) {
method onDragFinished (line 421) | onDragFinished() {
method onDrawForeground (line 432) | onDrawForeground(ctx) {
method findOutputGetNode (line 464) | findOutputGetNode(graph) {
method onResize (line 468) | onResize(size) {
method onDblClick (line 474) | onDblClick(event, pos, canvas) {
method registerCustomNodes (line 501) | registerCustomNodes() {
FILE: web/samplerversions.js
constant TAB_CONFIG (line 4) | const TAB_CONFIG = {
method nodeCreated (line 20) | nodeCreated(node) {
FILE: web/tabs.js
constant NODE_METRICS (line 7) | const NODE_METRICS = {
constant TAB_STYLE (line 19) | const TAB_STYLE = {
class BoundingBoxCalculator (line 35) | class BoundingBoxCalculator {
method constructor (line 36) | constructor() {
method calculate (line 45) | calculate(rects) {
class TabManager (line 75) | class TabManager {
method getTabWidth (line 82) | static getTabWidth(config, isSecondTab = false) {
method wouldTabsOverlap (line 108) | static wouldTabsOverlap(properties) {
method drawTab (line 125) | static drawTab(ctx, x, y, text, width) {
method handleNodeOrder (line 172) | static handleNodeOrder(node, toFront = true) {
class NodeWithTabs (line 224) | class NodeWithTabs {
method onNodeCreated (line 229) | static onNodeCreated(node) {
method nodeCreated (line 276) | nodeCreated(node) {
FILE: web/textversions.js
constant CONFIG (line 6) | const CONFIG = {
constant TAB_STYLE (line 15) | const TAB_STYLE = {
function createTabConfig (line 28) | function createTabConfig(numVersions) {
function debounce (line 35) | function debounce(func, wait) {
method nodeCreated (line 51) | nodeCreated(node) {
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (7,568K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 92,
"preview": "# These are supported funding model platforms\n\ngithub: robertvoy\nbuy_me_a_coffee: robertvoy\n"
},
{
"path": ".github/workflows/publish_action.yml",
"chars": 461,
"preview": "name: Publish to Comfy registry\non:\n workflow_dispatch:\n push:\n branches:\n - main\n paths:\n - \"pyprojec"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2024 Robert\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 11697,
"preview": "# ComfyUI Flux Continuum - Modular Interface\n\n => {\n if"
},
{
"path": "web/guidanceversions.js",
"chars": 6155,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n// Configuration Constants\nconst TAB_CONFIG = {\n width: 40,\n heigh"
},
{
"path": "web/help.js",
"chars": 10250,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n// Replace this with the category/categories used by your nodes.\nconst c"
},
{
"path": "web/hint.js",
"chars": 11722,
"preview": "import { app } from \"../../../scripts/app.js\";\n\napp.registerExtension({\n name: \"FluxContinuum.HintNode\",\n\n registe"
},
{
"path": "web/imagedisplay.js",
"chars": 9011,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n// Default white square image (200x200 pixels, white)\nconst DEFAULT_IMAG"
},
{
"path": "web/imagetransfershortcut.js",
"chars": 2874,
"preview": "import { app } from \"/scripts/app.js\";\nimport { api } from \"/scripts/api.js\";\n\napp.registerExtension({\n name: \"FluxCont"
},
{
"path": "web/impactpackfix.js",
"chars": 1058,
"preview": "import { app } from \"../../scripts/app.js\";\n\napp.registerExtension({\n name: \"FluxContinuum.ImpactControlBridgeFix\",\n "
},
{
"path": "web/outputgetnode.js",
"chars": 18433,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n//based on KJNodes SetGet: https://github.com/kijai/ComfyUI-KJNodes\n\n// "
},
{
"path": "web/samplerversions.js",
"chars": 6611,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n// Configuration Constants\nconst TAB_CONFIG = {\n width: 40,\n heigh"
},
{
"path": "web/tabs.js",
"chars": 18837,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n/**\n * Constants for node and tab dimensions and appearance\n * @readonly"
},
{
"path": "web/textversions.js",
"chars": 8183,
"preview": "import { app } from \"../../../scripts/app.js\";\n\n/**\n * Default and limit configurations\n */\nconst CONFIG = {\n DEFAULT"
},
{
"path": "workflow/Flux+ 1.3_release.json",
"chars": 998122,
"preview": "{\n \"last_node_id\": 3156,\n \"last_link_id\": 4684,\n \"nodes\": [\n {\n \"id\": 1026,\n \"type\": \"ImagePass\",\n "
},
{
"path": "workflow/Flux+ 1.4.4_release.json",
"chars": 1026674,
"preview": "{\n \"last_node_id\": 3818,\n \"last_link_id\": 5743,\n \"nodes\": [\n {\n \"id\": 1026,\n \"type\": \"ImagePass\",\n "
},
{
"path": "workflow/Flux+ 1.4.5_release.json",
"chars": 1026225,
"preview": "{\n \"last_node_id\": 3829,\n \"last_link_id\": 5773,\n \"nodes\": [\n {\n \"id\": 1026,\n \"type\": \"ImagePass\",\n "
},
{
"path": "workflow/Flux+ 1.6.4_release.json",
"chars": 1041489,
"preview": "{\n \"id\": \"58bb46b3-6af0-4368-8562-5b29e4c07d28\",\n \"revision\": 0,\n \"last_node_id\": 4106,\n \"last_link_id\": 6239,\n \"no"
},
{
"path": "workflow/Flux+ 1.7.0_release.json",
"chars": 1062916,
"preview": "{\n \"id\": \"72381480-a0d5-4292-88ec-5961ca04ee42\",\n \"revision\": 0,\n \"last_node_id\": 4184,\n \"last_link_id\": 6373,\n \"no"
},
{
"path": "workflow/Flux+ 1.7.1_beta.json",
"chars": 1061722,
"preview": "{\n \"id\": \"72381480-a0d5-4292-88ec-5961ca04ee42\",\n \"revision\": 0,\n \"last_node_id\": 4185,\n \"last_link_id\": 6375,\n \"no"
},
{
"path": "workflow/Flux+ Light 1.0.0_release.json",
"chars": 843824,
"preview": "{\n \"last_node_id\": 3853,\n \"last_link_id\": 5833,\n \"nodes\": [\n {\n \"id\": 1026,\n \"type\": \"ImagePass\",\n "
}
]
About this extraction
This page contains the full source code of the robertvoy/ComfyUI-Flux-Continuum GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (6.9 MB), approximately 1.8M tokens, and a symbol index with 154 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.