diff --git a/js/ltx_sequencer.js b/js/ltx_sequencer.js deleted file mode 100644 index 000587a..0000000 --- a/js/ltx_sequencer.js +++ /dev/null @@ -1,398 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Global registry to track all LTXSequencer nodes across all subgraphs -window._LTXSequencerGlobalNodes = window._LTXSequencerGlobalNodes || new Set(); - -// ComfyUI native trick to cleanly hide/show widgets without deleting them -function toggleWidget(widget, visible) { - if (visible) { - if (widget.origType !== undefined) { - widget.type = widget.origType; - widget.computeSize = widget.origComputeSize; - delete widget.origType; - delete widget.origComputeSize; - } - } else { - if (widget.type !== "hidden") { - widget.origType = widget.type; - widget.origComputeSize = widget.computeSize; - widget.type = "hidden"; - widget.computeSize = () => [0, -4]; - } - } -} - -/** - * FULL STATE SYNC: - * Instead of syncing one widget, we push the entire properties object - * to ensure no values are ever lost during subgraph transitions or deletions. - */ -function syncFullStateAcrossNodes(sourceNode) { - if (!window._LTXSequencerGlobalNodes) return; - - for (const targetNode of window._LTXSequencerGlobalNodes) { - if (targetNode === sourceNode) continue; - - // 1. Mirror the properties object completely - // We use a shallow copy to ensure we don't accidentally share object references - const newState = { ...sourceNode.properties }; - targetNode.properties = { ...targetNode.properties, ...newState }; - - // 2. Check if we need to rebuild the widget list (if num_images changed) - const targetImageCount = targetNode.properties["num_images"] || 0; - const currentVisibleCount = targetNode._currentImageCount; - - if (targetImageCount !== currentVisibleCount) { - targetNode._applyWidgetCount(targetImageCount); - } - - // 3. Update all existing widget values visually - if (targetNode.widgets) { - let modeChanged = false; - targetNode.widgets.forEach(w => { - const newValue = targetNode.properties[w.name]; - if (newValue !== undefined && w.value !== newValue) { - w.value = newValue; - if (w.name === "insert_mode") modeChanged = true; - } - }); - - if (modeChanged && targetNode._updateVisibility) { - targetNode._updateVisibility(); - } - } - - targetNode.setDirtyCanvas(true, false); - } -} - -app.registerExtension({ - name: "Comfy.LTXSequencer.DynamicInputs", - async nodeCreated(node) { - if (node.comfyClass !== "LTXSequencer") return; - - // Register this node instance globally - window._LTXSequencerGlobalNodes.add(node); - - node._currentImageCount = -1; // Force first update - - // Initialize persistent properties cache - node.properties = node.properties || {}; - - // Add subtle separator line above images_loaded - node.addCustomWidget({ - name: "num_images_separator", - type: "text", - draw(ctx, node, widget_width, y, widget_height) { - ctx.save(); - ctx.strokeStyle = "#444"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(10, y + 5); - ctx.lineTo(widget_width - 10, y + 5); - ctx.stroke(); - ctx.restore(); - }, - computeSize(width) { - return [width, 10]; - } - }); - - // Move separator before num_images - const moveSeparator = () => { - const idx = node.widgets.findIndex(w => w.name === "num_images"); - const sepIdx = node.widgets.findIndex(w => w.name === "num_images_separator"); - if (idx !== -1 && sepIdx !== -1) { - const separator = node.widgets.splice(sepIdx, 1)[0]; - node.widgets.splice(idx, 0, separator); - } - }; - setTimeout(moveSeparator, 50); - - // Binds custom callbacks to python-schema generated widgets - node._hookStaticWidgets = function() { - if (!this.widgets) return; - const staticNames = ["num_images", "insert_mode", "frame_rate"]; - staticNames.forEach(name => { - const w = this.widgets.find(w => w.name === name); - if (w && !w._has_custom_callback) { - const orig = w.callback; - w.callback = (val) => { - this.properties[name] = val; - - if (name === "num_images") { - this._applyWidgetCount(val); - } - - // Push full state to siblings - syncFullStateAcrossNodes(this); - - if (name === "insert_mode") { - this._updateVisibility(); - } - if (orig) orig.apply(w, [val]); - }; - w._has_custom_callback = true; - } - }); - }; - - // Handles show/hiding widgets based on insertion method - node._updateVisibility = function() { - const mode = this.properties["insert_mode"] || "frames"; - if (!this.widgets) return; - - let changed = false; - for (const w of this.widgets) { - let shouldBeVisible = true; - - if (w.name.startsWith("insert_frame_")) { - shouldBeVisible = (mode === "frames"); - } else if (w.name.startsWith("insert_second_")) { - shouldBeVisible = (mode === "seconds"); - } - - const isHidden = (w.type === "hidden"); - if (shouldBeVisible && isHidden) { - toggleWidget(w, true); - changed = true; - } else if (!shouldBeVisible && !isHidden) { - toggleWidget(w, false); - changed = true; - } - } - - if (changed) { - this.setDirtyCanvas(true, true); - requestAnimationFrame(() => { - if (this.computeSize) { - this.setSize(this.computeSize()); - } - }); - } - }; - - // Core update: synchronize widget visibility to match imageCount - node._applyWidgetCount = function(count) { - this._hookStaticWidgets(); - - const isInitialLoad = this._currentImageCount === -1; - if (this._currentImageCount === count && !isInitialLoad) return; - this._currentImageCount = count; - - const initialWidth = this.size[0]; - const numWidget = this.widgets?.find(w => w.name === "num_images"); - if (numWidget) { - numWidget.label = "images_loaded"; - numWidget.value = Math.max(0, Math.min(count || 0, 50)); - } - - // 1. Update properties from current widget values before restructuring - if (this.widgets) { - this.widgets.forEach(w => { - if (w.name.startsWith("insert_") || w.name.startsWith("strength_") || ["num_images", "insert_mode", "frame_rate"].includes(w.name)) { - if (w.type !== "hidden" && w.type !== "button") this.properties[w.name] = w.value; - } - }); - } - - // 2. Clear dynamic widgets - if (this.widgets) { - this.widgets = this.widgets.filter(w => - !w.name.startsWith("insert_frame_") && - !w.name.startsWith("insert_second_") && - !w.name.startsWith("strength_") && - !w.name.startsWith("header_") - ); - } else { - this.widgets = []; - } - - // 3. Rebuild precisely - for (let i = 1; i <= count; i++) { - // Header - const headerName = `header_${i}`; - this.addCustomWidget({ - name: headerName, - type: "text", - value: `Image #${i}`, - draw(ctx, node, widget_width, y, widget_height) { - ctx.save(); - const margin = 10; - const topPadding = 15; - ctx.strokeStyle = "#333"; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(margin, y + 5); - ctx.lineTo(widget_width - margin, y + 5); - ctx.stroke(); - ctx.fillStyle = "#dddddd"; - ctx.font = "bold 12px Arial"; - ctx.textAlign = "left"; - ctx.fillText(`Image #${i}`, margin, y + topPadding + 10); - ctx.restore(); - }, - computeSize(width) { return [width, 35]; } - }); - - // Helper to add widget with full sync - const addSyncedWidget = (type, name, def, options) => { - const saved = this.properties[name]; - return this.addWidget(type, name, saved !== undefined ? saved : def, (val) => { - this.properties[name] = val; - syncFullStateAcrossNodes(this); - }, options); - }; - - addSyncedWidget("number", `insert_frame_${i}`, 0, { min: -9999, max: 9999, step: 10, precision: 0 }); - addSyncedWidget("number", `insert_second_${i}`, 0.0, { min: 0.0, max: 9999.0, step: 0.1, precision: 2 }); - addSyncedWidget("number", `strength_${i}`, 1.0, { min: 0.0, max: 1.0, step: 0.01 }); - } - - this._updateVisibility(); - this.setDirtyCanvas(true, true); - requestAnimationFrame(() => { - if (this.computeSize) { - this.setSize(this.computeSize()); - this.size[0] = initialWidth; - } - }); - }; - - const origConfigure = node.configure; - node.configure = function(info) { - if (origConfigure) origConfigure.apply(this, arguments); - if (this.widgets) { - this.widgets.forEach(w => { - if (w.name.startsWith("insert_") || w.name.startsWith("strength_") || ["num_images", "insert_mode", "frame_rate"].includes(w.name)) { - if (w.type !== "button") this.properties[w.name] = w.value; - } - }); - } - }; - - node.onConfigure = function(info) { - if (info.properties) { - this.properties = { ...this.properties, ...info.properties }; - } - this._hookStaticWidgets(); - setTimeout(() => { - const count = readSourceImageCount(this); - let targetCount = count !== null ? count : (this.properties.num_images || 0); - this._applyWidgetCount(targetCount); - this._updateVisibility(); - }, 100); - }; - - const originalOnSerialize = node.onSerialize; - node.onSerialize = function(info) { - if (this.widgets) { - this.widgets.forEach(w => { - if (w.name.startsWith("insert_") || w.name.startsWith("strength_") || ["num_images", "insert_mode", "frame_rate"].includes(w.name)) { - if (w.type !== "hidden" && w.type !== "button") this.properties[w.name] = w.value; - } - }); - } - if (originalOnSerialize) originalOnSerialize.apply(this, arguments); - info.properties = { ...this.properties }; - - const strictArray = []; - strictArray.push(this.properties["num_images"] !== undefined ? this.properties["num_images"] : 1); - strictArray.push(this.properties["insert_mode"] !== undefined ? this.properties["insert_mode"] : "frames"); - strictArray.push(this.properties["frame_rate"] !== undefined ? this.properties["frame_rate"] : 24); - - for (let i = 1; i <= 50; i++) { - strictArray.push(this.properties[`insert_frame_${i}`] !== undefined ? this.properties[`insert_frame_${i}`] : 0); - strictArray.push(this.properties[`insert_second_${i}`] !== undefined ? this.properties[`insert_second_${i}`] : 0.0); - strictArray.push(this.properties[`strength_${i}`] !== undefined ? this.properties[`strength_${i}`] : 1.0); - } - info.widgets_values = strictArray; - }; - - function readSourceImageCount(self) { - const multiInput = self.inputs?.find(inp => inp.name === "multi_input"); - if (!multiInput || !multiInput.link) return null; - const nodeGraph = self.graph || app.graph; - - function traceUpstream(graph, linkId, visited = new Set()) { - if (!linkId || 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; - if (originNode.comfyClass === "MultiImageLoader") return originNode; - if (originNode.type === "Reroute" || originNode.comfyClass === "Reroute") { - if (originNode.inputs?.[0]?.link) return traceUpstream(graph, originNode.inputs[0].link, visited); - } - if (typeof originNode.getInnerNode === "function") { - try { - const innerNode = originNode.getInnerNode(link.origin_slot); - if (innerNode?.comfyClass === "MultiImageLoader") return innerNode; - } catch (e) {} - } - return null; - } - - let sourceNode = traceUpstream(nodeGraph, multiInput.link); - function getCountFromNode(n) { - if (typeof n._imageCount === "number") return n._imageCount; - const pathsWidget = n.widgets?.find(w => w.name === "image_paths"); - return pathsWidget ? (pathsWidget.value || "").split('\n').filter(p => p.trim()).length : null; - } - - if (sourceNode) return getCountFromNode(sourceNode); - - let multiImageLoaders = []; - function findAllLoaders(nodes) { - if (!nodes) return; - for (let n of nodes) { - if (n.comfyClass === "MultiImageLoader") multiImageLoaders.push(n); - if (n.subgraph?._nodes) findAllLoaders(n.subgraph._nodes); - } - } - if (app.graph?._nodes) findAllLoaders(app.graph._nodes); - if (multiImageLoaders.length === 1) return getCountFromNode(multiImageLoaders[0]); - return null; - } - - const pollInterval = setInterval(() => { - if (!node.graph) { - clearInterval(pollInterval); - return; - } - const count = readSourceImageCount(node); - if (count !== null && count !== node._currentImageCount) { - node._applyWidgetCount(count); - syncFullStateAcrossNodes(node); // Sync the new count to others - } - }, 500); - - const origOnRemoved = node.onRemoved; - node.onRemoved = function() { - window._LTXSequencerGlobalNodes.delete(node); - clearInterval(pollInterval); - if (origOnRemoved) origOnRemoved.apply(this, arguments); - }; - - node.onConnectionsChange = function(type, index, connected) { - if (type === 1 && this.inputs[index]?.name === "multi_input") { - if (connected) { - setTimeout(() => { - const count = readSourceImageCount(this); - this._applyWidgetCount(count !== null ? count : (this.properties.num_images || 0)); - }, 100); - } else { - this._applyWidgetCount(0); - } - } - }; - - node.onAdded = function() { - setTimeout(() => { - const count = readSourceImageCount(this); - this._applyWidgetCount(count !== null ? count : (this.properties.num_images || 0)); - }, 100); - }; - } -}); \ No newline at end of file