add retag auto when fixe node recreate

This commit is contained in:
tritant
2025-10-12 15:44:37 +02:00
committed by GitHub
parent c2eec1f88d
commit e9929e514f
+304 -109
View File
@@ -1,6 +1,90 @@
import { app } from "/scripts/app.js";
function isNodeSubgraph(node) {
if (!node.type || typeof node.type !== 'string') return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(node.type);
}
function getNodeDefaultTitle(node) {
const nodeType = LiteGraph.getNodeType(node.type);
if (nodeType && nodeType.title) {
return nodeType.title;
}
return node.type;
}
function saveTitlesToLocalStorage(workflowId) {
//console.log(`[Orchestrator] SAVE: Lancement de la sauvegarde pour workflow '${workflowId}'.`);
if (!workflowId) return;
const allNodes = getAllNodesInGraph();
const backup = {
taggedProfiles: {},
allNodeIds: allNodes.map(n => n.id)
};
for (const node of allNodes) {
if (node.title && node.title.includes('[[')) {
backup.taggedProfiles[node.id] = {
title: node.title,
type: node.type
};
}
}
const key = `orchestrator_titles_${workflowId}`;
localStorage.setItem(key, JSON.stringify(backup));
//console.log("[Orchestrator] SAVE: Sauvegarde terminée avec les données :", backup);
}
function restoreTitlesFromLocalStorage(workflowId) {
//console.log(`[Orchestrator] RESTORE: Lancement pour workflow '${workflowId}'.`);
if (!workflowId) return false;
const key = `orchestrator_titles_${workflowId}`;
const savedBackupJSON = localStorage.getItem(key);
if (!savedBackupJSON) {
//console.log("[Orchestrator] RESTORE: Aucune sauvegarde trouvée.");
return false;
}
let backup = JSON.parse(savedBackupJSON);
let hasChanged = false;
const graphNodes = getAllNodesInGraph();
const graphNodeIds = new Set(graphNodes.map(n => n.id));
const backupAllNodeIds = new Set(backup.allNodeIds || []);
const backupTaggedProfiles = backup.taggedProfiles || {};
const orphans = Array.from(backupAllNodeIds).filter(id => !graphNodeIds.has(id));
const newcomers = Array.from(graphNodeIds).filter(id => !backupAllNodeIds.has(id));
if (orphans.length === 1 && newcomers.length === 1) {
const orphanId = orphans[0];
const newcomerId = newcomers[0];
const orphanProfile = backupTaggedProfiles[orphanId];
const newcomerNode = graphNodes.find(n => n.id === newcomerId);
if (orphanProfile && newcomerNode && orphanProfile.type === newcomerNode.type) {
//console.log(`%c[Orchestrator] RECREATE DÉTECTÉ ! L'orphelin #${orphanId} devient #${newcomerId}. RESTAURATION du titre.`, 'color: orange; font-weight: bold;');
newcomerNode.title = orphanProfile.title;
hasChanged = true;
backup.taggedProfiles[newcomerId] = orphanProfile;
delete backup.taggedProfiles[orphanId];
const newAllNodeIds = Array.from(backupAllNodeIds).filter(id => id != orphanId);
newAllNodeIds.push(newcomerId);
backup.allNodeIds = newAllNodeIds;
localStorage.setItem(key, JSON.stringify(backup));
}
}
//console.log(`[Orchestrator] RESTORE: Terminé. Un changement a-t-il eu lieu ? ${hasChanged}`);
return hasChanged;
}
function getAllNodesInGraph() {
if (!app.graph) return [];
const allNodes = [...app.graph._nodes];
if (app.graph._subgraphs) {
for (const subgraph of app.graph._subgraphs.values()) {
@@ -10,11 +94,59 @@ function getAllNodesInGraph() {
return allNodes;
}
function recreateNodeSafely(node, parentGraph) {
try {
const graph = parentGraph || app.graph;
const nodeType = node.type;
const pos = Array.isArray(node.pos) ? [...node.pos] : [0, 0];
const title = node.title || node.type;
const properties = structuredClone(node.properties || {});
const widgets_values = structuredClone(node.widgets_values || []);
const flags = structuredClone(node.flags || {});
const inputs = node.inputs ? structuredClone(node.inputs) : undefined;
const outputs = node.outputs ? structuredClone(node.outputs) : undefined;
//console.log(`[Orchestrator] RECREATE DÉTECTÉ ! L'orphelin #${node.id} → recréation "${title}"`);
try { graph.remove(node); } catch(e) { /* ignore */ }
const newNode = LiteGraph.createNode(nodeType);
if (!newNode) {
console.warn(`[Orchestrator] Impossible de recréer le node de type "${nodeType}"`);
return null;
}
newNode.pos = pos;
if (title) newNode.title = title;
try { newNode.properties = Object.assign({}, newNode.properties || {}, properties); } catch(e){}
try { newNode.widgets_values = widgets_values; } catch(e){}
try { newNode.flags = flags; } catch(e){}
if (inputs) newNode.inputs = inputs;
if (outputs) newNode.outputs = outputs;
graph.add(newNode);
try {
if (app.graph._subgraphs) {
app.graph._subgraphs = new Map([...app.graph._subgraphs.entries()].filter(([k, s]) => s && s.nodes));
}
} catch (e) { /* ignore */ }
try { app.graph.setDirtyCanvas(true, true); } catch(e){}
try { if (app.canvas) app.canvas.setDirty(true, true); } catch(e){}
//console.log(`[Orchestrator] ✅ Node "${title}" recréé proprement (nouvel ID=${newNode.id})`);
return newNode;
} catch (e) {
console.error("[Orchestrator] Erreur dans recreateNodeSafely:", e);
return null;
}
}
function updateNodeStates(selection, groupID) {
const changesToApply = [];
const dirtyGraphs = new Set([app.graph]);
for (const targetNode of getAllNodesInGraph()) {
// Modifié pour le nom du noeud Muter
if (targetNode.type === "OrchestratorNodeMuter") continue;
const title = targetNode.title || "";
const match = title.match(/\[\[(.*?):(.*?)\]\]/g);
@@ -25,13 +157,11 @@ function updateNodeStates(selection, groupID) {
const nodeTag = parts[1];
if (nodeGroupID === groupID) {
let shouldBeActive = false;
if (Array.isArray(selection)) {
shouldBeActive = selection.includes(nodeTag);
} else {
shouldBeActive = selection && nodeTag === selection;
}
// Modifié pour utiliser le mode Mute (2) au lieu de Bypass (4)
const newMode = shouldBeActive ? 0 : 2;
if (Array.isArray(selection)) { shouldBeActive = selection.includes(nodeTag); }
else { shouldBeActive = selection && nodeTag === selection; }
const newMode = shouldBeActive ? 0 : 2;
if (targetNode.mode !== newMode) {
changesToApply.push({ node: targetNode, mode: newMode });
if (targetNode.graph) dirtyGraphs.add(targetNode.graph);
@@ -42,133 +172,198 @@ function updateNodeStates(selection, groupID) {
}
}
if (changesToApply.length > 0) {
for (const change of changesToApply) {
change.node.mode = change.mode;
}
for (const graph of dirtyGraphs) {
graph.setDirtyCanvas(true, true);
}
for (const change of changesToApply) { change.node.mode = change.mode; }
for (const graph of dirtyGraphs) { graph.setDirtyCanvas(true, true); }
}
}
app.registerExtension({
// Modifié pour le nom de l'extension Muter
name: "Comfy.OrchestratorNodeMuter.Final.Flexible",
name: "Comfy.OrchestratorNodeMuter.Final.Flexible",
beforeRegisterNodeDef(nodeType, nodeData, app) {
// Modifié pour le nom du noeud Muter
if (nodeData.name === "OrchestratorNodeMuter") {
if (nodeData.name === "OrchestratorNodeMuter") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
//console.log(`[Orchestrator] onNodeCreated: Création du nœud ID ${this.id}`);
this.lastKnownNodeTitles = null;
this.resyncDebounce = null;
this.isFixingDuplicate = false;
// On utilise this.properties pour un état persistant
if (!this.properties) this.properties = {};
if (this.properties.workflow_id === undefined) this.properties.workflow_id = "default_workflow";
if (this.properties.show_settings === undefined) this.properties.show_settings = true;
if (this.properties.group_id === undefined) this.properties.group_id = "DEFAULT";
if (this.properties.node_mode === undefined) this.properties.node_mode = true;
const discoverAndBuildUI = () => {
//console.log("[Orchestrator] discoverAndBuildUI: Lancement...");
const groupID = this.properties.group_id;
const isExclusiveMode = this.properties.node_mode;
const settingsVisible = this.properties.show_settings;
const discoverAndBuildUI = () => {
const groupID = this.properties.group_id;
const isExclusiveMode = this.properties.node_mode;
const settingsVisible = this.properties.show_settings;
this.widgets = [];
this.addWidget("toggle", "node_settings", settingsVisible, (v) => { this.properties.show_settings = v; this.resync(); }, { on: "Hide Settings", off: "Show Settings" });
// On efface tout sauf le widget de base qui ne sera jamais créé ici
this.widgets = [];
if (settingsVisible) {
this.addWidget("STRING", "workflow_id", this.properties.workflow_id, (v) => { this.properties.workflow_id = v; this.resync(); });
this.addWidget("STRING", "group_id", groupID, (v) => { this.properties.group_id = v; this.resync(); });
this.addWidget("toggle", "node_mode", isExclusiveMode, (v) => {
this.properties.node_mode = v;
if (v === true) updateNodeStates(null, this.properties.group_id);
this.resync();
}, { on: "Exclusive selection", off: "Multiple selection" });
}
const activeTagsInGraph = new Set();
const discoveredTags = new Set();
if (groupID) {
const regex = /\[\[(.*?):(.*?)\]\]/g;
for (const node of getAllNodesInGraph()) {
if (node.mode === 0 && node.title) { for (const match of node.title.matchAll(regex)) { if (match[1] === groupID) activeTagsInGraph.add(match[2]); } }
if (node.title) { for (const match of [...node.title.matchAll(regex)]) { if (match[1] === groupID) discoveredTags.add(match[2]); } }
}
}
const WORKFLOW_TAGS = Array.from(discoveredTags).sort();
if (WORKFLOW_TAGS.length > 0) {
this.widgets.push({ name: "top_spacer", type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] });
if (!isExclusiveMode && WORKFLOW_TAGS.length > 1) {
const allAreActive = WORKFLOW_TAGS.every(tag => activeTagsInGraph.has(tag));
this.addWidget("toggle", "Active All", allAreActive, (v) => {
const newSelection = v ? WORKFLOW_TAGS : [];
updateNodeStates(newSelection, this.properties.group_id);
this.resync();
});
this.widgets.push({ name: "separator_spacer", type: "CUSTOM_SPACER", draw: (ctx, node, width, y) => {
ctx.strokeStyle = "#222";
ctx.beginPath();
ctx.moveTo(10, y + 5);
ctx.lineTo(width - 10, y + 5);
ctx.stroke();
}, computeSize: () => [0, 10] });
}
const handleToggleClick = (toggledTagName, isTurningOn) => {
//console.log(`[Orchestrator] Clic sur le toggle '${toggledTagName}', valeur: ${isTurningOn}`);
if (isExclusiveMode) {
updateNodeStates(isTurningOn ? toggledTagName : null, groupID);
}
else {
const activeTags = WORKFLOW_TAGS.filter(tag => {
const w = this.widgets.find(w => w.name === tag);
return w && ((tag === toggledTagName) ? isTurningOn : w.value);
});
updateNodeStates(activeTags, groupID);
}
this.resync();
};
// On recrée les widgets à chaque fois à partir de this.properties
this.addWidget("toggle", "node_settings", settingsVisible, (value) => {
this.properties.show_settings = value;
discoverAndBuildUI();
}, { on: "Hide Settings", off: "Show Settings" });
for (const tag of WORKFLOW_TAGS) {
this.addWidget("toggle", tag, activeTagsInGraph.has(tag), (v) => handleToggleClick(tag, v));
}
}
if (settingsVisible) {
this.addWidget("STRING", "group_id", groupID, (value) => {
this.properties.group_id = value;
discoverAndBuildUI();
});
this.addWidget("toggle", "node_mode", isExclusiveMode, (value) => {
this.properties.node_mode = value;
// Si on passe en mode exclusif, on désactive tout pour forcer un nouveau choix.
if (value === true) {
updateNodeStates(null, this.properties.group_id);
}
discoverAndBuildUI();
}, { on: "Exclusive selection", off: "Multiple selection" });
const newComputedSize = this.computeSize();
this.size = [this.size[0], newComputedSize[1]];
app.graph.setDirtyCanvas(true, true);
};
this.discoverAndBuildUI = discoverAndBuildUI;
this.resync = () => {
//console.log("[Orchestrator] --- CYCLE DE RESYNCHRONISATION COMPLET ---");
const restored = restoreTitlesFromLocalStorage(this.properties.workflow_id);
this.discoverAndBuildUI();
if (!restored) {
saveTitlesToLocalStorage(this.properties.workflow_id);
}
const activeTagsInGraph = new Set();
if (groupID) {
const regex = /\[\[(.*?):(.*?)\]\]/g;
for (const node of getAllNodesInGraph()) {
if (node.mode === 0 && node.title) {
for (const match of node.title.matchAll(regex)) {
if (match[1] === groupID) activeTagsInGraph.add(match[2]);
}
}
}
}
const discoveredTags = new Set();
if (groupID) {
const regex = /\[\[(.*?):(.*?)\]\]/g;
for (const node of getAllNodesInGraph()) {
if (node.title) {
for (const match of [...node.title.matchAll(regex)]) {
if (match[1] === groupID) discoveredTags.add(match[2]);
}
}
}
}
const WORKFLOW_TAGS = Array.from(discoveredTags).sort();
if (WORKFLOW_TAGS.length > 0) {
this.widgets.push({ name: "top_spacer", type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] });
const handleToggleClick = (toggledTagName, isTurningOn) => {
if (isExclusiveMode) {
const newSelectedTag = isTurningOn ? toggledTagName : null;
updateNodeStates(newSelectedTag, groupID);
} else {
const activeTags = WORKFLOW_TAGS.filter(tag => {
const w = this.widgets.find(w => w.name === tag);
if (!w) return false;
return (tag === toggledTagName) ? isTurningOn : w.value;
});
updateNodeStates(activeTags, groupID);
}
setTimeout(() => discoverAndBuildUI(), 50);
};
for (const tag of WORKFLOW_TAGS) {
this.addWidget("toggle", tag, activeTagsInGraph.has(tag), (value) => handleToggleClick(tag, value));
}
}
const newComputedSize = this.computeSize();
this.size = [this.size[0], newComputedSize[1]];
app.graph.setDirtyCanvas(true, true);
//console.log("[Orchestrator] --- FIN DU CYCLE ---");
};
setTimeout(() => discoverAndBuildUI(), 100);
};
//console.log("[Orchestrator] Planification du premier 'resync' dans 500ms.");
setTimeout(() => this.resync(), 500);
const onDrawBackground = nodeType.prototype.onDrawBackground;
nodeType.prototype.onDrawBackground = function(ctx) {
onDrawBackground?.apply(this, arguments);
let titlesSignature = "";
for (const node of getAllNodesInGraph()) {
if (node.title?.includes('[[')) titlesSignature += node.title;
}
if (this.lastKnownNodeTitles !== titlesSignature) {
this.lastKnownNodeTitles = titlesSignature;
if(this.widgets[0]?.callback) {
this.widgets[0].callback(this.widgets[0].value);
this.backgroundCheckInterval = setInterval(() => {
if (!this.resync) return;
const allNodes = getAllNodesInGraph();
const nodeCount = allNodes.length;
let titlesSignature = "";
for (const node of allNodes) {
if (node.title?.includes('[[')) titlesSignature += `${node.id}:${node.title};`;
}
}
if (this.lastKnownNodeTitles === null) {
this.lastKnownNodeTitles = titlesSignature;
this.lastNodeCount = nodeCount;
return;
}
if (this.lastNodeCount !== nodeCount || this.lastKnownNodeTitles !== titlesSignature) {
//console.log(`[Orchestrator] setInterval: Changement de signature détecté !`);
this.lastNodeCount = nodeCount;
this.lastKnownNodeTitles = titlesSignature;
if (this.isFixingDuplicate) return;
const allIds = allNodes.map(n => String(n.id));
const seenIds = new Set();
let duplicateId = null;
for (const id of allIds) {
if (seenIds.has(id)) { duplicateId = id; break; }
seenIds.add(id);
}
if (duplicateId) {
const conflictingNodes = allNodes.filter(n => String(n.id) === duplicateId);
const hasSubgraph = conflictingNodes.some(n => isNodeSubgraph(n));
const hasNode = conflictingNodes.some(n => !isNodeSubgraph(n));
if (hasSubgraph && hasNode) {
const targetNode = conflictingNodes.find(n => !isNodeSubgraph(n));
if (targetNode) {
this.isFixingDuplicate = true;
//console.log(`%c[Orchestrator] Conflit détecté ! Remplacement du node "${targetNode.title}" (ID: ${targetNode.id}).`, "color: red; font-weight: bold;");
let parentSubgraph = null;
if (app.graph._subgraphs) {
for (const subgraph of app.graph._subgraphs.values()) {
if (subgraph.nodes.includes(targetNode)) {
parentSubgraph = subgraph;
break;
}
}
}
const correctPos = [...targetNode.pos];
const graphToRemoveFrom = parentSubgraph || app.graph;
graphToRemoveFrom.remove(targetNode);
const recreated = recreateNodeSafely({
type: targetNode.type, pos: correctPos, title: targetNode.title,
properties: targetNode.properties, widgets_values: targetNode.widgets_values,
flags: targetNode.flags, inputs: targetNode.inputs, outputs: targetNode.outputs
}, graphToRemoveFrom);
if (recreated) {
//console.log(`%c[Orchestrator] Node recréé proprement: ${recreated.type}`, "color: lightblue");
}
setTimeout(() => { this.isFixingDuplicate = false; this.resync(); }, 500);
return;
}
}
}
this.resync();
}
}, 200);
};
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
if (this.backgroundCheckInterval) {
clearInterval(this.backgroundCheckInterval);
//console.log(`[Orchestrator] Intervalle nettoyé pour le nœud ID ${this.id}`);
}
onRemoved?.apply(this, arguments);
};
}
},
});