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
625 lines
12 KiB
JavaScript
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();
|
|
}
|
|
|
|
}); |