Add files via upload
This commit is contained in:
+29
@@ -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"]
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,3 @@
|
||||
torch>=2.0.0
|
||||
numpy>=1.20.0
|
||||
Pillow>=8.0.0
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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!");
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user