Efficiency Nodes V2.0

This commit is contained in:
TSC
2023-10-20 15:49:32 -05:00
committed by GitHub
parent 749c42b69b
commit 93eb925686
41 changed files with 5013 additions and 888 deletions

180
js/node_options/addLinks.js Normal file
View File

@@ -0,0 +1,180 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
import { addNode } from "./common/utils.js";
function createKSamplerEntry(node, samplerType, subNodeType = null, isSDXL = false) {
const samplerLabelMap = {
"Eff": "KSampler (Efficient)",
"Adv": "KSampler Adv. (Efficient)",
"SDXL": "KSampler SDXL (Eff.)"
};
const subNodeLabelMap = {
"XYPlot": "XY Plot",
"NoiseControl": "Noise Control Script",
"HiResFix": "HighRes-Fix Script",
"TiledUpscale": "Tiled Upscaler Script",
"AnimateDiff": "AnimateDiff Script"
};
const nicknameMap = {
"KSampler (Efficient)": "KSampler",
"KSampler Adv. (Efficient)": "KSampler(Adv)",
"KSampler SDXL (Eff.)": "KSampler",
"XY Plot": "XY Plot",
"Noise Control Script": "NoiseControl",
"HighRes-Fix Script": "HiResFix",
"Tiled Upscaler Script": "TiledUpscale",
"AnimateDiff Script": "AnimateDiff"
};
const kSamplerLabel = samplerLabelMap[samplerType];
const subNodeLabel = subNodeLabelMap[subNodeType];
const kSamplerNickname = nicknameMap[kSamplerLabel];
const subNodeNickname = nicknameMap[subNodeLabel];
const contentLabel = subNodeNickname ? `${kSamplerNickname} + ${subNodeNickname}` : kSamplerNickname;
return {
content: contentLabel,
callback: function() {
const kSamplerNode = addNode(kSamplerLabel, node, { shiftX: node.size[0] + 50 });
// Standard connections for all samplers
node.connect(0, kSamplerNode, 0); // MODEL
node.connect(1, kSamplerNode, 1); // CONDITIONING+
node.connect(2, kSamplerNode, 2); // CONDITIONING-
// Additional connections for non-SDXL
if (!isSDXL) {
node.connect(3, kSamplerNode, 3); // LATENT
node.connect(4, kSamplerNode, 4); // VAE
}
if (subNodeLabel) {
const subNode = addNode(subNodeLabel, node, { shiftX: 50, shiftY: node.size[1] + 50 });
const dependencyIndex = isSDXL ? 3 : 5;
node.connect(dependencyIndex, subNode, 0);
subNode.connect(0, kSamplerNode, dependencyIndex);
}
},
};
}
function createStackerNode(node, type) {
const stackerLabelMap = {
"LoRA": "LoRA Stacker",
"ControlNet": "Control Net Stacker"
};
const contentLabel = stackerLabelMap[type];
return {
content: contentLabel,
callback: function() {
const stackerNode = addNode(contentLabel, node);
// Calculate the left shift based on the width of the new node
const shiftX = -(stackerNode.size[0] + 25);
stackerNode.pos[0] += shiftX; // Adjust the x position of the new node
// Introduce a Y offset of 200 for ControlNet Stacker node
if (type === "ControlNet") {
stackerNode.pos[1] += 300;
}
// Connect outputs to the Efficient Loader based on type
if (type === "LoRA") {
stackerNode.connect(0, node, 0);
} else if (type === "ControlNet") {
stackerNode.connect(0, node, 1);
}
},
};
}
function createXYPlotNode(node, type) {
const contentLabel = "XY Plot";
return {
content: contentLabel,
callback: function() {
const xyPlotNode = addNode(contentLabel, node);
// Center the X coordinate of the XY Plot node
const centerXShift = (node.size[0] - xyPlotNode.size[0]) / 2;
xyPlotNode.pos[0] += centerXShift;
// Adjust the Y position to place it below the loader node
xyPlotNode.pos[1] += node.size[1] + 60;
// Depending on the node type, connect the appropriate output to the XY Plot node
if (type === "Efficient") {
node.connect(6, xyPlotNode, 0);
} else if (type === "SDXL") {
node.connect(3, xyPlotNode, 0);
}
},
};
}
function getMenuValues(type, node) {
const subNodeTypes = [null, "XYPlot", "NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"];
const excludedSubNodeTypes = ["NoiseControl", "HiResFix", "TiledUpscale", "AnimateDiff"]; // Nodes to exclude from the menu
const menuValues = [];
// Add the new node types to the menu first for the correct order
menuValues.push(createStackerNode(node, "LoRA"));
menuValues.push(createStackerNode(node, "ControlNet"));
for (const subNodeType of subNodeTypes) {
// Skip adding submenu items that are in the excludedSubNodeTypes array
if (!excludedSubNodeTypes.includes(subNodeType)) {
const menuEntry = createKSamplerEntry(node, type === "Efficient" ? "Eff" : "SDXL", subNodeType, type === "SDXL");
menuValues.push(menuEntry);
}
}
// Insert the standalone XY Plot option after the KSampler without any subNodeTypes and before any other KSamplers with subNodeTypes
menuValues.splice(3, 0, createXYPlotNode(node, type));
return menuValues;
}
function showAddLinkMenuCommon(value, options, e, menu, node, type) {
const values = getMenuValues(type, node);
new LiteGraph.ContextMenu(values, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false;
}
// Extension Definition
app.registerExtension({
name: "efficiency.addLinks",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const linkTypes = {
"Efficient Loader": "Efficient",
"Eff. Loader SDXL": "SDXL"
};
const linkType = linkTypes[nodeData.name];
if (linkType) {
addMenuHandler(nodeType, function(insertOption) {
insertOption({
content: "⛓ Add link...",
has_submenu: true,
callback: (value, options, e, menu, node) => showAddLinkMenuCommon(value, options, e, menu, node, linkType)
});
});
}
},
});

