Files
PozzettiAndrea 4888e4886e Vendor viewer JS, drop comfy-3d-viewers
Vendor the viewer JavaScript into javascript/ and drop the
comfy-3d-viewers dependency. The viewers were previously copied into web/ at
every startup by prestartup_script.py, which made the served frontend depend on
a second package resolving at runtime.

Bundles are vendored as .mjs so ComfyUI's **/*.js auto-import glob skips them:
they are iframe-only and must never enter the shared main browser realm. Only
the widget entry points stay .js.

Also replace comfy_env.copy_files with a local helper. Its two extra features
(frame-derived source dir, silent no-op on a missing source) were unused here,
and the asset seeding now resolves the input directory through
folder_paths.get_input_directory() so it follows ComfyUI Desktop and
--input-directory instead of writing into the code tree.

comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no
"gpu" key -- the accelerator is named by backend, and the old spelling parsed to
an empty list, so CUDA runners silently ran the full unfiltered set), an
explicit [test.platforms] platforms allowlist (per-platform booleans are no
longer supported), and the javascript level.

Namespace every registerExtension name under sam3. and inline the spinner
styles: the stylesheet used to be injected into document.head, which is shared
state every other pack inherits and nobody can clean up. It is now inline on the
element with the rotation driven by the Web Animations API.
2026-08-22 19:44:03 +02:00

493 lines
24 KiB
JavaScript

