482 lines
19 KiB
JavaScript
482 lines
19 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
const getValFromDialog = async (title, message, defaultValue) => {
|
|
return await app.extensionManager.dialog.prompt({
|
|
title,
|
|
message,
|
|
defaultValue,
|
|
});
|
|
}
|
|
const MAX_CHAR_VARNAME = 50;
|
|
function makeValidVariable(
|
|
varName,
|
|
textContent,
|
|
regex = /^[a-z_][a-z0-9_]*$/i
|
|
) {
|
|
if (
|
|
!varName ||
|
|
varName.trim() === "" ||
|
|
varName.length > MAX_CHAR_VARNAME ||
|
|
!regex.test(varName)
|
|
) {
|
|
alert(`${textContent}校验未通过!`);
|
|
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.DynamicParams",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "DynamicParams") {
|
|
// 初始化默认属性
|
|
nodeType.prototype.properties = {
|
|
dynamic_params: []
|
|
};
|
|
|
|
// 保存原始方法
|
|
const origConfigure = nodeType.prototype.configure;
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
|
|
// 添加节点创建方法
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
|
|
|
// 初始化基本属性
|
|
if (!this.initialized) {
|
|
this.initialized = true;
|
|
this.properties = this.properties || {};
|
|
this.properties.dynamic_params = this.properties.dynamic_params || [];
|
|
this.dynamic_params = this.properties.dynamic_params;
|
|
|
|
// 清除现有widgets和连接点
|
|
this.widgets = this.widgets || [];
|
|
this.inputs = this.inputs || [];
|
|
this.outputs = this.outputs || [];
|
|
|
|
// 添加"添加参数"按钮
|
|
this.addWidget("button", "Add Parameter", "add_param", () => {
|
|
this.showAddParamDialog();
|
|
});
|
|
|
|
// 设置节点大小
|
|
this.setSize(this.computeSize());
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
// 修改配置加载方法
|
|
nodeType.prototype.configure = function (info) {
|
|
// 避免重复调用configure
|
|
if (this._configuring) return;
|
|
this._configuring = true;
|
|
|
|
try {
|
|
// 确保properties和dynamic_params存在
|
|
this.properties = this.properties || {};
|
|
this.properties.dynamic_params = this.properties.dynamic_params || [];
|
|
this.dynamic_params = this.properties.dynamic_params;
|
|
|
|
// 调用原始configure
|
|
if (origConfigure) {
|
|
origConfigure.call(this, info);
|
|
}
|
|
|
|
// 合并新的属性
|
|
if (info.properties) {
|
|
this.properties = {
|
|
...this.properties,
|
|
...info.properties,
|
|
dynamic_params: info.properties.dynamic_params || this.properties.dynamic_params
|
|
};
|
|
this.dynamic_params = this.properties.dynamic_params;
|
|
}
|
|
|
|
// 清除现有widgets和连接点
|
|
this.widgets = [];
|
|
this.inputs = [];
|
|
this.outputs = [this.outputs[0]]; // python中RETURN_TYPES有一个默认placeholder占位, 再次加载workflow时直接使用
|
|
|
|
// 添加"Add Parameter"按钮
|
|
this.addWidget("button", "Add Parameter", "add_param", () => {
|
|
this.showAddParamDialog();
|
|
});
|
|
|
|
// 恢复参数
|
|
if (Array.isArray(this.dynamic_params) && this.dynamic_params.length > 0) {
|
|
this.dynamic_params.forEach(param => {
|
|
if (param && param.name && param.type) {
|
|
this.addDynamicParam(param.name, param.type, param.value, param.description, false);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 恢复连接
|
|
if (Array.isArray(info.inputs)) {
|
|
info.inputs.forEach((input, index) => {
|
|
if (this.inputs[index]) {
|
|
this.inputs[index].link = input.link;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(info.outputs)) {
|
|
info.outputs.forEach((output, index) => {
|
|
if (this.outputs[index]) {
|
|
this.outputs[index].links = output.links || [];
|
|
}
|
|
});
|
|
}
|
|
|
|
// 更新widget状态
|
|
if (Array.isArray(this.inputs)) {
|
|
this.inputs.forEach(input => {
|
|
if (input && input.link != null && Array.isArray(this.widgets)) {
|
|
const widget = this.widgets.find(w => w && w.options && w.options.nameLinkable === input.name);
|
|
if (widget) {
|
|
widget.disabled = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
this.setSize(this.computeSize());
|
|
} finally {
|
|
this._configuring = false;
|
|
}
|
|
};
|
|
|
|
// 添加参数对话框
|
|
nodeType.prototype.showAddParamDialog = async function () {
|
|
const outputLen = this?.outputs?.length;
|
|
const defaultVarValue = outputLen ? `var${outputLen + 1}` : "var1";
|
|
const name = await getValFromDialog("Variable", "Enter output variable name:", defaultVarValue);
|
|
|
|
if (!makeValidVariable(name, "名称")) {
|
|
return;
|
|
}
|
|
// 检查参数名是否已存在
|
|
if (Array.isArray(this.dynamic_params) &&
|
|
this.dynamic_params.some(p => p && p.name === name)) {
|
|
alert("变量名称已经存在");
|
|
return;
|
|
}
|
|
|
|
const type = await getValFromDialog("Type", "Enter output variable type:", "STRING");
|
|
if (!makeValidVariable(type, "类型", /^[*a-z_][a-z0-9_]*$/i)) {
|
|
return;
|
|
}
|
|
|
|
const defaultValue = await getValFromDialog("Default Value", "Enter output variable Default Value:", "");
|
|
if (defaultValue === null) return;
|
|
|
|
const description = await getValFromDialog("description", "Enter parameter description (optional):", "");
|
|
if (description === null) return;
|
|
|
|
this.addDynamicParam(name, type.toUpperCase(), defaultValue, description, true);
|
|
};
|
|
|
|
// 添加参数方法
|
|
nodeType.prototype.addDynamicParam = function (name, type, defaultValue, description, updateStorage = true) {
|
|
const validTypes = ["STRING", "INT", "FLOAT"];
|
|
if (!validTypes.includes(type)) {
|
|
if (updateStorage) {
|
|
console.warn("Invalid parameter type. Must be STRING, INT, or FLOAT");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 确保dynamic_params是数组
|
|
if (!Array.isArray(this.dynamic_params)) {
|
|
this.dynamic_params = [];
|
|
}
|
|
|
|
// 创建参数配置
|
|
const param = {
|
|
name,
|
|
type,
|
|
value: defaultValue,
|
|
description: description || ""
|
|
};
|
|
|
|
if (updateStorage) {
|
|
this.dynamic_params.push(param);
|
|
this.properties.dynamic_params = this.dynamic_params;
|
|
}
|
|
|
|
// 创建widget
|
|
let widget;
|
|
const widgetConfig = {
|
|
tooltip: description,
|
|
linkable: true,
|
|
nameLinkable: name
|
|
};
|
|
|
|
switch (type) {
|
|
case "INT":
|
|
widget = this.addWidget("number", name, parseInt(defaultValue) || 0,
|
|
(v) => this.updateParamValue(name, v),
|
|
{ ...widgetConfig, precision: 0 });
|
|
break;
|
|
case "FLOAT":
|
|
widget = this.addWidget("number", name, parseFloat(defaultValue) || 0.0,
|
|
(v) => this.updateParamValue(name, v),
|
|
{ ...widgetConfig, precision: 3 });
|
|
break;
|
|
default: // STRING
|
|
widget = this.addWidget("string", name, defaultValue || "",
|
|
(v) => this.updateParamValue(name, v),
|
|
widgetConfig);
|
|
}
|
|
|
|
// 添加input和output
|
|
this.addInput(name, type, { widget: widget });
|
|
this.addOutput(name, type);
|
|
};
|
|
|
|
// 更新参数值
|
|
nodeType.prototype.updateParamValue = function (name, value) {
|
|
if (!Array.isArray(this.dynamic_params)) return;
|
|
|
|
const param = this.dynamic_params.find(p => p && p.name === name);
|
|
if (param) {
|
|
param.value = value;
|
|
this.properties.dynamic_params = this.dynamic_params;
|
|
this.setDirtyCanvas(true, false);
|
|
}
|
|
};
|
|
|
|
// 删除参数方法
|
|
nodeType.prototype.removeParam = function (name) {
|
|
// 找到参数索引
|
|
const paramIndex = this.dynamic_params.findIndex(p => p.name === name);
|
|
if (paramIndex === -1) return;
|
|
|
|
// 删除参数
|
|
this.dynamic_params.splice(paramIndex, 1);
|
|
this.properties.dynamic_params = this.dynamic_params;
|
|
|
|
// 找到并删除相关的widget
|
|
const widgetIndex = this.widgets.findIndex(w => w.name === name);
|
|
if (widgetIndex !== -1) {
|
|
this.widgets.splice(widgetIndex, 1);
|
|
}
|
|
|
|
// 找到并删除相关的input和其连接
|
|
const inputIndex = this.inputs.findIndex(input => input.name === name);
|
|
if (inputIndex !== -1) {
|
|
// 如果input有连接,先删除连接
|
|
const input = this.inputs[inputIndex];
|
|
if (input.link !== null) {
|
|
const graph = app.graph;
|
|
if (graph) {
|
|
graph.removeLink(input.link);
|
|
}
|
|
}
|
|
// 删除input
|
|
this.inputs.splice(inputIndex, 1);
|
|
}
|
|
|
|
// 找到并删除相关的output和其连接
|
|
const outputIndex = this.outputs.findIndex(output => output.name === name);
|
|
if (outputIndex !== -1) {
|
|
// 如果output有连接,先删除所有连接
|
|
const output = this.outputs[outputIndex];
|
|
if (output.links && output.links.length > 0) {
|
|
const graph = app.graph;
|
|
if (graph) {
|
|
[...output.links].forEach(linkId => {
|
|
graph.removeLink(linkId);
|
|
});
|
|
}
|
|
}
|
|
// 删除output
|
|
this.outputs.splice(outputIndex, 1);
|
|
}
|
|
|
|
// 更新工作流配置
|
|
if (this.properties.connections) {
|
|
// 删除input连接配置
|
|
if (this.properties.connections.inputs) {
|
|
this.properties.connections.inputs = this.properties.connections.inputs.filter(
|
|
input => input.name !== name
|
|
);
|
|
}
|
|
|
|
// 删除output连接配置
|
|
if (this.properties.connections.outputs) {
|
|
this.properties.connections.outputs = this.properties.connections.outputs.filter(
|
|
output => output.name !== name
|
|
);
|
|
}
|
|
}
|
|
|
|
// 删除widget值
|
|
if (this.widgets_values) {
|
|
const valueIndex = this.widgets.findIndex(w => w.name === name);
|
|
if (valueIndex !== -1) {
|
|
this.widgets_values.splice(valueIndex, 1);
|
|
}
|
|
}
|
|
|
|
// 更新序列化数据
|
|
this.onSerialize({
|
|
properties: this.properties
|
|
});
|
|
|
|
// 重新配置节点
|
|
this.setDirtyCanvas(true, true);
|
|
this.setSize(this.computeSize());
|
|
|
|
// 通知图表已修改
|
|
if (app.graph) {
|
|
app.graph.setDirtyCanvas(true, true);
|
|
app.graph.change();
|
|
}
|
|
};
|
|
|
|
// 序列化方法
|
|
nodeType.prototype.onSerialize = function (o) {
|
|
if (!o.properties) {
|
|
o.properties = {};
|
|
}
|
|
// 更新dynamic_params
|
|
o.properties.dynamic_params = this.dynamic_params;
|
|
|
|
// 更新连接信息
|
|
o.properties.connections = {
|
|
inputs: this.inputs.map(input => ({
|
|
name: input.name,
|
|
link: input.link,
|
|
type: input.type
|
|
})),
|
|
outputs: this.outputs.map(output => ({
|
|
name: output.name,
|
|
links: output.links,
|
|
type: output.type
|
|
}))
|
|
};
|
|
};
|
|
|
|
// 反序列化方法
|
|
nodeType.prototype.onConfigure = function (o) {
|
|
if (o.properties) {
|
|
this.properties = o.properties;
|
|
if (Array.isArray(o.properties.dynamic_params)) {
|
|
this.dynamic_params = o.properties.dynamic_params;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 保存原始的getExtraMenuOptions方法
|
|
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
|
|
|
// 重写getExtraMenuOptions方法
|
|
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
|
// 调用原始方法
|
|
if (origGetExtraMenuOptions) {
|
|
origGetExtraMenuOptions.apply(this, arguments);
|
|
}
|
|
|
|
// 如果没有动态参数,直接返回
|
|
if (!this.dynamic_params || !this.dynamic_params.length) {
|
|
return options;
|
|
}
|
|
|
|
// 添加分隔符
|
|
options.push(null);
|
|
|
|
// 为每个动态参数添加删除选项
|
|
this.dynamic_params.forEach(param => {
|
|
if (param && param.name) {
|
|
options.push({
|
|
content: `Delete Parameter "${param.name}"`,
|
|
callback: () => {
|
|
if (confirm(`Are you sure you want to delete parameter "${param.name}"?`)) {
|
|
this.removeParam(param.name);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// return options;
|
|
};
|
|
|
|
// 添加处理 widget 输入值的方法
|
|
nodeType.prototype.onWidgetChanged = function (name, value, old_value, widget) {
|
|
if (widget && widget.type === "*") {
|
|
// 如果是从 Primitive Node 传入的值,优先使用该值
|
|
return value;
|
|
}
|
|
return old_value;
|
|
};
|
|
|
|
// 处理连接状态变化
|
|
nodeType.prototype.onConnectionsChange = function (slotType, slot, isConnected, link_info, ioSlot) {
|
|
if (!ioSlot || slotType !== LiteGraph.INPUT || !Array.isArray(this.widgets)) return;
|
|
|
|
const widget = this.widgets.find(w => w && w.options && w.options.nameLinkable === ioSlot.name);
|
|
if (widget) {
|
|
widget.disabled = isConnected;
|
|
}
|
|
};
|
|
|
|
// 处理输入值更新
|
|
nodeType.prototype.onExecute = function () {
|
|
for (const widget of this.widgets) {
|
|
if (!widget.options?.nameLinkable) continue;
|
|
|
|
const input = this.inputs.find(input => input.name === widget.options.nameLinkable);
|
|
if (input?.link !== null) {
|
|
const value = this.getInputData(input.slot);
|
|
if (value !== undefined) {
|
|
widget.value = value;
|
|
this.updateParamValue(widget.options.nameLinkable, value);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 绘制连接点
|
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
|
if (this.flags.collapsed) return;
|
|
|
|
// 不再绘制外部连接点
|
|
// 如果需要其他UI元素的绘制,可以在这里添加
|
|
};
|
|
|
|
// 由于移除了外部连接点,相应的鼠标交互逻辑也需要移除
|
|
nodeType.prototype.onMouseDown = function (e, localPos) {
|
|
// 不再处理外部连接点的点击
|
|
return false;
|
|
};
|
|
|
|
// 处理输入值变化
|
|
nodeType.prototype.onInputChanged = function (slot, value) {
|
|
const input = this.inputs[slot];
|
|
if (!input) return;
|
|
|
|
const widget = this.widgets.find(w => w.options.nameLinkable === input.name);
|
|
if (widget) {
|
|
widget.value = value;
|
|
this.updateParamValue(input.name, value);
|
|
}
|
|
};
|
|
|
|
// 切换参数模式
|
|
nodeType.prototype.toggleParamMode = function (name) {
|
|
const param = this.dynamic_params.find(p => p.name === name);
|
|
if (!param) return;
|
|
|
|
param.widget_mode = !param.widget_mode;
|
|
|
|
// 重新配置节点以更新UI
|
|
this.onConfigure({ properties: this.properties });
|
|
};
|
|
}
|
|
}
|
|
});
|