View File

@@ -0,0 +1,152 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
import { addNode } from "./common/utils.js";
const connectionMap = {
"KSampler (Efficient)": ["input", 5],
"KSampler Adv. (Efficient)": ["input", 5],
"KSampler SDXL (Eff.)": ["input", 3],
"XY Plot": ["output", 0],
"Noise Control Script": ["input & output", 0],
"HighRes-Fix Script": ["input & output", 0],
"Tiled Upscaler Script": ["input & output", 0],
"AnimateDiff Script": ["output", 0]
};
/**
* connect this node output to the input of another node
* @method connect
* @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot)
* @param {LGraphNode} node the target node
* @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
* @return {Object} the link_info is created, otherwise null
LGraphNode.prototype.connect = function(output_slot, target_node, input_slot)
**/
function addAndConnectScriptNode(scriptType, selectedNode) {
const selectedNodeType = connectionMap[selectedNode.type];
const newNodeType = connectionMap[scriptType];
// 1. Create the new node without position adjustments
const newNode = addNode(scriptType, selectedNode, { shiftX: 0, shiftY: 0 });
// 2. Adjust position of the new node based on conditions
if (newNodeType[0].includes("input") && selectedNodeType[0].includes("output")) {
newNode.pos[0] += selectedNode.size[0] + 50;
} else if (newNodeType[0].includes("output") && selectedNodeType[0].includes("input")) {
newNode.pos[0] -= (newNode.size[0] + 50);
}
// 3. Logic for connecting the nodes
switch (selectedNodeType[0]) {
case "output":
if (newNodeType[0] === "input & output") {
// For every node that was previously connected to the selectedNode's output
const connectedNodes = selectedNode.getOutputNodes(selectedNodeType[1]);
if (connectedNodes && connectedNodes.length) {
for (let connectedNode of connectedNodes) {
// Disconnect the node from selectedNode's output
selectedNode.disconnectOutput(selectedNodeType[1]);
// Connect the newNode's output to the previously connected node,
// using the appropriate slot based on the type of the connectedNode
const targetSlot = (connectedNode.type in connectionMap) ? connectionMap[connectedNode.type][1] : 0;
newNode.connect(0, connectedNode, targetSlot);
}
}
// Connect selectedNode's output to newNode's input
selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
}
break;
case "input":
if (newNodeType[0] === "output") {
newNode.connect(0, selectedNode, selectedNodeType[1]);
} else if (newNodeType[0] === "input & output") {
const ogInputNode = selectedNode.getInputNode(selectedNodeType[1]);
if (ogInputNode) {
ogInputNode.connect(0, newNode, 0);
}
newNode.connect(0, selectedNode, selectedNodeType[1]);
}
break;
case "input & output":
if (newNodeType[0] === "output") {
newNode.connect(0, selectedNode, 0);
} else if (newNodeType[0] === "input & output") {
const connectedNodes = selectedNode.getOutputNodes(0);
if (connectedNodes && connectedNodes.length) {
for (let connectedNode of connectedNodes) {
selectedNode.disconnectOutput(0);
newNode.connect(0, connectedNode, connectedNode.type in connectionMap ? connectionMap[connectedNode.type][1] : 0);
}
}
// Connect selectedNode's output to newNode's input
selectedNode.connect(selectedNodeType[1], newNode, newNodeType[1]);
}
break;
}
return newNode;
}
function createScriptEntry(node, scriptType) {
return {
content: scriptType,
callback: function() {
addAndConnectScriptNode(scriptType, node);
},
};
}
function getScriptOptions(nodeType, node) {
const allScriptTypes = [
"XY Plot",
"Noise Control Script",
"HighRes-Fix Script",
"Tiled Upscaler Script",
"AnimateDiff Script"
];
// Filter script types based on node type
const scriptTypes = allScriptTypes.filter(scriptType => {
const scriptBehavior = connectionMap[scriptType][0];
if (connectionMap[nodeType][0] === "output") {
return scriptBehavior.includes("input"); // Includes nodes that are "input" or "input & output"
} else {
return true;
}
});
return scriptTypes.map(script => createScriptEntry(node, script));
}
function showAddScriptMenu(_, options, e, menu, node) {
const scriptOptions = getScriptOptions(node.type, node);
new LiteGraph.ContextMenu(scriptOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false;
}
// Extension Definition
app.registerExtension({
name: "efficiency.addScripts",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (connectionMap[nodeData.name]) {
addMenuHandler(nodeType, function(insertOption) {
insertOption({
content: "📜 Add script...",
has_submenu: true,
callback: showAddScriptMenu
});
});
}
},
});

View File

@@ -0,0 +1,89 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler, addNode } from "./common/utils.js";
const nodePxOffsets = 80;
function getXYInputNodes() {
return [
"XY Input: Seeds++ Batch",
"XY Input: Add/Return Noise",
"XY Input: Steps",
"XY Input: CFG Scale",
"XY Input: Sampler/Scheduler",
"XY Input: Denoise",
"XY Input: VAE",
"XY Input: Prompt S/R",
"XY Input: Aesthetic Score",
"XY Input: Refiner On/Off",
"XY Input: Checkpoint",
"XY Input: Clip Skip",
"XY Input: LoRA",
"XY Input: LoRA Plot",
"XY Input: LoRA Stacks",
"XY Input: Control Net",
"XY Input: Control Net Plot",
"XY Input: Manual XY Entry"
];
}
function showAddXYInputMenu(type, e, menu, node) {
const specialNodes = [
"XY Input: LoRA Plot",
"XY Input: Control Net Plot",
"XY Input: Manual XY Entry"
];
const values = getXYInputNodes().map(nodeType => {
return {
content: nodeType,
callback: function() {
const newNode = addNode(nodeType, node);
// Calculate the left shift based on the width of the new node
const shiftX = -(newNode.size[0] + 35);
newNode.pos[0] += shiftX;
if (specialNodes.includes(nodeType)) {
newNode.pos[1] += 20;
// Connect both outputs to the XY Plot's 2nd and 3rd input.
newNode.connect(0, node, 1);
newNode.connect(1, node, 2);
} else if (type === 'X') {
newNode.pos[1] += 20;
newNode.connect(0, node, 1); // Connect to 2nd input
} else {
newNode.pos[1] += node.size[1] + 45;
newNode.connect(0, node, 2); // Connect to 3rd input
}
}
};
});
new LiteGraph.ContextMenu(values, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false;
}
app.registerExtension({
name: "efficiency.addXYinputs",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "XY Plot") {
addMenuHandler(nodeType, function(insertOption) {
insertOption({
content: "✏️ Add 𝚇 input...",
has_submenu: true,
callback: (value, options, e, menu, node) => showAddXYInputMenu('X', e, menu, node)
});
insertOption({
content: "✏️ Add 𝚈 input...",
has_submenu: true,
callback: (value, options, e, menu, node) => showAddXYInputMenu('Y', e, menu, node)
});
});
}
},
});

