Add files via upload

This commit is contained in:
mercu-lore
2025-11-10 18:16:04 +01:00
committed by GitHub
parent 14f9a6f5e1
commit 19a18a3511
13 changed files with 4479 additions and 0 deletions
+29
View File
@@ -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"]
+148
View File
@@ -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"}
+191
View File
@@ -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"}
+113
View File
@@ -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"}
+76
View File
@@ -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"}
+3
View File
@@ -0,0 +1,3 @@
torch>=2.0.0
numpy>=1.20.0
Pillow>=8.0.0
+516
View File
@@ -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 = '<p>Clicca e trascina sull\'immagine per disegnare un rettangolo</p>';
}
function updateZoom() {
const zoomPercent = Math.round(currentZoom * 100);
zoomValue.textContent = zoomPercent + '%';
if (currentRectangle) {
currentRectangle.style.transform = `scale(${currentZoom})`;
}
zoomInBtn.disabled = currentZoom >= maxZoom;
zoomOutBtn.disabled = currentZoom <= minZoom;
}
function addResizeHandles() {
const handles = ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
handles.forEach(position => {
const handle = document.createElement('div');
handle.className = `resize-handle ${position}`;
handle.dataset.edge = position;
currentRectangle.appendChild(handle);
handle.addEventListener('mousedown', (e) => {
e.stopPropagation();
isResizing = true;
resizingEdge = position;
startX = e.clientX;
startY = e.clientY;
rectStartX = parseFloat(currentRectangle.style.left);
rectStartY = parseFloat(currentRectangle.style.top);
rectStartWidth = parseFloat(currentRectangle.style.width);
rectStartHeight = parseFloat(currentRectangle.style.height);
canvasContainer.style.cursor = getComputedStyle(handle).cursor;
});
});
}
function updateAllDimensions() {
if (!currentRectangle) return;
const baseX1 = baseX;
const baseY1 = baseY;
const baseX2 = baseX + baseWidth;
const baseY2 = baseY + baseHeight;
const centerX = baseX + (baseWidth / 2);
const centerY = baseY + (baseHeight / 2);
const zoomedWidth = baseWidth * currentZoom;
const zoomedHeight = baseHeight * currentZoom;
const zoomedX1 = centerX - (zoomedWidth / 2);
const zoomedY1 = centerY - (zoomedHeight / 2);
const zoomedX2 = centerX + (zoomedWidth / 2);
const zoomedY2 = centerY + (zoomedHeight / 2);
let effectiveX1, effectiveY1, effectiveX2, effectiveY2;
let effectiveWidth, effectiveHeight;
if (borderPosition === 'inside') {
effectiveX1 = zoomedX1;
effectiveY1 = zoomedY1;
effectiveX2 = zoomedX2;
effectiveY2 = zoomedY2;
effectiveWidth = zoomedWidth;
effectiveHeight = zoomedHeight;
} else {
effectiveX1 = zoomedX1 - currentBorderWidth;
effectiveY1 = zoomedY1 - currentBorderWidth;
effectiveX2 = zoomedX2 + currentBorderWidth;
effectiveY2 = zoomedY2 + currentBorderWidth;
effectiveWidth = zoomedWidth + (currentBorderWidth * 2);
effectiveHeight = zoomedHeight + (currentBorderWidth * 2);
}
baseCoordinates.innerHTML = `
<strong>📍 Coordinate Base:</strong><br>
x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px<br>
x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px<br>
<br>
<strong>📏 Dimensioni Base:</strong><br>
Larghezza: ${Math.round(baseWidth)}px<br>
Altezza: ${Math.round(baseHeight)}px
`;
currentDimensions.innerHTML = `
<strong>🔍 Con Zoom (${Math.round(currentZoom * 100)}%):</strong><br>
Larghezza: ${Math.round(zoomedWidth)}px<br>
Altezza: ${Math.round(zoomedHeight)}px<br>
<br>
<strong>🎨 Bordo ${borderPosition === 'inside' ? 'Interno' : 'Esterno'} (${currentBorderWidth}px):</strong><br>
Larghezza Totale: ${Math.round(effectiveWidth)}px<br>
Altezza Totale: ${Math.round(effectiveHeight)}px
`;
}
// Esporta la funzione per ottenere le coordinate
return {
getCoordinates: () => {
// Calcola le coordinate dei lati (x1, x2, y1, y2) con zoom
const baseX1 = baseX;
const baseY1 = baseY;
const baseX2 = baseX + baseWidth;
const baseY2 = baseY + baseHeight;
// Applica lo zoom mantenendo il centro
const centerX = baseX + (baseWidth / 2);
const centerY = baseY + (baseHeight / 2);
const zoomedWidth = baseWidth * currentZoom;
const zoomedHeight = baseHeight * currentZoom;
const zoomedX1 = centerX - (zoomedWidth / 2);
const zoomedY1 = centerY - (zoomedHeight / 2);
const zoomedX2 = centerX + (zoomedWidth / 2);
const zoomedY2 = centerY + (zoomedHeight / 2);
// Applica il bordo
let effectiveX1 = zoomedX1;
let effectiveY1 = zoomedY1;
let effectiveX2 = zoomedX2;
let effectiveY2 = zoomedY2;
if (borderPosition === 'outside') {
// Se il bordo è esterno, espandi le coordinate
effectiveX1 = Math.max(0, zoomedX1 - currentBorderWidth);
effectiveY1 = Math.max(0, zoomedY1 - currentBorderWidth);
effectiveX2 = zoomedX2 + currentBorderWidth;
effectiveY2 = zoomedY2 + currentBorderWidth;
} else {
// Se il bordo è interno, restringe le coordinate
effectiveX1 = zoomedX1 + currentBorderWidth;
effectiveY1 = zoomedY1 + currentBorderWidth;
effectiveX2 = Math.max(effectiveX1 + 1, zoomedX2 - currentBorderWidth);
effectiveY2 = Math.max(effectiveY1 + 1, zoomedY2 - currentBorderWidth);
}
return {
x1: Math.round(effectiveX1),
y1: Math.round(effectiveY1),
x2: Math.round(effectiveX2),
y2: Math.round(effectiveY2),
zoom: currentZoom,
borderWidth: currentBorderWidth,
borderPosition: borderPosition,
};
},
getState: () => ({
exists: rectangleExists,
baseX, baseY, baseWidth, baseHeight,
zoom: currentZoom,
borderWidth: currentBorderWidth,
borderPosition
})
};
}
window.CanvasSelector = { initializeCanvasSelector };
+503
View File
@@ -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;
}
+626
View File
@@ -0,0 +1,626 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Region Selector - ComfyUI</title>
<style>
/* 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;
}
</style>
</head>
<body>
<div class="selector-container">
<!-- Header -->
<div class="selector-header">
<h1>🎯 Region Selector</h1>
<p>Clicca e trascina sull'immagine per selezionare una regione</p>
<button id="close-btn" class="close-button" title="Chiudi editor">✕</button>
</div>
<!-- Main Content -->
<div class="selector-content">
<!-- Control Panel -->
<div class="control-panel">
<h3>Controlli</h3>
<!-- Image Upload -->
<div class="control-group">
<label for="image-upload">Carica Immagine:</label>
<input type="file" id="image-upload" accept="image/*" style="display: none;">
<button id="upload-btn" class="btn btn-primary">
<span>📁</span> Scegli Immagine
</button>
<small id="image-name">Immagine corrente: picsum.photos</small>
</div>
<hr>
<!-- Zoom Controls -->
<div class="control-group">
<label>Zoom Selezione:</label>
<div class="zoom-buttons">
<button id="zoom-out-btn" class="btn btn-small" disabled>🔍- Zoom Out</button>
<button id="zoom-in-btn" class="btn btn-small" disabled>🔍+ Zoom In</button>
</div>
<div class="zoom-display">
<span id="zoom-value">100%</span>
</div>
<small>Zoom del rettangolo selezionato</small>
</div>
<!-- Border Width -->
<div class="control-group">
<label for="border-slider">
Spessore Bordo: <span id="border-value">3px</span>
</label>
<input type="range" id="border-slider" min="1" max="20" value="3" step="1">
<small>Larghezza linea di selezione</small>
</div>
<!-- Border Position -->
<div class="control-group">
<label>Posizione Bordo:</label>
<div class="radio-group">
<label>
<input type="radio" name="border-position" value="inside" checked>
Interno
</label>
<label>
<input type="radio" name="border-position" value="outside">
Esterno
</label>
</div>
</div>
<hr>
<!-- Reset Button -->
<div class="control-group">
<button id="reset-btn" class="btn btn-secondary" disabled>
🔄 Cancella Selezione
</button>
</div>
<hr>
<!-- Coordinates Info -->
<div id="coordinates-info">
<h4>Informazioni Selezione</h4>
<div id="base-coordinates" class="info-box">
<p>Clicca e trascina sull'immagine per selezionare una regione</p>
</div>
<div id="dimensions-info" style="display: none;">
<h4 style="margin-top: 15px;">Dimensioni Con Zoom</h4>
<div id="current-dimensions" class="info-box info-box-secondary">
</div>
</div>
</div>
<hr>
<!-- Action Buttons -->
<div class="control-group action-buttons">
<button id="confirm-btn" class="btn btn-success" disabled>
✅ Conferma Selezione
</button>
<button id="cancel-btn" class="btn btn-danger">
❌ Annulla
</button>
</div>
</div>
<!-- Canvas Area -->
<div class="canvas-area">
<div id="canvas-container">
<img src="https://picsum.photos/800/500" alt="Immagine di esempio" id="background-image">
</div>
</div>
</div>
</div>
<script src="/extensions/image_region_select/region_selector_core.js"></script>
</body>
</html>
+584
View File
@@ -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 = '<p>Clicca e trascina sull\'immagine per disegnare un rettangolo</p>';
dimensionsInfo.style.display = 'none';
disableControls();
}
// ============================================================================
// UPDATE DISPLAY
// ============================================================================
function updateAllDimensions() {
if (!rectangleExists) return;
// Coordinate base (senza zoom e bordi)
const baseInfo = `
<strong>X:</strong> ${Math.round(baseX)} px<br>
<strong>Y:</strong> ${Math.round(baseY)} px<br>
<strong>Larghezza:</strong> ${Math.round(baseWidth)} px<br>
<strong>Altezza:</strong> ${Math.round(baseHeight)} px<br>
<br>
<strong>Percentuali:</strong><br>
<strong>X%:</strong> ${((baseX / backgroundImage.width) * 100).toFixed(2)}%<br>
<strong>Y%:</strong> ${((baseY / backgroundImage.height) * 100).toFixed(2)}%<br>
<strong>W%:</strong> ${((baseWidth / backgroundImage.width) * 100).toFixed(2)}%<br>
<strong>H%:</strong> ${((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 = `
<strong>Con Zoom ${Math.round(currentZoom * 100)}%:</strong><br>
<strong>Larghezza:</strong> ${Math.round(zoomedWidth)} px<br>
<strong>Altezza:</strong> ${Math.round(zoomedHeight)} px<br>
<br>
<strong>Bordo:</strong> ${currentBorderWidth}px (${borderPosition})<br>
`;
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);
}
};
+584
View File
@@ -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 = '<p>Clicca e trascina sull\'immagine per disegnare un rettangolo</p>';
dimensionsInfo.style.display = 'none';
disableControls();
}
// ============================================================================
// UPDATE DISPLAY
// ============================================================================
function updateAllDimensions() {
if (!rectangleExists) return;
// Coordinate base (senza zoom e bordi)
const baseInfo = `
<strong>X:</strong> ${Math.round(baseX)} px<br>
<strong>Y:</strong> ${Math.round(baseY)} px<br>
<strong>Larghezza:</strong> ${Math.round(baseWidth)} px<br>
<strong>Altezza:</strong> ${Math.round(baseHeight)} px<br>
<br>
<strong>Percentuali:</strong><br>
<strong>X%:</strong> ${((baseX / backgroundImage.width) * 100).toFixed(2)}%<br>
<strong>Y%:</strong> ${((baseY / backgroundImage.height) * 100).toFixed(2)}%<br>
<strong>W%:</strong> ${((baseWidth / backgroundImage.width) * 100).toFixed(2)}%<br>
<strong>H%:</strong> ${((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 = `
<strong>Con Zoom ${Math.round(currentZoom * 100)}%:</strong><br>
<strong>Larghezza:</strong> ${Math.round(zoomedWidth)} px<br>
<strong>Altezza:</strong> ${Math.round(zoomedHeight)} px<br>
<br>
<strong>Bordo:</strong> ${currentBorderWidth}px (${borderPosition})<br>
`;
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);
}
};
+804
View File
@@ -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 = '<p>Click and drag to select</p>';
}
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 = `
<strong>📍 Coordinates:</strong><br>
x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px<br>
x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px<br>
<br>
<strong>📏 Size:</strong><br>
Width: ${Math.round(baseWidth)}px<br>
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 = `
<div class="selector-container" style="height: 100%; display: flex; flex-direction: column;">
<!-- Header -->
<div class="selector-header" style="
position: relative;
padding: 20px 25px;
background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
">
<h1 style="font-size: 24px; margin: 0 0 5px 0; font-weight: 700;">📦 Box Selector</h1>
<p style="font-size: 13px; opacity: 0.9; margin: 0;">Click and drag on the image to select a box</p>
</div>
<!-- Main Content -->
<div class="selector-content" style="display: flex; flex: 1; overflow: hidden;">
<!-- Control Panel -->
<div class="control-panel" style="
width: 320px;
background-color: #ffffff;
overflow-y: auto;
padding: 20px;
">
<h3 style="font-size: 16px; margin-bottom: 15px; font-weight: 600;">Controls</h3>
<!-- Image info -->
<div class="control-group" style="margin-bottom: 20px;">
<small id="image-name" style="display: block; font-size: 12px; color: #64748b;">Image: ${imageName}</small>
</div>
<!-- Reset Button -->
<button id="reset-btn" class="btn" disabled style="
background-color: #64748b;
color: white;
border: 1px solid #64748b;
width: 100%;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
opacity: 0.5;
margin-bottom: 20px;
">🔄 Clear Selection</button>
<!-- Coordinates Info -->
<div id="coordinates-info">
<h4 style="font-size: 13px; margin-bottom: 10px; font-weight: 600; text-transform: uppercase;">Selection</h4>
<div id="base-coordinates" style="
background-color: transparent;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
margin-bottom: 10px;
">
<p style="margin: 0;">Click and drag to select</p>
</div>
<div id="current-dimensions" style="
background-color: transparent;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
display: none;
"></div>
</div>
<!-- Dimensions Info -->
<div id="dimensions-info" style="display: none; margin-top: 10px;"></div>
</div>
<!-- Canvas Area -->
<div class="canvas-area" style="
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #fafbfc;
overflow: hidden;
position: relative;
">
<div id="canvas-container" style="
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%;
">
<img src="${imageUrl}" alt="Region Selector" id="background-image" style="
display: block;
max-width: 100%;
max-height: 100%;
user-select: none;
">
</div>
</div>
</div>
</div>
`;
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!");
+302
View File
@@ -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);