Files
ausboss-ComfyUI-AusBoss/js/shared/input_preview.mjs
T
ausboss 7b8aa698e6 Preview nodes keep their size in Nodes 2.0
In Nodes 2.0 a node's saved height is only a minimum and its content can
make it taller. The preview panel's picture was in normal flow, so a
portrait result drawn at full width grew Save Image from 730 to 1276 px in
the Krea 2 Outpaint example, over the Run Timer under it. Nodes 2.0 also
drew its own copy of the result under the panel, because the panel never
set hideOutputImages.

- The stage's picture is out of flow and centred, so the stage gets its
  size from the node and the picture scales to fit, in both renderers.
- suppressCoreImagePreview sets hideOutputImages; classic never reads it.
- In Nodes 2.0 that copy was the only thing filling node.imgs, so the
  panel falls back to the node's stored output (app.nodeOutputs) and
  re-reads it when a workflow tab restores its outputs. node.imgs still
  wins where it exists, so classic keeps its old path.

Select Frame, Mask Refine and LaMa Inpaint share the panel and grew the
same way. Core Save Image and Preview Image keep their size in Nodes 2.0.
Classic is unchanged.

Tests: the stage-picture guard in panel_guards, hideOutputImages in
core_preview and the stored-output lookup in input_preview fail on main
and pass here.
2026-09-29 05:11:19 -05:00

116 lines
4.8 KiB
JavaScript

// Pure resolution rules for the upstream-input preview panel. DOM-free so
// node:test covers them without a browser; the wiring lives in
// js/input_preview/index.js.
//
// Best-effort order for a connected source node:
// 1. cached execution previews (sourceNode.imgs[0].src)
// 2. an uploadable image/video file combo widget -> deterministic /view query
// 3. nothing -> the caller shows a quiet placeholder
import { splitMediaName } from "./video_preview.mjs";
// Core loader nodes keep the uploaded file name in a combo widget with one of
// these names (LoadImage uses "image", video loaders use "video").
const FILE_WIDGET_NAMES = new Set(["image", "video"]);
// Graph link stores differ by frontend generation: a Map on the modern
// frontend, a plain id-keyed object on legacy LiteGraph.
export function linkById(graph, linkId) {
const links = graph?.links;
if (links == null || linkId == null) return null;
const link = typeof links.get === "function" ? links.get(linkId) : links[linkId];
return link ?? null;
}
// Walk the named input's link back to the node feeding it, or null.
export function upstreamNode(node, inputName) {
const graph = node?.graph;
if (!graph) return null;
const input = node.inputs?.find((candidate) => candidate?.name === inputName);
const link = linkById(graph, input?.link);
if (!link) return null;
return graph.getNodeById?.(link.origin_id) ?? null;
}
// The widget whose callback fires when the user picks a different file.
export function sourceFileWidget(sourceNode) {
return sourceNode?.widgets?.find((widget) => FILE_WIDGET_NAMES.has(widget?.name)) ?? null;
}
// Deterministic /view query (no cache buster) for an input-folder file name,
// tolerating Windows separators and " [input]" annotations.
export function viewQueryForFile(name) {
const { filename, subfolder } = splitMediaName(name);
return new URLSearchParams({ filename, subfolder, type: "input" }).toString();
}
// Describe how to preview a source node:
// { kind: "url", url } cached preview image
// { kind: "view", isVideo, query } file widget, serve via api /view
// null nothing resolvable
export function describeSourcePreview(sourceNode) {
if (!sourceNode) return null;
const cached = sourceNode.imgs?.[0]?.src;
if (typeof cached === "string" && cached) return { kind: "url", url: cached };
const widget = sourceFileWidget(sourceNode);
if (widget && typeof widget.value === "string" && widget.value) {
return {
kind: "view",
isVideo: widget.name === "video",
query: viewQueryForFile(widget.value),
};
}
return null;
}
// The node's own latest result, if it has produced one.
//
// imgs is where the frontend leaves both the streamed progress frames of a
// running node and the images from its ui payload, so the last entry is the
// most recent thing this node made. The panel prefers it over the upstream
// input for the obvious reason: once a node has a result, its result is what
// you want to look at.
export function describeOwnResult(node) {
const images = node?.imgs;
if (!Array.isArray(images) || images.length === 0) return null;
const src = images[images.length - 1]?.src;
return typeof src === "string" && src ? { kind: "url", url: src } : null;
}
// The key the frontend files a node's outputs under (app.nodeOutputs): the
// node's id in the root graph, "<subgraph id>:<id>" inside a subgraph.
export function outputLocatorId(node) {
const graph = node?.graph;
if (!graph || node.id == null) return null;
const root = graph.rootGraph ?? graph;
return graph !== root && graph.id ? `${graph.id}:${node.id}` : String(node.id);
}
// The /view query for the newest image in a node's output record
// ({ images: [{ filename, subfolder, type }] }), or null when it has none.
// Nodes 2.0 fills node.imgs only from its own output preview, which the panel
// stands down, so there this record is the node's result.
export function outputRecordQuery(record) {
const images = record?.images;
const last = Array.isArray(images) ? images[images.length - 1] : null;
if (!last || typeof last.filename !== "string" || !last.filename) return null;
return new URLSearchParams({
filename: last.filename,
subfolder: last.subfolder ?? "",
type: last.type ?? "output",
}).toString();
}
// What the panel shows, best first: this node's result (node.imgs, then the
// URL of its stored output), then whatever feeds it, then nothing.
export function describeNodePreview(node, inputName, storedUrl = null) {
return describeOwnResult(node)
?? (storedUrl ? { kind: "url", url: storedUrl } : null)
?? describeSourcePreview(upstreamNode(node, inputName));
}
// Quiet ASCII placeholder copy; never an error.
export function placeholderText(connected, noun = "an image") {
return connected ? "run to preview" : `connect ${noun} to preview`;
}