111 lines
3.9 KiB
JavaScript
111 lines
3.9 KiB
JavaScript
// Live resolution preview for the Resolution Selector node.
|
||
// Mirrors compute_dimensions() in resolution_selector.py - keep in sync.
|
||
import { app } from "../../../scripts/app.js";
|
||
|
||
const NODE_NAME = "ResolutionSelectorMP";
|
||
|
||
console.log("[ResolutionSelector] preview extension loaded");
|
||
|
||
function computeDimensions(megapixels, ratio, multiple) {
|
||
multiple = Math.max(1, Math.floor(multiple || 1));
|
||
const target = Math.max(megapixels || 0.01, 0.01) * 1_000_000;
|
||
ratio = Math.max(ratio || 1, 1e-6);
|
||
|
||
const idealH = Math.sqrt(target / ratio);
|
||
const idealW = idealH * ratio;
|
||
|
||
let best = null;
|
||
for (const sw of [Math.floor(idealW / multiple), Math.ceil(idealW / multiple)]) {
|
||
for (const sh of [Math.floor(idealH / multiple), Math.ceil(idealH / multiple)]) {
|
||
const w = Math.max(multiple, sw * multiple);
|
||
const h = Math.max(multiple, sh * multiple);
|
||
const pxErr = Math.abs(w * h - target) / target;
|
||
const ratioErr = Math.abs(Math.log((w / h) / ratio));
|
||
const score = pxErr + ratioErr;
|
||
if (!best || score < best.score) best = { score, w, h };
|
||
}
|
||
}
|
||
return [best.w, best.h];
|
||
}
|
||
|
||
function updatePreview(node) {
|
||
const widgets = node.widgets;
|
||
if (!widgets) return;
|
||
const val = (n) => widgets.find((w) => w.name === n)?.value;
|
||
const preview = widgets.find((w) => w.name === "resolution");
|
||
if (!preview) return;
|
||
|
||
const imageLinked = node.inputs?.some(
|
||
(i) => i.name === "image" && i.link != null
|
||
);
|
||
|
||
let text;
|
||
if (val("use_image") && val("use_image_ratio") && imageLinked) {
|
||
text = "runtime (image ratio)";
|
||
} else {
|
||
const aw = Math.max(1, val("aspect_width") || 1);
|
||
const ah = Math.max(1, val("aspect_height") || 1);
|
||
const [w, h] = computeDimensions(
|
||
val("megapixels"), aw / ah, val("multiple")
|
||
);
|
||
text = `${w} × ${h} (${((w * h) / 1_000_000).toFixed(2)} MP)`;
|
||
}
|
||
|
||
if (preview.value !== text) {
|
||
preview.value = text;
|
||
node.setDirtyCanvas?.(true, false);
|
||
}
|
||
}
|
||
|
||
app.registerExtension({
|
||
name: "resolutionSelector.livePreview",
|
||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||
if (nodeData.name !== NODE_NAME) return;
|
||
|
||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||
nodeType.prototype.onNodeCreated = function () {
|
||
const r = onNodeCreated?.apply(this, arguments);
|
||
|
||
if (!this.widgets?.some((w) => w.name === "resolution")) {
|
||
const w = this.addWidget(
|
||
"text", "resolution", "", () => {}, { serialize: false }
|
||
);
|
||
w.disabled = true;
|
||
}
|
||
|
||
// Recompute whenever any input widget changes.
|
||
const self = this;
|
||
for (const widget of this.widgets) {
|
||
if (widget.name === "resolution") continue;
|
||
const cb = widget.callback;
|
||
widget.callback = function (...args) {
|
||
const res = cb?.apply(this, args);
|
||
updatePreview(self);
|
||
return res;
|
||
};
|
||
}
|
||
|
||
updatePreview(this);
|
||
return r;
|
||
};
|
||
|
||
for (const hook of ["onConfigure", "onConnectionsChange"]) {
|
||
const orig = nodeType.prototype[hook];
|
||
nodeType.prototype[hook] = function (...args) {
|
||
const r = orig?.apply(this, args);
|
||
updatePreview(this);
|
||
return r;
|
||
};
|
||
}
|
||
|
||
// Safety net: keeps the readout current even if a change slips
|
||
// past the callbacks (runs on every canvas redraw, cheap).
|
||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||
nodeType.prototype.onDrawForeground = function (...args) {
|
||
const r = onDrawForeground?.apply(this, args);
|
||
updatePreview(this);
|
||
return r;
|
||
};
|
||
},
|
||
});
|