diff --git a/loop_nodes.py b/loop_nodes.py index 29844c4..7bdd131 100644 --- a/loop_nodes.py +++ b/loop_nodes.py @@ -54,6 +54,14 @@ INDEX_SWITCH_MISSING_BEHAVIORS = ["fallback", "none", "clamp", "wrap"] _ACCUMULATOR_STORES: dict[str, list[dict[str, Any]]] = {} +def _entry_preview_key(entry: dict[str, Any]) -> str: + key = str(entry.get("_sxcp_preview_key") or "").strip() + if not key: + key = f"entry_{random.getrandbits(64):016x}" + entry["_sxcp_preview_key"] = key + return key + + class AnyType(str): def __ne__(self, _other: object) -> bool: return False @@ -218,6 +226,7 @@ def _entry_infos(store: list[dict[str, Any]]) -> list[dict[str, Any]]: { "index": index, "id": str(entry.get("id") or ""), + "preview_key": _entry_preview_key(entry), "has_image": image is not None, "has_metadata": has_metadata, "shape": list(shape) if shape is not None else [], @@ -424,7 +433,13 @@ def _preview_image_results( metadata = _metadata_for_entry(entry, prompt, extra_pnginfo) file = f"{filename}_{counter:05}_.png" _image_to_pil(image).save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=1) - results.append({"filename": file, "subfolder": subfolder, "type": "temp"}) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": "temp", + "preview_key": _entry_preview_key(entry), + "entry_id": str(entry.get("id") or ""), + }) counter += 1 return results diff --git a/web/accumulator_preview.js b/web/accumulator_preview.js index e957813..0bd4480 100644 --- a/web/accumulator_preview.js +++ b/web/accumulator_preview.js @@ -82,6 +82,40 @@ function imageEntries(node) { return entries.filter((entry) => entry?.has_image).slice(0, previewLimit(node)); } +function entryKey(entry) { + const key = String(entry?.preview_key || "").trim(); + if (key) return `key:${key}`; + const id = String(entry?.id || "").trim(); + if (id) return `id:${id}`; + return `index:${entry?.index ?? ""}`; +} + +function buildImageMap(entries, images, previous = new Map()) { + const next = new Map(previous); + const imageEntries = asArray(entries).filter((entry) => entry?.has_image); + asArray(images).filter(Boolean).forEach((image, index) => { + const directKey = String(image?.preview_key || "").trim(); + if (directKey) { + next.set(`key:${directKey}`, image); + return; + } + const entryId = String(image?.entry_id || "").trim(); + if (entryId) { + next.set(`id:${entryId}`, image); + return; + } + const entry = imageEntries[index]; + if (entry) next.set(entryKey(entry), image); + }); + return next; +} + +function imageParamsForEntry(node, entry, fallbackIndex) { + const keyed = node._sxapImageByKey?.get(entryKey(entry)); + if (keyed) return keyed; + return (node._sxapImages || [])[fallbackIndex]; +} + function imageUrl(params) { const query = new URLSearchParams(params).toString(); const preview = app.getPreviewFormatParam?.() || ""; @@ -322,7 +356,6 @@ function renderGrid(node) { const grid = node._sxapGridEl; if (!grid) return; const entries = imageEntries(node); - const images = node._sxapImages || []; grid.replaceChildren(); if (!entries.length) { @@ -332,7 +365,7 @@ function renderGrid(node) { grid.appendChild(empty); } else { entries.forEach((entry, index) => { - grid.appendChild(renderCell(node, entry, images[index], index)); + grid.appendChild(renderCell(node, entry, imageParamsForEntry(node, entry, index), index)); }); } @@ -364,6 +397,7 @@ function applyData(node, data, status = "") { if (Object.prototype.hasOwnProperty.call(data || {}, "images")) { node._sxapImages = asArray(data.images).filter(Boolean); } + node._sxapImageByKey = buildImageMap(node._sxapEntries, node._sxapImages, node._sxapImageByKey); setStatus(node, status || data?.status || ""); renderGrid(node); } @@ -532,6 +566,7 @@ function setupNode(node) { node._sxapStatusEl = status; node._sxapEntries = node._sxapEntries || []; node._sxapImages = node._sxapImages || []; + node._sxapImageByKey = node._sxapImageByKey || new Map(); node._sxapLastCount = -1; recomputeSize(node); node._sxapWidget = node.addDOMWidget("accumulator_grid", "div", wrap, {