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.
493 lines
24 KiB
JavaScript
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);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|