import { app } from "../../scripts/app.js"; import { registerMcePill } from "/extensions/ComfyUI-MCE/mce_bottom_bar.js"; const EXT_NAME = "ComfyUI-NodeColorizer.Panel"; let panelEl = null; let isOpen = false; function loadCssOnce() { const id = "mce-node-colorizer-css"; if (document.getElementById(id)) return; const link = document.createElement("link"); link.id = id; link.rel = "stylesheet"; link.href = "/extensions/ComfyUI-NodeColorizer/node_colorizer.css"; document.head.appendChild(link); } function clamp01(x) { x = Number(x); if (Number.isNaN(x)) return 0; return Math.min(1, Math.max(0, x)); } function clamp255(x) { x = Number(x); if (Number.isNaN(x)) return 0; return Math.min(255, Math.max(0, Math.round(x))); } function rgbaToCss(r, g, b, a) { return `rgba(${clamp255(r)}, ${clamp255(g)}, ${clamp255(b)}, ${clamp01(a)})`; } function rgbaToHex(r, g, b) { const rr = clamp255(r).toString(16).padStart(2, "0"); const gg = clamp255(g).toString(16).padStart(2, "0"); const bb = clamp255(b).toString(16).padStart(2, "0"); return `#${rr}${gg}${bb}`; } function hexToRgb(hex) { const h = (hex || "").trim(); const m = /^#?([0-9a-f]{6})$/i.exec(h); if (!m) return { r: 255, g: 0, b: 0 }; const n = parseInt(m[1], 16); return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; } function getSelectedNodes() { // LiteGraph selection is usually stored in app.canvas.selected_nodes (map) const sel = app?.canvas?.selected_nodes; if (!sel) return []; // sel can be an object map { [id]: node } return Object.values(sel).filter(Boolean); } function markCanvasDirty() { try { app.graph?.setDirtyCanvas(true, true); } catch {} try { app.canvas?.draw(true, true); } catch {} } function applyColorToNodes(cssColor) { const nodes = getSelectedNodes(); if (!nodes.length) { alert("No nodes selected.\nSelect one or more nodes, then click Apply."); return; } for (const node of nodes) { // LiteGraph nodes typically respect color/bgcolor node.color = cssColor; node.bgcolor = cssColor; // Optional: persist a hint in properties (handy for later tooling) node.properties = node.properties || {}; node.properties.mce_node_color = cssColor; } markCanvasDirty(); } function makePanel() { if (panelEl) return panelEl; panelEl = document.createElement("div"); panelEl.id = "mce-node-colorizer-panel"; panelEl.style.display = "none"; // Default position near bottom-right-ish, above the bar panelEl.style.left = "calc(50% + 220px)"; panelEl.style.bottom = "70px"; panelEl.innerHTML = `
Node Color
`; document.body.appendChild(panelEl); const q = (sel) => panelEl.querySelector(sel); const r = q('[data-nc="r"]'); const g = q('[data-nc="g"]'); const b = q('[data-nc="b"]'); const a = q('[data-nc="a"]'); const rnum = q('[data-nc="rnum"]'); const gnum = q('[data-nc="gnum"]'); const bnum = q('[data-nc="bnum"]'); const anum = q('[data-nc="anum"]'); const hex = q('[data-nc="hex"]'); const preview = q('[data-nc="preview"]'); function syncPreview() { const css = rgbaToCss(r.value, g.value, b.value, a.value); preview.style.background = css; // keep the hex picker aligned to RGB (alpha not supported by ) hex.value = rgbaToHex(r.value, g.value, b.value); } function syncFromHex() { const { r: rr, g: gg, b: bb } = hexToRgb(hex.value); r.value = rr; g.value = gg; b.value = bb; rnum.value = rr; gnum.value = gg; bnum.value = bb; syncPreview(); } function bindPair(rangeEl, numEl, clampFn) { rangeEl.addEventListener("input", () => { numEl.value = rangeEl.value; syncPreview(); }); numEl.addEventListener("input", () => { const v = clampFn(numEl.value); rangeEl.value = v; numEl.value = v; syncPreview(); }); } bindPair(r, rnum, clamp255); bindPair(g, gnum, clamp255); bindPair(b, bnum, clamp255); a.addEventListener("input", () => { anum.value = a.value; syncPreview(); }); anum.addEventListener("input", () => { const v = clamp01(anum.value); a.value = v; anum.value = v; syncPreview(); }); hex.addEventListener("input", syncFromHex); q(".nc-close").addEventListener("click", () => setOpen(false)); q('[data-nc="apply"]').addEventListener("click", () => { const css = rgbaToCss(r.value, g.value, b.value, a.value); applyColorToNodes(css); }); // Grab the first selected node's color as the UI color q('[data-nc="useSelected"]').addEventListener("click", () => { const nodes = getSelectedNodes(); if (!nodes.length) { alert("No nodes selected."); return; } const n = nodes[0]; const c = (n?.bgcolor || n?.color || "").toString(); // Try parse rgba(...) if present; otherwise ignore const m = /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i.exec(c); if (m) { const rr = clamp255(m[1]), gg = clamp255(m[2]), bb = clamp255(m[3]); const aa = m[4] == null ? 1 : clamp01(m[4]); r.value = rr; g.value = gg; b.value = bb; a.value = aa; rnum.value = rr; gnum.value = gg; bnum.value = bb; anum.value = aa; syncPreview(); } else { // fallback: keep UI as-is but still sync preview syncPreview(); } }); // Dragging const header = q('[data-nc-drag]'); let dragging = false; let startX = 0, startY = 0; let startLeft = 0, startTop = 0; function px(n) { return `${Math.round(n)}px`; } header.addEventListener("mousedown", (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const rect = panelEl.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // When dragging, switch to top/left positioning for stability panelEl.style.left = px(startLeft); panelEl.style.top = px(startTop); panelEl.style.bottom = "auto"; e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const newLeft = Math.max(8, Math.min(window.innerWidth - panelEl.offsetWidth - 8, startLeft + dx)); const newTop = Math.max(8, Math.min(window.innerHeight - panelEl.offsetHeight - 8, startTop + dy)); panelEl.style.left = px(newLeft); panelEl.style.top = px(newTop); }); window.addEventListener("mouseup", () => { dragging = false; }); // initialize preview syncPreview(); return panelEl; } function setOpen(next) { isOpen = !!next; const el = makePanel(); el.style.display = isOpen ? "block" : "none"; } app.registerExtension({ name: EXT_NAME, async setup() { loadCssOnce(); registerMcePill({ id: "node_color", label: "Node Color", // Put it wherever you want in the bar ordering // (Example: WF=10, Files=20, Node Color=30, Restart=40) order: 30, getIsOpen: () => isOpen, onToggle: () => setOpen(!isOpen), }); }, });