kurppahub

Fixed insert frames and strengths loading on (deno) LTX Sequencer

Apr 24th, 2026
171
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 50.46 KB | Software | 0 0
  1. import { app } from "../../scripts/app.js";
  2. import { api } from "../../scripts/api.js";
  3.  
  4. const LOADER_NODE = "DenoMultiImageLoader";
  5. const SEQUENCER_NODE = "DenoLTXSequencer";
  6. const LOADER_MIN_SIZE = [360, 520];
  7.  
  8. window.__denoLtxSequencerNodes = window.__denoLtxSequencerNodes || new Set();
  9.  
  10. app.registerExtension({
  11.     name: "Deno.ExtraNodes",
  12.     async beforeRegisterNodeDef(nodeType, nodeData) {
  13.         if (nodeData.name === LOADER_NODE) {
  14.             patchMultiImageLoader(nodeType);
  15.         }
  16.         if (nodeData.name === SEQUENCER_NODE) {
  17.             patchSequencer(nodeType);
  18.         }
  19.     },
  20. });
  21.  
  22. function patchMultiImageLoader(nodeType) {
  23.     const onNodeCreated = nodeType.prototype.onNodeCreated;
  24.     nodeType.prototype.onNodeCreated = function () {
  25.         const result = onNodeCreated?.apply(this, arguments);
  26.         setupMultiImageLoader(this);
  27.         return result;
  28.     };
  29. }
  30.  
  31. function setupMultiImageLoader(node) {
  32.     const pathsWidget = getWidget(node, "image_paths");
  33.     if (!pathsWidget || node.__denoLoaderReady) {
  34.         return;
  35.     }
  36.  
  37.     node.__denoLoaderReady = true;
  38.     hideWidget(pathsWidget);
  39.  
  40.     node._denoUpdateLoaderVisibility = function () {
  41.         const mode = getWidget(this, "mode")?.value ?? "Preset Ratio";
  42.         toggleWidgetVisibility(getWidget(this, "ratio_preset"), mode === "Preset Ratio");
  43.         toggleWidgetVisibility(getWidget(this, "megapixels"), mode === "Preset Ratio");
  44.         toggleWidgetVisibility(getWidget(this, "width"), mode !== "Preset Ratio");
  45.         toggleWidgetVisibility(getWidget(this, "height"), mode !== "Preset Ratio");
  46.         this.setDirtyCanvas?.(true, true);
  47.     };
  48.  
  49.     const container = document.createElement("div");
  50.     container.style.cssText = `
  51.         width: 100%;
  52.         height: 320px;
  53.         display: flex;
  54.         flex-direction: column;
  55.         gap: 10px;
  56.         padding: 10px;
  57.         box-sizing: border-box;
  58.         background: rgba(4, 8, 7, 0.96);
  59.         border: 1px solid rgba(72, 255, 132, 0.28);
  60.         border-radius: 12px;
  61.         pointer-events: auto;
  62.         overflow: hidden;
  63.     `;
  64.  
  65.     const topBar = document.createElement("div");
  66.     topBar.style.cssText = "display:flex; gap:8px; align-items:center;";
  67.  
  68.     const uploadBtn = createActionButton("Upload");
  69.     const clearBtn = createActionButton("Clear", true);
  70.     topBar.append(uploadBtn, clearBtn);
  71.  
  72.     const countLabel = document.createElement("div");
  73.     countLabel.style.cssText = "margin-left:auto; color:#94f7af; font:600 11px sans-serif;";
  74.     topBar.appendChild(countLabel);
  75.  
  76.     const hint = document.createElement("div");
  77.     hint.style.cssText = "color:#7dcf92; font:11px sans-serif; opacity:0.85;";
  78.     hint.textContent = "Drag files, press Ctrl+V, or use Upload. Drag cards to reorder.";
  79.  
  80.     const grid = document.createElement("div");
  81.     grid.style.cssText = `
  82.         flex: 1;
  83.         min-height: 0;
  84.         overflow-y: auto;
  85.         display: grid;
  86.         grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  87.         gap: 10px;
  88.         align-content: start;
  89.         padding-right: 4px;
  90.     `;
  91.  
  92.     const fileInput = document.createElement("input");
  93.     fileInput.type = "file";
  94.     fileInput.accept = "image/*";
  95.     fileInput.multiple = true;
  96.     fileInput.style.display = "none";
  97.  
  98.     container.append(topBar, hint, grid, fileInput);
  99.     const widget = node.addDOMWidget("loader_panel", "deno_multi_image_loader", container, { serialize: false });
  100.     widget.computeSize = () => [Math.max(node.size?.[0] ?? 0, LOADER_MIN_SIZE[0]), 332];
  101.  
  102.     node.size = [
  103.         Math.max(node.size?.[0] ?? 0, LOADER_MIN_SIZE[0]),
  104.         Math.max(node.size?.[1] ?? 0, LOADER_MIN_SIZE[1]),
  105.     ];
  106.  
  107.     let draggedCard = null;
  108.     let placeholder = null;
  109.     let isReordering = false;
  110.  
  111.     for (const currentWidget of node.widgets || []) {
  112.         if (currentWidget.__denoLoaderWrapped) {
  113.             continue;
  114.         }
  115.         const originalCallback = currentWidget.callback;
  116.         currentWidget.callback = function (value) {
  117.             const result = originalCallback?.apply(this, arguments);
  118.             node._denoUpdateLoaderVisibility?.();
  119.             return result;
  120.         };
  121.         currentWidget.__denoLoaderWrapped = true;
  122.     }
  123.  
  124.     function getPaths() {
  125.         return (pathsWidget.value || "")
  126.             .split("\n")
  127.             .map((entry) => entry.trim())
  128.             .filter(Boolean);
  129.     }
  130.  
  131.     function setPaths(paths) {
  132.         const deduped = paths.filter(Boolean);
  133.         pathsWidget.value = deduped.join("\n");
  134.         pathsWidget.callback?.(pathsWidget.value);
  135.         node._denoImageCount = deduped.length;
  136.         notifyConnectedSequencers(node, deduped.length);
  137.         node.setDirtyCanvas?.(true, true);
  138.         app.graph?.setDirtyCanvas?.(true, true);
  139.         render();
  140.     }
  141.  
  142.     function createPlaceholder() {
  143.         const el = document.createElement("div");
  144.         el.style.cssText = `
  145.             border: 1px dashed rgba(72,255,132,0.55);
  146.             border-radius: 10px;
  147.             background: rgba(28,68,42,0.35);
  148.             min-height: 92px;
  149.         `;
  150.         return el;
  151.     }
  152.  
  153.     function buildCard(path, index) {
  154.         const card = document.createElement("div");
  155.         card.draggable = true;
  156.         card.dataset.path = path;
  157.         card.style.cssText = `
  158.             position: relative;
  159.             min-height: 92px;
  160.             border-radius: 10px;
  161.             overflow: hidden;
  162.             background: #050707;
  163.             border: 1px solid rgba(54, 110, 74, 0.9);
  164.             cursor: grab;
  165.             box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35);
  166.         `;
  167.  
  168.         const image = document.createElement("img");
  169.         image.src = `/api/view?filename=${encodeURIComponent(path)}&type=input`;
  170.         image.style.cssText = "display:block; width:100%; height:100%; object-fit:cover; pointer-events:none;";
  171.  
  172.         const badge = document.createElement("div");
  173.         badge.textContent = String(index + 1);
  174.         badge.style.cssText = `
  175.             position:absolute; left:0; bottom:0;
  176.             background:rgba(0,0,0,0.72); color:#d7ffe3;
  177.             padding:2px 6px; font:700 11px sans-serif;
  178.             border-top-right-radius:8px;
  179.         `;
  180.  
  181.         const remove = document.createElement("button");
  182.         remove.type = "button";
  183.         remove.textContent = "x";
  184.         remove.style.cssText = `
  185.             position:absolute; top:6px; right:6px;
  186.             width:22px; height:22px; border:none; border-radius:999px;
  187.             background:rgba(0,0,0,0.72); color:#fff; cursor:pointer;
  188.             font:700 14px/1 sans-serif;
  189.         `;
  190.         remove.onclick = (event) => {
  191.             event.stopPropagation();
  192.             const nextPaths = getPaths();
  193.             nextPaths.splice(index, 1);
  194.             setPaths(nextPaths);
  195.         };
  196.  
  197.         card.addEventListener("dragstart", () => {
  198.             draggedCard = card;
  199.             placeholder = createPlaceholder();
  200.             isReordering = true;
  201.             card.style.opacity = "0.35";
  202.             setTimeout(() => {
  203.                 if (card.parentElement) {
  204.                     card.parentElement.insertBefore(placeholder, card.nextSibling);
  205.                 }
  206.             }, 0);
  207.         });
  208.  
  209.         card.addEventListener("dragend", () => {
  210.             card.style.opacity = "1";
  211.             if (placeholder?.parentElement && draggedCard) {
  212.                 placeholder.parentElement.insertBefore(draggedCard, placeholder);
  213.             }
  214.             placeholder?.remove();
  215.             placeholder = null;
  216.             draggedCard = null;
  217.             isReordering = false;
  218.             const newOrder = Array.from(grid.children)
  219.                 .filter((child) => child.dataset?.path)
  220.                 .map((child) => child.dataset.path);
  221.             setPaths(newOrder);
  222.         });
  223.  
  224.         card.addEventListener("dragover", (event) => {
  225.             event.preventDefault();
  226.             if (!draggedCard || draggedCard === card || !placeholder) {
  227.                 return;
  228.             }
  229.             const rect = card.getBoundingClientRect();
  230.             const centerX = rect.left + rect.width / 2;
  231.             const centerY = rect.top + rect.height / 2;
  232.             const dx = event.clientX - centerX;
  233.             const dy = event.clientY - centerY;
  234.             const horizontalDominant = Math.abs(dx) >= Math.abs(dy);
  235.             // Reorder a bit earlier than strict 50% to feel more responsive.
  236.             const insertAfter = horizontalDominant
  237.                 ? event.clientX > rect.left + rect.width * 0.4
  238.                 : event.clientY > rect.top + rect.height * 0.4;
  239.             grid.insertBefore(placeholder, insertAfter ? card.nextSibling : card);
  240.         });
  241.  
  242.         card.append(image, remove, badge);
  243.         return card;
  244.     }
  245.  
  246.     async function uploadFiles(fileList) {
  247.         const uploaded = [];
  248.         for (const file of Array.from(fileList || [])) {
  249.             const body = new FormData();
  250.             body.append("image", file);
  251.             const response = await api.fetchApi("/upload/image", { method: "POST", body });
  252.             if (response.status !== 200) {
  253.                 continue;
  254.             }
  255.             const payload = await response.json();
  256.             uploaded.push(payload.subfolder ? `${payload.subfolder}/${payload.name}` : payload.name);
  257.         }
  258.         if (uploaded.length) {
  259.             setPaths(getPaths().concat(uploaded));
  260.         }
  261.     }
  262.  
  263.     function render() {
  264.         const paths = getPaths();
  265.         countLabel.textContent = `${paths.length} image${paths.length === 1 ? "" : "s"}`;
  266.         grid.replaceChildren(...paths.map((path, index) => buildCard(path, index)));
  267.     }
  268.  
  269.     function syncLoaderStateFromWidget() {
  270.         const count = getPaths().length;
  271.         const visibleCardCount = Array.from(grid.children).filter((child) => child.dataset?.path).length;
  272.         if (node._denoImageCount !== count || (!isReordering && visibleCardCount !== count)) {
  273.             node._denoImageCount = count;
  274.             notifyConnectedSequencers(node, count);
  275.             render();
  276.         }
  277.     }
  278.  
  279.     uploadBtn.onclick = () => fileInput.click();
  280.     clearBtn.onclick = () => setPaths([]);
  281.     fileInput.onchange = (event) => uploadFiles(event.target.files);
  282.  
  283.     container.addEventListener("dragover", (event) => {
  284.         event.preventDefault();
  285.         container.style.borderColor = "rgba(72,255,132,0.9)";
  286.     });
  287.     container.addEventListener("dragleave", () => {
  288.         container.style.borderColor = "rgba(72,255,132,0.28)";
  289.     });
  290.     container.addEventListener("drop", (event) => {
  291.         event.preventDefault();
  292.         event.stopPropagation();
  293.         container.style.borderColor = "rgba(72,255,132,0.28)";
  294.         if (isReordering) {
  295.             return;
  296.         }
  297.         if (event.dataTransfer?.files?.length) {
  298.             uploadFiles(event.dataTransfer.files);
  299.         }
  300.     });
  301.  
  302.     const pasteHandler = (event) => {
  303.         if (!app.canvas.selected_nodes?.[node.id]) {
  304.             return;
  305.         }
  306.         const files = Array.from(event.clipboardData?.items || [])
  307.             .filter((item) => item.kind === "file" && item.type.startsWith("image/"))
  308.             .map((item) => item.getAsFile())
  309.             .filter(Boolean);
  310.         if (!files.length) {
  311.             return;
  312.         }
  313.         event.preventDefault();
  314.         event.stopImmediatePropagation();
  315.         uploadFiles(files);
  316.     };
  317.  
  318.     document.addEventListener("paste", pasteHandler, { capture: true });
  319.     const originalRemoved = node.onRemoved;
  320.     node.onRemoved = function () {
  321.         document.removeEventListener("paste", pasteHandler, { capture: true });
  322.         originalRemoved?.apply(this, arguments);
  323.     };
  324.  
  325.     const originalDraw = node.onDrawBackground;
  326.     node.onDrawBackground = function () {
  327.         originalDraw?.apply(this, arguments);
  328.         syncLoaderStateFromWidget();
  329.     };
  330.  
  331.     setTimeout(syncLoaderStateFromWidget, 50);
  332.     setTimeout(syncLoaderStateFromWidget, 250);
  333.     node._denoUpdateLoaderVisibility?.();
  334.     render();
  335. }
  336.  
  337. function patchSequencer(nodeType) {
  338.     const onNodeCreated = nodeType.prototype.onNodeCreated;
  339.     nodeType.prototype.onNodeCreated = function () {
  340.         const result = onNodeCreated?.apply(this, arguments);
  341.         setupSequencer(this);
  342.         return result;
  343.     };
  344.  
  345.     const onConfigure = nodeType.prototype.onConfigure;
  346.     nodeType.prototype.onConfigure = function () {
  347.         // Mark nodes restored from workflow serialization so we do not overwrite
  348.         // saved dynamic values with peer clone defaults.
  349.         this.__denoLoadedFromWorkflow = true;
  350.         const result = onConfigure?.apply(this, arguments);
  351.         setupSequencer(this);
  352.         return result;
  353.     };
  354. }
  355.  
  356. function isStrengthValueName(name) {
  357.     return /^strength_\d+$/.test(name || "");
  358. }
  359.  
  360. function normalizeBooleanValue(value) {
  361.     if (typeof value === "boolean") {
  362.         return value;
  363.     }
  364.     if (typeof value === "number") {
  365.         return value !== 0;
  366.     }
  367.     if (typeof value === "string") {
  368.         const v = value.trim().toLowerCase();
  369.         if (["false", "0", "off", "no", ""].includes(v)) {
  370.             return false;
  371.         }
  372.         if (["true", "1", "on", "yes"].includes(v)) {
  373.             return true;
  374.         }
  375.     }
  376.     return Boolean(value);
  377. }
  378.  
  379. function normalizeSequencerValue(name, value) {
  380.     const numeric = Number(value);
  381.     if (!Number.isFinite(numeric)) {
  382.         if (name === "strength_sync") {
  383.             return normalizeBooleanValue(value);
  384.         }
  385.         return value;
  386.     }
  387.  
  388.     if (name === "num_images" || name === "frame_rate" || name.startsWith("insert_frame_")) {
  389.         return Math.round(numeric);
  390.     }
  391.  
  392.     if (name === "strength_sync") {
  393.         return normalizeBooleanValue(value);
  394.     }
  395.  
  396.     if (name.startsWith("insert_second_")) {
  397.         return Math.max(0, Number(numeric.toFixed(2)));
  398.     }
  399.  
  400.     if (isStrengthValueName(name)) {
  401.         return Math.max(0, Math.min(1, Number(numeric.toFixed(2))));
  402.     }
  403.  
  404.     return value;
  405. }
  406.  
  407. function getSequencerDefaultValue(name) {
  408.     if (name.startsWith("insert_frame_") || name.startsWith("insert_second_")) {
  409.         return 0;
  410.     }
  411.     if (isStrengthValueName(name)) {
  412.         return 1.0;
  413.     }
  414.     if (name === "num_images") {
  415.         return 0;
  416.     }
  417.     return 0;
  418. }
  419.  
  420. function normalizeSequencerOrDefault(name, value, fallback = undefined) {
  421.     const normalized = normalizeSequencerValue(name, value);
  422.     if (normalized === undefined || Number.isNaN(normalized)) {
  423.         if (fallback !== undefined) {
  424.             const normalizedFallback = normalizeSequencerValue(name, fallback);
  425.             if (normalizedFallback !== undefined && !Number.isNaN(normalizedFallback)) {
  426.                 return normalizedFallback;
  427.             }
  428.         }
  429.         return getSequencerDefaultValue(name);
  430.     }
  431.     return normalized;
  432. }
  433.  
  434. function hasSequencerDynamicState(node) {
  435.     if (!node?.properties) {
  436.         return false;
  437.     }
  438.     for (let index = 1; index <= 50; index += 1) {
  439.         if (node.properties[`insert_frame_${index}`] !== undefined) {
  440.             return true;
  441.         }
  442.         if (node.properties[`insert_second_${index}`] !== undefined) {
  443.             return true;
  444.         }
  445.         if (node.properties[`strength_${index}`] !== undefined) {
  446.             return true;
  447.         }
  448.     }
  449.     return false;
  450. }
  451.  
  452. function getAllSequencerNodes(referenceNode = null) {
  453.     const graph = referenceNode?.graph || app.graph;
  454.     const registry = window.__denoLtxSequencerNodes || new Set();
  455.     const result = new Set();
  456.  
  457.     for (const candidate of registry) {
  458.         if (!candidate || candidate.comfyClass !== SEQUENCER_NODE || candidate.graph !== graph) {
  459.             registry.delete(candidate);
  460.             continue;
  461.         }
  462.  
  463.         const inGraph = typeof graph?.getNodeById === "function"
  464.             ? graph.getNodeById(candidate.id) === candidate
  465.             : (graph?._nodes || []).includes(candidate);
  466.         if (!inGraph) {
  467.             registry.delete(candidate);
  468.             continue;
  469.         }
  470.  
  471.         result.add(candidate);
  472.     }
  473.  
  474.     for (const candidate of graph?._nodes || []) {
  475.         if (candidate?.comfyClass === SEQUENCER_NODE) {
  476.             result.add(candidate);
  477.             if (!candidate.__denoSequencerReady) {
  478.                 try {
  479.                     setupSequencer(candidate);
  480.                 } catch (_err) {}
  481.             }
  482.         }
  483.     }
  484.     return Array.from(result);
  485. }
  486.  
  487. function canMirrorImageCountFromPeer(targetNode, count) {
  488.     if (!targetNode) {
  489.         return false;
  490.     }
  491.     const normalizedCount = normalizeSequencerValue("num_images", count);
  492.     const upstreamCount = readUpstreamImageCount(targetNode);
  493.     if (typeof upstreamCount === "number") {
  494.         // Respect independently connected chains with different known loader counts.
  495.         return upstreamCount === normalizedCount;
  496.     }
  497.     // If upstream is unresolved (or disconnected), allow peer synchronization.
  498.     return true;
  499. }
  500.  
  501. function mirrorSequencerImageCount(sourceNode, count) {
  502.     const normalizedCount = normalizeSequencerValue("num_images", count);
  503.     for (const targetNode of getAllSequencerNodes(sourceNode)) {
  504.         if (targetNode === sourceNode) {
  505.             continue;
  506.         }
  507.         if (!canMirrorImageCountFromPeer(targetNode, normalizedCount)) {
  508.             continue;
  509.         }
  510.         targetNode._syncImageCount?.(normalizedCount, { propagate: false });
  511.     }
  512. }
  513.  
  514. function findStrengthSyncPeer(node) {
  515.     for (const peerNode of getAllSequencerNodes(node)) {
  516.         if (peerNode === node) {
  517.             continue;
  518.         }
  519.         const peerSyncEnabled = peerNode.properties.strength_sync ?? getWidget(peerNode, "strength_sync")?.value ?? true;
  520.         if (peerSyncEnabled) {
  521.             return peerNode;
  522.         }
  523.     }
  524.     return null;
  525. }
  526.  
  527. function adoptStrengthValuesFromPeer(targetNode, sourceNode) {
  528.     const count = Number(targetNode.properties.num_images ?? getWidget(targetNode, "num_images")?.value ?? 0);
  529.     targetNode.__denoApplyingSync = true;
  530.     for (let index = 1; index <= count; index += 1) {
  531.         const name = `strength_${index}`;
  532.         const sourceWidget = getWidget(sourceNode, name);
  533.         const normalizedValue = normalizeSequencerValue(name, sourceWidget?.value ?? sourceNode.properties[name] ?? 1.0);
  534.         targetNode.properties[name] = normalizedValue;
  535.         const targetWidget = getWidget(targetNode, name);
  536.         if (targetWidget) {
  537.             targetWidget.value = normalizedValue;
  538.         }
  539.     }
  540.     targetNode.__denoApplyingSync = false;
  541.     targetNode._denoUpdateVisibility?.();
  542.     targetNode.setDirtyCanvas?.(true, true);
  543. }
  544.  
  545. function enableStrengthSync(node) {
  546.     const peerNode = findStrengthSyncPeer(node);
  547.     if (peerNode) {
  548.         adoptStrengthValuesFromPeer(node, peerNode);
  549.         return;
  550.     }
  551.     syncAllStrengthValues(node);
  552. }
  553.  
  554. function getSequencerNumImagesValue(node, fallbackValue) {
  555.     const upstreamCount = readUpstreamImageCount(node);
  556.     if (typeof upstreamCount === "number") {
  557.         return upstreamCount;
  558.     }
  559.     return normalizeSequencerValue("num_images", fallbackValue);
  560. }
  561.  
  562. function deferSequencerWidgetUpdate(fn) {
  563.     setTimeout(fn, 0);
  564. }
  565.  
  566. function scheduleUpstreamCountSync(node, options = {}) {
  567.     const propagate = options?.propagate !== false;
  568.     const delays = [0, 50, 140, 320];
  569.     for (const delay of delays) {
  570.         setTimeout(() => {
  571.             if (!node?.graph) {
  572.                 return;
  573.             }
  574.             const multiInputSlot = node.inputs?.find((slot) => slot.name === "multi_input");
  575.             const hasLinks = getInputLinkIds(multiInputSlot).length > 0;
  576.             if (!hasLinks) {
  577.                 if (node.__denoHadInputLink) {
  578.                     node.__denoHadInputLink = false;
  579.                     node._syncImageCount?.(0, { propagate: false });
  580.                 }
  581.                 return;
  582.             }
  583.             node.__denoHadInputLink = true;
  584.             const count = readUpstreamImageCount(node);
  585.             if (typeof count === "number") {
  586.                 node._syncImageCount?.(count, { propagate });
  587.             }
  588.         }, delay);
  589.     }
  590. }
  591.  
  592. function setupSequencer(node) {
  593.     if (node.__denoSequencerReady) {
  594.         return;
  595.     }
  596.     node.__denoSequencerReady = true;
  597.     node.properties = node.properties || {};
  598.     window.__denoLtxSequencerNodes.add(node);
  599.     node._currentImageCount = -1;
  600.     node.__denoApplyingSync = false;
  601.     node.__denoHadInputLink = false;
  602.  
  603.     const strengthSyncWidget = getWidget(node, "strength_sync");
  604.     const initialStrengthSync = normalizeBooleanValue(
  605.         node.properties.strength_sync ?? strengthSyncWidget?.value ?? true
  606.     );
  607.     if (strengthSyncWidget) {
  608.         strengthSyncWidget.value = initialStrengthSync;
  609.     }
  610.     node.properties.strength_sync = initialStrengthSync;
  611.  
  612.     const originalRemoved = node.onRemoved;
  613.     node.onRemoved = function () {
  614.         window.__denoLtxSequencerNodes.delete(node);
  615.         if (node.__denoCountPoll) {
  616.             clearInterval(node.__denoCountPoll);
  617.             delete node.__denoCountPoll;
  618.         }
  619.         delete node._syncImageCount;
  620.         originalRemoved?.apply(this, arguments);
  621.     };
  622.  
  623.     // Compatibility hook:
  624.     // WhatDreamsCost MultiImageLoader broadcasts image-count updates to connected nodes
  625.     // via targetNode._syncImageCount(count). Implement the same contract here.
  626.     node._syncImageCount = function (imageCount, options = {}) {
  627.         const count = normalizeSequencerValue("num_images", imageCount);
  628.         const currentCount = Number(this.properties.num_images ?? getWidget(this, "num_images")?.value ?? 0);
  629.         // Also check whether dynamic widgets actually exist; on first load they may not
  630.         // even if the count already matches the serialized value.
  631.         const dynamicWidgetsExist = (this.widgets || []).some(
  632.             (w) => w.name?.startsWith("insert_frame_") || isStrengthValueName(w.name)
  633.         );
  634.         if (count === currentCount && dynamicWidgetsExist) {
  635.             return;
  636.         }
  637.  
  638.         this.__denoApplyingSync = true;
  639.         const numWidget = getWidget(this, "num_images");
  640.         if (numWidget) {
  641.             numWidget.value = count;
  642.         }
  643.         this.properties.num_images = count;
  644.         this._applyWidgetCount(count);
  645.         this.__denoApplyingSync = false;
  646.         this._denoUpdateVisibility?.();
  647.         this.setDirtyCanvas?.(true, true);
  648.  
  649.         if (options?.propagate !== false) {
  650.             mirrorSequencerImageCount(this, count);
  651.         }
  652.     };
  653.  
  654.     node._hookStaticWidgets = function () {
  655.         for (const widget of this.widgets || []) {
  656.             if (widget.__denoStaticWrapped) {
  657.                 continue;
  658.             }
  659.             if (!["num_images", "insert_mode", "frame_rate", "strength_sync"].includes(widget.name)) {
  660.                 continue;
  661.             }
  662.  
  663.             const originalCallback = widget.callback;
  664.             widget.callback = (value) => {
  665.                 const callbackResult = originalCallback?.apply(widget, [value]);
  666.                 deferSequencerWidgetUpdate(() => {
  667.                     const rawValue = value ?? widget.value;
  668.                     const nextValue = widget.name === "num_images"
  669.                         ? getSequencerNumImagesValue(this, rawValue)
  670.                         : normalizeSequencerValue(widget.name, rawValue);
  671.                     widget.value = nextValue;
  672.                     this.properties[widget.name] = nextValue;
  673.  
  674.                     if (widget.name === "num_images") {
  675.                         this._applyWidgetCount(nextValue);
  676.                         this._denoUpdateVisibility?.();
  677.                     } else if (widget.name === "strength_sync") {
  678.                         if (nextValue) {
  679.                             enableStrengthSync(this);
  680.                         }
  681.                     } else {
  682.                         syncSequencerState(this, widget.name, nextValue);
  683.                         this._denoUpdateVisibility?.();
  684.                     }
  685.                 });
  686.                 return callbackResult;
  687.             };
  688.             widget.__denoStaticWrapped = true;
  689.         }
  690.     };
  691.  
  692.     const originalWidgetChanged = node.onWidgetChanged;
  693.     node.onWidgetChanged = function (name, value, oldValue, widget) {
  694.         const result = originalWidgetChanged?.apply(this, arguments);
  695.         if (this.__denoApplyingSync) {
  696.             return result;
  697.         }
  698.  
  699.         const widgetName = widget?.name ?? name;
  700.         if (!widgetName) {
  701.             return result;
  702.         }
  703.  
  704.         const isDynamicWidget =
  705.             widgetName.startsWith("insert_frame_") ||
  706.             widgetName.startsWith("insert_second_") ||
  707.             isStrengthValueName(widgetName);
  708.  
  709.         // Dynamic widgets are managed by addSyncedWidget callback.
  710.         // Handling them here can overwrite in-flight arrow increments.
  711.         if (isDynamicWidget) {
  712.             return result;
  713.         }
  714.  
  715.         const rawValue = value ?? widget?.value;
  716.         const normalizedValue = widgetName === "num_images"
  717.             ? getSequencerNumImagesValue(this, rawValue)
  718.             : normalizeSequencerValue(widgetName, rawValue);
  719.         if (widget) {
  720.             widget.value = normalizedValue;
  721.         }
  722.         this.properties[widgetName] = normalizedValue;
  723.         if (widgetName === "num_images") {
  724.             this._applyWidgetCount(normalizedValue);
  725.         } else if (widgetName === "strength_sync") {
  726.             if (normalizedValue) {
  727.                 enableStrengthSync(this);
  728.             }
  729.             this.setDirtyCanvas?.(true, true);
  730.         } else {
  731.             const isStrength = isStrengthValueName(widgetName);
  732.             const strengthSyncEnabled = this.properties.strength_sync ?? getWidget(this, "strength_sync")?.value ?? true;
  733.             if (!isStrength || strengthSyncEnabled) {
  734.                 syncSequencerState(this, widgetName, normalizedValue);
  735.             }
  736.         }
  737.         return result;
  738.     };
  739.  
  740.     node._denoUpdateVisibility = function () {
  741.         const count = Number(this.properties.num_images ?? getWidget(this, "num_images")?.value ?? 0);
  742.         const mode = this.properties.insert_mode ?? getWidget(this, "insert_mode")?.value ?? "frames";
  743.  
  744.         for (const widget of this.widgets || []) {
  745.             const name = widget.name || "";
  746.             if (name.startsWith("insert_frame_")) {
  747.                 const index = Number(name.split("_").pop());
  748.                 toggleWidgetVisibility(widget, index <= count && mode === "frames");
  749.             } else if (name.startsWith("insert_second_")) {
  750.                 const index = Number(name.split("_").pop());
  751.                 toggleWidgetVisibility(widget, index <= count && mode === "seconds");
  752.             } else if (isStrengthValueName(name)) {
  753.                 const index = Number(name.split("_").pop());
  754.                 toggleWidgetVisibility(widget, index <= count);
  755.             }
  756.         }
  757.  
  758.         this.setDirtyCanvas?.(true, true);
  759.     };
  760.  
  761.     node._applyWidgetCount = function (count) {
  762.         this._hookStaticWidgets();
  763.         const normalizedCount = Math.max(0, Math.min(Number(count) || 0, 50));
  764.         const width = this.size?.[0] ?? 360;
  765.  
  766.         if (this.widgets) {
  767.             for (const widget of this.widgets) {
  768.                 const name = widget.name || "";
  769.                 if (
  770.                     name.startsWith("insert_frame_") ||
  771.                     name.startsWith("insert_second_") ||
  772.                     isStrengthValueName(name)
  773.                 ) {
  774.                     // Preserve already-saved properties first, then fall back to current widget value.
  775.                     this.properties[name] = normalizeSequencerOrDefault(
  776.                         name,
  777.                         this.properties[name],
  778.                         widget.value
  779.                     );
  780.                 }
  781.             }
  782.         }
  783.  
  784.         this.widgets = (this.widgets || []).filter((widget) => {
  785.             const name = widget.name || "";
  786.             return !(
  787.                 name.startsWith("insert_frame_") ||
  788.                 name.startsWith("insert_second_") ||
  789.                 isStrengthValueName(name) ||
  790.                 name.startsWith("header_")
  791.             );
  792.         });
  793.  
  794.         const addSyncedWidget = (type, name, fallbackValue, options) => {
  795.             const savedValue = this.properties[name];
  796.             const initialValue = normalizeSequencerOrDefault(name, savedValue, fallbackValue);
  797.             this.properties[name] = initialValue;
  798.             const widget = this.addWidget(type, name, initialValue, (value) => {
  799.                 const applyValue = (rawValue) => {
  800.                     const prevValue = normalizeSequencerOrDefault(name, this.properties[name], fallbackValue);
  801.                     let nextValue = normalizeSequencerValue(name, rawValue);
  802.                     if (nextValue === undefined || Number.isNaN(nextValue)) {
  803.                         nextValue = normalizeSequencerOrDefault(name, rawValue, prevValue);
  804.                     }
  805.  
  806.                     // Some arrow paths emit tiny deltas while displayed precision is coarser.
  807.                     // Promote one visible step for arrow-like deltas that would otherwise look stuck.
  808.                     const isInsertFrameParam = name.startsWith("insert_frame_");
  809.                     const isFineStepParam = name.startsWith("insert_second_") || isStrengthValueName(name);
  810.                     const rawNumeric = Number(rawValue);
  811.                     const prevNumeric = Number(prevValue);
  812.                     if (
  813.                         (isFineStepParam || isInsertFrameParam) &&
  814.                         Number.isFinite(rawNumeric) &&
  815.                         Number.isFinite(prevNumeric) &&
  816.                         nextValue === prevValue &&
  817.                         rawNumeric !== prevNumeric
  818.                     ) {
  819.                         const delta = Math.abs(rawNumeric - prevNumeric);
  820.                         const isLikelyArrowDelta = isInsertFrameParam ? delta <= 0.11 : true;
  821.                         if (isLikelyArrowDelta) {
  822.                             const direction = rawNumeric > prevNumeric ? 1 : -1;
  823.                             const step = isInsertFrameParam ? 1 : 0.01;
  824.                             nextValue = normalizeSequencerValue(name, prevNumeric + direction * step);
  825.                         }
  826.                     }
  827.  
  828.                     // Always coerce the visible widget text/number to the normalized format
  829.                     // (e.g. prevent "-1.20000000000002" staying in an INT field).
  830.                     const normalizedWidgetValue = normalizeSequencerOrDefault(
  831.                         name,
  832.                         widget.value ?? rawValue,
  833.                         nextValue
  834.                     );
  835.                     if (widget.value !== normalizedWidgetValue) {
  836.                         widget.value = normalizedWidgetValue;
  837.                     }
  838.  
  839.                     if (nextValue === prevValue) {
  840.                         this.properties[name] = prevValue;
  841.                         this.setDirtyCanvas?.(true, true);
  842.                         return;
  843.                     }
  844.  
  845.                     widget.value = nextValue;
  846.                     this.properties[name] = nextValue;
  847.  
  848.                     const isStrength = isStrengthValueName(name);
  849.                     const strengthSyncEnabled = this.properties.strength_sync ?? getWidget(this, "strength_sync")?.value ?? true;
  850.                     if (!isStrength || strengthSyncEnabled) {
  851.                         syncSequencerState(this, name, nextValue);
  852.                     }
  853.                     this.setDirtyCanvas?.(true, true);
  854.                 };
  855.  
  856.                 // Arrow/button clicks can update widget.value after callback dispatch in some UI paths.
  857.                 // Avoid forcing a stale immediate value; sync from the post-update widget state.
  858.                 const immediateValue = value;
  859.                 const prevValue = normalizeSequencerValue(name, this.properties[name] ?? fallbackValue);
  860.                 const normalizedImmediate = normalizeSequencerValue(name, immediateValue);
  861.                 if (
  862.                     immediateValue !== undefined &&
  863.                     normalizedImmediate !== undefined &&
  864.                     !Number.isNaN(normalizedImmediate) &&
  865.                     normalizedImmediate !== prevValue
  866.                 ) {
  867.                     applyValue(immediateValue);
  868.                 }
  869.                 deferSequencerWidgetUpdate(() => applyValue(widget.value));
  870.                 requestAnimationFrame(() => applyValue(widget.value));
  871.                 setTimeout(() => applyValue(widget.value), 16);
  872.             }, options);
  873.             return widget;
  874.         };
  875.  
  876.         for (let index = 1; index <= normalizedCount; index += 1) {
  877.             this.addCustomWidget({
  878.                 name: `header_${index}`,
  879.                 type: "text",
  880.                 draw(ctx, currentNode, widgetWidth, y) {
  881.                     ctx.save();
  882.                     ctx.strokeStyle = "#333";
  883.                     ctx.lineWidth = 1;
  884.                     ctx.beginPath();
  885.                     ctx.moveTo(10, y + 5);
  886.                     ctx.lineTo(widgetWidth - 10, y + 5);
  887.                     ctx.stroke();
  888.                     ctx.fillStyle = "#dddddd";
  889.                     ctx.font = "bold 12px Arial";
  890.                     ctx.textAlign = "left";
  891.                     ctx.fillText(`Image #${index}`, 10, y + 24);
  892.                     ctx.restore();
  893.                 },
  894.                 computeSize(widgetWidth) {
  895.                     return [widgetWidth, 35];
  896.                 },
  897.             });
  898.  
  899.             addSyncedWidget("number", `insert_frame_${index}`, 0, { min: -9999, max: 9999, step: 1, precision: 0 });
  900.             addSyncedWidget("number", `insert_second_${index}`, 0.0, { min: 0.0, max: 9999.0, step: 0.01, precision: 2 });
  901.             addSyncedWidget("number", `strength_${index}`, 1.0, { min: 0.0, max: 1.0, step: 0.01, precision: 2 });
  902.         }
  903.  
  904.         this.properties.num_images = normalizedCount;
  905.         this._currentImageCount = normalizedCount;
  906.         this._denoUpdateVisibility?.();
  907.         this.setDirtyCanvas?.(true, true);
  908.         requestAnimationFrame(() => {
  909.             if (this.computeSize) {
  910.                 this.setSize([width, this.computeSize()[1]]);
  911.             }
  912.         });
  913.     };
  914.  
  915.     const originalConnectInput = node.onConnectInput;
  916.     node.onConnectInput = function (inputIndex) {
  917.         const result = originalConnectInput?.apply(this, arguments);
  918.         if (result === false) {
  919.             return result;
  920.         }
  921.         if (this.inputs?.[inputIndex]?.name === "multi_input") {
  922.             this.__denoHadInputLink = true;
  923.             scheduleUpstreamCountSync(this);
  924.         }
  925.         return result;
  926.     };
  927.  
  928.     const originalConnectionsChange = node.onConnectionsChange;
  929.     node.onConnectionsChange = function (type, index, connected, linkInfo) {
  930.         originalConnectionsChange?.apply(this, arguments);
  931.         if (type !== 1 || this.inputs?.[index]?.name !== "multi_input") {
  932.             return;
  933.         }
  934.         if (connected) {
  935.             this.__denoHadInputLink = true;
  936.             scheduleUpstreamCountSync(this);
  937.             return;
  938.         }
  939.         if (this.__denoHadInputLink) {
  940.             this.__denoHadInputLink = false;
  941.             this._syncImageCount?.(0, { propagate: false });
  942.         }
  943.     };
  944.  
  945.     setTimeout(() => {
  946.         if (node.__denoLoadedFromWorkflow) {
  947.             // Workflow load: properties already have correct values. Just rebuild widgets
  948.             // from properties without overwriting them, then sync count from upstream
  949.             // without propagating (which would overwrite peer values too).
  950.             node._applyWidgetCount(node.properties.num_images ?? getWidget(node, "num_images")?.value ?? 0);
  951.             scheduleUpstreamCountSync(node, { propagate: false });
  952.             return;
  953.         }
  954.         // Fresh node: clone state from a peer if one exists.
  955.         if (!hasSequencerDynamicState(node)) {
  956.             const peerNode = getAllSequencerNodes(node).find((candidate) => candidate !== node);
  957.             if (peerNode) {
  958.                 cloneSequencerState(peerNode, node);
  959.             }
  960.         }
  961.         const count = readUpstreamImageCount(node);
  962.         if (typeof count === "number") {
  963.             node._syncImageCount?.(count);
  964.         }
  965.         node._applyWidgetCount(node.properties.num_images ?? getWidget(node, "num_images")?.value ?? 0);
  966.         scheduleUpstreamCountSync(node, { propagate: false });
  967.     }, 50);
  968.  
  969.     // Keep count in sync even when an intermediate node sits between loader and sequencer.
  970.     node.__denoCountPoll = setInterval(() => {
  971.         if (!node.graph) {
  972.             if (node.__denoCountPoll) {
  973.                 clearInterval(node.__denoCountPoll);
  974.                 delete node.__denoCountPoll;
  975.             }
  976.             return;
  977.         }
  978.         const multiInputSlot = node.inputs?.find((slot) => slot.name === "multi_input");
  979.         const hasLinks = getInputLinkIds(multiInputSlot).length > 0;
  980.         if (!hasLinks) {
  981.             if (node.__denoHadInputLink) {
  982.                 node.__denoHadInputLink = false;
  983.                 node._syncImageCount?.(0, { propagate: false });
  984.             }
  985.             return;
  986.         }
  987.         node.__denoHadInputLink = true;
  988.         const count = readUpstreamImageCount(node);
  989.         if (typeof count !== "number") {
  990.             return;
  991.         }
  992.         const currentCount = Number(node.properties.num_images ?? getWidget(node, "num_images")?.value ?? 0);
  993.         if (count !== currentCount) {
  994.             node._syncImageCount?.(count);
  995.         }
  996.     }, 800);
  997. }
  998.  
  999. function syncSequencerState(sourceNode, changedName, value) {
  1000.     if (changedName === "num_images") {
  1001.         return;
  1002.     }
  1003.  
  1004.     const isStrength = isStrengthValueName(changedName);
  1005.     const normalizedValue = normalizeSequencerValue(changedName, value);
  1006.     const sourceAllowsStrengthSync =
  1007.         sourceNode.properties.strength_sync ?? getWidget(sourceNode, "strength_sync")?.value ?? true;
  1008.     if (isStrength && !sourceAllowsStrengthSync) {
  1009.         return;
  1010.     }
  1011.  
  1012.     for (const targetNode of getAllSequencerNodes(sourceNode)) {
  1013.         if (targetNode === sourceNode) {
  1014.             continue;
  1015.         }
  1016.  
  1017.         if (isStrength) {
  1018.             const targetAllowsStrengthSync = targetNode.properties.strength_sync ?? getWidget(targetNode, "strength_sync")?.value ?? true;
  1019.             if (!targetAllowsStrengthSync) {
  1020.                 continue;
  1021.             }
  1022.         }
  1023.  
  1024.         const currentTargetValue = normalizeSequencerValue(
  1025.             changedName,
  1026.             targetNode.properties[changedName] ?? getWidget(targetNode, changedName)?.value
  1027.         );
  1028.         if (currentTargetValue === normalizedValue) {
  1029.             continue;
  1030.         }
  1031.  
  1032.         targetNode.__denoApplyingSync = true;
  1033.         targetNode.properties[changedName] = normalizedValue;
  1034.         const widget = getWidget(targetNode, changedName);
  1035.         if (widget) {
  1036.             widget.value = normalizedValue;
  1037.         }
  1038.         if (changedName === "num_images") {
  1039.             targetNode._applyWidgetCount?.(normalizedValue);
  1040.         }
  1041.         targetNode._denoUpdateVisibility?.();
  1042.         targetNode.setDirtyCanvas?.(true, true);
  1043.         targetNode.__denoApplyingSync = false;
  1044.     }
  1045. }
  1046.  
  1047. function cloneSequencerState(sourceNode, targetNode) {
  1048.     targetNode.__denoApplyingSync = true;
  1049.     targetNode.properties = { ...targetNode.properties, ...sourceNode.properties };
  1050.  
  1051.     const count = Number(sourceNode.properties.num_images ?? getWidget(sourceNode, "num_images")?.value ?? 0);
  1052.     targetNode._applyWidgetCount?.(count);
  1053.  
  1054.     for (const widget of targetNode.widgets || []) {
  1055.         const name = widget.name || "";
  1056.         if (targetNode.properties[name] !== undefined) {
  1057.             const normalizedValue = normalizeSequencerValue(name, targetNode.properties[name]);
  1058.             targetNode.properties[name] = normalizedValue;
  1059.             widget.value = normalizedValue;
  1060.         }
  1061.     }
  1062.  
  1063.     targetNode._denoUpdateVisibility?.();
  1064.     targetNode.setDirtyCanvas?.(true, true);
  1065.     targetNode.__denoApplyingSync = false;
  1066. }
  1067.  
  1068. function syncAllStrengthValues(sourceNode) {
  1069.     const count = Number(sourceNode.properties.num_images ?? getWidget(sourceNode, "num_images")?.value ?? 0);
  1070.     for (let index = 1; index <= count; index += 1) {
  1071.         const widget = getWidget(sourceNode, `strength_${index}`);
  1072.         const value = normalizeSequencerValue(`strength_${index}`, widget?.value ?? sourceNode.properties[`strength_${index}`]);
  1073.         if (value !== undefined) {
  1074.             syncSequencerState(sourceNode, `strength_${index}`, value);
  1075.         }
  1076.     }
  1077. }
  1078.  
  1079. function notifyConnectedSequencers(loaderNode, count) {
  1080.     if (!loaderNode.graph) {
  1081.         return;
  1082.     }
  1083.  
  1084.     for (const output of loaderNode.outputs || []) {
  1085.         for (const linkId of output?.links || []) {
  1086.             const link = loaderNode.graph.links[linkId];
  1087.             if (!link) {
  1088.                 continue;
  1089.             }
  1090.             const targetNode = loaderNode.graph.getNodeById(link.target_id);
  1091.             if (!targetNode || targetNode.comfyClass !== SEQUENCER_NODE) {
  1092.                 continue;
  1093.             }
  1094.             targetNode._syncImageCount?.(count);
  1095.         }
  1096.     }
  1097. }
  1098.  
  1099. function getInputLinkIds(inputSlot) {
  1100.     if (!inputSlot) {
  1101.         return [];
  1102.     }
  1103.  
  1104.     const ids = [];
  1105.     if (inputSlot.link !== undefined && inputSlot.link !== null && inputSlot.link !== -1) {
  1106.         ids.push(inputSlot.link);
  1107.     }
  1108.     if (Array.isArray(inputSlot.links)) {
  1109.         for (const linkId of inputSlot.links) {
  1110.             if (linkId !== undefined && linkId !== null && linkId !== -1) {
  1111.                 ids.push(linkId);
  1112.             }
  1113.         }
  1114.     }
  1115.     return [...new Set(ids)];
  1116. }
  1117.  
  1118. function getGraphLink(graph, linkId) {
  1119.     if (!graph || linkId === undefined || linkId === null) {
  1120.         return null;
  1121.     }
  1122.     const links = graph.links;
  1123.     if (!links) {
  1124.         return null;
  1125.     }
  1126.     if (typeof links.get === "function") {
  1127.         return links.get(linkId) ?? links.get(Number(linkId)) ?? links.get(String(linkId)) ?? null;
  1128.     }
  1129.     return links[linkId] ?? links[Number(linkId)] ?? links[String(linkId)] ?? null;
  1130. }
  1131.  
  1132. function readUpstreamImageCount(node) {
  1133.     const input = node.inputs?.find((slot) => slot.name === "multi_input");
  1134.     const startLinks = getInputLinkIds(input);
  1135.     const graph = node.graph || app.graph;
  1136.     if (!startLinks.length || !graph) {
  1137.         return null;
  1138.     }
  1139.  
  1140.     function isLoaderNode(targetNode) {
  1141.         if (!targetNode) {
  1142.             return false;
  1143.         }
  1144.         const clsRaw = targetNode.comfyClass || targetNode.type || "";
  1145.         const cls = String(clsRaw).toLowerCase().replace(/\s+/g, "");
  1146.         return (
  1147.             cls === String(LOADER_NODE).toLowerCase() ||
  1148.             cls === "multiimageloader" ||
  1149.             cls.endsWith("multiimageloader") ||
  1150.             typeof targetNode._denoImageCount === "number" ||
  1151.             typeof targetNode._imageCount === "number" ||
  1152.             !!getWidget(targetNode, "image_paths")
  1153.         );
  1154.     }
  1155.  
  1156.     function getCountFromLoaderNode(loaderNode) {
  1157.         if (!isLoaderNode(loaderNode)) {
  1158.             return null;
  1159.         }
  1160.         if (typeof loaderNode._denoImageCount === "number") {
  1161.             return loaderNode._denoImageCount;
  1162.         }
  1163.         if (typeof loaderNode._imageCount === "number") {
  1164.             return loaderNode._imageCount;
  1165.         }
  1166.         const imagePathsWidget = getWidget(loaderNode, "image_paths");
  1167.         const rawPaths = imagePathsWidget?.value ?? loaderNode.properties?.image_paths;
  1168.         if (typeof rawPaths === "string") {
  1169.             return rawPaths.split(/\n|,/).map((entry) => entry.trim()).filter(Boolean).length;
  1170.         }
  1171.         if (Array.isArray(rawPaths)) {
  1172.             return rawPaths.map((entry) => String(entry || "").trim()).filter(Boolean).length;
  1173.         }
  1174.         return null;
  1175.     }
  1176.  
  1177.     function scoreInputSlot(slot) {
  1178.         const name = String(slot?.name || "").toLowerCase();
  1179.         if (name.includes("multi") || name.includes("image")) {
  1180.             return 0;
  1181.         }
  1182.         return 1;
  1183.     }
  1184.  
  1185.     function enqueueNodeInputs(targetNode, queue) {
  1186.         if (!targetNode || targetNode.graph !== graph) {
  1187.             return;
  1188.         }
  1189.         const linkedInputs = (targetNode.inputs || [])
  1190.             .map((slot) => ({ slot, linkIds: getInputLinkIds(slot) }))
  1191.             .filter((entry) => entry.linkIds.length > 0)
  1192.             .sort((a, b) => scoreInputSlot(a.slot) - scoreInputSlot(b.slot));
  1193.         for (const entry of linkedInputs) {
  1194.             for (const nestedLink of entry.linkIds) {
  1195.                 queue.push(nestedLink);
  1196.             }
  1197.         }
  1198.     }
  1199.  
  1200.     const visitedLinks = new Set();
  1201.     const visitedNodeIds = new Set();
  1202.     const pendingLinks = [...startLinks];
  1203.  
  1204.     while (pendingLinks.length) {
  1205.         const linkId = pendingLinks.shift();
  1206.         const linkKey = String(linkId);
  1207.         if (!linkKey || visitedLinks.has(linkKey)) {
  1208.             continue;
  1209.         }
  1210.         visitedLinks.add(linkKey);
  1211.  
  1212.         const upstreamLink = getGraphLink(graph, linkId);
  1213.         if (!upstreamLink) {
  1214.             continue;
  1215.         }
  1216.         const originNodeId = upstreamLink.origin_id ?? upstreamLink.originId ?? upstreamLink.origin;
  1217.         if (originNodeId === undefined || originNodeId === null) {
  1218.             continue;
  1219.         }
  1220.  
  1221.         const upstreamNode = graph.getNodeById?.(originNodeId);
  1222.         if (!upstreamNode) {
  1223.             continue;
  1224.         }
  1225.         const nodeKey = String(upstreamNode.id ?? originNodeId);
  1226.         if (visitedNodeIds.has(nodeKey)) {
  1227.             continue;
  1228.         }
  1229.         visitedNodeIds.add(nodeKey);
  1230.  
  1231.         const directCount = getCountFromLoaderNode(upstreamNode);
  1232.         if (typeof directCount === "number") {
  1233.             return directCount;
  1234.         }
  1235.  
  1236.         // Support virtual Get/Set style nodes (e.g. easy getNode / KJ GetNode):
  1237.         // resolve the source link from its paired Set node and continue tracing.
  1238.         const originSlot = upstreamLink.origin_slot ?? upstreamLink.originSlot ?? 0;
  1239.         if (typeof upstreamNode.getInputLink === "function") {
  1240.             try {
  1241.                 const virtualLink = upstreamNode.getInputLink(originSlot);
  1242.                 const virtualOriginId = virtualLink?.origin_id ?? virtualLink?.originId;
  1243.                 if (virtualOriginId !== undefined && virtualOriginId !== null) {
  1244.                     const virtualOriginNode = (upstreamNode.graph || graph).getNodeById?.(virtualOriginId);
  1245.                     if (virtualOriginNode) {
  1246.                         const virtualCount = getCountFromLoaderNode(virtualOriginNode);
  1247.                         if (typeof virtualCount === "number") {
  1248.                             return virtualCount;
  1249.                         }
  1250.                         enqueueNodeInputs(virtualOriginNode, pendingLinks);
  1251.                     }
  1252.                 }
  1253.             } catch (_err) {}
  1254.         }
  1255.         if (typeof upstreamNode.resolveVirtualOutput === "function") {
  1256.             try {
  1257.                 const resolved = upstreamNode.resolveVirtualOutput(originSlot);
  1258.                 const virtualOriginNode = resolved?.node;
  1259.                 if (virtualOriginNode) {
  1260.                     const virtualCount = getCountFromLoaderNode(virtualOriginNode);
  1261.                     if (typeof virtualCount === "number") {
  1262.                         return virtualCount;
  1263.                     }
  1264.                     enqueueNodeInputs(virtualOriginNode, pendingLinks);
  1265.                 }
  1266.             } catch (_err) {}
  1267.         }
  1268.  
  1269.         // Reroute/pass-through nodes
  1270.         if (upstreamNode.type === "Reroute" || upstreamNode.comfyClass === "Reroute") {
  1271.             const rerouteLinks = getInputLinkIds(upstreamNode.inputs?.[0]);
  1272.             for (const nestedLink of rerouteLinks) {
  1273.                 pendingLinks.unshift(nestedLink);
  1274.             }
  1275.             continue;
  1276.         }
  1277.  
  1278.         // Group/subgraph nodes that can expose inner node for connected output slot
  1279.         if (typeof upstreamNode.getInnerNode === "function") {
  1280.             try {
  1281.                 const originSlot = upstreamLink.origin_slot ?? upstreamLink.originSlot ?? 0;
  1282.                 const innerNode = upstreamNode.getInnerNode(originSlot);
  1283.                 const innerCount = getCountFromLoaderNode(innerNode);
  1284.                 if (typeof innerCount === "number") {
  1285.                     return innerCount;
  1286.                 }
  1287.             } catch (_err) {}
  1288.         }
  1289.  
  1290.         // Generic pass-through tracing:
  1291.         // follow all linked inputs (prioritize image-like names) to find the true upstream loader.
  1292.         enqueueNodeInputs(upstreamNode, pendingLinks);
  1293.     }
  1294.  
  1295.     // Conservative fallback: only when a single known loader exists in the graph.
  1296.     const allNodes = graph?._nodes || [];
  1297.     const loaderCandidates = allNodes.filter((candidate) => isLoaderNode(candidate));
  1298.     if (loaderCandidates.length === 1) {
  1299.         return getCountFromLoaderNode(loaderCandidates[0]);
  1300.     }
  1301.  
  1302.     return null;
  1303. }
  1304.  
  1305. function toggleWidgetVisibility(widget, visible) {
  1306.     if (!widget) {
  1307.         return;
  1308.     }
  1309.     if (visible) {
  1310.         if (widget.__denoOrigType !== undefined) {
  1311.             widget.type = widget.__denoOrigType;
  1312.             widget.computeSize = widget.__denoOrigComputeSize;
  1313.             delete widget.__denoOrigType;
  1314.             delete widget.__denoOrigComputeSize;
  1315.         }
  1316.         return;
  1317.     }
  1318.  
  1319.     if (widget.type !== "hidden") {
  1320.         widget.__denoOrigType = widget.type;
  1321.         widget.__denoOrigComputeSize = widget.computeSize;
  1322.         widget.type = "hidden";
  1323.         widget.computeSize = () => [0, -4];
  1324.     }
  1325. }
  1326.  
  1327. function createActionButton(label, danger = false) {
  1328.     const button = document.createElement("button");
  1329.     button.type = "button";
  1330.     button.textContent = label;
  1331.     button.style.cssText = `
  1332.         border:none;
  1333.         border-radius:999px;
  1334.         padding:6px 10px;
  1335.         cursor:pointer;
  1336.         font:600 11px sans-serif;
  1337.         color:${danger ? "#ffd5d5" : "#d9ffe5"};
  1338.         background:${danger ? "rgba(119, 26, 26, 0.95)" : "rgba(22, 58, 35, 0.95)"};
  1339.     `;
  1340.     return button;
  1341. }
  1342.  
  1343. function hideWidget(widget) {
  1344.     widget.hidden = true;
  1345.     widget.computeSize = () => [0, -4];
  1346.     if (widget.element) {
  1347.         widget.element.style.display = "none";
  1348.     }
  1349. }
  1350.  
  1351. function getWidget(node, name) {
  1352.     return (node.widgets || []).find((widget) => widget.name === name);
  1353. }
  1354.  
Add Comment
Please, Sign In to add comment