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.
645 lines
28 KiB
JavaScript
645 lines
28 KiB
JavaScript
/**
|
||
* SAM3 Multi-Region Collector Widget
|
||
* Combines point and box collection with multiple prompt regions
|
||
* Version: 2025-01-20-v1-MULTIREGION
|
||
*/
|
||
|
||
import { app } from "../../scripts/app.js";
|
||
|
||
console.log("[SAM3] ===== MULTI-REGION COLLECTOR VERSION 1 =====");
|
||
|
||
// Color palette for different prompt regions (max 8)
|
||
const PROMPT_COLORS = [
|
||
{ name: "cyan", primary: "#00FFFF", dim: "#006666" },
|
||
{ name: "yellow", primary: "#FFFF00", dim: "#666600" },
|
||
{ name: "magenta", primary: "#FF00FF", dim: "#660066" },
|
||
{ name: "lime", primary: "#00FF00", dim: "#006600" },
|
||
{ name: "orange", primary: "#FF8000", dim: "#663300" },
|
||
{ name: "pink", primary: "#FF69B4", dim: "#662944" },
|
||
{ name: "blue", primary: "#4169E1", dim: "#1a2a5c" },
|
||
{ name: "teal", primary: "#20B2AA", dim: "#0d4744" },
|
||
];
|
||
|
||
const MAX_PROMPTS = PROMPT_COLORS.length;
|
||
|
||
// Helper function to hide widgets
|
||
function hideWidgetForGood(node, widget, suffix = '') {
|
||
if (!widget) return;
|
||
widget.origType = widget.type;
|
||
widget.origComputeSize = widget.computeSize;
|
||
widget.computeSize = () => [0, -4];
|
||
widget.type = "converted-widget" + suffix;
|
||
widget.hidden = true;
|
||
if (widget.element) {
|
||
widget.element.style.display = "none";
|
||
widget.element.style.visibility = "hidden";
|
||
}
|
||
}
|
||
|
||
app.registerExtension({
|
||
name: "sam3.multiregioncollector",
|
||
|
||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||
if (nodeData.name !== "SAM3MultiRegionCollector") return;
|
||
|
||
console.log("[SAM3] Registering SAM3MultiRegionCollector node");
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
const result = onNodeCreated?.apply(this, arguments);
|
||
|
||
// Create main container
|
||
const container = document.createElement("div");
|
||
container.style.cssText = "position: relative; width: 100%; background: #222; overflow: hidden; box-sizing: border-box; display: flex; flex-direction: column;";
|
||
|
||
// Create info bar (top)
|
||
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);
|
||
|
||
// Counter display
|
||
const counter = document.createElement("div");
|
||
counter.style.cssText = "padding: 5px 10px; background: rgba(0,0,0,0.7); color: #fff; border-radius: 3px; font-size: 12px; font-family: monospace;";
|
||
counter.textContent = "Prompt 1: 0 pts, 0 boxes";
|
||
infoBar.appendChild(counter);
|
||
|
||
// Button container
|
||
const buttonContainer = document.createElement("div");
|
||
buttonContainer.style.cssText = "display: flex; gap: 5px;";
|
||
infoBar.appendChild(buttonContainer);
|
||
|
||
// Clear Prompt button
|
||
const clearPromptBtn = document.createElement("button");
|
||
clearPromptBtn.textContent = "Clear Prompt";
|
||
clearPromptBtn.style.cssText = "padding: 5px 10px; background: #a50; color: #fff; border: 1px solid #830; border-radius: 3px; cursor: pointer; font-size: 11px;";
|
||
clearPromptBtn.onmouseover = () => clearPromptBtn.style.background = "#c60";
|
||
clearPromptBtn.onmouseout = () => clearPromptBtn.style.background = "#a50";
|
||
buttonContainer.appendChild(clearPromptBtn);
|
||
|
||
// Clear All button
|
||
const clearAllBtn = document.createElement("button");
|
||
clearAllBtn.textContent = "Clear All";
|
||
clearAllBtn.style.cssText = "padding: 5px 10px; background: #d44; color: #fff; border: 1px solid #a22; border-radius: 3px; cursor: pointer; font-size: 11px;";
|
||
clearAllBtn.onmouseover = () => clearAllBtn.style.background = "#e55";
|
||
clearAllBtn.onmouseout = () => clearAllBtn.style.background = "#d44";
|
||
buttonContainer.appendChild(clearAllBtn);
|
||
|
||
// Canvas wrapper (for centering)
|
||
const canvasWrapper = document.createElement("div");
|
||
canvasWrapper.style.cssText = "flex: 1; display: flex; align-items: center; justify-content: center; min-height: 200px;";
|
||
container.appendChild(canvasWrapper);
|
||
|
||
// Canvas
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = 512;
|
||
canvas.height = 512;
|
||
canvas.style.cssText = "display: block; max-width: 100%; max-height: 100%; object-fit: contain; cursor: crosshair;";
|
||
canvasWrapper.appendChild(canvas);
|
||
|
||
const ctx = canvas.getContext("2d");
|
||
|
||
// Tab bar (bottom)
|
||
const tabBar = document.createElement("div");
|
||
tabBar.style.cssText = "display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; background: #1a1a1a; border-top: 1px solid #333;";
|
||
container.appendChild(tabBar);
|
||
|
||
// Store state
|
||
this.canvasWidget = {
|
||
canvas: canvas,
|
||
ctx: ctx,
|
||
container: container,
|
||
canvasWrapper: canvasWrapper,
|
||
image: null,
|
||
// Multi-prompt state
|
||
prompts: [{
|
||
positive_points: [],
|
||
negative_points: [],
|
||
positive_boxes: [],
|
||
negative_boxes: []
|
||
}],
|
||
activePromptIndex: 0,
|
||
// Drawing state
|
||
currentBox: null,
|
||
isDrawingBox: false,
|
||
hoveredItem: null,
|
||
// UI elements
|
||
tabBar: tabBar,
|
||
counter: counter
|
||
};
|
||
|
||
// Add as DOM widget
|
||
const widget = this.addDOMWidget("canvas", "customCanvas", container);
|
||
this.canvasWidget.domWidget = widget;
|
||
|
||
widget.computeSize = (width) => {
|
||
const nodeHeight = this.size ? this.size[1] : 520;
|
||
const widgetHeight = Math.max(250, nodeHeight - 80);
|
||
return [width, widgetHeight];
|
||
};
|
||
|
||
// Build initial tab bar
|
||
this.rebuildTabBar();
|
||
|
||
// Clear Prompt button handler
|
||
clearPromptBtn.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.clearActivePrompt();
|
||
});
|
||
|
||
// Clear All button handler
|
||
clearAllBtn.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.clearAllPrompts();
|
||
});
|
||
|
||
// Hide storage widget
|
||
const storeWidget = this.widgets.find(w => w.name === "multi_prompts_store");
|
||
if (storeWidget) {
|
||
storeWidget.value = storeWidget.value || "[]";
|
||
this._hiddenWidgets = { multi_prompts_store: storeWidget };
|
||
hideWidgetForGood(this, storeWidget);
|
||
}
|
||
|
||
// Override onDrawForeground to hide converted widgets
|
||
const originalDrawForeground = this.onDrawForeground;
|
||
this.onDrawForeground = function(ctx) {
|
||
const hiddenWidgets = this.widgets.filter(w => w.type?.includes("converted-widget"));
|
||
const originalTypes = hiddenWidgets.map(w => w.type);
|
||
hiddenWidgets.forEach(w => w.type = null);
|
||
if (originalDrawForeground) originalDrawForeground.apply(this, arguments);
|
||
hiddenWidgets.forEach((w, i) => w.type = originalTypes[i]);
|
||
|
||
// Update container height
|
||
const containerHeight = Math.max(250, this.size[1] - 80);
|
||
if (container.style.height !== containerHeight + "px") {
|
||
container.style.height = containerHeight + "px";
|
||
}
|
||
};
|
||
|
||
// Mouse event handlers
|
||
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;
|
||
|
||
const activePrompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
|
||
const isNegative = e.button === 2;
|
||
|
||
// Shift key = box mode
|
||
if (e.shiftKey) {
|
||
this.canvasWidget.currentBox = { x1: x, y1: y, x2: x, y2: y, isNegative };
|
||
this.canvasWidget.isDrawingBox = true;
|
||
return;
|
||
}
|
||
|
||
// Add point (left-click = positive, right-click = negative)
|
||
const pointList = isNegative ? activePrompt.negative_points : activePrompt.positive_points;
|
||
pointList.push({ x, y });
|
||
this.updateStorage();
|
||
this.redrawCanvas();
|
||
});
|
||
|
||
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;
|
||
|
||
if (this.canvasWidget.isDrawingBox && this.canvasWidget.currentBox) {
|
||
this.canvasWidget.currentBox.x2 = x;
|
||
this.canvasWidget.currentBox.y2 = y;
|
||
this.redrawCanvas();
|
||
} else {
|
||
// Check hover
|
||
const hovered = this.findItemAt(x, y);
|
||
if (hovered !== this.canvasWidget.hoveredItem) {
|
||
this.canvasWidget.hoveredItem = hovered;
|
||
this.redrawCanvas();
|
||
}
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener("mouseup", (e) => {
|
||
if (this.canvasWidget.isDrawingBox && this.canvasWidget.currentBox) {
|
||
const box = this.canvasWidget.currentBox;
|
||
const width = Math.abs(box.x2 - box.x1);
|
||
const height = Math.abs(box.y2 - box.y1);
|
||
|
||
if (width > 5 && height > 5) {
|
||
const normalizedBox = {
|
||
x1: Math.min(box.x1, box.x2),
|
||
y1: Math.min(box.y1, box.y2),
|
||
x2: Math.max(box.x1, box.x2),
|
||
y2: Math.max(box.y1, box.y2)
|
||
};
|
||
|
||
const activePrompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
|
||
const boxList = box.isNegative ? activePrompt.negative_boxes : activePrompt.positive_boxes;
|
||
boxList.push(normalizedBox);
|
||
this.updateStorage();
|
||
}
|
||
|
||
this.canvasWidget.currentBox = null;
|
||
this.canvasWidget.isDrawingBox = false;
|
||
this.redrawCanvas();
|
||
}
|
||
});
|
||
|
||
canvas.addEventListener("contextmenu", (e) => {
|
||
e.preventDefault();
|
||
canvas.dispatchEvent(new MouseEvent('mousedown', {
|
||
button: 2,
|
||
clientX: e.clientX,
|
||
clientY: e.clientY,
|
||
shiftKey: e.shiftKey
|
||
}));
|
||
});
|
||
|
||
// Handle image loading
|
||
this.onExecuted = (message) => {
|
||
if (message.bg_image && message.bg_image[0]) {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
this.canvasWidget.image = img;
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
this.redrawCanvas();
|
||
};
|
||
img.src = "data:image/jpeg;base64," + message.bg_image[0];
|
||
}
|
||
};
|
||
|
||
// Handle node resize
|
||
const originalOnResize = this.onResize;
|
||
this.onResize = function(size) {
|
||
if (originalOnResize) originalOnResize.apply(this, arguments);
|
||
const containerHeight = Math.max(250, size[1] - 80);
|
||
container.style.height = containerHeight + "px";
|
||
};
|
||
|
||
// Initial draw
|
||
this.redrawCanvas();
|
||
|
||
// Set initial size
|
||
this.setSize([400, 520]);
|
||
container.style.height = "440px";
|
||
|
||
return result;
|
||
};
|
||
|
||
// Restore state when workflow is loaded
|
||
const onConfigure = nodeType.prototype.onConfigure;
|
||
nodeType.prototype.onConfigure = function(info) {
|
||
onConfigure?.apply(this, arguments);
|
||
|
||
// Restore prompts from stored widget value
|
||
const storeWidget = this._hiddenWidgets?.multi_prompts_store;
|
||
if (storeWidget && storeWidget.value) {
|
||
try {
|
||
const stored = JSON.parse(storeWidget.value);
|
||
if (Array.isArray(stored) && stored.length > 0) {
|
||
this.canvasWidget.prompts = stored;
|
||
this.canvasWidget.activePromptIndex = 0;
|
||
this.rebuildTabBar();
|
||
this.redrawCanvas();
|
||
console.log("[SAM3] Restored", stored.length, "prompts from saved state");
|
||
}
|
||
} catch (e) {
|
||
console.log("[SAM3] Failed to restore prompts:", e);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Rebuild tab bar
|
||
nodeType.prototype.rebuildTabBar = function() {
|
||
const tabBar = this.canvasWidget.tabBar;
|
||
tabBar.innerHTML = "";
|
||
|
||
// Create tabs for each prompt
|
||
this.canvasWidget.prompts.forEach((prompt, idx) => {
|
||
const tab = document.createElement("div");
|
||
const color = PROMPT_COLORS[idx % PROMPT_COLORS.length];
|
||
const isActive = idx === this.canvasWidget.activePromptIndex;
|
||
|
||
tab.style.cssText = `
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 4px 8px; background: ${isActive ? '#333' : '#2a2a2a'};
|
||
border: 1px solid ${isActive ? color.primary : '#444'};
|
||
border-radius: 4px; cursor: pointer; font-size: 11px;
|
||
color: ${isActive ? '#fff' : '#aaa'};
|
||
`;
|
||
tab.dataset.promptIndex = idx;
|
||
|
||
// Color indicator
|
||
const colorDot = document.createElement("span");
|
||
colorDot.style.cssText = `width: 10px; height: 10px; border-radius: 2px; background: ${color.primary};`;
|
||
tab.appendChild(colorDot);
|
||
|
||
// Label
|
||
const label = document.createElement("span");
|
||
label.textContent = `Prompt ${idx + 1}`;
|
||
tab.appendChild(label);
|
||
|
||
// Delete button (only if more than 1 prompt)
|
||
if (this.canvasWidget.prompts.length > 1) {
|
||
const deleteBtn = document.createElement("span");
|
||
deleteBtn.textContent = "×";
|
||
deleteBtn.style.cssText = "color: #888; cursor: pointer; font-size: 14px; padding: 0 2px; margin-left: 2px;";
|
||
deleteBtn.onmouseover = () => deleteBtn.style.color = "#f00";
|
||
deleteBtn.onmouseout = () => deleteBtn.style.color = "#888";
|
||
deleteBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this.deletePrompt(idx);
|
||
};
|
||
tab.appendChild(deleteBtn);
|
||
}
|
||
|
||
// Tab click handler
|
||
tab.onclick = () => this.setActivePrompt(idx);
|
||
tab.onmouseover = () => { if (!isActive) tab.style.background = '#3a3a3a'; };
|
||
tab.onmouseout = () => { if (!isActive) tab.style.background = '#2a2a2a'; };
|
||
|
||
tabBar.appendChild(tab);
|
||
});
|
||
|
||
// Add prompt button
|
||
if (this.canvasWidget.prompts.length < MAX_PROMPTS) {
|
||
const addBtn = document.createElement("button");
|
||
addBtn.textContent = "+";
|
||
addBtn.style.cssText = `
|
||
padding: 4px 12px; background: #2a5a2a; border: 1px solid #3a7a3a;
|
||
border-radius: 4px; color: #8f8; cursor: pointer; font-size: 14px; font-weight: bold;
|
||
`;
|
||
addBtn.onmouseover = () => addBtn.style.background = "#3a6a3a";
|
||
addBtn.onmouseout = () => addBtn.style.background = "#2a5a2a";
|
||
addBtn.onclick = () => this.addNewPrompt();
|
||
tabBar.appendChild(addBtn);
|
||
}
|
||
|
||
this.updateCounter();
|
||
};
|
||
|
||
// Set active prompt
|
||
nodeType.prototype.setActivePrompt = function(index) {
|
||
this.canvasWidget.activePromptIndex = index;
|
||
this.rebuildTabBar();
|
||
this.redrawCanvas();
|
||
};
|
||
|
||
// Add new prompt
|
||
nodeType.prototype.addNewPrompt = function() {
|
||
if (this.canvasWidget.prompts.length >= MAX_PROMPTS) return;
|
||
|
||
this.canvasWidget.prompts.push({
|
||
positive_points: [],
|
||
negative_points: [],
|
||
positive_boxes: [],
|
||
negative_boxes: []
|
||
});
|
||
this.canvasWidget.activePromptIndex = this.canvasWidget.prompts.length - 1;
|
||
this.rebuildTabBar();
|
||
this.updateStorage();
|
||
this.redrawCanvas();
|
||
};
|
||
|
||
// Delete prompt
|
||
nodeType.prototype.deletePrompt = function(index) {
|
||
if (this.canvasWidget.prompts.length <= 1) {
|
||
this.clearActivePrompt();
|
||
return;
|
||
}
|
||
|
||
this.canvasWidget.prompts.splice(index, 1);
|
||
if (this.canvasWidget.activePromptIndex >= this.canvasWidget.prompts.length) {
|
||
this.canvasWidget.activePromptIndex = this.canvasWidget.prompts.length - 1;
|
||
}
|
||
this.rebuildTabBar();
|
||
this.updateStorage();
|
||
this.redrawCanvas();
|
||
};
|
||
|
||
// Clear active prompt
|
||
nodeType.prototype.clearActivePrompt = function() {
|
||
const prompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
|
||
prompt.positive_points = [];
|
||
prompt.negative_points = [];
|
||
prompt.positive_boxes = [];
|
||
prompt.negative_boxes = [];
|
||
this.updateStorage();
|
||
this.redrawCanvas();
|
||
};
|
||
|
||
// Clear all prompts
|
||
nodeType.prototype.clearAllPrompts = function() {
|
||
this.canvasWidget.prompts = [{
|
||
positive_points: [],
|
||
negative_points: [],
|
||
positive_boxes: [],
|
||
negative_boxes: []
|
||
}];
|
||
this.canvasWidget.activePromptIndex = 0;
|
||
this.rebuildTabBar();
|
||
this.updateStorage();
|
||
this.redrawCanvas();
|
||
};
|
||
|
||
// Find item at coordinates (only checks active prompt)
|
||
nodeType.prototype.findItemAt = function(x, y) {
|
||
const threshold = 10;
|
||
const pIdx = this.canvasWidget.activePromptIndex;
|
||
const prompt = this.canvasWidget.prompts[pIdx];
|
||
|
||
// Check points
|
||
for (let i = 0; i < prompt.positive_points.length; i++) {
|
||
const pt = prompt.positive_points[i];
|
||
if (Math.abs(pt.x - x) < threshold && Math.abs(pt.y - y) < threshold) {
|
||
return { type: "point", index: i, promptIndex: pIdx, isNegative: false };
|
||
}
|
||
}
|
||
for (let i = 0; i < prompt.negative_points.length; i++) {
|
||
const pt = prompt.negative_points[i];
|
||
if (Math.abs(pt.x - x) < threshold && Math.abs(pt.y - y) < threshold) {
|
||
return { type: "point", index: i, promptIndex: pIdx, isNegative: true };
|
||
}
|
||
}
|
||
|
||
// Check boxes
|
||
for (let i = 0; i < prompt.positive_boxes.length; i++) {
|
||
const box = prompt.positive_boxes[i];
|
||
if (x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2) {
|
||
return { type: "box", index: i, promptIndex: pIdx, isNegative: false };
|
||
}
|
||
}
|
||
for (let i = 0; i < prompt.negative_boxes.length; i++) {
|
||
const box = prompt.negative_boxes[i];
|
||
if (x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2) {
|
||
return { type: "box", index: i, promptIndex: pIdx, isNegative: true };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
// Update storage widget
|
||
nodeType.prototype.updateStorage = function() {
|
||
const widget = this._hiddenWidgets?.multi_prompts_store;
|
||
if (widget) {
|
||
widget.value = JSON.stringify(this.canvasWidget.prompts);
|
||
}
|
||
this.updateCounter();
|
||
};
|
||
|
||
// Update counter display
|
||
nodeType.prototype.updateCounter = function() {
|
||
const prompt = this.canvasWidget.prompts[this.canvasWidget.activePromptIndex];
|
||
const pts = prompt.positive_points.length + prompt.negative_points.length;
|
||
const boxes = prompt.positive_boxes.length + prompt.negative_boxes.length;
|
||
this.canvasWidget.counter.textContent = `Prompt ${this.canvasWidget.activePromptIndex + 1}: ${pts} pts, ${boxes} boxes`;
|
||
};
|
||
|
||
// Redraw canvas
|
||
nodeType.prototype.redrawCanvas = function() {
|
||
const { canvas, ctx, image, prompts, activePromptIndex, currentBox, hoveredItem } = this.canvasWidget;
|
||
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Draw image or placeholder
|
||
if (image) {
|
||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||
} else {
|
||
ctx.fillStyle = "#333";
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
ctx.fillStyle = "#666";
|
||
ctx.font = "14px sans-serif";
|
||
ctx.textAlign = "center";
|
||
ctx.fillText("Click: Positive point | Right-click: Negative point", canvas.width / 2, canvas.height / 2 - 10);
|
||
ctx.fillText("Shift+Drag: Box | Shift+Right-drag: Negative box", canvas.width / 2, canvas.height / 2 + 15);
|
||
}
|
||
|
||
// Only draw the active prompt
|
||
const prompt = prompts[activePromptIndex];
|
||
const color = PROMPT_COLORS[activePromptIndex % PROMPT_COLORS.length];
|
||
|
||
// Draw boxes first
|
||
this.drawBoxes(ctx, prompt.positive_boxes, color.primary, 1.0, false, activePromptIndex, hoveredItem);
|
||
this.drawBoxes(ctx, prompt.negative_boxes, color.primary, 1.0, true, activePromptIndex, hoveredItem);
|
||
|
||
// Draw points
|
||
this.drawPoints(ctx, prompt.positive_points, color.primary, 1.0, false, activePromptIndex, hoveredItem);
|
||
this.drawPoints(ctx, prompt.negative_points, color.primary, 1.0, true, activePromptIndex, hoveredItem);
|
||
|
||
// Draw current box being drawn
|
||
if (currentBox) {
|
||
const color = PROMPT_COLORS[activePromptIndex % PROMPT_COLORS.length];
|
||
ctx.setLineDash([5, 5]);
|
||
ctx.strokeStyle = currentBox.isNegative ? "#f80" : color.primary;
|
||
ctx.lineWidth = 2;
|
||
const w = currentBox.x2 - currentBox.x1;
|
||
const h = currentBox.y2 - currentBox.y1;
|
||
ctx.strokeRect(currentBox.x1, currentBox.y1, w, h);
|
||
ctx.setLineDash([]);
|
||
|
||
ctx.fillStyle = currentBox.isNegative ? "rgba(255,128,0,0.1)" : this.colorWithAlpha(color.primary, 0.1);
|
||
ctx.fillRect(currentBox.x1, currentBox.y1, w, h);
|
||
}
|
||
|
||
// Draw image dimensions
|
||
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 points
|
||
nodeType.prototype.drawPoints = function(ctx, points, color, alpha, isNegative, promptIndex, hoveredItem) {
|
||
// Scale point size relative to image height (1080p = baseline)
|
||
const canvas = this.canvasWidget.canvas;
|
||
const scaleFactor = Math.max(0.5, canvas.height / 1080);
|
||
const baseRadius = 6 * scaleFactor;
|
||
const hoverRadius = 8 * scaleFactor;
|
||
|
||
for (let i = 0; i < points.length; i++) {
|
||
const pt = points[i];
|
||
const isHovered = hoveredItem?.type === "point" &&
|
||
hoveredItem?.promptIndex === promptIndex &&
|
||
hoveredItem?.index === i &&
|
||
hoveredItem?.isNegative === isNegative;
|
||
const radius = isHovered ? hoverRadius : baseRadius;
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(pt.x, pt.y, radius, 0, 2 * Math.PI);
|
||
|
||
// Fill
|
||
if (isNegative) {
|
||
ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 0.8})`;
|
||
} else {
|
||
ctx.fillStyle = this.colorWithAlpha(color, alpha * 0.8);
|
||
}
|
||
ctx.fill();
|
||
|
||
// Stroke
|
||
ctx.strokeStyle = isHovered ? "#fff" : this.colorWithAlpha(color, alpha);
|
||
ctx.lineWidth = (isHovered ? 3 : 2) * scaleFactor;
|
||
ctx.stroke();
|
||
|
||
// X marker for negative
|
||
if (isNegative) {
|
||
const xSize = 3 * scaleFactor;
|
||
ctx.strokeStyle = "#fff";
|
||
ctx.lineWidth = 2 * scaleFactor;
|
||
ctx.beginPath();
|
||
ctx.moveTo(pt.x - xSize, pt.y - xSize);
|
||
ctx.lineTo(pt.x + xSize, pt.y + xSize);
|
||
ctx.moveTo(pt.x + xSize, pt.y - xSize);
|
||
ctx.lineTo(pt.x - xSize, pt.y + xSize);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
};
|
||
|
||
// Draw boxes
|
||
nodeType.prototype.drawBoxes = function(ctx, boxes, color, alpha, isNegative, promptIndex, hoveredItem) {
|
||
for (let i = 0; i < boxes.length; i++) {
|
||
const box = boxes[i];
|
||
const w = box.x2 - box.x1;
|
||
const h = box.y2 - box.y1;
|
||
const isHovered = hoveredItem?.type === "box" &&
|
||
hoveredItem?.promptIndex === promptIndex &&
|
||
hoveredItem?.index === i &&
|
||
hoveredItem?.isNegative === isNegative;
|
||
|
||
// Fill
|
||
if (isNegative) {
|
||
ctx.fillStyle = `rgba(255, 0, 0, ${alpha * 0.15})`;
|
||
} else {
|
||
ctx.fillStyle = this.colorWithAlpha(color, alpha * 0.15);
|
||
}
|
||
ctx.fillRect(box.x1, box.y1, w, h);
|
||
|
||
// Stroke
|
||
ctx.strokeStyle = isHovered ? "#fff" : (isNegative ? `rgba(255,0,0,${alpha})` : this.colorWithAlpha(color, alpha));
|
||
ctx.lineWidth = isHovered ? 3 : 2;
|
||
|
||
if (isNegative) {
|
||
ctx.setLineDash([4, 4]);
|
||
}
|
||
ctx.strokeRect(box.x1, box.y1, w, h);
|
||
ctx.setLineDash([]);
|
||
}
|
||
};
|
||
|
||
// Helper: color with alpha
|
||
nodeType.prototype.colorWithAlpha = function(hexColor, alpha) {
|
||
const r = parseInt(hexColor.slice(1, 3), 16);
|
||
const g = parseInt(hexColor.slice(3, 5), 16);
|
||
const b = parseInt(hexColor.slice(5, 7), 16);
|
||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||
};
|
||
}
|
||
});
|