Update 1.5.0

Fixed bug
added information to schemas
added new schemas
This commit is contained in:
drago87
2026-04-05 22:45:55 +02:00
parent 7c05b9b774
commit 2505051c4f
12 changed files with 1758 additions and 2169 deletions
+273 -255
View File
@@ -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:
<info>...</info>
<prompt>...</prompt>
Format1:
<info>...</info>
<prompt>...</prompt>
Format2:
<info>...</info>
raw text
Format2:
<info>...</info>
raw text
Format3:
<prompt>...</prompt>
Format3:
<prompt>...</prompt>
Format4:
raw text
"""
Format4:
raw text
"""
info = ""
prompt = content.strip()
info = ""
prompt = content.strip()
# Extract <info> block if present
info_match = re.search(r"<info>([\s\S]*?)</info>", content, re.IGNORECASE)
if info_match:
info = info_match.group(1).strip()
# Extract <info> block if present
info_match = re.search(r"<info>([\s\S]*?)</info>", content, re.IGNORECASE)
if info_match:
info = info_match.group(1).strip()
# Extract <prompt> block if present
prompt_match = re.search(r"<prompt>([\s\S]*?)</prompt>", 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 <prompt> block if present
prompt_match = re.search(r"<prompt>([\s\S]*?)</prompt>", 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
},)
+602 -562
View File
File diff suppressed because it is too large Load Diff
-707
View File
@@ -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();
}
});
File diff suppressed because it is too large Load Diff
-40
View File
@@ -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",
+161
View File
@@ -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"]
}
}
}
-40
View File
@@ -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": [
"",
-40
View File
@@ -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": [
"",
+165
View File
@@ -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"]
}
}
}
-40
View File
@@ -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": [
"",
-40
View File
@@ -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": [
"",
+65
View File
@@ -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": ""
}
}