Files
drago87-Dragos-SceneBuilder/web/dragos_dynamic.js
T
drago87 0bd1dccc2e Update
Added functionality for users to add a _meta extension so that they can optionally add information. See action.json and clothes.json they are optional but only makes sense when connected to a character.
extra input nodes will now purge faster
2026-02-24 14:00:44 +01:00

625 lines
12 KiB
JavaScript

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 getValueRecursive(node, graph)
{
if (!node) return undefined;
if (!isNodeActive(node)) return undefined;
if (node.comfyClass === "DragosVariable")
return getWidgetValue(node, ["text"]) ?? "";
if (node.comfyClass === "DragosStructuredBuilder")
{
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;
}
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}`;
scene[key] = entry.value;
entry.sceneKey = key;
}
else if (entry.category)
{
const key = entry.category;
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] = {};
Object.assign(
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"
) 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 PADDING = 10;
let widgetY = HEADER;
for (const w of node.widgets)
{
if (w === previewWidget) break;
const size = w.computeSize?.(node.size[0]);
widgetY += (size?.[1] || SLOT) + 4;
}
const inputsHeight =
(node.inputs?.length || 0) * SLOT;
const available =
node.size[1]
- widgetY
- inputsHeight
- PADDING;
previewWidget.inputEl.style.height =
Math.max(available, 50) + "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();
}
});