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:
@@ -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);
|
||||
};
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user