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.
116 lines
4.8 KiB
JavaScript
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`;
|
|
}
|