diff --git a/README.md b/README.md index fa4840b..565f832 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,11 @@ undo/redo possible with https://github.com/bmad4ever/ComfyUI-Bmad-DirtyUndoRedo recommend: remove reroute nodes so the algorithms work better as it figures out the ranks/depth, after applied layout u can re-add reroute nodes for any wires partially hidden by nodes -TODO: -- add UI options to change density +**TODO**: +- [x] refresh after apply layout +- [x] add UI options to change density +- [ ] better UI than pop-up for options to change density +- [ ] option to select layout strategy (see docs for each algo) ## example using [noisy latent composition example](https://comfyanonymous.github.io/ComfyUI_examples/noisy_latent_composition/) diff --git a/web/autoLayout.js b/web/autoLayout.js index d142a7e..0cc0fd1 100644 --- a/web/autoLayout.js +++ b/web/autoLayout.js @@ -6,6 +6,7 @@ import "./dagre.min.js"; import "./elk.bundled.min.js"; // idk how to do named import with those + app.registerExtension({ "name": "doof.autoNodesLayout", setup() { // Add canvas menu options @@ -15,9 +16,10 @@ app.registerExtension({ options.push(myRightClickMenu); // def below return options; } - } + }, }); + const myRightClickMenu = { "content": "📍 auto nodes layout", "has_submenu": true, @@ -34,44 +36,53 @@ const myRightClickMenu = { { "content": "ELK.js ‘layered’ layout", "callback": elkLayeredLayout // def below - } - ] - } + }, + ], + }, }; + +// 2 values to control layout density (string for max compability with window.prompt) +var ranksep = "200"; // spacing (px) between ranks/depths/columns +var nodesep = "150"; // spacing (px) between nodes in same rank/depth/column + + /** * arrange nodes using Dagre layout * @see https://github.com/dagrejs/dagre - * @todo add options to change density + * @todo better UI than pop-up * @returns {undefined} Nothing is returned. */ function dagreLayout() { - detectRerouteNode(); // def below + popupInput(); // def below // setup dagre const daG = new dagre.graphlib - .Graph() + .Graph({ "compound": false }) .setGraph({ "rankdir": "LR", // left to right - "ranker": "network-simplex", // values: "network-simplex", "tight-tree", "longest-path" - "nodesep": 100, // spacing in same column, can be changed - "ranksep": 200, // spacing between columns, can be changed - }); + "ranker": "network-simplex", + // values: "network-simplex", "tight-tree", "longest-path" + "ranksep": parseFloat(ranksep), + "nodesep": parseFloat(nodesep), + }) + .setDefaultNodeLabel(() => ({})) + .setDefaultEdgeLabel(() => ({})); // convert litegraph to dagre app.graph._nodes.forEach((n) => daG.setNode( n.id.toString(), { - "label": n.type, "width": n.size[0], - "height": n.size[1] + "height": n.size[1], } )); app.graph.links.forEach((e) => daG.setEdge( e.origin_id.toString(), - e.target_id.toString(), - { "label": e.type } + e.target_id.toString() )); + + // apply layout algorithm dagre.layout(daG); // retrieve nodes position @@ -85,43 +96,53 @@ function dagreLayout() { return; } + /** * arrange nodes using ELK ‘layered’ layout * @see https://github.com/kieler/elkjs - * @todo add options to change density + * @todo better UI than pop-up * @returns {undefined} Nothing is returned. */ function elkLayeredLayout() { - detectRerouteNode(); // def below + popupInput(); // def below // convert litegraph to elk const myElkNodes = app.graph._nodes.map((n) => ({ "id": n.id, "width": n.size[0], - "height": n.size[1] + "height": n.size[1], })); const myElkEdges = app.graph.links.filter(Boolean).map((e) => ({ "id": e.id, "sources": [ e.origin_id ], - "targets": [ e.target_id ] + "targets": [ e.target_id ], })); + + // setup ELK const myElkGraph = { "id": "root", "children": myElkNodes, "edges": myElkEdges, "layoutOptions": { - "elk.algorithm": "layered" - } + "elk.algorithm": "layered", + "elk.direction": "RIGHT", + "elk.layered.layering.strategy": "NETWORK_SIMPLEX", + // values: "NETWORK_SIMPLEX", "LONGEST_PATH", "COFFMAN_GRAHAM" + "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", + // values: "NETWORK_SIMPLEX", "BRANDES_KOEPF", "LINEAR_SEGMENTS" + "elk.layered.spacing.nodeNodeBetweenLayers": ranksep, + "elk.spacing.nodeNode": nodesep, + }, } - // setup ELK & retrieve nodes position + // apply layout algorithm const elk = new ELK() .layout(myElkGraph) - .then((val) => { + .then((val) => { // retrieve nodes position for (const nodeLaidOut of val.children) { const n = app.graph.getNodeById(nodeLaidOut.id); - n.pos[0] = nodeLaidOut.x * 2; // add more spacing - n.pos[1] = nodeLaidOut.y * 2; + n.pos[0] = nodeLaidOut.x; + n.pos[1] = nodeLaidOut.y; } }) .catch(console.error); @@ -130,19 +151,27 @@ function elkLayeredLayout() { return; } + /** + * ask for user input to control layout density then * make a alert in case there’s any reroute node * @returns {undefined} Nothing is returned. */ -function detectRerouteNode() { +function popupInput() { + // change global value in a session + ranksep = window.prompt("Enter spacing (px) between ranks/depths/columns", ranksep); + nodesep = window.prompt("Enter spacing (px) between nodes in same rank/depth/column", nodesep); + + // make a alert in case there’s any reroute node for (const n of app.graph._nodes) { - if (n.constructor.name === "RerouteNode") { + if (n.type === "Reroute") { window.alert( - "Layout algorithms work best without Reroute nodes!\n" + "Layout algorithms work better without Reroute nodes!\n" + "better remove reroute before auto-layout then re-add after" ); break; } } + return; }