View File

@@ -0,0 +1,104 @@
.pysssss-model-info {
color: white;
font-family: sans-serif;
max-width: 90vw;
}
.pysssss-model-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
.pysssss-model-info h2 {
text-align: center;
margin: 0 0 10px 0;
}
.pysssss-model-info p {
margin: 5px 0;
}
.pysssss-model-info a {
color: dodgerblue;
}
.pysssss-model-info a:hover {
text-decoration: underline;
}
.pysssss-model-tags-list {
display: flex;
flex-wrap: wrap;
list-style: none;
gap: 10px;
max-height: 200px;
overflow: auto;
margin: 10px 0;
padding: 0;
}
.pysssss-model-tag {
background-color: rgb(128, 213, 247);
color: #000;
display: flex;
align-items: center;
gap: 5px;
border-radius: 5px;
padding: 2px 5px;
cursor: pointer;
}
.pysssss-model-tag--selected span::before {
content: "✅";
position: absolute;
background-color: dodgerblue;
left: 0;
top: 0;
right: 0;
bottom: 0;
text-align: center;
}
.pysssss-model-tag:hover {
outline: 2px solid dodgerblue;
}
.pysssss-model-tag p {
margin: 0;
}
.pysssss-model-tag span {
text-align: center;
border-radius: 5px;
background-color: dodgerblue;
color: #fff;
padding: 2px;
position: relative;
min-width: 20px;
overflow: hidden;
}
.pysssss-model-metadata .comfy-modal-content {
max-width: 100%;
}
.pysssss-model-metadata label {
margin-right: 1ch;
color: #ccc;
}
.pysssss-model-metadata span {
color: dodgerblue;
}
.pysssss-preview {
max-width: 50%;
margin-left: 10px;
position: relative;
}
.pysssss-preview img {
max-height: 300px;
}
.pysssss-preview button {
position: absolute;
font-size: 12px;
bottom: 10px;
right: 10px;
}
.pysssss-model-notes {
background-color: rgba(0, 0, 0, 0.25);
padding: 5px;
margin-top: 5px;
}
.pysssss-model-notes:empty {
display: none;
}

View File

