Fix preview: stable WebGL backing store, re-upload LUT on slider, fill node height, 2048 cache.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+109
-57
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* In-node live preview — WebGL merged-LUT (same reshape as CPU path).
|
||||
* Backing store stays at cache resolution; CSS scales with the node.
|
||||
*/
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
const PREVIEW_CLASS = "MagicMatchPreview";
|
||||
const LUT_SIZE = 25;
|
||||
const MIN_PREVIEW_W = 80;
|
||||
const MIN_PREVIEW_PX = 64;
|
||||
const NODE_CHROME_H = 118;
|
||||
|
||||
function b64ToArrayBuffer(b64) {
|
||||
const bin = atob(b64);
|
||||
@@ -15,7 +17,7 @@ function b64ToArrayBuffer(b64) {
|
||||
return buf.buffer;
|
||||
}
|
||||
|
||||
/** RGBc merged 25³ → cBGR flat (same as reshape_merged_lut_for_apply in lut.py) */
|
||||
/** RGBc merged 25³ → cBGR flat (reshape_merged_lut_for_apply in lut.py) */
|
||||
function reshapeMergedLutForApply(mergedRgb) {
|
||||
const size = LUT_SIZE;
|
||||
const merged = mergedRgb;
|
||||
@@ -34,7 +36,7 @@ function reshapeMergedLutForApply(mergedRgb) {
|
||||
return reshaped;
|
||||
}
|
||||
|
||||
/** cBGR flat → 3D texel (x=B,y=G,z=R) with RGB = output R,G,B at that voxel */
|
||||
/** cBGR flat → 3D texels (OpenGL x=B fastest) */
|
||||
function reshapedLutTo3DUint8(reshaped) {
|
||||
const size = LUT_SIZE;
|
||||
const n = size * size * size;
|
||||
@@ -63,6 +65,7 @@ class LivePreviewPanel {
|
||||
this.node = node;
|
||||
this.cache = null;
|
||||
this._img = null;
|
||||
this._reshapedLut = null;
|
||||
this.gl = null;
|
||||
this.prog = null;
|
||||
this.lutTex = null;
|
||||
@@ -73,6 +76,8 @@ class LivePreviewPanel {
|
||||
this.displayW = 200;
|
||||
this.displayH = 200;
|
||||
this.aspect = 1;
|
||||
this._glBw = 0;
|
||||
this._glBh = 0;
|
||||
this._srcCanvas = document.createElement("canvas");
|
||||
|
||||
this.wrap = document.createElement("div");
|
||||
@@ -85,10 +90,9 @@ class LivePreviewPanel {
|
||||
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.style.cssText =
|
||||
"display:block;width:100%;height:auto;background:#1a1a1a;border-radius:4px;";
|
||||
"display:block;max-width:100%;background:#1a1a1a;border-radius:4px;image-rendering:auto;";
|
||||
}
|
||||
|
||||
/** Reset GL handles after canvas resize (resizing clears the WebGL context). */
|
||||
resetGl() {
|
||||
this.gl = null;
|
||||
this.prog = null;
|
||||
@@ -101,7 +105,10 @@ class LivePreviewPanel {
|
||||
|
||||
initGl() {
|
||||
if (this.gl) return true;
|
||||
const gl = this.canvas.getContext("webgl2", { premultipliedAlpha: false });
|
||||
const gl = this.canvas.getContext("webgl2", {
|
||||
premultipliedAlpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
if (!gl) {
|
||||
this.hint.textContent = "WebGL2 required for live preview";
|
||||
return false;
|
||||
@@ -176,29 +183,66 @@ class LivePreviewPanel {
|
||||
return true;
|
||||
}
|
||||
|
||||
layoutFromNodeWidth(nodeWidth) {
|
||||
const w = Math.max(MIN_PREVIEW_W, Math.floor(nodeWidth - 24));
|
||||
const h = Math.max(MIN_PREVIEW_W, Math.round(w / this.aspect));
|
||||
/** CSS display size — uses node width and height so portrait fills tall nodes. */
|
||||
layoutPreview(nodeWidth, nodeHeight) {
|
||||
const pad = 16;
|
||||
const availW = Math.max(MIN_PREVIEW_PX, Math.floor(nodeWidth - pad));
|
||||
const availH = nodeHeight
|
||||
? Math.max(MIN_PREVIEW_PX, Math.floor(nodeHeight - NODE_CHROME_H))
|
||||
: null;
|
||||
|
||||
let w = availW;
|
||||
let h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect));
|
||||
|
||||
if (availH && h < availH) {
|
||||
h = availH;
|
||||
w = Math.max(MIN_PREVIEW_PX, Math.round(h * this.aspect));
|
||||
if (w > availW) {
|
||||
w = availW;
|
||||
h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect));
|
||||
}
|
||||
}
|
||||
|
||||
this.displayW = w;
|
||||
this.displayH = h;
|
||||
return [w, h + 32];
|
||||
this.canvas.style.width = `${w}px`;
|
||||
this.canvas.style.height = `${h}px`;
|
||||
return [nodeWidth, h + NODE_CHROME_H];
|
||||
}
|
||||
|
||||
/** Apply backing-store size; must run before initGl or call resetGl() afterward. */
|
||||
applyCanvasSize() {
|
||||
/** Backing store = cache pixels × DPR (only changes when cache size changes). */
|
||||
syncGlBacking() {
|
||||
if (!this.cache) return false;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const bw = Math.max(1, Math.round(this.displayW * dpr));
|
||||
const bh = Math.max(1, Math.round(this.displayH * dpr));
|
||||
if (this.canvas.width === bw && this.canvas.height === bh) return false;
|
||||
this.canvas.width = bw;
|
||||
this.canvas.height = bh;
|
||||
const bw = Math.max(1, Math.round(this.cache.w * dpr));
|
||||
const bh = Math.max(1, Math.round(this.cache.h * dpr));
|
||||
if (bw === this._glBw && bh === this._glBh && this.gl) return false;
|
||||
this._glBw = bw;
|
||||
this._glBh = bh;
|
||||
if (this.canvas.width !== bw || this.canvas.height !== bh) {
|
||||
this.canvas.width = bw;
|
||||
this.canvas.height = bh;
|
||||
this.resetGl();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uploadLut(gl, cache) {
|
||||
ensureGlReady() {
|
||||
if (!this.cache) return false;
|
||||
this.syncGlBacking();
|
||||
return this.initGl();
|
||||
}
|
||||
|
||||
prepareLutData(cache) {
|
||||
const floats = new Float32Array(b64ToArrayBuffer(cache.lut));
|
||||
const reshaped = reshapeMergedLutForApply(floats);
|
||||
const texData = reshapedLutTo3DUint8(reshaped);
|
||||
this._reshapedLut = reshapeMergedLutForApply(floats);
|
||||
return reshapedLutTo3DUint8(this._reshapedLut);
|
||||
}
|
||||
|
||||
uploadLut(gl) {
|
||||
if (!this.cache) return;
|
||||
const texData = this._lutTexData || this.prepareLutData(this.cache);
|
||||
this._lutTexData = texData;
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_3D, this.lutTex);
|
||||
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
@@ -247,36 +291,25 @@ class LivePreviewPanel {
|
||||
);
|
||||
}
|
||||
|
||||
ensureGlReady() {
|
||||
if (!this.cache) return false;
|
||||
const sizeChanged = this.applyCanvasSize();
|
||||
if (sizeChanged) this.resetGl();
|
||||
if (!this.initGl()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
refreshLayout() {
|
||||
if (!this.cache || !this.node.size) return;
|
||||
this.layoutFromNodeWidth(this.node.size[0]);
|
||||
if (!this.ensureGlReady()) return;
|
||||
const gl = this.gl;
|
||||
this.uploadLut(gl, this.cache);
|
||||
if (this._img) {
|
||||
this.uploadSource(gl);
|
||||
this.drawPreview(this.getStrength());
|
||||
if (!this.cache) return;
|
||||
const nw = this.node.size?.[0] ?? 320;
|
||||
const nh = this.node.size?.[1];
|
||||
this.layoutPreview(nw, nh);
|
||||
this.drawPreview(this.getStrength());
|
||||
if (this.node.setSize) {
|
||||
this.node.setSize(this.node.computeSize());
|
||||
}
|
||||
}
|
||||
|
||||
setCache(cache) {
|
||||
this.cache = cache;
|
||||
this.aspect = cache.w / cache.h;
|
||||
if (this.node.size) {
|
||||
this.layoutFromNodeWidth(this.node.size[0]);
|
||||
}
|
||||
if (!this.ensureGlReady()) return;
|
||||
|
||||
const gl = this.gl;
|
||||
this.uploadLut(gl, cache);
|
||||
this._lutTexData = null;
|
||||
this._reshapedLut = null;
|
||||
this._glBw = 0;
|
||||
this._glBh = 0;
|
||||
this.resetGl();
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
@@ -293,20 +326,28 @@ class LivePreviewPanel {
|
||||
|
||||
getStrength() {
|
||||
const w = this.node.widgets?.find((x) => x.name === "strength");
|
||||
return w ? Number(w.value) : 1.0;
|
||||
const v = w != null ? w.value : 1.0;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1.0;
|
||||
}
|
||||
|
||||
drawPreview(strength) {
|
||||
if (!this.ensureGlReady()) return;
|
||||
const gl = this.gl;
|
||||
this.uploadLut(gl);
|
||||
if (this._img) this.uploadSource(gl);
|
||||
this.render(strength);
|
||||
}
|
||||
|
||||
render(strength) {
|
||||
if (!this.cache || !this.gl || !this.prog || !this._img) return;
|
||||
if (!this.cache || !this.gl || !this.prog) return;
|
||||
const gl = this.gl;
|
||||
const s = Math.max(0, Math.min(1, Number(strength)));
|
||||
if (!Number.isFinite(s)) return;
|
||||
|
||||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||
gl.clearColor(0.1, 0.1, 0.1, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(this.prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.srcTex);
|
||||
@@ -314,7 +355,7 @@ class LivePreviewPanel {
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_3D, this.lutTex);
|
||||
gl.uniform1i(this.uLut, 1);
|
||||
gl.uniform1f(this.uStrength, Math.max(0, Math.min(1, strength)));
|
||||
gl.uniform1f(this.uStrength, s);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
}
|
||||
@@ -330,6 +371,18 @@ function getPanel(node) {
|
||||
return p;
|
||||
}
|
||||
|
||||
function bindStrengthSlider(node, panel) {
|
||||
const strengthWidget = node.widgets?.find((w) => w.name === "strength");
|
||||
if (!strengthWidget || strengthWidget._magicmatchBound) return;
|
||||
strengthWidget._magicmatchBound = true;
|
||||
|
||||
const prev = strengthWidget.callback;
|
||||
strengthWidget.callback = function (...args) {
|
||||
if (prev) prev.apply(this, args);
|
||||
panel.drawPreview(panel.getStrength());
|
||||
};
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "MAGICMATCH.live_in_node",
|
||||
|
||||
@@ -350,7 +403,7 @@ app.registerExtension({
|
||||
|
||||
domWidget.computeSize = function (width) {
|
||||
if (panel.cache) {
|
||||
return panel.layoutFromNodeWidth(width);
|
||||
return panel.layoutPreview(width, panel.node.size?.[1]);
|
||||
}
|
||||
return [width, 200];
|
||||
};
|
||||
@@ -358,18 +411,14 @@ app.registerExtension({
|
||||
const origOnResize = node.onResize;
|
||||
node.onResize = function (size) {
|
||||
const res = origOnResize?.apply(this, arguments);
|
||||
panel.refreshLayout();
|
||||
if (panel.cache) {
|
||||
panel.layoutPreview(size[0], size[1]);
|
||||
panel.drawPreview(panel.getStrength());
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const strengthWidget = node.widgets?.find((w) => w.name === "strength");
|
||||
if (strengthWidget) {
|
||||
const prev = strengthWidget.callback;
|
||||
strengthWidget.callback = function (v, ...rest) {
|
||||
if (prev) prev.call(this, v, ...rest);
|
||||
panel.drawPreview(Number(v));
|
||||
};
|
||||
}
|
||||
bindStrengthSlider(node, panel);
|
||||
},
|
||||
|
||||
async setup() {
|
||||
@@ -383,7 +432,10 @@ app.registerExtension({
|
||||
app.graph._nodes_by_id?.[nodeId];
|
||||
if (!graphNode || graphNode.comfyClass !== PREVIEW_CLASS) return;
|
||||
|
||||
getPanel(graphNode).setCache(out.magicmatch_live[0]);
|
||||
const panel = getPanel(graphNode);
|
||||
bindStrengthSlider(graphNode, panel);
|
||||
panel.setCache(out.magicmatch_live[0]);
|
||||
panel.drawPreview(panel.getStrength());
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import io
|
||||
|
||||
import numpy as np
|
||||
|
||||
LIVE_PREVIEW_MAX_EDGE = 1024
|
||||
LIVE_PREVIEW_MAX_EDGE = 2048
|
||||
|
||||
|
||||
def _downscale_hwc(hwc: np.ndarray, max_edge: int) -> np.ndarray:
|
||||
|
||||
Reference in New Issue
Block a user