Files
jtrue-ComfyUI-Rect/js/index.js
T
2025-08-23 19:32:32 -04:00

232 lines
8.9 KiB
JavaScript

// ComfyUI-Rect front-end (Rect / Select) — marching ants, image-required, auto-prefill
import { app } from "../../scripts/app.js";
function setWidget(node, name, value) {
const w = node.widgets?.find(w => w.name === name);
if (w) {
w.value = value;
node.onWidgetChanged?.(name, value, w);
}
node.properties[name] = value;
node.setDirtyCanvas?.(true, true);
}
function toast(text, ms = 1800) {
const div = document.createElement("div");
Object.assign(div.style, {
position: "fixed", right: "16px", bottom: "16px",
background: "rgba(20,20,20,.9)", color: "#eee",
padding: "10px 12px", borderRadius: "10px",
zIndex: 10000, font: "12px/1.3 system-ui, sans-serif",
boxShadow: "0 6px 16px rgba(0,0,0,.35)"
});
div.textContent = text;
document.body.appendChild(div);
setTimeout(() => div.remove(), ms);
}
function upstreamFilenameFromImageInput(node) {
const idx = node.inputs?.findIndex(i => i.name === "image");
if (idx == null || idx < 0) return null;
const linkId = node.inputs[idx]?.link;
if (!linkId) return null;
const link = app.graph.links?.[linkId];
const upstream = link ? app.graph._nodes_by_id?.[link.origin_id] : null;
const w = upstream?.widgets?.find(w => w.name === "image" && typeof w.value === "string" && w.value.length);
return w ? String(w.value) : null;
}
function buildInputViewURL(nameOrPath) {
let p = String(nameOrPath).replace(/\\/g, "/");
const parts = p.split("/");
const file = parts.pop();
const subfolder = parts.join("/");
const ext = (file.split(".").pop() || "png").toLowerCase();
const format = ext === "jpg" || ext === "jpeg" ? "jpeg" : "png";
const u = new URL(`${location.origin}/view`);
u.searchParams.set("type", "input");
u.searchParams.set("filename", file);
u.searchParams.set("subfolder", subfolder);
u.searchParams.set("format", format);
return u.toString();
}
function openRectSelect(node) {
const upstreamName = upstreamFilenameFromImageInput(node);
if (!upstreamName) { toast("Rect / Select: connect an image to the 'image' input."); return; }
const upstreamURL = buildInputViewURL(upstreamName);
// Overlay & modal
const overlay = document.createElement("div");
Object.assign(overlay.style, {
position: "fixed", inset: "0", background: "rgba(0,0,0,0.6)",
zIndex: 9999, display: "flex", alignItems: "center", justifyContent: "center"
});
const modal = document.createElement("div");
Object.assign(modal.style, {
background: "#111", color: "#eee", padding: "16px", borderRadius: "12px",
width: "min(92vw, 1100px)", maxHeight: "90vh",
display: "grid", gridTemplateRows: "auto 1fr auto", gap: "12px",
boxShadow: "0 10px 30px rgba(0,0,0,0.5)"
});
overlay.appendChild(modal);
// Header
const header = document.createElement("div");
Object.assign(header.style, { display: "flex", justifyContent: "space-between", alignItems: "center" });
header.innerHTML = `<div style="font-weight:600">Rect / Select</div>`;
const closeBtn = document.createElement("button");
closeBtn.textContent = "Close";
closeBtn.onclick = () => document.body.removeChild(overlay);
header.appendChild(closeBtn);
modal.appendChild(header);
// Canvas area
const area = document.createElement("div");
Object.assign(area.style, { overflow: "auto", background: "#222", padding: "8px" });
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
area.appendChild(canvas);
modal.appendChild(area);
// Footer (coords left, Apply right)
const footer = document.createElement("div");
Object.assign(footer.style, { display: "flex", gap: "12px", alignItems: "center", flexWrap: "wrap" });
const coords = document.createElement("div");
Object.assign(coords.style, { color: "#aaa", fontStyle: "italic", fontSize: "12px", minHeight: "1em" });
const spacer = document.createElement("div");
spacer.style.flex = "1";
const applyBtn = document.createElement("button");
applyBtn.textContent = "Apply Rect";
applyBtn.disabled = true;
Object.assign(applyBtn.style, { padding: "10px 14px", fontWeight: "600", borderRadius: "8px", border: "none", cursor: "pointer" });
footer.append(coords, spacer, applyBtn);
modal.appendChild(footer);
// State
let img = new Image(), imgLoaded = false;
let dragging = false, sx = 0, sy = 0, cx = 0, cy = 0;
let antsOffset = 0;
// Helpers
function clampRect(x, y, w, h, W, H) {
x = Math.max(0, Math.min(x, W));
y = Math.max(0, Math.min(y, H));
w = Math.max(1, Math.min(w, W));
h = Math.max(1, Math.min(h, H));
if (x + w > W) w = Math.max(1, W - x);
if (y + h > H) h = Math.max(1, H - y);
return [x, y, w, h];
}
function setSelectionFromNode() {
if (!imgLoaded) return;
const W = img.naturalWidth, H = img.naturalHeight;
let nx = Number(node.properties?.x ?? 0);
let ny = Number(node.properties?.y ?? 0);
let nw = Number(node.properties?.w ?? Math.floor(W / 2));
let nh = Number(node.properties?.h ?? Math.floor(H / 2));
[nx, ny, nw, nh] = clampRect(nx, ny, nw, nh, W, H);
const sxScale = canvas.width / W;
const syScale = canvas.height / H;
sx = Math.round(nx * sxScale); sy = Math.round(ny * syScale);
cx = Math.round((nx + nw) * sxScale); cy = Math.round((ny + nh) * syScale);
draw();
}
function fitToViewport() {
if (!imgLoaded) return;
const maxW = Math.min(window.innerWidth * 0.88, 1600);
const maxH = Math.min(window.innerHeight * 0.65, 900);
const scale = Math.min(maxW / img.naturalWidth, maxH / img.naturalHeight, 1);
canvas.width = Math.max(2, Math.floor(img.naturalWidth * scale));
canvas.height = Math.max(2, Math.floor(img.naturalHeight * scale));
setSelectionFromNode();
}
function draw() {
ctx.fillStyle = "#333"; ctx.fillRect(0, 0, canvas.width, canvas.height);
if (imgLoaded) ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const x = Math.min(sx, cx), y = Math.min(sy, cy);
const w = Math.abs(cx - sx), h = Math.abs(cy - sy);
if (w > 0 && h > 0) {
const seg = 8;
ctx.lineWidth = 2;
ctx.setLineDash([seg, seg]);
ctx.lineDashOffset = -antsOffset; ctx.strokeStyle = "#fff"; ctx.strokeRect(x, y, w, h);
ctx.lineDashOffset = seg - antsOffset; ctx.strokeStyle = "#000"; ctx.strokeRect(x, y, w, h);
coords.textContent = `x=${x}, y=${y}, w=${w}, h=${h}`;
} else {
coords.textContent = imgLoaded ? "Drag to draw a rectangle." : "";
}
}
function animate() {
antsOffset = (antsOffset + 1) % 16;
draw();
if (document.body.contains(overlay)) requestAnimationFrame(animate);
}
function loadFromURL(url) {
img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => { imgLoaded = true; applyBtn.disabled = false; fitToViewport(); };
img.onerror = () => { toast("Rect / Select: could not load upstream image."); };
img.src = url;
}
// Interactions
canvas.addEventListener("mousedown", (e) => {
if (!imgLoaded) return;
const r = canvas.getBoundingClientRect();
sx = cx = e.clientX - r.left; sy = cy = e.clientY - r.top; dragging = true; draw();
});
canvas.addEventListener("mousemove", (e) => {
if (!dragging || !imgLoaded) return;
const r = canvas.getBoundingClientRect();
cx = e.clientX - r.left; cy = e.clientY - r.top; draw();
});
window.addEventListener("mouseup", () => { if (dragging) { dragging = false; draw(); } });
window.addEventListener("resize", fitToViewport);
applyBtn.onclick = () => {
if (!imgLoaded) return;
const scaleX = img.naturalWidth / canvas.width;
const scaleY = img.naturalHeight / canvas.height;
const x = Math.round(Math.min(sx, cx) * scaleX);
const y = Math.round(Math.min(sy, cy) * scaleY);
const w = Math.round(Math.abs(cx - sx) * scaleX);
const h = Math.round(Math.abs(cy - sy) * scaleY);
if (w < 1 || h < 1) { toast("Draw a rectangle first."); return; }
setWidget(node, "x", x); setWidget(node, "y", y);
setWidget(node, "w", w); setWidget(node, "h", h);
setTimeout(() => document.body.contains(overlay) && document.body.removeChild(overlay), 250);
};
document.body.appendChild(overlay);
requestAnimationFrame(animate);
loadFromURL(upstreamURL);
}
// Register: attach button to RectSelect nodes
app.registerExtension({
name: "ComfyUI-Rect",
nodeCreated(node) {
if (node?.comfyClass !== "RectSelect") return;
if (node.widgets?.some(w => w.__rect_btn)) return;
const btn = node.addWidget("button", "Open Rect / Select", "open", () => openRectSelect(node));
btn.__rect_btn = true;
node.properties ??= {};
node.properties.x ??= 0; node.properties.y ??= 0;
node.properties.w ??= 256; node.properties.h ??= 256;
console.log("[Rect] button attached to node", node.id);
},
});