175 lines
5.4 KiB
JavaScript
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");
|
|
},
|
|
});
|