import { app } from "/scripts/app.js"; import { t } from "../core/i18n.js"; import { syncAllNodeBadges } from "../comfy/nodes.js"; export function showNotification(message, type = "success", title = null) { const summary = title || (type === "error" ? t("Error") : type === "warn" ? t("Warning") : "Comfy Sidebar"); if (app.extensionManager?.toast?.add) { app.extensionManager.toast.add({ severity: type === "error" ? "error" : type === "warn" ? "warn" : "success", summary, detail: message, life: 4000 }); return; } // Elegant non-blocking fallback if native toast is unavailable const id = "comfy-sidebar-toast"; let toast = document.getElementById(id); if (!toast) { toast = document.createElement("div"); toast.id = id; document.body.appendChild(toast); } toast.style.cssText = ` position: fixed; bottom: 24px; right: 24px; background: #18181b; color: #f4f4f5; padding: 12px 18px; border-radius: 8px; border-left: 4px solid ${type === "error" ? "#ef4444" : type === "warn" ? "#f59e0b" : "#10b981"}; box-shadow: 0 8px 24px rgba(0,0,0,0.5); z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; line-height: 1.4; max-width: 380px; transition: opacity 0.25s ease, transform 0.25s ease; opacity: 0; transform: translateY(10px); pointer-events: none; `; toast.innerHTML = `${summary}
${message}
`; requestAnimationFrame(() => { toast.style.opacity = "1"; toast.style.transform = "translateY(0)"; }); clearTimeout(toast._timeout); toast._timeout = setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateY(10px)"; setTimeout(() => toast.remove(), 300); }, 3500); } export async function renumberNodesTopologically() { if (!app.graph || !app.graph._nodes || app.graph._nodes.length === 0) { showNotification(t("No nodes found on canvas to renumber."), "warn"); return; } const confirmed = confirm( t("Renumbering will rewrite all node IDs on the canvas in topological execution order.\n\nThis will fix intermediate node outputs showing as the final result, but may break external scripts that rely on specific node IDs\n\nDo you want to proceed?") ); if (!confirmed) return; try { // Compute LiteGraph's topological execution order if (typeof app.graph.computeExecutionOrder === "function") { app.graph.computeExecutionOrder(false); } // Sort nodes by order (lowest executes first; highest executes last) const sortedNodes = [...app.graph._nodes].sort((a, b) => { const orderA = typeof a.order === "number" ? a.order : Infinity; const orderB = typeof b.order === "number" ? b.order : Infinity; if (orderA !== orderB) return orderA - orderB; const posXA = Array.isArray(a.pos) ? a.pos[0] : 0; const posXB = Array.isArray(b.pos) ? b.pos[0] : 0; if (posXA !== posXB) return posXA - posXB; const posYA = Array.isArray(a.pos) ? a.pos[1] : 0; const posYB = Array.isArray(b.pos) ? b.pos[1] : 0; return posYA - posYB; }); const oldToNew = new Map(); sortedNodes.forEach((node, index) => { const newId = index + 1; oldToNew.set(node.id, newId); oldToNew.set(String(node.id), newId); }); // 1. Remap node IDs in-place on app.graph app.graph._nodes_by_id = {}; sortedNodes.forEach(node => { const oldId = node.id; const newId = oldToNew.get(oldId); if (newId != null) { // Update internal widget IDs like "$2034-0" (PreviewBridge) if (Array.isArray(node.widgets)) { for (const w of node.widgets) { if (typeof w.value === "string" && w.value.startsWith(`$${oldId}-`)) { w.value = w.value.replace(`$${oldId}-`, `$${newId}-`); } } } node.id = newId; app.graph._nodes_by_id[newId] = node; } }); // 2. Remap links if (app.graph.links) { const links = app.graph.links instanceof Map ? Array.from(app.graph.links.values()) : Object.values(app.graph.links); links.forEach(link => { if (!link) return; if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id); if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id); }); } app.graph.last_node_id = sortedNodes.length; app.graph.setDirtyCanvas(true, true); if (app.canvas?.draw) { app.canvas.draw(true, true); } syncAllNodeBadges(); const lastNode = sortedNodes[sortedNodes.length - 1]; const lastNodeName = lastNode ? (lastNode.title || lastNode.type) : "SaveImage"; showNotification(t("Renumbered {count} nodes by execution order. Latest node: #{id} ({name}).", { count: sortedNodes.length, id: lastNode ? lastNode.id : "", name: lastNodeName }), "success"); } catch (err) { console.error("ComfySidebar: Failed to renumber nodes", err); showNotification(t("Failed to renumber nodes: ") + err.message, "error"); } } export async function setManualNodeId(targetNode = null) { if (!app.graph) return; const rawSelected = app.canvas?.selected_nodes; const selectedList = rawSelected instanceof Map || rawSelected instanceof Set ? Array.from(rawSelected.values()) : (Array.isArray(rawSelected) ? rawSelected : (rawSelected && typeof rawSelected === "object" ? Object.values(rawSelected) : [])); const node = targetNode || selectedList[0] || app.canvas?.current_active_node || null; if (!node) { showNotification(t("Please select a node first to change its ID."), "warn"); return; } const currentId = node.id; const input = prompt(t("Enter new ID for node #{id} ({name}):", { id: currentId, name: node.title || node.type }), String(currentId)); if (!input) return; const newId = parseInt(input.trim(), 10); if (isNaN(newId) || newId <= 0) { showNotification(t("Please enter a valid positive number."), "error"); return; } if (newId === currentId) return; try { const existingNode = app.graph.getNodeById ? app.graph.getNodeById(newId) : null; if (existingNode && existingNode !== node) { const confirmSwap = confirm(t("Node ID #{id} is already used by \"{name}\". Swap their IDs?", { id: newId, name: existingNode.title || existingNode.type })); if (!confirmSwap) return; } const oldToNew = new Map(); if (existingNode && existingNode !== node) { oldToNew.set(currentId, newId); oldToNew.set(newId, currentId); existingNode.id = currentId; } else { oldToNew.set(currentId, newId); } node.id = newId; // Rebuild lookup map app.graph._nodes_by_id = {}; for (const n of app.graph._nodes) { app.graph._nodes_by_id[n.id] = n; } // Remap links if (app.graph.links) { const links = app.graph.links instanceof Map ? Array.from(app.graph.links.values()) : Object.values(app.graph.links); links.forEach(link => { if (!link) return; if (oldToNew.has(link.origin_id)) link.origin_id = oldToNew.get(link.origin_id); if (oldToNew.has(link.target_id)) link.target_id = oldToNew.get(link.target_id); }); } app.graph.last_node_id = Math.max(app.graph.last_node_id || 0, newId, app.graph._nodes.length); app.graph.setDirtyCanvas(true, true); if (app.canvas?.draw) { app.canvas.draw(true, true); } syncAllNodeBadges(); showNotification(t("Assigned node ID #{id} to \"{name}\".", { id: newId, name: node.title || node.type }), "success"); } catch (err) { console.error("ComfySidebar: Failed to set node ID", err); showNotification(t("Failed to set node ID: ") + err.message, "error"); } }