diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fcb602f --- /dev/null +++ b/__init__.py @@ -0,0 +1,29 @@ +""" +BoxBox - Pacchetto nodi completo per selezione, crop, resize e reinserimento regioni immagine +""" + +import os +from .region_selector_node import RegionSelectorNode, NODE_CLASS_MAPPINGS as RSN_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RSN_DISPLAY +from .region_crop_node import RegionCropNode, NODE_CLASS_MAPPINGS as RCN_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RCN_DISPLAY +from .box_resize_node import BoxResizeNode, NODE_CLASS_MAPPINGS as BRN_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as BRN_DISPLAY +from .box_reinsert_node import BoxReinsertNode, NODE_CLASS_MAPPINGS as BRI_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as BRI_DISPLAY + +# Combina le mappature di tutti i nodi +NODE_CLASS_MAPPINGS = { + **RSN_MAPPINGS, + **RCN_MAPPINGS, + **BRN_MAPPINGS, + **BRI_MAPPINGS, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + **RSN_DISPLAY, + **RCN_DISPLAY, + **BRN_DISPLAY, + **BRI_DISPLAY, +} + +# Percorso file web (HTML/CSS/JS) +WEB_DIRECTORY = os.path.join(os.path.dirname(__file__), "web") + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/box_reinsert_node.py b/box_reinsert_node.py new file mode 100644 index 0000000..32ab745 --- /dev/null +++ b/box_reinsert_node.py @@ -0,0 +1,148 @@ +""" +Box Reinsert Node - Nodo per rimettere l'immagine generata nel punto originale +""" + +import json +import torch +import numpy as np +from PIL import Image + + +class BoxReinsertNode: + """ + Nodo che rimette l'immagine generata nel punto originale. + + Workflow: + 1. BoxSelector → estrae una regione (metadata con x1, x2, y1, y2) + 2. BoxCrop → ritaglia la regione + 3. BoxResize → ridimensiona per generazione (metadata con scale info) + 4. [Generazione AI] → produce immagine generata + 5. BoxReinsert → annulla il resize e rimette nel punto originale + + Input: + - original_image: Immagine originale intera + - generated_image: Immagine generata (ridimensionata) + - box_metadata: Metadata dal BoxSelector (coordinate selezione) + - resize_metadata: Metadata dal BoxResize (scale info) + + Output: + - image: Immagine originale con generated_image rimessa nel posto corretto + """ + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "original_image": ("IMAGE",), + "generated_image": ("IMAGE",), + "box_metadata": ("STRING",), + "resize_metadata": ("STRING",), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "reinsert_image" + CATEGORY = "image/box" + + def reinsert_image(self, original_image, generated_image, box_metadata, resize_metadata): + """ + Rimette l'immagine generata nel punto originale. + + Se resize_metadata è vuoto, bypassa il resize e rimette direttamente l'immagine generata. + + Args: + original_image: Immagine originale (B, H, W, C) + generated_image: Immagine generata/elaborata (B, H, W, C) + box_metadata: JSON metadata dal BoxSelector con x1, x2, y1, y2 + resize_metadata: JSON metadata dal BoxResize con scale_x, scale_y (opzionale) + + Returns: + (final_image,): Immagine con generated_image rimessa nel punto corretto + """ + + try: + box_meta = json.loads(box_metadata) + except json.JSONDecodeError: + print("[BoxReinsertNode] Invalid box_metadata JSON") + return (original_image,) + + # Prova a leggere resize_metadata + resize_meta = {} + use_resize = False + if resize_metadata and resize_metadata.strip() and resize_metadata != "{}": + try: + resize_meta = json.loads(resize_metadata) + use_resize = True + except json.JSONDecodeError: + print("[BoxReinsertNode] Invalid resize_metadata JSON, bypassing resize") + use_resize = False + + # Estrai coordinate della selezione originale + x1 = box_meta.get("x1", 0) + x2 = box_meta.get("x2", 0) + y1 = box_meta.get("y1", 0) + y2 = box_meta.get("y2", 0) + + # Normalizza coordinate (assicura che x1 < x2, y1 < y2) + box_x_start = min(x1, x2) + box_x_end = max(x1, x2) + box_y_start = min(y1, y2) + box_y_end = max(y1, y2) + + 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) + original_pil = Image.fromarray(original_np) + + generated_np = (generated_image[0].cpu().numpy() * 255).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 + 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) + + # Assicurati che il tensor abbia le giuste dimensioni + channels = original_image.shape[3] + if final_tensor.shape[-1] != channels: + if channels == 4 and final_tensor.shape[-1] == 3: + alpha = torch.ones((final_tensor.shape[0], final_tensor.shape[1], final_tensor.shape[2], 1)) + final_tensor = torch.cat([final_tensor, alpha], dim=-1) + elif channels == 3 and final_tensor.shape[-1] == 4: + final_tensor = final_tensor[:, :, :, :3] + + print(f"[BoxReinsertNode] Reinserted generated image at position ({box_x_start}, {box_y_start})") + print(f"[BoxReinsertNode] Final image size: {final_tensor.shape[2]}x{final_tensor.shape[1]}") + + return (final_tensor,) + + +NODE_CLASS_MAPPINGS = {"BoxReinsert": BoxReinsertNode} +NODE_DISPLAY_NAME_MAPPINGS = {"BoxReinsert": "🎨 BoxReinsert"} diff --git a/box_resize_node.py b/box_resize_node.py new file mode 100644 index 0000000..57fc68f --- /dev/null +++ b/box_resize_node.py @@ -0,0 +1,191 @@ +""" +Box Resize Node - Nodo per ridimensionare immagini con preset aspect ratio o custom +""" + +import json +import torch +import torch.nn.functional as F +from typing import Dict, Any, Tuple + + +class BoxResizeNode: + """ + Nodo che ridimensiona immagini con supporto per preset aspect ratio comuni. + + Supporta: + - Preset di aspect ratio (1:1, 3:4, 5:8, 9:16, ecc...) + - Modalità custom width/height + - Keep aspect ratio + - Interpolazione (bilinear, bicubic, nearest) + + Outputs metadata con informazioni complete di trasformazione. + """ + + RESIZE_PRESETS = { + "Custom": None, + "1:1 Square 1024x1024": (1024, 1024), + "3:4 Portrait 896x1152": (896, 1152), + "5:8 Portrait 832x1216": (832, 1216), + "9:16 Portrait 768x1344": (768, 1344), + "9:21 Portrait 640x1536": (640, 1536), + "4:3 Landscape 1152x896": (1152, 896), + "3:2 Landscape 1216x832": (1216, 832), + "16:9 Landscape 1344x768": (1344, 768), + "21:9 Landscape 1536x640": (1536, 640), + } + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Definisce gli input del nodo. + + Returns: + Dict con tipologie e configurazioni degli input + """ + return { + "required": { + "image": ("IMAGE",), + "size": (list(cls.RESIZE_PRESETS.keys()), { + "default": "Custom" + }), + "keep_aspect_ratio": ("BOOLEAN", { + "default": True + }), + "interpolation_mode": (["bilinear", "bicubic", "nearest"], { + "default": "bilinear" + }), + }, + "optional": { + "width": ("INT", { + "default": 1024, + "min": 64, + "max": 8192, + "step": 8 + }), + "height": ("INT", { + "default": 1024, + "min": 64, + "max": 8192, + "step": 8 + }), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "resize_metadata") + FUNCTION = "resize" + CATEGORY = "image/box" + + def resize( + self, + image: torch.Tensor, + size: str, + keep_aspect_ratio: bool, + interpolation_mode: str = "bilinear", + width: int = 1024, + height: int = 1024 + ) -> Tuple[torch.Tensor, str]: + """ + Ridimensiona l'immagine con support per preset. + + Args: + image: Tensor immagine in formato (batch, height, width, channels) + size: Preset selezionato o "Custom" + keep_aspect_ratio: Se True, mantiene le proporzioni + interpolation_mode: Modalità di interpolazione ("bilinear", "bicubic", "nearest") + width: Larghezza custom (se size = "Custom") + height: Altezza custom (se size = "Custom") + + Returns: + Tuple contenente: + - resized_image: Tensor dell'immagine ridimensionata + - resize_metadata: Stringa JSON con informazioni di ridimensionamento + """ + + # Estrai dimensioni dal preset o usa custom + if size == "Custom": + target_width, target_height = width, height + else: + target_width, target_height = self.RESIZE_PRESETS[size] + + # Valida l'input + if len(image.shape) != 4: + raise ValueError(f"Formato immagine non valido: atteso (batch, height, width, channels), ricevuto {image.shape}") + + # Estrai le dimensioni originali + original_height = image.shape[1] + original_width = image.shape[2] + channels = image.shape[3] + + # Calcola le dimensioni finali + if keep_aspect_ratio: + # Mantieni le proporzioni usando target_width come riferimento + aspect_ratio = original_height / original_width + final_width = target_width + final_height = int(target_width * aspect_ratio) + else: + final_width = target_width + final_height = target_height + + # Se le dimensioni sono già corrette, ritorna l'immagine originale + if final_width == original_width and final_height == original_height: + resize_metadata = { + "original_width": int(original_width), + "original_height": int(original_height), + "resized_width": int(final_width), + "resized_height": int(final_height), + "scale_x": 1.0, + "scale_y": 1.0, + "size_preset": size, + "keep_aspect_ratio": keep_aspect_ratio, + "interpolation_mode": interpolation_mode + } + return (image, json.dumps(resize_metadata)) + + # Converte l'immagine da (batch, height, width, channels) a (batch, channels, height, width) + # formato richiesto da torch.nn.functional.interpolate + image_permuted = image.permute(0, 3, 1, 2) + + # Esegui il ridimensionamento + align_corners = False if interpolation_mode != "nearest" else None + + resized = F.interpolate( + image_permuted, + size=(final_height, final_width), + mode=interpolation_mode, + align_corners=align_corners + ) + + # Riconverti al formato originale: (batch, height, width, channels) + resized = resized.permute(0, 2, 3, 1) + + # Calcola i fattori di scala + scale_x = final_width / original_width + scale_y = final_height / original_height + + # Crea metadati con informazioni complete di ridimensionamento + resize_metadata = { + "original_width": int(original_width), + "original_height": int(original_height), + "resized_width": int(final_width), + "resized_height": int(final_height), + "scale_x": float(scale_x), + "scale_y": float(scale_y), + "size_preset": size, + "keep_aspect_ratio": keep_aspect_ratio, + "interpolation_mode": interpolation_mode + } + + resize_metadata_str = json.dumps(resize_metadata) + + print(f"[BoxResizeNode] Resized {original_width}x{original_height} → {final_width}x{final_height} (preset: {size}, keep_aspect: {keep_aspect_ratio})") + print(f"[BoxResizeNode] Metadata: {resize_metadata_str}") + + return (resized, resize_metadata_str) + + +NODE_CLASS_MAPPINGS = {"BoxResize": BoxResizeNode} +NODE_DISPLAY_NAME_MAPPINGS = {"BoxResize": "📦 BoxResize"} diff --git a/region_crop_node.py b/region_crop_node.py new file mode 100644 index 0000000..906c1ee --- /dev/null +++ b/region_crop_node.py @@ -0,0 +1,113 @@ +""" +Region Crop Node - Nodo che taglia l'immagine secondo le coordinate selezionate +""" + +import json +import torch + + +class RegionCropNode: + """ + Nodo che taglia un'immagine usando le coordinate fornite dal RegionSelectorNode. + """ + + def __init__(self): + """Inizializza il nodo""" + pass + + @classmethod + def INPUT_TYPES(cls): + """ + Definisce gli input del nodo. + """ + return { + "required": { + "image": ("IMAGE",), + "region_metadata": ("STRING", { + "default": "{}", + "multiline": True, + }), + }, + "optional": { + "fallback_mode": (["use_full_image", "return_zero", "error"], { + "default": "use_full_image", + }), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("cropped_image",) + FUNCTION = "crop_image" + CATEGORY = "image/region" + + def crop_image(self, image, region_metadata, fallback_mode="use_full_image"): + """ + Taglia l'immagine secondo le coordinate nel metadata. + + Args: + image: Tensor immagine (B, H, W, C) + region_metadata: JSON string con coordinate della regione + fallback_mode: Cosa fare se non ci sono coordinate valide + + Returns: + (cropped_image,): Immagine ritagliata + """ + + try: + metadata = json.loads(region_metadata) + except json.JSONDecodeError: + metadata = {} + + # Estrai coordinate (x1, x2, y1, y2 sono i lati) + x1 = metadata.get("x1", None) + x2 = metadata.get("x2", None) + y1 = metadata.get("y1", None) + y2 = metadata.get("y2", None) + + # Se non c'è selezione + if x1 is None or x2 is None or y1 is None or y2 is None: + if fallback_mode == "use_full_image": + return (image,) + elif fallback_mode == "return_zero": + return (torch.zeros_like(image),) + else: + raise ValueError("No region coordinates provided in metadata") + + # Assicura che le coordinate siano integer + x1 = int(round(x1)) + x2 = int(round(x2)) + y1 = int(round(y1)) + y2 = int(round(y2)) + + # Estrai dimensioni immagine + batch_size, img_height, img_width, channels = image.shape + + # Assicura che x1 < x2 e y1 < y2 + x_start = min(x1, x2) + x_end = max(x1, x2) + y_start = min(y1, y2) + y_end = max(y1, y2) + + # Calcola coordinate finali con clipping ai bordi + x_start = max(0, min(x_start, img_width - 1)) + x_end = max(x_start + 1, min(x_end, img_width)) + y_start = max(0, min(y_start, img_height - 1)) + y_end = max(y_start + 1, min(y_end, img_height)) + + # Verifica validità della region + if x_end <= x_start or y_end <= y_start: + if fallback_mode == "use_full_image": + return (image,) + elif fallback_mode == "return_zero": + return (torch.zeros_like(image),) + else: + raise ValueError("Invalid region coordinates: no overlap with image") + + # Taglia l'immagine + cropped = image[:, y_start:y_end, x_start:x_end, :] + + return (cropped,) + + +NODE_CLASS_MAPPINGS = {"BoxCrop": RegionCropNode} +NODE_DISPLAY_NAME_MAPPINGS = {"BoxCrop": "✂️ BoxCrop"} diff --git a/region_selector_node.py b/region_selector_node.py new file mode 100644 index 0000000..c0e7073 --- /dev/null +++ b/region_selector_node.py @@ -0,0 +1,76 @@ +""" +Region Selector Node - Nodo per selezionare regioni di immagini con interfaccia popup +""" + +import json + + +class RegionSelectorNode: + """ + Nodo che permette di selezionare una regione rettangolare su un'immagine + tramite un'interfaccia interattiva in popup. + + Outputs: + - IMAGE: L'immagine passata + - STRING: Metadata JSON con coordinate della regione selezionata + """ + + def __init__(self): + # Metadata di default con coordinate x1, x2, y1, y2 (lati, non posizione+dimensioni) + self.last_metadata = json.dumps({ + "x1": 0, + "y1": 0, + "x2": 0, + "y2": 0, + "zoom": 1, + "borderWidth": 0, + "borderPosition": "inside", + "selected": False + }) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + }, + "optional": { + "region_metadata": ("STRING", { + "default": "{}", + "multiline": True, + }), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "region_metadata") + FUNCTION = "process_region_selection" + CATEGORY = "image/region" + OUTPUT_NODE = True + + def process_region_selection(self, image, region_metadata="{}"): + """ + Processa l'immagine e ritorna l'immagine + metadata della regione selezionata. + + Args: + image: Tensor immagine (B, H, W, C) + region_metadata: JSON string con coordinate della regione (x1, x2, y1, y2) + + Returns: + (image, region_metadata_json): Immagine originale e metadata JSON + """ + + # Usa il metadata fornito, o l'ultimo salvato se vuoto + if region_metadata and region_metadata.strip() and region_metadata != "{}": + metadata_to_return = region_metadata + self.last_metadata = region_metadata + else: + # Se nessun metadata è stato fornito, usa l'ultimo salvato + metadata_to_return = self.last_metadata + + print(f"[RegionSelectorNode] Processing selection, metadata: {metadata_to_return}") + return (image, metadata_to_return) + + +NODE_CLASS_MAPPINGS = {"BoxSelector": RegionSelectorNode} +NODE_DISPLAY_NAME_MAPPINGS = {"BoxSelector": "📦 BoxSelector"} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b82f7c8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +torch>=2.0.0 +numpy>=1.20.0 +Pillow>=8.0.0 diff --git a/web/canvas_selector.js b/web/canvas_selector.js new file mode 100644 index 0000000..f148ab0 --- /dev/null +++ b/web/canvas_selector.js @@ -0,0 +1,516 @@ +/** + * Canvas Selector - Funzionalità complete di selezione rettangoli + * Basato sul codice di canva_html + */ + +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 }; diff --git a/web/region_selector.css b/web/region_selector.css new file mode 100644 index 0000000..0e9faa4 --- /dev/null +++ b/web/region_selector.css @@ -0,0 +1,503 @@ +/* Region Selector Stylesheet */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #2563eb; + --secondary-color: #64748b; + --success-color: #16a34a; + --danger-color: #dc2626; + --warning-color: #ea580c; + --bg-color: #f8fafc; + --panel-bg: #ffffff; + --border-color: #e2e8f0; + --text-color: #1e293b; + --text-light: #64748b; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--bg-color); + color: var(--text-color); + line-height: 1.6; +} + +.selector-container { + display: flex; + flex-direction: column; + height: 100vh; + background-color: var(--panel-bg); +} + +/* Header */ +.selector-header { + position: relative; + padding: 20px 25px; + background: linear-gradient(135deg, var(--primary-color) 0%, #1e40af 100%); + color: white; + box-shadow: var(--shadow); + z-index: 10; +} + +.selector-header h1 { + font-size: 24px; + margin-bottom: 5px; + font-weight: 700; +} + +.selector-header p { + font-size: 13px; + opacity: 0.9; + margin-bottom: 0; +} + +.close-button { + position: absolute; + top: 15px; + right: 15px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + font-size: 18px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-button:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.5); +} + +/* Content Layout */ +.selector-content { + display: flex; + flex: 1; + overflow: hidden; +} + +/* Control Panel */ +.control-panel { + width: 320px; + background-color: var(--panel-bg); + border-right: 1px solid var(--border-color); + overflow-y: auto; + padding: 20px; +} + +.control-panel h3 { + font-size: 16px; + margin-bottom: 15px; + color: var(--text-color); + font-weight: 600; +} + +.control-panel h4 { + font-size: 13px; + margin-bottom: 10px; + color: var(--text-color); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.control-group { + margin-bottom: 20px; +} + +.control-group label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 8px; + color: var(--text-color); +} + +.control-group small { + display: block; + font-size: 11px; + color: var(--text-light); + margin-top: 5px; + font-style: italic; +} + +/* Input Range */ +input[type="range"] { + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--border-color); + outline: none; + -webkit-appearance: none; + appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--primary-color); + cursor: pointer; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: all 0.2s ease; +} + +/* Buttons */ +.btn { + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: var(--shadow); +} + +.btn:active:not(:disabled) { + transform: translateY(0); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; + border-color: var(--primary-color); + width: 100%; +} + +.btn-primary:hover:not(:disabled) { + background-color: #1d4ed8; +} + +.btn-secondary { + background-color: var(--secondary-color); + color: white; + border-color: var(--secondary-color); + width: 100%; +} + +.btn-secondary:hover:not(:disabled) { + background-color: #475569; +} + +.btn-success { + background-color: var(--success-color); + color: white; + border-color: var(--success-color); + width: 100%; + font-weight: 600; +} + +.btn-success:hover:not(:disabled) { + background-color: #15803d; +} + +.btn-danger { + background-color: var(--danger-color); + color: white; + border-color: var(--danger-color); + width: 100%; + font-weight: 600; +} + +.btn-danger:hover:not(:disabled) { + background-color: #b91c1c; +} + +.btn-small { + width: 100%; + padding: 6px 10px; + font-size: 12px; +} + +.zoom-buttons { + display: flex; + gap: 8px; +} + +.zoom-buttons .btn { + flex: 1; +} + +/* Radio Group */ +.radio-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.radio-group label { + display: flex; + align-items: center; + font-weight: 400; + margin-bottom: 0; + cursor: pointer; +} + +.radio-group input[type="radio"] { + margin-right: 8px; + cursor: pointer; + width: 16px; + height: 16px; + accent-color: var(--primary-color); +} + +/* Zoom Display */ +.zoom-display { + background-color: #f1f5f9; + padding: 10px; + border-radius: 4px; + text-align: center; + margin: 10px 0; + font-weight: 600; + color: var(--primary-color); +} + +/* Info Boxes */ +.info-box { + background-color: #f9fafb; + border: 1px solid var(--border-color); + padding: 12px; + border-radius: 6px; + font-family: 'Monaco', 'Courier New', monospace; + font-size: 12px; + color: var(--text-color); + line-height: 1.5; + overflow-x: auto; +} + +.info-box p { + margin: 0; +} + +.info-box-secondary { + background-color: #eff6ff; + border-color: #bfdbfe; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + flex-direction: column; + gap: 10px; +} + +.action-buttons .btn { + padding: 10px; + font-size: 13px; + font-weight: 600; +} + +/* HR */ +hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 15px 0; +} + +/* Canvas Area */ +.canvas-area { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(45deg, #f0f4f8 25%, transparent 25%, transparent 75%, #f0f4f8 75%, #f0f4f8), + linear-gradient(45deg, #f0f4f8 25%, transparent 25%, transparent 75%, #f0f4f8 75%, #f0f4f8); + background-size: 40px 40px; + background-position: 0 0, 20px 20px; + background-color: #fafbfc; + overflow: hidden; + position: relative; +} + +#canvas-container { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + background-color: white; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + overflow: hidden; + max-width: 90%; + max-height: 90%; +} + +#background-image { + display: block; + max-width: 100%; + max-height: 100%; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; +} + +/* Rectangle */ +.rectangle { + position: absolute; + background-color: transparent; + cursor: move; + transform-origin: center center; + transition: none; + display: flex; + align-items: center; + justify-content: center; +} + +.rectangle.border-inside { + border: 3px solid #2563eb; + box-shadow: inset 0 0 0 2px rgba(37, 99, 235, 0.3); +} + +.rectangle.border-outside { + outline: 3px solid #2563eb; + outline-offset: -3px; +} + +/* Resize Handles */ +.resize-handle { + position: absolute; + width: 12px; + height: 12px; + background-color: #2563eb; + border: 2px solid white; + border-radius: 50%; + opacity: 0; + transition: opacity 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.rectangle:hover .resize-handle, +.rectangle.resizing .resize-handle { + opacity: 1; +} + +.rectangle:hover { + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); +} + +/* Handle positions */ +.resize-handle.top-left { + top: -6px; + left: -6px; + cursor: nwse-resize; +} + +.resize-handle.top-right { + top: -6px; + right: -6px; + cursor: nesw-resize; +} + +.resize-handle.bottom-left { + bottom: -6px; + left: -6px; + cursor: nesw-resize; +} + +.resize-handle.bottom-right { + bottom: -6px; + right: -6px; + cursor: nwse-resize; +} + +/* Scrollbar Styling */ +.control-panel::-webkit-scrollbar { + width: 8px; +} + +.control-panel::-webkit-scrollbar-track { + background: transparent; +} + +.control-panel::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +.control-panel::-webkit-scrollbar-thumb:hover { + background: var(--secondary-color); +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .control-panel { + width: 280px; + } +} + +@media (max-width: 768px) { + .selector-content { + flex-direction: column; + } + + .control-panel { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--border-color); + max-height: 40vh; + } + + .canvas-area { + flex: 1; + min-height: 60vh; + } + + .control-panel h3 { + margin-top: 10px; + } +} + +/* Accessibility */ +.btn:focus, +input:focus, +textarea:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Utility */ +.hidden { + display: none !important; +} diff --git a/web/region_selector.html b/web/region_selector.html new file mode 100644 index 0000000..18a4e05 --- /dev/null +++ b/web/region_selector.html @@ -0,0 +1,626 @@ + + + + + + Region Selector - ComfyUI + + + +
+ +
+

