diff --git a/IDENode/README.md b/IDENode/README.md
index 277384b..c69a565 100644
--- a/IDENode/README.md
+++ b/IDENode/README.md
@@ -4,6 +4,10 @@
**IDE Node** is an experimental node that allows you to run code written in **Python** or **Javascript** directly in the node, it is also possible to receive input data, which is stored in variables, process and output the result.
+# Changelog:
+
+- 2024.08.28: Implemented add output variables and types
+
# Features:
- Selecting a syntax highlighting theme
diff --git a/IDENode/ide_node.py b/IDENode/ide_node.py
index 1ce80a6..21151c9 100644
--- a/IDENode/ide_node.py
+++ b/IDENode/ide_node.py
@@ -3,7 +3,9 @@ from server import PromptServer
from aiohttp import web
from asyncio import sleep, run
import json
+import re
+remove_type_name = re.compile(r"(\{.*\})", re.I | re.M)
# Hack: string type that is always equal in not equal comparisons, thanks pythongosssss
class AnyType(str):
@@ -49,6 +51,24 @@ async def wait_js_complete(unique_id, time_out=40):
return False
+# - Thank you very much for the class -> Trung0246 -
+# - https://github.com/Trung0246/ComfyUI-0246/blob/main/utils.py#L51
+class TautologyStr(str):
+ def __ne__(self, other):
+ return False
+
+
+class ByPassTypeTuple(tuple):
+ def __getitem__(self, index):
+ if index > 0:
+ index = 0
+ item = super().__getitem__(index)
+ if isinstance(item, str):
+ return TautologyStr(item)
+ return item
+# ---------------------------
+
+
class IDENode:
def __init__(self):
self.js_complete = False
@@ -78,43 +98,57 @@ result = runCode()"""
},
),
},
- "hidden": {"unique_id": "UNIQUE_ID"},
+ "hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
}
- RETURN_TYPES = (PY_CODE,)
- RETURN_NAMES = ("any",)
+ RETURN_TYPES = ByPassTypeTuple((PY_CODE,))
+ RETURN_NAMES = ("result{ANY}",)
FUNCTION = "exec_py"
CATEGORY = "AlekPet Nodes/experiments"
- def exec_py(self, pycode, language, unique_id, **kwargs):
+ def exec_py(self, pycode, language, unique_id, extra_pnginfo, **kwargs):
if unique_id not in IDEs_DICT:
IDEs_DICT[unique_id] = self
- if language == "python":
- my_namespace = types.SimpleNamespace()
- my_namespace.__dict__.update(kwargs)
- my_namespace.__dict__.setdefault("result", "The result variable is not assigned")
+ outputs = {}
+
+ for node in extra_pnginfo['workflow']['nodes']:
+ if node['id'] == int(unique_id):
+ outputs_valid = [ouput for ouput in node.get('outputs', []) if ouput.get('name','') != '' and ouput.get('type','') != '']
+ outputs = {re.sub(remove_type_name, "", ouput['name']): None for ouput in outputs_valid}
+ self.RETURN_TYPES = ByPassTypeTuple(out["type"] for out in outputs_valid)
+ self.RETURN_NAMES = tuple(name for name in outputs.keys())
+
+ my_namespace = types.SimpleNamespace()
+ my_namespace.__dict__.update(outputs)
+ my_namespace.__dict__.update({re.sub(remove_type_name, "", prop): kwargs[prop] for prop in kwargs})
+ my_namespace.__dict__.setdefault("result", "The result variable is not assigned")
+
+ if language == "python":
try:
exec(pycode, my_namespace.__dict__)
except Exception as e:
my_namespace.result = f"Error in python code: {e}"
- return (my_namespace.result,)
+ new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])}
+
+ return (*new_dict.values(),)
else:
IDEs_DICT[unique_id].js_complete = False
IDEs_DICT[unique_id].js_result = None
+ new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])}
+
PromptServer.instance.send_sync(
"alekpet_js_result",
- {"unique_id": unique_id, "vars": json.dumps(kwargs)},
+ {"unique_id": unique_id, "vars": json.dumps(new_dict)},
)
if not run(wait_js_complete(unique_id)):
print(f"IDENode_{unique_id}: Failed to get data!")
else:
print(f"IDENode_{unique_id}: Data received successful!")
- print(IDEs_DICT[unique_id].js_result)
- return (IDEs_DICT[unique_id].js_result,)
+ return (*IDEs_DICT[unique_id].js_result,)
diff --git a/IDENode/ide_node_image.png b/IDENode/ide_node_image.png
index c0feb14..bf806d4 100644
Binary files a/IDENode/ide_node_image.png and b/IDENode/ide_node_image.png differ
diff --git a/IDENode/js/ide_node.js b/IDENode/js/ide_node.js
index 24bc925..12c19e8 100644
--- a/IDENode/js/ide_node.js
+++ b/IDENode/js/ide_node.js
@@ -17,7 +17,7 @@ ace.config.set(
);
// Constants
-const MAX_CHAR_VARNAME = 20;
+const MAX_CHAR_VARNAME = 50;
const LIST_THEMES = [
"ambiance",
"chaos",
@@ -77,7 +77,7 @@ const runCode = () => {
const date = new Date().toJSON().replace("T"," ");
return date.slice(0,date.indexOf("."))
}
-result = runCode();`,
+result = \`Hello ComfyUI with us today \${runCode()}!\``,
py: `# !!! Attention, do not insert unverified code !!!
# ---- Example code ----
# Globals inputs variables: var1, var2, var3, user variables ...
@@ -227,23 +227,23 @@ app.registerExtension({
};
}
- node.addWidget("button", "Add Variable", "add_variable", () => {
- const nameInput = node?.inputs?.length
- ? `var${node.inputs.length + 1}`
- : "var1";
- const varName = prompt("Enter variable name:", nameInput);
-
+ function makeValidVariable(
+ varName,
+ textContent,
+ regex = /^[a-z_][a-z0-9_]*$/i
+ ) {
if (
!varName ||
varName.trim() === "" ||
varName.length > MAX_CHAR_VARNAME ||
- !/^[a-z_][a-z0-9_]*$/i.test(varName)
+ !regex.test(varName)
) {
- const windowError = createWindowModal({
+ createWindowModal({
textTitle: "WARNING",
textBody: [
makeElement("div", {
- innerHTML: `
Variable name is incorrect!
- starts with a number
- has spaces or tabs
- is empty
- variable name is greater ${MAX_CHAR_VARNAME}
`,
+ style: { fontSize: "0.7rem" },
+ innerHTML: textContent,
}),
],
...THEMES_MODAL_WINDOW.warning,
@@ -258,12 +258,105 @@ app.registerExtension({
},
});
- return;
+ return false;
}
- const currentWidth = node.size[0];
- node.addInput(varName, "*");
- node.setSize([currentWidth, node.size[1]]);
- });
+ return true;
+ }
+
+ node.addWidget(
+ "button",
+ "Add Input variable",
+ "add_input_variable",
+ () => {
+ // Input name variable and check
+ const nameInput = node?.inputs?.length
+ ? `var${node.inputs.length + 1}`
+ : "var1";
+
+ const varName = prompt(
+ "Enter input variable name:",
+ nameInput
+ ).trim();
+
+ if (
+ !makeValidVariable(
+ varName,
+ `Variable for Input name is incorrect!
- starts with a number
- has spaces or tabs
- is empty
- variable name is greater ${MAX_CHAR_VARNAME}
`
+ )
+ )
+ return;
+
+ // Type variable and check
+ let type = prompt(
+ "Enter type data output (default: *):",
+ "*"
+ ).trim();
+
+ if (
+ !makeValidVariable(
+ type,
+ `Type value is incorrect!
- has spaces or tabs
- is empty
- type value length is greater ${MAX_CHAR_VARNAME}
`,
+ /^[*a-z_][a-z0-9_]*$/i
+ )
+ )
+ return;
+
+ const currentWidth = node.size[0];
+ node.addInput(
+ `${varName}{${type === "*" ? "ANY" : type.toUpperCase()}}`,
+ type
+ );
+ node.setSize([currentWidth, node.size[1]]);
+ }
+ );
+
+ node.addWidget(
+ "button",
+ "Add Output variable",
+ "add_output_variable",
+ () => {
+ const currentWidth = node.size[0];
+
+ // Output name variable
+ const nameOutput = node?.outputs?.length
+ ? `result${node.outputs.length + 1}`
+ : "result1";
+ const varName = prompt(
+ "Enter output variable name:",
+ nameOutput
+ ).trim();
+
+ // Check output variable name
+ if (
+ !makeValidVariable(
+ varName,
+ `Variable for Output name is incorrect!
- starts with a number
- has spaces or tabs
- is empty
- variable name is greater ${MAX_CHAR_VARNAME}
`
+ )
+ )
+ return;
+
+ // Type variable and check
+ let type = prompt(
+ "Enter type data output (default: *):",
+ "*"
+ ).trim();
+
+ if (
+ !makeValidVariable(
+ type,
+ `Type value is incorrect!
- has spaces or tabs
- is empty
- type value length is greater ${MAX_CHAR_VARNAME}
`,
+ /^[*a-z_][a-z0-9_]*$/i
+ )
+ )
+ return;
+
+ node.addOutput(
+ `${varName}{${type === "*" ? "ANY" : type.toUpperCase()}}`,
+ type
+ );
+ node.setSize([currentWidth, node.size[1]]);
+ }
+ );
node.addWidget("button", "Clear", "clear_code", () => {
widget.editor.setValue("");
@@ -300,7 +393,7 @@ app.registerExtension({
// Create default inputs, when first create node
if (!this?.inputs) {
- ["var1", "var2", "var3"].forEach((inputName) => {
+ ["var1{ANY}", "var2{ANY}", "var3{ANY}"].forEach((inputName) => {
const currentWidth = this.size[0];
this.addInput(inputName, "*");
this.setSize([currentWidth, this.size[1]]);
@@ -311,13 +404,14 @@ app.registerExtension({
// JS run
api.addEventListener("alekpet_js_result", async ({ detail }) => {
const { vars, unique_id } = detail;
+
if ((vars && !Object.keys(vars).length) || +unique_id !== this.id) {
return;
}
await new Promise((res) => {
const edit_vars = JSON.parse(vars);
- let code_run = `\nlet result = null;\n`;
+ let code_run = "";
for (let [k, v] of Object.entries(edit_vars)) {
// Check type
if (v instanceof String || typeof v === "string") {
@@ -328,15 +422,36 @@ app.registerExtension({
code_run += `let ${k} = ${v};\n`;
}
- code_run += `${widgetEditor.editor.getValue()}\nreturn result\n`;
+ code_run += `${widgetEditor.editor.getValue()}\nreturn [${Object.keys(
+ edit_vars
+ )},]\n`;
let result_run_code = null;
try {
result_run_code = eval(`(function(){${code_run}}())`);
} catch (e) {
result_run_code = `Error in javascript code: ${e}`;
console.error(`<${this.name}> ${result_run_code}`);
+ createWindowModal({
+ textTitle: "Error in javascript code",
+ textBody: [
+ makeElement("div", {
+ style: { fontSize: "0.7rem" },
+ textContent: e,
+ }),
+ ],
+ ...THEMES_MODAL_WINDOW.error,
+ options: {
+ auto: {
+ autohide: true,
+ autoremove: true,
+ autoshow: true,
+ timewait: 2000,
+ },
+ parent: widgetEditor.codeElement,
+ },
+ });
+ res(Object.keys(edit_vars).map((v) => e.message));
}
-
res(result_run_code);
}).then((result_code) => {
api
@@ -344,7 +459,7 @@ app.registerExtension({
method: "POST",
body: JSON.stringify({
unique_id: this.id.toString(),
- result_code: JSON.stringify(result_code),
+ result_code,
}),
})
.then((res) => res.json())
@@ -360,7 +475,7 @@ app.registerExtension({
});
});
- this.setSize([500, 350]);
+ this.setSize([530, this.size[1]]);
return ret;
};
@@ -409,12 +524,11 @@ app.registerExtension({
nodeType.prototype.getExtraMenuOptions = function (_, options) {
getExtraMenuOptions?.apply(this, arguments);
- const lastSepId = options.lastIndexOf(null);
const past_index = options.length - 1;
const past = options[past_index];
- // options.splice(lastSepId, 0, null);
if (!!past) {
+ // Inputs remove
for (const input_idx in this.inputs) {
const input = this.inputs[input_idx];
@@ -432,6 +546,26 @@ app.registerExtension({
},
});
}
+
+ // Output remove
+ for (const output_idx in this.outputs) {
+ const output = this.outputs[output_idx];
+
+ if (["result{ANY}"].includes(output.name)) continue;
+
+ options.splice(past_index + 1, 0, {
+ content: `Remove Output ${output.name}`,
+ callback: (e) => {
+ const currentWidth = this.size[0];
+ console.log(output);
+ if (output.link) {
+ app.graph.removeLink(output.link);
+ }
+ this.removeOutput(output_idx);
+ this.setSize([currentWidth, this.size[1]]);
+ },
+ });
+ }
}
};
// end - ExtraMenuOptions
diff --git a/pyproject.toml b/pyproject.toml
index b13f098..64d8e2b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "comfyui_custom_nodes_alekpet"
description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode."
-version = "1.0.20"
+version = "1.0.21"
license = { file = "LICENSE" }
[project.urls]