Files
yorkane-ComfyUI-KYNode/web/python-editor.js.bak
T
2025-11-23 15:06:24 +08:00

500 lines
15 KiB
Plaintext

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// Constants
const MAX_CHAR_VARNAME = 50;
const LIST_THEMES = ["monokai"];
const varTypes = [
"int",
"boolean",
"string",
"float",
"json",
"list",
"dict",
];
const typeMap = {
int: 'int',
boolean: 'bool',
string: 'str',
float: 'float',
json: 'json',
list: 'list',
dict: 'dict',
}
const DEFAULT_TEMPLATES = {
py: `import re, json, os, traceback
from time import strftime
def runCode():
nowDataTime = strftime("%Y-%m-%d %H:%M:%S")
return f"Hello ComfyUI with us today {nowDataTime}!"
r0 = runCode()
`,
};
function getPostition(node, ctx, w_width, y, n_height) {
const margin = 5;
const rect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
return {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e + rect.left}px`,
top: `${transform.d + transform.f + rect.top}px`,
maxWidth: `${w_width - margin * 2}px`,
maxHeight: `${n_height - margin * 2 - y - 15}px`,
width: `${w_width - margin * 2}px`,
height: "90%",
position: "absolute",
scrollbarColor: "var(--descrip-text) var(--bg-color)",
scrollbarWidth: "thin",
zIndex: app.graph._nodes.indexOf(node),
};
}
function findWidget(node, value, attr = "name", func = "find") {
return node?.widgets
? node.widgets[func]((w) =>
Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value
)
: null;
}
// Create editor code
function codeEditor(node, inputName, inputData) {
const widget = {
type: "pycode",
name: inputName,
options: { hideOnZoom: true },
value:
inputData[1]?.default ||
`def my(a, b=1):
return a * b<br>
r0 = str(my(23, 9))`,
draw(ctx, node, widget_width, y, widget_height) {
const hidden =
node.flags?.collapsed ||
(!!widget.options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.type === "converted-widget" ||
widget.type === "hidden";
widget.codeElement.hidden = hidden;
if (hidden) {
widget.options.onHide?.(widget);
return;
}
Object.assign(
this.codeElement.style,
getPostition(node, ctx, widget_width, y, node.size[1])
);
},
computeSize(...args) {
return [500, 250];
},
};
// 创建一个标准的 textarea 元素
widget.codeElement = document.createElement('div');
widget.codeElement.className = 'python-editor-container';
widget.codeElement.style.width = '100%';
widget.codeElement.style.height = '100%';
widget.codeElement.style.position = 'relative';
// 创建 textarea 作为编辑器
const textarea = document.createElement('textarea');
textarea.value = widget.value;
textarea.style.width = '100%';
textarea.style.height = '80%';
textarea.style.fontFamily = 'cursive';
textarea.style.fontSize = '1.1em';
textarea.style.boxSizing = 'border-box';
textarea.style.backgroundColor = '#000';
textarea.style.color = '#2bb356ff';
textarea.style.resize = 'none';
textarea.style.wordBreak = 'break-word';
textarea.wrap = 'soft';
textarea.spellcheck = false;
// 创建错误显示区域
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.style.background = 'rgba(255, 0, 0, 0.1)';
errorDiv.style.color = '#ff4444';
errorDiv.style.padding = '4px';
errorDiv.style.marginTop = '4px';
errorDiv.style.borderRadius = '4px';
errorDiv.style.fontFamily = 'monospace';
errorDiv.style.fontSize = '12px';
errorDiv.style.display = 'none';
// 创建输出区域
const outputDiv = document.createElement('div');
outputDiv.style.background = '#2a2a2a';
outputDiv.style.color = '#ffffff';
outputDiv.style.padding = '8px';
outputDiv.style.marginTop = '4px';
outputDiv.style.borderRadius = '4px';
outputDiv.style.fontFamily = 'monospace';
outputDiv.style.fontSize = '12px';
outputDiv.style.height = '20%';
outputDiv.style.overflowY = 'auto';
// 将元素添加到容器
widget.codeElement.appendChild(textarea);
widget.codeElement.appendChild(errorDiv);
widget.codeElement.appendChild(outputDiv);
// 设置编辑器引用
widget.editor = {
getValue: () => textarea.value,
setValue: (value) => {
textarea.value = value;
},
clearOutput: () => {
outputDiv.innerHTML = '';
},
showError: (message) => {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
},
addOutput: (text) => {
const outputLine = document.createElement('div');
outputLine.textContent = text;
outputDiv.appendChild(outputLine);
outputDiv.scrollTop = outputDiv.scrollHeight;
}
};
// 添加事件监听
textarea.addEventListener('input', () => {
widget.value = textarea.value;
if (node) {
node.setDirtyCanvas(true);
}
});
// 支持Tab键缩进
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// 在光标位置插入4个空格
textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end);
// 将光标位置移动到插入空格后的位置
textarea.selectionStart = textarea.selectionEnd = start + 4;
// 触发 input 事件以更新 widget.value
textarea.dispatchEvent(new Event('input'));
}
});
widget.codeElement.hidden = true;
document.body.appendChild(widget.codeElement);
const collapse = node.collapse;
node.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
widget.codeElement.hidden = true;
} else {
if (this.flags?.collapsed === false) {
widget.codeElement.hidden = false;
}
}
};
return widget;
}
// Save data to workflow forced!
function saveValue() {
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState();
}
// Register extensions
app.registerExtension({
name: "KYNode.KY_Eval_Python",
getCustomWidgets(app) {
return {
PYCODE: (node, inputName, inputData, app) => {
const widget = codeEditor(node, inputName, inputData);
const varTypeList = node.addWidget(
"combo",
"select_type",
"string",
(v) => {
// widget.editor.setTheme(`ace/theme/${varTypeList.value}`);
},
{
values: varTypes,
serialize: false,
}
);
// 6. 使用 addDOMWidget 将容器添加到节点上
// - 第一个参数是 widget 的名称,在节点内部需要是唯一的。
// - 第二个参数是 widget 的类型,对于自定义 DOM 元素,通常是 "div"。
// - 第三个参数是您创建的 DOM 元素。
// - 第四个参数是一个选项对象,可以用来配置 widget。
// node.addDOMWidget("rowOfButtons", "div", container, {
// });
node.addWidget(
"button",
"Add Input variable",
"add_input_variable",
async () => {
// Input name variable and check
let nameInput = node?.inputs?.length
? `p${node.inputs.length - 1}`
: "p0";
const currentWidth = node.size[0];
let tp = varTypeList.value
nameInput = nameInput + '_' + typeMap[tp]
node.addInput(nameInput, "*");
node.setSize([currentWidth, node.size[1]]);
let cv = widget.editor.getValue();
if(tp == 'json') {
cv = cv + '\n' + nameInput + ' = json.loads('+ nameInput + ')'
} else if(tp == 'list') {
cv = cv + '\nfor i in '+ nameInput +':\n print(i)'
} else if(tp == 'dict') {
cv = cv + '\nval = ' + nameInput + '["key"]'
} else {
cv = cv + '\n' + nameInput + ' = ' + typeMap[tp] + '('+ nameInput + ')'
}
widget.editor.setValue(cv)
saveValue();
}
);
node.addWidget(
"button",
"Add Output variable",
"add_output_variable",
async () => {
const currentWidth = node.size[0];
// Output name variable
let nameOutput = node?.outputs?.length
? `r${node.outputs.length}`
: "r0";
let tp = varTypeList.value
nameOutput = nameOutput + '_' + typeMap[tp]
node.addOutput(nameOutput, tp);
node.setSize([currentWidth, node.size[1]]);
let cv = widget.editor.getValue();
if(tp == 'json') {
cv = cv + '\n' + nameOutput + ' = json.dumps('+ nameOutput + ')'
} else if(tp == 'list') {
cv = cv + '\n' + nameOutput + ' = []'
} else if(tp == 'dict') {
cv = cv + '\n' + nameOutput + ' = {}'
} else {
cv = cv + '\n' + nameOutput + ' = ' + typeMap[tp] + '('+ nameOutput + ')'
}
widget.editor.setValue(cv)
saveValue();
}
);
node.onRemoved = function () {
for (const w of node?.widgets) {
if (w?.codeElement) w.codeElement.remove();
}
};
node.addCustomWidget(widget);
return widget;
},
};
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// --- IDENode
if (nodeData.name === "KY_Eval_Python") {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
const node_title = await this.getTitle();
const nodeName = `${nodeData.name}_${this.id}`;
console.log(`Create ${nodeData.name}: ${nodeName}`);
this.name = nodeName;
// Create default inputs, when first create node
if (this?.inputs?.length<2) {
["p0_str"].forEach((inputName) => {
const currentWidth = this.size[0];
this.addInput(inputName, "*");
this.setSize([currentWidth, this.size[1]]);
});
}
// const widgetEditor = findWidget(this, "pycode", "type");
this.setSize([530, this.size[1]]);
return ret;
};
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
// if (this.flags?.collapsed) return r;
if (this?.outputs?.length) {
for (let o = 0; o < this.outputs.length; o++) {
const { name, type } = this.outputs[o];
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
const nameSize = ctx.measureText(name);
const typeSize = ctx.measureText(
`[${type === "*" ? "any" : type.toLowerCase()}]`
);
ctx.fillStyle = colorType === "" ? "#AAA" : colorType;
ctx.font = "12px Arial, sans-serif";
ctx.textAlign = "right";
ctx.fillText(
`[${type === "*" ? "any" : type.toLowerCase()}]`,
this.size[0] - nameSize.width - typeSize.width,
o * 20 + 19
);
}
}
if (this?.inputs?.length) {
const not_showing = ["select_type", "pycode"];
for (let i = 1; i < this.inputs.length; i++) {
const { name, type } = this.inputs[i];
if (not_showing.includes(name)) continue;
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
const nameSize = ctx.measureText(name);
ctx.fillStyle = !colorType || colorType === "" ? "#AAA" : colorType;
ctx.font = "12px Arial, sans-serif";
ctx.textAlign = "left";
ctx.fillText(
`[${type === "*" ? "any" : type.toLowerCase()}]`,
nameSize.width + 25,
i * 20
);
}
}
return r;
};
// Node Configure
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (node) {
onConfigure?.apply(this, arguments);
if (node?.widgets_values?.length) {
const widget_code_id = findWidget(
this,
"pycode",
"type",
"findIndex"
);
const widget_theme_id = findWidget(
this,
"varTypeList",
"name",
"findIndex"
);
const widget_language_id = findWidget(
this,
"language",
"name",
"findIndex"
);
const editor = this.widgets[widget_code_id]?.editor;
if (editor) {
// 适配 textarea 编辑器
if (editor.setValue) {
editor.setValue(this.widgets_values[widget_code_id]);
}
}
}
};
// ExtraMenuOptions
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
getExtraMenuOptions?.apply(this, arguments);
const past_index = options.length - 1;
const past = options[past_index];
if (!!past) {
// Inputs remove
for (const input_idx in this.inputs) {
const input = this.inputs[input_idx];
if (["language", "select_type"].includes(input.name)) continue;
options.splice(past_index + 1, 0, {
content: `Remove Input ${input.name}`,
callback: (e) => {
const currentWidth = this.size[0];
if (input.link) {
app.graph.removeLink(input.link);
}
this.removeInput(input_idx);
this.setSize([80, this.size[1]]);
saveValue();
},
});
}
// Output remove
for (const output_idx in this.outputs) {
const output = this.outputs[output_idx];
if (output.name === "r0") continue;
options.splice(past_index + 1, 0, {
content: `Remove Output ${output.name}`,
callback: (e) => {
const currentWidth = this.size[0];
if (output.link) {
app.graph.removeLink(output.link);
}
this.removeOutput(output_idx);
this.setSize([currentWidth, this.size[1]]);
saveValue();
},
});
}
}
};
// end - ExtraMenuOptions
}
},
});