🎯 Region Selector

+

Clicca e trascina sull'immagine per selezionare una regione

+ +
+ + +
+ +
+

Controlli

+ + +
+ + + + Immagine corrente: picsum.photos +
+ +
+ + +
+ +
+ + +
+
+ 100% +
+ Zoom del rettangolo selezionato +
+ + +
+ + + Larghezza linea di selezione +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+

Informazioni Selezione

+
+

Clicca e trascina sull'immagine per selezionare una regione

+
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ Immagine di esempio +
+
+
+
+ + + + diff --git a/web/region_selector.js b/web/region_selector.js new file mode 100644 index 0000000..0fa73b1 --- /dev/null +++ b/web/region_selector.js @@ -0,0 +1,584 @@ +/** + * Region Selector JavaScript - Logica interazione canvas editor + */ + +// ============================================================================ +// DOM ELEMENTS +// ============================================================================ + +const container = document.getElementById('canvas-container'); +const backgroundImage = document.getElementById('background-image'); +const baseCoordinates = document.getElementById('base-coordinates'); +const dimensionsInfo = document.getElementById('dimensions-info'); +const currentDimensions = document.getElementById('current-dimensions'); +const zoomInBtn = document.getElementById('zoom-in-btn'); +const zoomOutBtn = document.getElementById('zoom-out-btn'); +const zoomValue = document.getElementById('zoom-value'); +const borderSlider = document.getElementById('border-slider'); +const borderValue = document.getElementById('border-value'); +const resetBtn = document.getElementById('reset-btn'); +const borderPositionRadios = document.querySelectorAll('input[name="border-position"]'); +const imageUpload = document.getElementById('image-upload'); +const uploadBtn = document.getElementById('upload-btn'); +const imageName = document.getElementById('image-name'); +const confirmBtn = document.getElementById('confirm-btn'); +const cancelBtn = document.getElementById('cancel-btn'); +const closeBtn = document.getElementById('close-btn'); + +// ============================================================================ +// STATE VARIABLES +// ============================================================================ + +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 = 3; +let borderPosition = 'inside'; +let rectangleExists = false; + +// Coordinate base (senza zoom e bordi) +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; + +// Callback per comunicazione con il nodo +let onConfirmCallback = null; +let onCancelCallback = null; + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + initializeEventListeners(); +}); + +function initializeEventListeners() { + // Upload button + uploadBtn.addEventListener('click', handleUploadClick); + imageUpload.addEventListener('change', handleImageUpload); + + // Zoom buttons + zoomInBtn.addEventListener('click', handleZoomIn); + zoomOutBtn.addEventListener('click', handleZoomOut); + + // Border controls + borderSlider.addEventListener('input', handleBorderChange); + borderPositionRadios.forEach(radio => { + radio.addEventListener('change', handleBorderPositionChange); + }); + + // Reset button + resetBtn.addEventListener('click', handleReset); + + // Canvas drawing + container.addEventListener('mousedown', handleCanvasMouseDown); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + // Action buttons + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + closeBtn.addEventListener('click', handleCancel); + + // Prevent image drag + backgroundImage.addEventListener('dragstart', (e) => e.preventDefault()); +} + +// ============================================================================ +// FILE UPLOAD +// ============================================================================ + +function handleUploadClick() { + imageUpload.click(); +} + +function handleImageUpload(e) { + const file = e.target.files[0]; + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + + reader.onload = function(event) { + backgroundImage.src = event.target.result; + imageName.textContent = `Immagine corrente: ${file.name}`; + + if (currentRectangle) { + if (confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) { + resetRectangle(); + } else { + imageUpload.value = ''; + return; + } + } + }; + + reader.readAsDataURL(file); + } +} + +// ============================================================================ +// RECTANGLE DRAWING +// ============================================================================ + +function handleCanvasMouseDown(e) { + if (e.button !== 0) return; // Solo left click + if (!container || !backgroundImage) return; + + const rect = container.getBoundingClientRect(); + const imgRect = backgroundImage.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + // Coordinate del click relative all'immagine + const imgX = e.clientX - imgRect.left; + const imgY = e.clientY - imgRect.top; + + // Se il click è fuori dall'immagine, non fare nulla + if (imgX < 0 || imgY < 0 || imgX > imgRect.width || imgY > imgRect.height) { + return; + } + + // Verifica se stai ridimensionando + if (currentRectangle) { + const handleRect = detectResizeHandle(e, currentRectangle); + if (handleRect) { + isResizing = true; + resizingEdge = handleRect; + startX = e.clientX; + startY = e.clientY; + rectStartX = baseX; + rectStartY = baseY; + rectStartWidth = baseWidth; + rectStartHeight = baseHeight; + currentRectangle.classList.add('resizing'); + return; + } + + // Verifica se stai spostando il rettangolo + if (isClickInsideRectangle(e, currentRectangle, imgRect)) { + isDrawing = true; + startX = e.clientX; + startY = e.clientY; + rectStartX = baseX; + rectStartY = baseY; + return; + } + } + + // Inizia nuovo rettangolo + isDrawing = true; + startX = imgX; + startY = imgY; + baseX = imgX; + baseY = imgY; + baseWidth = 1; + baseHeight = 1; + + createNewRectangle(imgX, imgY, 1, 1); +} + +function handleMouseMove(e) { + if (!isDrawing && !isResizing) return; + if (!container || !backgroundImage) return; + + const imgRect = backgroundImage.getBoundingClientRect(); + + if (isResizing && currentRectangle && resizingEdge) { + handleResizeRectangle(e, imgRect); + } else if (isDrawing) { + const imgX = e.clientX - imgRect.left; + const imgY = e.clientY - imgRect.top; + + const width = Math.max(10, Math.abs(imgX - startX)); + const height = Math.max(10, Math.abs(imgY - startY)); + const x = Math.min(startX, imgX); + const y = Math.min(startY, imgY); + + baseX = x; + baseY = y; + baseWidth = width; + baseHeight = height; + + updateRectanglePosition(x, y, width, height); + updateAllDimensions(); + } +} + +function handleMouseUp(e) { + if (isResizing && currentRectangle) { + currentRectangle.classList.remove('resizing'); + } + isDrawing = false; + isResizing = false; + resizingEdge = null; +} + +function createNewRectangle(x, y, width, height) { + // Rimuovi il vecchio rettangolo se esiste + if (currentRectangle) { + currentRectangle.remove(); + } + + const rect = document.createElement('div'); + rect.className = `rectangle border-${borderPosition}`; + rect.style.left = x + 'px'; + rect.style.top = y + 'px'; + rect.style.width = width + 'px'; + rect.style.height = height + 'px'; + rect.style.borderWidth = currentBorderWidth + 'px'; + + // Aggiungi resize handles + ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach(position => { + const handle = document.createElement('div'); + handle.className = `resize-handle ${position}`; + rect.appendChild(handle); + }); + + container.appendChild(rect); + currentRectangle = rect; + rectangleExists = true; + + // Abilita bottoni + enableControls(); + updateAllDimensions(); +} + +function updateRectanglePosition(x, y, width, height) { + if (!currentRectangle) return; + + currentRectangle.style.left = x + 'px'; + currentRectangle.style.top = y + 'px'; + currentRectangle.style.width = width + 'px'; + currentRectangle.style.height = height + 'px'; +} + +function handleResizeRectangle(e, imgRect) { + if (!currentRectangle) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newX = rectStartX; + let newY = rectStartY; + let newWidth = rectStartWidth; + let newHeight = rectStartHeight; + + // Handle diverse per lato di resize + if (resizingEdge.includes('left')) { + newX = Math.max(0, rectStartX + deltaX); + newWidth = rectStartWidth - (newX - rectStartX); + } + if (resizingEdge.includes('right')) { + newWidth = Math.max(10, rectStartWidth + deltaX); + } + if (resizingEdge.includes('top')) { + newY = Math.max(0, rectStartY + deltaY); + newHeight = rectStartHeight - (newY - rectStartY); + } + if (resizingEdge.includes('bottom')) { + newHeight = Math.max(10, rectStartHeight + deltaY); + } + + newWidth = Math.max(10, newWidth); + newHeight = Math.max(10, newHeight); + + baseX = newX; + baseY = newY; + baseWidth = newWidth; + baseHeight = newHeight; + + updateRectanglePosition(newX, newY, newWidth, newHeight); + updateAllDimensions(); +} + +function isClickInsideRectangle(e, rect, imgRect) { + const rectBounds = rect.getBoundingClientRect(); + return e.clientX >= rectBounds.left && + e.clientX <= rectBounds.right && + e.clientY >= rectBounds.top && + e.clientY <= rectBounds.bottom; +} + +function detectResizeHandle(e, rect) { + const handles = rect.querySelectorAll('.resize-handle'); + for (let handle of handles) { + const handleRect = handle.getBoundingClientRect(); + const distance = Math.sqrt( + Math.pow(e.clientX - (handleRect.left + handleRect.width / 2), 2) + + Math.pow(e.clientY - (handleRect.top + handleRect.height / 2), 2) + ); + if (distance < 12) { + return handle.className.split(' ').find(c => c !== 'resize-handle'); + } + } + return null; +} + +// ============================================================================ +// ZOOM CONTROLS +// ============================================================================ + +function handleZoomIn() { + if (currentRectangle) { + currentZoom = Math.min(currentZoom + zoomStep, maxZoom); + updateZoom(); + updateAllDimensions(); + } +} + +function handleZoomOut() { + if (currentRectangle) { + currentZoom = Math.max(currentZoom - zoomStep, minZoom); + updateZoom(); + updateAllDimensions(); + } +} + +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; +} + +// ============================================================================ +// BORDER CONTROLS +// ============================================================================ + +function handleBorderChange(e) { + currentBorderWidth = parseInt(e.target.value); + borderValue.textContent = currentBorderWidth + 'px'; + + if (currentRectangle) { + currentRectangle.style.borderWidth = currentBorderWidth + 'px'; + updateAllDimensions(); + } +} + +function handleBorderPositionChange(e) { + borderPosition = e.target.value; + + if (currentRectangle) { + currentRectangle.classList.remove('border-inside', 'border-outside'); + currentRectangle.classList.add(`border-${borderPosition}`); + updateAllDimensions(); + } +} + +// ============================================================================ +// RESET AND CLEAR +// ============================================================================ + +function handleReset() { + resetRectangle(); +} + +function resetRectangle() { + if (currentRectangle) { + currentRectangle.remove(); + currentRectangle = null; + } + + rectangleExists = false; + currentZoom = 1; + baseWidth = 0; + baseHeight = 0; + baseX = 0; + baseY = 0; + + baseCoordinates.innerHTML = '

