Fix widget bugs for subgraph node connected to BV Control Center widget

This commit is contained in:
BlackVortexAI
2026-01-02 10:51:34 +01:00
parent 3b7c327bcc
commit 2cdebcc76c
7 changed files with 262 additions and 69 deletions
+1
View File
@@ -243,6 +243,7 @@ Here is an example if, for example, Only Preview is active
### CHANGE LOG
* 2026-01-02 - v0.0.17: Change toggle label system from `BV Control Center` widget.
* 2026-01-02 - v0.0.16: Change input names to prevent possible collisions on `BV Control Center` widget.
* 2026-01-01 - v0.0.15: Fix renaming bug for subgraph node connected to `BV Control Center` widget.
* 2026-01-01 - v0.0.14: Fix widget bugs for subgraph node connected to `BV Control Center` widget.
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
AST = "BV_AST"
class BVPromptEncodeNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"prompt": "STRING", {d}}
}
+206
View File
@@ -0,0 +1,206 @@
from typing import Dict, Any, List
def parse_prompt_to_ast(text: str) -> Dict[str, Any]:
def make_node(t: str, **kwargs):
n = {"type": t}
n.update(kwargs)
return n
root = make_node("doc", children=[])
current_block = make_node("block", cat="default", children=[])
root["children"].append(current_block)
# Stack for nested spans: each entry is a node with children
span_stack: List[Dict[str, Any]] = []
def current_container_children() -> List[Dict[str, Any]]:
if span_stack:
return span_stack[-1]["children"]
return current_block["children"]
def emit_text(s: str):
if not s:
return
children = current_container_children()
# merge adjacent text nodes to keep AST compact
if children and children[-1]["type"] == "text":
children[-1]["v"] += s
else:
children.append(make_node("text", v=s))
def is_valid_cat(cat: str) -> bool:
return bool(cat) and all(ch.isalnum() or ch in "_-" for ch in cat)
for line in text.splitlines(keepends=True):
# Block directive only if standalone line starting with @@
if line.startswith("@@"):
raw = line[2:]
cat_candidate = raw.strip("\r\n").strip()
if cat_candidate == "" or is_valid_cat(cat_candidate):
# close any open spans (fail-soft: auto-close at block boundary)
span_stack.clear()
current_block = make_node("block", cat=(cat_candidate or "default"), children=[])
root["children"].append(current_block)
continue
i = 0
while i < len(line):
ch = line[i]
# Try span start: {cat| ...}
if ch == "{":
j = i + 1
cat_chars = []
while j < len(line) and line[j] not in "|}":
cat_chars.append(line[j])
j += 1
if j < len(line) and line[j] == "|":
cat = "".join(cat_chars).strip()
if is_valid_cat(cat):
# start span
span = make_node("span", cat=cat, children=[])
current_container_children().append(span)
span_stack.append(span)
i = j + 1
continue
# else: treat '{' as literal
# Span end
if ch == "}" and span_stack:
span_stack.pop()
i += 1
continue
# Normal char
emit_text(ch)
i += 1
return root
def ast_to_plain_text(ast: Dict[str, Any]) -> str:
out: List[str] = []
def walk(node: Dict[str, Any]):
t = node["type"]
if t == "text":
out.append(node["v"])
else:
for c in node.get("children", []):
walk(c)
walk(ast)
return "".join(out)
def ast_to_tagged_text(ast: Dict[str, Any]) -> str:
out: List[str] = []
def walk(node: Dict[str, Any]):
t = node["type"]
if t == "doc":
for c in node["children"]:
walk(c)
elif t == "block":
cat = node.get("cat", "default")
out.append(f"@@{'' if cat == 'default' else cat}\n")
for c in node.get("children", []):
walk(c)
elif t == "span":
out.append("{%s|" % node.get("cat", ""))
for c in node.get("children", []):
walk(c)
out.append("}")
elif t == "text":
out.append(node["v"])
walk(ast)
return "".join(out)
def extract_text_by_category(ast: Dict[str, Any], cat: str) -> str:
out: List[str] = []
cat = cat.strip()
def walk(node: Dict[str, Any], active: List[str]):
t = node["type"]
if t == "block":
active2 = active + ([node["cat"]] if node["cat"] != "default" else [])
for c in node.get("children", []):
walk(c, active2)
elif t == "span":
active2 = active + [node["cat"]]
for c in node.get("children", []):
walk(c, active2)
elif t == "text":
if cat in active:
out.append(node["v"])
else: # doc
for c in node.get("children", []):
walk(c, active)
walk(ast, [])
return "".join(out)
def ast_to_plain_text(ast: Dict[str, Any]) -> str:
out: List[str] = []
def walk(node: Dict[str, Any]):
t = node.get("type")
if t == "text":
out.append(node.get("v", ""))
return
for child in node.get("children", []) or []:
walk(child)
walk(ast)
return "".join(out)
def ast_to_tagged_text(ast: Dict[str, Any]) -> str:
out: List[str] = []
def walk(node: Dict[str, Any]):
t = node.get("type")
if t == "doc":
for child in node.get("children", []) or []:
walk(child)
return
if t == "block":
cat = node.get("cat", "default")
out.append(f"@@{'' if cat == 'default' else cat}\n")
for child in node.get("children", []) or []:
walk(child)
return
if t == "span":
cat = node.get("cat", "")
out.append("{%s|" % cat)
for child in node.get("children", []) or []:
walk(child)
out.append("}")
return
if t == "text":
out.append(node.get("v", ""))
return
# Unknown node types: best-effort traverse
for child in node.get("children", []) or []:
walk(child)
walk(ast)
return "".join(out)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "bv_nodepack"
version = "0.0.16"
version = "0.0.17"
description = "A ComfyUI node pack providing advanced utility and workflow nodes, including a config-driven Control Center for muting and bypassing node groups, a flexible Pipe system, and Subgraph-friendly UI nodes (Heading, Spacer, Divider) with dynamic JavaScript rendering."
authors = [
{name = "BlackVortex", email = "blackvortexx@proton.me"}
+36 -58
View File
@@ -20,8 +20,8 @@ import {
} from "../../util/control/subgraphHandler";
import {patchGraphAddSingletonGuard} from "../../util/control/singletonHandler";
const ACTIVE = "( # ACTIVE # )";
const MUTE = "(MUTE/BYPASS)";
const ACTIVE = "ACTIVE";
const MUTE = "MUTE/BYPASS";
const comfyApp = getApp();
@@ -99,14 +99,8 @@ function updateSingleControlNode(node: any, config: BVControlConfig, savedValues
// Ensure enough inputs
while (node.inputs.length < desiredCount) {
const i = node.inputs.length;
const inputName = `v_${String(i).padStart(3, "0")}`;
let label = config.rows[i].title;
if (getWidgetValue(node, i)) {
label = label + " - " + ACTIVE
} else {
label = label + " - " + MUTE
}
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
const label = config.rows[i].title;
node.addInput(inputName, "BOOLEAN", {
label,
@@ -116,7 +110,8 @@ function updateSingleControlNode(node: any, config: BVControlConfig, savedValues
});
const initialValue = (savedValues && savedValues[i] !== undefined) ? savedValues[i] : true;
const w = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true));
// (node as LGraphNode).addWidget()
const w = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true), {on: ACTIVE, off: MUTE});
w.label = label;
w.serialize = true;
@@ -128,7 +123,7 @@ function updateSingleControlNode(node: any, config: BVControlConfig, savedValues
// Remove extra inputs
while (node.inputs.length > desiredCount) {
const i = node.inputs.length - 1;
const inputName = `v_${String(i).padStart(3, "0")}`;
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
const wi = node.widgets?.findIndex((w: any) => w.name === inputName) ?? -1;
if (wi !== -1) node.widgets.splice(wi, 1);
@@ -143,13 +138,7 @@ function updateSingleControlNode(node: any, config: BVControlConfig, savedValues
for (let i = 0; i < desiredCount; i++) {
let label = config.rows[i].title;
if (getWidgetValue(node, i)) {
label = label + " - " + ACTIVE
} else {
label = label + " - " + MUTE
}
const inputName = `v_${String(i).padStart(3, "0")}`;
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
if (node.inputs[i]?.label !== label) {
node.inputs[i].label = label;
@@ -159,17 +148,21 @@ function updateSingleControlNode(node: any, config: BVControlConfig, savedValues
let w = node.widgets.find((ww: any) => ww.name === inputName);
if (!w) {
const initialValue = (savedValues && savedValues[i] !== undefined) ? savedValues[i] : true;
w = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true));
w.label = label;
w = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true), {on: ACTIVE, off: MUTE});
w.serialize = true;
w.label = label;
changed = true;
} else {
if (w.label !== label) {
w.label = label;
changed = true;
if (w.options) {
if (w.options.on !== ACTIVE || w.options.off !== MUTE) {
w.options.on = ACTIVE;
w.options.off = MUTE;
changed = true;
}
}
if (overrideValue && savedValues && savedValues[i] !== undefined && w.value !== savedValues[i]) {
w.value = savedValues[i];
w.label = label;
changed = true;
}
}
@@ -247,15 +240,8 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
const commonCount = Math.min(node.inputs.length, config.rows.length);
for (let i = 0; i < commonCount; i++) {
let label = config.rows[i].title;
const rawLabel = config.rows[i].title;
if (getWidgetValue(node, i)) {
label = label + " - " + ACTIVE
} else {
label = label + " - " + MUTE
}
const inputName = `v_${String(i).padStart(3, "0")}`;
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
if (node.inputs[i].label !== label) {
node.inputs[i].label = label;
@@ -265,8 +251,7 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
let widget = node.widgets.find(w => w.name === inputName);
if (!widget) {
const initialValue = (savedValues && savedValues[i] !== undefined) ? savedValues[i] : true;
widget = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true));
widget.label = label;
widget = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true), {on: ACTIVE, off: MUTE});
widget.serialize = true;
changed = true;
} else {
@@ -275,9 +260,12 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
widget.value = savedValues[i];
changed = true;
}
if (widget.label !== label) {
widget.label = label;
changed = true;
if (widget.options) {
if (widget.options.on !== ACTIVE || widget.options.off !== MUTE) {
widget.options.on = ACTIVE;
widget.options.off = MUTE;
changed = true;
}
}
}
@@ -290,7 +278,7 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
if (node.inputs.length > config.rows.length) {
for (let i = node.inputs.length - 1; i >= config.rows.length; i--) {
const inputName = `v_${String(i).padStart(3, "0")}`;
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
const widgetIndex = node.widgets?.findIndex(w => w.name === inputName) ?? -1;
if (widgetIndex !== -1) node.widgets.splice(widgetIndex, 1);
if (node.isInputConnected(i)) node.disconnectInput(i);
@@ -300,12 +288,7 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
} else if (node.inputs.length < config.rows.length) {
for (let i = node.inputs.length; i < config.rows.length; i++) {
let label = config.rows[i].title;
if (getWidgetValue(node, i)) {
label = label + " - " + ACTIVE
} else {
label = label + " - " + MUTE
}
const inputName = `v_${String(i).padStart(3, "0")}`;
const inputName = `bvcc_${String(i).padStart(3, "0")}`;
node.addInput(inputName, "BOOLEAN", {
label,
@@ -315,9 +298,7 @@ function updateControlNodes(config: BVControlConfig, savedValuesOverride?: any[]
});
const initialValue = (savedValues && savedValues[i] !== undefined) ? savedValues[i] : true;
const widget = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true));
// @ts-ignore
widget.label = label;
const widget = node.addWidget("toggle", inputName, initialValue, () => node.setDirtyCanvas(true), {on: ACTIVE, off: MUTE});
widget.serialize = true;
// @ts-ignore
@@ -350,11 +331,11 @@ function hookWidgetChanged(node: any) {
const config = readConfig();
if (config) {
// const title = widget.label?.split(" - ")[0]
const title = widget.label
const index = node.widgets.indexOf(widget);
const title = (index !== -1 && node.inputs[index]) ? node.inputs[index].label : widget.label;
if (!title) return;
executeChange(title, value);
updateSingleControlNode(node, config, undefined, false);
// updateSingleControlNode(node, config, undefined, false);
}
return r
@@ -392,7 +373,7 @@ function executeChange(title: string, value: boolean): void {
const applyDisable = !value;
const foundRow = config.rows.find(row => row.title.split(" - ")[0] === title.split(" - ")[0]);
const foundRow = config.rows.find(row => row.title === title);
if (!foundRow) return;
for (const entry of foundRow.entries) {
const groupTitle = normalizeGroupTitle(entry);
@@ -425,7 +406,10 @@ comfyApp.registerExtension({
const cfg = readConfig();
if (cfg) {
updateSingleControlNode(this, cfg);
setTimeout(() => {
updateSingleControlNode(this, cfg);
}, 50)
}
hookWidgetChanged(this)
@@ -539,11 +523,6 @@ comfyApp.registerExtension({
}
setWidgetValue(sNode, sWidget.name, value)
const config = readConfig()
if(config){
updateSingleControlNode(ccNode, config)
}
}
}
@@ -565,7 +544,6 @@ function patchAllSubgraphs(): number {
let patched = 0;
for (const s of subs) {
console.log("patching", s.title)
const didPatch = patchSubgraphContainerPrototype(s, hookSubgraphWidgetChanged, executeChange);
if (didPatch) patched++;
}
+1 -1
View File
@@ -143,7 +143,7 @@ export function patchSubgraphContainerPrototype(subgraphNode: any, hookSubgraphW
if (node && iNode) {
console.debug("Subgraph container widget changed", name, value, old_value, widget, node, widget.label)
// hookSubgraphWidgetChanged(node, name)
widget.label = widget.label.split(" - ")[0] + " - " + (value ? ACTIVE : MUTE)
// widget.label = widget.label.split(" - ")[0] + " - " + (value ? ACTIVE : MUTE)
iNode.widgets?.forEach((w) => {
if(w.name == sWidget.name){