/**
* SAM3 Simple BBox Collector
* Uses plain HTML5 Canvas for drawing bounding boxes
* Version: 2025-01-20-v1-BBOX
*/
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
console.log("[SAM3] ===== BBOX COLLECTOR VERSION 1 =====");
// Helper function to properly hide widgets (enhanced for complete hiding)
function hideWidgetForGood(node, widget, suffix = '') {
if (!widget) return;
// Save original properties
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
// Multiple hiding approaches to ensure widget is fully hidden
widget.computeSize = () => [0, -4]; // -4 compensates for litegraph's automatic widget gap
widget.type = "converted-widget" + suffix;
widget.hidden = true; // Mark as hidden
// IMPORTANT: Keep serialization enabled so values are sent to backend
// (We just hide it visually, but it still needs to send data)
// Make the widget completely invisible in the DOM if it has element
if (widget.element) {
widget.element.style.display = "none";
widget.element.style.visibility = "hidden";
}
// Handle linked widgets recursively
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidgetForGood(node, w, ':' + widget.name);
}
}
}
app.registerExtension({
name: "sam3.simplebboxcollector",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SAM3BBoxCollector") {
console.log("[SAM3] Registering SAM3BBoxCollector node");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
console.log("[SAM3] onNodeCreated called for SAM3BBoxCollector");
// Call original onNodeCreated FIRST to create all widgets
const result = onNodeCreated?.apply(this, arguments);
console.log("[SAM3] Widgets after creation:", this.widgets?.map(w => w.name));
console.log("[SAM3] Creating canvas container");
// Create canvas container - dynamically sized based on node height
const container = document.createElement("div");
container.style.cssText = "position: relative; width: 100%; background: #222; overflow: hidden; box-sizing: border-box; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center;";
// Create info/button bar
const infoBar = document.createElement("div");
infoBar.style.cssText = "position: absolute; top: 5px; left: 5px; right: 5px; z-index: 10; display: flex; justify-content: space-between; align-items: center;";
container.appendChild(infoBar);
// Create bbox counter
const bboxCounter = document.createElement("div");
bboxCounter.style.cssText = "padding: 5px 10px; background: rgba(0,0,0,0.7); color: #fff; border-radius: 3px; font-size: 12px; font-family: monospace;";
bboxCounter.textContent = "Bboxes: 0 pos, 0 neg";
infoBar.appendChild(bboxCounter);
// Create clear button
const clearButton = document.createElement("button");
clearButton.textContent = "Clear All";
clearButton.style.cssText = "padding: 5px 10px; background: #d44; color: #fff; border: 1px solid #a22; border-radius: 3px; cursor: pointer; font-size: 12px; font-weight: bold;";
clearButton.onmouseover = () => clearButton.style.background = "#e55";
clearButton.onmouseout = () => clearButton.style.background = "#d44";
infoBar.appendChild(clearButton);
// Create canvas for image and bboxes
const canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 512;
// Use max-width and max-height instead of width/height 100% to prevent overflow
canvas.style.cssText = "display: block; max-width: 100%; max-height: 100%; object-fit: contain; cursor: crosshair; margin: 0 auto;";
container.appendChild(canvas);
const ctx = canvas.getContext("2d");
console.log("[SAM3] Canvas created:", canvas);
// Store state
this.canvasWidget = {
canvas: canvas,
ctx: ctx,
container: container,
image: null,
positiveBBoxes: [],
negativeBBoxes: [],
currentBBox: null, // BBox being drawn {bbox, isNegative}
hoveredBBox: null,
bboxCounter: bboxCounter
};
// Add as DOM widget
console.log("[SAM3] Adding DOM widget via addDOMWidget");
const widget = this.addDOMWidget("canvas", "customCanvas", container);
console.log("[SAM3] addDOMWidget returned:", widget);
// Store widget reference for updates
this.canvasWidget.domWidget = widget;
// Make widget dynamically sized - override computeSize
widget.computeSize = (width) => {
// Widget height = node height - title bar/padding (approx 80px)
const nodeHeight = this.size ? this.size[1] : 480;
const widgetHeight = Math.max(200, nodeHeight - 80);
return [width, widgetHeight];
};
// Clear button handler
clearButton.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
console.log("[SAM3] Clearing all bboxes");
this.canvasWidget.positiveBBoxes = [];
this.canvasWidget.negativeBBoxes = [];
this.updateBBoxes();
this.redrawCanvas();
});
// Hide the string storage widgets
console.log("[SAM3] Attempting to hide widgets...");
console.log("[SAM3] Widgets before hiding:", this.widgets.map(w => w.name));
const bboxesWidget = this.widgets.find(w => w.name === "bboxes");
const negBboxesWidget = this.widgets.find(w => w.name === "neg_bboxes");
console.log("[SAM3] Found widgets to hide:", { bboxesWidget, negBboxesWidget });
// Initialize default values BEFORE hiding
if (bboxesWidget) {
bboxesWidget.value = bboxesWidget.value || "[]";
}
if (negBboxesWidget) {
negBboxesWidget.value = negBboxesWidget.value || "[]";
}
// Store references before hiding
this._hiddenWidgets = {
bboxes: bboxesWidget,
neg_bboxes: negBboxesWidget
};
// Apply hiding
if (bboxesWidget) {
hideWidgetForGood(this, bboxesWidget);
console.log("[SAM3] bboxes - type:", bboxesWidget.type, "hidden:", bboxesWidget.hidden, "value:", bboxesWidget.value);
}
if (negBboxesWidget) {
hideWidgetForGood(this, negBboxesWidget);
console.log("[SAM3] neg_bboxes - type:", negBboxesWidget.type, "hidden:", negBboxesWidget.hidden, "value:", negBboxesWidget.value);
}
// CRITICAL FIX: Override onDrawForeground to skip rendering hidden widgets
const originalDrawForeground = this.onDrawForeground;
this.onDrawForeground = function(ctx) {
// Temporarily hide converted widgets from rendering
const hiddenWidgets = this.widgets.filter(w => w.type?.includes("converted-widget"));
const originalTypes = hiddenWidgets.map(w => w.type);
// Temporarily set to null to prevent rendering
hiddenWidgets.forEach(w => w.type = null);
// Call original draw
if (originalDrawForeground) {
originalDrawForeground.apply(this, arguments);
}
// Restore types
hiddenWidgets.forEach((w, i) => w.type = originalTypes[i]);
// Update container height based on current node size
const containerHeight = Math.max(200, this.size[1] - 80);
if (container.style.height !== containerHeight + "px") {
container.style.height = containerHeight + "px";
}
};
console.log("[SAM3] Widgets after hiding:", this.widgets.map(w => `${w.name}(${w.type})`));
console.log("[SAM3] All widgets processing complete");
// Mouse event handlers for bbox drawing
canvas.addEventListener("mousedown", (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
console.log(`[SAM3] MouseDown at canvas coords: (${x.toFixed(1)}, ${y.toFixed(1)})`);
// Check if right-click on existing bbox to delete
const clickedBBox = this.findBBoxAt(x, y);
if (clickedBBox && e.button === 2) {
// Right-click on existing bbox = delete
if (clickedBBox.type === 'positive') {
this.canvasWidget.positiveBBoxes = this.canvasWidget.positiveBBoxes.filter(b => b !== clickedBBox.bbox);
} else {
this.canvasWidget.negativeBBoxes = this.canvasWidget.negativeBBoxes.filter(b => b !== clickedBBox.bbox);
}
this.updateBBoxes();
this.redrawCanvas();
return;
}
// Start drawing new bbox
// Determine if it's negative (shift+click or right-click and drag)
const isNegative = e.shiftKey || e.button === 2;
this.canvasWidget.currentBBox = {
bbox: {
x1: x,
y1: y,
x2: x,
y2: y
},
isNegative: isNegative
};
});
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
// Update current bbox if drawing
if (this.canvasWidget.currentBBox) {
this.canvasWidget.currentBBox.bbox.x2 = x;
this.canvasWidget.currentBBox.bbox.y2 = y;
this.redrawCanvas();
} else {
// Check for hover
const hovered = this.findBBoxAt(x, y);
if (hovered !== this.canvasWidget.hoveredBBox) {
this.canvasWidget.hoveredBBox = hovered;
this.redrawCanvas();
}
}
});
canvas.addEventListener("mouseup", (e) => {
if (this.canvasWidget.currentBBox) {
const rect = canvas.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
this.canvasWidget.currentBBox.bbox.x2 = x;
this.canvasWidget.currentBBox.bbox.y2 = y;
// Only add bbox if it has some size
const width = Math.abs(this.canvasWidget.currentBBox.bbox.x2 - this.canvasWidget.currentBBox.bbox.x1);
const height = Math.abs(this.canvasWidget.currentBBox.bbox.y2 - this.canvasWidget.currentBBox.bbox.y1);
if (width > 5 && height > 5) {
// Normalize so x1,y1 is top-left and x2,y2 is bottom-right
const bbox = {
x1: Math.min(this.canvasWidget.currentBBox.bbox.x1, this.canvasWidget.currentBBox.bbox.x2),
y1: Math.min(this.canvasWidget.currentBBox.bbox.y1, this.canvasWidget.currentBBox.bbox.y2),
x2: Math.max(this.canvasWidget.currentBBox.bbox.x1, this.canvasWidget.currentBBox.bbox.x2),
y2: Math.max(this.canvasWidget.currentBBox.bbox.y1, this.canvasWidget.currentBBox.bbox.y2)
};
// Add to appropriate list
if (this.canvasWidget.currentBBox.isNegative) {
this.canvasWidget.negativeBBoxes.push(bbox);
console.log(`[SAM3] Added negative bbox: (${bbox.x1.toFixed(1)}, ${bbox.y1.toFixed(1)}) to (${bbox.x2.toFixed(1)}, ${bbox.y2.toFixed(1)})`);
} else {
this.canvasWidget.positiveBBoxes.push(bbox);
console.log(`[SAM3] Added positive bbox: (${bbox.x1.toFixed(1)}, ${bbox.y1.toFixed(1)}) to (${bbox.x2.toFixed(1)}, ${bbox.y2.toFixed(1)})`);
}
this.updateBBoxes();
}
this.canvasWidget.currentBBox = null;
this.redrawCanvas();
}
});
canvas.addEventListener("contextmenu", (e) => {
e.preventDefault();
// Trigger mousedown with right button flag
canvas.dispatchEvent(new MouseEvent('mousedown', {
button: 2,
clientX: e.clientX,
clientY: e.clientY
}));
});
// Handle image input changes
this.onExecuted = (message) => {
console.log("[SAM3] onExecuted called with message:", message);
if (message.bg_image && message.bg_image[0]) {
const img = new Image();
img.onload = () => {
console.log(`[SAM3] Image loaded: ${img.width}x${img.height}`);
this.canvasWidget.image = img;
canvas.width = img.width;
canvas.height = img.height;
console.log(`[SAM3] Canvas resized to: ${canvas.width}x${canvas.height}`);
this.redrawCanvas();
};
img.src = "data:image/jpeg;base64," + message.bg_image[0];
}
};
// Update container height dynamically when node size changes
const originalOnResize = this.onResize;
this.onResize = function(size) {
if (originalOnResize) {
originalOnResize.apply(this, arguments);
}
// Update container to match widget size
const containerHeight = Math.max(200, size[1] - 80);
container.style.height = containerHeight + "px";
console.log(`[SAM3] Node resized to: ${size[0]}x${size[1]}, container height: ${containerHeight}px`);
};
// Also update on draw to handle any size changes
const originalOnDrawForeground = this.onDrawForeground;
this.onDrawForeground = function(ctx) {
if (originalOnDrawForeground) {
originalOnDrawForeground.apply(this, arguments);
}
// Update container height based on current node size
const containerHeight = Math.max(200, this.size[1] - 80);
if (container.style.height !== containerHeight + "px") {
container.style.height = containerHeight + "px";
}
};
// Draw initial placeholder
console.log("[SAM3] Drawing initial placeholder");
this.redrawCanvas();
// Set initial node size
const nodeWidth = Math.max(400, this.size[0] || 400);
const nodeHeight = 480; // Initial height: canvas (400) + space (80)
this.setSize([nodeWidth, nodeHeight]);
// Set initial container height
container.style.height = "400px";
console.log("[SAM3] Node size set to:", [nodeWidth, nodeHeight]);
console.log("[SAM3] onNodeCreated complete");
return result;
};
// Helper: Find bbox at coordinates
nodeType.prototype.findBBoxAt = function(x, y) {
// Check positive bboxes
for (const bbox of this.canvasWidget.positiveBBoxes) {
if (x >= bbox.x1 && x <= bbox.x2 && y >= bbox.y1 && y <= bbox.y2) {
return {type: 'positive', bbox};
}
}
// Check negative bboxes
for (const bbox of this.canvasWidget.negativeBBoxes) {
if (x >= bbox.x1 && x <= bbox.x2 && y >= bbox.y1 && y <= bbox.y2) {
return {type: 'negative', bbox};
}
}
return null;
};
// Helper: Update widget values
nodeType.prototype.updateBBoxes = function() {
// Use stored hidden widget references
const bboxesWidget = this._hiddenWidgets?.bboxes || this.widgets.find(w => w.name === "bboxes");
const negBboxesWidget = this._hiddenWidgets?.neg_bboxes || this.widgets.find(w => w.name === "neg_bboxes");
if (bboxesWidget) {
bboxesWidget.value = JSON.stringify(this.canvasWidget.positiveBBoxes);
}
if (negBboxesWidget) {
negBboxesWidget.value = JSON.stringify(this.canvasWidget.negativeBBoxes);
}
// Update bbox counter display
const posCount = this.canvasWidget.positiveBBoxes.length;
const negCount = this.canvasWidget.negativeBBoxes.length;
this.canvasWidget.bboxCounter.textContent = `Bboxes: ${posCount} pos, ${negCount} neg`;
};
// Helper: Redraw canvas
nodeType.prototype.redrawCanvas = function() {
const {canvas, ctx, image, positiveBBoxes, negativeBBoxes, currentBBox, hoveredBBox} = this.canvasWidget;
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw image if available
if (image) {
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
} else {
// Placeholder
ctx.fillStyle = "#333";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#666";
ctx.font = "16px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Click and drag to draw bounding boxes", canvas.width / 2, canvas.height / 2);
ctx.fillText("Left-click: Positive (cyan)", canvas.width / 2, canvas.height / 2 + 25);
ctx.fillText("Shift/Right-click: Negative (red)", canvas.width / 2, canvas.height / 2 + 50);
}
// Draw canvas dimensions overlay (helpful for debugging)
if (image) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(5, canvas.height - 25, 150, 20);
ctx.fillStyle = "#0f0";
ctx.font = "12px monospace";
ctx.textAlign = "left";
ctx.fillText(`Image: ${canvas.width}x${canvas.height}`, 10, canvas.height - 10);
}
// Draw positive bboxes (cyan)
for (const bbox of positiveBBoxes) {
const isHovered = hoveredBBox?.bbox === bbox && hoveredBBox?.type === 'positive';
if (isHovered) {
ctx.strokeStyle = "#ff0";
ctx.lineWidth = 3;
} else {
ctx.strokeStyle = "#0ff";
ctx.lineWidth = 2;
}
const width = bbox.x2 - bbox.x1;
const height = bbox.y2 - bbox.y1;
ctx.strokeRect(bbox.x1, bbox.y1, width, height);
// Draw semi-transparent fill for hovered
if (isHovered) {
ctx.fillStyle = "rgba(255, 255, 0, 0.1)";
ctx.fillRect(bbox.x1, bbox.y1, width, height);
}
}
// Draw negative bboxes (red)
for (const bbox of negativeBBoxes) {
const isHovered = hoveredBBox?.bbox === bbox && hoveredBBox?.type === 'negative';
if (isHovered) {
ctx.strokeStyle = "#ff0";
ctx.lineWidth = 3;
} else {
ctx.strokeStyle = "#f00";
ctx.lineWidth = 2;
}
const width = bbox.x2 - bbox.x1;
const height = bbox.y2 - bbox.y1;
ctx.strokeRect(bbox.x1, bbox.y1, width, height);
// Draw semi-transparent fill for hovered
if (isHovered) {
ctx.fillStyle = "rgba(255, 255, 0, 0.1)";
ctx.fillRect(bbox.x1, bbox.y1, width, height);
}
}
// Draw current bbox being drawn (yellow for positive, orange for negative, with dotted line)
if (currentBBox) {
ctx.strokeStyle = currentBBox.isNegative ? "#f80" : "#ff0";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const width = currentBBox.bbox.x2 - currentBBox.bbox.x1;
const height = currentBBox.bbox.y2 - currentBBox.bbox.y1;
ctx.strokeRect(currentBBox.bbox.x1, currentBBox.bbox.y1, width, height);
ctx.setLineDash([]);
// Draw semi-transparent fill
if (currentBBox.isNegative) {
ctx.fillStyle = "rgba(255, 128, 0, 0.1)";
} else {
ctx.fillStyle = "rgba(255, 255, 0, 0.1)";
}
ctx.fillRect(currentBBox.bbox.x1, currentBBox.bbox.y1, width, height);
}
};
}
}
});