v1.1.0
- Added the "Model Switch" node - Updated README - Cleaned up the CHANGELOG to meet standard
This commit is contained in:
+3
-2
@@ -88,8 +88,9 @@ yarn-error.log*
|
||||
|
||||
# AF speciffic
|
||||
Status.txt
|
||||
*.docx
|
||||
*.psd
|
||||
*.docx
|
||||
helpers/
|
||||
nodes.zip
|
||||
node.zip
|
||||
Docs/screenshots/
|
||||
Docs/screenshots/
|
||||
|
||||
+16
-17
@@ -1,43 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.9] - 2025-11-11
|
||||
### 1.1.0
|
||||
- Added the "Model Switch" node
|
||||
- Updated README
|
||||
- Cleaned up the CHANGELOG to meet standard
|
||||
|
||||
### v1.0.9
|
||||
- Added "Show Text" node documentation
|
||||
- Added a video about the "Edit Generated Prompt" node
|
||||
- Updated the workflow example to use my own "Show Text" node
|
||||
|
||||
## [1.0.8] - 2025-11-10
|
||||
### v1.0.8
|
||||
- Fixed an error on the "Edit Generated Prompt" node ( invalid prompt: {'type': 'prompt_no_outputs', 'message': 'Prompt has no outputs', 'details': '', 'extra_info': {}} )
|
||||
- Added a "Show Text" node, so folks don't have to install any other custom nodes to run the example workflow (Yeah ComfyUI does NOT have a simple show text node...)
|
||||
|
||||
## [1.0.7] - 2025-11-05
|
||||
### v1.0.7
|
||||
- Changed Icon location to my own website as the ComfyUI Registry botched out on any GitHub URL I tried...
|
||||
|
||||
## [1.0.6] - 2025-10-30
|
||||
### v1.0.6
|
||||
- Minor changes for registry compliance
|
||||
|
||||
## [1.0.5] - 2025-10-30
|
||||
### Changed
|
||||
### v1.0.5
|
||||
- Added screenshots
|
||||
- Added workflow examples
|
||||
- Updated README
|
||||
|
||||
## [1.0.4] - 2025-10-30
|
||||
### Changed
|
||||
### v1.0.4
|
||||
- Save Prompt History now only saves, when the prompt changed, eliminating duplicate prompts in the archive.
|
||||
- Added screenshots
|
||||
- Added workflow examples
|
||||
- Updated README
|
||||
|
||||
## [1.0.3] - 2025-10-29
|
||||
### Changed
|
||||
### v1.0.3
|
||||
- Fixed links in nodes.py to git
|
||||
- Added CHANGELOG.md
|
||||
- Simplified __init__.py
|
||||
- Added package.json for ComfyUI Manager compliance
|
||||
- Added pyproject.toml for registry compliance
|
||||
|
||||
## [1.0.2] - 2025-10-29
|
||||
### Changed
|
||||
### v1.0.2
|
||||
- Changed "project" to "filename" in load prompt node
|
||||
- Changed display on info node
|
||||
- Changed console output to include ID number
|
||||
@@ -45,11 +46,9 @@
|
||||
- Fixed Versioning text
|
||||
- Made print out in console less prominent
|
||||
|
||||
## [1.0.1] - 2025-10-22
|
||||
### Added
|
||||
### v1.0.1
|
||||
- Added "project" back in as a tag that gets stored in the json
|
||||
|
||||
## [1.0.0] - 2025-10-20
|
||||
### Major Changes
|
||||
### v1.0.0
|
||||
- Merged AF Edit Generated Prompt and AF Prompt History Nodes into single pack
|
||||
- Complete overhauled Load Prompt node
|
||||
- Complete overhauled Load Prompt node
|
||||
|
||||
@@ -7,7 +7,8 @@ A lightweight suite of ComfyUI custom nodes for AI prompt management and history
|
||||
**[AF - Edit Generated Prompt](#af---edit-generated-prompt)** - Receive LLM-generated prompts and optionally edit them manually
|
||||
**[AF - Save Prompt History](#af---save-prompt-history)** - Archive prompts to JSON files with timestamps
|
||||
**[AF - Load Prompt History](#af---load-prompt-history)** - Browse and load previous prompts by index
|
||||
**[AF - Show Text](#af---show-text)** - SImple node that displays the connected text
|
||||
**[AF - Show Text](#af---show-text)** - Simple node that displays the connected text
|
||||
**[AF - Model Switch](#af---model-switch)** - A dynamic model/clip/vae switching node
|
||||
|
||||
---
|
||||
|
||||
@@ -204,6 +205,18 @@ AF Load - ✓ Successfully loaded: ID: 126 | 2025-10-29 14:32:15
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 💡 Pro Tips
|
||||
|
||||
- **Index 0 is your friend** - Always points to newest prompt
|
||||
- **Info output shows everything** - See exactly what you loaded
|
||||
- **Console logging** - Check ID and timestamp confirmation
|
||||
- **Multiple load nodes** - Each can access different files/indices
|
||||
- **Workflows persist** - Your selections save with the workflow
|
||||
- **Project field** - Use it as a mental note when saving, great for manual inspection
|
||||
|
||||
---
|
||||
|
||||
### AF - Show Text
|
||||
|
||||
A very simple node that shows the text connected to the input of the Node
|
||||
@@ -239,14 +252,89 @@ A very simple node that shows the text connected to the input of the Node
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro Tips
|
||||
## AF - Model Switch
|
||||
|
||||
- **Index 0 is your friend** - Always points to newest prompt
|
||||
- **Info output shows everything** - See exactly what you loaded
|
||||
- **Console logging** - Check ID and timestamp confirmation
|
||||
- **Multiple load nodes** - Each can access different files/indices
|
||||
- **Workflows persist** - Your selections save with the workflow
|
||||
- **Project field** - Use it as a mental note when saving, great for manual inspection
|
||||
A dynamic model switching node that allows you to toggle between multiple model/CLIP/VAE combinations without rewiring your workflow.
|
||||
|
||||
### Features
|
||||
|
||||
- **Dynamic Rails**: Add 2-10 model rails on demand
|
||||
- **Smart AUTO Mode**: Automatically selects the first connected rail
|
||||
- **1-Based Indexing**: Intuitive numbering (Rail 1, Rail 2, Rail 3...)
|
||||
- **Permanent Base Rails**: Rails 1-2 never lose connections when adjusting rail count
|
||||
- **Real-Time Status**: Color-coded display shows active rail instantly
|
||||
- **Flexible Sizing**: Resizable node that preserves your layout preferences
|
||||
|
||||
### How It Works
|
||||
|
||||
The Model Switch acts as a gateway for your model pipeline. Instead of maintaining multiple workflow branches or manually reconnecting loaders, you can:
|
||||
|
||||
1. Connect multiple checkpoint loaders (or AIO loaders) to different rails
|
||||
2. Choose which rail to use via the `selected_rail` parameter
|
||||
3. The node outputs the MODEL, CLIP, and VAE from your selected rail
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Range | Description |
|
||||
|-----------|------|-------|-------------|
|
||||
| `number_of_rails` | INT | 2-10 | Number of visible model rails |
|
||||
| `selected_rail` | INT | 0-10 | Active rail: `0` = AUTO (first connected), `1-10` = manual selection |
|
||||
| `status` | STRING | - | Read-only display showing current active rail |
|
||||
|
||||
### Status Display Colors
|
||||
|
||||
- 🟠 **Orange**: AUTO mode - automatically using first connected rail
|
||||
- 🔵 **Blue**: Manual mode - specific rail selected
|
||||
- 🔴 **Red**: Error - no models connected
|
||||
|
||||
### Usage Example
|
||||
|
||||
**Basic Setup:**
|
||||
```
|
||||
Checkpoint Loader A → model_1, clip_1, vae_1
|
||||
Checkpoint Loader B → model_2, clip_2, vae_2
|
||||
↓
|
||||
AF Model Switch
|
||||
(selected_rail: 0)
|
||||
↓
|
||||
→ model, clip, vae
|
||||
```
|
||||
|
||||
**A/B Testing Workflow:**
|
||||
1. Set `number_of_rails: 2`
|
||||
2. Connect two different models to Rails 1 and 2
|
||||
3. Set `selected_rail: 1` to use first model, `2` for second model
|
||||
4. Set `selected_rail: 0` for AUTO mode (uses first connected)
|
||||
|
||||
**Advanced: Quality vs Speed:**
|
||||
1. Set `number_of_rails: 3`
|
||||
2. Rail 1: High-quality slow model
|
||||
3. Rail 2: Balanced model
|
||||
4. Rail 3: Fast draft model
|
||||
5. Switch between them by changing `selected_rail`
|
||||
|
||||
### Tips
|
||||
|
||||
- **Rails 1-2 are permanent**: These connections are never lost when you adjust `number_of_rails`
|
||||
- **Rails 3-10 are dynamic**: These are added/removed as needed
|
||||
- **Use with rgthree Group Bypasser**: Bypass entire model loading groups to save memory - only the selected rail's loaders will execute
|
||||
- **AUTO mode is smart**: It will find the first rail with actual data, skipping any bypassed/muted nodes
|
||||
- **Status updates instantly**: No need to run the workflow to see which rail is selected
|
||||
|
||||
### Integration with Other Nodes
|
||||
|
||||
**Works great with:**
|
||||
- `rgthree Context Switch` - for full pipeline switching
|
||||
- `rgthree Group Bypasser` - to disable unused model loaders
|
||||
- `AF - Save Prompt History` - save which model was used with each prompt
|
||||
- Any checkpoint loader or AIO loader node
|
||||
|
||||
### Technical Notes
|
||||
|
||||
- Input names are 1-based: `model_1`, `clip_1`, `vae_1`, etc.
|
||||
- Internally consistent: no confusing 0-based conversions
|
||||
- Minimum node width: 160px
|
||||
- Height auto-adjusts based on number of rails
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -475,21 +475,143 @@ class AF_Show_Text:
|
||||
"ui": {"text": [str(text)]},
|
||||
"result": (text,)
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Node Mappings
|
||||
# AF - Model Switch Node (1-based Rails, 0=AUTO)
|
||||
# =============================================================================
|
||||
|
||||
class AF_Model_Switch:
|
||||
"""
|
||||
Switch between model configurations with dynamic input visibility.
|
||||
Uses 1-based indexing: Rail 1, Rail 2, Rail 3, etc.
|
||||
Rails 1 and 2 are permanent (connections never lost).
|
||||
Rails 3-10 are dynamically added/removed.
|
||||
|
||||
Selected rail: 0 = AUTO (first connected), 1-10 = manual selection
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"number_of_rails": ("INT", {
|
||||
"default": 2,
|
||||
"min": 2,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"tooltip": "Number of visible rails (2-10)"
|
||||
}),
|
||||
"selected_rail": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"tooltip": "Active rail: 0=AUTO (first connected), 1-10=manual selection"
|
||||
}),
|
||||
"status": ("STRING", {
|
||||
"default": "💡 Active Rail: auto",
|
||||
"multiline": True,
|
||||
"dynamicPrompts": False
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# Rail 1 and Rail 2 are PERMANENT - never removed
|
||||
"model_1": ("MODEL",),
|
||||
"clip_1": ("CLIP",),
|
||||
"vae_1": ("VAE",),
|
||||
"model_2": ("MODEL",),
|
||||
"clip_2": ("CLIP",),
|
||||
"vae_2": ("VAE",),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
|
||||
RETURN_NAMES = ("model", "clip", "vae")
|
||||
FUNCTION = "switch_model"
|
||||
CATEGORY = "AF - Nodes/AF - Prompt Pack"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def switch_model(self, number_of_rails, selected_rail, status="", unique_id=None, extra_pnginfo=None, **kwargs):
|
||||
# Clamp values to valid ranges
|
||||
number_of_rails = max(2, min(10, number_of_rails))
|
||||
selected_rail = max(0, min(number_of_rails, selected_rail))
|
||||
|
||||
# Determine actual active rail (1-based throughout!)
|
||||
actual_rail = None
|
||||
is_auto_mode = (selected_rail == 0)
|
||||
|
||||
if is_auto_mode:
|
||||
# AUTO mode - find first rail with data
|
||||
for i in range(1, number_of_rails + 1): # Rails 1 to number_of_rails
|
||||
test_model = kwargs.get(f"model_{i}", None)
|
||||
if test_model is not None:
|
||||
actual_rail = i
|
||||
print(f"AF Model Switch - AUTO → Rail {actual_rail}")
|
||||
break
|
||||
|
||||
if actual_rail is None:
|
||||
# No rails have data, default to rail 1
|
||||
actual_rail = 1
|
||||
print(f"AF Model Switch - AUTO → No data found, defaulting to Rail 1")
|
||||
|
||||
status_text = f"💡 Active Rail: auto → {actual_rail}"
|
||||
else:
|
||||
# Manual mode - user selected specific rail (1-based)
|
||||
actual_rail = selected_rail
|
||||
status_text = f"🎯 Active Rail: {actual_rail}"
|
||||
print(f"AF Model Switch - Manual → Rail {actual_rail}")
|
||||
|
||||
# Get selected components using 1-based rail number directly
|
||||
model = kwargs.get(f"model_{actual_rail}", None)
|
||||
clip = kwargs.get(f"clip_{actual_rail}", None)
|
||||
vae = kwargs.get(f"vae_{actual_rail}", None)
|
||||
|
||||
# Final fallback if still no model
|
||||
if model is None:
|
||||
for i in range(1, number_of_rails + 1):
|
||||
test_model = kwargs.get(f"model_{i}", None)
|
||||
if test_model is not None:
|
||||
model = test_model
|
||||
clip = kwargs.get(f"clip_{i}", None)
|
||||
vae = kwargs.get(f"vae_{i}", None)
|
||||
print(f"AF Model Switch - Fallback to rail {i}")
|
||||
break
|
||||
|
||||
if model is not None:
|
||||
print(f"AF Model Switch - ✓ Outputting Rail {actual_rail} (of 1-{number_of_rails})")
|
||||
else:
|
||||
print(f"AF Model Switch - ⚠️ No models connected")
|
||||
status_text = "⚠️ Active Rail: none (no connections)"
|
||||
|
||||
return {
|
||||
"ui": {"status": [status_text]},
|
||||
"result": (model, clip, vae)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, number_of_rails, selected_rail, **kwargs):
|
||||
# Force re-evaluation when inputs change
|
||||
return f"{number_of_rails}-{selected_rail}"
|
||||
|
||||
# Update NODE_CLASS_MAPPINGS
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"AF_Edit_Generated_Prompt": AF_Edit_Generated_Prompt,
|
||||
"AF_Save_Prompt_History": AF_Save_Prompt_History,
|
||||
"AF_Load_Prompt_History": AF_Load_Prompt_History,
|
||||
"AF_Show_Text": AF_Show_Text,
|
||||
"AF_Model_Switch": AF_Model_Switch,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"AF_Edit_Generated_Prompt": "AF - Edit Generated Prompt",
|
||||
"AF_Save_Prompt_History": "AF - Save Prompt History",
|
||||
"AF_Save_Prompt_History": "AF - Save Prompt History",
|
||||
"AF_Load_Prompt_History": "AF - Load Prompt History",
|
||||
"AF_Show_Text": "AF - Show Text",
|
||||
}
|
||||
"AF_Model_Switch": "AF - Model Switch",
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "comfyui-af-pack-prompt-nodes",
|
||||
"version": "1.0.9",
|
||||
"version": "1.1.0",
|
||||
"description": "A lightweight suite of ComfyUI custom nodes for AI prompt management and history tracking.",
|
||||
"author": "Alex Furer",
|
||||
"license": "MIT",
|
||||
@@ -9,4 +9,4 @@
|
||||
"url": "https://github.com/alFrame/ComfyUI-AF-Pack-Prompt-Nodes.git"
|
||||
},
|
||||
"nodepath": "custom_nodes/ComfyUI-AF-Pack-Prompt-Nodes"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-af-pack-prompt-nodes"
|
||||
description = "A lightweight suite of ComfyUI custom nodes for AI prompt management and history tracking."
|
||||
version = "1.0.9"
|
||||
version = "1.1.0"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = []
|
||||
|
||||
@@ -11,4 +11,4 @@ Repository = "https://github.com/alFrame/ComfyUI-AF-Pack-Prompt-Nodes"
|
||||
[tool.comfy]
|
||||
PublisherId = "alframe"
|
||||
DisplayName = "ComfyUI AF - Pack-Prompt-Nodes"
|
||||
Icon = "https://fullframestudios.ch/ComfyUI-Icon/icon.png"
|
||||
Icon = "https://fullframestudios.ch/ComfyUI-Icon/icon.png"
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "AF.ModelSwitch",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === "AF_Model_Switch") {
|
||||
|
||||
// Hook into onExecuted to update status after workflow runs
|
||||
const originalOnExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = originalOnExecuted?.apply(this, arguments);
|
||||
|
||||
if (message?.status && Array.isArray(message.status) && message.status.length > 0) {
|
||||
const statusText = message.status[0];
|
||||
|
||||
const statusWidget = this.widgets?.find(w => w.name === "status");
|
||||
if (statusWidget) {
|
||||
statusWidget.value = statusText;
|
||||
|
||||
if (statusWidget.inputEl) {
|
||||
statusWidget.inputEl.value = statusText;
|
||||
|
||||
if (statusText.includes("auto")) {
|
||||
statusWidget.inputEl.style.backgroundColor = "#2a2a1a";
|
||||
statusWidget.inputEl.style.color = "#ffaa44";
|
||||
statusWidget.inputEl.style.borderColor = "#cc8833";
|
||||
} else if (statusText.includes("none")) {
|
||||
statusWidget.inputEl.style.backgroundColor = "#2a1a1a";
|
||||
statusWidget.inputEl.style.color = "#ff6666";
|
||||
statusWidget.inputEl.style.borderColor = "#cc4444";
|
||||
} else {
|
||||
statusWidget.inputEl.style.backgroundColor = "#1a2a3a";
|
||||
statusWidget.inputEl.style.color = "#88ccff";
|
||||
statusWidget.inputEl.style.borderColor = "#4488cc";
|
||||
}
|
||||
}
|
||||
|
||||
this.setDirtyCanvas(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
// Store original onNodeCreated
|
||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = originalOnNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Rails 1 and 2 are PERMANENT (defined in Python INPUT_TYPES)
|
||||
// So we start counting from rail 2 as the baseline
|
||||
// _currentRailCount represents the TOTAL number of rails visible
|
||||
|
||||
// Detect existing rail count from loaded inputs
|
||||
// Check for the highest rail number present (rails 3-10 are dynamic)
|
||||
let detectedRailCount = 2; // Always have at least rails 1 and 2
|
||||
if (this.inputs) {
|
||||
for (let i = 10; i >= 3; i--) {
|
||||
if (this.inputs.find(inp => inp.name === `model_${i}`)) {
|
||||
detectedRailCount = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize tracking
|
||||
this._currentRailCount = detectedRailCount;
|
||||
this._userResized = false;
|
||||
this._isInitializing = true; // Flag to prevent duplicate initialization
|
||||
|
||||
// Setup callbacks with delay to ensure widgets exist
|
||||
setTimeout(() => {
|
||||
const railCountWidget = this.widgets?.find(w => w.name === "number_of_rails");
|
||||
const selectWidget = this.widgets?.find(w => w.name === "selected_rail");
|
||||
|
||||
// Rail count callback
|
||||
if (railCountWidget) {
|
||||
railCountWidget.callback = (value) => {
|
||||
const count = Math.max(2, Math.min(10, parseInt(value) || 2));
|
||||
this.updateRailInputs(count);
|
||||
};
|
||||
}
|
||||
|
||||
// Selected rail callback
|
||||
if (selectWidget) {
|
||||
selectWidget.callback = (value) => {
|
||||
this.updateStatusDisplay(parseInt(value));
|
||||
};
|
||||
}
|
||||
|
||||
this.updateStatusDisplay(0);
|
||||
this._isInitializing = false;
|
||||
}, 100);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Update status display (UI only, no execution)
|
||||
nodeType.prototype.updateStatusDisplay = function(selectedRail) {
|
||||
const statusWidget = this.widgets?.find(w => w.name === "status");
|
||||
if (!statusWidget) return;
|
||||
|
||||
let statusText, bgColor, textColor, borderColor;
|
||||
|
||||
// 0 = AUTO, 1-10 = manual rail selection
|
||||
if (selectedRail === 0 || isNaN(selectedRail)) {
|
||||
statusText = "💡 Active Rail: auto";
|
||||
bgColor = "#2a2a1a";
|
||||
textColor = "#ffaa44";
|
||||
borderColor = "#cc8833";
|
||||
} else {
|
||||
// Display 1-based rail number
|
||||
statusText = `🎯 Active Rail: ${selectedRail}`;
|
||||
bgColor = "#1a2a3a";
|
||||
textColor = "#88ccff";
|
||||
borderColor = "#4488cc";
|
||||
}
|
||||
|
||||
statusWidget.value = statusText;
|
||||
|
||||
if (statusWidget.inputEl) {
|
||||
statusWidget.inputEl.value = statusText;
|
||||
statusWidget.inputEl.style.backgroundColor = bgColor;
|
||||
statusWidget.inputEl.style.color = textColor;
|
||||
statusWidget.inputEl.style.borderColor = borderColor;
|
||||
}
|
||||
|
||||
this.setDirtyCanvas(true, false);
|
||||
};
|
||||
|
||||
// Update rail inputs dynamically
|
||||
nodeType.prototype.updateRailInputs = function(railCount) {
|
||||
railCount = Math.max(2, Math.min(10, railCount));
|
||||
|
||||
if (this._currentRailCount === railCount) return;
|
||||
|
||||
const oldCount = this._currentRailCount;
|
||||
const newCount = railCount;
|
||||
|
||||
console.log(`AF Model Switch - Updating rails from ${oldCount} to ${newCount}`);
|
||||
|
||||
if (newCount > oldCount) {
|
||||
// Adding rails: model_3, model_4, etc. (1-based!)
|
||||
// Rails 1 and 2 are permanent, so we only add 3+
|
||||
for (let i = oldCount + 1; i <= newCount; i++) {
|
||||
// Check if this input already exists before adding
|
||||
const modelExists = this.inputs.find(inp => inp.name === `model_${i}`);
|
||||
const clipExists = this.inputs.find(inp => inp.name === `clip_${i}`);
|
||||
const vaeExists = this.inputs.find(inp => inp.name === `vae_${i}`);
|
||||
|
||||
if (!modelExists) {
|
||||
console.log(`AF Model Switch - Adding model_${i}`);
|
||||
this.addInput(`model_${i}`, "MODEL");
|
||||
}
|
||||
if (!clipExists) {
|
||||
console.log(`AF Model Switch - Adding clip_${i}`);
|
||||
this.addInput(`clip_${i}`, "CLIP");
|
||||
}
|
||||
if (!vaeExists) {
|
||||
console.log(`AF Model Switch - Adding vae_${i}`);
|
||||
this.addInput(`vae_${i}`, "VAE");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Removing rails from the end
|
||||
// NEVER remove rails 1 or 2 (they're permanent)
|
||||
for (let i = oldCount; i > newCount && i > 2; i--) {
|
||||
console.log(`AF Model Switch - Removing rail ${i}`);
|
||||
for (let j = this.inputs.length - 1; j >= 0; j--) {
|
||||
const input = this.inputs[j];
|
||||
if (input.name === `vae_${i}` ||
|
||||
input.name === `clip_${i}` ||
|
||||
input.name === `model_${i}`) {
|
||||
this.removeInput(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._currentRailCount = newCount;
|
||||
|
||||
// Update select widget: 0=AUTO, 1 to newCount for rails
|
||||
const selectWidget = this.widgets?.find(w => w.name === "selected_rail");
|
||||
if (selectWidget) {
|
||||
const maxRail = newCount;
|
||||
selectWidget.options = selectWidget.options || {};
|
||||
selectWidget.options.min = 0;
|
||||
selectWidget.options.max = maxRail;
|
||||
|
||||
// Clamp current selection to valid range
|
||||
const currentValue = parseInt(selectWidget.value);
|
||||
if (currentValue > maxRail) {
|
||||
selectWidget.value = maxRail;
|
||||
this.updateStatusDisplay(maxRail);
|
||||
}
|
||||
|
||||
// Update the input element's attributes
|
||||
if (selectWidget.inputEl) {
|
||||
selectWidget.inputEl.max = maxRail;
|
||||
selectWidget.inputEl.min = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Only adjust height, preserve width
|
||||
const widgetHeight = 100;
|
||||
const inputHeight = 20;
|
||||
const totalInputs = this._currentRailCount * 3;
|
||||
const newHeight = widgetHeight + (totalInputs * inputHeight);
|
||||
|
||||
// Preserve current width, only update height
|
||||
if (this.size) {
|
||||
this.size[1] = newHeight;
|
||||
} else {
|
||||
this.size = [160, newHeight];
|
||||
}
|
||||
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
// Override computeSize - minimal intervention, let LiteGraph work
|
||||
const originalComputeSize = nodeType.prototype.computeSize;
|
||||
nodeType.prototype.computeSize = function(out) {
|
||||
// If user hasn't resized, calculate proper height
|
||||
if (!this._userResized) {
|
||||
let height = 200;
|
||||
|
||||
if (this._currentRailCount) {
|
||||
const widgetHeight = 100;
|
||||
const inputHeight = 20;
|
||||
const totalInputs = this._currentRailCount * 3;
|
||||
height = widgetHeight + (totalInputs * inputHeight);
|
||||
}
|
||||
|
||||
const size = [160, height];
|
||||
|
||||
if (out) {
|
||||
out[0] = size[0];
|
||||
out[1] = size[1];
|
||||
return out;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
// User has resized - let LiteGraph handle it normally
|
||||
return originalComputeSize?.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Track when user manually resizes
|
||||
const originalOnResize = nodeType.prototype.onResize;
|
||||
nodeType.prototype.onResize = function(size) {
|
||||
this._userResized = true;
|
||||
return originalOnResize?.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "AF_Model_Switch") {
|
||||
setTimeout(() => {
|
||||
// Style rail count widget
|
||||
const railCountWidget = node.widgets?.find(w => w.name === "number_of_rails");
|
||||
if (railCountWidget?.inputEl) {
|
||||
railCountWidget.inputEl.style.border = "1px solid #4488cc";
|
||||
railCountWidget.inputEl.style.backgroundColor = "#1a2a3a";
|
||||
railCountWidget.inputEl.style.padding = "4px";
|
||||
railCountWidget.inputEl.style.borderRadius = "3px";
|
||||
}
|
||||
|
||||
// Style selected rail widget
|
||||
const selectWidget = node.widgets?.find(w => w.name === "selected_rail");
|
||||
if (selectWidget?.inputEl) {
|
||||
selectWidget.inputEl.style.border = "1px solid #88cc44";
|
||||
selectWidget.inputEl.style.backgroundColor = "#1a3a2a";
|
||||
selectWidget.inputEl.style.padding = "4px";
|
||||
selectWidget.inputEl.style.borderRadius = "3px";
|
||||
selectWidget.inputEl.style.fontWeight = "bold";
|
||||
}
|
||||
|
||||
// Style status widget
|
||||
const statusWidget = node.widgets?.find(w => w.name === "status");
|
||||
if (statusWidget?.inputEl) {
|
||||
statusWidget.inputEl.readOnly = true;
|
||||
statusWidget.inputEl.style.fontFamily = "monospace";
|
||||
statusWidget.inputEl.style.fontSize = "13px";
|
||||
statusWidget.inputEl.style.fontWeight = "bold";
|
||||
statusWidget.inputEl.style.cursor = "default";
|
||||
statusWidget.inputEl.style.padding = "6px 8px";
|
||||
statusWidget.inputEl.style.textAlign = "center";
|
||||
statusWidget.inputEl.style.userSelect = "none";
|
||||
statusWidget.inputEl.style.resize = "none";
|
||||
statusWidget.inputEl.style.minHeight = "32px";
|
||||
statusWidget.inputEl.style.maxHeight = "32px";
|
||||
statusWidget.inputEl.style.height = "32px";
|
||||
statusWidget.inputEl.style.overflow = "hidden";
|
||||
statusWidget.inputEl.style.width = "100%";
|
||||
statusWidget.inputEl.style.boxSizing = "border-box";
|
||||
|
||||
// Set initial AUTO color (orange)
|
||||
statusWidget.inputEl.style.backgroundColor = "#2a2a1a";
|
||||
statusWidget.inputEl.style.color = "#ffaa44";
|
||||
statusWidget.inputEl.style.border = "1px solid #cc8833";
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
async loadedGraphNode(node) {
|
||||
if (node.comfyClass === "AF_Model_Switch") {
|
||||
setTimeout(() => {
|
||||
// Mark as user-resized if node has custom size stored
|
||||
if (node.size && node.size[0] !== 160) {
|
||||
node._userResized = true;
|
||||
}
|
||||
|
||||
// Detect current rail count from actual inputs
|
||||
let actualRailCount = 2; // Minimum (rails 1 and 2 are permanent)
|
||||
if (node.inputs) {
|
||||
for (let i = 10; i >= 3; i--) {
|
||||
if (node.inputs.find(inp => inp.name === `model_${i}`)) {
|
||||
actualRailCount = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update _currentRailCount to match actual state
|
||||
node._currentRailCount = actualRailCount;
|
||||
|
||||
const railCountWidget = node.widgets?.find(w => w.name === "number_of_rails");
|
||||
if (railCountWidget) {
|
||||
const widgetValue = parseInt(railCountWidget.value) || 2;
|
||||
|
||||
console.log(`AF Model Switch - Loaded: widget=${widgetValue}, actual inputs=${actualRailCount}`);
|
||||
|
||||
// Sync: if widget and actual don't match, update to match widget
|
||||
if (widgetValue !== actualRailCount && node.updateRailInputs) {
|
||||
console.log(`AF Model Switch - Syncing rails to ${widgetValue}`);
|
||||
node.updateRailInputs(widgetValue);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user