Files
CraftopiaStudio-ComfyUI-Cra…/js/smart_batch_resize.js
T
rubz 25f80d8e33 fix: reorganize Smart Batch Resize into labeled sections; bump to 1.1.0
- Add section-header dividers (Resize/Filename/Output Location/Output
  Format/Options) to js/smart_batch_resize.js — the node grew to ~19
  widgets after the review fixes and needed grouping to stay scannable
- Move `delimiter` next to the other filename-building fields, and
  reorder Output Location/Output Format/Options into a more logical
  flow. This changes the node's widget order — workflows saved before
  this change will need the node re-placed once (values shift on load)
- Update README: new screenshot, full input table rewritten to match
  the new field order and the alpha_mode/output_format/quality/
  preview_limit/upscale_if_smaller additions, "Why these nodes?" bullet
  on the transparency handling
2026-07-28 00:28:12 +02:00

239 lines
10 KiB
JavaScript

import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Craftopia.SmartBatchResize",
async nodeCreated(node) {
if (node.comfyClass !== "SmartBatchResize") return;
const folderWidget = node.widgets?.find(w => w.name === "input_folder");
if (!folderWidget) return;
const btn = node.addWidget("button", "📁 Browse folder", null, async () => {
try {
const res = await fetch("/craftkit/browse_folder", { method: "POST" });
const data = await res.json();
if (data.ok && data.path) {
folderWidget.value = data.path;
node.setDirtyCanvas(true);
}
} catch (e) {
console.error("[SmartBatchResize] Browse failed:", e);
}
}, { serialize: false });
btn.serialize = false;
// Move Browse button to right after input_folder (index 1)
const folderIdx = node.widgets.indexOf(folderWidget);
const btnIdx = node.widgets.indexOf(btn);
if (btnIdx !== folderIdx + 1) {
node.widgets.splice(btnIdx, 1);
node.widgets.splice(folderIdx + 1, 0, btn);
}
// Compact preset chips for longest_side. Start from a REAL button widget
// (so all of ComfyUI's layout/computeSize/sizing integration is correct)
// and only override its draw + mouse. A bare custom widget with our own
// computeSize forced the node width and broke the layout on click.
const longestSideWidget = node.widgets?.find(w => w.name === "longest_side");
if (longestSideWidget) {
const PRESETS = [512, 768, 1024, 1536];
// Per-chip click handled in the button callback (buttons route clicks
// to their callback, not to .mouse). We read the pointer position from
// the canvas at click time and match it against the drawn chip rects.
const presetWidget = node.addWidget("button", "size_presets", null, (value, canvas, node) => {
const gm = (canvas || app.canvas)?.graph_mouse;
if (!gm) return;
const lx = gm[0] - node.pos[0];
const ly = gm[1] - node.pos[1];
for (const r of presetWidget._rects) {
if (lx >= r.x1 && lx <= r.x2 && ly >= r.y1 && ly <= r.y2) {
longestSideWidget.value = r.size;
longestSideWidget.callback?.(r.size, app.canvas, node);
node.setDirtyCanvas(true);
return;
}
}
}, { serialize: false });
presetWidget.serialize = false;
presetWidget._rects = [];
presetWidget.draw = function (ctx, node, widgetWidth, y, height) {
const margin = 14;
const gap = 5;
const innerW = Math.max(40, widgetWidth - margin * 2);
const n = PRESETS.length;
const cellW = Math.max(10, (innerW - gap * (n - 1)) / n);
const h = Math.min(height - 2, 18);
const top = y + (height - h) / 2;
const current = longestSideWidget.value;
this._rects = [];
ctx.save();
ctx.font = "11px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let i = 0; i < n; i++) {
const x = margin + i * (cellW + gap);
const active = Number(current) === PRESETS[i];
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
else ctx.rect(x, top, cellW, h);
ctx.fillStyle = active ? "#f28f41" : "#2a2a2a";
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = active ? "#f28f41" : "#555";
ctx.stroke();
ctx.fillStyle = active ? "#111" : "#ccc";
ctx.fillText(String(PRESETS[i]), x + cellW / 2, top + h / 2 + 0.5);
this._rects.push({ x1: x, x2: x + cellW, y1: top, y2: top + h, size: PRESETS[i] });
}
ctx.restore();
};
// Move preset row to right after longest_side
const lsIdx = node.widgets.indexOf(longestSideWidget);
const pwIdx = node.widgets.indexOf(presetWidget);
if (pwIdx !== lsIdx + 1) {
node.widgets.splice(pwIdx, 1);
node.widgets.splice(lsIdx + 1, 0, presetWidget);
}
}
// Section-header dividers — visual only, not serialized. Standalone node
// with a lot of inputs, so grouping them keeps it scannable at a glance.
const addSectionDivider = (beforeWidgetName, label) => {
const target = node.widgets.find(w => w.name === beforeWidgetName);
if (!target) return;
const div = node.addWidget("button", `_div_${label}`, null, () => {}, { serialize: false });
div.serialize = false;
div.draw = function (ctx, node, widgetWidth, y, height) {
const lx = 14;
ctx.save();
ctx.font = "bold 10px sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillStyle = "#888";
ctx.fillText(label, lx, y + height / 2);
const lineX = lx + ctx.measureText(label).width + 8;
ctx.beginPath();
ctx.moveTo(lineX, y + height / 2);
ctx.lineTo(widgetWidth - 14, y + height / 2);
ctx.strokeStyle = "#333";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
};
const targetIdx = node.widgets.indexOf(target);
const divIdx = node.widgets.indexOf(div);
node.widgets.splice(divIdx, 1);
node.widgets.splice(targetIdx, 0, div);
};
addSectionDivider("longest_side", "RESIZE");
addSectionDivider("prefix", "FILENAME");
addSectionDivider("folder_resolution", "OUTPUT LOCATION");
addSectionDivider("skip_if_exists", "OPTIONS");
addSectionDivider("output_format", "OUTPUT FORMAT");
// Override serialize so non-serializable JS widgets (Browse, presets, status,
// run batch) are excluded from widgets_values in the saved workflow JSON.
// Without this, LiteGraph saves null slots for these widgets which then shift
// all Python widget values on load (configure runs before nodeCreated).
// Invariant: Python widgets must stay in their original relative order in
// this.widgets — JS widgets may be spliced in between, but never reorder
// the Python ones, or the positional value assignment on load will silently
// misalign.
const origSerialize = node.serialize;
node.serialize = function () {
const data = origSerialize.call(this);
if (data.widgets_values) {
data.widgets_values = this.widgets
.filter(w => w.serialize !== false && w.options?.serialize !== false)
.map(w => w.value);
}
return data;
};
// Canvas-drawn status display
const statusWidget = node.addWidget("button", "_sbr_status", null, () => {}, { serialize: false });
statusWidget.serialize = false;
statusWidget._text = "";
statusWidget.draw = function (ctx, node, widgetWidth, y, height) {
const margin = 14;
const innerW = widgetWidth - margin * 2;
ctx.save();
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(margin, y + 2, innerW, height - 4, 4);
else ctx.rect(margin, y + 2, innerW, height - 4);
ctx.fillStyle = "#111";
ctx.fill();
ctx.strokeStyle = this._text ? "#3a3a3a" : "#222";
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = "11px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = this._text ? "#f28f41" : "#666";
ctx.fillText(this._text || "— RUN TO PROCESS —", widgetWidth / 2, y + height / 2);
ctx.restore();
};
// Run Batch button
const runBtn = node.addWidget("button", "▶ Run Batch", null, () => {
app.queuePrompt(0, 1).catch(e => console.error("[SmartBatchResize] Queue failed:", e));
}, { serialize: false });
runBtn.serialize = false;
// Show/hide counter_start based on use_counter toggle
const useCounterWidget = node.widgets?.find(w => w.name === "use_counter");
const counterStartWidget = node.widgets?.find(w => w.name === "counter_start");
if (useCounterWidget && counterStartWidget) {
const defaultComputeSize = counterStartWidget.computeSize;
const updateCounterVisibility = () => {
counterStartWidget.computeSize = useCounterWidget.value
? defaultComputeSize
: () => [0, -4];
node.setDirtyCanvas(true);
};
const origCallback = useCounterWidget.callback;
useCounterWidget.callback = function (...args) {
origCallback?.call(this, ...args);
updateCounterVisibility();
};
const origOnConfigure = node.onConfigure;
node.onConfigure = function (...args) {
origOnConfigure?.call(this, ...args);
updateCounterVisibility();
};
updateCounterVisibility();
}
// Hide any customtext widget already present (e.g. from a previously run workflow)
const hideCustomText = () => {
for (const w of node.widgets) {
if (w.type === "customtext") w.computeSize = () => [0, -4];
}
};
hideCustomText();
// Intercept onExecuted to update status and hide the default customtext output
const origOnExecuted = node.onExecuted;
node.onExecuted = function (output) {
origOnExecuted?.call(this, output);
if (output?.text?.[0]) {
statusWidget._text = output.text[0];
hideCustomText();
}
node.setDirtyCanvas(true);
};
},
});