Clicca e trascina sull\'immagine per disegnare un rettangolo

'; + dimensionsInfo.style.display = 'none'; + + disableControls(); +} + +// ============================================================================ +// UPDATE DISPLAY +// ============================================================================ + +function updateAllDimensions() { + if (!rectangleExists) return; + + // Coordinate base (senza zoom e bordi) + const baseInfo = ` + X: ${Math.round(baseX)} px
+ Y: ${Math.round(baseY)} px
+ Larghezza: ${Math.round(baseWidth)} px
+ Altezza: ${Math.round(baseHeight)} px
+
+ Percentuali:
+ X%: ${((baseX / backgroundImage.width) * 100).toFixed(2)}%
+ Y%: ${((baseY / backgroundImage.height) * 100).toFixed(2)}%
+ W%: ${((baseWidth / backgroundImage.width) * 100).toFixed(2)}%
+ H%: ${((baseHeight / backgroundImage.height) * 100).toFixed(2)}% + `; + + baseCoordinates.innerHTML = baseInfo; + + // Mostra info zoom e bordi se zoom > 1 o bordo > 0 + if (currentZoom > 1 || currentBorderWidth > 0) { + const zoomedWidth = baseWidth * currentZoom; + const zoomedHeight = baseHeight * currentZoom; + + const dimensionsHTML = ` + Con Zoom ${Math.round(currentZoom * 100)}%:
+ Larghezza: ${Math.round(zoomedWidth)} px
+ Altezza: ${Math.round(zoomedHeight)} px
+
+ Bordo: ${currentBorderWidth}px (${borderPosition})
+ `; + + currentDimensions.innerHTML = dimensionsHTML; + dimensionsInfo.style.display = 'block'; + } else { + dimensionsInfo.style.display = 'none'; + } +} + +// ============================================================================ +// BUTTON STATES +// ============================================================================ + +function enableControls() { + zoomInBtn.disabled = false; + zoomOutBtn.disabled = false; + resetBtn.disabled = false; + confirmBtn.disabled = false; +} + +function disableControls() { + zoomInBtn.disabled = true; + zoomOutBtn.disabled = true; + resetBtn.disabled = true; + confirmBtn.disabled = true; +} + +// ============================================================================ +// COORDINATE EXPORT +// ============================================================================ + +function getCoordinates() { + if (!rectangleExists) { + return {}; + } + + return { + x: Math.round(baseX), + y: Math.round(baseY), + width: Math.round(baseWidth), + height: Math.round(baseHeight), + zoom: currentZoom, + borderWidth: currentBorderWidth, + borderPosition: borderPosition, + }; +} + +// ============================================================================ +// ACTION BUTTONS +// ============================================================================ + +function handleConfirm() { + const coordinates = getCoordinates(); + + if (!rectangleExists) { + alert('Nessuna selezione effettuata'); + return; + } + + // Invia coordinate al callback + if (onConfirmCallback) { + onConfirmCallback(coordinates); + } + + // Log per debug + console.log('Coordinates confirmed:', coordinates); +} + +function handleCancel() { + if (onCancelCallback) { + onCancelCallback(); + } + window.close(); +} + +// ============================================================================ +// PUBLIC API (per integrazione ComfyUI) +// ============================================================================ + +window.RegionSelector = { + /** + * Inizializza il selettore di regioni con callback personalizzati + * @param {Function} onConfirm - Callback quando l'utente conferma (riceve coordinates) + * @param {Function} onCancel - Callback quando l'utente annulla + */ + init: function(onConfirm, onCancel) { + onConfirmCallback = onConfirm; + onCancelCallback = onCancel; + }, + + /** + * Ottieni le coordinate attuali della selezione + * @returns {Object} Oggetto con x, y, width, height, zoom, borderWidth, borderPosition + */ + getCoordinates: function() { + return getCoordinates(); + }, + + /** + * Imposta un'immagine da URL + * @param {String} imageUrl - URL dell'immagine + */ + setImage: function(imageUrl) { + if (currentRectangle) { + if (confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) { + resetRectangle(); + } else { + return; + } + } + backgroundImage.src = imageUrl; + imageName.textContent = 'Immagine caricata'; + }, + + /** + * Carica una selezione precedente + * @param {Object} coordinates - Oggetto coordinate + */ + loadCoordinates: function(coordinates) { + if (!coordinates || !coordinates.x) return; + + baseX = coordinates.x || 0; + baseY = coordinates.y || 0; + baseWidth = coordinates.width || 100; + baseHeight = coordinates.height || 100; + currentZoom = coordinates.zoom || 1; + currentBorderWidth = coordinates.borderWidth || 3; + borderPosition = coordinates.borderPosition || 'inside'; + + // Aggiorna UI + borderSlider.value = currentBorderWidth; + borderValue.textContent = currentBorderWidth + 'px'; + document.querySelector(`input[value="${borderPosition}"]`).checked = true; + updateZoom(); + + // Ridisegna + createNewRectangle(baseX, baseY, baseWidth, baseHeight); + } +}; diff --git a/web/region_selector_core.js b/web/region_selector_core.js new file mode 100644 index 0000000..d31b07a --- /dev/null +++ b/web/region_selector_core.js @@ -0,0 +1,584 @@ +/** + * Region Selector JavaScript - Logica interazione canvas editor + */ + +// ============================================================================ +// DOM ELEMENTS +// ============================================================================ + +const container = document.getElementById('canvas-container'); +const backgroundImage = document.getElementById('background-image'); +const baseCoordinates = document.getElementById('base-coordinates'); +const dimensionsInfo = document.getElementById('dimensions-info'); +const currentDimensions = document.getElementById('current-dimensions'); +const zoomInBtn = document.getElementById('zoom-in-btn'); +const zoomOutBtn = document.getElementById('zoom-out-btn'); +const zoomValue = document.getElementById('zoom-value'); +const borderSlider = document.getElementById('border-slider'); +const borderValue = document.getElementById('border-value'); +const resetBtn = document.getElementById('reset-btn'); +const borderPositionRadios = document.querySelectorAll('input[name="border-position"]'); +const imageUpload = document.getElementById('image-upload'); +const uploadBtn = document.getElementById('upload-btn'); +const imageName = document.getElementById('image-name'); +const confirmBtn = document.getElementById('confirm-btn'); +const cancelBtn = document.getElementById('cancel-btn'); +const closeBtn = document.getElementById('close-btn'); + +// ============================================================================ +// STATE VARIABLES +// ============================================================================ + +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 = 3; +let borderPosition = 'inside'; +let rectangleExists = false; + +// Coordinate base (senza zoom e bordi) +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; + +// Callback per comunicazione con il nodo +let onConfirmCallback = null; +let onCancelCallback = null; + +// ============================================================================ +// INITIALIZATION +// ============================================================================ + +document.addEventListener('DOMContentLoaded', function() { + initializeEventListeners(); +}); + +function initializeEventListeners() { + // Upload button + uploadBtn.addEventListener('click', handleUploadClick); + imageUpload.addEventListener('change', handleImageUpload); + + // Zoom buttons + zoomInBtn.addEventListener('click', handleZoomIn); + zoomOutBtn.addEventListener('click', handleZoomOut); + + // Border controls + borderSlider.addEventListener('input', handleBorderChange); + borderPositionRadios.forEach(radio => { + radio.addEventListener('change', handleBorderPositionChange); + }); + + // Reset button + resetBtn.addEventListener('click', handleReset); + + // Canvas drawing + container.addEventListener('mousedown', handleCanvasMouseDown); + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + // Action buttons + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + closeBtn.addEventListener('click', handleCancel); + + // Prevent image drag + backgroundImage.addEventListener('dragstart', (e) => e.preventDefault()); +} + +// ============================================================================ +// FILE UPLOAD +// ============================================================================ + +function handleUploadClick() { + imageUpload.click(); +} + +function handleImageUpload(e) { + const file = e.target.files[0]; + if (file && file.type.startsWith('image/')) { + const reader = new FileReader(); + + reader.onload = function(event) { + backgroundImage.src = event.target.result; + imageName.textContent = `Immagine corrente: ${file.name}`; + + if (currentRectangle) { + if (confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) { + resetRectangle(); + } else { + imageUpload.value = ''; + return; + } + } + }; + + reader.readAsDataURL(file); + } +} + +// ============================================================================ +// RECTANGLE DRAWING +// ============================================================================ + +function handleCanvasMouseDown(e) { + if (e.button !== 0) return; // Solo left click + if (!container || !backgroundImage) return; + + const rect = container.getBoundingClientRect(); + const imgRect = backgroundImage.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + // Coordinate del click relative all'immagine + const imgX = e.clientX - imgRect.left; + const imgY = e.clientY - imgRect.top; + + // Se il click è fuori dall'immagine, non fare nulla + if (imgX < 0 || imgY < 0 || imgX > imgRect.width || imgY > imgRect.height) { + return; + } + + // Verifica se stai ridimensionando + if (currentRectangle) { + const handleRect = detectResizeHandle(e, currentRectangle); + if (handleRect) { + isResizing = true; + resizingEdge = handleRect; + startX = e.clientX; + startY = e.clientY; + rectStartX = baseX; + rectStartY = baseY; + rectStartWidth = baseWidth; + rectStartHeight = baseHeight; + currentRectangle.classList.add('resizing'); + return; + } + + // Verifica se stai spostando il rettangolo + if (isClickInsideRectangle(e, currentRectangle, imgRect)) { + isDrawing = true; + startX = e.clientX; + startY = e.clientY; + rectStartX = baseX; + rectStartY = baseY; + return; + } + } + + // Inizia nuovo rettangolo + isDrawing = true; + startX = imgX; + startY = imgY; + baseX = imgX; + baseY = imgY; + baseWidth = 1; + baseHeight = 1; + + createNewRectangle(imgX, imgY, 1, 1); +} + +function handleMouseMove(e) { + if (!isDrawing && !isResizing) return; + if (!container || !backgroundImage) return; + + const imgRect = backgroundImage.getBoundingClientRect(); + + if (isResizing && currentRectangle && resizingEdge) { + handleResizeRectangle(e, imgRect); + } else if (isDrawing) { + const imgX = e.clientX - imgRect.left; + const imgY = e.clientY - imgRect.top; + + const width = Math.max(10, Math.abs(imgX - startX)); + const height = Math.max(10, Math.abs(imgY - startY)); + const x = Math.min(startX, imgX); + const y = Math.min(startY, imgY); + + baseX = x; + baseY = y; + baseWidth = width; + baseHeight = height; + + updateRectanglePosition(x, y, width, height); + updateAllDimensions(); + } +} + +function handleMouseUp(e) { + if (isResizing && currentRectangle) { + currentRectangle.classList.remove('resizing'); + } + isDrawing = false; + isResizing = false; + resizingEdge = null; +} + +function createNewRectangle(x, y, width, height) { + // Rimuovi il vecchio rettangolo se esiste + if (currentRectangle) { + currentRectangle.remove(); + } + + const rect = document.createElement('div'); + rect.className = `rectangle border-${borderPosition}`; + rect.style.left = x + 'px'; + rect.style.top = y + 'px'; + rect.style.width = width + 'px'; + rect.style.height = height + 'px'; + rect.style.borderWidth = currentBorderWidth + 'px'; + + // Aggiungi resize handles + ['top-left', 'top-right', 'bottom-left', 'bottom-right'].forEach(position => { + const handle = document.createElement('div'); + handle.className = `resize-handle ${position}`; + rect.appendChild(handle); + }); + + container.appendChild(rect); + currentRectangle = rect; + rectangleExists = true; + + // Abilita bottoni + enableControls(); + updateAllDimensions(); +} + +function updateRectanglePosition(x, y, width, height) { + if (!currentRectangle) return; + + currentRectangle.style.left = x + 'px'; + currentRectangle.style.top = y + 'px'; + currentRectangle.style.width = width + 'px'; + currentRectangle.style.height = height + 'px'; +} + +function handleResizeRectangle(e, imgRect) { + if (!currentRectangle) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newX = rectStartX; + let newY = rectStartY; + let newWidth = rectStartWidth; + let newHeight = rectStartHeight; + + // Handle diverse per lato di resize + if (resizingEdge.includes('left')) { + newX = Math.max(0, rectStartX + deltaX); + newWidth = rectStartWidth - (newX - rectStartX); + } + if (resizingEdge.includes('right')) { + newWidth = Math.max(10, rectStartWidth + deltaX); + } + if (resizingEdge.includes('top')) { + newY = Math.max(0, rectStartY + deltaY); + newHeight = rectStartHeight - (newY - rectStartY); + } + if (resizingEdge.includes('bottom')) { + newHeight = Math.max(10, rectStartHeight + deltaY); + } + + newWidth = Math.max(10, newWidth); + newHeight = Math.max(10, newHeight); + + baseX = newX; + baseY = newY; + baseWidth = newWidth; + baseHeight = newHeight; + + updateRectanglePosition(newX, newY, newWidth, newHeight); + updateAllDimensions(); +} + +function isClickInsideRectangle(e, rect, imgRect) { + const rectBounds = rect.getBoundingClientRect(); + return e.clientX >= rectBounds.left && + e.clientX <= rectBounds.right && + e.clientY >= rectBounds.top && + e.clientY <= rectBounds.bottom; +} + +function detectResizeHandle(e, rect) { + const handles = rect.querySelectorAll('.resize-handle'); + for (let handle of handles) { + const handleRect = handle.getBoundingClientRect(); + const distance = Math.sqrt( + Math.pow(e.clientX - (handleRect.left + handleRect.width / 2), 2) + + Math.pow(e.clientY - (handleRect.top + handleRect.height / 2), 2) + ); + if (distance < 12) { + return handle.className.split(' ').find(c => c !== 'resize-handle'); + } + } + return null; +} + +// ============================================================================ +// ZOOM CONTROLS +// ============================================================================ + +function handleZoomIn() { + if (currentRectangle) { + currentZoom = Math.min(currentZoom + zoomStep, maxZoom); + updateZoom(); + updateAllDimensions(); + } +} + +function handleZoomOut() { + if (currentRectangle) { + currentZoom = Math.max(currentZoom - zoomStep, minZoom); + updateZoom(); + updateAllDimensions(); + } +} + +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; +} + +// ============================================================================ +// BORDER CONTROLS +// ============================================================================ + +function handleBorderChange(e) { + currentBorderWidth = parseInt(e.target.value); + borderValue.textContent = currentBorderWidth + 'px'; + + if (currentRectangle) { + currentRectangle.style.borderWidth = currentBorderWidth + 'px'; + updateAllDimensions(); + } +} + +function handleBorderPositionChange(e) { + borderPosition = e.target.value; + + if (currentRectangle) { + currentRectangle.classList.remove('border-inside', 'border-outside'); + currentRectangle.classList.add(`border-${borderPosition}`); + updateAllDimensions(); + } +} + +// ============================================================================ +// RESET AND CLEAR +// ============================================================================ + +function handleReset() { + resetRectangle(); +} + +function resetRectangle() { + if (currentRectangle) { + currentRectangle.remove(); + currentRectangle = null; + } + + rectangleExists = false; + currentZoom = 1; + baseWidth = 0; + baseHeight = 0; + baseX = 0; + baseY = 0; + + baseCoordinates.innerHTML = '

Clicca e trascina sull\'immagine per disegnare un rettangolo

'; + dimensionsInfo.style.display = 'none'; + + disableControls(); +} + +// ============================================================================ +// UPDATE DISPLAY +// ============================================================================ + +function updateAllDimensions() { + if (!rectangleExists) return; + + // Coordinate base (senza zoom e bordi) + const baseInfo = ` + X: ${Math.round(baseX)} px
+ Y: ${Math.round(baseY)} px
+ Larghezza: ${Math.round(baseWidth)} px
+ Altezza: ${Math.round(baseHeight)} px
+
+ Percentuali:
+ X%: ${((baseX / backgroundImage.width) * 100).toFixed(2)}%
+ Y%: ${((baseY / backgroundImage.height) * 100).toFixed(2)}%
+ W%: ${((baseWidth / backgroundImage.width) * 100).toFixed(2)}%
+ H%: ${((baseHeight / backgroundImage.height) * 100).toFixed(2)}% + `; + + baseCoordinates.innerHTML = baseInfo; + + // Mostra info zoom e bordi se zoom > 1 o bordo > 0 + if (currentZoom > 1 || currentBorderWidth > 0) { + const zoomedWidth = baseWidth * currentZoom; + const zoomedHeight = baseHeight * currentZoom; + + const dimensionsHTML = ` + Con Zoom ${Math.round(currentZoom * 100)}%:
+ Larghezza: ${Math.round(zoomedWidth)} px
+ Altezza: ${Math.round(zoomedHeight)} px
+
+ Bordo: ${currentBorderWidth}px (${borderPosition})
+ `; + + currentDimensions.innerHTML = dimensionsHTML; + dimensionsInfo.style.display = 'block'; + } else { + dimensionsInfo.style.display = 'none'; + } +} + +// ============================================================================ +// BUTTON STATES +// ============================================================================ + +function enableControls() { + zoomInBtn.disabled = false; + zoomOutBtn.disabled = false; + resetBtn.disabled = false; + confirmBtn.disabled = false; +} + +function disableControls() { + zoomInBtn.disabled = true; + zoomOutBtn.disabled = true; + resetBtn.disabled = true; + confirmBtn.disabled = true; +} + +// ============================================================================ +// COORDINATE EXPORT +// ============================================================================ + +function getCoordinates() { + if (!rectangleExists) { + return {}; + } + + return { + x: Math.round(baseX), + y: Math.round(baseY), + width: Math.round(baseWidth), + height: Math.round(baseHeight), + zoom: currentZoom, + borderWidth: currentBorderWidth, + borderPosition: borderPosition, + }; +} + +// ============================================================================ +// ACTION BUTTONS +// ============================================================================ + +function handleConfirm() { + const coordinates = getCoordinates(); + + if (!rectangleExists) { + alert('Nessuna selezione effettuata'); + return; + } + + // Invia coordinate al callback + if (onConfirmCallback) { + onConfirmCallback(coordinates); + } + + // Log per debug + console.log('Coordinates confirmed:', coordinates); +} + +function handleCancel() { + if (onCancelCallback) { + onCancelCallback(); + } + // Non chiudere la finestra - il callback si occuperà di chiudere il modal +} + +// ============================================================================ +// PUBLIC API (per integrazione ComfyUI) +// ============================================================================ + +window.RegionSelector = { + /** + * Inizializza il selettore di regioni con callback personalizzati + * @param {Function} onConfirm - Callback quando l'utente conferma (riceve coordinates) + * @param {Function} onCancel - Callback quando l'utente annulla + */ + init: function(onConfirm, onCancel) { + onConfirmCallback = onConfirm; + onCancelCallback = onCancel; + }, + + /** + * Ottieni le coordinate attuali della selezione + * @returns {Object} Oggetto con x, y, width, height, zoom, borderWidth, borderPosition + */ + getCoordinates: function() { + return getCoordinates(); + }, + + /** + * Imposta un'immagine da URL + * @param {String} imageUrl - URL dell'immagine + */ + setImage: function(imageUrl) { + if (currentRectangle) { + if (confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) { + resetRectangle(); + } else { + return; + } + } + backgroundImage.src = imageUrl; + imageName.textContent = 'Immagine caricata'; + }, + + /** + * Carica una selezione precedente + * @param {Object} coordinates - Oggetto coordinate + */ + loadCoordinates: function(coordinates) { + if (!coordinates || !coordinates.x) return; + + baseX = coordinates.x || 0; + baseY = coordinates.y || 0; + baseWidth = coordinates.width || 100; + baseHeight = coordinates.height || 100; + currentZoom = coordinates.zoom || 1; + currentBorderWidth = coordinates.borderWidth || 3; + borderPosition = coordinates.borderPosition || 'inside'; + + // Aggiorna UI + borderSlider.value = currentBorderWidth; + borderValue.textContent = currentBorderWidth + 'px'; + document.querySelector(`input[value="${borderPosition}"]`).checked = true; + updateZoom(); + + // Ridisegna + createNewRectangle(baseX, baseY, baseWidth, baseHeight); + } +}; diff --git a/web/region_selector_extension.js b/web/region_selector_extension.js new file mode 100644 index 0000000..1e7b1bf --- /dev/null +++ b/web/region_selector_extension.js @@ -0,0 +1,804 @@ +/** + * Region Selector Extension - Integrazione nativa con ComfyUI Dialog + * Mantiene il bottone nel nodo e apre un dialog nativo + */ + +import { app } from "../../scripts/app.js"; +import { ComfyDialog } from "../../scripts/ui.js"; + +console.log("[RegionSelectorExt] Loading extension..."); + +/** + * Canvas Selector - Funzionalità complete di selezione rettangoli + * Basato sul codice di canva_html + */ + +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 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; + + // Carica l'immagine iniziale + if (imageUrl) { + backgroundImage.src = imageUrl; + const filename = imageUrl.split('/').pop(); + imageName.textContent = `Image: ${filename}`; + } + + // Disabilita drag dell'immagine + backgroundImage.addEventListener('dragstart', (e) => e.preventDefault()); + backgroundImage.style.userSelect = 'none'; + canvasContainer.style.cursor = 'crosshair'; + + // Border slider - removed from UI, using default values + // currentBorderWidth = defaultBorderWidth (already set to 3) + + // Border position - removed from UI, using default 'inside' + // borderPosition = 'inside' (already set) + + // Reset button + resetBtn.addEventListener('click', () => { + if (confirm('Clear selection and draw a new one?')) { + 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; + + dimensionsInfo.style.display = 'block'; + + addResizeHandles(); + updateAllDimensions(); + + isDrawing = false; + } + + if (isResizing) { + isResizing = false; + resizingEdge = null; + canvasContainer.style.cursor = 'default'; + } + }); + + function resetRectangle() { + if (currentRectangle) { + currentRectangle.remove(); + } + currentRectangle = null; + rectangleExists = false; + canvasContainer.classList.remove('drawing-disabled'); + canvasContainer.style.cursor = 'crosshair'; + resetBtn.disabled = true; + dimensionsInfo.style.display = 'none'; + baseCoordinates.innerHTML = '

Click and drag to select

'; + } + + 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; + + baseCoordinates.innerHTML = ` + 📍 Coordinates:
+ x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px
+ x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px
+
+ 📏 Size:
+ Width: ${Math.round(baseWidth)}px
+ Height: ${Math.round(baseHeight)}px + `; + } + + return { + getCoordinates: () => { + const baseX1 = baseX; + const baseY1 = baseY; + const baseX2 = baseX + baseWidth; + const baseY2 = baseY + baseHeight; + + let effectiveX1 = baseX1; + let effectiveY1 = baseY1; + let effectiveX2 = baseX2; + let effectiveY2 = baseY2; + + if (borderPosition === 'outside') { + effectiveX1 = Math.max(0, baseX1 - currentBorderWidth); + effectiveY1 = Math.max(0, baseY1 - currentBorderWidth); + effectiveX2 = baseX2 + currentBorderWidth; + effectiveY2 = baseY2 + currentBorderWidth; + } else { + effectiveX1 = baseX1 + currentBorderWidth; + effectiveY1 = baseY1 + currentBorderWidth; + effectiveX2 = Math.max(effectiveX1 + 1, baseX2 - currentBorderWidth); + effectiveY2 = Math.max(effectiveY1 + 1, baseY2 - currentBorderWidth); + } + + return { + x1: Math.round(effectiveX1), + y1: Math.round(effectiveY1), + x2: Math.round(effectiveX2), + y2: Math.round(effectiveY2), + borderWidth: currentBorderWidth, + borderPosition: borderPosition, + }; + }, + getState: () => ({ + exists: rectangleExists, + baseX, baseY, baseWidth, baseHeight, + borderWidth: currentBorderWidth, + borderPosition + }) + }; +} + +window.CanvasSelector = { initializeCanvasSelector }; + +app.registerExtension({ + name: "BoxBox.BoxSelectorExtension", + + async setup(app) { + console.log("[RegionSelectorExt] Setup called"); + }, + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "BoxSelector") return; + + console.log("[RegionSelectorExt] Registering BoxSelector..."); + + 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..."); + + // Aggiungi bottone nativo ComfyUI + this.addWidget("button", "📦 Select Box", null, () => { + openRegionDialog(node, app); + }); + + return r; + }; + }, +}); + +/** + * Apre il dialog del selettore di regioni + */ +async function openRegionDialog(node, app) { + console.log("[RegionSelectorExt] Opening dialog..."); + console.log("[RegionSelectorExt] Node inputs:", node.inputs); + console.log("[RegionSelectorExt] Node widgets:", node.widgets); + + let imageName = null; + let imageUrl = null; + + // 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]; + + 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); + } + } + } + + // 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); + } + } + + if (!imageName) { + alert("⚠️ Nessuna immagine collegata al nodo!\n\nCollega un'immagine dal nodo LoadImage."); + return; + } + + imageUrl = `/view?filename=${imageName}&type=input`; + console.log("[RegionSelectorExt] Image URL:", imageUrl); + + // Crea il dialog + const dialog = new ComfyDialog(); + dialog.element.style.width = "95vw"; + dialog.element.style.height = "95vh"; + dialog.element.style.maxWidth = "none"; + dialog.element.style.maxHeight = "none"; + + // Crea il contenitore HTML + const container = document.createElement("div"); + container.style.cssText = ` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + background: white; + `; + + // Aggiungi CSS per il rettangolo e i resize handles + const styleTag = document.createElement("style"); + styleTag.textContent = ` + .rectangle { + position: absolute; + background-color: rgba(37, 99, 235, 0.05); + cursor: move; + transition: none; + z-index: 10; + } + + .rectangle.border-inside { + border: 3px solid #2563eb; + } + + .rectangle.border-outside { + outline: 3px solid #2563eb; + outline-offset: 0px; + } + + .rectangle.thick-border { + box-shadow: 0 0 8px rgba(37, 99, 235, 0.3); + } + + .resize-handle { + position: absolute; + background-color: #2563eb; + z-index: 20; + opacity: 0.7; + transition: opacity 0.2s; + } + + .resize-handle:hover { + opacity: 1; + box-shadow: 0 0 6px rgba(37, 99, 235, 0.8); + } + + .resize-handle.top, + .resize-handle.bottom { + width: 100%; + height: 8px; + cursor: ns-resize; + } + + .resize-handle.top { + top: -4px; + left: 0; + } + + .resize-handle.bottom { + bottom: -4px; + left: 0; + } + + .resize-handle.left, + .resize-handle.right { + width: 8px; + height: 100%; + cursor: ew-resize; + } + + .resize-handle.left { + left: -4px; + top: 0; + } + + .resize-handle.right { + right: -4px; + top: 0; + } + + .resize-handle.top-left, + .resize-handle.bottom-right { + width: 12px; + height: 12px; + cursor: nwse-resize; + } + + .resize-handle.top-left { + top: -6px; + left: -6px; + border-radius: 50%; + } + + .resize-handle.bottom-right { + bottom: -6px; + right: -6px; + border-radius: 50%; + } + + .resize-handle.top-right, + .resize-handle.bottom-left { + width: 12px; + height: 12px; + cursor: nesw-resize; + } + + .resize-handle.top-right { + top: -6px; + right: -6px; + border-radius: 50%; + } + + .resize-handle.bottom-left { + bottom: -6px; + left: -6px; + border-radius: 50%; + } + + #canvas-container.drawing-disabled { + cursor: default !important; + } + `; + document.head.appendChild(styleTag); + + // Carica l'HTML del selettore + const innerHtml = ` +
+ +
+

📦 Box Selector

+

Click and drag on the image to select a box

+
+ + +
+ +
+

Controls

+ + +
+ Image: ${imageName} +
+ + + + + + +
+

Selection

+
+

Click and drag to select

+
+
+
+ + + +
+ + +
+
+ Region Selector +
+
+
+
+ `; + + container.innerHTML = innerHtml; + dialog.element.appendChild(container); + + // Aggiungi i pulsanti d'azione + const confirmBtn = document.createElement("button"); + confirmBtn.textContent = "✅ Confirm"; + confirmBtn.style.cssText = ` + background-color: #16a34a; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + margin-right: 10px; + `; + + const cancelBtn = document.createElement("button"); + cancelBtn.textContent = "❌ Cancel"; + cancelBtn.style.cssText = ` + background-color: #dc2626; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + `; + + // Il confirmBtn.onclick sarà impostato dopo l'inizializzazione del canvas_selector + confirmBtn.onclick = () => { + console.log("[RegionSelectorExt] Confirm button clicked but CanvasSelector not ready yet"); + alert("Per favore aspetta che il selettore sia caricato"); + }; + + cancelBtn.onclick = () => { + console.log("[RegionSelectorExt] Dialog cancelled"); + dialog.close(); + }; + + dialog.show(confirmBtn, cancelBtn); + + // Inizializza il selettore dopo che il DOM è renderizzato + setTimeout(() => { + // Carica canvas_selector.js se non è già caricato + let attempts = 0; + const waitForCanvasSelector = setInterval(() => { + attempts++; + if (window.CanvasSelector) { + clearInterval(waitForCanvasSelector); + const selector = window.CanvasSelector.initializeCanvasSelector(container, imageUrl); + + // Aggiorna il confirm button per usare le coordinate reali + confirmBtn.onclick = () => { + const coords = selector.getCoordinates(); + + // Cerca il widget region_metadata tra i widget del nodo + // ComfyUI lo crea automaticamente dal INPUT_TYPES opzionale + let metadataWidget = node.widgets?.find((w) => w.name === "region_metadata"); + + if (metadataWidget) { + const metadata = JSON.stringify({ + ...coords, + selected: true + }); + metadataWidget.value = metadata; + if (metadataWidget.callback) { + metadataWidget.callback(metadata); + } + console.log("[RegionSelectorExt] Metadata widget updated with:", metadata); + } else { + console.warn("[RegionSelectorExt] region_metadata widget not found. Available widgets:", node.widgets?.map(w => w.name) || []); + } + + console.log("[RegionSelectorExt] Coordinates saved:", coords); + dialog.close(); + }; + console.log("[RegionSelectorExt] CanvasSelector initialized successfully"); + } else if (attempts > 50) { + clearInterval(waitForCanvasSelector); + console.error("[RegionSelectorExt] CanvasSelector failed to load after 5 seconds"); + alert("Error: Box Selector failed to load. Check browser console for details."); + } + }, 100); + }, 200); +} + +/** + * Esegue il nodo sorgente per ottenere l'immagine + */ +async function executeSourceNode(sourceNode, app) { + try { + // Se il nodo sorgente è LoadImage, il widget "image" contiene il filename + const imageWidget = sourceNode.widgets?.find(w => w.name === "image"); + if (imageWidget && imageWidget.value) { + console.log("[RegionSelectorExt] Found image in source node:", imageWidget.value); + return imageWidget.value; + } + } catch (e) { + console.error("[RegionSelectorExt] Error in executeSourceNode:", e); + } +} + +console.log("[RegionSelectorExt] Extension loaded!"); diff --git a/web/region_selector_widget.js b/web/region_selector_widget.js new file mode 100644 index 0000000..1bb4722 --- /dev/null +++ b/web/region_selector_widget.js @@ -0,0 +1,302 @@ +/** + * Region Selector Widget - Integrazione con ComfyUI + * Aggiunge un bottone al nodo per aprire il selettore di regioni + */ + +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "ImageRegionSelect.RegionSelector", + + /** + * Quando un nodo viene creato, aggiungi il widget personalizzato + */ + async nodeCreated(node, app) { + // Esegui solo per il nodo RegionSelectorNode + if (node.comfyClass !== "RegionSelectorNode") return; + + // Trova i widget del nodo + const widthWidget = node.widgets.find(w => w.name === "image_width"); + const heightWidget = node.widgets.find(w => w.name === "image_height"); + const coordinatesWidget = node.widgets.find(w => w.name === "coordinates_json"); + + // Crea bottone per aprire il selettore + const selectorButton = document.createElement("button"); + selectorButton.textContent = "🎯 Apri Selettore Regioni"; + selectorButton.style.cssText = ` + width: 100%; + padding: 10px; + margin: 5px 0; + background-color: #2563eb; + color: white; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + `; + + selectorButton.addEventListener("mouseover", () => { + selectorButton.style.backgroundColor = "#1d4ed8"; + }); + + selectorButton.addEventListener("mouseout", () => { + selectorButton.style.backgroundColor = "#2563eb"; + }); + + selectorButton.addEventListener("click", () => { + openRegionSelector(node, widthWidget, heightWidget, coordinatesWidget); + }); + + // Aggiungi il bottone al nodo + const container = document.createElement("div"); + container.style.marginBottom = "10px"; + container.appendChild(selectorButton); + + // Inserisci prima del primo widget + if (node.widgets.length > 0) { + node.widgets[0].element?.parentNode?.insertBefore(container, node.widgets[0].element); + } + + // Aggiungi anche come custom widget per miglior tracking + node.regionSelectorButton = selectorButton; + node.regionSelectorContainer = container; + + // Aggiungi etichetta informativa + const infoLabel = document.createElement("div"); + infoLabel.style.cssText = ` + font-size: 11px; + color: #64748b; + padding: 5px 0; + margin: 5px 0; + text-align: center; + `; + infoLabel.textContent = "Apri l'interfaccia per selezionare le coordinate della regione"; + container.appendChild(infoLabel); + }, + + /** + * Hook che si chiama quando il nodo viene caricato + */ + async loadedGraphNode(node, app) { + if (node.comfyClass !== "RegionSelectorNode") return; + + // Ripristina lo stato del bottone se necessario + const coordinatesWidget = node.widgets.find(w => w.name === "coordinates_json"); + if (coordinatesWidget && coordinatesWidget.value && coordinatesWidget.value !== "{}") { + console.log("Region coordinates loaded:", coordinatesWidget.value); + } + } +}); + +/** + * Apre l'interfaccia selettore di regioni + */ +function openRegionSelector(node, widthWidget, heightWidget, coordinatesWidget) { + // Carica l'HTML dell'interfaccia + const htmlPath = "extensions/ImageRegionSelect/region_selector.html"; + + fetch(htmlPath) + .then(response => { + if (!response.ok) throw new Error(`Failed to load ${htmlPath}`); + return response.text(); + }) + .then(html => { + // Crea una finestra modale + const modal = createModal(html, node, widthWidget, heightWidget, coordinatesWidget); + document.body.appendChild(modal); + }) + .catch(error => { + console.error("Error loading region selector:", error); + alert("Errore nel caricamento del selettore di regioni: " + error.message); + }); +} + +/** + * Crea una finestra modale con il selettore + */ +function createModal(html, node, widthWidget, heightWidget, coordinatesWidget) { + const modal = document.createElement("div"); + modal.id = "region-selector-modal"; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); + `; + + // Container principale + const container = document.createElement("div"); + container.style.cssText = ` + width: 90vw; + height: 90vh; + background-color: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + overflow: hidden; + `; + + // Inserisci l'HTML del selettore + container.innerHTML = html; + + modal.appendChild(container); + + // Attendi che il DOM sia pronto + setTimeout(() => { + // Inizializza il selettore con i callback + if (window.RegionSelector) { + window.RegionSelector.init( + (coordinates) => handleCoordinatesConfirmed(coordinates, modal, coordinatesWidget, node), + () => handleSelectorCancel(modal) + ); + + // Carica coordinate precedenti se esistono + if (coordinatesWidget && coordinatesWidget.value && coordinatesWidget.value !== "{}") { + try { + const previousCoordinates = JSON.parse(coordinatesWidget.value); + if (previousCoordinates.x !== undefined) { + window.RegionSelector.loadCoordinates(previousCoordinates); + } + } catch (e) { + console.warn("Could not load previous coordinates:", e); + } + } + } + + // Aggiorna i bottoni di chiusura + const closeBtn = document.getElementById("close-btn"); + const cancelBtn = document.getElementById("cancel-btn"); + + if (closeBtn) { + closeBtn.addEventListener("click", () => handleSelectorCancel(modal)); + } + if (cancelBtn) { + cancelBtn.addEventListener("click", () => handleSelectorCancel(modal)); + } + + // Precarica l'immagine di sfondo (opzionale) + loadBackgroundImage(node, widthWidget, heightWidget); + + }, 50); + + return modal; +} + +/** + * Gestisce la conferma delle coordinate + */ +function handleCoordinatesConfirmed(coordinates, modal, coordinatesWidget, node) { + // Salva le coordinate nel widget JSON + coordinatesWidget.value = JSON.stringify(coordinates); + + // Forza l'aggiornamento del nodo + if (node.widgets) { + // Trigga callback se esiste + if (coordinatesWidget.callback) { + coordinatesWidget.callback(coordinatesWidget.value); + } + } + + // Log per debug + console.log("Region coordinates confirmed and saved:", coordinates); + + // Mostra messaggio di successo + showNotification("✅ Selezione confermata!", "success"); + + // Chiudi la modale + modal.remove(); +} + +/** + * Gestisce l'annullamento + */ +function handleSelectorCancel(modal) { + modal.remove(); + console.log("Region selector cancelled"); +} + +/** + * Carica un'immagine come sfondo nell'interfaccia + * (Opzionale - per preview) + */ +function loadBackgroundImage(node, widthWidget, heightWidget) { + try { + // Cerca un nodo con input immagine nel flusso + // Per ora, usa l'immagine default di picsum.photos + const bgImg = document.getElementById("background-image"); + if (bgImg) { + // Opzionalmente puoi aggiungere logica per caricare immagini + console.log("Image loader ready"); + } + } catch (e) { + console.warn("Could not auto-load image:", e); + } +} + +/** + * Mostra una notifica temporanea + */ +function showNotification(message, type = "info") { + const notification = document.createElement("div"); + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + z-index: 20000; + animation: slideIn 0.3s ease; + ${type === "success" ? "background-color: #16a34a; color: white;" : + type === "error" ? "background-color: #dc2626; color: white;" : + "background-color: #2563eb; color: white;"} + `; + + document.body.appendChild(notification); + + // Auto-remove dopo 3 secondi + setTimeout(() => { + notification.style.animation = "slideOut 0.3s ease"; + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +// Aggiungi animazioni CSS +const style = document.createElement("style"); +style.textContent = ` + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(400px); + opacity: 0; + } + } +`; +document.head.appendChild(style);