Update BoxBox: NumPy 2.0 & ComfyUI v0.12.2 compatibility, improved coordinate precision and modern API support

This commit is contained in:
Pablo Montero
2026-02-04 18:13:08 +01:00
parent ed47ef8091
commit eeff47f654
5 changed files with 239 additions and 655 deletions
+13
View File
@@ -27,6 +27,19 @@ This is an improved fork of the original [BoxBox](https://github.com/mercu-lore/
- Now works with processing nodes (Brightness, Blur, etc.) between LoadImage and BoxSelector
- Searches up to 20 levels deep in the node graph
5. **NumPy 2.0 Compatibility**
- Updated backend logic with explicit casting (`np.uint8`, `np.float32`)
- Fixed binary incompatibility issues in coordinate processing
6. **ComfyUI v0.12.2+ (Modern API) Support**
- Re-engineered frontend to use the modern `window.comfyAPI` system
- Eliminated "Legacy API" deprecation warnings
- Improved dialog instantiation with robust fallback mechanisms
7. **Adaptive Window Scaling**
- Fixed coordinate misalignment when the browser window is resized
- System now dynamically calculates total scale (Server Scale * Browser CSS Scale)
### 🆕 New Features
1. **Aspect Ratio Memory**
+8 -15
View File
@@ -107,38 +107,31 @@ class BoxReinsertNode:
crop_width = box_x_end - box_x_start
crop_height = box_y_end - box_y_start
# Converti immagini a PIL
original_np = (original_image[0].cpu().numpy() * 255).astype(np.uint8)
# Converti immagini a PIL con casting esplicito
# original_image es (B, H, W, C)
original_np = (original_image[0].detach().cpu().numpy() * 255).round().astype(np.uint8)
original_pil = Image.fromarray(original_np)
generated_np = (generated_image[0].cpu().numpy() * 255).astype(np.uint8)
generated_np = (generated_image[0].detach().cpu().numpy() * 255).round().astype(np.uint8)
generated_pil = Image.fromarray(generated_np)
print(f"[BoxReinsertNode] Generated image size: {generated_pil.size}")
print(f"[BoxReinsertNode] Target crop size: {crop_width}x{crop_height}")
print(f"[BoxReinsertNode] Use resize: {use_resize}")
# Step 1: Ridimensiona solo se resize_metadata è fornito
# Step 1: Ridimensiona
if use_resize:
# Annulla il resize fatto da BoxResize
print(f"[BoxReinsertNode] Resizing generated image back to crop dimensions...")
generated_resized = generated_pil.resize((crop_width, crop_height), Image.Resampling.LANCZOS)
print(f"[BoxReinsertNode] Generated image resized from {generated_pil.size} to {generated_resized.size}")
else:
# Bypassa il resize e usa direttamente l'immagine generata
generated_resized = generated_pil
print(f"[BoxReinsertNode] Bypassing resize, using generated image as-is")
# Step 2: Rimetti nel punto originale
# Crea una copia dell'immagine originale
final_image = original_pil.copy()
# Incolla l'immagine generata nel punto corretto
final_image.paste(generated_resized, (box_x_start, box_y_start))
# Converti back a tensor
final_np = np.array(final_image).astype(np.float32) / 255.0
final_tensor = torch.from_numpy(final_np).unsqueeze(0)
# Converti back a tensor con casting sicuro per NumPy 2.0
final_np_array = np.array(final_image, dtype=np.float32) / 255.0
final_tensor = torch.from_numpy(final_np_array).unsqueeze(0)
# Assicurati che il tensor abbia le giuste dimensioni
channels = original_image.shape[3]
+39 -10
View File
@@ -38,16 +38,31 @@ class RegionSelectorNode:
return (image, self.last_metadata)
def scale_image_if_needed(filename, max_size=1024):
input_path = folder_paths.get_input_directory()
full_path = os.path.join(input_path, filename)
def scale_image_if_needed(filename, type="input", subfolder="", max_size=1024):
if type == "output":
base_dir = folder_paths.get_output_directory()
elif type == "temp":
base_dir = folder_paths.get_temp_directory()
else:
base_dir = folder_paths.get_input_directory()
if subfolder:
full_path = os.path.join(base_dir, subfolder, filename)
else:
full_path = os.path.join(base_dir, filename)
if not os.path.exists(full_path):
return {"error": "file not found"}
return {"error": f"file not found: {full_path}"}
with Image.open(full_path) as im:
w, h = im.size
# Use comma as separator for filename, type, subfolder to assume default behaviour or backward compatibility
path_params = f"filename={filename}&type={type}"
if subfolder:
path_params += f"&subfolder={subfolder}"
if w <= max_size and h <= max_size:
return {"scaled": False, "path": f"/view?filename={filename}&type=input"}
return {"scaled": False, "path": f"/view?{path_params}"}
scale_factor = min(max_size / w, max_size / h)
new_size = (int(w * scale_factor), int(h * scale_factor))
@@ -59,6 +74,9 @@ def scale_image_if_needed(filename, max_size=1024):
# Sanitize filename: replace path separators with underscores
safe_filename = filename.replace('/', '_').replace('\\', '_')
if subfolder:
safe_filename = f"{subfolder}_{safe_filename}".replace('/', '_').replace('\\', '_')
scaled_name = f"scaled_{safe_filename}"
scaled_path = os.path.join(temp_dir, scaled_name)
im_resized.save(scaled_path)
@@ -71,15 +89,26 @@ def scale_image_if_needed(filename, max_size=1024):
async def scale_image_endpoint(request):
from aiohttp import web
try:
data = await request.json()
# Intentar leer el JSON de forma segura
try:
data = await request.json()
except:
# Fallback si el content-type no es exacto o el cuerpo está malformado
post_data = await request.post()
data = dict(post_data)
filename = data.get("filename")
type_ = data.get("type", "input")
subfolder = data.get("subfolder", "")
if not filename:
return web.json_response({"error": "filename missing"})
result = scale_image_if_needed(filename)
return web.json_response({"error": "filename missing"}, status=400)
result = scale_image_if_needed(filename, type_, subfolder)
return web.json_response(result)
except Exception as e:
print(f"[RegionSelector] Error in scale endpoint: {e}")
return web.json_response({"error": str(e)})
print(f"[BoxBox] Error in scale endpoint: {e}")
return web.json_response({"error": str(e)}, status=500)
NODE_CLASS_MAPPINGS = {"BoxSelector": RegionSelectorNode}
+3 -514
View File
@@ -1,516 +1,5 @@
/**
* Canvas Selector - Funzionalità complete di selezione rettangoli
* Basato sul codice di canva_html
* Canvas Selector - Placeholder
* Functionality moved to region_selector_extension.js to avoid duplicate code and synchronization issues.
*/
function initializeCanvasSelector(container, imageUrl) {
console.log("[CanvasSelector] Initializing with image:", imageUrl);
// Riferimenti agli elementi DOM (cercati dentro il container)
const canvasContainer = container.querySelector('#canvas-container');
const backgroundImage = container.querySelector('#background-image');
const baseCoordinates = container.querySelector('#base-coordinates');
const dimensionsInfo = container.querySelector('#dimensions-info');
const currentDimensions = container.querySelector('#current-dimensions');
const zoomInBtn = container.querySelector('#zoom-in-btn');
const zoomOutBtn = container.querySelector('#zoom-out-btn');
const zoomValue = container.querySelector('#zoom-value');
const borderSlider = container.querySelector('#border-slider');
const borderValue = container.querySelector('#border-value');
const resetBtn = container.querySelector('#reset-btn');
const borderPositionRadios = container.querySelectorAll('input[name="border-position"]');
const imageUpload = container.querySelector('#image-upload');
const uploadBtn = container.querySelector('#upload-btn');
const imageName = container.querySelector('#image-name');
// Variabili di stato
let isDrawing = false;
let isResizing = false;
let resizingEdge = null;
let startX = 0;
let startY = 0;
let rectStartX = 0;
let rectStartY = 0;
let rectStartWidth = 0;
let rectStartHeight = 0;
let currentRectangle = null;
let currentZoom = 1;
let currentBorderWidth = 0;
let borderPosition = 'inside';
let rectangleExists = false;
// Dimensioni base
let baseWidth = 0;
let baseHeight = 0;
let baseX = 0;
let baseY = 0;
const defaultBorderWidth = 3;
const zoomStep = 0.25;
const minZoom = 0.25;
const maxZoom = 4;
// Carica l'immagine iniziale
if (imageUrl) {
backgroundImage.src = imageUrl;
const filename = imageUrl.split('/').pop();
imageName.textContent = `Immagine: ${filename}`;
}
// ========== EVENT LISTENERS ==========
// Upload button
uploadBtn.addEventListener('click', () => imageUpload.click());
imageUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (event) => {
backgroundImage.src = event.target.result;
imageName.textContent = `Immagine: ${file.name}`;
if (currentRectangle && confirm('Cambiando immagine il rettangolo verrà cancellato. Continuare?')) {
resetRectangle();
}
};
reader.readAsDataURL(file);
}
});
// Zoom buttons
zoomInBtn.addEventListener('click', () => {
if (currentRectangle) {
currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
updateZoom();
updateAllDimensions();
}
});
zoomOutBtn.addEventListener('click', () => {
if (currentRectangle) {
currentZoom = Math.max(currentZoom - zoomStep, minZoom);
updateZoom();
updateAllDimensions();
}
});
// Border slider
borderSlider.addEventListener('input', (e) => {
currentBorderWidth = parseInt(e.target.value);
borderValue.textContent = currentBorderWidth + 'px';
if (currentRectangle) {
if (currentRectangle.classList.contains('border-outside')) {
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
} else {
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
}
if (currentBorderWidth > defaultBorderWidth) {
currentRectangle.classList.add('thick-border');
} else {
currentRectangle.classList.remove('thick-border');
}
updateAllDimensions();
}
});
// Border position radios
borderPositionRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
borderPosition = e.target.value;
if (currentRectangle) {
if (borderPosition === 'outside') {
currentRectangle.classList.remove('border-inside');
currentRectangle.classList.add('border-outside');
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
currentRectangle.style.borderWidth = '0px';
} else {
currentRectangle.classList.remove('border-outside');
currentRectangle.classList.add('border-inside');
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
currentRectangle.style.outlineWidth = '0px';
}
updateAllDimensions();
}
});
});
// Reset button
resetBtn.addEventListener('click', () => {
if (confirm('Vuoi cancellare il rettangolo e disegnarne uno nuovo?')) {
resetRectangle();
}
});
// Mouse down - inizio disegno
canvasContainer.addEventListener('mousedown', (e) => {
if (rectangleExists) return;
if (e.target.classList.contains('resize-handle')) return;
const rect = canvasContainer.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
isDrawing = true;
currentRectangle = document.createElement('div');
currentRectangle.className = 'rectangle';
if (borderPosition === 'outside') {
currentRectangle.classList.add('border-outside');
currentRectangle.style.outlineWidth = currentBorderWidth + 'px';
currentRectangle.style.outlineStyle = 'solid';
currentRectangle.style.borderWidth = '0px';
} else {
currentRectangle.classList.add('border-inside');
currentRectangle.style.borderWidth = currentBorderWidth + 'px';
}
currentRectangle.style.left = startX + 'px';
currentRectangle.style.top = startY + 'px';
currentRectangle.style.width = '0px';
currentRectangle.style.height = '0px';
if (currentBorderWidth > defaultBorderWidth) {
currentRectangle.classList.add('thick-border');
}
canvasContainer.appendChild(currentRectangle);
});
// Mouse move
document.addEventListener('mousemove', (e) => {
if (isDrawing) {
const rect = canvasContainer.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
const width = currentX - startX;
const height = currentY - startY;
if (width < 0) {
currentRectangle.style.left = currentX + 'px';
currentRectangle.style.width = Math.abs(width) + 'px';
} else {
currentRectangle.style.width = width + 'px';
}
if (height < 0) {
currentRectangle.style.top = currentY + 'px';
currentRectangle.style.height = Math.abs(height) + 'px';
} else {
currentRectangle.style.height = height + 'px';
}
}
if (isResizing && currentRectangle) {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
switch(resizingEdge) {
case 'top':
const newTop = rectStartY + deltaY;
const newHeight = rectStartHeight - deltaY;
if (newHeight > 0) {
currentRectangle.style.top = newTop + 'px';
currentRectangle.style.height = newHeight + 'px';
}
break;
case 'bottom':
const bottomHeight = rectStartHeight + deltaY;
if (bottomHeight > 0) {
currentRectangle.style.height = bottomHeight + 'px';
}
break;
case 'left':
const newLeft = rectStartX + deltaX;
const newWidth = rectStartWidth - deltaX;
if (newWidth > 0) {
currentRectangle.style.left = newLeft + 'px';
currentRectangle.style.width = newWidth + 'px';
}
break;
case 'right':
const rightWidth = rectStartWidth + deltaX;
if (rightWidth > 0) {
currentRectangle.style.width = rightWidth + 'px';
}
break;
case 'top-left':
const tlNewTop = rectStartY + deltaY;
const tlNewLeft = rectStartX + deltaX;
const tlNewHeight = rectStartHeight - deltaY;
const tlNewWidth = rectStartWidth - deltaX;
if (tlNewHeight > 0 && tlNewWidth > 0) {
currentRectangle.style.top = tlNewTop + 'px';
currentRectangle.style.left = tlNewLeft + 'px';
currentRectangle.style.height = tlNewHeight + 'px';
currentRectangle.style.width = tlNewWidth + 'px';
}
break;
case 'top-right':
const trNewTop = rectStartY + deltaY;
const trNewHeight = rectStartHeight - deltaY;
const trNewWidth = rectStartWidth + deltaX;
if (trNewHeight > 0 && trNewWidth > 0) {
currentRectangle.style.top = trNewTop + 'px';
currentRectangle.style.height = trNewHeight + 'px';
currentRectangle.style.width = trNewWidth + 'px';
}
break;
case 'bottom-left':
const blNewLeft = rectStartX + deltaX;
const blNewHeight = rectStartHeight + deltaY;
const blNewWidth = rectStartWidth - deltaX;
if (blNewHeight > 0 && blNewWidth > 0) {
currentRectangle.style.left = blNewLeft + 'px';
currentRectangle.style.height = blNewHeight + 'px';
currentRectangle.style.width = blNewWidth + 'px';
}
break;
case 'bottom-right':
const brNewWidth = rectStartWidth + deltaX;
const brNewHeight = rectStartHeight + deltaY;
if (brNewHeight > 0 && brNewWidth > 0) {
currentRectangle.style.width = brNewWidth + 'px';
currentRectangle.style.height = brNewHeight + 'px';
}
break;
}
baseX = parseFloat(currentRectangle.style.left);
baseY = parseFloat(currentRectangle.style.top);
baseWidth = parseFloat(currentRectangle.style.width);
baseHeight = parseFloat(currentRectangle.style.height);
updateAllDimensions();
}
});
// Mouse up
document.addEventListener('mouseup', (e) => {
if (isDrawing) {
const rect = canvasContainer.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
const x1 = Math.min(startX, endX);
const y1 = Math.min(startY, endY);
const x2 = Math.max(startX, endX);
const y2 = Math.max(startY, endY);
baseX = x1;
baseY = y1;
baseWidth = x2 - x1;
baseHeight = y2 - y1;
rectangleExists = true;
currentRectangle.classList.add('complete');
canvasContainer.classList.add('drawing-disabled');
canvasContainer.style.cursor = 'default';
resetBtn.disabled = false;
zoomInBtn.disabled = currentZoom >= maxZoom;
zoomOutBtn.disabled = currentZoom <= minZoom;
dimensionsInfo.style.display = 'block';
addResizeHandles();
updateAllDimensions();
isDrawing = false;
}
if (isResizing) {
isResizing = false;
resizingEdge = null;
canvasContainer.style.cursor = 'default';
}
});
// ========== FUNZIONI HELPER ==========
function resetRectangle() {
if (currentRectangle) {
currentRectangle.remove();
}
currentRectangle = null;
rectangleExists = false;
currentZoom = 1;
updateZoom();
canvasContainer.classList.remove('drawing-disabled');
canvasContainer.style.cursor = 'crosshair';
resetBtn.disabled = true;
zoomInBtn.disabled = true;
zoomOutBtn.disabled = true;
dimensionsInfo.style.display = 'none';
baseCoordinates.innerHTML = '<p>Clicca e trascina sull\'immagine per disegnare un rettangolo</p>';
}
function updateZoom() {
const zoomPercent = Math.round(currentZoom * 100);
zoomValue.textContent = zoomPercent + '%';
if (currentRectangle) {
currentRectangle.style.transform = `scale(${currentZoom})`;
}
zoomInBtn.disabled = currentZoom >= maxZoom;
zoomOutBtn.disabled = currentZoom <= minZoom;
}
function addResizeHandles() {
const handles = ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
handles.forEach(position => {
const handle = document.createElement('div');
handle.className = `resize-handle ${position}`;
handle.dataset.edge = position;
currentRectangle.appendChild(handle);
handle.addEventListener('mousedown', (e) => {
e.stopPropagation();
isResizing = true;
resizingEdge = position;
startX = e.clientX;
startY = e.clientY;
rectStartX = parseFloat(currentRectangle.style.left);
rectStartY = parseFloat(currentRectangle.style.top);
rectStartWidth = parseFloat(currentRectangle.style.width);
rectStartHeight = parseFloat(currentRectangle.style.height);
canvasContainer.style.cursor = getComputedStyle(handle).cursor;
});
});
}
function updateAllDimensions() {
if (!currentRectangle) return;
const baseX1 = baseX;
const baseY1 = baseY;
const baseX2 = baseX + baseWidth;
const baseY2 = baseY + baseHeight;
const centerX = baseX + (baseWidth / 2);
const centerY = baseY + (baseHeight / 2);
const zoomedWidth = baseWidth * currentZoom;
const zoomedHeight = baseHeight * currentZoom;
const zoomedX1 = centerX - (zoomedWidth / 2);
const zoomedY1 = centerY - (zoomedHeight / 2);
const zoomedX2 = centerX + (zoomedWidth / 2);
const zoomedY2 = centerY + (zoomedHeight / 2);
let effectiveX1, effectiveY1, effectiveX2, effectiveY2;
let effectiveWidth, effectiveHeight;
if (borderPosition === 'inside') {
effectiveX1 = zoomedX1;
effectiveY1 = zoomedY1;
effectiveX2 = zoomedX2;
effectiveY2 = zoomedY2;
effectiveWidth = zoomedWidth;
effectiveHeight = zoomedHeight;
} else {
effectiveX1 = zoomedX1 - currentBorderWidth;
effectiveY1 = zoomedY1 - currentBorderWidth;
effectiveX2 = zoomedX2 + currentBorderWidth;
effectiveY2 = zoomedY2 + currentBorderWidth;
effectiveWidth = zoomedWidth + (currentBorderWidth * 2);
effectiveHeight = zoomedHeight + (currentBorderWidth * 2);
}
baseCoordinates.innerHTML = `
<strong>📍 Coordinate Base:</strong><br>
x1 = ${Math.round(baseX1)}px, y1 = ${Math.round(baseY1)}px<br>
x2 = ${Math.round(baseX2)}px, y2 = ${Math.round(baseY2)}px<br>
<br>
<strong>📏 Dimensioni Base:</strong><br>
Larghezza: ${Math.round(baseWidth)}px<br>
Altezza: ${Math.round(baseHeight)}px
`;
currentDimensions.innerHTML = `
<strong>🔍 Con Zoom (${Math.round(currentZoom * 100)}%):</strong><br>
Larghezza: ${Math.round(zoomedWidth)}px<br>
Altezza: ${Math.round(zoomedHeight)}px<br>
<br>
<strong>🎨 Bordo ${borderPosition === 'inside' ? 'Interno' : 'Esterno'} (${currentBorderWidth}px):</strong><br>
Larghezza Totale: ${Math.round(effectiveWidth)}px<br>
Altezza Totale: ${Math.round(effectiveHeight)}px
`;
}
// Esporta la funzione per ottenere le coordinate
return {
getCoordinates: () => {
// Calcola le coordinate dei lati (x1, x2, y1, y2) con zoom
const baseX1 = baseX;
const baseY1 = baseY;
const baseX2 = baseX + baseWidth;
const baseY2 = baseY + baseHeight;
// Applica lo zoom mantenendo il centro
const centerX = baseX + (baseWidth / 2);
const centerY = baseY + (baseHeight / 2);
const zoomedWidth = baseWidth * currentZoom;
const zoomedHeight = baseHeight * currentZoom;
const zoomedX1 = centerX - (zoomedWidth / 2);
const zoomedY1 = centerY - (zoomedHeight / 2);
const zoomedX2 = centerX + (zoomedWidth / 2);
const zoomedY2 = centerY + (zoomedHeight / 2);
// Applica il bordo
let effectiveX1 = zoomedX1;
let effectiveY1 = zoomedY1;
let effectiveX2 = zoomedX2;
let effectiveY2 = zoomedY2;
if (borderPosition === 'outside') {
// Se il bordo è esterno, espandi le coordinate
effectiveX1 = Math.max(0, zoomedX1 - currentBorderWidth);
effectiveY1 = Math.max(0, zoomedY1 - currentBorderWidth);
effectiveX2 = zoomedX2 + currentBorderWidth;
effectiveY2 = zoomedY2 + currentBorderWidth;
} else {
// Se il bordo è interno, restringe le coordinate
effectiveX1 = zoomedX1 + currentBorderWidth;
effectiveY1 = zoomedY1 + currentBorderWidth;
effectiveX2 = Math.max(effectiveX1 + 1, zoomedX2 - currentBorderWidth);
effectiveY2 = Math.max(effectiveY1 + 1, zoomedY2 - currentBorderWidth);
}
return {
x1: Math.round(effectiveX1),
y1: Math.round(effectiveY1),
x2: Math.round(effectiveX2),
y2: Math.round(effectiveY2),
zoom: currentZoom,
borderWidth: currentBorderWidth,
borderPosition: borderPosition,
};
},
getState: () => ({
exists: rectangleExists,
baseX, baseY, baseWidth, baseHeight,
zoom: currentZoom,
borderWidth: currentBorderWidth,
borderPosition
})
};
}
window.CanvasSelector = { initializeCanvasSelector };
console.log("[BoxBox] canvas_selector.js loaded (placeholder)");
+176 -116
View File
@@ -1,13 +1,7 @@
/**
* Region Selector Extension - Integrazione nativa con ComfyUI Dialog
* Mantiene il bottone nel nodo e apre un dialog nativo
*/
// BoxBox Extension - Modernizada para ComfyUI v1.0 / v0.12.2+
import { app } from "../../scripts/app.js";
import { ComfyDialog } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
console.log("[RegionSelectorExt] Loading extension...");
console.log("[BoxBox] Loading extension (Modern API)...");
/**
* Canvas Selector - Funzionalità complete di selezione rettangoli
@@ -73,49 +67,71 @@ function initializeCanvasSelector(container, imageUrl, previousMetadata = null)
// Ridimensionamento immagini > 1024px lato backend
// ========================================================
if (imageUrl) {
// Extract filename from URL (works with both encoded and plain URLs)
let filename = imageUrl.split('filename=')[1]?.split('&')[0];
if (filename) {
filename = decodeURIComponent(filename);
// Parse URL parameters
try {
// Create a dummy base URL to handle relative URLs comfortably
const checkUrl = new URL(imageUrl, document.baseURI);
const params = new URLSearchParams(checkUrl.search);
fetch("/region_selector/scale", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename })
})
.then(res => res.json())
.then(data => {
if (data.path) {
// Try to use API helper for the scaled image path too
let scaledPath = data.path;
try {
scaledPath = api.apiURL(data.path) || data.path;
} catch (e) {
console.warn("[RegionSelectorExt] api.apiURL not available for scaled path:", e);
}
backgroundImage.src = scaledPath;
backgroundImage.dataset.scaleFactor = data.scale || 1;
backgroundImage.dataset.scaled = data.scaled || false;
console.log(`[RegionSelectorExt] Image loaded - Scale: ${data.scale || 1}`);
} else if (data.error) {
console.warn(`[RegionSelectorExt] Scale error: ${data.error}, using original`);
backgroundImage.src = imageUrl;
backgroundImage.dataset.scaleFactor = 1;
backgroundImage.dataset.scaled = false;
}
imageName.textContent = `Image: ${filename}`;
const filename = params.get("filename");
const type = params.get("type") || "input";
const subfolder = params.get("subfolder") || "";
if (filename) {
console.log(`[BoxBox] Requesting scale for: ${filename} (type: ${type}, subfolder: ${subfolder})`);
// Usare fetch diretto con configurazione robusta
fetch("/region_selector/scale", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
filename: String(filename),
type: String(type),
subfolder: String(subfolder)
})
})
.catch(e => {
console.error("[RegionSelectorExt] Error in scale fetch:", e);
// Fallback to direct image URL
backgroundImage.src = imageUrl;
backgroundImage.dataset.scaleFactor = 1;
backgroundImage.dataset.scaled = false;
imageName.textContent = `Image: ${filename}`;
});
} else {
// No filename extracted, use imageUrl directly
console.log("[RegionSelectorExt] No filename extracted, using imageUrl directly");
.then(async res => {
if (!res.ok) {
const errText = await res.text();
throw new Error(`Server returned ${res.status}: ${errText}`);
}
return res.json();
})
.then(data => {
if (data.path) {
let scaledPath = data.path;
// Assicurati che il path sia convertito correttamente per l'API di ComfyUI
if (window.comfyAPI && window.comfyAPI.api) {
try {
scaledPath = window.comfyAPI.api.api.apiURL(data.path);
} catch (e) { }
}
backgroundImage.src = scaledPath;
backgroundImage.dataset.scaleFactor = data.scale || 1;
backgroundImage.dataset.scaled = data.scaled || false;
console.log(`[BoxBox] Image loaded - Scale: ${data.scale || 1}`);
} else if (data.error) {
console.warn(`[BoxBox] Scale error: ${data.error}, using original`);
backgroundImage.src = imageUrl;
}
imageName.textContent = `Image: ${filename}`;
})
.catch(e => {
console.error("[BoxBox] Error in scale fetch:", e);
backgroundImage.src = imageUrl;
imageName.textContent = `Image: ${filename}`;
});
} else {
// Fallback for when filename param is missing
console.log("[RegionSelectorExt] No filename param, using imageUrl directly");
backgroundImage.src = imageUrl;
}
} catch (e) {
console.error("[RegionSelectorExt] URL parsing error:", e);
backgroundImage.src = imageUrl;
}
}
@@ -865,10 +881,12 @@ function initializeCanvasSelector(container, imageUrl, previousMetadata = null)
return {
getCoordinates: () => {
// Read the scale factor from backend (image was scaled for display)
const scaleFactor = parseFloat(backgroundImage.dataset.scaleFactor || "1");
// Calcola il fattore di scala combinato (Backend + Browser CSS)
const serverScale = parseFloat(backgroundImage.dataset.scaleFactor || "1");
const browserScale = backgroundImage.offsetWidth / (backgroundImage.naturalWidth || backgroundImage.offsetWidth);
const totalScale = serverScale * browserScale;
console.log(`[RegionSelector] Getting coordinates with scaleFactor: ${scaleFactor}`);
console.log(`[BoxBox] Getting coordinates - ServerScale: ${serverScale}, BrowserScale: ${browserScale.toFixed(3)}, Total: ${totalScale.toFixed(3)}`);
const baseX1 = baseX;
const baseY1 = baseY;
@@ -896,8 +914,7 @@ function initializeCanvasSelector(container, imageUrl, previousMetadata = null)
// REMOVED: BoxCrop backend already handles this conversion
// Return display coordinates as-is, BoxCrop will divide by displayScaleFactor
console.log(`[RegionSelector] Display coords: (${Math.round(effectiveX1)}, ${Math.round(effectiveY1)}) to (${Math.round(effectiveX2)}, ${Math.round(effectiveY2)})`);
console.log(`[RegionSelector] Scale factor ${scaleFactor} will be handled by BoxCrop backend`);
console.log(`[BoxBox] Final coordinates: (${Math.round(effectiveX1)}, ${Math.round(effectiveY1)}) to (${Math.round(effectiveX2)}, ${Math.round(effectiveY2)})`);
return {
x1: Math.round(effectiveX1),
@@ -906,7 +923,7 @@ function initializeCanvasSelector(container, imageUrl, previousMetadata = null)
y2: Math.round(effectiveY2),
borderWidth: currentBorderWidth,
borderPosition: borderPosition,
displayScaleFactor: scaleFactor,
displayScaleFactor: totalScale,
};
},
getState: () => ({
@@ -920,27 +937,35 @@ function initializeCanvasSelector(container, imageUrl, previousMetadata = null)
window.CanvasSelector = { initializeCanvasSelector };
console.log("[BoxBox] Preparing to register extension...");
app.registerExtension({
name: "BoxBox.BoxSelectorExtension",
async setup(app) {
console.log("[RegionSelectorExt] Setup called");
console.log("[BoxBox] Setup extension called");
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Log every node to see what's happening
if (nodeData.name && nodeData.name.includes("Box")) {
console.log(`[BoxBox] Checking node: ${nodeData.name}`);
}
if (nodeData.name !== "BoxSelector") return;
console.log("[RegionSelectorExt] Registering BoxSelector...");
console.log("[BoxBox] Found BoxSelector node! Adding button...");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated?.apply(this, arguments);
const node = this;
console.log("[RegionSelectorExt] Node created, adding button...");
console.log("[BoxBox] Node instance created, attaching button widget...");
// Aggiungi bottone nativo ComfyUI
// Add native ComfyUI button
this.addWidget("button", "📦 Select Box", null, () => {
console.log("[BoxBox] Button clicked!");
openRegionDialog(node, app);
});
@@ -950,40 +975,53 @@ app.registerExtension({
});
/**
* Recursively searches for an image filename by traversing the node chain backwards
* @param {Object} node - Current node to search
* @param {Object} app - ComfyUI app instance
* @param {number} depth - Current recursion depth
* @param {number} maxDepth - Maximum recursion depth to prevent infinite loops
* @returns {string|null} - Image filename if found, null otherwise
* Recursively searches for image metadata by traversing the node chain backwards
* @returns {Object|null} - Image metadata {filename, type, subfolder} if found, null otherwise
*/
function findImageInChain(node, app, depth = 0, maxDepth = 20) {
if (depth > maxDepth) {
console.warn("[RegionSelectorExt] Max depth reached in node chain search");
return null;
}
if (depth > maxDepth) return null;
console.log(`[RegionSelectorExt] Searching at depth ${depth}, node type:`, node.type);
console.log(`[RegionSelectorExt] Searching at depth ${depth}, node type: ${node.type} (${node.comfyClass})`);
// Check if this node has an "image" widget (like LoadImage)
if (node.widgets) {
const imageWidget = node.widgets.find(w => w.name === "image");
if (imageWidget && imageWidget.value) {
console.log(`[RegionSelectorExt] ✅ Found image at depth ${depth}:`, imageWidget.value);
return imageWidget.value;
// 1. Check if the node has preview images (standard ComfyUI way)
if (node.imgs && node.imgs.length > 0) {
const img = node.imgs[0];
// Ensure it's a real image and not just a placeholder from some nodes
if (img.filename && !img.filename.startsWith("$")) {
console.log(`[RegionSelectorExt] ✅ Found image in node.imgs at depth ${depth}:`, img);
return {
filename: img.filename,
type: img.type || "temp",
subfolder: img.subfolder || ""
};
}
}
// If not found, traverse backwards through IMAGE inputs
// 2. Check for common widgets that hold image names
if (node.widgets) {
// Look for any widget that might contain a filename
const imageWidget = node.widgets.find(w => w.name === "image" || w.name === "image_name");
if (imageWidget && imageWidget.value && typeof imageWidget.value === "string") {
// Check if it looks like a real filename (has extension) or a special ID
if (imageWidget.value.includes(".") || imageWidget.value.startsWith("$")) {
console.log(`[RegionSelectorExt] ✅ Found image widget at depth ${depth}:`, imageWidget.name, "=", imageWidget.value);
return {
filename: imageWidget.value,
type: "input", // Fallback, will be corrected for $ IDs later
subfolder: ""
};
}
}
}
// 3. Traverse backwards through IMAGE inputs
if (node.inputs && node.inputs.length > 0) {
for (const input of node.inputs) {
// Look for IMAGE type inputs
if (input.type === "IMAGE" && input.link !== undefined && input.link !== null) {
const link = app.graph.links[input.link];
if (link) {
const sourceNode = app.graph._nodes_by_id[link.origin_id];
if (sourceNode) {
console.log(`[RegionSelectorExt] → Traversing to node: ${sourceNode.type}`);
const result = findImageInChain(sourceNode, app, depth + 1, maxDepth);
if (result) return result;
}
@@ -992,7 +1030,6 @@ function findImageInChain(node, app, depth = 0, maxDepth = 20) {
}
}
console.log(`[RegionSelectorExt] No image found at depth ${depth}`);
return null;
}
@@ -1001,57 +1038,80 @@ function findImageInChain(node, app, depth = 0, maxDepth = 20) {
*/
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];
let imageInfo = null;
// Search in the connected node chain
if (node.inputs && node.inputs[0]?.link != null) {
const link = app.graph.links[node.inputs[0].link];
if (link) {
const sourceNode = app.graph._nodes_by_id[link.origin_id];
console.log("[RegionSelectorExt] Source node found:", sourceNode);
// Use recursive search to find image through the node chain
imageName = findImageInChain(sourceNode, app);
if (imageName) {
console.log("[RegionSelectorExt] ✅ Image found through chain traversal:", imageName);
} else {
console.warn("[RegionSelectorExt] ⚠️ No image found in node chain");
}
imageInfo = findImageInChain(sourceNode, app);
}
}
// 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);
}
// Fallback: Check the node itself
if (!imageInfo) {
imageInfo = findImageInChain(node, app);
}
if (!imageName) {
alert("⚠️ Nessuna immagine collegata al nodo!\n\nCollega un'immagine dal nodo LoadImage.");
if (!imageInfo || !imageInfo.filename) {
alert("⚠️ No image found!\n\nPlease connect a node that shows an image (like LoadImage or PreviewBridge) and ensure it has executed at least once.");
return;
}
// Try to use ComfyUI's api helper for correct path
// SPECIAL HANDLING: Impact Pack PreviewBridge IDs ($...)
if (imageInfo.filename.startsWith("$")) {
console.log("[RegionSelectorExt] Detected PreviewBridge ID, attempting to resolve:", imageInfo.filename);
try {
const response = await fetch(`/impact/get/pb_id_image?id=${encodeURIComponent(imageInfo.filename)}`);
if (response.ok) {
const pbInfo = await response.json();
console.log("[RegionSelectorExt] Resolved PreviewBridge ID:", pbInfo);
imageInfo = {
filename: pbInfo.filename,
type: pbInfo.type || "temp",
subfolder: pbInfo.subfolder || ""
};
} else {
console.warn("[RegionSelectorExt] Failed to resolve PreviewBridge ID via API");
}
} catch (e) {
console.error("[RegionSelectorExt] Error resolving PreviewBridge ID via API:", e);
}
}
// Construct URL - use direct path /view to avoid /api/view ambiguity
const params = new URLSearchParams();
params.append("filename", imageInfo.filename);
params.append("type", imageInfo.type || "input");
if (imageInfo.subfolder) params.append("subfolder", imageInfo.subfolder);
// Use ROOT /view as standard ComfyUI does
const imageUrl = `/view?${params.toString()}`;
console.log("[RegionSelectorExt] Final Image URL:", imageUrl);
// Crea il dialog usando la API moderna o il fallback sicuro
let dialog;
try {
imageUrl = api.apiURL(`/view?filename=${encodeURIComponent(imageName)}&type=input`);
if (window.comfyAPI && window.comfyAPI.ui && window.comfyAPI.ui.ComfyDialog) {
dialog = new window.comfyAPI.ui.ComfyDialog();
} else {
// Fallback se siamo su una versione vecchia o il bus bridge è attivo
const { ComfyDialog } = await import("../../scripts/ui.js");
dialog = new ComfyDialog();
}
} catch (e) {
console.warn("[RegionSelectorExt] api.apiURL not available, using fallback:", e);
imageUrl = `/view?filename=${encodeURIComponent(imageName)}&type=input`;
console.error("[BoxBox] Failed to create dialog via modern API, trying fallback:", e);
// Fallback estremo: molti nodi usano app.ui.dialog o simili
if (app.ui && app.ui.dialog) {
dialog = app.ui.dialog;
} else {
alert("Error: ComfyUI Dialog system not available. Please check console (F12).");
return;
}
}
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";
@@ -1210,7 +1270,7 @@ async function openRegionDialog(node, app) {
<!-- Image info -->
<div class="control-group" style="margin-bottom: 20px;">
<small id="image-name" style="display: block; font-size: 12px; color: #64748b;">Image: ${imageName}</small>
<small id="image-name" style="display: block; font-size: 12px; color: #64748b;">Image: ${imageInfo.filename}</small>
</div>
<!-- Aspect Ratio Selector -->