import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import "./ace/ace.js"; import { createWindowModal, makeElement, findWidget, THEMES_MODAL_WINDOW, comfyuiDesktopConfirm, comfyuiDesktopPrompt, } from "./ace/utils.js"; ace.config.set("basePath", new URL("./ace/", import.meta.url).toString()); // 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), }; } // 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
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]; }, }; widget.codeElement = makeElement("pre", { innerHTML: widget.value, }); widget.editor = ace.edit(widget.codeElement); widget.editor.setTheme("ace/theme/monokai"); widget.editor.session.setMode("ace/mode/python"); widget.editor.setOptions({ enableBasicAutocompletion: true, fontFamily: "monospace", }); 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); widget.editor.getSession().on("change", function (e) { widget.value = widget.editor.getValue(); saveValue(); }); 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 + '\n' + nameInput + ' = []' } else if(tp == 'dict') { cv = cv + '\n' + nameInput + ' = {}' } 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) { // editor.setTheme( // `ace/theme/${this.widgets_values[widget_theme_id]}` // ); // editor.session.setMode( // `ace/mode/${this.widgets_values[widget_language_id]}` // ); editor.setValue(this.widgets_values[widget_code_id]); editor.clearSelection(); } } }; // 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 } }, });