Files
ComfyUI_SuperScaler/js/superscaler.js
2025-11-05 16:51:37 +01:00

232 lines
9.8 KiB
JavaScript

import { app } from "/scripts/app.js";
// --- Définition des groupes ---
const widgetGroups = {
"enable_latent_pass": [
"latent_upscale_by", "latent_denoise", "latent_sampler_name",
"latent_scheduler", "latent_steps", "latent_cfg"
],
"enable_tiled_pass_2": [
"tiled_upscale_by_2", "tiled_denoise_2", "tile_size_2", "tile_overlap_2",
"tiled_sampler_name_2", "tiled_scheduler_2", "tiled_steps_2", "tiled_cfg_2"
],
"enable_tiled_pass_3": [
"tiled_upscale_by_3", "tiled_denoise_3", "tile_size_3", "tile_overlap_3",
"tiled_sampler_name_3", "tiled_scheduler_3", "tiled_steps_3", "tiled_cfg_3"
],
"enable_sharpen": [
"sharpen_amount", "sharpen_radius"
],
"enable_grain": [
"grain_intensity", "grain_type", "grain_size", "saturation_mix", "adaptive_grain"
]
};
// Liste de TOUS les widgets gérés (parents et enfants)
const allManagedWidgetNames = new Set();
for (const parentName in widgetGroups) {
allManagedWidgetNames.add(parentName);
for (const childName of widgetGroups[parentName]) {
allManagedWidgetNames.add(childName);
}
}
function getCleanTitle(toggleName) {
let title = toggleName.replace("enable_", "").replace(/_/g, " ");
return title.replace(/\b\w/g, l => l.toUpperCase());
}
/**
* Construit l'interface utilisateur (les widgets) en lisant l'état depuis node.properties.
* @param {LGraphNode} node - Le nœud ComfyUI
*/
function rebuildWidgets(node) {
// 1. On efface tous les widgets actuels
node.widgets.length = 0;
// 2. On reconstruit la liste en lisant nos propriétés
for (const toggleName of Object.keys(widgetGroups)) {
const parentWidget = node.allPythonWidgets.find(w => w.name === toggleName);
if (!parentWidget) continue;
// Applique la valeur depuis notre "source de vérité"
// (Vérifie si la propriété existe, sinon utilise la valeur par défaut du widget)
parentWidget.value = node.properties.hasOwnProperty(toggleName)
? node.properties[toggleName]
: parentWidget.defaultValue;
// --- AJOUT DU SÉPARATEUR ---
node.widgets.push({
name: "separator_spacer_" + parentWidget.name,
type: "CUSTOM_SPACER",
draw: (ctx, node, width, y) => {
const title = getCleanTitle(parentWidget.name);
const rectHeight = 20, marginY = 5, x_padding = 10;
ctx.fillStyle = "#272";
ctx.fillRect(x_padding, y + marginY, width - (x_padding * 2), rectHeight);
ctx.fillStyle = "#CCC";
ctx.font = "bold 12px Arial";
ctx.textAlign = "center";
const textY = y + marginY + rectHeight / 2 + 4;
ctx.fillText(title, width / 2, textY);
},
computeSize: () => [0, 30]
});
// A. On ajoute le "parent" (toggle)
node.widgets.push(parentWidget);
// B. On vérifie (en lisant nos propriétés) s'il faut ajouter ses enfants
const showChildren = parentWidget.value;
if (showChildren) {
const childrenNames = widgetGroups[toggleName];
for (const childName of childrenNames) {
const childWidget = node.allPythonWidgets.find(w => w.name === childName);
if (childWidget) {
// Applique la valeur depuis notre "source de vérité"
childWidget.value = node.properties.hasOwnProperty(childName)
? node.properties[childName]
: childWidget.defaultValue;
node.widgets.push(childWidget);
} else {
console.warn(`[SuperScaler] Enfant widget '${childName}' NON TROUVÉ.`);
}
}
}
}
// --- AJOUT : AFFICHER LES WIDGETS NON GÉRÉS (SEED, BLEND, ETC.) ---
// 1. Trouver tous les widgets non gérés
const unmanagedWidgets = [];
for (const widget of node.allPythonWidgets) {
if (!allManagedWidgetNames.has(widget.name)) {
unmanagedWidgets.push(widget);
}
}
// 2. S'il y en a, ajouter un titre et les widgets
if (unmanagedWidgets.length > 0) {
// Ajouter un spacer "Final Settings"
node.widgets.push({
name: "separator_spacer_final_settings",
type: "CUSTOM_SPACER",
draw: (ctx, node, width, y) => {
const title = "Final Settings"; // Nouveau titre
const rectHeight = 20, marginY = 5, x_padding = 10;
ctx.fillStyle = "#272";
ctx.fillRect(x_padding, y + marginY, width - (x_padding * 2), rectHeight);
ctx.fillStyle = "#CCC";
ctx.font = "bold 12px Arial";
ctx.textAlign = "center";
const textY = y + marginY + rectHeight / 2 + 4;
ctx.fillText(title, width / 2, textY);
},
computeSize: () => [0, 30]
});
// 3. Ajouter tous les widgets non gérés (seed, compagnon, et mask_blend_weight)
for (const widget of unmanagedWidgets) {
// Applique la valeur depuis notre "source de vérité"
if (node.properties.hasOwnProperty(widget.name)) {
widget.value = node.properties[widget.name];
}
node.widgets.push(widget);
}
}
// --- FIN DE L'AJOUT ---
// 3. Force le redessinage
const newComputedSize = node.computeSize();
node.size[1] = newComputedSize[1];
if (app.graph) {
app.graph.setDirtyCanvas(true, true);
}
}
app.registerExtension({
name: "SuperScaler.DynamicWidgets.vSafe",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SuperScaler_Pipeline") {
// --- C'est ici que la logique de ton exemple 'Orchestrator' est appliquée ---
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
const node = this;
// 1. Initialise la "source de vérité" des propriétés (comme Orchestrator)
if (!node.properties) {
node.properties = {};
}
// 2. Sauvegarde les widgets "templates" (avec leurs valeurs par défaut)
node.allPythonWidgets = [...node.widgets];
// 3. Remplit la "source de vérité" avec les valeurs par défaut (par NOM)
// UNIQUEMENT si elles n'existent pas déjà (chargées par le workflow)
for (const widget of node.allPythonWidgets) {
if (widget.name && !node.properties.hasOwnProperty(widget.name)) {
node.properties[widget.name] = widget.value;
}
// Stocke la valeur par défaut pour la réinitialisation
widget.defaultValue = widget.value;
}
// 4. Attache les callbacks (une seule fois)
// Le callback met à jour 'properties' et reconstruit l'UI
if (!node.callbacksAttached) {
for (const widget of node.allPythonWidgets) {
if (widget.callback) {
const originalCallback = widget.callback;
widget.callback = (value, ...args) => {
// Exécute le callback original (s'il existe)
originalCallback?.call(widget, value, ...args);
// Met à jour la "source de vérité"
node.properties[widget.name] = value;
// Reconstruit l'interface
rebuildWidgets(node);
};
}
}
node.callbacksAttached = true; // On pose le drapeau
}
// Lance la construction initiale de l'UI
// (utilise un court délai, comme ton 'Orchestrator', pour s'assurer que tout est chargé)
setTimeout(() => rebuildWidgets(node), 10);
};
// Garde une trace de la fonction 'onConfigure' originale
// Nous l'utilisons SEULEMENT pour fusionner les valeurs chargées
const originalOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(values) {
originalOnConfigure?.apply(this, arguments);
// Quand ComfyUI charge un workflow, il met les valeurs dans this.properties
// (remplaçant 'values' qui est déprécié)
// Nous devons juste nous assurer que notre 'rebuild' est appelé après.
if (this.properties) {
// 'values' (l'argument) est souvent l'ancien 'properties'
Object.assign(this.properties, values);
}
// Force une reconstruction de l'UI au cas où
if (this.allPythonWidgets) {
rebuildWidgets(this);
}
};
}
}
});