feat: live sync linked director controls
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user