Merge pull request #6 from Latentnaut/main
Fixes for ComfyUI compatibility and new usability features (Aspect Ratio Memory, Chain Traversal)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this enhanced version of BoxBox will be documented in this file.
|
||||
|
||||
## [Enhanced v1.0.0] - 2026-01-19
|
||||
|
||||
### Added
|
||||
- **Aspect Ratio Memory**: Remembers last selected aspect ratio using localStorage
|
||||
- **Selection Restoration**: Automatically restores previous selection when reopening
|
||||
- **Recursive Node Traversal**: Finds source images through intermediate processing nodes
|
||||
- **Better Error Handling**: Image error handler with console logging
|
||||
- **Improved Coordinate Logic**: Refined the balance between frontend and backend scaling to ensure pixel-perfect crops.
|
||||
- **Removed Annoying Popups**: Removed the "Immagine già piccola" alert which interrupted the workflow.
|
||||
|
||||
### Fixed
|
||||
- **Image Loading**: Updated URL construction for ComfyUI compatibility
|
||||
- **Backend Issues**:
|
||||
- Fixed temp directory to use `folder_paths.get_temp_directory()`
|
||||
- Fixed HTTP responses to return proper `web.json_response()`
|
||||
- Sanitized filenames to prevent path separator issues
|
||||
- **Coordinate Scaling**: Removed duplicate division, now handled correctly by backend
|
||||
- **Intermediate Nodes**: Now works with Brightness, Blur, and other processing nodes
|
||||
- **Coord Sync**: Reverted frontend division to allow the backend `BoxCrop` to handle scaling, preventing "double-scaling" errors.
|
||||
- **UI Polish**: Silenced the "already small" alert popup.
|
||||
|
||||
### Changed
|
||||
- Removed annoying popup alert for small images
|
||||
- Improved console logging throughout
|
||||
- Aspect ratio now stays locked when drawing new selections
|
||||
|
||||
### Technical Details
|
||||
- Added `findImageInChain()` recursive function (max depth: 20)
|
||||
- Updated `openRegionDialog()` to use chain traversal
|
||||
- Added `api` module import for proper URL construction
|
||||
- Enhanced scale endpoint with better filename extraction
|
||||
|
||||
---
|
||||
|
||||
## [Original] - Before 2026-01-19
|
||||
|
||||
Original BoxBox implementation by mercu-lore.
|
||||
@@ -1,423 +1,131 @@
|
||||
# BoxBox - Complete Image Region Processing Suite
|
||||
# BoxBox - Enhanced Version
|
||||
|
||||
A comprehensive ComfyUI custom nodes package for selecting, cropping, resizing, and reinserting image regions with full metadata tracking.
|
||||
ComfyUI custom node for interactive box/region selection with advanced features.
|
||||
|
||||
## 🎉 What's New in This Enhanced Version
|
||||
|
||||
## 🚧 *Known Issue*
|
||||
This is an improved fork of the original [BoxBox](https://github.com/mercu-lore/BoxBox) with several bug fixes and new features:
|
||||
|
||||
In some cases, when the ComfyUI window is too small or zoomed, the selection overlay may appear misaligned with the actual image.
|
||||
A fix is currently in development.
|
||||
### ✅ Bug Fixes
|
||||
|
||||
1. **Fixed Image Loading After ComfyUI Update**
|
||||
- Updated image URL construction to use ComfyUI's API helper
|
||||
- Added proper filename encoding for special characters
|
||||
- Improved error handling with fallback mechanisms
|
||||
|
||||
## 📋 Overview
|
||||
2. **Fixed Backend Issues**
|
||||
- Corrected temp directory usage to use ComfyUI's proper temp folder
|
||||
- Fixed HTTP response format (now returns proper `web.json_response`)
|
||||
- Sanitized filenames to prevent path separator issues
|
||||
|
||||
BoxBox provides a complete workflow for working with specific image regions:
|
||||
3. **Fixed Coordinate Scaling**
|
||||
- Removed duplicate coordinate division (was happening in both frontend and backend)
|
||||
- Coordinates now correctly convert from display space to original image space
|
||||
|
||||
1. **📦 BoxSelector** - Interactive interface to select rectangular regions on images
|
||||
2. **✂️ BoxCrop** - Crop selected regions from original images
|
||||
3. **📦 BoxResize** - Resize cropped regions with preset aspect ratios for AI generation
|
||||
4. **🎨 BoxReinsert** - Reinsert generated images back into original positions
|
||||
4. **Fixed Intermediate Node Support**
|
||||
- Added recursive node chain traversal to find source images
|
||||
- Now works with processing nodes (Brightness, Blur, etc.) between LoadImage and BoxSelector
|
||||
- Searches up to 20 levels deep in the node graph
|
||||
|
||||
## 🎯 Available Nodes
|
||||
5. **NumPy 2.0 Compatibility**
|
||||
- Updated backend logic with explicit casting (`np.uint8`, `np.float32`)
|
||||
- Fixed binary incompatibility issues in coordinate processing
|
||||
|
||||
### 1. BoxSelector (📦 BoxSelector)
|
||||
6. **ComfyUI v0.12.2+ (Modern API) Support**
|
||||
- Re-engineered frontend to use the modern `window.comfyAPI` system
|
||||
- Eliminated "Legacy API" deprecation warnings
|
||||
- Improved dialog instantiation with robust fallback mechanisms
|
||||
|
||||
Interactive interface to select rectangular regions on images.
|
||||
7. **Adaptive Window Scaling**
|
||||
- Fixed coordinate misalignment when the browser window is resized
|
||||
- System now dynamically calculates total scale (Server Scale * Browser CSS Scale)
|
||||
|
||||
#### Input
|
||||
- `image` (IMAGE): Source image to select from
|
||||
### 🆕 New Features
|
||||
|
||||
#### Output
|
||||
- `image` (IMAGE): Original image (passthrough)
|
||||
- `box_metadata` (STRING): JSON with selection coordinates
|
||||
1. **Aspect Ratio Memory**
|
||||
- Remembers your last selected aspect ratio using localStorage
|
||||
- Persists across browser sessions and system restarts
|
||||
- Keeps aspect ratio locked when drawing new selections
|
||||
|
||||
#### Features
|
||||
- 🖱️ **Interactive Selection**: Click and drag to draw rectangles
|
||||
- 📐 **Side Coordinates**: Returns x1, x2, y1, y2 (not x, y, width, height)
|
||||
- 🔄 **Clear Button**: Reset selection and start over
|
||||
- 📊 **Live Coordinates**: Shows selection position in real-time
|
||||
2. **Selection Restoration**
|
||||
- Automatically restores your previous selection when reopening the selector
|
||||
- Shows the blue selection box from your last session
|
||||
- Allows you to adjust or modify existing selections
|
||||
|
||||
#### Output JSON Format
|
||||
```json
|
||||
{
|
||||
"x1": 100,
|
||||
"y1": 150,
|
||||
"x2": 600,
|
||||
"y2": 450,
|
||||
"borderWidth": 3,
|
||||
"borderPosition": "inside",
|
||||
"displayScaleFactor": 1.0,
|
||||
"selected": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Advanced Features
|
||||
- 📐 **Aspect Ratio Locking**: Select from preset ratios or draw freely
|
||||
- Presets: 1:1, 3:4, 5:8, 9:16, 9:21, 4:3, 3:2, 16:9, 21:9
|
||||
- Custom: Draw freely and get closest standard ratio
|
||||
- 🔧 **Auto-Scale Large Images**: Images > 1024px automatically scaled down for smooth selection
|
||||
- Scaling factor saved in metadata (`displayScaleFactor`)
|
||||
- BoxCrop and BoxReinsert automatically correct coordinates
|
||||
|
||||
---
|
||||
|
||||
### 2. BoxCrop (✂️ BoxCrop)
|
||||
|
||||
Crops images according to coordinates provided by BoxSelector.
|
||||
|
||||
#### Input
|
||||
- `image` (IMAGE): Image to crop
|
||||
- `box_metadata` (STRING): JSON coordinates from BoxSelector (x1, x2, y1, y2)
|
||||
- Connect directly from BoxSelector output
|
||||
|
||||
#### Optional Input
|
||||
- `fallback_mode` (COMBO): Behavior when no valid coordinates
|
||||
- `use_full_image`: Return entire image (default)
|
||||
- `return_zero`: Return black image
|
||||
- `error`: Raise error
|
||||
|
||||
#### Output
|
||||
- `cropped_image` (IMAGE): Cropped region
|
||||
|
||||
#### How It Works
|
||||
1. Extracts x1, x2, y1, y2 from metadata
|
||||
2. If `displayScaleFactor` is present (from large image auto-scaling):
|
||||
- Divides coordinates by scale factor to convert from preview space to original image space
|
||||
3. Normalizes coordinates (ensures x1 < x2, y1 < y2)
|
||||
4. Clips to image boundaries
|
||||
5. Crops the specified region
|
||||
|
||||
#### Automatic Scale Correction
|
||||
When BoxSelector uses auto-scale for large images (> 1024px), BoxCrop automatically:
|
||||
- Detects the `displayScaleFactor` in metadata
|
||||
- Converts preview-space coordinates back to original-image-space
|
||||
- Ensures crops match the intended selection on the original image
|
||||
|
||||
---
|
||||
|
||||
### 3. BoxResize (📦 BoxResize)
|
||||
|
||||
Resizes cropped regions with preset aspect ratios or custom dimensions, with full metadata tracking.
|
||||
|
||||
#### Input
|
||||
- `image` (IMAGE): Image to resize
|
||||
- `size` (COMBO): Preset selection or "Custom"
|
||||
- `1:1 Square 1024x1024`
|
||||
- `3:4 Portrait 896x1152`
|
||||
- `5:8 Portrait 832x1216`
|
||||
- `9:16 Portrait 768x1344`
|
||||
- `9:21 Portrait 640x1536`
|
||||
- `4:3 Landscape 1152x896`
|
||||
- `3:2 Landscape 1216x832`
|
||||
- `16:9 Landscape 1344x768`
|
||||
- `21:9 Landscape 1536x640`
|
||||
- `Custom` (use width/height parameters)
|
||||
|
||||
#### Optional Input
|
||||
- `keep_aspect_ratio` (BOOLEAN): Maintain original aspect ratio (default: True)
|
||||
- `interpolation_mode` (COMBO): Resize algorithm
|
||||
- `bilinear` (default, faster)
|
||||
- `bicubic` (higher quality)
|
||||
- `nearest` (pixel-perfect)
|
||||
- `width` (INT): Custom width [64-8192, step 8] (when size = "Custom")
|
||||
- `height` (INT): Custom height [64-8192, step 8] (when size = "Custom")
|
||||
|
||||
#### Output
|
||||
- `image` (IMAGE): Resized image
|
||||
- `resize_metadata` (STRING): JSON with resize information
|
||||
|
||||
#### Output JSON Format
|
||||
```json
|
||||
{
|
||||
"original_width": 500,
|
||||
"original_height": 300,
|
||||
"resized_width": 1024,
|
||||
"resized_height": 1024,
|
||||
"scale_x": 2.048,
|
||||
"scale_y": 3.413,
|
||||
"size_preset": "1:1 Square 1024x1024",
|
||||
"keep_aspect_ratio": true,
|
||||
"interpolation_mode": "bilinear"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. BoxReinsert (🎨 BoxReinsert)
|
||||
|
||||
Reinserts generated images back into original positions using metadata.
|
||||
|
||||
#### Input
|
||||
- `original_image` (IMAGE): Original full-size image
|
||||
- `generated_image` (IMAGE): Generated/processed image
|
||||
- `box_metadata` (STRING): Selection coordinates from BoxSelector
|
||||
- `resize_metadata` (STRING): Resize information from BoxResize
|
||||
|
||||
#### Output
|
||||
- `image` (IMAGE): Final image with generated content reinserted
|
||||
|
||||
#### How It Works
|
||||
1. Reads box_metadata for original selection position (x1, x2, y1, y2)
|
||||
2. If `displayScaleFactor` is present (from large image auto-scaling):
|
||||
- Divides coordinates by scale factor to convert from preview space to original image space
|
||||
3. If resize_metadata is provided:
|
||||
- Resizes generated_image back to crop dimensions
|
||||
- Inserts at exact original position
|
||||
4. If resize_metadata is empty:
|
||||
- Inserts generated_image as-is (bypasses resize reversal)
|
||||
5. Returns full image with processed region in place
|
||||
|
||||
#### Automatic Scale Correction
|
||||
When BoxSelector uses auto-scale for large images (> 1024px), BoxReinsert automatically:
|
||||
- Detects the `displayScaleFactor` in metadata
|
||||
- Converts preview-space coordinates back to original-image-space
|
||||
- Reinserts the generated content at the correct position on the original image
|
||||
|
||||
#### Flexible Workflow
|
||||
- **With Resize**: Generated → De-resize → Reinsert
|
||||
- **Without Resize**: Generated → Reinsert directly
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Complete Workflow
|
||||
|
||||
### Standard AI Generation Workflow
|
||||
```
|
||||
LoadImage
|
||||
↓
|
||||
📦 BoxSelector (select region interactively)
|
||||
↓ box_metadata
|
||||
✂️ BoxCrop (extract region)
|
||||
↓ cropped_image
|
||||
📦 BoxResize (prepare for AI, e.g., 1024x1024)
|
||||
↓ resize_metadata
|
||||
[AI Generation Model - SDXL, SD, etc.]
|
||||
↓ generated_image
|
||||
🎨 BoxReinsert (put back in original position)
|
||||
↓
|
||||
Output: Final image with AI-generated region
|
||||
```
|
||||
|
||||
### Direct Editing Workflow (skip resize)
|
||||
```
|
||||
LoadImage
|
||||
↓
|
||||
📦 BoxSelector (select region)
|
||||
↓ box_metadata
|
||||
✂️ BoxCrop (extract region)
|
||||
↓ cropped_image
|
||||
[Edit/Filter directly]
|
||||
↓ edited_image
|
||||
🎨 BoxReinsert (reinsert without resize)
|
||||
↓
|
||||
Output: Final image
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Metadata System
|
||||
|
||||
The metadata system ensures complete traceability:
|
||||
|
||||
1. **BoxSelector Output**: `{"x1": ..., "y1": ..., "x2": ..., "y2": ..., "displayScaleFactor": ...}`
|
||||
- Stores original selection coordinates
|
||||
- Includes scale factor if image was auto-scaled (for large images > 1024px)
|
||||
- borderWidth and borderPosition also included
|
||||
|
||||
2. **BoxResize Output**: `{"original_width": ..., "resized_width": ..., "scale_x": ..., ...}`
|
||||
- Tracks resize transformation for reversal
|
||||
|
||||
3. **BoxCrop & BoxReinsert**: Automatically handle:
|
||||
- If `displayScaleFactor` present: Convert coordinates from preview-space to original-image-space
|
||||
- Use converted coordinates for accurate cropping and reinsertion
|
||||
- Both nodes work seamlessly with auto-scaled selections
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Settings & Presets
|
||||
|
||||
### BoxSelector
|
||||
- **Border Width**: Fixed at 3px (built-in)
|
||||
- **Border Position**: Fixed at "inside" (built-in)
|
||||
|
||||
### BoxResize Presets
|
||||
All presets optimize for AI generation models:
|
||||
- **Square**: 1:1 ratio at 1024x1024
|
||||
- **Portrait**: Multiple heights (3:4, 5:8, 9:16, 9:21)
|
||||
- **Landscape**: Multiple widths (4:3, 3:2, 16:9, 21:9)
|
||||
|
||||
### BoxResize Interpolation
|
||||
- `bilinear`: Fast, good quality (default)
|
||||
- `bicubic`: Slower, higher quality
|
||||
- `nearest`: Pixel-perfect, no smoothing
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Coordinate System
|
||||
|
||||
All nodes use **side coordinates (x1, x2, y1, y2)**:
|
||||
- `x1`: Left edge
|
||||
- `x2`: Right edge
|
||||
- `y1`: Top edge
|
||||
- `y2`: Bottom edge
|
||||
|
||||
This allows:
|
||||
- Easy understanding of selected region
|
||||
- Automatic validation (x1 < x2, y1 < y2)
|
||||
- Direct use for cropping and reinsertion
|
||||
|
||||
---
|
||||
|
||||
## 🎨 User Interface
|
||||
|
||||
### BoxSelector Dialog
|
||||
|
||||
**Left Panel (Controls)**
|
||||
- Image name
|
||||
- Aspect Ratio selector (9 presets + Custom mode)
|
||||
- Clear Selection button
|
||||
- Selection coordinates display with live aspect ratio info
|
||||
- Scale info display (when large image is auto-scaled)
|
||||
|
||||
**Right Panel (Canvas)**
|
||||
- Interactive image canvas
|
||||
- Auto-scaled preview (for images > 1024px)
|
||||
- Click + drag to draw rectangle
|
||||
- Drag handles to resize
|
||||
- Drag body to move
|
||||
- Resize handles with smooth interaction
|
||||
|
||||
**Features**
|
||||
- 🔧 **Automatic Large Image Handling**: Seamlessly scales down for preview while maintaining accuracy
|
||||
- 📐 **Aspect Ratio Locking**: Lock to standard ratios or draw freely
|
||||
- 📊 **Live Feedback**: Real-time coordinate and ratio display
|
||||
3. **Improved User Experience**
|
||||
- Removed annoying popup alerts for small images
|
||||
- Better console logging for debugging
|
||||
- Image error handler with helpful messages
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
1. Clone/copy the BoxBox folder to:
|
||||
```
|
||||
ComfyUI/custom_nodes/BoxBox/
|
||||
```
|
||||
### Method 1: ComfyUI Manager (Recommended)
|
||||
1. Open ComfyUI Manager
|
||||
2. Search for "BoxBox Enhanced"
|
||||
3. Click Install
|
||||
|
||||
2. Restart ComfyUI
|
||||
|
||||
3. Nodes will appear in `image/box` category
|
||||
|
||||
---
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- **ComfyUI**: Latest version
|
||||
- **Python**: 3.8+
|
||||
- **PyTorch**: >= 2.0.0
|
||||
- **Pillow (PIL)**: >= 8.0.0
|
||||
- **NumPy**: >= 1.20.0
|
||||
- **torch.nn.functional**: For interpolation (included with PyTorch)
|
||||
|
||||
### requirements.txt
|
||||
```
|
||||
torch>=2.0.0
|
||||
Pillow>=8.0.0
|
||||
numpy>=1.20.0
|
||||
### Method 2: Manual Installation
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/Latentnaut/BoxBox.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. Add **LoadImage** node
|
||||
2. Add **📦 BoxSelector** node, connect image
|
||||
3. Click "📦 Select Box" button
|
||||
4. Draw rectangle on image, click "✅ Confirm"
|
||||
5. Add **✂️ BoxCrop**, connect BoxSelector output
|
||||
6. Add **📦 BoxResize**, select preset (e.g., "1:1 Square 1024x1024")
|
||||
7. Send to AI model or editor
|
||||
8. Add **🎨 BoxReinsert**:
|
||||
- Connect original image from step 1
|
||||
- Connect generated image from step 7
|
||||
- Connect box_metadata from step 4 output
|
||||
- Connect resize_metadata from step 6 output
|
||||
9. Execute graph → final image!
|
||||
Then restart ComfyUI.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
## 🎯 Usage
|
||||
|
||||
### Selection Dialog Doesn't Open
|
||||
- Make sure image is connected to BoxSelector
|
||||
- Check browser console (F12) for errors
|
||||
- Try reloading page (Ctrl+Shift+R)
|
||||
1. Add a **LoadImage** node to your workflow
|
||||
2. Add a **BoxSelector** node
|
||||
3. Connect the image output to BoxSelector
|
||||
4. Click **📦 Select Box** button
|
||||
5. Draw a rectangular selection on your image
|
||||
6. Choose an aspect ratio (optional)
|
||||
7. Click **✅ Confirm**
|
||||
8. Use **BoxCrop** to crop the selected region
|
||||
9. Use **BoxResize** to resize the cropped image
|
||||
|
||||
### Drawing Rectangle Doesn't Work
|
||||
- Ensure you're clicking inside the image area
|
||||
- Check that cursor shows crosshair
|
||||
- Try refreshing the page
|
||||
### Supported Workflows
|
||||
|
||||
### Resize Creates Black/Empty Image
|
||||
- Check that `keep_aspect_ratio` is set correctly
|
||||
- Verify image dimensions are valid
|
||||
- Try different `interpolation_mode`
|
||||
|
||||
### Reinsert Doesn't Work
|
||||
- Verify all 4 inputs are connected (original_image, generated_image, box_metadata, resize_metadata)
|
||||
- Check that metadata JSON is valid
|
||||
- Ensure original_image and box_metadata match (same image used for selection)
|
||||
✅ **Direct**: LoadImage → BoxSelector → BoxCrop
|
||||
✅ **With Processing**: LoadImage → Brightness → BoxSelector → BoxCrop
|
||||
✅ **Complex Chains**: LoadImage → Node1 → Node2 → ... → BoxSelector
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
## 🔧 Nodes Included
|
||||
|
||||
### v1.1.0 (Current)
|
||||
- ✅ **Auto-Scale Large Images**: Images > 1024px automatically scaled in preview for smooth selection
|
||||
- ✅ **displayScaleFactor Metadata**: Scale factor saved in metadata for accurate coordinate conversion
|
||||
- ✅ **Aspect Ratio Locking**: 9 preset aspect ratios + custom free drawing mode
|
||||
- ✅ **Smart Coordinate Correction**: BoxCrop and BoxReinsert auto-correct coordinates from scaled previews
|
||||
- ✅ **Live Aspect Ratio Display**: Shows closest standard ratio when drawing freely
|
||||
|
||||
### v1.0.0
|
||||
- ✅ BoxSelector with interactive region selection
|
||||
- ✅ BoxCrop with coordinate-based cropping
|
||||
- ✅ BoxResize with preset aspect ratios and custom sizes
|
||||
- ✅ BoxReinsert with flexible resize handling
|
||||
- ✅ Complete metadata tracking system
|
||||
- ✅ Optional resize bypass in BoxReinsert
|
||||
- ✅ Multiple interpolation modes
|
||||
- **📦 BoxSelector**: Interactive region selection with aspect ratio locking
|
||||
- **✂️ BoxCrop**: Crops image based on selected coordinates
|
||||
- **📐 BoxResize**: Resizes images with aspect ratio preservation
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Advanced Usage
|
||||
## 🐛 Known Issues
|
||||
|
||||
### Chaining Multiple Regions
|
||||
```
|
||||
Select Region 1 → Crop → Resize → AI Generate → Reinsert
|
||||
Parallel with:
|
||||
Select Region 2 → Crop → Resize → AI Generate → Reinsert
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
Use Reroute nodes to process same coordinates on multiple images.
|
||||
|
||||
### Custom Aspect Ratios
|
||||
Use BoxResize with "Custom" mode and specify exact width/height.
|
||||
- Preview shows original image (not processed) when using intermediate nodes
|
||||
- This is intentional for performance reasons
|
||||
- Coordinates are still applied correctly to the processed image
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **Original Author**: [mercu-lore](https://github.com/mercu-lore)
|
||||
- **Enhanced Version**: Latentnaut
|
||||
- **Improvements**: AI-assisted development with Claude
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT
|
||||
Same license as the original BoxBox project.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
For issues, feature requests, or suggestions, please contact the developer.
|
||||
|
||||
---
|
||||
|
||||
**Created for ComfyUI** - Transform your images with precision! 🎯✂️📦🎨
|
||||
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Issues and pull requests are welcome! If you find bugs or have feature requests, please open an issue on GitHub.
|
||||
|
||||
+8
-15
@@ -107,38 +107,31 @@ class BoxReinsertNode:
|
||||
crop_width = box_x_end - box_x_start
|
||||
crop_height = box_y_end - box_y_start
|
||||
|
||||
# Converti immagini a PIL
|
||||
original_np = (original_image[0].cpu().numpy() * 255).astype(np.uint8)
|
||||
# Converti immagini a PIL con casting esplicito
|
||||
# original_image es (B, H, W, C)
|
||||
original_np = (original_image[0].detach().cpu().numpy() * 255).round().astype(np.uint8)
|
||||
original_pil = Image.fromarray(original_np)
|
||||
|
||||
generated_np = (generated_image[0].cpu().numpy() * 255).astype(np.uint8)
|
||||
generated_np = (generated_image[0].detach().cpu().numpy() * 255).round().astype(np.uint8)
|
||||
generated_pil = Image.fromarray(generated_np)
|
||||
|
||||
print(f"[BoxReinsertNode] Generated image size: {generated_pil.size}")
|
||||
print(f"[BoxReinsertNode] Target crop size: {crop_width}x{crop_height}")
|
||||
print(f"[BoxReinsertNode] Use resize: {use_resize}")
|
||||
|
||||
# Step 1: Ridimensiona solo se resize_metadata è fornito
|
||||
# Step 1: Ridimensiona
|
||||
if use_resize:
|
||||
# Annulla il resize fatto da BoxResize
|
||||
print(f"[BoxReinsertNode] Resizing generated image back to crop dimensions...")
|
||||
generated_resized = generated_pil.resize((crop_width, crop_height), Image.Resampling.LANCZOS)
|
||||
print(f"[BoxReinsertNode] Generated image resized from {generated_pil.size} to {generated_resized.size}")
|
||||
else:
|
||||
# Bypassa il resize e usa direttamente l'immagine generata
|
||||
generated_resized = generated_pil
|
||||
print(f"[BoxReinsertNode] Bypassing resize, using generated image as-is")
|
||||
|
||||
# Step 2: Rimetti nel punto originale
|
||||
# Crea una copia dell'immagine originale
|
||||
final_image = original_pil.copy()
|
||||
|
||||
# Incolla l'immagine generata nel punto corretto
|
||||
final_image.paste(generated_resized, (box_x_start, box_y_start))
|
||||
|
||||
# Converti back a tensor
|
||||
final_np = np.array(final_image).astype(np.float32) / 255.0
|
||||
final_tensor = torch.from_numpy(final_np).unsqueeze(0)
|
||||
# Converti back a tensor con casting sicuro per NumPy 2.0
|
||||
final_np_array = np.array(final_image, dtype=np.float32) / 255.0
|
||||
final_tensor = torch.from_numpy(final_np_array).unsqueeze(0)
|
||||
|
||||
# Assicurati che il tensor abbia le giuste dimensioni
|
||||
channels = original_image.shape[3]
|
||||
|
||||
+51
-16
@@ -38,42 +38,77 @@ class RegionSelectorNode:
|
||||
return (image, self.last_metadata)
|
||||
|
||||
|
||||
def scale_image_if_needed(filename, max_size=1024):
|
||||
input_path = folder_paths.get_input_directory()
|
||||
full_path = os.path.join(input_path, filename)
|
||||
def scale_image_if_needed(filename, type="input", subfolder="", max_size=1024):
|
||||
if type == "output":
|
||||
base_dir = folder_paths.get_output_directory()
|
||||
elif type == "temp":
|
||||
base_dir = folder_paths.get_temp_directory()
|
||||
else:
|
||||
base_dir = folder_paths.get_input_directory()
|
||||
|
||||
if subfolder:
|
||||
full_path = os.path.join(base_dir, subfolder, filename)
|
||||
else:
|
||||
full_path = os.path.join(base_dir, filename)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {"error": "file non trovato"}
|
||||
return {"error": f"file not found: {full_path}"}
|
||||
|
||||
with Image.open(full_path) as im:
|
||||
w, h = im.size
|
||||
# Use comma as separator for filename, type, subfolder to assume default behaviour or backward compatibility
|
||||
path_params = f"filename={filename}&type={type}"
|
||||
if subfolder:
|
||||
path_params += f"&subfolder={subfolder}"
|
||||
|
||||
if w <= max_size and h <= max_size:
|
||||
return {"scaled": False, "path": f"view?filename={filename}&type=input"}
|
||||
return {"scaled": False, "path": f"/view?{path_params}"}
|
||||
|
||||
scale_factor = min(max_size / w, max_size / h)
|
||||
new_size = (int(w * scale_factor), int(h * scale_factor))
|
||||
im_resized = im.resize(new_size, Image.Resampling.LANCZOS)
|
||||
|
||||
os.makedirs("temp", exist_ok=True)
|
||||
scaled_name = f"scaled_{filename}"
|
||||
scaled_path = os.path.join("temp", scaled_name)
|
||||
# Use ComfyUI's temp folder
|
||||
temp_dir = folder_paths.get_temp_directory()
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
# Sanitize filename: replace path separators with underscores
|
||||
safe_filename = filename.replace('/', '_').replace('\\', '_')
|
||||
if subfolder:
|
||||
safe_filename = f"{subfolder}_{safe_filename}".replace('/', '_').replace('\\', '_')
|
||||
|
||||
scaled_name = f"scaled_{safe_filename}"
|
||||
scaled_path = os.path.join(temp_dir, scaled_name)
|
||||
im_resized.save(scaled_path)
|
||||
print(f"[RegionSelector] Immagine scalata {w}x{h} -> {new_size[0]}x{new_size[1]}")
|
||||
print(f"[RegionSelector] Image scaled {w}x{h} -> {new_size[0]}x{new_size[1]}")
|
||||
|
||||
return {"scaled": True, "path": f"view?filename={scaled_name}&type=temp", "scale": scale_factor}
|
||||
return {"scaled": True, "path": f"/view?filename={scaled_name}&type=temp", "scale": scale_factor}
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/region_selector/scale")
|
||||
async def scale_image_endpoint(request):
|
||||
from aiohttp import web
|
||||
try:
|
||||
data = await request.json()
|
||||
# Intentar leer el JSON de forma segura
|
||||
try:
|
||||
data = await request.json()
|
||||
except:
|
||||
# Fallback si el content-type no es exacto o el cuerpo está malformado
|
||||
post_data = await request.post()
|
||||
data = dict(post_data)
|
||||
|
||||
filename = data.get("filename")
|
||||
type_ = data.get("type", "input")
|
||||
subfolder = data.get("subfolder", "")
|
||||
|
||||
if not filename:
|
||||
return {"error": "filename mancante"}
|
||||
result = scale_image_if_needed(filename)
|
||||
return result
|
||||
return web.json_response({"error": "filename missing"}, status=400)
|
||||
|
||||
result = scale_image_if_needed(filename, type_, subfolder)
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
print(f"[RegionSelector] Errore endpoint scale: {e}")
|
||||
return {"error": str(e)}
|
||||
print(f"[BoxBox] Error in scale endpoint: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"BoxSelector": RegionSelectorNode}
|
||||
|
||||
+3
-514
@@ -1,516 +1,5 @@
|
||||
/**
|
||||
* Canvas Selector - Funzionalità complete di selezione rettangoli
|
||||
* Basato sul codice di canva_html
|
||||
* Canvas Selector - Placeholder
|
||||
* Functionality moved to region_selector_extension.js to avoid duplicate code and synchronization issues.
|
||||
*/
|
||||
|
||||
function initializeCanvasSelector(container, imageUrl) {
|
||||
console.log("[CanvasSelector] Initializing with image:", imageUrl);
|
||||
|
||||
// Riferimenti agli elementi DOM (cercati dentro il container)
|
||||
const canvasContainer = container.querySelector('#canvas-container');
|
||||
const backgroundImage = container.querySelector('#background-image');
|
||||
const baseCoordinates = container.querySelector('#base-coordinates');
|
||||
const dimensionsInfo = container.querySelector('#dimensions-info');
|
||||
const currentDimensions = container.querySelector('#current-dimensions');
|
||||
const zoomInBtn = container.querySelector('#zoom-in-btn');
|
||||
const zoomOutBtn = container.querySelector('#zoom-out-btn');
|
||||
const zoomValue = container.querySelector('#zoom-value');
|
||||
const borderSlider = container.querySelector('#border-slider');
|
||||
const borderValue = container.querySelector('#border-value');
|
||||
const resetBtn = container.querySelector('#reset-btn');
|
||||
const borderPositionRadios = container.querySelectorAll('input[name="border-position"]');
|
||||
const imageUpload = container.querySelector('#image-upload');
|
||||
const uploadBtn = container.querySelector('#upload-btn');
|
||||
const imageName = container.querySelector('#image-name');
|
||||
|
||||
// Variabili di stato
|
||||
let isDrawing = false;
|
||||
let isResizing = false;
|
||||
let resizingEdge = null;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let rectStartX = 0;
|
||||
let rectStartY = 0;
|
||||
let rectStartWidth = 0;
|
||||
let rectStartHeight = 0;
|
||||
let currentRectangle = null;
|
||||
let currentZoom = 1;
|
||||
let currentBorderWidth = 0;
|
||||
let borderPosition = 'inside';
|
||||
let rectangleExists = false;
|
||||
|
||||
// Dimensioni base
|
||||
let baseWidth = 0;
|
||||
let baseHeight = 0;
|
||||
let baseX = 0;
|
||||
let baseY = 0;
|
||||
|
||||
const defaultBorderWidth = 3;
|
||||
const zoomStep = 0.25;
|
||||
const minZoom = 0.25;
|
||||
const maxZoom = 4;
|
||||
|
||||
// Carica l'immagine iniziale
|
||||
if (imageUrl) {
|
||||
backgroundImage.src = imageUrl;
|
||||
const filename = imageUrl.split('/').pop();
|
||||
imageName.textContent = `Immagine: ${filename}`;
|
||||
}
|
||||
|
||||
// ========== EVENT LISTENERS ==========
|
||||
|
||||
// Upload button
|
||||
uploadBtn.addEventListener('click', () => imageUpload.click());
|
||||
|
||||
imageUpload.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
backgroundImage.src = event.target.result;
|
||||
imageName.textContent = `Immagine: ${file.name}`;
|
||||
if (currentRectangle && confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) {
|
||||
resetRectangle();
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom buttons
|
||||
zoomInBtn.addEventListener('click', () => {
|
||||
if (currentRectangle) {
|
||||
currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
|
||||
updateZoom();
|
||||
updateAllDimensions();
|
||||
}
|
||||
});
|
||||
|
||||
zoomOutBtn.addEventListener('click', () => {
|
||||
if (currentRectangle) {
|
||||
currentZoom = Math.max(currentZoom - zoomStep, minZoom);
|
||||
updateZoom();
|
||||
updateAllDimensions();
|
||||
}
|
||||
});
|
||||
|
||||
// Border slider
|
||||
borderSlider.addEventListener('input', (e) => {
|
||||
currentBorderWidth = parseInt(e.target.value);
|
||||
borderValue.textContent = currentBorderWidth + 'px';
|
||||
|
||||
if (currentRectangle) {
|
||||
if (currentRectangle.classList.contains('border-outside')) {
|
||||
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
|
||||
} else {
|
||||
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
|
||||
}
|
||||
|
||||
if (currentBorderWidth > defaultBorderWidth) {
|
||||
currentRectangle.classList.add('thick-border');
|
||||
} else {
|
||||
currentRectangle.classList.remove('thick-border');
|
||||
}
|
||||
|
||||
updateAllDimensions();
|
||||
}
|
||||
});
|
||||
|
||||
// Border position radios
|
||||
borderPositionRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
borderPosition = e.target.value;
|
||||
|
||||
if (currentRectangle) {
|
||||
if (borderPosition === 'outside') {
|
||||
currentRectangle.classList.remove('border-inside');
|
||||
currentRectangle.classList.add('border-outside');
|
||||
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
|
||||
currentRectangle.style.borderWidth = '0px';
|
||||
} else {
|
||||
currentRectangle.classList.remove('border-outside');
|
||||
currentRectangle.classList.add('border-inside');
|
||||
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
|
||||
currentRectangle.style.outlineWidth = '0px';
|
||||
}
|
||||
|
||||
updateAllDimensions();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Reset button
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Vuoi cancellare il rettangolo e disegnarne uno nuovo?')) {
|
||||
resetRectangle();
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse down - inizio disegno
|
||||
canvasContainer.addEventListener('mousedown', (e) => {
|
||||
if (rectangleExists) return;
|
||||
if (e.target.classList.contains('resize-handle')) return;
|
||||
|
||||
const rect = canvasContainer.getBoundingClientRect();
|
||||
startX = e.clientX - rect.left;
|
||||
startY = e.clientY - rect.top;
|
||||
|
||||
isDrawing = true;
|
||||
|
||||
currentRectangle = document.createElement('div');
|
||||
currentRectangle.className = 'rectangle';
|
||||
|
||||
if (borderPosition === 'outside') {
|
||||
currentRectangle.classList.add('border-outside');
|
||||
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
|
||||
currentRectangle.style.outlineStyle = 'solid';
|
||||
currentRectangle.style.borderWidth = '0px';
|
||||
} else {
|
||||
currentRectangle.classList.add('border-inside');
|
||||
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
|
||||
}
|
||||
|
||||
currentRectangle.style.left = startX + 'px';
|
||||
currentRectangle.style.top = startY + 'px';
|
||||
currentRectangle.style.width = '0px';
|
||||
currentRectangle.style.height = '0px';
|
||||
|
||||
if (currentBorderWidth > defaultBorderWidth) {
|
||||
currentRectangle.classList.add('thick-border');
|
||||
}
|
||||
|
||||
canvasContainer.appendChild(currentRectangle);
|
||||
});
|
||||
|
||||
// Mouse move
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (isDrawing) {
|
||||
const rect = canvasContainer.getBoundingClientRect();
|
||||
const currentX = e.clientX - rect.left;
|
||||
const currentY = e.clientY - rect.top;
|
||||
|
||||
const width = currentX - startX;
|
||||
const height = currentY - startY;
|
||||
|
||||
if (width < 0) {
|
||||
currentRectangle.style.left = currentX + 'px';
|
||||
currentRectangle.style.width = Math.abs(width) + 'px';
|
||||
} else {
|
||||
currentRectangle.style.width = width + 'px';
|
||||
}
|
||||
|
||||
if (height < 0) {
|
||||
currentRectangle.style.top = currentY + 'px';
|
||||
currentRectangle.style.height = Math.abs(height) + 'px';
|
||||
} else {
|
||||
currentRectangle.style.height = height + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
if (isResizing && currentRectangle) {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
switch(resizingEdge) {
|
||||
case 'top':
|
||||
const newTop = rectStartY + deltaY;
|
||||
const newHeight = rectStartHeight - deltaY;
|
||||
if (newHeight > 0) {
|
||||
currentRectangle.style.top = newTop + 'px';
|
||||
currentRectangle.style.height = newHeight + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
const bottomHeight = rectStartHeight + deltaY;
|
||||
if (bottomHeight > 0) {
|
||||
currentRectangle.style.height = bottomHeight + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
const newLeft = rectStartX + deltaX;
|
||||
const newWidth = rectStartWidth - deltaX;
|
||||
if (newWidth > 0) {
|
||||
currentRectangle.style.left = newLeft + 'px';
|
||||
currentRectangle.style.width = newWidth + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
const rightWidth = rectStartWidth + deltaX;
|
||||
if (rightWidth > 0) {
|
||||
currentRectangle.style.width = rightWidth + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'top-left':
|
||||
const tlNewTop = rectStartY + deltaY;
|
||||
const tlNewLeft = rectStartX + deltaX;
|
||||
const tlNewHeight = rectStartHeight - deltaY;
|
||||
const tlNewWidth = rectStartWidth - deltaX;
|
||||
if (tlNewHeight > 0 && tlNewWidth > 0) {
|
||||
currentRectangle.style.top = tlNewTop + 'px';
|
||||
currentRectangle.style.left = tlNewLeft + 'px';
|
||||
currentRectangle.style.height = tlNewHeight + 'px';
|
||||
currentRectangle.style.width = tlNewWidth + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'top-right':
|
||||
const trNewTop = rectStartY + deltaY;
|
||||
const trNewHeight = rectStartHeight - deltaY;
|
||||
const trNewWidth = rectStartWidth + deltaX;
|
||||
if (trNewHeight > 0 && trNewWidth > 0) {
|
||||
currentRectangle.style.top = trNewTop + 'px';
|
||||
currentRectangle.style.height = trNewHeight + 'px';
|
||||
currentRectangle.style.width = trNewWidth + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bottom-left':
|
||||
const blNewLeft = rectStartX + deltaX;
|
||||
const blNewHeight = rectStartHeight + deltaY;
|
||||
const blNewWidth = rectStartWidth - deltaX;
|
||||
if (blNewHeight > 0 && blNewWidth > 0) {
|
||||
currentRectangle.style.left = blNewLeft + 'px';
|
||||
currentRectangle.style.height = blNewHeight + 'px';
|
||||
currentRectangle.style.width = blNewWidth + 'px';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bottom-right':
|
||||
const brNewWidth = rectStartWidth + deltaX;
|
||||
const brNewHeight = rectStartHeight + deltaY;
|
||||
if (brNewHeight > 0 && brNewWidth > 0) {
|
||||
currentRectangle.style.width = brNewWidth + 'px';
|
||||
currentRectangle.style.height = brNewHeight + 'px';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
baseX = parseFloat(currentRectangle.style.left);
|
||||
baseY = parseFloat(currentRectangle.style.top);
|
||||
baseWidth = parseFloat(currentRectangle.style.width);
|
||||
baseHeight = parseFloat(currentRectangle.style.height);
|
||||
|
||||
updateAllDimensions();
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse up
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (isDrawing) {
|
||||
const rect = canvasContainer.getBoundingClientRect();
|
||||
const endX = e.clientX - rect.left;
|
||||
const endY = e.clientY - rect.top;
|
||||
|
||||
const x1 = Math.min(startX, endX);
|
||||
const y1 = Math.min(startY, endY);
|
||||
const x2 = Math.max(startX, endX);
|
||||
const y2 = Math.max(startY, endY);
|
||||
|
||||
baseX = x1;
|
||||
baseY = y1;
|
||||
baseWidth = x2 - x1;
|
||||
baseHeight = y2 - y1;
|
||||
|
||||
rectangleExists = true;
|
||||
currentRectangle.classList.add('complete');
|
||||
|
||||
canvasContainer.classList.add('drawing-disabled');
|
||||
canvasContainer.style.cursor = 'default';
|
||||
|
||||
resetBtn.disabled = false;
|
||||
zoomInBtn.disabled = currentZoom >= maxZoom;
|
||||
zoomOutBtn.disabled = currentZoom <= minZoom;
|
||||
|
||||
dimensionsInfo.style.display = 'block';
|
||||
|
||||
addResizeHandles();
|
||||
updateAllDimensions();
|
||||
|
||||
isDrawing = false;
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
resizingEdge = null;
|
||||
canvasContainer.style.cursor = 'default';
|
||||
}
|
||||
});
|
||||
|
||||
// ========== FUNZIONI HELPER ==========
|
||||
|
||||
function resetRectangle() {
|
||||
if (currentRectangle) {
|
||||
currentRectangle.remove();
|
||||
}
|
||||
currentRectangle = null;
|
||||
rectangleExists = false;
|
||||
currentZoom = 1;
|
||||
updateZoom();
|
||||
canvasContainer.classList.remove('drawing-disabled');
|
||||
canvasContainer.style.cursor = 'crosshair';
|
||||
resetBtn.disabled = true;
|
||||
zoomInBtn.disabled = true;
|
||||
zoomOutBtn.disabled = true;
|
||||
dimensionsInfo.style.display = 'none';
|
||||
baseCoordinates.innerHTML = '<p>Clicca e trascina sull\'immagine per disegnare un rettangolo</p>';
|
||||
}
|
||||
|
||||
function updateZoom() {
|
||||
const zoomPercent = Math.round(currentZoom * 100);
|
||||
zoomValue.textContent = zoomPercent + '%';
|
||||
if (currentRectangle) {
|
||||
currentRectangle.style.transform = `scale(${currentZoom})`;
|
||||
}
|
||||
|
||||
zoomInBtn.disabled = currentZoom >= maxZoom;
|
||||
zoomOutBtn.disabled = currentZoom <= minZoom;
|
||||
}
|
||||
|
||||
function addResizeHandles() {
|
||||
const handles = ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
handles.forEach(position => {
|
||||
const handle = document.createElement('div');
|
||||
handle.className = `resize-handle ${position}`;
|
||||
handle.dataset.edge = position;
|
||||
currentRectangle.appendChild(handle);
|
||||
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
isResizing = true;
|
||||
resizingEdge = position;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
rectStartX = parseFloat(currentRectangle.style.left);
|
||||
rectStartY = parseFloat(currentRectangle.style.top);
|
||||
rectStartWidth = parseFloat(currentRectangle.style.width);
|
||||
rectStartHeight = parseFloat(currentRectangle.style.height);
|
||||
|
||||
canvasContainer.style.cursor = getComputedStyle(handle).cursor;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllDimensions() {
|
||||
if (!currentRectangle) return;
|
||||
|
||||
const baseX1 = baseX;
|
||||
const baseY1 = baseY;
|
||||
const baseX2 = baseX + baseWidth;
|
||||
const baseY2 = baseY + baseHeight;
|
||||
|
||||
const centerX = baseX + (baseWidth / 2);
|
||||
const centerY = baseY + (baseHeight / 2);
|
||||
|
||||
const zoomedWidth = baseWidth * currentZoom;
|
||||
const zoomedHeight = baseHeight * currentZoom;
|
||||
|
||||
const zoomedX1 = centerX - (zoomedWidth / 2);
|
||||
const zoomedY1 = centerY - (zoomedHeight / 2);
|
||||
const zoomedX2 = centerX + (zoomedWidth / 2);
|
||||
const zoomedY2 = centerY + (zoomedHeight / 2);
|
||||
|
||||
let effectiveX1, effectiveY1, effectiveX2, effectiveY2;
|
||||
let effectiveWidth, effectiveHeight;
|
||||
|
||||
if (borderPosition === 'inside') {
|
||||
effectiveX1 = zoomedX1;
|
||||
effectiveY1 = zoomedY1;
|
||||
effectiveX2 = zoomedX2;
|
||||
effectiveY2 = zoomedY2;
|
||||
effectiveWidth = zoomedWidth;
|
||||
effectiveHeight = zoomedHeight;
|
||||
} else {
|
||||
effectiveX1 = zoomedX1 - currentBorderWidth;
|
||||
effectiveY1 = zoomedY1 - currentBorderWidth;
|
||||
effectiveX2 = zoomedX2 + currentBorderWidth;
|
||||
effectiveY2 = zoomedY2 + currentBorderWidth;
|
||||
effectiveWidth = zoomedWidth + (currentBorderWidth * 2);
|
||||
effectiveHeight = zoomedHeight + (currentBorderWidth * 2);
|
||||
}
|
||||
|
||||
baseCoordinates.innerHTML = `
|
||||
<strong>📍 Coordinate Base:</strong><br>
|
||||
x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px<br>
|
||||
x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px<br>
|
||||
<br>
|
||||
<strong>📏 Dimensioni Base:</strong><br>
|
||||
Larghezza: ${Math.round(baseWidth)}px<br>
|
||||
Altezza: ${Math.round(baseHeight)}px
|
||||
`;
|
||||
|
||||
currentDimensions.innerHTML = `
|
||||
<strong>🔍 Con Zoom (${Math.round(currentZoom * 100)}%):</strong><br>
|
||||
Larghezza: ${Math.round(zoomedWidth)}px<br>
|
||||
Altezza: ${Math.round(zoomedHeight)}px<br>
|
||||
<br>
|
||||
<strong>🎨 Bordo ${borderPosition === 'inside' ? 'Interno' : 'Esterno'} (${currentBorderWidth}px):</strong><br>
|
||||
Larghezza Totale: ${Math.round(effectiveWidth)}px<br>
|
||||
Altezza Totale: ${Math.round(effectiveHeight)}px
|
||||
`;
|
||||
}
|
||||
|
||||
// Esporta la funzione per ottenere le coordinate
|
||||
return {
|
||||
getCoordinates: () => {
|
||||
// Calcola le coordinate dei lati (x1, x2, y1, y2) con zoom
|
||||
const baseX1 = baseX;
|
||||
const baseY1 = baseY;
|
||||
const baseX2 = baseX + baseWidth;
|
||||
const baseY2 = baseY + baseHeight;
|
||||
|
||||
// Applica lo zoom mantenendo il centro
|
||||
const centerX = baseX + (baseWidth / 2);
|
||||
const centerY = baseY + (baseHeight / 2);
|
||||
const zoomedWidth = baseWidth * currentZoom;
|
||||
const zoomedHeight = baseHeight * currentZoom;
|
||||
const zoomedX1 = centerX - (zoomedWidth / 2);
|
||||
const zoomedY1 = centerY - (zoomedHeight / 2);
|
||||
const zoomedX2 = centerX + (zoomedWidth / 2);
|
||||
const zoomedY2 = centerY + (zoomedHeight / 2);
|
||||
|
||||
// Applica il bordo
|
||||
let effectiveX1 = zoomedX1;
|
||||
let effectiveY1 = zoomedY1;
|
||||
let effectiveX2 = zoomedX2;
|
||||
let effectiveY2 = zoomedY2;
|
||||
|
||||
if (borderPosition === 'outside') {
|
||||
// Se il bordo è esterno, espandi le coordinate
|
||||
effectiveX1 = Math.max(0, zoomedX1 - currentBorderWidth);
|
||||
effectiveY1 = Math.max(0, zoomedY1 - currentBorderWidth);
|
||||
effectiveX2 = zoomedX2 + currentBorderWidth;
|
||||
effectiveY2 = zoomedY2 + currentBorderWidth;
|
||||
} else {
|
||||
// Se il bordo è interno, restringe le coordinate
|
||||
effectiveX1 = zoomedX1 + currentBorderWidth;
|
||||
effectiveY1 = zoomedY1 + currentBorderWidth;
|
||||
effectiveX2 = Math.max(effectiveX1 + 1, zoomedX2 - currentBorderWidth);
|
||||
effectiveY2 = Math.max(effectiveY1 + 1, zoomedY2 - currentBorderWidth);
|
||||
}
|
||||
|
||||
return {
|
||||
x1: Math.round(effectiveX1),
|
||||
y1: Math.round(effectiveY1),
|
||||
x2: Math.round(effectiveX2),
|
||||
y2: Math.round(effectiveY2),
|
||||
zoom: currentZoom,
|
||||
borderWidth: currentBorderWidth,
|
||||
borderPosition: borderPosition,
|
||||
};
|
||||
},
|
||||
getState: () => ({
|
||||
exists: rectangleExists,
|
||||
baseX, baseY, baseWidth, baseHeight,
|
||||
zoom: currentZoom,
|
||||
borderWidth: currentBorderWidth,
|
||||
borderPosition
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
window.CanvasSelector = { initializeCanvasSelector };
|
||||
console.log("[BoxBox] canvas_selector.js loaded (placeholder)");
|
||||
|
||||
+347
-120
@@ -1,20 +1,16 @@
|
||||
/**
|
||||
* Region Selector Extension - Integrazione nativa con ComfyUI Dialog
|
||||
* Mantiene il bottone nel nodo e apre un dialog nativo
|
||||
*/
|
||||
|
||||
// BoxBox Extension - Modernizada para ComfyUI v1.0 / v0.12.2+
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyDialog } from "../../scripts/ui.js";
|
||||
|
||||
console.log("[RegionSelectorExt] Loading extension...");
|
||||
console.log("[BoxBox] Loading extension (Modern API)...");
|
||||
|
||||
/**
|
||||
* Canvas Selector - Funzionalità complete di selezione rettangoli
|
||||
* Basato sul codice di canva_html
|
||||
*/
|
||||
|
||||
function initializeCanvasSelector(container, imageUrl) {
|
||||
function initializeCanvasSelector(container, imageUrl, previousMetadata = null) {
|
||||
console.log("[CanvasSelector] Initializing with image:", imageUrl);
|
||||
console.log("[CanvasSelector] Previous metadata:", previousMetadata);
|
||||
|
||||
// Riferimenti agli elementi DOM (cercati dentro il container)
|
||||
const canvasContainer = container.querySelector('#canvas-container');
|
||||
@@ -71,35 +67,73 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
// Ridimensionamento immagini > 1024px lato backend
|
||||
// ========================================================
|
||||
if (imageUrl) {
|
||||
const filename = imageUrl.split('/').pop();
|
||||
// Parse URL parameters
|
||||
try {
|
||||
// Create a dummy base URL to handle relative URLs comfortably
|
||||
const checkUrl = new URL(imageUrl, document.baseURI);
|
||||
const params = new URLSearchParams(checkUrl.search);
|
||||
|
||||
fetch("/region_selector/scale", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.path) {
|
||||
backgroundImage.src = data.path;
|
||||
backgroundImage.dataset.scaleFactor = data.scale || 1;
|
||||
backgroundImage.dataset.scaled = data.scaled || false;
|
||||
console.log(`[RegionSelectorExt] Immagine caricata - Scale: ${data.scale || 1}`);
|
||||
} else if (data.error) {
|
||||
console.warn(`[RegionSelectorExt] Scale error: ${data.error}`);
|
||||
const filename = params.get("filename");
|
||||
const type = params.get("type") || "input";
|
||||
const subfolder = params.get("subfolder") || "";
|
||||
|
||||
if (filename) {
|
||||
console.log(`[BoxBox] Requesting scale for: ${filename} (type: ${type}, subfolder: ${subfolder})`);
|
||||
|
||||
// Usare fetch diretto con configurazione robusta
|
||||
fetch("/region_selector/scale", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: String(filename),
|
||||
type: String(type),
|
||||
subfolder: String(subfolder)
|
||||
})
|
||||
})
|
||||
.then(async res => {
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
throw new Error(`Server returned ${res.status}: ${errText}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.path) {
|
||||
let scaledPath = data.path;
|
||||
// Assicurati che il path sia convertito correttamente per l'API di ComfyUI
|
||||
if (window.comfyAPI && window.comfyAPI.api) {
|
||||
try {
|
||||
scaledPath = window.comfyAPI.api.api.apiURL(data.path);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
backgroundImage.src = scaledPath;
|
||||
backgroundImage.dataset.scaleFactor = data.scale || 1;
|
||||
backgroundImage.dataset.scaled = data.scaled || false;
|
||||
console.log(`[BoxBox] Image loaded - Scale: ${data.scale || 1}`);
|
||||
} else if (data.error) {
|
||||
console.warn(`[BoxBox] Scale error: ${data.error}, using original`);
|
||||
backgroundImage.src = imageUrl;
|
||||
}
|
||||
imageName.textContent = `Image: ${filename}`;
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("[BoxBox] Error in scale fetch:", e);
|
||||
backgroundImage.src = imageUrl;
|
||||
imageName.textContent = `Image: ${filename}`;
|
||||
});
|
||||
} else {
|
||||
// Fallback for when filename param is missing
|
||||
console.log("[RegionSelectorExt] No filename param, using imageUrl directly");
|
||||
backgroundImage.src = imageUrl;
|
||||
backgroundImage.dataset.scaleFactor = 1;
|
||||
backgroundImage.dataset.scaled = false;
|
||||
}
|
||||
imageName.textContent = `Image: ${filename}`;
|
||||
})
|
||||
.catch(e => {
|
||||
console.error("[RegionSelectorExt] Errore scale:", e);
|
||||
} catch (e) {
|
||||
console.error("[RegionSelectorExt] URL parsing error:", e);
|
||||
backgroundImage.src = imageUrl;
|
||||
backgroundImage.dataset.scaleFactor = 1;
|
||||
backgroundImage.dataset.scaled = false;
|
||||
imageName.textContent = `Image: ${filename}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Disabilita drag dell'immagine
|
||||
@@ -124,22 +158,34 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
const aspectRatioSelect = container.querySelector('#aspect-ratio-select');
|
||||
const aspectRatioHint = container.querySelector('#aspect-ratio-hint');
|
||||
|
||||
// Load saved aspect ratio from localStorage
|
||||
const savedAspectRatio = localStorage.getItem('boxSelector_aspectRatio');
|
||||
if (savedAspectRatio && aspectRatioSelect) {
|
||||
aspectRatioSelect.value = savedAspectRatio;
|
||||
aspectRatioMode = savedAspectRatio;
|
||||
console.log(`[RegionSelectorExt] Loaded saved aspect ratio: ${savedAspectRatio}`);
|
||||
}
|
||||
|
||||
if (aspectRatioSelect) {
|
||||
aspectRatioSelect.addEventListener('change', (e) => {
|
||||
aspectRatioMode = e.target.value;
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('boxSelector_aspectRatio', aspectRatioMode);
|
||||
console.log(`[RegionSelectorExt] Saved aspect ratio: ${aspectRatioMode}`);
|
||||
|
||||
// Calcola valore numerico
|
||||
const ratioMap = {
|
||||
"free": null,
|
||||
"1:1": 1/1,
|
||||
"3:4": 3/4,
|
||||
"5:8": 5/8,
|
||||
"9:16": 9/16,
|
||||
"9:21": 9/21,
|
||||
"4:3": 4/3,
|
||||
"3:2": 3/2,
|
||||
"16:9": 16/9,
|
||||
"21:9": 21/9,
|
||||
"1:1": 1 / 1,
|
||||
"3:4": 3 / 4,
|
||||
"5:8": 5 / 8,
|
||||
"9:16": 9 / 16,
|
||||
"9:21": 9 / 21,
|
||||
"4:3": 4 / 3,
|
||||
"3:2": 3 / 2,
|
||||
"16:9": 16 / 9,
|
||||
"21:9": 21 / 9,
|
||||
};
|
||||
|
||||
aspectRatioValue = ratioMap[aspectRatioMode];
|
||||
@@ -162,6 +208,32 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
adjustRectangleToAspectRatio();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize aspectRatioValue and hint based on loaded or default value
|
||||
const ratioMap = {
|
||||
"free": null,
|
||||
"1:1": 1 / 1,
|
||||
"3:4": 3 / 4,
|
||||
"5:8": 5 / 8,
|
||||
"9:16": 9 / 16,
|
||||
"9:21": 9 / 21,
|
||||
"4:3": 4 / 3,
|
||||
"3:2": 3 / 2,
|
||||
"16:9": 16 / 9,
|
||||
"21:9": 21 / 9,
|
||||
};
|
||||
aspectRatioValue = ratioMap[aspectRatioMode];
|
||||
|
||||
if (aspectRatioMode === "free") {
|
||||
aspectRatioHint.textContent = "Disegno libero - il ratio verrà calcolato";
|
||||
aspectRatioHint.style.color = "#64748b";
|
||||
aspectRatioHint.style.fontWeight = "normal";
|
||||
} else {
|
||||
aspectRatioHint.textContent = `Rettangolo vincolato a ${aspectRatioMode}`;
|
||||
aspectRatioHint.style.color = "#16a34a";
|
||||
aspectRatioHint.style.fontWeight = "600";
|
||||
}
|
||||
console.log(`[AspectRatio] Initialized - Mode: ${aspectRatioMode}, Value: ${aspectRatioValue}`);
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -181,7 +253,7 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
const maxCurrent = Math.max(naturalW, naturalH);
|
||||
|
||||
if (maxCurrent <= maxDim) {
|
||||
alert("✓ Immagine già piccola!");
|
||||
console.log("[FixImage] Image is already small enough, no scaling needed");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,6 +330,69 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Restore previous selection if metadata exists
|
||||
if (previousMetadata) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const metadata = JSON.parse(previousMetadata);
|
||||
const scaleFactor = parseFloat(backgroundImage.dataset.scaleFactor || "1");
|
||||
|
||||
// Check if we have valid coordinates
|
||||
if (metadata.x1 !== undefined && metadata.y1 !== undefined &&
|
||||
metadata.x2 !== undefined && metadata.y2 !== undefined &&
|
||||
metadata.selected) {
|
||||
|
||||
console.log("[CanvasSelector] Restoring previous selection:", metadata);
|
||||
|
||||
// Coordinates in metadata are in display space, use them directly
|
||||
const x1 = metadata.x1;
|
||||
const y1 = metadata.y1;
|
||||
const x2 = metadata.x2;
|
||||
const y2 = metadata.y2;
|
||||
|
||||
// Calculate base position and size
|
||||
baseX = Math.min(x1, x2);
|
||||
baseY = Math.min(y1, y2);
|
||||
baseWidth = Math.abs(x2 - x1);
|
||||
baseHeight = Math.abs(y2 - y1);
|
||||
|
||||
// Create the rectangle
|
||||
currentRectangle = document.createElement('div');
|
||||
currentRectangle.className = 'rectangle complete';
|
||||
|
||||
if (borderPosition === 'outside') {
|
||||
currentRectangle.classList.add('border-outside');
|
||||
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
|
||||
currentRectangle.style.outlineStyle = 'solid';
|
||||
currentRectangle.style.borderWidth = '0px';
|
||||
} else {
|
||||
currentRectangle.classList.add('border-inside');
|
||||
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
|
||||
}
|
||||
|
||||
currentRectangle.style.left = baseX + 'px';
|
||||
currentRectangle.style.top = baseY + 'px';
|
||||
currentRectangle.style.width = baseWidth + 'px';
|
||||
currentRectangle.style.height = baseHeight + 'px';
|
||||
|
||||
canvasContainer.appendChild(currentRectangle);
|
||||
rectangleExists = true;
|
||||
canvasContainer.classList.add('drawing-disabled');
|
||||
canvasContainer.style.cursor = 'default';
|
||||
resetBtn.disabled = false;
|
||||
dimensionsInfo.style.display = 'block';
|
||||
|
||||
addResizeHandles();
|
||||
updateAllDimensions();
|
||||
|
||||
console.log("[CanvasSelector] Previous selection restored successfully");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[CanvasSelector] Failed to restore previous selection:", e);
|
||||
}
|
||||
}, 600); // Wait a bit longer than the auto-fix to ensure everything is ready
|
||||
}
|
||||
|
||||
// Mouse down - inizio disegno o drag
|
||||
canvasContainer.addEventListener('mousedown', (e) => {
|
||||
// Se il rettangolo esiste e clicchi su di esso (non su un handle), inizia il drag
|
||||
@@ -284,22 +419,9 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
currentRectangle = document.createElement('div');
|
||||
currentRectangle.className = 'rectangle';
|
||||
|
||||
// Se sei in modalità vincolata, resetta a free (l'utente sta facendo una nuova selezione)
|
||||
if (aspectRatioMode !== "free") {
|
||||
const aspectRatioSelect = container.querySelector('#aspect-ratio-select');
|
||||
if (aspectRatioSelect) {
|
||||
aspectRatioSelect.value = "free";
|
||||
aspectRatioMode = "free";
|
||||
aspectRatioValue = null;
|
||||
const aspectRatioHint = container.querySelector('#aspect-ratio-hint');
|
||||
if (aspectRatioHint) {
|
||||
aspectRatioHint.textContent = "Disegno libero - il ratio verrà calcolato";
|
||||
aspectRatioHint.style.color = "#64748b";
|
||||
aspectRatioHint.style.fontWeight = "normal";
|
||||
}
|
||||
console.log("[AspectRatio] Reset to FREE mode for new selection");
|
||||
}
|
||||
}
|
||||
// Keep the aspect ratio locked if it was set
|
||||
// (removed auto-reset to 'free' mode so it remembers user preference)
|
||||
|
||||
|
||||
if (borderPosition === 'outside') {
|
||||
currentRectangle.classList.add('border-outside');
|
||||
@@ -394,7 +516,7 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
|
||||
switch(resizingEdge) {
|
||||
switch (resizingEdge) {
|
||||
case 'bottom-right':
|
||||
let brNewWidth = rectStartWidth + deltaX;
|
||||
let brNewHeight = rectStartHeight + deltaY;
|
||||
@@ -703,15 +825,15 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
if (aspectRatioMode === "free") {
|
||||
// Lista aspect ratio standard
|
||||
const standardRatios = [
|
||||
{ value: 21/9, label: "21:9 Landscape", display: "21:9" },
|
||||
{ value: 16/9, label: "16:9 Landscape", display: "16:9" },
|
||||
{ value: 3/2, label: "3:2 Landscape", display: "3:2" },
|
||||
{ value: 4/3, label: "4:3 Landscape", display: "4:3" },
|
||||
{ value: 1/1, label: "1:1 Square", display: "1:1" },
|
||||
{ value: 3/4, label: "3:4 Portrait", display: "3:4" },
|
||||
{ value: 5/8, label: "5:8 Portrait", display: "5:8" },
|
||||
{ value: 9/16, label: "9:16 Portrait", display: "9:16" },
|
||||
{ value: 9/21, label: "9:21 Portrait", display: "9:21" },
|
||||
{ value: 21 / 9, label: "21:9 Landscape", display: "21:9" },
|
||||
{ value: 16 / 9, label: "16:9 Landscape", display: "16:9" },
|
||||
{ value: 3 / 2, label: "3:2 Landscape", display: "3:2" },
|
||||
{ value: 4 / 3, label: "4:3 Landscape", display: "4:3" },
|
||||
{ value: 1 / 1, label: "1:1 Square", display: "1:1" },
|
||||
{ value: 3 / 4, label: "3:4 Portrait", display: "3:4" },
|
||||
{ value: 5 / 8, label: "5:8 Portrait", display: "5:8" },
|
||||
{ value: 9 / 16, label: "9:16 Portrait", display: "9:16" },
|
||||
{ value: 9 / 21, label: "9:21 Portrait", display: "9:21" },
|
||||
];
|
||||
|
||||
// Trova il più vicino
|
||||
@@ -759,8 +881,12 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
|
||||
return {
|
||||
getCoordinates: () => {
|
||||
// Leggi il fattore di scala salvato dal backend
|
||||
const scaleFactor = parseFloat(backgroundImage.dataset.scaleFactor || "1");
|
||||
// Calcola il fattore di scala combinato (Backend + Browser CSS)
|
||||
const serverScale = parseFloat(backgroundImage.dataset.scaleFactor || "1");
|
||||
const browserScale = backgroundImage.offsetWidth / (backgroundImage.naturalWidth || backgroundImage.offsetWidth);
|
||||
const totalScale = serverScale * browserScale;
|
||||
|
||||
console.log(`[BoxBox] Getting coordinates - ServerScale: ${serverScale}, BrowserScale: ${browserScale.toFixed(3)}, Total: ${totalScale.toFixed(3)}`);
|
||||
|
||||
const baseX1 = baseX;
|
||||
const baseY1 = baseY;
|
||||
@@ -784,6 +910,12 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
effectiveY2 = Math.max(effectiveY1 + 1, baseY2 - currentBorderWidth);
|
||||
}
|
||||
|
||||
// Convert coordinates from scaled space back to original image space
|
||||
// REMOVED: BoxCrop backend already handles this conversion
|
||||
// Return display coordinates as-is, BoxCrop will divide by displayScaleFactor
|
||||
|
||||
console.log(`[BoxBox] Final coordinates: (${Math.round(effectiveX1)}, ${Math.round(effectiveY1)}) to (${Math.round(effectiveX2)}, ${Math.round(effectiveY2)})`);
|
||||
|
||||
return {
|
||||
x1: Math.round(effectiveX1),
|
||||
y1: Math.round(effectiveY1),
|
||||
@@ -791,7 +923,7 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
y2: Math.round(effectiveY2),
|
||||
borderWidth: currentBorderWidth,
|
||||
borderPosition: borderPosition,
|
||||
displayScaleFactor: displayScaleFactor,
|
||||
displayScaleFactor: totalScale,
|
||||
};
|
||||
},
|
||||
getState: () => ({
|
||||
@@ -805,27 +937,35 @@ function initializeCanvasSelector(container, imageUrl) {
|
||||
|
||||
window.CanvasSelector = { initializeCanvasSelector };
|
||||
|
||||
console.log("[BoxBox] Preparing to register extension...");
|
||||
|
||||
app.registerExtension({
|
||||
name: "BoxBox.BoxSelectorExtension",
|
||||
|
||||
async setup(app) {
|
||||
console.log("[RegionSelectorExt] Setup called");
|
||||
console.log("[BoxBox] Setup extension called");
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Log every node to see what's happening
|
||||
if (nodeData.name && nodeData.name.includes("Box")) {
|
||||
console.log(`[BoxBox] Checking node: ${nodeData.name}`);
|
||||
}
|
||||
|
||||
if (nodeData.name !== "BoxSelector") return;
|
||||
|
||||
console.log("[RegionSelectorExt] Registering BoxSelector...");
|
||||
console.log("[BoxBox] Found BoxSelector node! Adding button...");
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated?.apply(this, arguments);
|
||||
const node = this;
|
||||
|
||||
console.log("[RegionSelectorExt] Node created, adding button...");
|
||||
console.log("[BoxBox] Node instance created, attaching button widget...");
|
||||
|
||||
// Aggiungi bottone nativo ComfyUI
|
||||
// Add native ComfyUI button
|
||||
this.addWidget("button", "📦 Select Box", null, () => {
|
||||
console.log("[BoxBox] Button clicked!");
|
||||
openRegionDialog(node, app);
|
||||
});
|
||||
|
||||
@@ -835,62 +975,143 @@ app.registerExtension({
|
||||
});
|
||||
|
||||
/**
|
||||
* Apre il dialog del selettore di regioni
|
||||
* Recursively searches for image metadata by traversing the node chain backwards
|
||||
* @returns {Object|null} - Image metadata {filename, type, subfolder} if found, null otherwise
|
||||
*/
|
||||
async function openRegionDialog(node, app) {
|
||||
console.log("[RegionSelectorExt] Opening dialog...");
|
||||
console.log("[RegionSelectorExt] Node inputs:", node.inputs);
|
||||
console.log("[RegionSelectorExt] Node widgets:", node.widgets);
|
||||
function findImageInChain(node, app, depth = 0, maxDepth = 20) {
|
||||
if (depth > maxDepth) return null;
|
||||
|
||||
let imageName = null;
|
||||
let imageUrl = null;
|
||||
console.log(`[RegionSelectorExt] Searching at depth ${depth}, node type: ${node.type} (${node.comfyClass})`);
|
||||
|
||||
// Metodo 1: Se c'è un input collegato
|
||||
if (node.inputs && node.inputs[0] && node.inputs[0].link !== undefined && node.inputs[0].link !== null) {
|
||||
const imageInput = node.inputs[0];
|
||||
const link = app.graph.links[imageInput.link];
|
||||
// 1. Check if the node has preview images (standard ComfyUI way)
|
||||
if (node.imgs && node.imgs.length > 0) {
|
||||
const img = node.imgs[0];
|
||||
// Ensure it's a real image and not just a placeholder from some nodes
|
||||
if (img.filename && !img.filename.startsWith("$")) {
|
||||
console.log(`[RegionSelectorExt] ✅ Found image in node.imgs at depth ${depth}:`, img);
|
||||
return {
|
||||
filename: img.filename,
|
||||
type: img.type || "temp",
|
||||
subfolder: img.subfolder || ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (link) {
|
||||
const sourceNode = app.graph._nodes_by_id[link.origin_id];
|
||||
console.log("[RegionSelectorExt] Source node found:", sourceNode);
|
||||
|
||||
// Esegui il nodo sorgente per ottenere l'immagine
|
||||
try {
|
||||
await executeSourceNode(sourceNode, app);
|
||||
|
||||
// Dopo l'esecuzione, prova a prendere il filename dal nodo sorgente
|
||||
if (sourceNode.widgets) {
|
||||
const filenameWidget = sourceNode.widgets.find(w => w.name === "image");
|
||||
if (filenameWidget && filenameWidget.value) {
|
||||
imageName = filenameWidget.value;
|
||||
console.log("[RegionSelectorExt] Image from source node:", imageName);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[RegionSelectorExt] Error executing source node:", e);
|
||||
// 2. Check for common widgets that hold image names
|
||||
if (node.widgets) {
|
||||
// Look for any widget that might contain a filename
|
||||
const imageWidget = node.widgets.find(w => w.name === "image" || w.name === "image_name");
|
||||
if (imageWidget && imageWidget.value && typeof imageWidget.value === "string") {
|
||||
// Check if it looks like a real filename (has extension) or a special ID
|
||||
if (imageWidget.value.includes(".") || imageWidget.value.startsWith("$")) {
|
||||
console.log(`[RegionSelectorExt] ✅ Found image widget at depth ${depth}:`, imageWidget.name, "=", imageWidget.value);
|
||||
return {
|
||||
filename: imageWidget.value,
|
||||
type: "input", // Fallback, will be corrected for $ IDs later
|
||||
subfolder: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Metodo 2: Prova a prendere l'immagine dal widget del nodo stesso
|
||||
if (!imageName) {
|
||||
const imageWidget = node.widgets?.find((w) => w.name === "image");
|
||||
if (imageWidget && imageWidget.value) {
|
||||
imageName = imageWidget.value;
|
||||
console.log("[RegionSelectorExt] Image from widget:", imageName);
|
||||
// 3. Traverse backwards through IMAGE inputs
|
||||
if (node.inputs && node.inputs.length > 0) {
|
||||
for (const input of node.inputs) {
|
||||
if (input.type === "IMAGE" && input.link !== undefined && input.link !== null) {
|
||||
const link = app.graph.links[input.link];
|
||||
if (link) {
|
||||
const sourceNode = app.graph._nodes_by_id[link.origin_id];
|
||||
if (sourceNode) {
|
||||
const result = findImageInChain(sourceNode, app, depth + 1, maxDepth);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageName) {
|
||||
alert("⚠️ Nessuna immagine collegata al nodo!\n\nCollega un'immagine dal nodo LoadImage.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apre il dialog del selettore di regioni
|
||||
*/
|
||||
async function openRegionDialog(node, app) {
|
||||
console.log("[RegionSelectorExt] Opening dialog...");
|
||||
|
||||
let imageInfo = null;
|
||||
|
||||
// Search in the connected node chain
|
||||
if (node.inputs && node.inputs[0]?.link != null) {
|
||||
const link = app.graph.links[node.inputs[0].link];
|
||||
if (link) {
|
||||
const sourceNode = app.graph._nodes_by_id[link.origin_id];
|
||||
imageInfo = findImageInChain(sourceNode, app);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check the node itself
|
||||
if (!imageInfo) {
|
||||
imageInfo = findImageInChain(node, app);
|
||||
}
|
||||
|
||||
if (!imageInfo || !imageInfo.filename) {
|
||||
alert("⚠️ No image found!\n\nPlease connect a node that shows an image (like LoadImage or PreviewBridge) and ensure it has executed at least once.");
|
||||
return;
|
||||
}
|
||||
|
||||
imageUrl = `/view?filename=${imageName}&type=input`;
|
||||
console.log("[RegionSelectorExt] Image URL:", imageUrl);
|
||||
// SPECIAL HANDLING: Impact Pack PreviewBridge IDs ($...)
|
||||
if (imageInfo.filename.startsWith("$")) {
|
||||
console.log("[RegionSelectorExt] Detected PreviewBridge ID, attempting to resolve:", imageInfo.filename);
|
||||
try {
|
||||
const response = await fetch(`/impact/get/pb_id_image?id=${encodeURIComponent(imageInfo.filename)}`);
|
||||
if (response.ok) {
|
||||
const pbInfo = await response.json();
|
||||
console.log("[RegionSelectorExt] Resolved PreviewBridge ID:", pbInfo);
|
||||
imageInfo = {
|
||||
filename: pbInfo.filename,
|
||||
type: pbInfo.type || "temp",
|
||||
subfolder: pbInfo.subfolder || ""
|
||||
};
|
||||
} else {
|
||||
console.warn("[RegionSelectorExt] Failed to resolve PreviewBridge ID via API");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[RegionSelectorExt] Error resolving PreviewBridge ID via API:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct URL - use direct path /view to avoid /api/view ambiguity
|
||||
const params = new URLSearchParams();
|
||||
params.append("filename", imageInfo.filename);
|
||||
params.append("type", imageInfo.type || "input");
|
||||
if (imageInfo.subfolder) params.append("subfolder", imageInfo.subfolder);
|
||||
|
||||
// Use ROOT /view as standard ComfyUI does
|
||||
const imageUrl = `/view?${params.toString()}`;
|
||||
console.log("[RegionSelectorExt] Final Image URL:", imageUrl);
|
||||
|
||||
// Crea il dialog usando la API moderna o il fallback sicuro
|
||||
let dialog;
|
||||
try {
|
||||
if (window.comfyAPI && window.comfyAPI.ui && window.comfyAPI.ui.ComfyDialog) {
|
||||
dialog = new window.comfyAPI.ui.ComfyDialog();
|
||||
} else {
|
||||
// Fallback se siamo su una versione vecchia o il bus bridge è attivo
|
||||
const { ComfyDialog } = await import("../../scripts/ui.js");
|
||||
dialog = new ComfyDialog();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[BoxBox] Failed to create dialog via modern API, trying fallback:", e);
|
||||
// Fallback estremo: molti nodi usano app.ui.dialog o simili
|
||||
if (app.ui && app.ui.dialog) {
|
||||
dialog = app.ui.dialog;
|
||||
} else {
|
||||
alert("Error: ComfyUI Dialog system not available. Please check console (F12).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Crea il dialog
|
||||
const dialog = new ComfyDialog();
|
||||
dialog.element.style.width = "95vw";
|
||||
dialog.element.style.height = "95vh";
|
||||
dialog.element.style.maxWidth = "none";
|
||||
@@ -1049,7 +1270,7 @@ async function openRegionDialog(node, app) {
|
||||
|
||||
<!-- Image info -->
|
||||
<div class="control-group" style="margin-bottom: 20px;">
|
||||
<small id="image-name" style="display: block; font-size: 12px; color: #64748b;">Image: ${imageName}</small>
|
||||
<small id="image-name" style="display: block; font-size: 12px; color: #64748b;">Image: ${imageInfo.filename}</small>
|
||||
</div>
|
||||
|
||||
<!-- Aspect Ratio Selector -->
|
||||
@@ -1142,7 +1363,9 @@ async function openRegionDialog(node, app) {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
">
|
||||
<img src="${imageUrl}" alt="Region Selector" id="background-image" style="
|
||||
<img src="${imageUrl}" alt="Region Selector" id="background-image"
|
||||
onerror="console.error('[RegionSelector] Failed to load image:', this.src); this.alt = 'Failed to load image. Check console for details.';"
|
||||
style="
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -1204,7 +1427,11 @@ async function openRegionDialog(node, app) {
|
||||
attempts++;
|
||||
if (window.CanvasSelector) {
|
||||
clearInterval(waitForCanvasSelector);
|
||||
const selector = window.CanvasSelector.initializeCanvasSelector(container, imageUrl);
|
||||
// Get previous metadata from node widget if it exists
|
||||
const metadataWidget = node.widgets?.find((w) => w.name === "box_metadata");
|
||||
const previousMetadata = metadataWidget?.value || null;
|
||||
|
||||
const selector = window.CanvasSelector.initializeCanvasSelector(container, imageUrl, previousMetadata);
|
||||
|
||||
// Aggiorna il confirm button per usare le coordinate reali
|
||||
confirmBtn.onclick = () => {
|
||||
|
||||
Reference in New Issue
Block a user