Merge pull request #1 from ComfyAssets/width-and-height
Width and height
This commit is contained in:
@@ -119,8 +119,10 @@ release-test: quality-check test
|
||||
@test -d examples || (echo "❌ examples directory missing" && exit 1)
|
||||
@echo "✅ Package structure validated"
|
||||
@echo "Checking documentation..."
|
||||
@test -f examples/documentation/resolution_calculator.md || (echo "❌ Tool documentation missing" && exit 1)
|
||||
@test -f examples/workflows/resolution_calculator_example.json || (echo "❌ Example workflow missing" && exit 1)
|
||||
@test -f examples/documentation/resolution_calculator.md || (echo "❌ Resolution Calculator documentation missing" && exit 1)
|
||||
@test -f examples/workflows/resolution_calculator_example.json || (echo "❌ Resolution Calculator workflow missing" && exit 1)
|
||||
@test -f examples/documentation/width_height_selector.md || (echo "❌ Width Height Selector documentation missing" && exit 1)
|
||||
@test -f examples/workflows/width_height_selector_example.json || (echo "❌ Width Height Selector workflow missing" && exit 1)
|
||||
@echo "✅ Documentation validated"
|
||||
@echo "🎉 Release validation completed!"
|
||||
|
||||
@@ -146,4 +148,32 @@ ci: quality-check test
|
||||
# Tool-specific commands (can be extended for new tools)
|
||||
test-resolution-calculator:
|
||||
@echo "Testing Resolution Calculator specifically..."
|
||||
@python -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
|
||||
@python -c "import sys, os; sys.path.insert(0, os.getcwd()); from kikotools.tools.resolution_calculator.node import ResolutionCalculatorNode; import torch; node = ResolutionCalculatorNode(); scenarios = [('SDXL Portrait', torch.randn(1, 1216, 832, 3), 1.5), ('FLUX Square', torch.randn(1, 1024, 1024, 3), 2.0), ('User Scenario', torch.randn(1, 1216, 832, 3), 1.53)]; [print(f'✅ {name}: {image.shape[2]}×{image.shape[1]} → {node.calculate_resolution(scale, image=image)[0]}×{node.calculate_resolution(scale, image=image)[1]} ({scale}x)') for name, image, scale in scenarios]; print('🎉 Resolution Calculator tests completed!')"
|
||||
|
||||
test-width-height-selector:
|
||||
@echo "Testing Width Height Selector specifically..."
|
||||
@python -c "\
|
||||
import sys, os; \
|
||||
sys.path.insert(0, os.getcwd()); \
|
||||
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode; \
|
||||
from kikotools.tools.width_height_selector.presets import PRESET_OPTIONS; \
|
||||
print(f'Testing Width Height Selector with {len(PRESET_OPTIONS)} presets...'); \
|
||||
node = WidthHeightSelectorNode(); \
|
||||
scenarios = [ \
|
||||
('SDXL Square', '1024×1024'), \
|
||||
('FLUX HD', '1920×1080'), \
|
||||
('SDXL Portrait', '832×1216'), \
|
||||
('Custom Dimensions', 'custom'), \
|
||||
('Ultra-Wide', '2560×1080') \
|
||||
]; \
|
||||
for name, preset in scenarios: \
|
||||
if preset == 'custom': \
|
||||
result = node.get_dimensions(preset, 1536, 768); \
|
||||
else: \
|
||||
result = node.get_dimensions(preset, 1024, 1024); \
|
||||
print(f'✅ {name}: {preset} → {result[0]}×{result[1]}'); \
|
||||
print('🎉 Width Height Selector tests completed!') \
|
||||
"
|
||||
|
||||
test-all-tools: test-resolution-calculator test-width-height-selector
|
||||
@echo "🎉 All tool-specific tests completed!"
|
||||
@@ -29,6 +29,21 @@ Calculate upscaled dimensions from image or latent inputs with precision.
|
||||
- Ensure ComfyUI tensor compatibility
|
||||
- Optimize batch processing workflows
|
||||
|
||||
#### 📏 Width Height Selector
|
||||
Advanced preset-based dimension selection with visual swap button.
|
||||
|
||||
- **26 Curated Presets**: SDXL, FLUX, and Ultra-Wide optimized resolutions
|
||||
- **Smart Categories**: Organized by model type and aspect ratio
|
||||
- **Visual Swap Button**: Modern blue button for quick orientation changes
|
||||
- **Intelligent Swapping**: Preset-aware orientation switching
|
||||
- **Custom Support**: Manual dimension input with validation
|
||||
|
||||
**Use Cases:**
|
||||
- Quick dimension selection for different models
|
||||
- Consistent aspect ratios across workflows
|
||||
- Mobile and ultra-wide format support
|
||||
- Integration with upscaling pipelines
|
||||
|
||||
### 🔧 Architecture Highlights
|
||||
|
||||
- **Modular Design**: Each tool is self-contained and independently testable
|
||||
@@ -70,6 +85,18 @@ Image Loader → Resolution Calculator → Upscaler
|
||||
**Scale:** 1.5x
|
||||
**Output:** 1248×1824 (ready for upscaling)
|
||||
|
||||
### Width Height Selector Example
|
||||
|
||||
```
|
||||
Width Height Selector → EmptyLatentImage → Model
|
||||
preset: "1920×1080" ↘ 1920×1080 ↗
|
||||
[swap button]
|
||||
```
|
||||
|
||||
**Preset:** FLUX HD (1920×1080)
|
||||
**Output:** 1920×1080 (16:9 cinematic)
|
||||
**Swap Button:** Click to get 1080×1920 (9:16 portrait)
|
||||
|
||||
### Common Workflows
|
||||
|
||||
<details>
|
||||
@@ -107,9 +134,9 @@ Image Loader → Resolution Calculator → Upscaler
|
||||
| Tool | Description | Status | Documentation |
|
||||
|------|-------------|--------|---------------|
|
||||
| **Resolution Calculator** | Calculate upscaled dimensions with model optimization | ✅ Complete | [Docs](examples/documentation/resolution_calculator.md) |
|
||||
| **Width Height Selector** | Preset-based dimension selection with 26 curated options | ✅ Complete | [Docs](examples/documentation/width_height_selector.md) |
|
||||
| **Batch Image Processor** | Process multiple images with consistent settings | 🚧 Planned | Coming Soon |
|
||||
| **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | 🚧 Planned | Coming Soon |
|
||||
| **Model Management Tools** | Efficient model loading and memory management | 🚧 Planned | Coming Soon |
|
||||
|
||||
### Technical Specifications
|
||||
|
||||
@@ -130,6 +157,27 @@ Image Loader → Resolution Calculator → Upscaler
|
||||
- Validates input tensors
|
||||
- Graceful error handling
|
||||
|
||||
#### Width Height Selector
|
||||
|
||||
**Inputs:**
|
||||
- `preset` (DROPDOWN): 26 preset options + custom
|
||||
- `width` (INT): 64-8192, step 8, default 1024
|
||||
- `height` (INT): 64-8192, step 8, default 1024
|
||||
|
||||
**Outputs:**
|
||||
- `width` (INT): Selected or calculated width
|
||||
- `height` (INT): Selected or calculated height
|
||||
|
||||
**UI Features:**
|
||||
- Visual blue swap button in bottom-right corner
|
||||
- Intelligent preset switching when swapping
|
||||
- Modern hover effects and cursor feedback
|
||||
|
||||
**Preset Categories:**
|
||||
- SDXL Presets (9): 1024×1024 to 1536×640 (~1MP optimized)
|
||||
- FLUX Presets (8): 1920×1080 to 1152×1728 (high resolution)
|
||||
- Ultra-Wide (8): 2560×1080 to 768×2304 (modern ratios)
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Prerequisites
|
||||
@@ -250,7 +298,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 📈 Stats
|
||||
|
||||
- **Nodes**: 1 (Resolution Calculator)
|
||||
- **Nodes**: 2 (Resolution Calculator, Width Height Selector)
|
||||
- **Presets**: 26 curated resolution presets
|
||||
- **Test Coverage**: 100%
|
||||
- **Python Version**: 3.8+
|
||||
- **ComfyUI Compatibility**: Latest
|
||||
|
||||
+7
-1
@@ -5,4 +5,10 @@ All nodes are grouped under the "ComfyAssets" category
|
||||
|
||||
from .kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
# Tell ComfyUI where to find our JavaScript extensions
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
# Print startup message
|
||||
print("\033[94m[ComfyUI-KikoTools] Loaded with swap button support!\033[0m")
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# Width Height Selector Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Width Height Selector is a ComfyUI node that provides preset-based dimension selection with swap functionality, optimized for SDXL and FLUX models. It offers 26 carefully curated resolution presets plus custom dimension support.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 **Preset Categories**
|
||||
- **SDXL Presets** (9): ~1MP optimized resolutions
|
||||
- **FLUX Presets** (8): Higher resolution options
|
||||
- **Ultra-Wide Presets** (8): Modern aspect ratios
|
||||
- **Custom**: Manual dimension input
|
||||
|
||||
### 🔄 **Smart Swap Logic**
|
||||
- Visual swap button in bottom-right corner
|
||||
- Intelligent orientation switching for presets
|
||||
- Simple value swap for custom dimensions
|
||||
- Maintains proper aspect ratios
|
||||
|
||||
### 🎨 **Enhanced UI**
|
||||
- Modern blue swap button with hover effects
|
||||
- Hidden swap parameter (controlled by button)
|
||||
- Clean interface with visual feedback
|
||||
|
||||
### ✅ **Built-in Validation**
|
||||
- Ensures divisible-by-8 constraint
|
||||
- Auto-sanitization of invalid inputs
|
||||
- Proper error handling and fallbacks
|
||||
|
||||
## Node Interface
|
||||
|
||||
### Inputs
|
||||
- **preset**: Dropdown with 26 preset options + custom
|
||||
- **width**: Custom width (64-8192, step 8)
|
||||
- **height**: Custom height (64-8192, step 8)
|
||||
|
||||
### Outputs
|
||||
- **width**: Integer width value
|
||||
- **height**: Integer height value
|
||||
|
||||
## Preset Reference
|
||||
|
||||
### SDXL Presets (~1 Megapixel)
|
||||
| Preset | Dimensions | Aspect Ratio | Use Case |
|
||||
|--------|------------|--------------|----------|
|
||||
| 1024×1024 | 1024×1024 | 1:1 | Square, base SDXL |
|
||||
| 896×1152 | 896×1152 | 7:9 | Moderate portrait |
|
||||
| 832×1216 | 832×1216 | 13:19 | Standard portrait |
|
||||
| 768×1344 | 768×1344 | 4:7 | Tall portrait |
|
||||
| 640×1536 | 640×1536 | 5:12 | Very tall portrait |
|
||||
| 1152×896 | 1152×896 | 9:7 | Moderate landscape |
|
||||
| 1216×832 | 1216×832 | 19:13 | Standard landscape |
|
||||
| 1344×768 | 1344×768 | 7:4 | Wide landscape |
|
||||
| 1536×640 | 1536×640 | 12:5 | Very wide landscape |
|
||||
|
||||
### FLUX Presets (High Resolution)
|
||||
| Preset | Dimensions | Aspect Ratio | Use Case |
|
||||
|--------|------------|--------------|----------|
|
||||
| 1920×1080 | 1920×1080 | 16:9 | Full HD, best quality/speed |
|
||||
| 1536×1536 | 1536×1536 | 1:1 | High-res square |
|
||||
| 1280×768 | 1280×768 | 5:3 | Cinematic wide |
|
||||
| 768×1280 | 768×1280 | 3:5 | Mobile optimized |
|
||||
| 1440×1080 | 1440×1080 | 4:3 | Classic aspect ratio |
|
||||
| 1080×1440 | 1080×1440 | 3:4 | Classic portrait |
|
||||
| 1728×1152 | 1728×1152 | 3:2 | Photography standard |
|
||||
| 1152×1728 | 1152×1728 | 2:3 | Portrait photography |
|
||||
|
||||
### Ultra-Wide Presets (Modern Ratios)
|
||||
| Preset | Dimensions | Aspect Ratio | Use Case |
|
||||
|--------|------------|--------------|----------|
|
||||
| 2560×1080 | 2560×1080 | 64:27 | Ultra-wide gaming |
|
||||
| 2048×768 | 2048×768 | 8:3 | Wide cinematic |
|
||||
| 1792×768 | 1792×768 | 7:3 | Panoramic |
|
||||
| 2304×768 | 2304×768 | 3:1 | Banner landscape |
|
||||
| 1080×2560 | 1080×2560 | 27:64 | Mobile ultra-tall |
|
||||
| 768×2048 | 768×2048 | 3:8 | Vertical cinematic |
|
||||
| 768×1792 | 768×1792 | 3:7 | Vertical panoramic |
|
||||
| 768×2304 | 768×2304 | 1:3 | Banner portrait |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
1. **Select Preset**: Choose from dropdown (e.g., "1920×1080")
|
||||
2. **Connect Outputs**: Link width/height to your target nodes
|
||||
3. **Use Swap Button**: Click blue button in bottom-right corner to swap orientation
|
||||
|
||||
### Custom Dimensions
|
||||
1. **Set Preset**: Select "custom"
|
||||
2. **Enter Dimensions**: Input width and height manually
|
||||
3. **Validation**: Automatic sanitization to divisible-by-8
|
||||
|
||||
### Orientation Swapping
|
||||
1. **Choose Preset**: Any preset (e.g., "1920×1080")
|
||||
2. **Click Swap Button**: Blue button in bottom-right corner of node
|
||||
3. **Result**: Gets 1080×1920 (landscape → portrait) or switches to equivalent preset if available
|
||||
|
||||
### Integration with Resolution Calculator
|
||||
```
|
||||
Width Height Selector → EmptyLatentImage → Resolution Calculator
|
||||
```
|
||||
1. Set base dimensions with Width Height Selector
|
||||
2. Create latent image with those dimensions
|
||||
3. Calculate upscaled dimensions with Resolution Calculator
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### SDXL Portrait Generation
|
||||
```
|
||||
Width Height Selector (832×1216) → EmptyLatentImage → SDXL Pipeline
|
||||
```
|
||||
- Perfect for portrait generation
|
||||
- Optimized ~1MP resolution
|
||||
- 13:19 aspect ratio
|
||||
|
||||
### FLUX HD Generation
|
||||
```
|
||||
Width Height Selector (1920×1080) → EmptyLatentImage → FLUX Pipeline
|
||||
```
|
||||
- Best quality/speed balance
|
||||
- Full HD resolution
|
||||
- 16:9 cinematic aspect ratio
|
||||
|
||||
### Ultra-Wide Panoramic
|
||||
```
|
||||
Width Height Selector (2560×1080) → EmptyLatentImage → Pipeline
|
||||
```
|
||||
- Modern ultra-wide format
|
||||
- Great for panoramic scenes
|
||||
- 64:27 gaming aspect ratio
|
||||
|
||||
### Upscaling Workflow
|
||||
```
|
||||
Width Height Selector → EmptyLatentImage → Resolution Calculator → Upscaler
|
||||
```
|
||||
1. Base dimensions from selector
|
||||
2. Generate at base resolution
|
||||
3. Calculate upscale dimensions
|
||||
4. Feed to upscaler node
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
### Model Optimization
|
||||
- **SDXL**: Use SDXL presets for best results (~1MP)
|
||||
- **FLUX**: Use FLUX presets for optimal quality/speed
|
||||
- **Custom**: Ensure divisible by 8 for all models
|
||||
|
||||
### Aspect Ratio Considerations
|
||||
- **Portrait**: 3:4, 2:3, 13:19 work well for people
|
||||
- **Landscape**: 16:9, 19:13, 7:4 for scenes and objects
|
||||
- **Square**: 1:1 for centered compositions
|
||||
- **Ultra-wide**: 21:9+ for panoramic and cinematic shots
|
||||
|
||||
### Memory Management
|
||||
- Higher resolutions use more VRAM
|
||||
- SDXL presets are memory-efficient
|
||||
- FLUX presets require more resources
|
||||
- Ultra-wide presets need substantial VRAM
|
||||
|
||||
### Workflow Integration
|
||||
- Always connect width/height outputs
|
||||
- Use swap button for quick orientation changes
|
||||
- Combine with Resolution Calculator for upscaling
|
||||
- Link to EmptyLatentImage for generation
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **Invalid dimensions**: Auto-sanitized to nearest valid values
|
||||
- **Memory errors**: Use lower resolution presets
|
||||
- **Orientation wrong**: Use the blue swap button instead of manual input
|
||||
- **Swap button not visible**: Ensure node is not collapsed
|
||||
|
||||
### Error Handling
|
||||
- Invalid presets fall back to custom dimensions
|
||||
- Out-of-range values are clamped and rounded
|
||||
- Division-by-8 constraint automatically enforced
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Validation Rules
|
||||
- Width/Height: 64-8192 pixels
|
||||
- Must be divisible by 8
|
||||
- Positive integers only
|
||||
|
||||
### Swap Button Implementation
|
||||
- JavaScript-based visual interface
|
||||
- Intelligent preset switching when possible
|
||||
- Falls back to custom dimensions when needed
|
||||
- Modern blue styling with hover effects
|
||||
|
||||
### Preset Organization
|
||||
- Categorized by model optimization
|
||||
- Sorted by aspect ratio within categories
|
||||
- Comprehensive tooltips for each preset
|
||||
@@ -0,0 +1,422 @@
|
||||
{
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 12,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "WidthHeightSelector",
|
||||
"pos": [100, 100],
|
||||
"size": [300, 180],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": [1, 7],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": [2, 8],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WidthHeightSelector"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1920×1080",
|
||||
1024,
|
||||
1024
|
||||
],
|
||||
"title": "Width Height Selector (FLUX HD)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "WidthHeightSelector",
|
||||
"pos": [100, 350],
|
||||
"size": [300, 180],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": [3, 9],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": [4, 10],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WidthHeightSelector"
|
||||
},
|
||||
"widgets_values": [
|
||||
"832×1216",
|
||||
1024,
|
||||
1024
|
||||
],
|
||||
"title": "Width Height Selector (SDXL Portrait) - Use swap button!"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "WidthHeightSelector",
|
||||
"pos": [100, 600],
|
||||
"size": [300, 180],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": [5, 11],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": [6, 12],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WidthHeightSelector"
|
||||
},
|
||||
"widgets_values": [
|
||||
"custom",
|
||||
1536,
|
||||
768
|
||||
],
|
||||
"title": "Width Height Selector (Custom 2:1)"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [500, 100],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"link": 1,
|
||||
"widget": {
|
||||
"name": "width"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"link": 2,
|
||||
"widget": {
|
||||
"name": "height"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
1920,
|
||||
1080,
|
||||
1
|
||||
],
|
||||
"title": "FLUX HD Latent (1920×1080)"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [500, 350],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"link": 3,
|
||||
"widget": {
|
||||
"name": "width"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"link": 4,
|
||||
"widget": {
|
||||
"name": "height"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
832,
|
||||
1216,
|
||||
1
|
||||
],
|
||||
"title": "SDXL Portrait Latent (832×1216)"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [500, 600],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"link": 5,
|
||||
"widget": {
|
||||
"name": "width"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"link": 6,
|
||||
"widget": {
|
||||
"name": "height"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
1536,
|
||||
768,
|
||||
1
|
||||
],
|
||||
"title": "Custom Latent (1536×768)"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "ResolutionCalculator",
|
||||
"pos": [900, 100],
|
||||
"size": [300, 150],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 1
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ResolutionCalculator"
|
||||
},
|
||||
"widgets_values": [
|
||||
1.5
|
||||
],
|
||||
"title": "Resolution Calculator Demo (1.5x)"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "ShowText",
|
||||
"pos": [900, 350],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowText"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Width Height Selector outputs clean INT values ready for any ComfyUI node"
|
||||
],
|
||||
"title": "Usage Note"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "ShowText",
|
||||
"pos": [900, 500],
|
||||
"size": [300, 150],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "text"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowText"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Instructions:\n1. Select presets from dropdown\n2. Use blue swap button (bottom-right) to change orientation\n3. Choose 'custom' for manual dimensions\n4. Connect width/height to any node needing dimensions"
|
||||
],
|
||||
"title": "How to Use"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [900, 700],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [7, 8, 9, 10, 11, 12],
|
||||
"shape": 3,
|
||||
"slot_index": 0,
|
||||
"widget": {
|
||||
"name": "value"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
1024
|
||||
],
|
||||
"title": "Connected Outputs Demo"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 4, 0, "INT"],
|
||||
[2, 1, 1, 4, 1, "INT"],
|
||||
[3, 2, 0, 5, 0, "INT"],
|
||||
[4, 2, 1, 5, 1, "INT"],
|
||||
[5, 3, 0, 6, 0, "INT"],
|
||||
[6, 3, 1, 6, 1, "INT"],
|
||||
[7, 1, 0, 10, 0, "INT"],
|
||||
[8, 1, 1, 10, 0, "INT"],
|
||||
[9, 2, 0, 10, 0, "INT"],
|
||||
[10, 2, 1, 10, 0, "INT"],
|
||||
[11, 3, 0, 10, 0, "INT"],
|
||||
[12, 3, 1, 10, 0, "INT"]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"title": "Width Height Selector Examples",
|
||||
"bounding": [80, 40, 360, 780],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"title": "Example Usage in Workflows",
|
||||
"bounding": [480, 40, 360, 280],
|
||||
"color": "#8A5A2B",
|
||||
"font_size": 24,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"title": "Documentation & Demos",
|
||||
"bounding": [880, 40, 360, 780],
|
||||
"color": "#8B5A8C",
|
||||
"font_size": 24,
|
||||
"locked": false
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
},
|
||||
"groupNodes": {},
|
||||
"info": {
|
||||
"name": "width_height_selector_example",
|
||||
"author": "ComfyUI-KikoTools",
|
||||
"description": "Demonstrates Width Height Selector with swap button functionality and various preset options. Shows integration with EmptyLatentImage and other nodes.",
|
||||
"version": "2.0",
|
||||
"created": "2024-12-14",
|
||||
"modified": "2024-12-14"
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -4,14 +4,17 @@ Handles automatic discovery and registration of all ComfyAssets tools
|
||||
"""
|
||||
|
||||
from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ResolutionCalculator": ResolutionCalculatorNode,
|
||||
"WidthHeightSelector": WidthHeightSelectorNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ResolutionCalculator": "Resolution Calculator",
|
||||
"WidthHeightSelector": "Width Height Selector",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Width Height Selector tool for ComfyUI-KikoTools."""
|
||||
|
||||
from .node import WidthHeightSelectorNode
|
||||
|
||||
__all__ = ["WidthHeightSelectorNode"]
|
||||
@@ -0,0 +1,264 @@
|
||||
"""Core logic for Width Height Selector tool."""
|
||||
|
||||
from typing import Tuple
|
||||
from math import gcd
|
||||
from .presets import PRESET_OPTIONS
|
||||
|
||||
|
||||
def get_preset_dimensions(
|
||||
preset: str, custom_width: int, custom_height: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Get dimensions from preset name or use custom dimensions.
|
||||
|
||||
Args:
|
||||
preset: Preset name (e.g., "1024×1024", "custom")
|
||||
custom_width: Custom width value
|
||||
custom_height: Custom height value
|
||||
|
||||
Returns:
|
||||
Tuple of (width, height) as integers
|
||||
"""
|
||||
if preset == "custom" or preset not in PRESET_OPTIONS:
|
||||
return custom_width, custom_height
|
||||
|
||||
return PRESET_OPTIONS[preset]
|
||||
|
||||
|
||||
def apply_swap_logic(width: int, height: int, swap_enabled: bool) -> Tuple[int, int]:
|
||||
"""
|
||||
Apply swap logic to dimensions.
|
||||
|
||||
Args:
|
||||
width: Original width
|
||||
height: Original height
|
||||
swap_enabled: Whether to swap dimensions
|
||||
|
||||
Returns:
|
||||
Tuple of (width, height) after potential swap
|
||||
"""
|
||||
if swap_enabled:
|
||||
return height, width
|
||||
return width, height
|
||||
|
||||
|
||||
def calculate_aspect_ratio(width: int, height: int) -> str:
|
||||
"""
|
||||
Calculate and format aspect ratio as a string.
|
||||
|
||||
Args:
|
||||
width: Image width
|
||||
height: Image height
|
||||
|
||||
Returns:
|
||||
Formatted aspect ratio string (e.g., "16:9", "4:3", "1:1")
|
||||
"""
|
||||
if width <= 0 or height <= 0:
|
||||
return "Invalid"
|
||||
|
||||
# Calculate greatest common divisor to simplify ratio
|
||||
ratio_gcd = gcd(width, height)
|
||||
ratio_width = width // ratio_gcd
|
||||
ratio_height = height // ratio_gcd
|
||||
|
||||
return f"{ratio_width}:{ratio_height}"
|
||||
|
||||
|
||||
def validate_dimensions(width: int, height: int) -> bool:
|
||||
"""
|
||||
Validate that dimensions meet ComfyUI requirements.
|
||||
|
||||
Args:
|
||||
width: Image width to validate
|
||||
height: Image height to validate
|
||||
|
||||
Returns:
|
||||
True if dimensions are valid, False otherwise
|
||||
"""
|
||||
# Check positive values
|
||||
if width <= 0 or height <= 0:
|
||||
return False
|
||||
|
||||
# Check divisible by 8 (ComfyUI requirement)
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
return False
|
||||
|
||||
# Check reasonable bounds (64 minimum, 8192 maximum)
|
||||
if not (64 <= width <= 8192) or not (64 <= height <= 8192):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def sanitize_dimensions(width: int, height: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Sanitize dimensions to meet ComfyUI requirements.
|
||||
|
||||
Rounds to nearest multiple of 8 and clamps to valid range.
|
||||
|
||||
Args:
|
||||
width: Raw width value
|
||||
height: Raw height value
|
||||
|
||||
Returns:
|
||||
Tuple of sanitized (width, height)
|
||||
"""
|
||||
# Clamp to valid range
|
||||
width = max(64, min(8192, width))
|
||||
height = max(64, min(8192, height))
|
||||
|
||||
# Round to nearest multiple of 8
|
||||
width = ((width + 4) // 8) * 8
|
||||
height = ((height + 4) // 8) * 8
|
||||
|
||||
return width, height
|
||||
|
||||
|
||||
def get_dimension_info(
|
||||
preset: str, width: int, height: int, swap_enabled: bool
|
||||
) -> dict:
|
||||
"""
|
||||
Get comprehensive dimension information including metadata.
|
||||
|
||||
Args:
|
||||
preset: Preset name
|
||||
width: Width value
|
||||
height: Height value
|
||||
swap_enabled: Whether swap is enabled
|
||||
|
||||
Returns:
|
||||
Dictionary with dimension info and metadata
|
||||
"""
|
||||
# Get base dimensions from preset or custom
|
||||
base_width, base_height = get_preset_dimensions(preset, width, height)
|
||||
|
||||
# Apply swap if enabled
|
||||
final_width, final_height = apply_swap_logic(base_width, base_height, swap_enabled)
|
||||
|
||||
# Calculate metadata
|
||||
aspect_ratio = calculate_aspect_ratio(final_width, final_height)
|
||||
is_valid = validate_dimensions(final_width, final_height)
|
||||
is_square = final_width == final_height
|
||||
is_landscape = final_width > final_height
|
||||
is_portrait = final_height > final_width
|
||||
|
||||
# Calculate megapixels
|
||||
megapixels = (final_width * final_height) / 1_000_000
|
||||
|
||||
return {
|
||||
"width": final_width,
|
||||
"height": final_height,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"is_valid": is_valid,
|
||||
"is_square": is_square,
|
||||
"is_landscape": is_landscape,
|
||||
"is_portrait": is_portrait,
|
||||
"megapixels": round(megapixels, 2),
|
||||
"preset_used": preset,
|
||||
"swap_applied": swap_enabled,
|
||||
}
|
||||
|
||||
|
||||
def format_dimension_string(width: int, height: int) -> str:
|
||||
"""
|
||||
Format dimensions as a readable string.
|
||||
|
||||
Args:
|
||||
width: Image width
|
||||
height: Image height
|
||||
|
||||
Returns:
|
||||
Formatted dimension string (e.g., "1920×1080")
|
||||
"""
|
||||
return f"{width}×{height}"
|
||||
|
||||
|
||||
def parse_dimension_string(dimension_str: str) -> Tuple[int, int]:
|
||||
"""
|
||||
Parse dimension string back to width/height integers.
|
||||
|
||||
Args:
|
||||
dimension_str: Dimension string (e.g., "1920×1080", "1920x1080")
|
||||
|
||||
Returns:
|
||||
Tuple of (width, height) as integers
|
||||
|
||||
Raises:
|
||||
ValueError: If dimension string cannot be parsed
|
||||
"""
|
||||
# Handle both × and x separators
|
||||
if "×" in dimension_str:
|
||||
parts = dimension_str.split("×")
|
||||
elif "x" in dimension_str:
|
||||
parts = dimension_str.split("x")
|
||||
else:
|
||||
raise ValueError(f"Invalid dimension string format: {dimension_str}")
|
||||
|
||||
if len(parts) != 2:
|
||||
raise ValueError(f"Dimension string must have exactly 2 parts: {dimension_str}")
|
||||
|
||||
try:
|
||||
width = int(parts[0].strip())
|
||||
height = int(parts[1].strip())
|
||||
return width, height
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Could not parse dimensions from {dimension_str}: {e}")
|
||||
|
||||
|
||||
def get_optimal_scale_factor(
|
||||
current_width: int, current_height: int, target_width: int, target_height: int
|
||||
) -> float:
|
||||
"""
|
||||
Calculate optimal scale factor to get from current to target dimensions.
|
||||
|
||||
Args:
|
||||
current_width: Current image width
|
||||
current_height: Current image height
|
||||
target_width: Target width
|
||||
target_height: Target height
|
||||
|
||||
Returns:
|
||||
Scale factor as float
|
||||
"""
|
||||
if current_width <= 0 or current_height <= 0:
|
||||
return 1.0
|
||||
|
||||
width_scale = target_width / current_width
|
||||
height_scale = target_height / current_height
|
||||
|
||||
# Return the average scale factor
|
||||
return (width_scale + height_scale) / 2.0
|
||||
|
||||
|
||||
def suggest_similar_presets(width: int, height: int, max_suggestions: int = 3) -> list:
|
||||
"""
|
||||
Suggest similar presets based on current dimensions.
|
||||
|
||||
Args:
|
||||
width: Current width
|
||||
height: Current height
|
||||
max_suggestions: Maximum number of suggestions to return
|
||||
|
||||
Returns:
|
||||
List of preset names sorted by similarity
|
||||
"""
|
||||
if width <= 0 or height <= 0:
|
||||
return []
|
||||
|
||||
current_ratio = width / height
|
||||
suggestions = []
|
||||
|
||||
for preset_name, (preset_width, preset_height) in PRESET_OPTIONS.items():
|
||||
if preset_name == "custom":
|
||||
continue
|
||||
|
||||
preset_ratio = preset_width / preset_height
|
||||
ratio_diff = abs(current_ratio - preset_ratio)
|
||||
|
||||
suggestions.append((preset_name, ratio_diff))
|
||||
|
||||
# Sort by similarity (smallest ratio difference first)
|
||||
suggestions.sort(key=lambda x: x[1])
|
||||
|
||||
# Return just the preset names
|
||||
return [preset[0] for preset in suggestions[:max_suggestions]]
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Width Height Selector node for ComfyUI."""
|
||||
|
||||
from typing import Tuple
|
||||
from ...base.base_node import ComfyAssetsBaseNode
|
||||
from .logic import (
|
||||
get_preset_dimensions,
|
||||
calculate_aspect_ratio,
|
||||
validate_dimensions,
|
||||
sanitize_dimensions,
|
||||
)
|
||||
from .presets import (
|
||||
PRESET_OPTIONS,
|
||||
PRESET_DESCRIPTIONS,
|
||||
get_model_recommendation,
|
||||
)
|
||||
|
||||
|
||||
class WidthHeightSelectorNode(ComfyAssetsBaseNode):
|
||||
"""
|
||||
Width Height Selector node for selecting image dimensions.
|
||||
|
||||
Provides preset-based dimension selection with swap functionality,
|
||||
optimized for SDXL and FLUX models with comprehensive aspect ratio support.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define the input types for the ComfyUI node."""
|
||||
# Get all preset options excluding the custom tuple
|
||||
preset_keys = [key for key in PRESET_OPTIONS.keys()]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"preset": (
|
||||
preset_keys,
|
||||
{
|
||||
"default": "custom",
|
||||
"tooltip": "Select from optimized resolution presets or use custom dimensions. "
|
||||
"SDXL presets are ~1MP, FLUX presets are higher resolution, "
|
||||
"Ultra-wide presets support modern aspect ratios.",
|
||||
},
|
||||
),
|
||||
"width": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 64,
|
||||
"max": 8192,
|
||||
"step": 8,
|
||||
"tooltip": "Custom width in pixels (must be multiple of 8). "
|
||||
"Used when preset is 'custom' or as fallback for invalid presets.",
|
||||
},
|
||||
),
|
||||
"height": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 64,
|
||||
"max": 8192,
|
||||
"step": 8,
|
||||
"tooltip": "Custom height in pixels (must be multiple of 8). "
|
||||
"Used when preset is 'custom' or as fallback for invalid presets.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("width", "height")
|
||||
FUNCTION = "get_dimensions"
|
||||
CATEGORY = "ComfyAssets"
|
||||
|
||||
def get_dimensions(self, preset: str, width: int, height: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Get width and height dimensions with preset and swap support.
|
||||
|
||||
Args:
|
||||
preset: Selected preset name or "custom"
|
||||
width: Custom width value
|
||||
height: Custom height value
|
||||
|
||||
Returns:
|
||||
Tuple of (width, height)
|
||||
"""
|
||||
try:
|
||||
# Get base dimensions from preset or custom input
|
||||
final_width, final_height = get_preset_dimensions(preset, width, height)
|
||||
|
||||
# Sanitize dimensions to ensure they meet ComfyUI requirements
|
||||
final_width, final_height = sanitize_dimensions(final_width, final_height)
|
||||
|
||||
# Validate final dimensions
|
||||
if not validate_dimensions(final_width, final_height):
|
||||
# This should not happen after sanitization, but handle gracefully
|
||||
self.handle_error(
|
||||
f"Generated invalid dimensions: {final_width}×{final_height}. "
|
||||
f"Using fallback dimensions 1024×1024."
|
||||
)
|
||||
final_width, final_height = 1024, 1024
|
||||
|
||||
return (final_width, final_height)
|
||||
|
||||
except Exception as e:
|
||||
# Handle any unexpected errors gracefully
|
||||
error_msg = (
|
||||
f"Error processing dimensions: {str(e)}. Using fallback 1024×1024."
|
||||
)
|
||||
self.handle_error(error_msg)
|
||||
return (1024, 1024)
|
||||
|
||||
def get_preset_info(self, preset: str) -> str:
|
||||
"""
|
||||
Get descriptive information about a preset.
|
||||
|
||||
Args:
|
||||
preset: Preset name
|
||||
|
||||
Returns:
|
||||
Description string for the preset
|
||||
"""
|
||||
if preset == "custom":
|
||||
return "Custom dimensions - use the width and height inputs below"
|
||||
|
||||
if preset in PRESET_DESCRIPTIONS:
|
||||
return PRESET_DESCRIPTIONS[preset]
|
||||
|
||||
# Fallback for unknown presets
|
||||
if preset in PRESET_OPTIONS:
|
||||
width, height = PRESET_OPTIONS[preset]
|
||||
aspect_ratio = calculate_aspect_ratio(width, height)
|
||||
return f"{preset} - {aspect_ratio} aspect ratio"
|
||||
|
||||
return f"Unknown preset: {preset}"
|
||||
|
||||
def get_model_optimization(self, preset: str) -> str:
|
||||
"""
|
||||
Get model optimization information for a preset.
|
||||
|
||||
Args:
|
||||
preset: Preset name
|
||||
|
||||
Returns:
|
||||
Optimization information string
|
||||
"""
|
||||
return get_model_recommendation(preset)
|
||||
|
||||
def validate_inputs(self, preset: str, width: int, height: int) -> bool:
|
||||
"""
|
||||
Validate node inputs.
|
||||
|
||||
Args:
|
||||
preset: Preset name
|
||||
width: Width value
|
||||
height: Height value
|
||||
|
||||
Returns:
|
||||
True if inputs are valid
|
||||
"""
|
||||
# Check if preset exists or is custom
|
||||
if preset != "custom" and preset not in PRESET_OPTIONS:
|
||||
return False
|
||||
|
||||
# For custom preset, validate dimensions
|
||||
if preset == "custom":
|
||||
if not validate_dimensions(width, height):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_preset_list(cls) -> list:
|
||||
"""
|
||||
Get list of available presets for external use.
|
||||
|
||||
Returns:
|
||||
List of preset names
|
||||
"""
|
||||
return list(PRESET_OPTIONS.keys())
|
||||
|
||||
@classmethod
|
||||
def get_preset_dimensions_static(cls, preset: str) -> Tuple[int, int]:
|
||||
"""
|
||||
Static method to get preset dimensions without node instance.
|
||||
|
||||
Args:
|
||||
preset: Preset name
|
||||
|
||||
Returns:
|
||||
Tuple of (width, height) or (0, 0) if invalid
|
||||
"""
|
||||
if preset in PRESET_OPTIONS:
|
||||
return PRESET_OPTIONS[preset]
|
||||
return (0, 0)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of the node."""
|
||||
return f"WidthHeightSelectorNode(presets={len(PRESET_OPTIONS)})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Detailed string representation of the node."""
|
||||
return (
|
||||
f"WidthHeightSelectorNode("
|
||||
f"presets={len(PRESET_OPTIONS)}, "
|
||||
f"category='{self.CATEGORY}', "
|
||||
f"function='{self.FUNCTION}'"
|
||||
f")"
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Preset definitions for Width Height Selector."""
|
||||
|
||||
from typing import Dict, Tuple
|
||||
|
||||
# SDXL optimized presets (~1 megapixel, dimensions divisible by 8)
|
||||
SDXL_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
# Square
|
||||
"1024×1024": (1024, 1024), # 1:1 - Base SDXL resolution
|
||||
# Portrait ratios
|
||||
"896×1152": (896, 1152), # 7:9 - Moderate portrait
|
||||
"832×1216": (832, 1216), # 13:19 - Standard portrait
|
||||
"768×1344": (768, 1344), # 4:7 - Tall portrait
|
||||
"640×1536": (640, 1536), # 5:12 - Very tall portrait
|
||||
# Landscape ratios
|
||||
"1152×896": (1152, 896), # 9:7 - Moderate landscape
|
||||
"1216×832": (1216, 832), # 19:13 - Standard landscape
|
||||
"1344×768": (1344, 768), # 7:4 - Wide landscape
|
||||
"1536×640": (1536, 640), # 12:5 - Very wide landscape
|
||||
}
|
||||
|
||||
# FLUX optimized presets (higher resolution, flexible ratios)
|
||||
FLUX_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
# Recommended high-quality resolutions
|
||||
"1920×1080": (1920, 1080), # 16:9 - Full HD landscape
|
||||
"1536×1536": (1536, 1536), # 1:1 - High-res square
|
||||
"1280×768": (1280, 768), # 5:3 - Wide landscape
|
||||
"768×1280": (768, 1280), # 3:5 - Tall portrait
|
||||
# Alternative quality resolutions
|
||||
"1440×1080": (1440, 1080), # 4:3 - Classic aspect ratio
|
||||
"1080×1440": (1080, 1440), # 3:4 - Classic portrait
|
||||
"1728×1152": (1728, 1152), # 3:2 - Photography standard
|
||||
"1152×1728": (1152, 1728), # 2:3 - Portrait photography
|
||||
}
|
||||
|
||||
# Ultra-wide and modern aspect ratios
|
||||
ULTRA_WIDE_PRESETS: Dict[str, Tuple[int, int]] = {
|
||||
# Ultra-wide landscape (21:9 and variants)
|
||||
"2560×1080": (2560, 1080), # 64:27 - Ultra-wide gaming
|
||||
"2048×768": (2048, 768), # 8:3 - Wide cinematic
|
||||
"1792×768": (1792, 768), # 7:3 - Panoramic
|
||||
# Ultra-wide portrait
|
||||
"1080×2560": (1080, 2560), # 27:64 - Mobile ultra-tall
|
||||
"768×2048": (768, 2048), # 3:8 - Vertical cinematic
|
||||
"768×1792": (768, 1792), # 3:7 - Vertical panoramic
|
||||
# Extreme ratios
|
||||
"2304×768": (2304, 768), # 3:1 - Banner landscape
|
||||
"768×2304": (768, 2304), # 1:3 - Banner portrait
|
||||
}
|
||||
|
||||
# Combined preset options for ComfyUI dropdown
|
||||
PRESET_OPTIONS: Dict[str, Tuple[int, int]] = {
|
||||
"custom": (0, 0), # Special case for custom dimensions
|
||||
**SDXL_PRESETS,
|
||||
**FLUX_PRESETS,
|
||||
**ULTRA_WIDE_PRESETS,
|
||||
}
|
||||
|
||||
# Organized preset categories for better UX
|
||||
PRESET_CATEGORIES = {
|
||||
"Custom": ["custom"],
|
||||
"SDXL Square": ["1024×1024"],
|
||||
"SDXL Portrait": ["896×1152", "832×1216", "768×1344", "640×1536"],
|
||||
"SDXL Landscape": ["1152×896", "1216×832", "1344×768", "1536×640"],
|
||||
"FLUX Recommended": ["1920×1080", "1536×1536", "1280×768", "768×1280"],
|
||||
"FLUX Alternative": ["1440×1080", "1080×1440", "1728×1152", "1152×1728"],
|
||||
"Ultra-Wide Landscape": ["2560×1080", "2048×768", "1792×768", "2304×768"],
|
||||
"Ultra-Wide Portrait": ["1080×2560", "768×2048", "768×1792", "768×2304"],
|
||||
}
|
||||
|
||||
# Preset descriptions for tooltips
|
||||
PRESET_DESCRIPTIONS = {
|
||||
# SDXL presets
|
||||
"1024×1024": "SDXL base resolution - perfect square",
|
||||
"896×1152": "SDXL portrait 7:9 - moderate portrait",
|
||||
"832×1216": "SDXL portrait 13:19 - standard portrait",
|
||||
"768×1344": "SDXL portrait 4:7 - tall portrait",
|
||||
"640×1536": "SDXL portrait 5:12 - very tall portrait",
|
||||
"1152×896": "SDXL landscape 9:7 - moderate landscape",
|
||||
"1216×832": "SDXL landscape 19:13 - standard landscape",
|
||||
"1344×768": "SDXL landscape 7:4 - wide landscape",
|
||||
"1536×640": "SDXL landscape 12:5 - very wide landscape",
|
||||
# FLUX presets
|
||||
"1920×1080": "FLUX Full HD 16:9 - best quality/speed balance",
|
||||
"1536×1536": "FLUX high-res square - premium quality",
|
||||
"1280×768": "FLUX 5:3 landscape - cinematic wide",
|
||||
"768×1280": "FLUX 3:5 portrait - mobile optimized",
|
||||
"1440×1080": "FLUX 4:3 classic - traditional aspect ratio",
|
||||
"1080×1440": "FLUX 3:4 portrait - classic portrait",
|
||||
"1728×1152": "FLUX 3:2 photo - photography standard",
|
||||
"1152×1728": "FLUX 2:3 portrait - portrait photography",
|
||||
# Ultra-wide presets
|
||||
"2560×1080": "Ultra-wide 64:27 - gaming/panoramic",
|
||||
"2048×768": "Wide cinematic 8:3 - movie aspect",
|
||||
"1792×768": "Panoramic 7:3 - landscape vista",
|
||||
"2304×768": "Banner 3:1 - extreme wide banner",
|
||||
"1080×2560": "Mobile ultra-tall 27:64 - modern phones",
|
||||
"768×2048": "Vertical cinematic 3:8 - portrait video",
|
||||
"768×1792": "Vertical panoramic 3:7 - tall vista",
|
||||
"768×2304": "Vertical banner 1:3 - extreme tall banner",
|
||||
}
|
||||
|
||||
# Model-specific recommendations
|
||||
MODEL_RECOMMENDATIONS = {
|
||||
"SDXL": list(SDXL_PRESETS.keys()),
|
||||
"FLUX": list(FLUX_PRESETS.keys()),
|
||||
"Ultra-Wide": list(ULTRA_WIDE_PRESETS.keys()),
|
||||
}
|
||||
|
||||
|
||||
def get_preset_category(preset_name: str) -> str:
|
||||
"""Get the category for a given preset name."""
|
||||
for category, presets in PRESET_CATEGORIES.items():
|
||||
if preset_name in presets:
|
||||
return category
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def get_model_recommendation(preset_name: str) -> str:
|
||||
"""Get model recommendation for a given preset."""
|
||||
if preset_name in SDXL_PRESETS:
|
||||
return "Optimized for SDXL"
|
||||
elif preset_name in FLUX_PRESETS:
|
||||
return "Optimized for FLUX"
|
||||
elif preset_name in ULTRA_WIDE_PRESETS:
|
||||
return "Modern ultra-wide ratios"
|
||||
else:
|
||||
return "Custom dimensions"
|
||||
|
||||
|
||||
def validate_preset_dimensions() -> bool:
|
||||
"""Validate that all presets meet ComfyUI requirements."""
|
||||
all_presets = {**SDXL_PRESETS, **FLUX_PRESETS, **ULTRA_WIDE_PRESETS}
|
||||
|
||||
for preset_name, (width, height) in all_presets.items():
|
||||
# Check divisible by 8
|
||||
if width % 8 != 0 or height % 8 != 0:
|
||||
print(
|
||||
f"ERROR: {preset_name} dimensions not divisible by 8: {width}×{height}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Check reasonable bounds
|
||||
if not (64 <= width <= 8192) or not (64 <= height <= 8192):
|
||||
print(f"ERROR: {preset_name} dimensions out of bounds: {width}×{height}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Validate presets on import
|
||||
if not validate_preset_dimensions():
|
||||
raise ValueError("Preset validation failed - check console for details")
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Tests for Width Height Selector tool."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from kikotools.tools.width_height_selector.node import WidthHeightSelectorNode
|
||||
from kikotools.tools.width_height_selector.logic import (
|
||||
get_preset_dimensions,
|
||||
calculate_aspect_ratio,
|
||||
validate_dimensions,
|
||||
)
|
||||
from kikotools.tools.width_height_selector.presets import (
|
||||
PRESET_OPTIONS,
|
||||
SDXL_PRESETS,
|
||||
FLUX_PRESETS,
|
||||
ULTRA_WIDE_PRESETS,
|
||||
)
|
||||
|
||||
|
||||
class TestWidthHeightSelectorNode:
|
||||
"""Test the WidthHeightSelectorNode class."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.node = WidthHeightSelectorNode()
|
||||
|
||||
def test_node_structure(self):
|
||||
"""Test that node has required ComfyUI structure."""
|
||||
# Test INPUT_TYPES
|
||||
input_types = self.node.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
assert "preset" in input_types["required"]
|
||||
assert "width" in input_types["required"]
|
||||
assert "height" in input_types["required"]
|
||||
|
||||
# Test return types
|
||||
assert self.node.RETURN_TYPES == ("INT", "INT")
|
||||
assert self.node.RETURN_NAMES == ("width", "height")
|
||||
assert self.node.FUNCTION == "get_dimensions"
|
||||
assert self.node.CATEGORY == "ComfyAssets"
|
||||
|
||||
def test_custom_dimensions(self):
|
||||
"""Test custom dimensions."""
|
||||
result = self.node.get_dimensions(preset="custom", width=1920, height=1080)
|
||||
assert result == (1920, 1080)
|
||||
|
||||
def test_sdxl_square_preset(self):
|
||||
"""Test SDXL square preset."""
|
||||
result = self.node.get_dimensions(
|
||||
preset="1024×1024",
|
||||
width=512, # Should be ignored
|
||||
height=512, # Should be ignored
|
||||
)
|
||||
assert result == (1024, 1024)
|
||||
|
||||
def test_sdxl_portrait_preset(self):
|
||||
"""Test SDXL portrait preset."""
|
||||
result = self.node.get_dimensions(preset="832×1216", width=512, height=512)
|
||||
assert result == (832, 1216)
|
||||
|
||||
def test_sdxl_landscape_preset(self):
|
||||
"""Test SDXL landscape preset."""
|
||||
result = self.node.get_dimensions(preset="1216×832", width=512, height=512)
|
||||
assert result == (1216, 832)
|
||||
|
||||
def test_flux_preset(self):
|
||||
"""Test FLUX preset."""
|
||||
result = self.node.get_dimensions(preset="1920×1080", width=512, height=512)
|
||||
assert result == (1920, 1080)
|
||||
|
||||
def test_ultra_wide_preset(self):
|
||||
"""Test ultra-wide preset."""
|
||||
result = self.node.get_dimensions(preset="2560×1080", width=512, height=512)
|
||||
assert result == (2560, 1080)
|
||||
|
||||
def test_all_presets_available(self):
|
||||
"""Test that all presets are available in INPUT_TYPES."""
|
||||
input_types = self.node.INPUT_TYPES()
|
||||
available_presets = input_types["required"]["preset"][0]
|
||||
|
||||
# Check that all major preset categories are available
|
||||
assert "custom" in available_presets
|
||||
assert "1024×1024" in available_presets # SDXL square
|
||||
assert "832×1216" in available_presets # SDXL portrait
|
||||
assert "1216×832" in available_presets # SDXL landscape
|
||||
assert "1920×1080" in available_presets # FLUX
|
||||
assert "2560×1080" in available_presets # Ultra-wide
|
||||
|
||||
def test_invalid_preset_fallback(self):
|
||||
"""Test handling of invalid preset."""
|
||||
# Should fall back to custom dimensions
|
||||
result = self.node.get_dimensions(
|
||||
preset="invalid_preset", width=800, height=600
|
||||
)
|
||||
assert result == (800, 600)
|
||||
|
||||
|
||||
class TestPresetLogic:
|
||||
"""Test the preset logic functions."""
|
||||
|
||||
def test_get_preset_dimensions_custom(self):
|
||||
"""Test getting custom dimensions."""
|
||||
width, height = get_preset_dimensions("custom", 1920, 1080)
|
||||
assert width == 1920
|
||||
assert height == 1080
|
||||
|
||||
def test_get_preset_dimensions_sdxl(self):
|
||||
"""Test getting SDXL preset dimensions."""
|
||||
width, height = get_preset_dimensions("1024×1024", 512, 512)
|
||||
assert width == 1024
|
||||
assert height == 1024
|
||||
|
||||
def test_get_preset_dimensions_flux(self):
|
||||
"""Test getting FLUX preset dimensions."""
|
||||
width, height = get_preset_dimensions("1920×1080", 512, 512)
|
||||
assert width == 1920
|
||||
assert height == 1080
|
||||
|
||||
def test_get_preset_dimensions_invalid(self):
|
||||
"""Test getting dimensions for invalid preset."""
|
||||
width, height = get_preset_dimensions("invalid", 800, 600)
|
||||
assert width == 800
|
||||
assert height == 600
|
||||
|
||||
|
||||
class TestDimensionValidation:
|
||||
"""Test dimension validation."""
|
||||
|
||||
def test_validate_dimensions_valid(self):
|
||||
"""Test validation of valid dimensions."""
|
||||
assert validate_dimensions(1024, 1024) is True
|
||||
assert validate_dimensions(1920, 1080) is True
|
||||
assert validate_dimensions(832, 1216) is True
|
||||
|
||||
def test_validate_dimensions_divisible_by_8(self):
|
||||
"""Test that dimensions must be divisible by 8."""
|
||||
assert validate_dimensions(1024, 1024) is True # Both divisible by 8
|
||||
assert validate_dimensions(1025, 1024) is False # Width not divisible by 8
|
||||
assert validate_dimensions(1024, 1025) is False # Height not divisible by 8
|
||||
assert validate_dimensions(1025, 1025) is False # Neither divisible by 8
|
||||
|
||||
def test_validate_dimensions_minimum_size(self):
|
||||
"""Test minimum dimension requirements."""
|
||||
assert validate_dimensions(64, 64) is True # Minimum allowed
|
||||
assert validate_dimensions(32, 64) is False # Width too small
|
||||
assert validate_dimensions(64, 32) is False # Height too small
|
||||
assert validate_dimensions(32, 32) is False # Both too small
|
||||
|
||||
def test_validate_dimensions_maximum_size(self):
|
||||
"""Test maximum dimension requirements."""
|
||||
assert validate_dimensions(8192, 8192) is True # Maximum allowed
|
||||
assert validate_dimensions(8200, 8192) is False # Width too large
|
||||
assert validate_dimensions(8192, 8200) is False # Height too large
|
||||
assert validate_dimensions(8200, 8200) is False # Both too large
|
||||
|
||||
|
||||
class TestPresetDefinitions:
|
||||
"""Test preset definitions."""
|
||||
|
||||
def test_sdxl_presets_structure(self):
|
||||
"""Test SDXL presets are properly defined."""
|
||||
assert "1024×1024" in SDXL_PRESETS
|
||||
assert "832×1216" in SDXL_PRESETS
|
||||
assert "1216×832" in SDXL_PRESETS
|
||||
|
||||
# Check dimensions are tuples
|
||||
for preset, dims in SDXL_PRESETS.items():
|
||||
assert isinstance(dims, tuple)
|
||||
assert len(dims) == 2
|
||||
assert isinstance(dims[0], int)
|
||||
assert isinstance(dims[1], int)
|
||||
|
||||
def test_flux_presets_structure(self):
|
||||
"""Test FLUX presets are properly defined."""
|
||||
assert "1920×1080" in FLUX_PRESETS
|
||||
assert "1536×1536" in FLUX_PRESETS
|
||||
|
||||
# Check dimensions are tuples
|
||||
for preset, dims in FLUX_PRESETS.items():
|
||||
assert isinstance(dims, tuple)
|
||||
assert len(dims) == 2
|
||||
assert isinstance(dims[0], int)
|
||||
assert isinstance(dims[1], int)
|
||||
|
||||
def test_ultra_wide_presets_structure(self):
|
||||
"""Test ultra-wide presets are properly defined."""
|
||||
assert "2560×1080" in ULTRA_WIDE_PRESETS
|
||||
|
||||
# Check dimensions are tuples
|
||||
for preset, dims in ULTRA_WIDE_PRESETS.items():
|
||||
assert isinstance(dims, tuple)
|
||||
assert len(dims) == 2
|
||||
assert isinstance(dims[0], int)
|
||||
assert isinstance(dims[1], int)
|
||||
|
||||
def test_preset_options_combined(self):
|
||||
"""Test that PRESET_OPTIONS combines all presets correctly."""
|
||||
assert "custom" in PRESET_OPTIONS
|
||||
|
||||
# Check SDXL presets are included
|
||||
for preset in SDXL_PRESETS:
|
||||
assert preset in PRESET_OPTIONS
|
||||
|
||||
# Check FLUX presets are included
|
||||
for preset in FLUX_PRESETS:
|
||||
assert preset in PRESET_OPTIONS
|
||||
|
||||
# Check ultra-wide presets are included
|
||||
for preset in ULTRA_WIDE_PRESETS:
|
||||
assert preset in PRESET_OPTIONS
|
||||
|
||||
def test_all_presets_divisible_by_8(self):
|
||||
"""Test that all preset dimensions are divisible by 8."""
|
||||
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
|
||||
for preset_name, (width, height) in preset_dict.items():
|
||||
assert width % 8 == 0, f"{preset_name} width {width} not divisible by 8"
|
||||
assert (
|
||||
height % 8 == 0
|
||||
), f"{preset_name} height {height} not divisible by 8"
|
||||
|
||||
def test_preset_dimensions_within_limits(self):
|
||||
"""Test that all preset dimensions are within acceptable limits."""
|
||||
for preset_dict in [SDXL_PRESETS, FLUX_PRESETS, ULTRA_WIDE_PRESETS]:
|
||||
for preset_name, (width, height) in preset_dict.items():
|
||||
assert 64 <= width <= 8192, f"{preset_name} width {width} out of range"
|
||||
assert (
|
||||
64 <= height <= 8192
|
||||
), f"{preset_name} height {height} out of range"
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Test edge cases and error conditions."""
|
||||
|
||||
def test_zero_dimensions(self):
|
||||
"""Test handling of zero dimensions."""
|
||||
assert validate_dimensions(0, 1024) is False
|
||||
assert validate_dimensions(1024, 0) is False
|
||||
assert validate_dimensions(0, 0) is False
|
||||
|
||||
def test_negative_dimensions(self):
|
||||
"""Test handling of negative dimensions."""
|
||||
assert validate_dimensions(-100, 1024) is False
|
||||
assert validate_dimensions(1024, -100) is False
|
||||
assert validate_dimensions(-100, -100) is False
|
||||
|
||||
def test_very_large_dimensions(self):
|
||||
"""Test handling of very large dimensions."""
|
||||
assert validate_dimensions(10000, 1024) is False
|
||||
assert validate_dimensions(1024, 10000) is False
|
||||
assert validate_dimensions(10000, 10000) is False
|
||||
|
||||
def test_aspect_ratio_edge_cases(self):
|
||||
"""Test aspect ratio calculation edge cases."""
|
||||
# Very wide aspect ratio
|
||||
ratio = calculate_aspect_ratio(3840, 1080)
|
||||
assert ratio == "32:9"
|
||||
|
||||
# Very tall aspect ratio
|
||||
ratio = calculate_aspect_ratio(1080, 3840)
|
||||
assert ratio == "9:32"
|
||||
|
||||
# Prime number dimensions
|
||||
ratio = calculate_aspect_ratio(1920, 1080)
|
||||
assert ratio == "16:9"
|
||||
@@ -0,0 +1,304 @@
|
||||
// ComfyUI-KikoTools - Width Height Selector with Swap Button
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfyassets.WidthHeightSelector",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
|
||||
if (nodeData.name === "WidthHeightSelector") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (onNodeCreated) onNodeCreated.apply(this, []);
|
||||
|
||||
// Track button click state for visual feedback
|
||||
this.swapButtonPressed = false;
|
||||
|
||||
// Override preset callback to update width/height widgets when preset changes
|
||||
const presetWidget = this.widgets.find(w => w.name === "preset");
|
||||
if (presetWidget) {
|
||||
const originalCallback = presetWidget.callback;
|
||||
presetWidget.callback = function(value, graphcanvas, node, pos, event) {
|
||||
// Call original callback first
|
||||
if (originalCallback) {
|
||||
originalCallback.call(this, value, graphcanvas, node, pos, event);
|
||||
}
|
||||
|
||||
// Update width/height widgets based on preset
|
||||
const widthWidget = node.widgets.find(w => w.name === "width");
|
||||
const heightWidget = node.widgets.find(w => w.name === "height");
|
||||
|
||||
if (widthWidget && heightWidget && value !== "custom") {
|
||||
// Define all available presets from our preset system
|
||||
const presetDimensions = {
|
||||
// SDXL Presets
|
||||
"1024×1024": [1024, 1024], "896×1152": [896, 1152], "832×1216": [832, 1216],
|
||||
"768×1344": [768, 1344], "640×1536": [640, 1536], "1152×896": [1152, 896],
|
||||
"1216×832": [1216, 832], "1344×768": [1344, 768], "1536×640": [1536, 640],
|
||||
// FLUX Presets
|
||||
"1920×1080": [1920, 1080], "1536×1536": [1536, 1536], "1280×768": [1280, 768],
|
||||
"768×1280": [768, 1280], "1440×1080": [1440, 1080], "1080×1440": [1080, 1440],
|
||||
"1728×1152": [1728, 1152], "1152×1728": [1152, 1728],
|
||||
// Ultra-Wide Presets
|
||||
"2560×1080": [2560, 1080], "2048×768": [2048, 768], "1792×768": [1792, 768],
|
||||
"2304×768": [2304, 768], "1080×2560": [1080, 2560], "768×2048": [768, 2048],
|
||||
"768×1792": [768, 1792], "768×2304": [768, 2304]
|
||||
};
|
||||
|
||||
if (presetDimensions[value]) {
|
||||
const [w, h] = presetDimensions[value];
|
||||
widthWidget.value = w;
|
||||
heightWidget.value = h;
|
||||
|
||||
// Trigger widget callbacks to update the UI
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(w, graphcanvas, node, pos, event);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(h, graphcanvas, node, pos, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add swap functionality
|
||||
this.swapDimensions = function() {
|
||||
const widthWidget = this.widgets.find(w => w.name === "width");
|
||||
const heightWidget = this.widgets.find(w => w.name === "height");
|
||||
const presetWidget = this.widgets.find(w => w.name === "preset");
|
||||
|
||||
if (widthWidget && heightWidget && presetWidget) {
|
||||
// Handle preset swapping first
|
||||
if (presetWidget.value !== "custom") {
|
||||
const currentPreset = presetWidget.value;
|
||||
|
||||
// Parse current preset dimensions (handle both × and x separators)
|
||||
let w, h;
|
||||
if (currentPreset.includes('×')) {
|
||||
[w, h] = currentPreset.split('×').map(v => parseInt(v));
|
||||
} else if (currentPreset.includes('x')) {
|
||||
[w, h] = currentPreset.split('x').map(v => parseInt(v));
|
||||
} else {
|
||||
return; // Invalid preset format
|
||||
}
|
||||
|
||||
const swappedPreset = `${h}×${w}`;
|
||||
|
||||
// Define all available presets from our preset system
|
||||
const availablePresets = [
|
||||
"custom",
|
||||
// SDXL Presets
|
||||
"1024×1024", "896×1152", "832×1216", "768×1344", "640×1536",
|
||||
"1152×896", "1216×832", "1344×768", "1536×640",
|
||||
// FLUX Presets
|
||||
"1920×1080", "1536×1536", "1280×768", "768×1280",
|
||||
"1440×1080", "1080×1440", "1728×1152", "1152×1728",
|
||||
// Ultra-Wide Presets
|
||||
"2560×1080", "2048×768", "1792×768", "2304×768",
|
||||
"1080×2560", "768×2048", "768×1792", "768×2304"
|
||||
];
|
||||
|
||||
if (availablePresets.includes(swappedPreset)) {
|
||||
// Swapped preset exists, use it
|
||||
presetWidget.value = swappedPreset;
|
||||
widthWidget.value = h;
|
||||
heightWidget.value = w;
|
||||
if (presetWidget.callback) {
|
||||
presetWidget.callback(swappedPreset, this, presetWidget);
|
||||
}
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(h, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(w, this, heightWidget);
|
||||
}
|
||||
} else {
|
||||
// Swapped preset doesn't exist, switch to custom and swap manual values
|
||||
presetWidget.value = "custom";
|
||||
widthWidget.value = h;
|
||||
heightWidget.value = w;
|
||||
|
||||
if (presetWidget.callback) {
|
||||
presetWidget.callback("custom", this, presetWidget);
|
||||
}
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(h, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(w, this, heightWidget);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Custom preset - just swap the width and height values
|
||||
const tempWidth = widthWidget.value;
|
||||
widthWidget.value = heightWidget.value;
|
||||
heightWidget.value = tempWidth;
|
||||
|
||||
// Trigger widget change events
|
||||
if (widthWidget.callback) {
|
||||
widthWidget.callback(widthWidget.value, this, widthWidget);
|
||||
}
|
||||
if (heightWidget.callback) {
|
||||
heightWidget.callback(heightWidget.value, this, heightWidget);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the graph as changed
|
||||
this.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
if (onDrawForeground) {
|
||||
onDrawForeground.apply(this, arguments);
|
||||
}
|
||||
|
||||
if (this.flags.collapsed) return;
|
||||
|
||||
// Draw swap button with consistent spacing from widgets
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX = this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Calculate button position based on widget spacing rather than bottom margin
|
||||
// Estimate widget area height and add consistent spacing
|
||||
const estimatedWidgetHeight = 90; // Approximate height for 3 widgets
|
||||
const topMargin = 35; // Space from top to first widget
|
||||
const buttonSpacing = 10; // Space between last widget and button
|
||||
const swapButtonY = topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
// Button background - change color based on pressed state
|
||||
if (this.swapButtonPressed) {
|
||||
// Darker when pressed
|
||||
ctx.fillStyle = "rgba(30, 120, 200, 0.9)"; // Darker blue when clicked
|
||||
} else {
|
||||
// Normal state
|
||||
ctx.fillStyle = "rgba(66, 165, 245, 0.8)"; // Material blue
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(swapButtonX, swapButtonY, swapButtonSize, swapButtonSize, 4);
|
||||
ctx.fill();
|
||||
|
||||
// Button border with subtle highlight
|
||||
ctx.strokeStyle = this.swapButtonPressed ? "rgba(20, 100, 180, 1.0)" : "rgba(33, 150, 243, 0.9)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw swap icon - modern double arrow design
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = "round";
|
||||
|
||||
const centerX = swapButtonX + 12;
|
||||
const centerY = swapButtonY + 12;
|
||||
|
||||
// Top arrow (pointing right) - width to height
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - 7, centerY - 3);
|
||||
ctx.lineTo(centerX + 5, centerY - 3);
|
||||
ctx.stroke();
|
||||
|
||||
// Top arrow head
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + 5, centerY - 3);
|
||||
ctx.lineTo(centerX + 2, centerY - 5);
|
||||
ctx.moveTo(centerX + 5, centerY - 3);
|
||||
ctx.lineTo(centerX + 2, centerY - 1);
|
||||
ctx.stroke();
|
||||
|
||||
// Bottom arrow (pointing left) - height to width
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX + 5, centerY + 3);
|
||||
ctx.lineTo(centerX - 7, centerY + 3);
|
||||
ctx.stroke();
|
||||
|
||||
// Bottom arrow head
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - 7, centerY + 3);
|
||||
ctx.lineTo(centerX - 4, centerY + 1);
|
||||
ctx.moveTo(centerX - 7, centerY + 3);
|
||||
ctx.lineTo(centerX - 4, centerY + 5);
|
||||
ctx.stroke();
|
||||
|
||||
// Add subtle tooltip text when hovering (if we had hover state)
|
||||
// This could be extended with hover detection for better UX
|
||||
};
|
||||
|
||||
const onMouseDown = nodeType.prototype.onMouseDown;
|
||||
nodeType.prototype.onMouseDown = function (e) {
|
||||
// Check if click is on swap button
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Use same positioning logic as drawing
|
||||
const estimatedWidgetHeight = 90;
|
||||
const topMargin = 35;
|
||||
const buttonSpacing = 10;
|
||||
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
if (
|
||||
e.canvasX >= swapButtonX &&
|
||||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||||
e.canvasY >= swapButtonY &&
|
||||
e.canvasY <= swapButtonY + swapButtonSize
|
||||
) {
|
||||
// Visual feedback - set button as pressed
|
||||
this.swapButtonPressed = true;
|
||||
this.setDirtyCanvas(true, true);
|
||||
|
||||
// Execute swap
|
||||
this.swapDimensions();
|
||||
|
||||
// Reset button state after a short delay for visual feedback
|
||||
setTimeout(() => {
|
||||
this.swapButtonPressed = false;
|
||||
this.setDirtyCanvas(true, true);
|
||||
}, 150);
|
||||
|
||||
return true; // Consume the event
|
||||
}
|
||||
|
||||
// Call original onMouseDown if not clicking swap button
|
||||
if (onMouseDown) {
|
||||
return onMouseDown.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Optional: Add hover effect for better user feedback
|
||||
const onMouseMove = nodeType.prototype.onMouseMove;
|
||||
nodeType.prototype.onMouseMove = function (e) {
|
||||
// Check if hovering over swap button
|
||||
const swapButtonSize = 24;
|
||||
const margin = 6;
|
||||
const swapButtonX = this.pos[0] + this.size[0] - swapButtonSize - margin;
|
||||
|
||||
// Use same positioning logic as drawing
|
||||
const estimatedWidgetHeight = 90;
|
||||
const topMargin = 35;
|
||||
const buttonSpacing = 10;
|
||||
const swapButtonY = this.pos[1] + topMargin + estimatedWidgetHeight + buttonSpacing;
|
||||
|
||||
const isHovering = (
|
||||
e.canvasX >= swapButtonX &&
|
||||
e.canvasX <= swapButtonX + swapButtonSize &&
|
||||
e.canvasY >= swapButtonY &&
|
||||
e.canvasY <= swapButtonY + swapButtonSize
|
||||
);
|
||||
|
||||
// Update cursor style for better UX (safely)
|
||||
if (isHovering && this.graph && this.graph.canvas && this.graph.canvas.canvas) {
|
||||
this.graph.canvas.canvas.style.cursor = "pointer";
|
||||
} else if (this.graph && this.graph.canvas && this.graph.canvas.canvas) {
|
||||
this.graph.canvas.canvas.style.cursor = "default";
|
||||
}
|
||||
|
||||
// Call original onMouseMove
|
||||
if (onMouseMove) {
|
||||
return onMouseMove.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user