From 4c0358c11b3a603244946f84055d04d504c4eddf Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 16 Jul 2026 16:14:10 +0000 Subject: [PATCH] feat: live sync linked director controls --- js/ltx_director.js | 259 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 214 insertions(+), 45 deletions(-) diff --git a/js/ltx_director.js b/js/ltx_director.js index d9023ee..4eef2ef 100644 --- a/js/ltx_director.js +++ b/js/ltx_director.js @@ -11442,12 +11442,72 @@ app.registerExtension({ compWidget.value = 18; } - const self = this; - 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 self = this; + this._resolveLinkedInputValue = function (inputName, options = {}) { + const graph = app.graph; + const visited = new Set(); + const readNodeOutputValue = (node, slotIndex) => { + 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); if (originNode) { // Usually string values are in widgets[0] for primitives @@ -11494,25 +11554,74 @@ app.registerExtension({ } else if (self._timelineEditor.globalPromptInput.value !== val) { self._timelineEditor.globalPromptInput.value = val; } - } - } - }; - - const origOnConnectionsChange = this.onConnectionsChange; - this.onConnectionsChange = function (type, index, connected, link_info) { - if (origOnConnectionsChange) { - origOnConnectionsChange.apply(this, arguments); - } - self._syncGlobalPromptFromLink(); - }; - - const origOnDrawForeground = this.onDrawForeground; - this.onDrawForeground = function (ctx) { - if (origOnDrawForeground) { - origOnDrawForeground.apply(this, arguments); - } - self._syncGlobalPromptFromLink(); - }; + } + } + }; + this._syncTimingFromLinks = function () { + const linkedStart = self._resolveLinkedInputValue("start", { decimals: 3 }); + const linkedEnd = self._resolveLinkedInputValue("end", { decimals: 3 }); + const linkedDuration = self._resolveLinkedInputValue("duration", { decimals: 3 }); + const snapshot = `${linkedStart ?? ""}|${linkedEnd ?? ""}|${linkedDuration ?? ""}`; + + if (snapshot === self._lastLinkedTimingSnapshot) { + return; + } + self._lastLinkedTimingSnapshot = snapshot; + + let changed = false; + const applyWidget = (name, value) => { + if (value == null) return; + 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) --- 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); }); presetRow.appendChild(presetSel); left.appendChild(presetRow); - const widthRow = mkRow("Width"); - const widthIn = document.createElement("input"); widthIn.type = "number"; widthIn.step = "32"; widthIn.min = "0"; sIn(widthIn); - widthRow.appendChild(widthIn); left.appendChild(widthRow); + const widthRow = mkRow("Width"); + const widthIn = document.createElement("input"); widthIn.type = "number"; widthIn.step = "32"; widthIn.min = "0"; sIn(widthIn); + widthRow.appendChild(widthIn); left.appendChild(widthRow); const heightRow = mkRow("Height"); 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); 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 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"; }; - 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(); }); - fpsWrap.appendChild(fpsSel); fpsWrap.appendChild(fpsIn); - fpsRow.appendChild(fpsWrap); left.appendChild(fpsRow); - - syncPreset(); syncFps(); - - // ---- shared seconds<->frames timing infrastructure ---- - const TIME_NAMES = ["start_second", "end_second", "duration_seconds", "start_frame", "end_frame", "duration_frames"]; + 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"; }; + 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(); }); + fpsWrap.appendChild(fpsSel); fpsWrap.appendChild(fpsIn); + fpsRow.appendChild(fpsWrap); left.appendChild(fpsRow); + + syncPreset(); syncFps(); + + const refreshResolutionInputLocks = () => { + 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 hideTimingWidgets = () => { const isLG = !window.LiteGraph || !window.LiteGraph.vueNodesMode; @@ -11745,12 +11913,13 @@ app.registerExtension({ if (ch) heightIn.value = ch.value; if (fr) fpsIn.value = fr.value; if (rm && rm.value != null) rmSel.value = rm.value; - if (rs) refIn.value = rs.value; - syncPreset(); syncFps(); - if (typeof unitSel !== "undefined" && unitSel) unitSel.value = timeMode(); - timeRefreshers.forEach(fn => fn()); - ensureTimingHidden(); - }; + if (rs) refIn.value = rs.value; + syncPreset(); syncFps(); + refreshResolutionInputLocks(); + if (typeof unitSel !== "undefined" && unitSel) unitSel.value = timeMode(); + timeRefreshers.forEach(fn => fn()); + ensureTimingHidden(); + }; node._ltxSettingsRefresh = refreshFromWidgets; refreshFromWidgets(); setTimeout(refreshFromWidgets, 0);