diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..71ba0a5 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 6a0421b..85d4809 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/box_reinsert_node.py b/box_reinsert_node.py index fecf8ad..34b5bc0 100644 --- a/box_reinsert_node.py +++ b/box_reinsert_node.py @@ -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] diff --git a/region_selector_node.py b/region_selector_node.py index 83a88ee..bfe868f 100644 --- a/region_selector_node.py +++ b/region_selector_node.py @@ -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} diff --git a/web/canvas_selector.js b/web/canvas_selector.js index f148ab0..3ba38cd 100644 --- a/web/canvas_selector.js +++ b/web/canvas_selector.js @@ -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 = '

Clicca e trascina sull\'immagine per disegnare un rettangolo

'; - } - - 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 = ` - πŸ“ Coordinate Base:
- x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px
- x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px
-
- πŸ“ Dimensioni Base:
- Larghezza: ${Math.round(baseWidth)}px
- Altezza: ${Math.round(baseHeight)}px - `; - - currentDimensions.innerHTML = ` - πŸ” Con Zoom (${Math.round(currentZoom * 100)}%):
- Larghezza: ${Math.round(zoomedWidth)}px
- Altezza: ${Math.round(zoomedHeight)}px
-
- 🎨 Bordo ${borderPosition === 'inside' ? 'Interno' : 'Esterno'} (${currentBorderWidth}px):
- Larghezza Totale: ${Math.round(effectiveWidth)}px
- 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)"); diff --git a/web/region_selector_extension.js b/web/region_selector_extension.js index 81b1746..cc765bb 100644 --- a/web/region_selector_extension.js +++ b/web/region_selector_extension.js @@ -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: ${imageName} + Image: ${imageInfo.filename}
@@ -1142,7 +1363,9 @@ async function openRegionDialog(node, app) { max-width: 90%; max-height: 90%; "> - Region Selector {