From 2505051c4f9f85145b1fe4436cc6044790313eac Mon Sep 17 00:00:00 2001 From: drago87 Date: Sun, 5 Apr 2026 22:45:55 +0200 Subject: [PATCH] Update 1.5.0 Fixed bug added information to schemas added new schemas --- nodes.py | 528 +++++----- web/dragos_dynamic.js | 1164 ++++++++++++----------- web/dragos_dynamic.js.txt | 707 -------------- web/dragos_structured_builder.js | 937 +++++++++--------- web/schema/Characters/Eiko.json | 40 - web/schema/character_female_animal.json | 161 ++++ web/schema/character_female_anthro.json | 40 - web/schema/character_female_human.json | 40 - web/schema/character_male_animal.json | 165 ++++ web/schema/character_male_anthro.json | 40 - web/schema/character_male_human.json | 40 - web/schema/character_placement.json | 65 ++ 12 files changed, 1758 insertions(+), 2169 deletions(-) delete mode 100644 web/dragos_dynamic.js.txt create mode 100644 web/schema/character_female_animal.json create mode 100644 web/schema/character_male_animal.json create mode 100644 web/schema/character_placement.json diff --git a/nodes.py b/nodes.py index 61ff057..66830e8 100644 --- a/nodes.py +++ b/nodes.py @@ -11,342 +11,360 @@ PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "web", "prompts") def load_prompt_files(): - files = [] + files = [] - for f in os.listdir(PROMPTS_DIR): - if f.endswith(".txt"): - files.append(os.path.splitext(f)[0]) + for f in os.listdir(PROMPTS_DIR): + if f.endswith(".txt"): + files.append(os.path.splitext(f)[0]) - return sorted(files) + return sorted(files) def parse_prompt_file(content: str): - """ - Parses a prompt file and returns (info, prompt) - - Supported formats: + """ + Parses a prompt file and returns (info, prompt) + + Supported formats: - Format1: - ... - ... + Format1: + ... + ... - Format2: - ... - raw text + Format2: + ... + raw text - Format3: - ... + Format3: + ... - Format4: - raw text - """ + Format4: + raw text + """ - info = "" - prompt = content.strip() + info = "" + prompt = content.strip() - # Extract block if present - info_match = re.search(r"([\s\S]*?)", content, re.IGNORECASE) - if info_match: - info = info_match.group(1).strip() + # Extract block if present + info_match = re.search(r"([\s\S]*?)", content, re.IGNORECASE) + if info_match: + info = info_match.group(1).strip() - # Extract block if present - prompt_match = re.search(r"([\s\S]*?)", content, re.IGNORECASE) - if prompt_match: - prompt = prompt_match.group(1).strip() - elif info_match: - # remove info block if present - prompt = content.replace(info_match.group(0), "").strip() - else: - # fallback: use entire content - prompt = content.strip() + # Extract block if present + prompt_match = re.search(r"([\s\S]*?)", content, re.IGNORECASE) + if prompt_match: + prompt = prompt_match.group(1).strip() + elif info_match: + # remove info block if present + prompt = content.replace(info_match.group(0), "").strip() + else: + # fallback: use entire content + prompt = content.strip() - return info, prompt + return info, prompt class DragosPromptLoaderNode: - CATEGORY = "DragosScene" - RETURN_TYPES = ("STRING",) - FUNCTION = "load_prompt" + CATEGORY = "DragosScene" + RETURN_TYPES = ("STRING",) + FUNCTION = "load_prompt" - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "prompt": (load_prompt_files(),), - }, - "optional": { - "info_text": ("STRING", {"multiline": True, "default": ""}), - "prompt_text": ("STRING", {"multiline": True, "default": ""}), - } - } + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": (load_prompt_files(),), + }, + "optional": { + "info_text": ("STRING", {"multiline": True, "default": ""}), + "prompt_text": ("STRING", {"multiline": True, "default": ""}), + } + } - def load_prompt(self, prompt, info_text="", prompt_text=""): - path = os.path.join(PROMPTS_DIR, prompt + ".txt") + def load_prompt(self, prompt, info_text="", prompt_text=""): + path = os.path.join(PROMPTS_DIR, prompt + ".txt") - content = "" - if os.path.exists(path): - with open(path, "r", encoding="utf-8") as f: - content = f.read() + content = "" + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() - info, parsed_prompt = parse_prompt_file(content) + info, parsed_prompt = parse_prompt_file(content) - # prioritize edited textbox - final_prompt = prompt_text.strip() if prompt_text.strip() else parsed_prompt + # prioritize edited textbox + final_prompt = prompt_text.strip() if prompt_text.strip() else parsed_prompt - return (final_prompt,) + return (final_prompt,) def load_schema_categories(): - categories = [] - - if not os.path.exists(SCHEMA_DIR): - return ["character"] + categories = [] + + if not os.path.exists(SCHEMA_DIR): + return ["character"] - # Walk through the schema directory and all subdirectories - for root, dirs, files in os.walk(SCHEMA_DIR): - for file in files: - if file.endswith(".json"): - # Get the full path of the file - full_path = os.path.join(root, file) - - # Get the path relative to the SCHEMA_DIR - rel_path = os.path.relpath(full_path, SCHEMA_DIR) - - # Remove the .json extension - name = os.path.splitext(rel_path)[0] - - # Normalize path separators to forward slashes for web URLs - # (Windows uses backslashes, web needs forward slashes) - name = name.replace("\\", "/") - - categories.append(name) + # Walk through the schema directory and all subdirectories + for root, dirs, files in os.walk(SCHEMA_DIR): + for file in files: + if file.endswith(".json"): + # Get the full path of the file + full_path = os.path.join(root, file) + + # Get the path relative to the SCHEMA_DIR + rel_path = os.path.relpath(full_path, SCHEMA_DIR) + + # Remove the .json extension + name = os.path.splitext(rel_path)[0] + + # Normalize path separators to forward slashes for web URLs + # (Windows uses backslashes, web needs forward slashes) + name = name.replace("\\", "/") + + categories.append(name) - return sorted(categories) + return sorted(categories) def is_valid_prompt_var(v): - if not isinstance(v, dict): - return False + if not isinstance(v, dict): + return False - if "name" not in v or "value" not in v: - return False + if "name" not in v or "value" not in v: + return False - if v["value"] is None: - return False + if v["value"] is None: + return False - return True + return True class DragosVariableNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "var_name": ("STRING", {"default": "subject"}), - "text": ("STRING", {"multiline": True, "default": ""}), - } - } + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "var_name": ("STRING", {"default": "subject"}), + "text": ("STRING", {"multiline": True, "default": ""}), + } + } - RETURN_TYPES = (PROMPT_VAR_TYPE,) - FUNCTION = "get_text" - CATEGORY = "DragosScene" + RETURN_TYPES = (PROMPT_VAR_TYPE,) + FUNCTION = "get_text" + CATEGORY = "DragosScene" - def get_text(self, var_name, text): + def get_text(self, var_name, text): - if var_name is None or var_name == "": - return (None,) + if var_name is None or var_name == "": + return (None,) - return ({ - "name": var_name, - "value": text - },) + return ({ + "name": var_name, + "value": text + },) class DragosObjectNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "obj_name": ("STRING", {"default": "object"}), - }, - "optional": { - "input_1": (PROMPT_VAR_TYPE, {"forceInput": True}), - } - } + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "obj_name": ("STRING", {"default": "object"}), + }, + "optional": { + "input_1": (PROMPT_VAR_TYPE, {"forceInput": True}), + } + } - RETURN_TYPES = (PROMPT_VAR_TYPE,) - FUNCTION = "build_object" - CATEGORY = "DragosScene" + RETURN_TYPES = (PROMPT_VAR_TYPE,) + FUNCTION = "build_object" + CATEGORY = "DragosScene" - @classmethod - def VALIDATE_INPUTS(cls, **kwargs): - return True + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + return True - def build_object(self, obj_name, **kwargs): + def build_object(self, obj_name, **kwargs): - if obj_name is None or obj_name == "": - return (None,) + if obj_name is None or obj_name == "": + return (None,) - combined = {} + combined = {} - for v in kwargs.values(): + for v in kwargs.values(): - if not is_valid_prompt_var(v): - continue + if not is_valid_prompt_var(v): + continue - combined[v["name"]] = v["value"] + combined[v["name"]] = v["value"] - if not combined: - return (None,) + if not combined: + return (None,) - return ({ - "name": obj_name, - "value": combined - },) + return ({ + "name": obj_name, + "value": combined + },) def deep_merge(base_dict, merge_dict): - """ - Deep merge merge_dict into base_dict. - - If a key exists in both and both values are dicts, merge recursively - - Otherwise, merge_dict's value overwrites base_dict's value - """ - result = dict(base_dict) # shallow copy - - for key, value in merge_dict.items(): - if ( - key in result - and isinstance(result[key], dict) - and isinstance(value, dict) - ): - result[key] = deep_merge(result[key], value) - else: - result[key] = value - - return result + """ + Deep merge merge_dict into base_dict. + - If a key exists in both and both values are dicts, merge recursively + - Otherwise, merge_dict's value overwrites base_dict's value + """ + result = dict(base_dict) # shallow copy + + for key, value in merge_dict.items(): + if ( + key in result + and isinstance(result[key], dict) + and isinstance(value, dict) + ): + result[key] = deep_merge(result[key], value) + else: + result[key] = value + + return result class DragosSceneCompiler: - @classmethod - def INPUT_TYPES(cls): - return { - "required": {}, - "optional": { - "input_1": ("PROMPT_VAR", {"forceInput": True}), - }, - "hidden": { - "output_json_string": ("STRING", {"multiline": True}), - } - } + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "input_1": ("PROMPT_VAR", {"forceInput": True}), + }, + "hidden": { + "output_json_string": ("STRING", {"multiline": True}), + } + } - RETURN_TYPES = ("STRING",) - OUTPUT_NODE = True - FUNCTION = "compile_json" - CATEGORY = "DragosScene" + RETURN_TYPES = ("STRING",) + OUTPUT_NODE = True + FUNCTION = "compile_json" + CATEGORY = "DragosScene" - @staticmethod - def _unwrap_prompt_var(v): - while isinstance(v, tuple) and len(v) == 1: - v = v[0] - return v + @staticmethod + def _unwrap_prompt_var(v): + while isinstance(v, tuple) and len(v) == 1: + v = v[0] + return v - @staticmethod - def _compact_for_llm(text: str) -> str: - """ - Replace tabs and newlines with spaces and collapse multiple spaces. - This reduces token count and improves semantic locality. - """ - if not text: - return "" + @staticmethod + def _compact_for_llm(text: str) -> str: + """ + Replace tabs and newlines with spaces and collapse multiple spaces. + This reduces token count and improves semantic locality. + """ + if not text: + return "" - # Replace newline and tab with space - text = re.sub(r"[\n\t]+", " ", text) + # Replace newline and tab with space + text = re.sub(r"[\n\t]+", " ", text) - # Collapse multiple spaces into one - text = re.sub(r" {2,}", " ", text) + # Collapse multiple spaces into one + text = re.sub(r" {2,}", " ", text) - return text.strip() + return text.strip() - def compile_json(self, **kwargs): + def compile_json(self, **kwargs): - hidden_json = kwargs.get("output_json_string") + hidden_json = kwargs.get("output_json_string") - if hidden_json: - compact = self._compact_for_llm(hidden_json) - return (compact,) + if hidden_json: + compact = self._compact_for_llm(hidden_json) + return (compact,) - scene = {} + scene = {} - for key, v in kwargs.items(): - unwrapped = self._unwrap_prompt_var(v) - if isinstance(unwrapped, dict) and "name" in unwrapped and "value" in unwrapped: - entry_name = unwrapped["name"] - entry_value = unwrapped["value"] - - # Check if key already exists and merge if both are dicts - if entry_name in scene and isinstance(scene[entry_name], dict) and isinstance(entry_value, dict): - scene[entry_name] = deep_merge(scene[entry_name], entry_value) - else: - scene[entry_name] = entry_value + for key, v in kwargs.items(): + unwrapped = self._unwrap_prompt_var(v) + if isinstance(unwrapped, dict) and "name" in unwrapped and "value" in unwrapped: + entry_name = unwrapped["name"] + entry_value = unwrapped["value"] + + # Check if key already exists and merge if both are dicts + if entry_name in scene and isinstance(scene[entry_name], dict) and isinstance(entry_value, dict): + scene[entry_name] = deep_merge(scene[entry_name], entry_value) + else: + scene[entry_name] = entry_value - json_out = json.dumps(scene, indent="\t", ensure_ascii=False) + json_out = json.dumps(scene, indent="\t", ensure_ascii=False) - compact = self._compact_for_llm(json_out) + compact = self._compact_for_llm(json_out) - return (compact,) + return (compact,) class DragosStructuredBuilderNode: - CATEGORY = "DragosScene" - RETURN_TYPES = (PROMPT_VAR_TYPE,) - FUNCTION = "build" + CATEGORY = "DragosScene" + RETURN_TYPES = (PROMPT_VAR_TYPE,) + FUNCTION = "build" - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "category": (load_schema_categories(),), - }, - "optional": { - "input_1": (PROMPT_VAR_TYPE, {"forceInput": True}), - }, - "hidden": { - "json_data": ("STRING",), - } - } + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "category": (load_schema_categories(),), + }, + "optional": { + "input_1": (PROMPT_VAR_TYPE, {"forceInput": True}), + }, + "hidden": { + "json_data": ("STRING",), + } + } - @classmethod - def VALIDATE_INPUTS(cls, **kwargs): - return True + @classmethod + def VALIDATE_INPUTS(cls, **kwargs): + return True - def build(self, category, json_data, **kwargs): - - try: - parsed = json.loads(json_data) if json_data else {} - except Exception: - parsed = {} - - meta = parsed.get("_meta", {}) - base_data = parsed.get("data", {}) - - # Collect all input values from kwargs (dynamic inputs) - input_values = {} - for v in kwargs.values(): - if not is_valid_prompt_var(v): - continue - input_values[v["name"]] = v["value"] - - # Merge input values with base data - # Input values override base data - merged_data = deep_merge(base_data, input_values) - - return ({ - "name": meta.get("category", category), - "value": merged_data, - "_meta": meta - },) + def build(self, category, json_data="", **kwargs): + + try: + parsed = json.loads(json_data) if json_data else {} + except Exception: + parsed = {} + + meta = parsed.get("_meta", {}) + base_data = parsed.get("data", {}) + + # Get this node's category (e.g., "character") to validate extensions + my_category = meta.get("category", "") + + # Collect all input values from kwargs (dynamic inputs) + input_values = {} + extensions_to_merge = [] + + for v in kwargs.values(): + if not is_valid_prompt_var(v): + continue + + # Check if this input is an extension schema + v_meta = v.get("_meta", {}) + if v_meta and v_meta.get("extension"): + # ONLY merge if the extension's target matches this node's category! + if v_meta.get("extension") == my_category: + extensions_to_merge.append(v["value"]) + else: + # Normal variable/object input + input_values[v["name"]] = v["value"] + + # Merge standard input values with base data + merged_data = deep_merge(base_data, input_values) + + # Deep merge extensions directly into the root (e.g., merging "top": "shirt" into character) + for ext_data in extensions_to_merge: + if isinstance(ext_data, dict): + merged_data = deep_merge(merged_data, ext_data) + + return ({ + "name": meta.get("category", category), + "value": merged_data, + "_meta": meta + },) \ No newline at end of file diff --git a/web/dragos_dynamic.js b/web/dragos_dynamic.js index 1425b90..5c33566 100644 --- a/web/dragos_dynamic.js +++ b/web/dragos_dynamic.js @@ -4,704 +4,744 @@ import { ComfyWidgets } from "../../scripts/widgets.js"; function isNodeActive(node) { - if (!node) return false; + if (!node) return false; - if (node.mode === 2) return false; - if (node.mode === 4) return false; + if (node.mode === 2) return false; + if (node.mode === 4) return false; - return true; + return true; } function getWidgetValue(node, names) { - if (!node.widgets) return null; + if (!node.widgets) return null; - for (const name of names) - { - const w = node.widgets.find(w => w.name === name); - if (w) return w.value; - } + for (const name of names) + { + const w = node.widgets.find(w => w.name === name); + if (w) return w.value; + } - return null; + return null; } function deepMergeObjects(base, override) { - const result = { ...base }; + const result = { ...base }; - for (const key in override) - { - if ( - key in result && - typeof result[key] === "object" && - result[key] !== null && - !Array.isArray(result[key]) && - typeof override[key] === "object" && - override[key] !== null && - !Array.isArray(override[key]) - ) - { - result[key] = deepMergeObjects(result[key], override[key]); - } - else - { - result[key] = override[key]; - } - } + for (const key in override) + { + if ( + key in result && + typeof result[key] === "object" && + result[key] !== null && + !Array.isArray(result[key]) && + typeof override[key] === "object" && + override[key] !== null && + !Array.isArray(override[key]) + ) + { + result[key] = deepMergeObjects(result[key], override[key]); + } + else + { + result[key] = override[key]; + } + } - return result; + return result; } function getValueRecursive(node, graph) { - if (!node) return undefined; - if (!isNodeActive(node)) return undefined; + if (!node) return undefined; + if (!isNodeActive(node)) return undefined; - if (node.comfyClass === "DragosVariable") - return getWidgetValue(node, ["text"]) ?? ""; + if (node.comfyClass === "DragosVariable") + return getWidgetValue(node, ["text"]) ?? ""; - if (node.comfyClass === "DragosStructuredBuilder") - { - // First, build the base result from widgets (schema data) - const result = {}; + if (node.comfyClass === "DragosStructuredBuilder") + { + // First, build the base result from widgets (schema data) + const result = {}; - for (const widget of node.widgets || []) - { - if (!widget.dragosPath) continue; - if (widget.hidden) continue; - if (widget.value === "" || widget.value == null) continue; + // Get this node's category for extension validation + let myCategory = ""; + const myJsonWidget = node.widgets?.find(w => w.name === "json_data"); + if (myJsonWidget?.value) { + try { + const myParsed = JSON.parse(myJsonWidget.value); + myCategory = myParsed._meta?.category || ""; + } catch(e) {} + } - const parts = widget.dragosPath.split("."); - let current = result; + for (const widget of node.widgets || []) + { + if (!widget.dragosPath) continue; + if (widget.hidden) continue; + if (widget.value === "" || widget.value == null) continue; - for (let i = 0; i < parts.length - 1; i++) - { - const part = parts[i]; + const parts = widget.dragosPath.split("."); + let current = result; - if (!(part in current)) - current[part] = {}; + for (let i = 0; i < parts.length - 1; i++) + { + const part = parts[i]; - current = current[part]; - } + if (!(part in current)) + current[part] = {}; - current[parts[parts.length - 1]] = widget.value; - } + current = current[part]; + } - // Now merge any connected inputs (DragosVariable or DragosObject) - if (node.inputs) - { - for (const input of node.inputs) - { - if (!input || input.link == null) continue; - if (!input.name || !input.name.startsWith("input_")) continue; + current[parts[parts.length - 1]] = widget.value; + } - const link = graph.links[input.link]; - if (!link) continue; + // Now merge any connected inputs + if (node.inputs) + { + for (const input of node.inputs) + { + if (!input || input.link == null) continue; + if (!input.name || !input.name.startsWith("input_")) continue; - const origin = graph.getNodeById(link.origin_id); - if (!origin || !isNodeActive(origin)) continue; + const link = graph.links[input.link]; + if (!link) continue; - // Get the name from the connected node - const inputName = getWidgetValue(origin, ["var_name", "obj_name"]); - const inputValue = getValueRecursive(origin, graph); + const origin = graph.getNodeById(link.origin_id); + if (!origin || !isNodeActive(origin)) continue; - if (inputName && inputValue !== undefined) - { - // If it's a simple value, add/override directly - // If it's an object, merge it - if (typeof inputValue === "object" && inputValue !== null) - { - result[inputName] = deepMergeObjects(result[inputName] || {}, inputValue); - } - else - { - result[inputName] = inputValue; - } - } - } - } + const inputValue = getValueRecursive(origin, graph); + if (inputValue === undefined) continue; - return result; - } + // Check if origin is a StructuredBuilder with an extension meta + let isExtension = false; + let extensionTarget = ""; - if (node.comfyClass === "DragosObject") - { - const result = {}; + if (origin.comfyClass === "DragosStructuredBuilder") { + const originJsonWidget = origin.widgets?.find(w => w.name === "json_data"); + if (originJsonWidget?.value) { + try { + const parsedOrigin = JSON.parse(originJsonWidget.value); + if (parsedOrigin._meta?.extension) { + isExtension = true; + extensionTarget = parsedOrigin._meta.extension; + } + } catch (e) {} + } + } - for (const input of node.inputs || []) - { - if (input.link == null) continue; + if (isExtension) { + // ONLY merge if the extension target matches THIS node's category + if (extensionTarget === myCategory) { + if (typeof inputValue === "object" && inputValue !== null) { + // Deep merge directly into root + for (const key in inputValue) { + if ( + key in result && + typeof result[key] === "object" && + result[key] !== null && + !Array.isArray(result[key]) && + typeof inputValue[key] === "object" && + inputValue[key] !== null && + !Array.isArray(inputValue[key]) + ) { + result[key] = deepMergeObjects(result[key], inputValue[key]); + } else { + result[key] = inputValue[key]; + } + } + } + } + } else { + // Standard DragosVariable / DragosObject behavior + const inputName = getWidgetValue(origin, ["var_name", "obj_name"]); + if (inputName) { + if (typeof inputValue === "object" && inputValue !== null) + { + result[inputName] = deepMergeObjects(result[inputName] || {}, inputValue); + } + else + { + result[inputName] = inputValue; + } + } + } + } + } - const link = graph.links[input.link]; - if (!link) continue; + return result; + } - const origin = graph.getNodeById(link.origin_id); - if (!isNodeActive(origin)) continue; + if (node.comfyClass === "DragosObject") + { + const result = {}; - const key = - getWidgetValue(origin, ["var_name", "obj_name"]) - ?? input.name; + for (const input of node.inputs || []) + { + if (input.link == null) continue; - const value = getValueRecursive(origin, graph); + const link = graph.links[input.link]; + if (!link) continue; - if (value !== undefined) - result[key] = value; - } + const origin = graph.getNodeById(link.origin_id); + if (!isNodeActive(origin)) continue; - return result; - } + const key = + getWidgetValue(origin, ["var_name", "obj_name"]) + ?? input.name; - return undefined; + const value = getValueRecursive(origin, graph); + + if (value !== undefined) + result[key] = value; + } + + return result; + } + + return undefined; } function refreshNode(node, graph, previewWidget, syncPreviewHeight) { - if (!node || !graph || !node.inputs) - return; + if (!node || !graph || !node.inputs) + return; - //////////////////////////////////////////////////////////// - // STEP 1 — Collect ordered origin nodes (stable order) - //////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////// + // STEP 1 — Collect ordered origin nodes (stable order) + ////////////////////////////////////////////////////////// - const orderedOrigins = []; + const orderedOrigins = []; - for (let i = 0; i < node.inputs.length; i++) - { - const input = node.inputs[i]; + for (let i = 0; i < node.inputs.length; i++) + { + const input = node.inputs[i]; - if (!input || input.link == null) - continue; + if (!input || input.link == null) + continue; - const link = graph.links[input.link]; - if (!link) - continue; + const link = graph.links[input.link]; + if (!link) + continue; - const origin = graph.getNodeById(link.origin_id); + const origin = graph.getNodeById(link.origin_id); - if (!origin) - continue; + if (!origin) + continue; - if (!isNodeActive(origin)) - continue; + if (!isNodeActive(origin)) + continue; - orderedOrigins.push(origin); - } + orderedOrigins.push(origin); + } - //////////////////////////////////////////////////////////// - // STEP 2 — Build intermediate structure - //////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////// + // STEP 2 — Build intermediate structure + ////////////////////////////////////////////////////////// - const built = []; + const built = []; - for (let i = 0; i < orderedOrigins.length; i++) - { - const origin = orderedOrigins[i]; + for (let i = 0; i < orderedOrigins.length; i++) + { + const origin = orderedOrigins[i]; - const value = - getValueRecursive(origin, graph); + const value = + getValueRecursive(origin, graph); - if ( - value == null || - (typeof value === "object" && - Object.keys(value).length === 0) - ) - continue; + if ( + value == null || + (typeof value === "object" && + Object.keys(value).length === 0) + ) + continue; - let category = ""; - let extension = ""; + let category = ""; + let extension = ""; - // Read _meta from json_data widget ONCE - const jsonWidget = - origin.widgets?.find( - w => w.name === "json_data" - ); + // Read _meta from json_data widget ONCE + const jsonWidget = + origin.widgets?.find( + w => w.name === "json_data" + ); - if (jsonWidget?.value) - { - try - { - const parsed = - JSON.parse(jsonWidget.value); + if (jsonWidget?.value) + { + try + { + const parsed = + JSON.parse(jsonWidget.value); - const meta = parsed?._meta; + const meta = parsed?._meta; - if (meta) - { - category = - String( - meta.display_name ?? - meta.displayName ?? - meta.category ?? - "" - ).toLowerCase(); + if (meta) + { + category = + String( + meta.display_name ?? + meta.displayName ?? + meta.category ?? + "" + ).toLowerCase(); - extension = - String( - meta.extension ?? "" - ).toLowerCase(); - } - } - catch {} - } + extension = + String( + meta.extension ?? "" + ).toLowerCase(); + } + } + catch {} + } - // Fallback category detection - if (!category) - { - category = - String( - getWidgetValue( - origin, - ["category", "obj_name", "var_name"] - ) || "" - ).toLowerCase(); - } + // Fallback category detection + if (!category) + { + category = + String( + getWidgetValue( + origin, + ["category", "obj_name", "var_name"] + ) || "" + ).toLowerCase(); + } - built.push({ - category, - extension, - value, - sceneKey: null - }); - } + built.push({ + category, + extension, + value, + sceneKey: null + }); + } - //////////////////////////////////////////////////////////// - // STEP 3 — Merge into final scene - //////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////// + // STEP 3 — Merge into final scene + ////////////////////////////////////////////////////////// - const scene = {}; + const scene = {}; - let characterCount = 0; + let characterCount = 0; - for (let i = 0; i < built.length; i++) - { - const entry = built[i]; + for (let i = 0; i < built.length; i++) + { + const entry = built[i]; - //////////////////////////////////////////////////////// - // TOP-LEVEL - //////////////////////////////////////////////////////// - - if (!entry.extension) - { - if (entry.category === "character") - { - characterCount++; + ////////////////////////////////////////////////////// + // TOP-LEVEL + ////////////////////////////////////////////////////// + + if (!entry.extension) + { + if (entry.category === "character") + { + characterCount++; - const key = - `character_${characterCount}`; + const key = + `character_${characterCount}`; - // Check if key already exists and merge if both are objects - if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") - { - scene[key] = deepMergeObjects(scene[key], entry.value); - } - else - { - scene[key] = entry.value; - } + // Check if key already exists and merge if both are objects + if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") + { + scene[key] = deepMergeObjects(scene[key], entry.value); + } + else + { + scene[key] = entry.value; + } - entry.sceneKey = key; - } - else if (entry.category) - { - const key = entry.category; + entry.sceneKey = key; + } + else if (entry.category) + { + const key = entry.category; - // Check if key already exists and merge if both are objects - if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") - { - scene[key] = deepMergeObjects(scene[key], entry.value); - } - else - { - scene[key] = entry.value; - } + // Check if key already exists and merge if both are objects + if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") + { + scene[key] = deepMergeObjects(scene[key], entry.value); + } + else + { + scene[key] = entry.value; + } - entry.sceneKey = key; - } + entry.sceneKey = key; + } - continue; - } + continue; + } - //////////////////////////////////////////////////////// - // EXTENSION MERGE - //////////////////////////////////////////////////////// + ////////////////////////////////////////////////////// + // EXTENSION MERGE + ////////////////////////////////////////////////////// - const targetCategory = - entry.extension; + const targetCategory = + entry.extension; - for (let j = i - 1; j >= 0; j--) - { - const candidate = built[j]; + for (let j = i - 1; j >= 0; j--) + { + const candidate = built[j]; - if ( - candidate.category !== targetCategory - ) - continue; + if ( + candidate.category !== targetCategory + ) + continue; - const targetKey = - candidate.sceneKey; + const targetKey = + candidate.sceneKey; - if (!targetKey) - break; + if (!targetKey) + break; - if (!scene[targetKey]) - scene[targetKey] = {}; + if (!scene[targetKey]) + scene[targetKey] = {}; - // Use deep merge instead of Object.assign for nested objects - scene[targetKey] = deepMergeObjects(scene[targetKey], entry.value); + // Use deep merge instead of Object.assign for nested objects + scene[targetKey] = deepMergeObjects(scene[targetKey], entry.value); - break; - } - } + break; + } + } - //////////////////////////////////////////////////////////// - // STEP 4 — Remove empty objects (deep clean) - //////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////// + // STEP 4 — Remove empty objects (deep clean) + ////////////////////////////////////////////////////////// - function removeEmpty(obj) - { - if (!obj || typeof obj !== "object") - return obj; - - for (const key in obj) - { - const val = - removeEmpty(obj[key]); - - if ( - val == null || - (typeof val === "object" && - Object.keys(val).length === 0) - ) - { - delete obj[key]; - } - } - - return obj; - } - - removeEmpty(scene); - - - - //////////////////////////////////////////////////////////// - // STEP 5 — Convert to JSON - //////////////////////////////////////////////////////////// - - const json = - JSON.stringify(scene, null, "\t"); - - - - //////////////////////////////////////////////////////////// - // STEP 6 — Update preview widget (only if changed) - //////////////////////////////////////////////////////////// - - if ( - previewWidget && - previewWidget.value !== json - ) - { - previewWidget.value = json; - - if (syncPreviewHeight) - syncPreviewHeight(); - } - - - - //////////////////////////////////////////////////////////// - // STEP 7 — Hidden output widget - //////////////////////////////////////////////////////////// - - let hiddenOutput = - node.widgets.find( - w => w.name === "output_json_string" - ); - - if (!hiddenOutput) - { - hiddenOutput = - node.addWidget( - "text", - "output_json_string", - "", - () => {} - ); - - hiddenOutput.hidden = true; - hiddenOutput.computeSize = - () => [0, -8]; - hiddenOutput.draw = - () => {}; - } - - hiddenOutput.value = json; - - - - //////////////////////////////////////////////////////////// - // STEP 8 — Dynamic inputs (CORRECT INDEX-SAFE VERSION) - //////////////////////////////////////////////////////////// - - const dynamicInputs = node.inputs - .map((slot, realIndex) => ({ - slot, - realIndex, - index: ( - slot && - slot.name && - slot.name.startsWith("input_") - ) - ? parseInt(slot.name.substring(6)) || 0 - : -1 - })) - .filter(x => x.index >= 0) - .sort((a, b) => a.index - b.index); - - - if (dynamicInputs.length === 0) - return; - - - let changed = false; - - - //////////////////////////////////////////////////////////// - // Add new slot if last is connected - //////////////////////////////////////////////////////////// - - const last = dynamicInputs[dynamicInputs.length - 1]; - - if (last.slot.link != null) - { - node.addInput( - `input_${last.index + 1}`, - "PROMPT_VAR" - ); - - changed = true; - } - - - //////////////////////////////////////////////////////////// - // Remove trailing empty slots SAFELY using REAL INDEX - //////////////////////////////////////////////////////////// - - while (dynamicInputs.length > 1) - { - const lastEntry = - dynamicInputs[dynamicInputs.length - 1]; - - const prevEntry = - dynamicInputs[dynamicInputs.length - 2]; - - if ( - lastEntry.slot.link == null && - prevEntry.slot.link == null - ) - { - // find CURRENT real index (important) - const currentRealIndex = - node.inputs.findIndex( - s => s === lastEntry.slot - ); - - if (currentRealIndex !== -1) - { - node.removeInput(currentRealIndex); - - dynamicInputs.pop(); - - changed = true; - - continue; - } - } - - break; - } - - - if (changed) - { - node.setDirtyCanvas(true, true); - } + function removeEmpty(obj) + { + if (!obj || typeof obj !== "object") + return obj; + + for (const key in obj) + { + const val = + removeEmpty(obj[key]); + + if ( + val == null || + (typeof val === "object" && + Object.keys(val).length === 0) + ) + { + delete obj[key]; + } + } + + return obj; + } + + removeEmpty(scene); + + + + ////////////////////////////////////////////////////////// + // STEP 5 — Convert to JSON + ////////////////////////////////////////////////////////// + + const json = + JSON.stringify(scene, null, "\t"); + + + + ////////////////////////////////////////////////////////// + // STEP 6 — Update preview widget (only if changed) + ////////////////////////////////////////////////////////// + + if ( + previewWidget && + previewWidget.value !== json + ) + { + previewWidget.value = json; + + if (syncPreviewHeight) + syncPreviewHeight(); + } + + + + ////////////////////////////////////////////////////////// + // STEP 7 — Hidden output widget + ////////////////////////////////////////////////////////// + + let hiddenOutput = + node.widgets.find( + w => w.name === "output_json_string" + ); + + if (!hiddenOutput) + { + hiddenOutput = + node.addWidget( + "text", + "output_json_string", + "", + () => {} + ); + + hiddenOutput.hidden = true; + hiddenOutput.computeSize = + () => [0, -8]; + hiddenOutput.draw = + () => {}; + } + + hiddenOutput.value = json; + + + + ////////////////////////////////////////////////////////// + // STEP 8 — Dynamic inputs (CORRECT INDEX-SAFE VERSION) + ////////////////////////////////////////////////////////// + + const dynamicInputs = node.inputs + .map((slot, realIndex) => ({ + slot, + realIndex, + index: ( + slot && + slot.name && + slot.name.startsWith("input_") + ) + ? parseInt(slot.name.substring(6)) || 0 + : -1 + })) + .filter(x => x.index >= 0) + .sort((a, b) => a.index - b.index); + + + if (dynamicInputs.length === 0) + return; + + + let changed = false; + + + ////////////////////////////////////////////////////////// + // Add new slot if last is connected + ////////////////////////////////////////////////////////// + + const last = dynamicInputs[dynamicInputs.length - 1]; + + if (last.slot.link != null) + { + node.addInput( + `input_${last.index + 1}`, + "PROMPT_VAR" + ); + + changed = true; + } + + + ////////////////////////////////////////////////////////// + // Remove trailing empty slots SAFELY using REAL INDEX + ////////////////////////////////////////////////////////// + + while (dynamicInputs.length > 1) + { + const lastEntry = + dynamicInputs[dynamicInputs.length - 1]; + + const prevEntry = + dynamicInputs[dynamicInputs.length - 2]; + + if ( + lastEntry.slot.link == null && + prevEntry.slot.link == null + ) + { + // find CURRENT real index (important) + const currentRealIndex = + node.inputs.findIndex( + s => s === lastEntry.slot + ); + + if (currentRealIndex !== -1) + { + node.removeInput(currentRealIndex); + + dynamicInputs.pop(); + + changed = true; + + continue; + } + } + + break; + } + + + if (changed) + { + node.setDirtyCanvas(true, true); + } } app.registerExtension({ - name: "Dragos.SceneBuilder.Dynamic", + name: "Dragos.SceneBuilder.Dynamic", - async nodeCreated(node) - { - if ( - node.comfyClass !== "DragosObject" - && - node.comfyClass !== "DragosSceneCompiler" - && - node.comfyClass !== "DragosStructuredBuilder" - ) return; + async nodeCreated(node) + { + if ( + node.comfyClass !== "DragosObject" + && + node.comfyClass !== "DragosSceneCompiler" + && + node.comfyClass !== "DragosStructuredBuilder" + ) return; - let previewWidget = null; - let syncPreviewHeight = null; + let previewWidget = null; + let syncPreviewHeight = null; - if (node.comfyClass === "DragosSceneCompiler") - { - previewWidget = - ComfyWidgets.STRING( - node, - "preview", - ["STRING", { multiline: true }], - app - ).widget; + if (node.comfyClass === "DragosSceneCompiler") + { + previewWidget = + ComfyWidgets.STRING( + node, + "preview", + ["STRING", { multiline: true }], + app + ).widget; - previewWidget.inputEl.readOnly = true; - previewWidget.inputEl.style.fontSize = "11px"; - previewWidget.inputEl.style.resize = "none"; + previewWidget.inputEl.readOnly = true; + previewWidget.inputEl.style.fontSize = "11px"; + previewWidget.inputEl.style.resize = "none"; - syncPreviewHeight = function() - { - if (!previewWidget?.inputEl) return; - if (!node.size) return; - - const HEADER = LiteGraph.NODE_TITLE_HEIGHT || 30; - const SLOT = LiteGraph.NODE_SLOT_HEIGHT || 20; - const WIDGET_SPACING = 4; - const BOTTOM_PADDING = 15; // Creates the consistent buffer at the bottom - - let widgetY = HEADER; - - // Calculate the Y position where the preview widget starts - for (const w of node.widgets) - { - if (w === previewWidget) break; - const size = w.computeSize?.(node.size[0]); - widgetY += (size?.[1] || 20) + WIDGET_SPACING; - } - - // Calculate height occupied by inputs on the left - // We subtract this to create the "buffer" so the text box doesn't overlap the inputs area - const inputCount = node.inputs?.length || 0; - const inputsHeight = inputCount * SLOT - SLOT; - - // Calculate available height - // Node Height - (Widgets Above) - (Inputs Height) - (Bottom Padding) - const available = node.size[1] - widgetY - inputsHeight - BOTTOM_PADDING; - - // Set height. - // We use Math.max(20) instead of 50. - // This allows the box to be small enough to FIT in small nodes (fixing Image_1) - // while the calculation above ensures it stops before the inputs (fixing Image_3). - previewWidget.inputEl.style.height = Math.max(available, 20) + "px"; - }; + syncPreviewHeight = function() + { + if (!previewWidget?.inputEl) return; + if (!node.size) return; + + const HEADER = LiteGraph.NODE_TITLE_HEIGHT || 30; + const SLOT = LiteGraph.NODE_SLOT_HEIGHT || 20; + const WIDGET_SPACING = 4; + const BOTTOM_PADDING = 15; + + let widgetY = HEADER; + + for (const w of node.widgets) + { + if (w === previewWidget) break; + const size = w.computeSize?.(node.size[0]); + widgetY += (size?.[1] || 20) + WIDGET_SPACING; + } + + const inputCount = node.inputs?.length || 0; + const inputsHeight = inputCount * SLOT - SLOT; + + const available = node.size[1] - widgetY - inputsHeight - BOTTOM_PADDING; + + previewWidget.inputEl.style.height = Math.max(available, 20) + "px"; + }; - const origOnResize = node.onResize; + const origOnResize = node.onResize; - node.onResize = function() - { - if (origOnResize) - origOnResize.apply(this, arguments); + node.onResize = function() + { + if (origOnResize) + origOnResize.apply(this, arguments); - syncPreviewHeight(); - }; + syncPreviewHeight(); + }; - // force correct node size first - requestAnimationFrame(() => - { - if (node.computeSize) - { - const newSize = node.computeSize(); - node.setSize(newSize); - } - - syncPreviewHeight(); - - node.setDirtyCanvas(true, true); - }); - - setTimeout(() => - { - if (node.computeSize) - { - const newSize = node.computeSize(); - node.setSize(newSize); - } - - syncPreviewHeight(); - - node.setDirtyCanvas(true, true); - - }, 100); - } + // force correct node size first + requestAnimationFrame(() => + { + if (node.computeSize) + { + const newSize = node.computeSize(); + node.setSize(newSize); + } + + syncPreviewHeight(); + + node.setDirtyCanvas(true, true); + }); + + setTimeout(() => + { + if (node.computeSize) + { + const newSize = node.computeSize(); + node.setSize(newSize); + } + + syncPreviewHeight(); + + node.setDirtyCanvas(true, true); + + }, 100); + } - const graph = app.graph; + const graph = app.graph; - const update = () => - refreshNode( - node, - graph, - previewWidget, - syncPreviewHeight - ); + const update = () => + refreshNode( + node, + graph, + previewWidget, + syncPreviewHeight + ); - const origConnectionsChange = node.onConnectionsChange; + const origConnectionsChange = node.onConnectionsChange; - node.onConnectionsChange = function () - { - if (origConnectionsChange) - origConnectionsChange.apply(this, arguments); + node.onConnectionsChange = function () + { + if (origConnectionsChange) + origConnectionsChange.apply(this, arguments); - update(); - }; + update(); + }; - const origWidgetChanged = node.onWidgetChanged; + const origWidgetChanged = node.onWidgetChanged; - node.onWidgetChanged = function () - { - if (origWidgetChanged) - origWidgetChanged.apply(this, arguments); + node.onWidgetChanged = function () + { + if (origWidgetChanged) + origWidgetChanged.apply(this, arguments); - update(); - }; + update(); + }; - const timer = setInterval(() => - { - if (!graph._nodes.includes(node)) - { - clearInterval(timer); - return; - } + const timer = setInterval(() => + { + if (!graph._nodes.includes(node)) + { + clearInterval(timer); + return; + } - update(); + update(); - }, 500); + }, 500); - update(); - } + update(); + } -}); +}); \ No newline at end of file diff --git a/web/dragos_dynamic.js.txt b/web/dragos_dynamic.js.txt deleted file mode 100644 index 1425b90..0000000 --- a/web/dragos_dynamic.js.txt +++ /dev/null @@ -1,707 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; - - -function isNodeActive(node) -{ - if (!node) return false; - - if (node.mode === 2) return false; - if (node.mode === 4) return false; - - return true; -} - -function getWidgetValue(node, names) -{ - if (!node.widgets) return null; - - for (const name of names) - { - const w = node.widgets.find(w => w.name === name); - if (w) return w.value; - } - - return null; -} - -function deepMergeObjects(base, override) -{ - const result = { ...base }; - - for (const key in override) - { - if ( - key in result && - typeof result[key] === "object" && - result[key] !== null && - !Array.isArray(result[key]) && - typeof override[key] === "object" && - override[key] !== null && - !Array.isArray(override[key]) - ) - { - result[key] = deepMergeObjects(result[key], override[key]); - } - else - { - result[key] = override[key]; - } - } - - return result; -} - -function getValueRecursive(node, graph) -{ - if (!node) return undefined; - if (!isNodeActive(node)) return undefined; - - if (node.comfyClass === "DragosVariable") - return getWidgetValue(node, ["text"]) ?? ""; - - if (node.comfyClass === "DragosStructuredBuilder") - { - // First, build the base result from widgets (schema data) - const result = {}; - - for (const widget of node.widgets || []) - { - if (!widget.dragosPath) continue; - if (widget.hidden) continue; - if (widget.value === "" || widget.value == null) continue; - - const parts = widget.dragosPath.split("."); - let current = result; - - for (let i = 0; i < parts.length - 1; i++) - { - const part = parts[i]; - - if (!(part in current)) - current[part] = {}; - - current = current[part]; - } - - current[parts[parts.length - 1]] = widget.value; - } - - // Now merge any connected inputs (DragosVariable or DragosObject) - if (node.inputs) - { - for (const input of node.inputs) - { - if (!input || input.link == null) continue; - if (!input.name || !input.name.startsWith("input_")) continue; - - const link = graph.links[input.link]; - if (!link) continue; - - const origin = graph.getNodeById(link.origin_id); - if (!origin || !isNodeActive(origin)) continue; - - // Get the name from the connected node - const inputName = getWidgetValue(origin, ["var_name", "obj_name"]); - const inputValue = getValueRecursive(origin, graph); - - if (inputName && inputValue !== undefined) - { - // If it's a simple value, add/override directly - // If it's an object, merge it - if (typeof inputValue === "object" && inputValue !== null) - { - result[inputName] = deepMergeObjects(result[inputName] || {}, inputValue); - } - else - { - result[inputName] = inputValue; - } - } - } - } - - return result; - } - - if (node.comfyClass === "DragosObject") - { - const result = {}; - - for (const input of node.inputs || []) - { - if (input.link == null) continue; - - const link = graph.links[input.link]; - if (!link) continue; - - const origin = graph.getNodeById(link.origin_id); - if (!isNodeActive(origin)) continue; - - const key = - getWidgetValue(origin, ["var_name", "obj_name"]) - ?? input.name; - - const value = getValueRecursive(origin, graph); - - if (value !== undefined) - result[key] = value; - } - - return result; - } - - return undefined; -} - - -function refreshNode(node, graph, previewWidget, syncPreviewHeight) -{ - if (!node || !graph || !node.inputs) - return; - - - //////////////////////////////////////////////////////////// - // STEP 1 — Collect ordered origin nodes (stable order) - //////////////////////////////////////////////////////////// - - const orderedOrigins = []; - - for (let i = 0; i < node.inputs.length; i++) - { - const input = node.inputs[i]; - - if (!input || input.link == null) - continue; - - const link = graph.links[input.link]; - if (!link) - continue; - - const origin = graph.getNodeById(link.origin_id); - - if (!origin) - continue; - - if (!isNodeActive(origin)) - continue; - - orderedOrigins.push(origin); - } - - - - //////////////////////////////////////////////////////////// - // STEP 2 — Build intermediate structure - //////////////////////////////////////////////////////////// - - const built = []; - - for (let i = 0; i < orderedOrigins.length; i++) - { - const origin = orderedOrigins[i]; - - const value = - getValueRecursive(origin, graph); - - if ( - value == null || - (typeof value === "object" && - Object.keys(value).length === 0) - ) - continue; - - - let category = ""; - let extension = ""; - - - // Read _meta from json_data widget ONCE - const jsonWidget = - origin.widgets?.find( - w => w.name === "json_data" - ); - - if (jsonWidget?.value) - { - try - { - const parsed = - JSON.parse(jsonWidget.value); - - const meta = parsed?._meta; - - if (meta) - { - category = - String( - meta.display_name ?? - meta.displayName ?? - meta.category ?? - "" - ).toLowerCase(); - - extension = - String( - meta.extension ?? "" - ).toLowerCase(); - } - } - catch {} - } - - - // Fallback category detection - if (!category) - { - category = - String( - getWidgetValue( - origin, - ["category", "obj_name", "var_name"] - ) || "" - ).toLowerCase(); - } - - - built.push({ - category, - extension, - value, - sceneKey: null - }); - } - - - - //////////////////////////////////////////////////////////// - // STEP 3 — Merge into final scene - //////////////////////////////////////////////////////////// - - const scene = {}; - - let characterCount = 0; - - - for (let i = 0; i < built.length; i++) - { - const entry = built[i]; - - //////////////////////////////////////////////////////// - // TOP-LEVEL - //////////////////////////////////////////////////////// - - if (!entry.extension) - { - if (entry.category === "character") - { - characterCount++; - - const key = - `character_${characterCount}`; - - // Check if key already exists and merge if both are objects - if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") - { - scene[key] = deepMergeObjects(scene[key], entry.value); - } - else - { - scene[key] = entry.value; - } - - entry.sceneKey = key; - } - else if (entry.category) - { - const key = entry.category; - - // Check if key already exists and merge if both are objects - if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object") - { - scene[key] = deepMergeObjects(scene[key], entry.value); - } - else - { - scene[key] = entry.value; - } - - entry.sceneKey = key; - } - - continue; - } - - - //////////////////////////////////////////////////////// - // EXTENSION MERGE - //////////////////////////////////////////////////////// - - const targetCategory = - entry.extension; - - for (let j = i - 1; j >= 0; j--) - { - const candidate = built[j]; - - if ( - candidate.category !== targetCategory - ) - continue; - - const targetKey = - candidate.sceneKey; - - if (!targetKey) - break; - - if (!scene[targetKey]) - scene[targetKey] = {}; - - // Use deep merge instead of Object.assign for nested objects - scene[targetKey] = deepMergeObjects(scene[targetKey], entry.value); - - break; - } - } - - - - //////////////////////////////////////////////////////////// - // STEP 4 — Remove empty objects (deep clean) - //////////////////////////////////////////////////////////// - - function removeEmpty(obj) - { - if (!obj || typeof obj !== "object") - return obj; - - for (const key in obj) - { - const val = - removeEmpty(obj[key]); - - if ( - val == null || - (typeof val === "object" && - Object.keys(val).length === 0) - ) - { - delete obj[key]; - } - } - - return obj; - } - - removeEmpty(scene); - - - - //////////////////////////////////////////////////////////// - // STEP 5 — Convert to JSON - //////////////////////////////////////////////////////////// - - const json = - JSON.stringify(scene, null, "\t"); - - - - //////////////////////////////////////////////////////////// - // STEP 6 — Update preview widget (only if changed) - //////////////////////////////////////////////////////////// - - if ( - previewWidget && - previewWidget.value !== json - ) - { - previewWidget.value = json; - - if (syncPreviewHeight) - syncPreviewHeight(); - } - - - - //////////////////////////////////////////////////////////// - // STEP 7 — Hidden output widget - //////////////////////////////////////////////////////////// - - let hiddenOutput = - node.widgets.find( - w => w.name === "output_json_string" - ); - - if (!hiddenOutput) - { - hiddenOutput = - node.addWidget( - "text", - "output_json_string", - "", - () => {} - ); - - hiddenOutput.hidden = true; - hiddenOutput.computeSize = - () => [0, -8]; - hiddenOutput.draw = - () => {}; - } - - hiddenOutput.value = json; - - - - //////////////////////////////////////////////////////////// - // STEP 8 — Dynamic inputs (CORRECT INDEX-SAFE VERSION) - //////////////////////////////////////////////////////////// - - const dynamicInputs = node.inputs - .map((slot, realIndex) => ({ - slot, - realIndex, - index: ( - slot && - slot.name && - slot.name.startsWith("input_") - ) - ? parseInt(slot.name.substring(6)) || 0 - : -1 - })) - .filter(x => x.index >= 0) - .sort((a, b) => a.index - b.index); - - - if (dynamicInputs.length === 0) - return; - - - let changed = false; - - - //////////////////////////////////////////////////////////// - // Add new slot if last is connected - //////////////////////////////////////////////////////////// - - const last = dynamicInputs[dynamicInputs.length - 1]; - - if (last.slot.link != null) - { - node.addInput( - `input_${last.index + 1}`, - "PROMPT_VAR" - ); - - changed = true; - } - - - //////////////////////////////////////////////////////////// - // Remove trailing empty slots SAFELY using REAL INDEX - //////////////////////////////////////////////////////////// - - while (dynamicInputs.length > 1) - { - const lastEntry = - dynamicInputs[dynamicInputs.length - 1]; - - const prevEntry = - dynamicInputs[dynamicInputs.length - 2]; - - if ( - lastEntry.slot.link == null && - prevEntry.slot.link == null - ) - { - // find CURRENT real index (important) - const currentRealIndex = - node.inputs.findIndex( - s => s === lastEntry.slot - ); - - if (currentRealIndex !== -1) - { - node.removeInput(currentRealIndex); - - dynamicInputs.pop(); - - changed = true; - - continue; - } - } - - break; - } - - - if (changed) - { - node.setDirtyCanvas(true, true); - } -} - - - -app.registerExtension({ - - name: "Dragos.SceneBuilder.Dynamic", - - async nodeCreated(node) - { - if ( - node.comfyClass !== "DragosObject" - && - node.comfyClass !== "DragosSceneCompiler" - && - node.comfyClass !== "DragosStructuredBuilder" - ) return; - - let previewWidget = null; - let syncPreviewHeight = null; - - if (node.comfyClass === "DragosSceneCompiler") - { - previewWidget = - ComfyWidgets.STRING( - node, - "preview", - ["STRING", { multiline: true }], - app - ).widget; - - previewWidget.inputEl.readOnly = true; - previewWidget.inputEl.style.fontSize = "11px"; - previewWidget.inputEl.style.resize = "none"; - - - syncPreviewHeight = function() - { - if (!previewWidget?.inputEl) return; - if (!node.size) return; - - const HEADER = LiteGraph.NODE_TITLE_HEIGHT || 30; - const SLOT = LiteGraph.NODE_SLOT_HEIGHT || 20; - const WIDGET_SPACING = 4; - const BOTTOM_PADDING = 15; // Creates the consistent buffer at the bottom - - let widgetY = HEADER; - - // Calculate the Y position where the preview widget starts - for (const w of node.widgets) - { - if (w === previewWidget) break; - const size = w.computeSize?.(node.size[0]); - widgetY += (size?.[1] || 20) + WIDGET_SPACING; - } - - // Calculate height occupied by inputs on the left - // We subtract this to create the "buffer" so the text box doesn't overlap the inputs area - const inputCount = node.inputs?.length || 0; - const inputsHeight = inputCount * SLOT - SLOT; - - // Calculate available height - // Node Height - (Widgets Above) - (Inputs Height) - (Bottom Padding) - const available = node.size[1] - widgetY - inputsHeight - BOTTOM_PADDING; - - // Set height. - // We use Math.max(20) instead of 50. - // This allows the box to be small enough to FIT in small nodes (fixing Image_1) - // while the calculation above ensures it stops before the inputs (fixing Image_3). - previewWidget.inputEl.style.height = Math.max(available, 20) + "px"; - }; - - - const origOnResize = node.onResize; - - node.onResize = function() - { - if (origOnResize) - origOnResize.apply(this, arguments); - - syncPreviewHeight(); - }; - - - // force correct node size first - requestAnimationFrame(() => - { - if (node.computeSize) - { - const newSize = node.computeSize(); - node.setSize(newSize); - } - - syncPreviewHeight(); - - node.setDirtyCanvas(true, true); - }); - - setTimeout(() => - { - if (node.computeSize) - { - const newSize = node.computeSize(); - node.setSize(newSize); - } - - syncPreviewHeight(); - - node.setDirtyCanvas(true, true); - - }, 100); - } - - - const graph = app.graph; - - const update = () => - refreshNode( - node, - graph, - previewWidget, - syncPreviewHeight - ); - - - const origConnectionsChange = node.onConnectionsChange; - - node.onConnectionsChange = function () - { - if (origConnectionsChange) - origConnectionsChange.apply(this, arguments); - - update(); - }; - - - const origWidgetChanged = node.onWidgetChanged; - - node.onWidgetChanged = function () - { - if (origWidgetChanged) - origWidgetChanged.apply(this, arguments); - - update(); - }; - - - const timer = setInterval(() => - { - if (!graph._nodes.includes(node)) - { - clearInterval(timer); - return; - } - - update(); - - }, 500); - - - update(); - } - -}); diff --git a/web/dragos_structured_builder.js b/web/dragos_structured_builder.js index 65680de..aad3351 100644 --- a/web/dragos_structured_builder.js +++ b/web/dragos_structured_builder.js @@ -6,600 +6,647 @@ const EXTENSION_NAME = "Dragos-SceneBuilder"; // Load schema // async function loadSchema(category) { - try { - const res = await fetch(`/extensions/${EXTENSION_NAME}/schema/${category}.json?t=${Date.now()}`); - if (!res.ok) throw new Error(res.status); - return await res.json(); - } catch (err) { - console.error("Schema load failed:", err); - return null; - } + try { + const res = await fetch(`/extensions/${EXTENSION_NAME}/schema/${category}.json?t=${Date.now()}`); + if (!res.ok) throw new Error(res.status); + return await res.json(); + } catch (err) { + console.error("Schema load failed:", err); + return null; + } } // // Utility // function prettifyLabel(key) { - return key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + return key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); } // // Clear dynamic widgets // function clearGeneratedWidgets(node) { - node.widgets = node.widgets.filter(w => w.name === "category"); + node.widgets = node.widgets.filter(w => w.name === "category"); } // // Apply visibility & layout // function applyWidgetVisibility(node) { - if (!node.size) node.size = [200, 100]; // default fallback + if (!node.size) node.size = [200, 100]; // default fallback - let y = LiteGraph.NODE_WIDGET_HEIGHT || 20; + let y = LiteGraph.NODE_WIDGET_HEIGHT || 20; - for (const w of node.widgets) { - if (!w) continue; + for (const w of node.widgets) { + if (!w) continue; - if (w.hidden) { - w.computeSize = () => [0, -4]; - } + if (w.hidden) { + w.computeSize = () => [0, -4]; + } - if (!w.computeSize) { - w.computeSize = LiteGraph.WIDGET_DEFAULT?.computeSize || (() => [node.size[0], 20]); - } + if (!w.computeSize) { + w.computeSize = LiteGraph.WIDGET_DEFAULT?.computeSize || (() => [node.size[0], 20]); + } - const width = node.size[0] || 200; - const size = w.computeSize(width); + const width = node.size[0] || 200; + const size = w.computeSize(width); - w.last_y = y; - y += (size[1] || 20) + 4; - } + w.last_y = y; + y += (size[1] || 20) + 4; + } - node.size[1] = y + 8; + node.size[1] = y + 8; } // // Show popup for "other" dropdown values // function attachOtherHandler(widget) { - // Store the original callback if it exists - const origCallback = widget.callback; + // Store the original callback if it exists + const origCallback = widget.callback; - widget.callback = () => { - // Run the original callback first - if (origCallback) { - origCallback.call(widget); - } + widget.callback = () => { + // Run the original callback first + if (origCallback) { + origCallback.call(widget); + } - // Check if the selected value is "other" - if (widget.value === "other") { - const userInput = prompt(`Enter custom value for ${widget.name}:`, ""); - - if (userInput !== null && userInput !== "") { - // Set the widget value to the custom input - widget.value = userInput; - } else { - // If cancelled or empty, revert to the first option - widget.value = widget.options.values[0] || ""; - } - } + // Check if the selected value is "other" + if (widget.value === "other") { + const userInput = prompt(`Enter custom value for ${widget.name}:`, ""); + + if (userInput !== null && userInput !== "") { + // Set the widget value to the custom input + widget.value = userInput; + } else { + // If cancelled or empty, revert to the first option + widget.value = widget.options.values[0] || ""; + } + } - // Trigger visibility update in case conditions depend on this value - evaluateConditions(widget.node); - applyWidgetVisibility(widget.node); - app.graph.setDirtyCanvas(true, true); - }; + // Trigger visibility update in case conditions depend on this value + evaluateConditions(widget.node); + applyWidgetVisibility(widget.node); + app.graph.setDirtyCanvas(true, true); + }; } // // Build widgets recursively // function buildWidgetsFromSchema(node, schema, path = "", inheritedConditions = {}) { - const mergedConditions = { ...inheritedConditions, ...(schema._conditions || {}) }; + const mergedConditions = { ...inheritedConditions, ...(schema._conditions || {}) }; - // Capture static values from _static (displayed as labels, included in output, never restored) - if (schema._static) { - for (const key in schema._static) { - const staticLabel = { - type: "dragos_static_label", - name: prettifyLabel(key), - value: schema._static[key], - dragosPath: key, // Allows buildNestedObjectFromWidgets to pick it up automatically - isStatic: true, - hidden: false, - options: {}, - computeSize(width) { - return this.hidden ? [0, -4] : [width, 20]; - }, - draw(ctx, node, width, y, height) { - if (this.hidden) return; - // Draw as a gray, italic label (e.g., "Gender: Male") - ctx.fillStyle = "#888"; - ctx.font = "italic 13px Arial"; - ctx.fillText(`${this.name}: ${this.value}`, 10, y + 14); - } - }; - node.widgets.push(staticLabel); - } - } + // Capture static values from _static (displayed as labels, included in output, never restored) + if (schema._static) { + for (const key in schema._static) { + const staticLabel = { + type: "dragos_static_label", + name: prettifyLabel(key), + value: schema._static[key], + dragosPath: key, // Allows buildNestedObjectFromWidgets to pick it up automatically + isStatic: true, + hidden: false, + options: {}, + computeSize(width) { + return this.hidden ? [0, -4] : [width, 20]; + }, + draw(ctx, node, width, y, height) { + if (this.hidden) return; + // Draw as a gray, italic label (e.g., "Gender: Male") + ctx.fillStyle = "#888"; + ctx.font = "italic 13px Arial"; + ctx.fillText(`${this.name}: ${this.value}`, 10, y + 14); + } + }; + node.widgets.push(staticLabel); + } + } - for (const key in schema) { - if (key.startsWith("_")) continue; + for (const key in schema) { + if (key.startsWith("_")) continue; - const value = schema[key]; - const fullPath = path ? `${path}.${key}` : key; - const meta = value?._meta || {}; - const label = meta.display_name || prettifyLabel(key); - const depth = path ? path.split(".").length : 0; + const value = schema[key]; + const fullPath = path ? `${path}.${key}` : key; + const meta = value?._meta || {}; + const label = meta.display_name || prettifyLabel(key); + const depth = path ? path.split(".").length : 0; - // 1. Dropdown (Array) - if (Array.isArray(value)) { - const widget = node.addWidget( - "combo", - label, - value[0], - // We pass null here because we will attach the handler manually below - null, - { values: value } - ); - - // Attach the "other" popup handler - attachOtherHandler(widget); + // 1. Dropdown (Array) + if (Array.isArray(value)) { + const widget = node.addWidget( + "combo", + label, + value[0], + // We pass null here because we will attach the handler manually below + null, + { values: value } + ); + + // Attach the "other" popup handler + attachOtherHandler(widget); - widget.dragosPath = fullPath; - widget.dragosConditions = { ...mergedConditions }; - continue; - } + widget.dragosPath = fullPath; + widget.dragosConditions = { ...mergedConditions }; + continue; + } - // 2. Nested object -> Header - if (typeof value === "object" && value !== null) { - const header = { - type: "dragos_header", - name: label, - value: null, - options: {}, - size: [node.size[0], 20], - dragosConditions: { ...mergedConditions }, - computeSize(width) { return this.hidden ? [0, -4] : [width, 20]; }, - draw(ctx, node, width, y, height) { - if (this.hidden) return; - ctx.fillStyle = "#aaa"; - ctx.font = "bold 14px Arial"; - const indent = 10 + depth * 18; - ctx.fillText(label, indent, y + 15); - } - }; - node.widgets.push(header); - buildWidgetsFromSchema(node, value, fullPath, mergedConditions); - continue; - } + // 2. Nested object -> Header + if (typeof value === "object" && value !== null) { + const header = { + type: "dragos_header", + name: label, + value: null, + options: {}, + size: [node.size[0], 20], + dragosConditions: { ...mergedConditions }, + computeSize(width) { return this.hidden ? [0, -4] : [width, 20]; }, + draw(ctx, node, width, y, height) { + if (this.hidden) return; + ctx.fillStyle = "#aaa"; + ctx.font = "bold 14px Arial"; + const indent = 10 + depth * 18; + ctx.fillText(label, indent, y + 15); + } + }; + node.widgets.push(header); + buildWidgetsFromSchema(node, value, fullPath, mergedConditions); + continue; + } - // 3. Text field (String) - if (typeof value === "string") { - // Determine default value: - // If value is "string" (type hint) or empty "", use empty string. - // Otherwise, use the value itself (e.g., "Eiko"). - let defaultValue = ""; - if (value !== "") { - defaultValue = value; - } + // 3. Text field (String) + if (typeof value === "string") { + // Determine default value: + // If value is "string" (type hint) or empty "", use empty string. + // Otherwise, use the value itself (e.g., "Eiko"). + let defaultValue = ""; + if (value !== "") { + defaultValue = value; + } - const widget = node.addWidget("text", label, defaultValue, () => { - evaluateConditions(node); - applyWidgetVisibility(node); - app.graph.setDirtyCanvas(true, true); - }); - widget.dragosPath = fullPath; - widget.dragosConditions = { ...mergedConditions }; - } - } + const widget = node.addWidget("text", label, defaultValue, () => { + evaluateConditions(node); + applyWidgetVisibility(node); + app.graph.setDirtyCanvas(true, true); + }); + widget.dragosPath = fullPath; + widget.dragosConditions = { ...mergedConditions }; + } + } } // // Evaluate conditions // function evaluateConditions(node) { - const values = {}; - for (const w of node.widgets) { - if (!w.dragosPath) continue; - values[w.dragosPath.split(".").pop()] = w.value; - } - for (const w of node.widgets) { - if (!w.dragosConditions) continue; - let visible = true; - for (const condKey in w.dragosConditions) { - const allowed = w.dragosConditions[condKey]; - const current = values[condKey]; - if (!current || !allowed.includes(current)) { - visible = false; - break; - } - } - w.hidden = !visible; - } + const values = {}; + for (const w of node.widgets) { + if (!w.dragosPath) continue; + values[w.dragosPath.split(".").pop()] = w.value; + } + for (const w of node.widgets) { + if (!w.dragosConditions) continue; + let visible = true; + for (const condKey in w.dragosConditions) { + const allowed = w.dragosConditions[condKey]; + const current = values[condKey]; + if (!current || !allowed.includes(current)) { + visible = false; + break; + } + } + w.hidden = !visible; + } } // // Deep merge objects // function deepMergeObjects(base, override) { - const result = { ...base }; + const result = { ...base }; - for (const key in override) { - if ( - key in result && - typeof result[key] === "object" && - result[key] !== null && - !Array.isArray(result[key]) && - typeof override[key] === "object" && - override[key] !== null && - !Array.isArray(override[key]) - ) - { - result[key] = deepMergeObjects(result[key], override[key]); - } - else - { - result[key] = override[key]; - } - } + for (const key in override) { + if ( + key in result && + typeof result[key] === "object" && + result[key] !== null && + !Array.isArray(result[key]) && + typeof override[key] === "object" && + override[key] !== null && + !Array.isArray(override[key]) + ) + { + result[key] = deepMergeObjects(result[key], override[key]); + } + else + { + result[key] = override[key]; + } + } - return result; + return result; } // // Get value from connected nodes // function getInputValue(node, graph) { - if (!node) return undefined; - if (node.mode === 2 || node.mode === 4) return undefined; + if (!node) return undefined; + if (node.mode === 2 || node.mode === 4) return undefined; - // Get widget value helper - const getWidgetValue = (n, names) => { - if (!n.widgets) return null; - for (const name of names) { - const w = n.widgets.find(w => w.name === name); - if (w) return w.value; - } - return null; - }; + // Get widget value helper + const getWidgetValue = (n, names) => { + if (!n.widgets) return null; + for (const name of names) { + const w = n.widgets.find(w => w.name === name); + if (w) return w.value; + } + return null; + }; - // DragosVariable returns its text value - if (node.comfyClass === "DragosVariable") { - return getWidgetValue(node, ["text"]) ?? ""; - } + // DragosVariable returns its text value + if (node.comfyClass === "DragosVariable") { + return getWidgetValue(node, ["text"]) ?? ""; + } - // DragosObject returns merged object from all inputs - if (node.comfyClass === "DragosObject") { - const result = {}; - for (const input of node.inputs || []) { - if (input.link == null) continue; - const link = graph.links[input.link]; - if (!link) continue; - const origin = graph.getNodeById(link.origin_id); - if (!origin || origin.mode === 2 || origin.mode === 4) continue; + // DragosStructuredBuilder returns its compiled data object + if (node.comfyClass === "DragosStructuredBuilder") { + const jsonWidget = node.widgets?.find(w => w.name === "json_data"); + if (jsonWidget?.value) { + try { + const parsed = JSON.parse(jsonWidget.value); + return parsed.data || {}; + } catch (e) { + return {}; + } + } + return {}; + } - const key = getWidgetValue(origin, ["var_name", "obj_name"]) ?? input.name; - const value = getInputValue(origin, graph); - if (value !== undefined) { - result[key] = value; - } - } - return result; - } + // DragosObject returns merged object from all inputs + if (node.comfyClass === "DragosObject") { + const result = {}; + for (const input of node.inputs || []) { + if (input.link == null) continue; + const link = graph.links[input.link]; + if (!link) continue; + const origin = graph.getNodeById(link.origin_id); + if (!origin || origin.mode === 2 || origin.mode === 4) continue; - return undefined; + const key = getWidgetValue(origin, ["var_name", "obj_name"]) ?? input.name; + const value = getInputValue(origin, graph); + if (value !== undefined) { + result[key] = value; + } + } + return result; + } + + return undefined; } // // Update json_data with connected inputs // function updateJsonWithInputs(node, graph) { - const jsonWidget = node.widgets?.find(w => w.name === "json_data"); - if (!jsonWidget?.value) return; + const jsonWidget = node.widgets?.find(w => w.name === "json_data"); + if (!jsonWidget?.value) return; - try { - const parsed = JSON.parse(jsonWidget.value); - const baseData = parsed.data || {}; - const meta = parsed._meta || {}; + try { + const parsed = JSON.parse(jsonWidget.value); + const baseData = parsed.data || {}; + const meta = parsed._meta || {}; - // Collect input values - const inputValues = {}; - if (node.inputs) { - for (const input of node.inputs) { - if (!input || input.link == null) continue; - if (!input.name || !input.name.startsWith("input_")) continue; + // Collect input values + const inputValues = {}; + const extensions = []; - const link = graph.links[input.link]; - if (!link) continue; + if (node.inputs) { + for (const input of node.inputs) { + if (!input || input.link == null) continue; + if (!input.name || !input.name.startsWith("input_")) continue; - const origin = graph.getNodeById(link.origin_id); - if (!origin || origin.mode === 2 || origin.mode === 4) continue; + const link = graph.links[input.link]; + if (!link) continue; - const inputName = - origin.widgets?.find(w => w.name === "var_name")?.value || - origin.widgets?.find(w => w.name === "obj_name")?.value; + const origin = graph.getNodeById(link.origin_id); + if (!origin || origin.mode === 2 || origin.mode === 4) continue; - const inputValue = getInputValue(origin, graph); + const inputValue = getInputValue(origin, graph); + if (inputValue === undefined) continue; - if (inputName && inputValue !== undefined) { - if (typeof inputValue === "object" && inputValue !== null) { - inputValues[inputName] = deepMergeObjects(inputValues[inputName] || {}, inputValue); - } else { - inputValues[inputName] = inputValue; - } - } - } - } + // Check if origin is a StructuredBuilder with an extension meta + const originJsonWidget = origin.widgets?.find(w => w.name === "json_data"); + let isExtension = false; + let extensionTarget = ""; - // Merge input values with base data - const mergedData = deepMergeObjects(baseData, inputValues); + if (originJsonWidget?.value) { + try { + const parsedOrigin = JSON.parse(originJsonWidget.value); + if (parsedOrigin._meta?.extension) { + isExtension = true; + extensionTarget = parsedOrigin._meta.extension; + } + } catch (e) {} + } - // Update json_data - const updated = { - _meta: meta, - data: mergedData - }; - jsonWidget.value = JSON.stringify(updated); - node.properties = node.properties || {}; - node.properties.json_data = jsonWidget.value; + if (isExtension) { + // ONLY merge if the extension target matches THIS node's category + if (extensionTarget === meta.category) { + if (typeof inputValue === "object" && inputValue !== null) { + extensions.push(inputValue); + } + } + } else { + // Standard DragosVariable / DragosObject behavior + const inputName = + origin.widgets?.find(w => w.name === "var_name")?.value || + origin.widgets?.find(w => w.name === "obj_name")?.value; - } catch (e) { - console.error("Failed to update json_data with inputs:", e); - } + if (inputName) { + if (typeof inputValue === "object" && inputValue !== null) { + inputValues[inputName] = deepMergeObjects(inputValues[inputName] || {}, inputValue); + } else { + inputValues[inputName] = inputValue; + } + } + } + } + } + + // 1. Merge standard input values with base data + let mergedData = deepMergeObjects(baseData, inputValues); + + // 2. Deep merge extensions directly into the root data + for (const ext of extensions) { + mergedData = deepMergeObjects(mergedData, ext); + } + + // Update json_data + const updated = { + _meta: meta, + data: mergedData + }; + jsonWidget.value = JSON.stringify(updated); + node.properties = node.properties || {}; + node.properties.json_data = jsonWidget.value; + + } catch (e) { + console.error("Failed to update json_data with inputs:", e); + } } // // Manage dynamic inputs for DragosStructuredBuilder // function manageDynamicInputs(node, graph) { - if (!node.inputs) return; + if (!node.inputs) return; - // Filter dynamic inputs (input_1, input_2, etc.) - const dynamicInputs = node.inputs - .map((slot, realIndex) => ({ - slot, - realIndex, - index: ( - slot && - slot.name && - slot.name.startsWith("input_") - ) - ? parseInt(slot.name.substring(6)) || 0 - : -1 - })) - .filter(x => x.index >= 0) - .sort((a, b) => a.index - b.index); + // Filter dynamic inputs (input_1, input_2, etc.) + const dynamicInputs = node.inputs + .map((slot, realIndex) => ({ + slot, + realIndex, + index: ( + slot && + slot.name && + slot.name.startsWith("input_") + ) + ? parseInt(slot.name.substring(6)) || 0 + : -1 + })) + .filter(x => x.index >= 0) + .sort((a, b) => a.index - b.index); - if (dynamicInputs.length === 0) return; + if (dynamicInputs.length === 0) return; - let changed = false; + let changed = false; - // Add new slot if last is connected - const last = dynamicInputs[dynamicInputs.length - 1]; + // Add new slot if last is connected + const last = dynamicInputs[dynamicInputs.length - 1]; - if (last.slot.link != null) { - node.addInput( - `input_${last.index + 1}`, - "PROMPT_VAR" - ); - changed = true; - } + if (last.slot.link != null) { + node.addInput( + `input_${last.index + 1}`, + "PROMPT_VAR" + ); + changed = true; + } - // Remove trailing empty slots - while (dynamicInputs.length > 1) { - const lastEntry = dynamicInputs[dynamicInputs.length - 1]; - const prevEntry = dynamicInputs[dynamicInputs.length - 2]; + // Remove trailing empty slots + while (dynamicInputs.length > 1) { + const lastEntry = dynamicInputs[dynamicInputs.length - 1]; + const prevEntry = dynamicInputs[dynamicInputs.length - 2]; - if (lastEntry.slot.link == null && prevEntry.slot.link == null) { - const currentRealIndex = node.inputs.findIndex(s => s === lastEntry.slot); - if (currentRealIndex !== -1) { - node.removeInput(currentRealIndex); - dynamicInputs.pop(); - changed = true; - continue; - } - } - break; - } + if (lastEntry.slot.link == null && prevEntry.slot.link == null) { + const currentRealIndex = node.inputs.findIndex(s => s === lastEntry.slot); + if (currentRealIndex !== -1) { + node.removeInput(currentRealIndex); + dynamicInputs.pop(); + changed = true; + continue; + } + } + break; + } - if (changed) { - node.setDirtyCanvas(true, true); - } + if (changed) { + node.setDirtyCanvas(true, true); + } - // Update json_data with connected inputs - updateJsonWithInputs(node, graph); + // Update json_data with connected inputs + updateJsonWithInputs(node, graph); } function buildNestedObjectFromWidgets(node) { - const result = {}; + const result = {}; - for (const w of node.widgets) - { - if (!w.dragosPath || w.hidden) - continue; + for (const w of node.widgets) + { + if (!w.dragosPath || w.hidden) + continue; - const value = - w._otherPopupValue !== undefined - ? w._otherPopupValue - : w.value; + const value = + w._otherPopupValue !== undefined + ? w._otherPopupValue + : w.value; - // SKIP empty values - if ( - value === "" || - value === null || - value === undefined - ) - continue; + // SKIP empty values + if ( + value === "" || + value === null || + value === undefined + ) + continue; - const keys = w.dragosPath.split("."); - let current = result; + const keys = w.dragosPath.split("."); + let current = result; - for (let i = 0; i < keys.length; i++) - { - const k = keys[i]; + for (let i = 0; i < keys.length; i++) + { + const k = keys[i]; - if (i === keys.length - 1) - { - current[k] = value; - } - else - { - if (!(k in current)) - current[k] = {}; + if (i === keys.length - 1) + { + current[k] = value; + } + else + { + if (!(k in current)) + current[k] = {}; - current = current[k]; - } - } - } + current = current[k]; + } + } + } - return result; + return result; } // // Rebuild UI safely // async function rebuild(node) { - const categoryWidget = node.widgets.find(w => w.name === "category"); - if (!categoryWidget) return; + const categoryWidget = node.widgets.find(w => w.name === "category"); + if (!categoryWidget) return; - // Load schema for selected category - const schema = await loadSchema(categoryWidget.value); - if (!schema) return; + // Load schema for selected category + const schema = await loadSchema(categoryWidget.value); + if (!schema) return; - // Parse saved data from previous json_data - let savedData = {}; - if (node.properties?.json_data) { - try { - const parsed = JSON.parse(node.properties.json_data); - savedData = parsed.data || {}; - } catch {} - } + // Parse saved data from previous json_data + let savedData = {}; + if (node.properties?.json_data) { + try { + const parsed = JSON.parse(node.properties.json_data); + savedData = parsed.data || {}; + } catch {} + } - // Clear old dynamic widgets (keep category) - node.widgets = node.widgets.filter(w => w.name === "category"); + // Clear old dynamic widgets (keep category) + node.widgets = node.widgets.filter(w => w.name === "category"); - // Rebuild widgets from schema - buildWidgetsFromSchema(node, schema); + // Rebuild widgets from schema + buildWidgetsFromSchema(node, schema); - // Restore values from savedData - for (const w of node.widgets) { - if (!w.dragosPath) continue; - if (w.isStatic) continue; // Skip static widgets - always use schema value - - const keys = w.dragosPath.split("."); - let current = savedData; + // Restore values from savedData + for (const w of node.widgets) { + if (!w.dragosPath) continue; + if (w.isStatic) continue; // Skip static widgets - always use schema value + + const keys = w.dragosPath.split("."); + let current = savedData; - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - if (i === keys.length - 1 && current && k in current) { - w.value = current[k]; - } else if (current && k in current) { - current = current[k]; - } else { - current = null; - } - } - } + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + if (i === keys.length - 1 && current && k in current) { + w.value = current[k]; + } else if (current && k in current) { + current = current[k]; + } else { + current = null; + } + } + } - // Evaluate conditional visibility & layout - evaluateConditions(node); - applyWidgetVisibility(node); + // Evaluate conditional visibility & layout + evaluateConditions(node); + applyWidgetVisibility(node); - // Build final JSON to store in hidden widget & properties - const data = buildNestedObjectFromWidgets(node); - const schemaMeta = schema._meta || {}; - const displayName = schemaMeta.display_name || schemaMeta.displayName || categoryWidget.value || ""; + // Build final JSON to store in hidden widget & properties + const data = buildNestedObjectFromWidgets(node); + const schemaMeta = schema._meta || {}; + const displayName = schemaMeta.display_name || schemaMeta.displayName || categoryWidget.value || ""; - const final = { - _meta: { - category: String(displayName).toLowerCase(), - extension: String(schemaMeta.extension || "").toLowerCase(), - schema: String(categoryWidget.value || "").toLowerCase() - }, - data: data - }; + const final = { + _meta: { + category: String(displayName).toLowerCase(), + extension: String(schemaMeta.extension || "").toLowerCase(), + schema: String(categoryWidget.value || "").toLowerCase() + }, + data: data + }; - // Store in hidden json_data widget - let jsonWidget = node.widgets.find(w => w.name === "json_data"); - if (!jsonWidget) { - jsonWidget = node.addWidget("text", "json_data", "", () => {}); - jsonWidget.hidden = true; - jsonWidget.computeSize = () => [0, -8]; - jsonWidget.draw = () => {}; - } - jsonWidget.value = JSON.stringify(final); + // Store in hidden json_data widget + let jsonWidget = node.widgets.find(w => w.name === "json_data"); + if (!jsonWidget) { + jsonWidget = node.addWidget("text", "json_data", "", () => {}); + jsonWidget.hidden = true; + jsonWidget.computeSize = () => [0, -8]; + jsonWidget.draw = () => {}; + } + jsonWidget.value = JSON.stringify(final); - // Also persist in node properties for graph save/load - node.properties = node.properties || {}; - node.properties.json_data = jsonWidget.value; + // Also persist in node properties for graph save/load + node.properties = node.properties || {}; + node.properties.json_data = jsonWidget.value; - // Hook dynamic widgets to update json_data on change - for (const w of node.widgets) { - if (!w.dragosPath) continue; + // Hook dynamic widgets to update json_data on change + for (const w of node.widgets) { + if (!w.dragosPath) continue; - const origCallback = w.callback; - w.callback = function() { - if (origCallback) origCallback.call(this); + const origCallback = w.callback; + w.callback = function() { + if (origCallback) origCallback.call(this); - const updatedData = buildNestedObjectFromWidgets(node); - const updatedFinal = { - _meta: final._meta, - data: updatedData - }; - jsonWidget.value = JSON.stringify(updatedFinal); - node.properties.json_data = jsonWidget.value; + const updatedData = buildNestedObjectFromWidgets(node); + const updatedFinal = { + _meta: final._meta, + data: updatedData + }; + jsonWidget.value = JSON.stringify(updatedFinal); + node.properties.json_data = jsonWidget.value; - evaluateConditions(node); - applyWidgetVisibility(node); - app.graph.setDirtyCanvas(true, true); - }; - } + evaluateConditions(node); + applyWidgetVisibility(node); + app.graph.setDirtyCanvas(true, true); + }; + } - // Resize node if needed - if (node.computeSize) - node.setSize(node.computeSize()); + // Resize node if needed + if (node.computeSize) + node.setSize(node.computeSize()); - app.graph.setDirtyCanvas(true, true); + app.graph.setDirtyCanvas(true, true); } // // Extension registration // app.registerExtension({ - name: "Dragos.StructuredBuilder", - nodeCreated(node) { - if (node.comfyClass !== "DragosStructuredBuilder") return; + name: "Dragos.StructuredBuilder", + nodeCreated(node) { + if (node.comfyClass !== "DragosStructuredBuilder") return; - const categoryWidget = node.widgets.find(w => w.name === "category"); - if (!categoryWidget) return; + const categoryWidget = node.widgets.find(w => w.name === "category"); + if (!categoryWidget) return; - const graph = app.graph; + const graph = app.graph; - const originalCallback = categoryWidget.callback; - categoryWidget.callback = async function(value) { - if (originalCallback) originalCallback.call(this, value); - await rebuild(node); - }; + const originalCallback = categoryWidget.callback; + categoryWidget.callback = async function(value) { + if (originalCallback) originalCallback.call(this, value); + await rebuild(node); + }; - // Handle dynamic inputs - const origConnectionsChange = node.onConnectionsChange; - node.onConnectionsChange = function() { - if (origConnectionsChange) origConnectionsChange.apply(this, arguments); - manageDynamicInputs(node, graph); - }; + // Handle dynamic inputs + const origConnectionsChange = node.onConnectionsChange; + node.onConnectionsChange = function() { + if (origConnectionsChange) origConnectionsChange.apply(this, arguments); + manageDynamicInputs(node, graph); + }; - // Periodic update for connected inputs - const timer = setInterval(() => { - if (!graph._nodes.includes(node)) { - clearInterval(timer); - return; - } - updateJsonWithInputs(node, graph); - }, 500); + // Periodic update for connected inputs + const timer = setInterval(() => { + if (!graph._nodes.includes(node)) { + clearInterval(timer); + return; + } + updateJsonWithInputs(node, graph); + }, 500); - setTimeout(() => rebuild(node), 10); - } + setTimeout(() => rebuild(node), 10); + } }); \ No newline at end of file diff --git a/web/schema/Characters/Eiko.json b/web/schema/Characters/Eiko.json index e03a52d..500e41c 100644 --- a/web/schema/Characters/Eiko.json +++ b/web/schema/Characters/Eiko.json @@ -8,46 +8,6 @@ "from": "Final Fantasy IX (FF9)" }, "age": "6", - "placement": { - "frame_position": [ - "center", - "left", - "right", - "top", - "bottom", - "top-left", - "top-right", - "bottom-left", - "bottom-right" - ], - "proximity_to_camera": [ - "", - "foreground", - "midground", - "background", - "close", - "medium", - "far" - ], - "orientation": [ - "", - "facing camera", - "facing left", - "facing right", - "facing away", - "profile left", - "profile right", - "three-quarter left", - "three-quarter right" - ], - "looking_at": [ - "", - "at camera", - "away from camera", - "other" - ], - "relative_to": "" - }, "appearance": { "physique": { "build": "petite, child", diff --git a/web/schema/character_female_animal.json b/web/schema/character_female_animal.json new file mode 100644 index 0000000..f7e5d5f --- /dev/null +++ b/web/schema/character_female_animal.json @@ -0,0 +1,161 @@ +{ + "_meta": { + "display_name": "Character" + }, + "_static": { + "gender": "Female", + "character_type": "Animal" + }, + "name": "", + "age": "", + "from": "", + "species": [ + "", + "Wolf", + "Fox", + "Feline", + "Canine", + "Equine", + "Bovine", + "Ursine", + "Reptile", + "Avian", + "Dragon", + "Lamia", + "Naga", + "Griffin", + "Unicorn", + "Wyvern", + "Kirin", + "other" + ], + "appearance": { + "body_build": [ + "", + "lean", + "athletic", + "toned", + "stocky", + "heavy", + "slender", + "lithe", + "other" + ], + "coat_type": [ + "", + "fur", + "scales", + "feathers", + "skin", + "chitin", + "other" + ], + "color_primary": [ + "", + "black", + "white", + "gray", + "brown", + "tan", + "red", + "golden", + "green", + "blue", + "other" + ], + "color_secondary": [ + "", + "black", + "white", + "gray", + "brown", + "tan", + "red", + "golden", + "green", + "blue", + "none", + "other" + ], + "pattern": [ + "", + "solid", + "striped", + "spotted", + "rosettes", + "bi-color", + "tri-color", + "brindle", + "other" + ], + "head_features": { + "ears": [ + "", + "pointed", + "rounded", + "floppy", + "horned", + "finned", + "none", + "other" + ], + "horns_antlers": [ + "", + "none", + "antlers", + "ram horns", + "straight horns", + "curved horns", + "other" + ] + }, + "special_traits": [ + "", + "wings", + "multiple tails", + "mane", + "spikes", + "frills", + "bioluminescence", + "other" + ] + }, + "anatomy": { + "genitals": { + "type": [ + "", + "standard", + "cloaca", + "other" + ], + "size": [ + "", + "small", + "average", + "large", + "other" + ], + "shape": [ + "", + "slit", + "rounded", + "other" + ] + }, + "secondary_sex_characteristics": { + "teats_presence": ["", "yes", "no"], + "teats_count": ["", "2", "4", "6", "8", "other"], + "plumpness": [ + "", + "flat", + "moderate", + "full", + "other" + ] + }, + "physique_details": { + "paws_claws": ["", "paws", "claws", "hooves", "talons", "other"], + "tail": ["", "long", "short", "fluffy", "hairless", "none", "other"] + } + } +} \ No newline at end of file diff --git a/web/schema/character_female_anthro.json b/web/schema/character_female_anthro.json index 6704b85..cb7bcc3 100644 --- a/web/schema/character_female_anthro.json +++ b/web/schema/character_female_anthro.json @@ -29,46 +29,6 @@ ], "hybrid_note": "" }, - "placement": { - "frame_position": [ - "center", - "left", - "right", - "top", - "bottom", - "top-left", - "top-right", - "bottom-left", - "bottom-right" - ], - "proximity_to_camera": [ - "", - "foreground", - "midground", - "background", - "close", - "medium", - "far" - ], - "orientation": [ - "", - "facing camera", - "facing left", - "facing right", - "facing away", - "profile left", - "profile right", - "three-quarter left", - "three-quarter right" - ], - "looking_at": [ - "", - "at camera", - "away from camera", - "other" - ], - "relative_to": "" - }, "appearance": { "body_form": [ "", diff --git a/web/schema/character_female_human.json b/web/schema/character_female_human.json index 3dd6f3f..11c03f1 100644 --- a/web/schema/character_female_human.json +++ b/web/schema/character_female_human.json @@ -8,46 +8,6 @@ "name":"", "age":"", "from":"", - "placement": { - "frame_position": [ - "center", - "left", - "right", - "top", - "bottom", - "top-left", - "top-right", - "bottom-left", - "bottom-right" - ], - "proximity_to_camera": [ - "", - "foreground", - "midground", - "background", - "close", - "medium", - "far" - ], - "orientation": [ - "", - "facing camera", - "facing left", - "facing right", - "facing away", - "profile left", - "profile right", - "three-quarter left", - "three-quarter right" - ], - "looking_at": [ - "", - "at camera", - "away from camera", - "other" - ], - "relative_to": "" - }, "appearance": { "body_form": [ "", diff --git a/web/schema/character_male_animal.json b/web/schema/character_male_animal.json new file mode 100644 index 0000000..1d9ba17 --- /dev/null +++ b/web/schema/character_male_animal.json @@ -0,0 +1,165 @@ +{ + "_meta": { + "display_name": "Character" + }, + "_static": { + "gender": "Male", + "character_type": "Animal" + }, + "name": "", + "age": "", + "from": "", + "species": [ + "", + "Wolf", + "Fox", + "Feline", + "Canine", + "Equine", + "Bovine", + "Ursine", + "Reptile", + "Avian", + "Dragon", + "Lamia", + "Naga", + "Griffin", + "Unicorn", + "Wyvern", + "Kirin", + "other" + ], + "appearance": { + "body_build": [ + "", + "lean", + "athletic", + "muscular", + "stocky", + "heavy", + "slender", + "lanky", + "other" + ], + "coat_type": [ + "", + "fur", + "scales", + "feathers", + "skin", + "chitin", + "other" + ], + "color_primary": [ + "", + "black", + "white", + "gray", + "brown", + "tan", + "red", + "golden", + "green", + "blue", + "other" + ], + "color_secondary": [ + "", + "black", + "white", + "gray", + "brown", + "tan", + "red", + "golden", + "green", + "blue", + "none", + "other" + ], + "pattern": [ + "", + "solid", + "striped", + "spotted", + "rosettes", + "bi-color", + "tri-color", + "brindle", + "other" + ], + "head_features": { + "ears": [ + "", + "pointed", + "rounded", + "floppy", + "horned", + "finned", + "none", + "other" + ], + "horns_antlers": [ + "", + "none", + "antlers", + "ram horns", + "straight horns", + "curved horns", + "other" + ] + }, + "special_traits": [ + "", + "wings", + "multiple tails", + "mane", + "spikes", + "frills", + "bioluminescence", + "other" + ] + }, + "anatomy": { + "genitals": { + "state": ["", "flaccid", "semi-erect", "fully-erect", "extended"], + "configuration": [ + "", + "humanoid", + "canine (knot)", + "equine (flare)", + "feline (barbs)", + "reptilian (slit/hemi)", + "other" + ], + "genitals_size": [ + "", + "small", + "average", + "large", + "huge" + ], + "sheath": { + "sheath_presence": ["", "yes", "no"], + "sheath_type": ["", "fur", "skin", "scales"] + }, + "knot": { + "knot_presence": ["", "yes", "no"], + "knot_size": ["", "small", "medium", "large"] + }, + "median_ring": { + "median_ring_presence": ["", "yes", "no"], + "median_ring_size": ["", "small", "medium", "large"] + }, + "pubic_fur": { + "pubic_fur_presence": ["", "yes", "no"], + "pubic_fur_density": ["", "sparse", "moderate", "dense"], + "pubic_fur_style": ["", "natural", "trimmed", "shaved"] + } + }, + "physique_details": { + "paws_claws": ["", "paws", "claws", "hooves", "talons", "other"], + "tail": ["", "long", "short", "fluffy", "hairless", "none", "other"] + } + } +} \ No newline at end of file diff --git a/web/schema/character_male_anthro.json b/web/schema/character_male_anthro.json index eba6837..087e0c2 100644 --- a/web/schema/character_male_anthro.json +++ b/web/schema/character_male_anthro.json @@ -29,46 +29,6 @@ ], "hybrid_note": "" }, - "placement": { - "frame_position": [ - "center", - "left", - "right", - "top", - "bottom", - "top-left", - "top-right", - "bottom-left", - "bottom-right" - ], - "proximity_to_camera": [ - "", - "foreground", - "midground", - "background", - "close", - "medium", - "far" - ], - "orientation": [ - "", - "facing camera", - "facing left", - "facing right", - "facing away", - "profile left", - "profile right", - "three-quarter left", - "three-quarter right" - ], - "looking_at": [ - "", - "at camera", - "away from camera", - "other" - ], - "relative_to": "" - }, "appearance": { "body_build": [ "", diff --git a/web/schema/character_male_human.json b/web/schema/character_male_human.json index 1432fcf..83bf831 100644 --- a/web/schema/character_male_human.json +++ b/web/schema/character_male_human.json @@ -8,46 +8,6 @@ "name":"", "age":"", "from":"", - "placement": { - "frame_position": [ - "center", - "left", - "right", - "top", - "bottom", - "top-left", - "top-right", - "bottom-left", - "bottom-right" - ], - "proximity_to_camera": [ - "", - "foreground", - "midground", - "background", - "close", - "medium", - "far" - ], - "orientation": [ - "", - "facing camera", - "facing left", - "facing right", - "facing away", - "profile left", - "profile right", - "three-quarter left", - "three-quarter right" - ], - "looking_at": [ - "", - "at camera", - "away from camera", - "other" - ], - "relative_to": "" - }, "appearance": { "body_build": [ "", diff --git a/web/schema/character_placement.json b/web/schema/character_placement.json new file mode 100644 index 0000000..f1a7bc1 --- /dev/null +++ b/web/schema/character_placement.json @@ -0,0 +1,65 @@ +{ + "_meta": { + "extension": "character" + }, + "character_placement": { + "frame_position": [ + "center", + "left", + "right", + "top", + "bottom", + "top-left", + "top-right", + "bottom-left", + "bottom-right" + ], + "proximity_to_camera": [ + "", + "foreground", + "midground", + "background", + "close", + "medium", + "far" + ], + "body_orientation": [ + "", + "facing camera", + "facing left", + "facing right", + "facing away", + "profile left", + "profile right", + "three-quarter left", + "three-quarter right", + "reclining", + "lying down" + ], + "head_orientation": [ + "", + "facing camera", + "facing left", + "facing right", + "facing up", + "facing down", + "profile left", + "profile right", + "tilted left", + "tilted right", + "turned back" + ], + "looking_at": [ + "", + "at camera", + "away from camera", + "up", + "down", + "left", + "right", + "closed eyes", + "other" + ], + "relative_to": "" + } +} \ No newline at end of file