Files

175 lines
5.4 KiB
JavaScript

import { app } from "../../scripts/app.js";
const RATIOS = {
"1:1": [1, 1],
"2:3": [2, 3],
"3:2": [3, 2],
"3:4": [3, 4],
"4:3": [4, 3],
"4:5": [4, 5],
"5:4": [5, 4],
"9:16": [9, 16],
"16:9": [16, 9],
"21:9": [21, 9],
};
function widget(node, name) {
return node.widgets?.find((w) => w.name === name);
}
function numberValue(node, name, fallback) {
const value = Number(widget(node, name)?.value);
return Number.isFinite(value) ? value : fallback;
}
function currentWidgetRatio(node) {
const width = Math.max(1, numberValue(node, "width", 1024));
const height = Math.max(1, numberValue(node, "height", 1024));
return width / height;
}
function linkedPreviewUrl(node) {
const input = node.inputs?.find((i) => i.name === "image");
if (!input?.link || !app.graph?.links) return null;
const link = app.graph.links[input.link];
const originId = Array.isArray(link) ? link[1] : link?.origin_id;
if (originId == null) return null;
const origin = app.graph.getNodeById ? app.graph.getNodeById(originId) : app.graph._nodes_by_id?.[originId];
if (!origin || origin.type !== "LoadImage") return null;
const imageWidget = origin.widgets?.find((w) => w.name === "image");
let filename = imageWidget?.value || origin.widgets_values?.[0];
if (!filename || typeof filename !== "string") return null;
let type = "input";
const match = filename.match(/\s+\[(input|output|temp)\]$/);
if (match) {
type = match[1];
filename = filename.replace(/\s+\[(input|output|temp)\]$/, "");
}
return `/view?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}&subfolder=`;
}
function loadImageSize(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight });
image.onerror = reject;
image.src = url;
});
}
async function refreshSourceImageRatio(node, changedName = "width") {
if (widget(node, "aspect_ratio")?.value !== "from image") return false;
const url = linkedPreviewUrl(node);
if (!url || url === node.__smartAspectSourceUrl) return false;
node.__smartAspectSourceUrl = url;
try {
const size = await loadImageSize(url);
if (!size.width || !size.height) return false;
node.__smartAspectSourceWidth = size.width;
node.__smartAspectSourceHeight = size.height;
node.__smartAspectBaseRatio = size.width / size.height;
updateSize(node, changedName);
return true;
} catch (error) {
console.warn("SmartAspectResize: unable to read linked image size", error);
return false;
}
}
function snap(value, snapTo) {
const size = Math.max(1, Math.round(value));
const multiple = Math.max(1, Math.round(snapTo || 1));
if (multiple <= 1) return size;
return Math.max(multiple, Math.round(size / multiple) * multiple);
}
function ratioForNode(node) {
const ratioName = widget(node, "aspect_ratio")?.value || "1:1";
if (ratioName === "custom") {
return Math.max(1, numberValue(node, "custom_aspect_width", 1)) /
Math.max(1, numberValue(node, "custom_aspect_height", 1));
}
if (ratioName === "from image") {
if (!Number.isFinite(node.__smartAspectBaseRatio) || node.__smartAspectBaseRatio <= 0) {
node.__smartAspectBaseRatio = node.__smartAspectSourceWidth && node.__smartAspectSourceHeight
? node.__smartAspectSourceWidth / node.__smartAspectSourceHeight
: currentWidgetRatio(node);
}
return node.__smartAspectBaseRatio;
}
const pair = RATIOS[ratioName] || RATIOS["1:1"];
return pair[0] / pair[1];
}
function setWidgetValue(node, name, value) {
const w = widget(node, name);
if (!w) return;
w.value = value;
}
function updateSize(node, changedName) {
if (node.__smartAspectUpdating) return;
node.__smartAspectUpdating = true;
try {
if (changedName === "aspect_ratio" && widget(node, "aspect_ratio")?.value === "from image") {
refreshSourceImageRatio(node, "width");
}
const locked = widget(node, "locked")?.value || "aspect_ratio";
const ratio = ratioForNode(node);
const snapTo = numberValue(node, "snap_to", 8);
const width = Math.max(1, numberValue(node, "width", 1024));
const height = Math.max(1, numberValue(node, "height", 1024));
if (locked === "width") {
setWidgetValue(node, "height", snap(width / ratio, snapTo));
} else if (locked === "height") {
setWidgetValue(node, "width", snap(height * ratio, snapTo));
} else if (changedName === "height") {
setWidgetValue(node, "width", snap(height * ratio, snapTo));
} else {
setWidgetValue(node, "height", snap(width / ratio, snapTo));
}
app.canvas?.setDirty?.(true, true);
} finally {
node.__smartAspectUpdating = false;
}
}
app.registerExtension({
name: "Comfy.SmartAspectResize",
async nodeCreated(node) {
if (node.comfyClass !== "SmartAspectResize") return;
node.__smartAspectBaseRatio = currentWidgetRatio(node);
for (const name of ["aspect_ratio", "width", "height", "locked", "custom_aspect_width", "custom_aspect_height", "snap_to"]) {
const w = widget(node, name);
if (!w) continue;
const original = w.callback;
w.callback = function(value) {
original?.call(this, value);
if (name === "aspect_ratio" || name === "width" || name === "height") {
refreshSourceImageRatio(node, name);
}
updateSize(node, name);
};
}
refreshSourceImageRatio(node, "width");
updateSize(node, "width");
},
});