@@ -0,0 +1,303 @@
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../../scripts/api.js";
import { addStylesheet } from "./utils.js";
addStylesheet(import.meta.url);
class MetadataDialog extends ComfyDialog {
constructor() {
super();
this.element.classList.add("pysssss-model-metadata");
}
show(metadata) {
super.show(
$el(
"div",
Object.keys(metadata).map((k) =>
$el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })])
)
)
);
}
}
export class ModelInfoDialog extends ComfyDialog {
constructor(name) {
super();
this.name = name;
this.element.classList.add("pysssss-model-info");
}
get customNotes() {
return this.metadata["pysssss.notes"];
}
set customNotes(v) {
this.metadata["pysssss.notes"] = v;
}
get hash() {
return this.metadata["pysssss.sha256"];
}
async show(type, value) {
this.type = type;
const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`));
this.info = $el("div", { style: { flex: "auto" } });
this.img = $el("img", { style: { display: "none" } });
this.imgWrapper = $el("div.pysssss-preview", [this.img]);
this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]);
this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]);
const loading = $el("div", { textContent: " Loading...", parent: this.content });
super.show(this.content);
this.metadata = await (await req).json();
this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = "";
this.viewMetadata.removeAttribute("disabled");
loading.remove();
this.addInfo();
}
createButtons() {
const btns = super.createButtons();
this.viewMetadata = $el("button", {
type: "button",
textContent: "View raw metadata",
disabled: "disabled",
style: {
opacity: 0.5,
cursor: "not-allowed",
},
onclick: (e) => {
if (this.metadata) {
new MetadataDialog().show(this.metadata);
}
},
});
btns.unshift(this.viewMetadata);
return btns;
}
getNoteInfo() {
function parseNote() {
if (!this.customNotes) return [];
let notes = [];
// Extract links from notes
const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
let end = 0;
let m;
do {
m = r.exec(this.customNotes);
let pos;
let fin = 0;
if (m) {
pos = m.index;
fin = m.index + m[0].length;
} else {
pos = this.customNotes.length;
}
let pre = this.customNotes.substring(end, pos);
if (pre) {
pre = pre.replaceAll("\n", "<br>");
notes.push(
$el("span", {
innerHTML: pre,
})
);
}
if (m) {
notes.push(
$el("a", {
href: m[0],
textContent: m[0],
target: "_blank",
})
);
}
end = fin;
} while (m);
return notes;
}
let textarea;
let notesContainer;
const editText = "✏️ Edit";
const edit = $el("a", {
textContent: editText,
href: "#",
style: {
float: "right",
color: "greenyellow",
textDecoration: "none",
},
onclick: async (e) => {
e.preventDefault();
if (textarea) {
this.customNotes = textarea.value;
const resp = await api.fetchApi(
"/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
{
method: "POST",
body: this.customNotes,
}
);
if (resp.status !== 200) {
console.error(resp);
alert(`Error saving notes (${req.status}) ${req.statusText}`);
return;
}
e.target.textContent = editText;
textarea.remove();
textarea = null;
notesContainer.replaceChildren(...parseNote.call(this));
} else {
e.target.textContent = "💾 Save";
textarea = $el("textarea", {
style: {
width: "100%",
minWidth: "200px",
minHeight: "50px",
},
textContent: this.customNotes,
});
e.target.after(textarea);
notesContainer.replaceChildren();
textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px";
}
},
});
notesContainer = $el("div.pysssss-model-notes", parseNote.call(this));
return $el(
"div",
{
style: { display: "contents" },
},
[edit, notesContainer]
);
}
addInfo() {
this.addInfoEntry("Notes", this.getNoteInfo());
}
addInfoEntry(name, value) {
return $el(
"p",
{
parent: this.info,
},
[
typeof name === "string" ? $el("label", { textContent: name + ": " }) : name,
typeof value === "string" ? $el("span", { textContent: value }) : value,
]
);
}
async getCivitaiDetails() {
const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash);
if (req.status === 200) {
return await req.json();
} else if (req.status === 404) {
throw new Error("Model not found");
} else {
throw new Error(`Error loading info (${req.status}) ${req.statusText}`);
}
}
addCivitaiInfo() {
const promise = this.getCivitaiDetails();
const content = $el("span", { textContent: " Loading..." });
this.addInfoEntry(
$el("label", [
$el("img", {
style: {
width: "18px",
position: "relative",
top: "3px",
margin: "0 5px 0 0",
},
src: "https://civitai.com/favicon.ico",
}),
$el("span", { textContent: "Civitai: " }),
]),
content
);
return promise
.then((info) => {
content.replaceChildren(
$el("a", {
href: "https://civitai.com/models/" + info.modelId,
textContent: "View " + info.model.name,
target: "_blank",
})
);
if (info.images?.length) {
this.img.src = info.images[0].url;
this.img.style.display = "";
this.imgSave = $el("button", {
textContent: "Use as preview",
parent: this.imgWrapper,
onclick: async () => {
// Convert the preview to a blob
const blob = await (await fetch(this.img.src)).blob();
// Store it in temp
const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1];
const body = new FormData();
body.append("image", new File([blob], name));
body.append("overwrite", "true");
body.append("type", "temp");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
console.error(resp);
alert(`Error saving preview (${req.status}) ${req.statusText}`);
return;
}
// Use as preview
await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
method: "POST",
body: JSON.stringify({
filename: name,
type: "temp",
}),
headers: {
"content-type": "application/json",
},
});
app.refreshComboInNodes();
},
});
}
return info;
})
.catch((err) => {
content.textContent = "⚠️ " + err.message;
});
}
}

View File

@@ -0,0 +1,94 @@
import { app } from '../../../../scripts/app.js'
import { $el } from "../../../../scripts/ui.js";
export function addStylesheet(url) {
if (url.endsWith(".js")) {
url = url.substr(0, url.length - 2) + "css";
}
$el("link", {
parent: document.head,
rel: "stylesheet",
type: "text/css",
href: url.startsWith("http") ? url : getUrl(url),
});
}
export function getUrl(path, baseUrl) {
if (baseUrl) {
return new URL(path, baseUrl).toString();
} else {
return new URL("../" + path, import.meta.url).toString();
}
}
export async function loadImage(url) {
return new Promise((res, rej) => {
const img = new Image();
img.onload = res;
img.onerror = rej;
img.src = url;
});
}
export function addMenuHandler(nodeType, cb) {
const GROUPED_MENU_ORDER = {
"🔄 Swap with...": 0,
"⛓ Add link...": 1,
"📜 Add script...": 2,
"🔍 View model info...": 3,
"🌱 Seed behavior...": 4,
"📐 Set Resolution...": 5,
"✏️ Add 𝚇 input...": 6,
"✏️ Add 𝚈 input...": 7
};
const originalGetOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
let r = originalGetOpts ? originalGetOpts.apply(this, arguments) || [] : [];
const insertOption = (option) => {
if (GROUPED_MENU_ORDER.hasOwnProperty(option.content)) {
// Find the right position for the option
let targetPos = r.length; // default to the end
for (let i = 0; i < r.length; i++) {
if (GROUPED_MENU_ORDER.hasOwnProperty(r[i].content) &&
GROUPED_MENU_ORDER[option.content] < GROUPED_MENU_ORDER[r[i].content]) {
targetPos = i;
break;
}
}
// Insert the option at the determined position
r.splice(targetPos, 0, option);
} else {
// If the option is not in the GROUPED_MENU_ORDER, simply add it to the end
r.push(option);
}
};
cb.call(this, insertOption);
return r;
};
}
export function findWidgetByName(node, widgetName) {
return node.widgets.find(widget => widget.name === widgetName);
}
// Utility functions
export function addNode(name, nextTo, options) {
options = { select: true, shiftX: 0, shiftY: 0, before: false, ...(options || {}) };
const node = LiteGraph.createNode(name);
app.graph.add(node);
node.pos = [
nextTo.pos[0] + options.shiftX,
nextTo.pos[1] + options.shiftY,
];
if (options.select) {
app.canvas.selectNode(node, false);
}
return node;
}

View File

@@ -0,0 +1,336 @@
import { app } from "../../../scripts/app.js";
import { $el } from "../../../scripts/ui.js";
import { ModelInfoDialog } from "./common/modelInfoDialog.js";
import { addMenuHandler } from "./common/utils.js";
const MAX_TAGS = 500;
class LoraInfoDialog extends ModelInfoDialog {
getTagFrequency() {
if (!this.metadata.ss_tag_frequency) return [];
const datasets = JSON.parse(this.metadata.ss_tag_frequency);
const tags = {};
for (const setName in datasets) {
const set = datasets[setName];
for (const t in set) {
if (t in tags) {
tags[t] += set[t];
} else {
tags[t] = set[t];
}
}
}
return Object.entries(tags).sort((a, b) => b[1] - a[1]);
}
getResolutions() {
let res = [];
if (this.metadata.ss_bucket_info) {
const parsed = JSON.parse(this.metadata.ss_bucket_info);
if (parsed?.buckets) {
for (const { resolution, count } of Object.values(parsed.buckets)) {
res.push([count, `${resolution.join("x")} * ${count}`]);
}
}
}
res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]);
let r = this.metadata.ss_resolution;
if (r) {
const s = r.split(",");
const w = s[0].replace("(", "");
const h = s[1].replace(")", "");
res.push(`${w.trim()}x${h.trim()} (Base res)`);
} else if ((r = this.metadata["modelspec.resolution"])) {
res.push(r + " (Base res");
}
if (!res.length) {
res.push("⚠️ Unknown");
}
return res;
}
getTagList(tags) {
return tags.map((t) =>
$el(
"li.pysssss-model-tag",
{
dataset: {
tag: t[0],
},
$: (el) => {
el.onclick = () => {
el.classList.toggle("pysssss-model-tag--selected");
};
},
},
[
$el("p", {
textContent: t[0],
}),
$el("span", {
textContent: t[1],
}),
]
)
);
}
addTags() {
let tags = this.getTagFrequency();
let hasMore;
if (tags?.length) {
const c = tags.length;
let list;
if (c > MAX_TAGS) {
tags = tags.slice(0, MAX_TAGS);
hasMore = $el("p", [
$el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }),
$el("a", {
href: "#",
textContent: `Show all ${c}`,
onclick: () => {
list.replaceChildren(...this.getTagList(this.getTagFrequency()));
hasMore.remove();
},
}),
]);
}
list = $el("ol.pysssss-model-tags-list", this.getTagList(tags));
this.tags = $el("div", [list]);
} else {
this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" });
}
this.content.append(this.tags);
if (hasMore) {
this.content.append(hasMore);
}
}
async addInfo() {
this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
this.addInfoEntry(
"Resolution",
$el(
"select",
this.getResolutions().map((r) => $el("option", { textContent: r }))
)
);
super.addInfo();
const p = this.addCivitaiInfo();
this.addTags();
const info = await p;
if (info) {
$el(
"p",
{
parent: this.content,
textContent: "Trained Words: ",
},
[
$el("pre", {
textContent: info.trainedWords.join(", "),
style: {
whiteSpace: "pre-wrap",
margin: "10px 0",
background: "#222",
padding: "5px",
borderRadius: "5px",
maxHeight: "250px",
overflow: "auto",
},
}),
]
);
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
maxHeight: "250px",
overflow: "auto",
},
});
}
}
createButtons() {
const btns = super.createButtons();
function copyTags(e, tags) {
const textarea = $el("textarea", {
parent: document.body,
style: {
position: "fixed",
},
textContent: tags.map((el) => el.dataset.tag).join(", "),
});
textarea.select();
try {
document.execCommand("copy");
if (!e.target.dataset.text) {
e.target.dataset.text = e.target.textContent;
}
e.target.textContent = "Copied " + tags.length + " tags";
setTimeout(() => {
e.target.textContent = e.target.dataset.text;
}, 1000);
} catch (ex) {
prompt("Copy to clipboard: Ctrl+C, Enter", text);
} finally {
document.body.removeChild(textarea);
}
}
btns.unshift(
$el("button", {
type: "button",
textContent: "Copy Selected",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag--selected")]);
},
}),
$el("button", {
type: "button",
textContent: "Copy All",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".pysssss-model-tag")]);
},
})
);
return btns;
}
}
class CheckpointInfoDialog extends ModelInfoDialog {
async addInfo() {
super.addInfo();
const info = await this.addCivitaiInfo();
if (info) {
this.addInfoEntry("Base Model", info.baseModel || "⚠️ Unknown");
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
maxHeight: "250px",
overflow: "auto",
},
});
}
}
}
const generateNames = (prefix, start, end) => {
const result = [];
if (start < end) {
for (let i = start; i <= end; i++) {
result.push(`${prefix}${i}`);
}
} else {
for (let i = start; i >= end; i--) {
result.push(`${prefix}${i}`);
}
}
return result
}
// NOTE: Orders reversed so they appear in ascending order
const infoHandler = {
"Efficient Loader": {
"loras": ["lora_name"],
"checkpoints": ["ckpt_name"]
},
"Eff. Loader SDXL": {
"checkpoints": ["refiner_ckpt_name", "base_ckpt_name"]
},
"LoRA Stacker": {
"loras": generateNames("lora_name_", 50, 1)
},
"XY Input: LoRA": {
"loras": generateNames("lora_name_", 50, 1)
},
"HighRes-Fix Script": {
"checkpoints": ["hires_ckpt_name"]
}
};
// Utility functions and other parts of your code remain unchanged
app.registerExtension({
name: "efficiency.ModelInfo",
beforeRegisterNodeDef(nodeType) {
const types = infoHandler[nodeType.comfyClass];
if (types) {
addMenuHandler(nodeType, function (insertOption) { // Here, we are calling addMenuHandler
let submenuItems = []; // to store submenu items
const addSubMenuOption = (type, widgetNames) => {
widgetNames.forEach(widgetName => {
const widgetValue = this.widgets.find(w => w.name === widgetName)?.value;
// Check if widgetValue is "None"
if (!widgetValue || widgetValue === "None") {
return;
}
let value = widgetValue;
if (value.content) {
value = value.content;
}
const cls = type === "loras" ? LoraInfoDialog : CheckpointInfoDialog;
const label = widgetName;
// Push to submenuItems
submenuItems.push({
content: label,
callback: async () => {
new cls(value).show(type, value);
},
});
});
};
if (typeof types === 'object') {
Object.keys(types).forEach(type => {
addSubMenuOption(type, types[type]);
});
}
// If we have submenu items, use insertOption
if (submenuItems.length) {
insertOption({ // Using insertOption here
content: "🔍 View model info...",
has_submenu: true,
callback: (value, options, e, menu, node) => {
new LiteGraph.ContextMenu(submenuItems, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false; // This ensures the original context menu doesn't proceed
}
});
}
});
}
},
});

View File

@@ -0,0 +1,88 @@
// Additional functions and imports
import { app } from "../../../scripts/app.js";
import { addMenuHandler, findWidgetByName } from "./common/utils.js";
// A mapping for resolutions based on the type of the loader
const RESOLUTIONS = {
"Efficient Loader": [
{width: 512, height: 512},
{width: 512, height: 768},
{width: 512, height: 640},
{width: 640, height: 512},
{width: 640, height: 768},
{width: 640, height: 640},
{width: 768, height: 512},
{width: 768, height: 768},
{width: 768, height: 640},
],
"Eff. Loader SDXL": [
{width: 1024, height: 1024},
{width: 1152, height: 896},
{width: 896, height: 1152},
{width: 1216, height: 832},
{width: 832, height: 1216},
{width: 1344, height: 768},
{width: 768, height: 1344},
{width: 1536, height: 640},
{width: 640, height: 1536}
]
};
// Function to set the resolution of a node
function setNodeResolution(node, width, height) {
let widthWidget = findWidgetByName(node, "empty_latent_width");
let heightWidget = findWidgetByName(node, "empty_latent_height");
if (widthWidget) {
widthWidget.value = width;
}
if (heightWidget) {
heightWidget.value = height;
}
}
// The callback for the resolution submenu
function resolutionMenuCallback(node, width, height) {
return function() {
setNodeResolution(node, width, height);
};
}
// Show the set resolution submenu
function showResolutionMenu(value, options, e, menu, node) {
const resolutions = RESOLUTIONS[node.type];
if (!resolutions) {
return false;
}
const resolutionOptions = resolutions.map(res => ({
content: `${res.width} x ${res.height}`,
callback: resolutionMenuCallback(node, res.width, res.height)
}));
new LiteGraph.ContextMenu(resolutionOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false; // This ensures the original context menu doesn't proceed
}
// Extension Definition
app.registerExtension({
name: "efficiency.SetResolution",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
addMenuHandler(nodeType, function (insertOption) {
insertOption({
content: "📐 Set Resolution...",
has_submenu: true,
callback: showResolutionMenu
});
});
}
},
});

View File

@@ -0,0 +1,135 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
import { findWidgetByName } from "./common/utils.js";
function replaceNode(oldNode, newNodeName) {
const newNode = LiteGraph.createNode(newNodeName);
if (!newNode) {
return;
}
app.graph.add(newNode);
newNode.pos = oldNode.pos.slice();
newNode.size = oldNode.size.slice();
// Transfer widget values
const widgetMapping = {
"ckpt_name": "base_ckpt_name",
"vae_name": "vae_name",
"clip_skip": "base_clip_skip",
"positive": "positive",
"negative": "negative",
"prompt_style": "prompt_style",
"empty_latent_width": "empty_latent_width",
"empty_latent_height": "empty_latent_height",
"batch_size": "batch_size"
};
let effectiveWidgetMapping = widgetMapping;
// Invert the mapping when going from "Eff. Loader SDXL" to "Efficient Loader"
if (oldNode.type === "Eff. Loader SDXL" && newNodeName === "Efficient Loader") {
effectiveWidgetMapping = {};
for (const [key, value] of Object.entries(widgetMapping)) {
effectiveWidgetMapping[value] = key;
}
}
oldNode.widgets.forEach(widget => {
const newName = effectiveWidgetMapping[widget.name];
if (newName) {
const newWidget = findWidgetByName(newNode, newName);
if (newWidget) {
newWidget.value = widget.value;
}
}
});
// Hardcoded transfer for specific outputs based on the output names from the nodes in the image
const outputMapping = {
"MODEL": null, // Not present in "Eff. Loader SDXL"
"CONDITIONING+": null, // Not present in "Eff. Loader SDXL"
"CONDITIONING-": null, // Not present in "Eff. Loader SDXL"
"LATENT": "LATENT",
"VAE": "VAE",
"CLIP": null, // Not present in "Eff. Loader SDXL"
"DEPENDENCIES": "DEPENDENCIES"
};
// Transfer connections from old node outputs to new node outputs based on the outputMapping
oldNode.outputs.forEach((output, index) => {
if (output && output.links && outputMapping[output.name]) {
const newOutputName = outputMapping[output.name];
// If the new node does not have this output, skip
if (newOutputName === null) {
return;
}
const newOutputIndex = newNode.findOutputSlot(newOutputName);
if (newOutputIndex !== -1) {
output.links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot);
}
}
});
}
}
});
// Remove old node
app.graph.remove(oldNode);
}
function replaceNodeMenuCallback(currentNode, targetNodeName) {
return function() {
replaceNode(currentNode, targetNodeName);
};
}
function showSwapMenu(value, options, e, menu, node) {
const swapOptions = [];
if (node.type !== "Efficient Loader") {
swapOptions.push({
content: "Efficient Loader",
callback: replaceNodeMenuCallback(node, "Efficient Loader")
});
}
if (node.type !== "Eff. Loader SDXL") {
swapOptions.push({
content: "Eff. Loader SDXL",
callback: replaceNodeMenuCallback(node, "Eff. Loader SDXL")
});
}
new LiteGraph.ContextMenu(swapOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false; // This ensures the original context menu doesn't proceed
}
// Extension Definition
app.registerExtension({
name: "efficiency.SwapLoaders",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["Efficient Loader", "Eff. Loader SDXL"].includes(nodeData.name)) {
addMenuHandler(nodeType, function (insertOption) {
insertOption({
content: "🔄 Swap with...",
has_submenu: true,
callback: showSwapMenu
});
});
}
},
});

View File

@@ -0,0 +1,191 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
import { findWidgetByName } from "./common/utils.js";
function replaceNode(oldNode, newNodeName) {
// Create new node
const newNode = LiteGraph.createNode(newNodeName);
if (!newNode) {
return;
}
app.graph.add(newNode);
// Position new node at the same position as the old node
newNode.pos = oldNode.pos.slice();
// Define widget mappings
const mappings = {
"KSampler (Efficient) <-> KSampler Adv. (Efficient)": {
seed: "noise_seed",
cfg: "cfg",
sampler_name: "sampler_name",
scheduler: "scheduler",
preview_method: "preview_method",
vae_decode: "vae_decode"
},
"KSampler (Efficient) <-> KSampler SDXL (Eff.)": {
seed: "noise_seed",
cfg: "cfg",
sampler_name: "sampler_name",
scheduler: "scheduler",
preview_method: "preview_method",
vae_decode: "vae_decode"
},
"KSampler Adv. (Efficient) <-> KSampler SDXL (Eff.)": {
noise_seed: "noise_seed",
steps: "steps",
cfg: "cfg",
sampler_name: "sampler_name",
scheduler: "scheduler",
start_at_step: "start_at_step",
preview_method: "preview_method",
vae_decode: "vae_decode"}
};
const swapKey = `${oldNode.type} <-> ${newNodeName}`;
let widgetMapping = {};
// Check if a reverse mapping is needed
if (!mappings[swapKey]) {
const reverseKey = `${newNodeName} <-> ${oldNode.type}`;
const reverseMapping = mappings[reverseKey];
if (reverseMapping) {
widgetMapping = Object.entries(reverseMapping).reduce((acc, [key, value]) => {
acc[value] = key;
return acc;
}, {});
}
} else {
widgetMapping = mappings[swapKey];
}
if (oldNode.type === "KSampler (Efficient)" && (newNodeName === "KSampler Adv. (Efficient)" || newNodeName === "KSampler SDXL (Eff.)")) {
const denoise = Math.min(Math.max(findWidgetByName(oldNode, "denoise").value, 0), 1); // Ensure denoise is between 0 and 1
const steps = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure steps is between 0 and 10000
const total_steps = Math.floor(steps / denoise);
const start_at_step = total_steps - steps;
findWidgetByName(newNode, "steps").value = Math.min(Math.max(total_steps, 0), 10000); // Ensure total_steps is between 0 and 10000
findWidgetByName(newNode, "start_at_step").value = Math.min(Math.max(start_at_step, 0), 10000); // Ensure start_at_step is between 0 and 10000
}
else if ((oldNode.type === "KSampler Adv. (Efficient)" || oldNode.type === "KSampler SDXL (Eff.)") && newNodeName === "KSampler (Efficient)") {
const stepsAdv = Math.min(Math.max(findWidgetByName(oldNode, "steps").value, 0), 10000); // Ensure stepsAdv is between 0 and 10000
const start_at_step = Math.min(Math.max(findWidgetByName(oldNode, "start_at_step").value, 0), 10000); // Ensure start_at_step is between 0 and 10000
const denoise = Math.min(Math.max((stepsAdv - start_at_step) / stepsAdv, 0), 1); // Ensure denoise is between 0 and 1
const stepsTotal = stepsAdv - start_at_step;
findWidgetByName(newNode, "denoise").value = denoise;
findWidgetByName(newNode, "steps").value = Math.min(Math.max(stepsTotal, 0), 10000); // Ensure stepsTotal is between 0 and 10000
}
// Transfer widget values from old node to new node
oldNode.widgets.forEach(widget => {
const newName = widgetMapping[widget.name];
if (newName) {
const newWidget = findWidgetByName(newNode, newName);
if (newWidget) {
newWidget.value = widget.value;
}
}
});
// Determine the starting indices based on the node types
let oldNodeInputStartIndex = 0;
let newNodeInputStartIndex = 0;
let oldNodeOutputStartIndex = 0;
let newNodeOutputStartIndex = 0;
if (oldNode.type === "KSampler SDXL (Eff.)" || newNodeName === "KSampler SDXL (Eff.)") {
oldNodeInputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
newNodeInputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
oldNodeOutputStartIndex = (oldNode.type === "KSampler SDXL (Eff.)") ? 1 : 3;
newNodeOutputStartIndex = (newNodeName === "KSampler SDXL (Eff.)") ? 1 : 3;
}
// Transfer connections from old node to new node
oldNode.inputs.slice(oldNodeInputStartIndex).forEach((input, index) => {
if (input && input.link !== null) {
const originLinkInfo = oldNode.graph.links[input.link];
if (originLinkInfo) {
const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
if (originNode) {
originNode.connect(originLinkInfo.origin_slot, newNode, index + newNodeInputStartIndex);
}
}
}
});
oldNode.outputs.slice(oldNodeOutputStartIndex).forEach((output, index) => {
if (output && output.links) {
output.links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(index + newNodeOutputStartIndex, targetNode, targetLinkInfo.target_slot);
}
}
});
}
});
// Remove old node
app.graph.remove(oldNode);
}
function replaceNodeMenuCallback(currentNode, targetNodeName) {
return function() {
replaceNode(currentNode, targetNodeName);
};
}
function showSwapMenu(value, options, e, menu, node) {
const swapOptions = [];
if (node.type !== "KSampler (Efficient)") {
swapOptions.push({
content: "KSampler (Efficient)",
callback: replaceNodeMenuCallback(node, "KSampler (Efficient)")
});
}
if (node.type !== "KSampler Adv. (Efficient)") {
swapOptions.push({
content: "KSampler Adv. (Efficient)",
callback: replaceNodeMenuCallback(node, "KSampler Adv. (Efficient)")
});
}
if (node.type !== "KSampler SDXL (Eff.)") {
swapOptions.push({
content: "KSampler SDXL (Eff.)",
callback: replaceNodeMenuCallback(node, "KSampler SDXL (Eff.)")
});
}
new LiteGraph.ContextMenu(swapOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false; // This ensures the original context menu doesn't proceed
}
// Extension Definition
app.registerExtension({
name: "efficiency.SwapSamplers",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["KSampler (Efficient)", "KSampler Adv. (Efficient)", "KSampler SDXL (Eff.)"].includes(nodeData.name)) {
addMenuHandler(nodeType, function (insertOption) {
insertOption({
content: "🔄 Swap with...",
has_submenu: true,
callback: showSwapMenu
});
});
}
},
});

View File

@@ -0,0 +1,100 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
function replaceNode(oldNode, newNodeName) {
const newNode = LiteGraph.createNode(newNodeName);
if (!newNode) {
return;
}
app.graph.add(newNode);
newNode.pos = oldNode.pos.slice();
// Transfer connections from old node to new node
// XY Plot and AnimateDiff have only one output
if(["XY Plot", "AnimateDiff Script"].includes(oldNode.type)) {
if (oldNode.outputs[0] && oldNode.outputs[0].links) {
oldNode.outputs[0].links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(0, targetNode, targetLinkInfo.target_slot);
}
}
});
}
} else {
// Noise Control Script, HighRes-Fix Script, and Tiled Upscaler Script have 1 input and 1 output at index 0
if (oldNode.inputs[0] && oldNode.inputs[0].link !== null) {
const originLinkInfo = oldNode.graph.links[oldNode.inputs[0].link];
if (originLinkInfo) {
const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id);
if (originNode) {
originNode.connect(originLinkInfo.origin_slot, newNode, 0);
}
}
}
if (oldNode.outputs[0] && oldNode.outputs[0].links) {
oldNode.outputs[0].links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(0, targetNode, targetLinkInfo.target_slot);
}
}
});
}
}
// Remove old node
app.graph.remove(oldNode);
}
function replaceNodeMenuCallback(currentNode, targetNodeName) {
return function() {
replaceNode(currentNode, targetNodeName);
};
}
function showSwapMenu(value, options, e, menu, node) {
const scriptNodes = [
"XY Plot",
"Noise Control Script",
"HighRes-Fix Script",
"Tiled Upscaler Script",
"AnimateDiff Script"
];
const swapOptions = scriptNodes.filter(n => n !== node.type).map(n => ({
content: n,
callback: replaceNodeMenuCallback(node, n)
}));
new LiteGraph.ContextMenu(swapOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false;
}
// Extension Definition
app.registerExtension({
name: "efficiency.SwapScripts",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["XY Plot", "Noise Control Script", "HighRes-Fix Script", "Tiled Upscaler Script", "AnimateDiff Script"].includes(nodeData.name)) {
addMenuHandler(nodeType, function (insertOption) {
insertOption({
content: "🔄 Swap with...",
has_submenu: true,
callback: showSwapMenu
});
});
}
},
});

View File

@@ -0,0 +1,98 @@
import { app } from "../../../scripts/app.js";
import { addMenuHandler } from "./common/utils.js";
function replaceNode(oldNode, newNodeName) {
const newNode = LiteGraph.createNode(newNodeName);
if (!newNode) {
return;
}
app.graph.add(newNode);
newNode.pos = oldNode.pos.slice();
// Handle the special nodes with two outputs
const nodesWithTwoOutputs = ["XY Input: LoRA Plot", "XY Input: Control Net Plot", "XY Input: Manual XY Entry"];
let outputCount = nodesWithTwoOutputs.includes(oldNode.type) ? 2 : 1;
// Transfer output connections from old node to new node
oldNode.outputs.slice(0, outputCount).forEach((output, index) => {
if (output && output.links) {
output.links.forEach(link => {
const targetLinkInfo = oldNode.graph.links[link];
if (targetLinkInfo) {
const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id);
if (targetNode) {
newNode.connect(index, targetNode, targetLinkInfo.target_slot);
}
}
});
}
});
// Remove old node
app.graph.remove(oldNode);
}
function replaceNodeMenuCallback(currentNode, targetNodeName) {
return function() {
replaceNode(currentNode, targetNodeName);
};
}
function showSwapMenu(value, options, e, menu, node) {
const swapOptions = [];
const xyInputNodes = [
"XY Input: Seeds++ Batch",
"XY Input: Add/Return Noise",
"XY Input: Steps",
"XY Input: CFG Scale",
"XY Input: Sampler/Scheduler",
"XY Input: Denoise",
"XY Input: VAE",
"XY Input: Prompt S/R",
"XY Input: Aesthetic Score",
"XY Input: Refiner On/Off",
"XY Input: Checkpoint",
"XY Input: Clip Skip",
"XY Input: LoRA",
"XY Input: LoRA Plot",
"XY Input: LoRA Stacks",
"XY Input: Control Net",
"XY Input: Control Net Plot",
"XY Input: Manual XY Entry"
];
for (const nodeType of xyInputNodes) {
if (node.type !== nodeType) {
swapOptions.push({
content: nodeType,
callback: replaceNodeMenuCallback(node, nodeType)
});
}
}
new LiteGraph.ContextMenu(swapOptions, {
event: e,
callback: null,
parentMenu: menu,
node: node
});
return false; // This ensures the original context menu doesn't proceed
}
// Extension Definition
app.registerExtension({
name: "efficiency.swapXYinputs",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name.startsWith("XY Input:")) {
addMenuHandler(nodeType, function (insertOption) {
insertOption({
content: "🔄 Swap with...",
has_submenu: true,
callback: showSwapMenu
});
});
}
},
});