add Smart Profile Switch, fix folder picker, README overhaul, v1.0.2

- rename Smart Category Config → Smart Profile Switch 🧩 (code, JS, locales, demo workflow)
- add banner.svg and embed it at the top of README
- replace fragile OpenFileDialog folder-browse hack with native IFileDialog/FOS_PICKFOLDERS picker — fixes wrong-folder bug on single-click select
- flip Smart Resize's upscale_if_smaller default to true — node now resizes to target by default, as the name implies
- add width/height source tooltip to Smart Resolution Multiplier's image input
- fix stale widgets_values in smart_batch_resize_demo.json (10 fields → 13 after prefix/counter fields were added)
- full README rewrite: correct inputs/defaults/outputs for all 5 nodes, new Smart Profile Switch section, Example workflows section, expanded "Why these nodes?"
- add new example workflow (seedvr2_smartresize_demo.json), updated screenshots
- add assets/*.ai to .gitignore
- bump version to 1.0.2
This commit is contained in:
rubz
2026-07-01 12:33:32 +02:00
parent f54122f895
commit c0609cfc85
24 changed files with 1092 additions and 115 deletions
+95 -4
View File
@@ -22,9 +22,6 @@ app.registerExtension({
}
}, { serialize: false });
// Fully exclude from serialization (both save AND load skip it) so it never
// shifts the index of the real widgets. serializeValue=()=>undefined did NOT
// do this — it left a placeholder on save but was skipped on load → drift.
btn.serialize = false;
// Move Browse button to right after input_folder (index 1)
@@ -81,7 +78,7 @@ app.registerExtension({
for (let i = 0; i < n; i++) {
const x = margin + i * (cellW + gap);
const active = current === PRESETS[i];
const active = Number(current) === PRESETS[i];
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
@@ -108,5 +105,99 @@ app.registerExtension({
node.widgets.splice(lsIdx + 1, 0, presetWidget);
}
}
// 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);
};
},
});
+105
View File
@@ -0,0 +1,105 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "Craftopia.SmartProfileSwitch",
async nodeCreated(node) {
if (node.comfyClass !== "SmartProfileSwitch") return;
// Slot header dividers — visual only, not serialized
const dividers = [];
for (let i = 1; i <= 4; i++) {
const div = node.addWidget("button", `_div_${i}`, null, () => {}, { serialize: false });
div.serialize = false;
const num = i;
div.draw = function (ctx, node, widgetWidth, y, height) {
const lx = 14;
const label = `SLOT ${num}`;
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();
};
dividers.push(div);
}
// Status widget — shows active slot after execution
const statusWidget = node.addWidget("button", "_sps_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 SEE RESULT —",
widgetWidth / 2,
y + height / 2
);
ctx.restore();
};
// Reorder: index, [div1, label_1, w_1, h_1], ..., [div4, label_4, w_4, h_4], status
const indexWidget = node.widgets.find(w => w.name === "index");
const slotWidgets = [];
for (let i = 1; i <= 4; i++) {
slotWidgets.push(
dividers[i - 1],
node.widgets.find(w => w.name === `label_${i}`),
node.widgets.find(w => w.name === `width_${i}`),
node.widgets.find(w => w.name === `height_${i}`),
);
}
node.widgets = [indexWidget, ...slotWidgets, statusWidget];
// Serialize override — keeps non-serializable widgets out of widgets_values
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;
};
// onExecuted — update status, hide default customtext textarea
const origOnExecuted = node.onExecuted;
node.onExecuted = function (output) {
origOnExecuted?.call(this, output);
if (output?.text?.[0]) {
statusWidget._text = output.text[0];
for (const w of node.widgets) {
if (w !== statusWidget && w.type === "customtext") {
w.computeSize = () => [0, -4];
}
}
}
node.setDirtyCanvas(true);
};
},
});
+1 -1
View File
@@ -31,7 +31,7 @@ app.registerExtension({
ctx.textBaseline = "middle";
ctx.fillStyle = this._text ? "#f28f41" : "#666";
ctx.fillText(
this._text || "— run to see result —",
this._text || "— RUN TO SEE RESULT —",
widgetWidth / 2,
y + height / 2
);
+14 -1
View File
@@ -46,7 +46,7 @@ app.registerExtension({
for (let i = 0; i < n; i++) {
const x = margin + i * (cellW + gap);
const active = current === PRESETS[i];
const active = Number(current) === PRESETS[i];
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(x, top, cellW, h, 4);
@@ -65,6 +65,19 @@ app.registerExtension({
ctx.restore();
};
// Override serialize to exclude the non-serializable preset widget from
// widgets_values — same drift fix as SmartBatchResize (see that file for details).
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;
};
// Move preset row to right after longest_side
const lsIdx = node.widgets.indexOf(longestSideWidget);
const pwIdx = node.widgets.indexOf(presetWidget);