add retag auto when fixe node recreate
This commit is contained in:
+304
-109
@@ -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);
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user