feat: add external timeline nodes
This commit is contained in:
@@ -11503,6 +11503,170 @@ app.registerExtension({
|
||||
const decimals = Number.isInteger(options.decimals) ? options.decimals : 3;
|
||||
return parseFloat(value.toFixed(decimals));
|
||||
};
|
||||
this._findLinkedOriginNode = function (targetNode, inputName) {
|
||||
const graph = app.graph;
|
||||
const visited = new Set();
|
||||
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 originNode;
|
||||
};
|
||||
|
||||
const input = targetNode?.inputs?.find(i => i.name === inputName);
|
||||
return input?.link != null ? walkLink(input.link) : null;
|
||||
};
|
||||
this._readNodeWidgetValue = function (node, widgetName, fallbackIndex = 0) {
|
||||
if (!Array.isArray(node?.widgets)) return null;
|
||||
const namedWidget = node.widgets.find(w => w?.name === widgetName);
|
||||
if (namedWidget?.value != null) return namedWidget.value;
|
||||
const fallbackWidget = node.widgets[fallbackIndex];
|
||||
return fallbackWidget?.value ?? null;
|
||||
};
|
||||
this._resolveNodeLinkedValue = function (targetNode, inputName, options = {}) {
|
||||
const originNode = self._findLinkedOriginNode(targetNode, inputName);
|
||||
if (!originNode) return null;
|
||||
const value = self._readNodeWidgetValue(originNode, inputName, 0);
|
||||
if (value == null || !Number.isFinite(Number(value))) return null;
|
||||
if (options.integer) return Math.round(Number(value));
|
||||
const decimals = Number.isInteger(options.decimals) ? options.decimals : 3;
|
||||
return parseFloat(Number(value).toFixed(decimals));
|
||||
};
|
||||
this._extractLinkedImageSource = function (targetNode, inputName) {
|
||||
const originNode = self._findLinkedOriginNode(targetNode, inputName);
|
||||
if (!originNode) return null;
|
||||
|
||||
const filenameValue =
|
||||
self._readNodeWidgetValue(originNode, "image") ??
|
||||
self._readNodeWidgetValue(originNode, "filename") ??
|
||||
self._readNodeWidgetValue(originNode, "file") ??
|
||||
self._readNodeWidgetValue(originNode, "", 0);
|
||||
if (typeof filenameValue !== "string" || !filenameValue.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subfolderValue =
|
||||
self._readNodeWidgetValue(originNode, "subfolder") ??
|
||||
self._readNodeWidgetValue(originNode, "folder");
|
||||
const filename = filenameValue.trim();
|
||||
const subfolder = typeof subfolderValue === "string" ? subfolderValue.trim() : "";
|
||||
const imageFile = subfolder ? `${subfolder}/${filename}` : filename;
|
||||
const imageUrl = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${encodeURIComponent(subfolder)}`);
|
||||
return { imageFile, imageB64: imageUrl };
|
||||
};
|
||||
this._buildExternalTimelinePayload = function () {
|
||||
const timelineNode = self._findLinkedOriginNode(self, "timeline");
|
||||
const comfyClass = timelineNode?.comfyClass || timelineNode?.type || "";
|
||||
if (!timelineNode || comfyClass !== "TimelineCS") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawDuration =
|
||||
self._resolveNodeLinkedValue(timelineNode, "duration", { decimals: 3 }) ??
|
||||
parseFloat(self._readNodeWidgetValue(timelineNode, "duration") || 0);
|
||||
const duration = Math.max(0.1, Number.isFinite(rawDuration) ? rawDuration : 5.0);
|
||||
|
||||
const sectionSetNode = self._findLinkedOriginNode(timelineNode, "section_set");
|
||||
const setClass = sectionSetNode?.comfyClass || sectionSetNode?.type || "";
|
||||
const sections = [];
|
||||
if (sectionSetNode && setClass === "TimelineSectionSetCS") {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const sectionNode = self._findLinkedOriginNode(sectionSetNode, `section_${i}`);
|
||||
const sectionClass = sectionNode?.comfyClass || sectionNode?.type || "";
|
||||
if (!sectionNode || sectionClass !== "TimelineSectionCS") continue;
|
||||
|
||||
const text = String(self._readNodeWidgetValue(sectionNode, "text") || "").trim();
|
||||
if (!text) continue;
|
||||
|
||||
const rawSectionDuration =
|
||||
self._resolveNodeLinkedValue(sectionNode, "duration", { decimals: 3 }) ??
|
||||
parseFloat(self._readNodeWidgetValue(sectionNode, "duration") || 0);
|
||||
const sectionDuration = Math.max(0.1, Number.isFinite(rawSectionDuration) ? rawSectionDuration : 1.0);
|
||||
const imageSource = self._extractLinkedImageSource(sectionNode, "image");
|
||||
sections.push({
|
||||
text,
|
||||
duration: sectionDuration,
|
||||
imageFile: imageSource?.imageFile || "",
|
||||
imageB64: imageSource?.imageB64 || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { duration, sections };
|
||||
};
|
||||
this._syncTimelineFromLink = function () {
|
||||
const payload = self._buildExternalTimelinePayload();
|
||||
const snapshot = JSON.stringify(payload || null);
|
||||
if (snapshot === self._lastLinkedTimelineSnapshot) {
|
||||
return;
|
||||
}
|
||||
self._lastLinkedTimelineSnapshot = snapshot;
|
||||
if (!payload || !self._timelineEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = self._timelineEditor;
|
||||
const frameRate = editor.getFrameRate ? editor.getFrameRate() : (parseFloat(self.widgets?.find(w => w.name === "frame_rate")?.value) || 24);
|
||||
let currentStart = 0;
|
||||
const builtSegments = [];
|
||||
|
||||
for (let i = 0; i < payload.sections.length; i++) {
|
||||
const section = payload.sections[i];
|
||||
const segLength = Math.max(1, Math.round(section.duration * frameRate));
|
||||
const seg = {
|
||||
id: `linked_section_${i}`,
|
||||
start: currentStart,
|
||||
length: segLength,
|
||||
prompt: section.text,
|
||||
type: section.imageFile ? "image" : "text",
|
||||
};
|
||||
if (section.imageFile) {
|
||||
seg.imageFile = section.imageFile;
|
||||
seg.imageB64 = section.imageB64;
|
||||
}
|
||||
builtSegments.push(seg);
|
||||
currentStart += segLength;
|
||||
}
|
||||
|
||||
const effectiveDuration = Math.max(payload.duration, currentStart / Math.max(frameRate, 1));
|
||||
const durationWidget = self.widgets?.find(w => w.name === "duration_seconds");
|
||||
if (durationWidget && durationWidget.value !== effectiveDuration) {
|
||||
durationWidget.value = effectiveDuration;
|
||||
if (durationWidget.callback) {
|
||||
try { durationWidget.callback(effectiveDuration); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
editor.timeline.segments = builtSegments;
|
||||
editor.selectionType = "image";
|
||||
editor.selectedIndex = builtSegments.length ? 0 : -1;
|
||||
editor.loadMedia();
|
||||
editor.updateUIFromSelection();
|
||||
editor.commitChanges();
|
||||
editor.render();
|
||||
|
||||
if (self._ltxSettingsRefresh) {
|
||||
try { self._ltxSettingsRefresh(); } catch (e) {}
|
||||
}
|
||||
if (self.setDirtyCanvas) {
|
||||
self.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
this._syncGlobalPromptFromLink = function () {
|
||||
const globalInput = self.inputs?.find(i => i.name === "global_prompt");
|
||||
if (globalInput && globalInput.link !== null && globalInput.link !== undefined) {
|
||||
@@ -11611,6 +11775,7 @@ app.registerExtension({
|
||||
self._syncGlobalPromptFromLink();
|
||||
self._syncTimingFromLinks();
|
||||
self._syncResolutionFromLinks?.();
|
||||
self._syncTimelineFromLink?.();
|
||||
};
|
||||
|
||||
const origOnDrawForeground = this.onDrawForeground;
|
||||
@@ -11621,6 +11786,7 @@ app.registerExtension({
|
||||
self._syncGlobalPromptFromLink();
|
||||
self._syncTimingFromLinks();
|
||||
self._syncResolutionFromLinks?.();
|
||||
self._syncTimelineFromLink?.();
|
||||
};
|
||||
|
||||
// --- LTX Director settings panel (Stage 1: Resolution | Timing/Reference) ---
|
||||
|
||||
Reference in New Issue
Block a user