Files
MajoorWaldi-ComfyUI-Majoor-…/js/preview/core/renderer.js
T

319 lines
12 KiB
JavaScript

import { getInputOriginSlot, getUpstreamNode, detectSource } from "./graph.js";
import { makeViewUrl, ensureBitmap, ensureImageElement, ensureVideoFrameCanvas, fitWithinMaxSize, renderImageSourceToCanvas } from "../source.js";
import { isImageOpsClass } from "../shared/classes.js";
function buildRenderer({ api, registry, canvasSize }) {
const MAX_RECURSION = 64;
function hashString(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16);
}
function getNativePreviewElement(node) {
const imgs = node?.imgs;
if (!Array.isArray(imgs) || imgs.length === 0) return null;
const index = typeof node.imageIndex === "number" ? node.imageIndex : imgs.length - 1;
return imgs[Math.max(0, Math.min(imgs.length - 1, index))] ?? imgs[imgs.length - 1] ?? null;
}
async function renderFromNativePreview(node) {
if (isImageOpsClass(node?.comfyClass)) return null;
const media = getNativePreviewElement(node);
if (!media) return null;
if (media instanceof HTMLVideoElement) {
if (media.readyState < 2) {
try {
await media.play();
} catch {
}
if (media.readyState < 2) return null;
}
const dims = fitWithinMaxSize(media.videoWidth || 1, media.videoHeight || 1, canvasSize);
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
}
if (media instanceof HTMLImageElement) {
if (!media.complete) {
try {
await media.decode?.();
} catch {
}
if (!media.complete) return null;
}
const dims = fitWithinMaxSize(media.naturalWidth || media.width || 1, media.naturalHeight || media.height || 1, canvasSize);
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
}
if (media instanceof HTMLCanvasElement) {
const dims = fitWithinMaxSize(media.width || 1, media.height || 1, canvasSize);
return renderImageSourceToCanvas(node, media, dims.width, dims.height, "nativeCanvas");
}
return null;
}
function makePlaceholderCanvas() {
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
return canvas;
}
const STATIC_CACHE_MAX = 16;
function getPersistentStaticCache(node) {
var _a;
node.__imageops_media ?? (node.__imageops_media = {});
(_a = node.__imageops_media).staticRenderCache ?? (_a.staticRenderCache = /* @__PURE__ */ new Map());
return node.__imageops_media.staticRenderCache;
}
function pruneStaticCache(cache) {
while (cache.size > STATIC_CACHE_MAX) {
const oldest = cache.keys().next().value;
if (oldest !== void 0) cache.delete(oldest);
}
}
async function render(node, tick = 0, outputSlot = null, canvasSizeOverride) {
const ctx = { api, canvasSize: canvasSizeOverride ?? canvasSize, tick, cache: /* @__PURE__ */ new Map(), visited: /* @__PURE__ */ new Set() };
const canvas = await renderNode(node, ctx, outputSlot);
return { canvas };
}
async function renderNode(node, ctx, outputSlot) {
if (!node) return null;
if (ctx.visited.has(node.id)) return null;
if (ctx.visited.size > MAX_RECURSION) return null;
ctx.visited.add(node.id);
const sig = signature(node, ctx.tick, outputSlot);
if (ctx.cache.has(sig)) {
ctx.visited.delete(node.id);
return ctx.cache.get(sig);
}
if (ctx.tick === 0) {
const persistent = getPersistentStaticCache(node);
const cached = persistent.get(sig);
if (cached) {
ctx.cache.set(sig, cached);
ctx.visited.delete(node.id);
return cached;
}
}
const src = detectSource(node);
if (src) {
const url = makeViewUrl(ctx.api, src.value);
if (!url) {
ctx.visited.delete(node.id);
return null;
}
let c = null;
if (src.kind === "image" && src.animated) {
const img = await ensureImageElement(node, url);
if (!img) {
ctx.visited.delete(node.id);
return null;
}
const dims = fitWithinMaxSize(img.naturalWidth || img.width || 1, img.naturalHeight || img.height || 1, ctx.canvasSize);
c = renderImageSourceToCanvas(node, img, dims.width, dims.height, "animatedImageCanvas");
} else if (src.kind === "image") {
const bmp = await ensureBitmap(node, url);
if (!bmp) {
ctx.visited.delete(node.id);
return null;
}
const dims = fitWithinMaxSize(bmp.width || 1, bmp.height || 1, ctx.canvasSize);
c = renderImageSourceToCanvas(node, bmp, dims.width, dims.height, "imageCanvas");
} else {
c = await ensureVideoFrameCanvas(node, url, ctx.canvasSize, ctx.tick);
}
if (!c) {
ctx.visited.delete(node.id);
return null;
}
ctx.cache.set(sig, c);
if (ctx.tick === 0) {
const persistent = getPersistentStaticCache(node);
persistent.clear();
persistent.set(sig, c);
pruneStaticCache(persistent);
}
ctx.visited.delete(node.id);
return c;
}
const nativePreview = await renderFromNativePreview(node);
if (nativePreview) {
ctx.cache.set(sig, nativePreview);
if (ctx.tick === 0) {
const persistent = getPersistentStaticCache(node);
persistent.clear();
persistent.set(sig, nativePreview);
pruneStaticCache(persistent);
}
ctx.visited.delete(node.id);
return nativePreview;
}
const adapter = registry.pick(node);
const adapterInputIndexes = adapter ? typeof adapter.inputIndexes === "function" ? adapter.inputIndexes(node) : adapter.inputIndexes ?? [] : [];
const resolvedIndexes = adapterInputIndexes.length > 0 ? adapterInputIndexes : [...Array(adapter ? typeof adapter.inputs === "function" ? adapter.inputs(node) : adapter.inputs ?? 1 : 1).keys()];
if (!adapter) {
const primaryInputIndex2 = resolvedIndexes[0] ?? 0;
const primary2 = await renderNode(getUpstreamNode(node, primaryInputIndex2), ctx, getInputOriginSlot(node, primaryInputIndex2));
ctx.visited.delete(node.id);
return primary2;
}
const renderInputAt = async (inputInfo, tickOverride) => {
if (!inputInfo.upstreamNode) return inputInfo.canvas ?? null;
const localCtx = {
api,
canvasSize: ctx.canvasSize,
tick: tickOverride,
cache: /* @__PURE__ */ new Map(),
visited: /* @__PURE__ */ new Set([node.id])
};
return await renderNode(inputInfo.upstreamNode, localCtx, inputInfo.originSlot ?? null);
};
if (resolvedIndexes.length === 0) {
const out2 = document.createElement("canvas");
out2.width = 1;
out2.height = 1;
const octx2 = out2.getContext("2d");
if (!octx2) {
ctx.visited.delete(node.id);
return null;
}
let adapted2 = out2;
try {
adapted2 = await adapter.apply({ node, ctx: octx2, canvasSize: ctx.canvasSize, inputs: [], tick: ctx.tick, renderInputAt });
} catch (err) {
console.warn(`[ImageOps] adapter '${adapter?.name ?? node.comfyClass}' threw \u2014 falling back to placeholder.`, err);
adapted2 = out2;
}
const result2 = adapted2 instanceof HTMLCanvasElement ? adapted2 : out2;
ctx.cache.set(sig, result2);
if (ctx.tick === 0) {
const persistent = getPersistentStaticCache(node);
persistent.clear();
persistent.set(sig, result2);
pruneStaticCache(persistent);
}
ctx.visited.delete(node.id);
return result2;
}
const primaryInputIndex = resolvedIndexes[0] ?? 0;
const primary = await renderNode(getUpstreamNode(node, primaryInputIndex), ctx, getInputOriginSlot(node, primaryInputIndex));
if (resolvedIndexes.length > 1) {
const primaryUp = getUpstreamNode(node, primaryInputIndex);
const primaryVid = primaryUp?.__imageops_media?.videoEl;
if (primaryVid && primaryVid.readyState >= 2 && Number.isFinite(primaryVid.currentTime)) {
const refTime = primaryVid.currentTime;
for (let si = 1; si < resolvedIndexes.length; si++) {
const secUp = getUpstreamNode(node, resolvedIndexes[si]);
const secVid = secUp?.__imageops_media?.videoEl;
if (secVid && secVid !== primaryVid && secVid.readyState >= 2 && Number.isFinite(secVid.duration) && secVid.duration > 0) {
const target = refTime % secVid.duration;
if (Math.abs(secVid.currentTime - target) > 0.05) {
try {
secVid.currentTime = target;
} catch {
}
}
}
}
}
}
if (!primary) {
ctx.visited.delete(node.id);
return null;
}
const inputs = [];
const inputInfos = [];
for (let i = 0; i < resolvedIndexes.length; i++) {
const inputIndex = resolvedIndexes[i];
const up = getUpstreamNode(node, inputIndex);
const originSlot = getInputOriginSlot(node, inputIndex);
if (i === 0 && !up) {
ctx.visited.delete(node.id);
return primary;
}
if (i === 0 && inputIndex === primaryInputIndex && primary) {
inputs.push(primary);
inputInfos.push({ canvas: primary, inputIndex, originSlot, upstreamNode: up });
continue;
}
const c = await renderNode(up, ctx, originSlot);
if (!c) {
if (up) {
const placeholder = makePlaceholderCanvas();
inputs.push(placeholder);
inputInfos.push({ canvas: placeholder, inputIndex, originSlot, upstreamNode: up });
continue;
}
ctx.cache.set(sig, primary);
ctx.visited.delete(node.id);
return primary;
}
inputs.push(c);
inputInfos.push({ canvas: c, inputIndex, originSlot, upstreamNode: up });
}
if (inputs.length === 0) {
ctx.cache.set(sig, primary);
ctx.visited.delete(node.id);
return primary;
}
const out = document.createElement("canvas");
out.width = primary.width;
out.height = primary.height;
const octx = out.getContext("2d");
if (!octx) {
ctx.visited.delete(node.id);
return null;
}
octx.drawImage(inputs[0], 0, 0);
let adapted = out;
try {
adapted = await adapter.apply({ node, ctx: octx, canvasSize: ctx.canvasSize, inputs, inputInfos, outputSlot, tick: ctx.tick, renderInputAt });
} catch (err) {
console.warn(`[ImageOps] adapter '${adapter?.name ?? node.comfyClass}' threw \u2014 passthrough first input.`, err);
adapted = inputs[0] ?? out;
}
const result = adapted instanceof HTMLCanvasElement ? adapted : out;
ctx.cache.set(sig, result);
if (ctx.tick === 0) {
const persistent = getPersistentStaticCache(node);
persistent.clear();
persistent.set(sig, result);
pruneStaticCache(persistent);
}
ctx.visited.delete(node.id);
return result;
}
function signature(node, tick, outputSlot) {
const parts = [node.id, String(node.comfyClass ?? ""), tick, outputSlot ?? -1];
for (const input of node.inputs ?? []) {
const inp = input;
const linkId = inp?.link ?? null;
parts.push(`in:${inp?.name ?? ""}:${linkId ?? "null"}`);
if (inp?.type === "STRING" && linkId != null) {
const linkData = node?.graph?.links?.[linkId];
if (linkData) {
const upNode = node?.graph?.getNodeById?.(linkData.origin_id);
const upWidget = (upNode?.widgets ?? []).find((w) => typeof w?.value === "string");
if (upWidget) {
const v = String(upWidget.value);
parts.push(v.length > 120 ? `cstr:${v.length}:${hashString(v)}` : `cstr:${v}`);
}
}
}
}
for (const w of node.widgets ?? []) {
const v = w?.value;
if (v == null) continue;
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
if (typeof v === "string" && v.length > 120) {
parts.push(`${w.name}:str:${v.length}:${hashString(v)}`);
} else {
parts.push(`${w.name}:${v}`);
}
}
}
return parts.join("|");
}
return { render };
}
export {
buildRenderer
};