feat: live sync linked director controls

This commit is contained in:
OpenClaw Agent
2026-07-16 16:14:10 +00:00
parent 4dfcb1e597
commit 4c0358c11b

View File

@@ -11442,12 +11442,72 @@ app.registerExtension({
compWidget.value = 18; compWidget.value = 18;
} }
const self = this; const self = this;
this._syncGlobalPromptFromLink = function () { this._resolveLinkedInputValue = function (inputName, options = {}) {
const globalInput = self.inputs?.find(i => i.name === "global_prompt"); const graph = app.graph;
if (globalInput && globalInput.link !== null && globalInput.link !== undefined) { const visited = new Set();
const link = app.graph.links[globalInput.link]; const readNodeOutputValue = (node, slotIndex) => {
if (link) { if (!node) return null;
if (Array.isArray(node.widgets)) {
for (const widget of node.widgets) {
const value = widget?.value;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
}
}
if (Array.isArray(node.outputs) && slotIndex != null && slotIndex >= 0) {
const outputName = node.outputs[slotIndex]?.name;
if (outputName && Array.isArray(node.widgets)) {
const namedWidget = node.widgets.find(w => w?.name === outputName);
const value = namedWidget?.value;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
}
}
return null;
};
const walkLink = (linkId) => {
if (!graph || linkId == null) return null;
if (visited.has(linkId)) return null;
visited.add(linkId);
const link = graph.links?.[linkId];
if (!link) return null;
const originNode = graph.getNodeById?.(link.origin_id);
if (!originNode) return null;
const comfyClass = originNode.comfyClass || originNode.type || "";
if (/reroute/i.test(comfyClass)) {
const upstreamLink = originNode.inputs?.[0]?.link;
if (upstreamLink != null) {
return walkLink(upstreamLink);
}
}
return readNodeOutputValue(originNode, link.origin_slot);
};
const input = self.inputs?.find(i => i.name === inputName);
const value = input?.link != null ? walkLink(input.link) : null;
if (value == null || !Number.isFinite(value)) return null;
if (options.integer) {
return Math.round(value);
}
const decimals = Number.isInteger(options.decimals) ? options.decimals : 3;
return parseFloat(value.toFixed(decimals));
};
this._syncGlobalPromptFromLink = function () {
const globalInput = self.inputs?.find(i => i.name === "global_prompt");
if (globalInput && globalInput.link !== null && globalInput.link !== undefined) {
const link = app.graph.links[globalInput.link];
if (link) {
const originNode = app.graph.getNodeById(link.origin_id); const originNode = app.graph.getNodeById(link.origin_id);
if (originNode) { if (originNode) {
// Usually string values are in widgets[0] for primitives // Usually string values are in widgets[0] for primitives
@@ -11494,25 +11554,74 @@ app.registerExtension({
} else if (self._timelineEditor.globalPromptInput.value !== val) { } else if (self._timelineEditor.globalPromptInput.value !== val) {
self._timelineEditor.globalPromptInput.value = val; self._timelineEditor.globalPromptInput.value = val;
} }
} }
} }
}; };
this._syncTimingFromLinks = function () {
const origOnConnectionsChange = this.onConnectionsChange; const linkedStart = self._resolveLinkedInputValue("start", { decimals: 3 });
this.onConnectionsChange = function (type, index, connected, link_info) { const linkedEnd = self._resolveLinkedInputValue("end", { decimals: 3 });
if (origOnConnectionsChange) { const linkedDuration = self._resolveLinkedInputValue("duration", { decimals: 3 });
origOnConnectionsChange.apply(this, arguments); const snapshot = `${linkedStart ?? ""}|${linkedEnd ?? ""}|${linkedDuration ?? ""}`;
}
self._syncGlobalPromptFromLink(); if (snapshot === self._lastLinkedTimingSnapshot) {
}; return;
}
const origOnDrawForeground = this.onDrawForeground; self._lastLinkedTimingSnapshot = snapshot;
this.onDrawForeground = function (ctx) {
if (origOnDrawForeground) { let changed = false;
origOnDrawForeground.apply(this, arguments); const applyWidget = (name, value) => {
} if (value == null) return;
self._syncGlobalPromptFromLink(); const widget = self.widgets?.find(w => w.name === name);
}; if (!widget) return;
if (widget.value === value) return;
widget.value = value;
if (widget.callback) {
try {
widget.callback(value);
} catch (e) {}
}
changed = true;
};
applyWidget("start_second", linkedStart);
if (linkedDuration != null) {
applyWidget("duration_seconds", Math.max(0.1, linkedDuration));
} else {
applyWidget("end_second", linkedEnd);
}
if (changed) {
if (self._ltxSettingsRefresh) {
try { self._ltxSettingsRefresh(); } catch (e) {}
}
if (self._timelineEditor) {
self._timelineEditor.render();
}
if (self.setDirtyCanvas) {
self.setDirtyCanvas(true, true);
}
}
};
const origOnConnectionsChange = this.onConnectionsChange;
this.onConnectionsChange = function (type, index, connected, link_info) {
if (origOnConnectionsChange) {
origOnConnectionsChange.apply(this, arguments);
}
self._syncGlobalPromptFromLink();
self._syncTimingFromLinks();
self._syncResolutionFromLinks?.();
};
const origOnDrawForeground = this.onDrawForeground;
this.onDrawForeground = function (ctx) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
self._syncGlobalPromptFromLink();
self._syncTimingFromLinks();
self._syncResolutionFromLinks?.();
};
// --- LTX Director settings panel (Stage 1: Resolution | Timing/Reference) --- // --- LTX Director settings panel (Stage 1: Resolution | Timing/Reference) ---
const _ltxBuildSettingsPanel = (node, panelRoot) => { const _ltxBuildSettingsPanel = (node, panelRoot) => {
@@ -11605,9 +11714,9 @@ app.registerExtension({
RES.forEach((p, i) => { const o = document.createElement("option"); o.value = String(i); o.textContent = p.label; presetSel.appendChild(o); }); RES.forEach((p, i) => { const o = document.createElement("option"); o.value = String(i); o.textContent = p.label; presetSel.appendChild(o); });
presetRow.appendChild(presetSel); left.appendChild(presetRow); presetRow.appendChild(presetSel); left.appendChild(presetRow);
const widthRow = mkRow("Width"); const widthRow = mkRow("Width");
const widthIn = document.createElement("input"); widthIn.type = "number"; widthIn.step = "32"; widthIn.min = "0"; sIn(widthIn); const widthIn = document.createElement("input"); widthIn.type = "number"; widthIn.step = "32"; widthIn.min = "0"; sIn(widthIn);
widthRow.appendChild(widthIn); left.appendChild(widthRow); widthRow.appendChild(widthIn); left.appendChild(widthRow);
const heightRow = mkRow("Height"); const heightRow = mkRow("Height");
const heightIn = document.createElement("input"); heightIn.type = "number"; heightIn.step = "32"; heightIn.min = "0"; sIn(heightIn); const heightIn = document.createElement("input"); heightIn.type = "number"; heightIn.step = "32"; heightIn.min = "0"; sIn(heightIn);
@@ -11639,17 +11748,76 @@ app.registerExtension({
const oCustom = document.createElement("option"); oCustom.value = "0"; oCustom.textContent = "Custom"; fpsSel.appendChild(oCustom); const oCustom = document.createElement("option"); oCustom.value = "0"; oCustom.textContent = "Custom"; fpsSel.appendChild(oCustom);
FPS.forEach(v => { const o = document.createElement("option"); o.value = String(v); o.textContent = v + " fps"; fpsSel.appendChild(o); }); FPS.forEach(v => { const o = document.createElement("option"); o.value = String(v); o.textContent = v + " fps"; fpsSel.appendChild(o); });
const fpsIn = document.createElement("input"); fpsIn.type = "number"; fpsIn.min = "1"; sIn(fpsIn, "48px"); const fpsIn = document.createElement("input"); fpsIn.type = "number"; fpsIn.min = "1"; sIn(fpsIn, "48px");
const frW = getW("frame_rate"); fpsIn.value = frW ? frW.value : 24; const frW = getW("frame_rate"); fpsIn.value = frW ? frW.value : 24;
const syncFps = () => { const v = parseInt(fpsIn.value) || 0; fpsSel.value = (FPS.indexOf(v) >= 0) ? String(v) : "0"; }; const syncFps = () => { const v = parseInt(fpsIn.value) || 0; fpsSel.value = (FPS.indexOf(v) >= 0) ? String(v) : "0"; };
fpsSel.addEventListener("change", () => { const v = parseInt(fpsSel.value) || 0; if (v > 0) { fpsIn.value = v; applyFrameRate(v); } }); fpsSel.addEventListener("change", () => { const v = parseInt(fpsSel.value) || 0; if (v > 0) { fpsIn.value = v; applyFrameRate(v); } });
fpsIn.addEventListener("change", () => { let v = Math.round(parseFloat(fpsIn.value)); if (isNaN(v) || v < 1) v = 1; fpsIn.value = v; applyFrameRate(v); syncFps(); }); fpsIn.addEventListener("change", () => { let v = Math.round(parseFloat(fpsIn.value)); if (isNaN(v) || v < 1) v = 1; fpsIn.value = v; applyFrameRate(v); syncFps(); });
fpsWrap.appendChild(fpsSel); fpsWrap.appendChild(fpsIn); fpsWrap.appendChild(fpsSel); fpsWrap.appendChild(fpsIn);
fpsRow.appendChild(fpsWrap); left.appendChild(fpsRow); fpsRow.appendChild(fpsWrap); left.appendChild(fpsRow);
syncPreset(); syncFps(); syncPreset(); syncFps();
// ---- shared seconds<->frames timing infrastructure ---- const refreshResolutionInputLocks = () => {
const TIME_NAMES = ["start_second", "end_second", "duration_seconds", "start_frame", "end_frame", "duration_frames"]; const widthLinked = !!node.inputs?.find(i => i.name === "custom_width")?.link;
const heightLinked = !!node.inputs?.find(i => i.name === "custom_height")?.link;
const fpsLinked = !!node.inputs?.find(i => i.name === "frame_rate")?.link;
const presetLinked = widthLinked || heightLinked;
widthIn.disabled = widthLinked;
heightIn.disabled = heightLinked;
presetSel.disabled = presetLinked;
fpsIn.disabled = fpsLinked;
fpsSel.disabled = fpsLinked;
};
node._syncResolutionFromLinks = () => {
const linkedWidth = node._resolveLinkedInputValue?.("custom_width", { integer: true });
const linkedHeight = node._resolveLinkedInputValue?.("custom_height", { integer: true });
const linkedFps = node._resolveLinkedInputValue?.("frame_rate", { integer: true });
const snapshot = `${linkedWidth ?? ""}|${linkedHeight ?? ""}|${linkedFps ?? ""}`;
refreshResolutionInputLocks();
if (snapshot === node._lastLinkedResolutionSnapshot) {
return;
}
node._lastLinkedResolutionSnapshot = snapshot;
let changed = false;
const widthWidget = getW("custom_width");
if (linkedWidth != null && widthWidget && widthWidget.value !== linkedWidth) {
widthWidget.value = linkedWidth;
if (widthWidget.callback) {
try { widthWidget.callback(linkedWidth); } catch (e) {}
}
changed = true;
}
const heightWidget = getW("custom_height");
if (linkedHeight != null && heightWidget && heightWidget.value !== linkedHeight) {
heightWidget.value = linkedHeight;
if (heightWidget.callback) {
try { heightWidget.callback(linkedHeight); } catch (e) {}
}
changed = true;
}
if (linkedFps != null) {
const currentFps = parseInt(getW("frame_rate")?.value) || 24;
if (currentFps !== linkedFps) {
applyFrameRate(Math.max(1, linkedFps));
changed = true;
}
}
if (changed && node._ltxSettingsRefresh) {
try { node._ltxSettingsRefresh(); } catch (e) {}
}
};
// ---- shared seconds<->frames timing infrastructure ----
const TIME_NAMES = ["start_second", "end_second", "duration_seconds", "start_frame", "end_frame", "duration_frames"];
const timeMode = () => { const dm = getW("display_mode"); return (dm && dm.value === "frames") ? "frames" : "seconds"; }; const timeMode = () => { const dm = getW("display_mode"); return (dm && dm.value === "frames") ? "frames" : "seconds"; };
const hideTimingWidgets = () => { const hideTimingWidgets = () => {
const isLG = !window.LiteGraph || !window.LiteGraph.vueNodesMode; const isLG = !window.LiteGraph || !window.LiteGraph.vueNodesMode;
@@ -11745,12 +11913,13 @@ app.registerExtension({
if (ch) heightIn.value = ch.value; if (ch) heightIn.value = ch.value;
if (fr) fpsIn.value = fr.value; if (fr) fpsIn.value = fr.value;
if (rm && rm.value != null) rmSel.value = rm.value; if (rm && rm.value != null) rmSel.value = rm.value;
if (rs) refIn.value = rs.value; if (rs) refIn.value = rs.value;
syncPreset(); syncFps(); syncPreset(); syncFps();
if (typeof unitSel !== "undefined" && unitSel) unitSel.value = timeMode(); refreshResolutionInputLocks();
timeRefreshers.forEach(fn => fn()); if (typeof unitSel !== "undefined" && unitSel) unitSel.value = timeMode();
ensureTimingHidden(); timeRefreshers.forEach(fn => fn());
}; ensureTimingHidden();
};
node._ltxSettingsRefresh = refreshFromWidgets; node._ltxSettingsRefresh = refreshFromWidgets;
refreshFromWidgets(); refreshFromWidgets();
setTimeout(refreshFromWidgets, 0); setTimeout(refreshFromWidgets, 0);