From 2e79919b5ae308e2afba61004d85819997c6750d Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:26:54 +0000 Subject: [PATCH] order widgets on group node various fixes --- .vscode/settings.json | 9 +++++ tests-ui/tests/groupNode.test.js | 68 ++++++++++++++++++++++++++------ web/extensions/core/groupNode.js | 43 +++++++++++++------- 3 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..202121e10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "path-intellisense.mappings": { + "../": "${workspaceFolder}/web/extensions/core" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.autopep8" + }, + "python.formatting.provider": "none" +} diff --git a/tests-ui/tests/groupNode.test.js b/tests-ui/tests/groupNode.test.js index 109eeb8b5..835781154 100644 --- a/tests-ui/tests/groupNode.test.js +++ b/tests-ui/tests/groupNode.test.js @@ -114,9 +114,9 @@ describe("group node", () => { expect(group.inputs.map((i) => i.input.name)).toEqual(["CLIPTextEncode clip", "CLIPTextEncode 2 clip"]); expect(group.outputs.map((i) => i.output.name)).toEqual([ + "EmptyLatentImage LATENT", "CLIPTextEncode CONDITIONING", "CLIPTextEncode 2 CONDITIONING", - "EmptyLatentImage LATENT", ]); // ckpt clip to both clip inputs on the group @@ -228,7 +228,7 @@ describe("group node", () => { expect(group.inputs).toHaveLength(1); expect(group.inputs[0].input.type).toEqual("CLIP"); - expect((await graph.toPrompt()).output).toEqual(getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id])); + expect((await graph.toPrompt()).output).toEqual(getOutput()); }); test("it can embed reroutes as outputs", async () => { const { ez, graph, app } = await start(); @@ -458,24 +458,25 @@ describe("group node", () => { group2.widgets["EmptyLatentImage width"].value = 1024; group2.widgets["KSampler seed"].value = 100; + let i = 0; expect((await graph.toPrompt()).output).toEqual({ - ...getOutput([nodes.pos.id, nodes.neg.id, nodes.empty.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], { - [nodes.pos.id]: { text: "hello" }, + ...getOutput([nodes.empty.id, nodes.pos.id, nodes.neg.id, nodes.sampler.id, nodes.decode.id, nodes.save.id], { [nodes.empty.id]: { width: 256 }, + [nodes.pos.id]: { text: "hello" }, [nodes.sampler.id]: { seed: 1 }, }), ...getOutput( { - [nodes.pos.id]: `${group2.id}:0`, - [nodes.neg.id]: `${group2.id}:1`, - [nodes.empty.id]: `${group2.id}:2`, - [nodes.sampler.id]: `${group2.id}:3`, - [nodes.decode.id]: `${group2.id}:4`, - [nodes.save.id]: `${group2.id}:5`, + [nodes.empty.id]: `${group2.id}:${i++}`, + [nodes.pos.id]: `${group2.id}:${i++}`, + [nodes.neg.id]: `${group2.id}:${i++}`, + [nodes.sampler.id]: `${group2.id}:${i++}`, + [nodes.decode.id]: `${group2.id}:${i++}`, + [nodes.save.id]: `${group2.id}:${i++}`, }, { - [nodes.pos.id]: { text: "world" }, [nodes.empty.id]: { width: 1024 }, + [nodes.pos.id]: { text: "world" }, [nodes.sampler.id]: { seed: 100 }, } ), @@ -581,4 +582,49 @@ describe("group node", () => { 2: { inputs: { text: "positive" }, class_type: "CLIPTextEncode" }, }); }); + test("adds widgets in node execution order", async () => { + const { ez, graph, app } = await start(); + const scale = ez.LatentUpscale(); + const save = ez.SaveImage(); + const empty = ez.EmptyLatentImage(); + const decode = ez.VAEDecode(); + + scale.outputs.LATENT.connectTo(decode.inputs.samples); + decode.outputs.IMAGE.connectTo(save.inputs.images); + empty.outputs.LATENT.connectTo(scale.inputs.samples); + + const group = await convertToGroup(app, graph, "test", [scale, save, empty, decode]); + const widgets = group.widgets.map((w) => w.widget.name); + expect(widgets).toStrictEqual([ + "EmptyLatentImage width", + "EmptyLatentImage height", + "EmptyLatentImage batch_size", + "LatentUpscale upscale_method", + "LatentUpscale width", + "LatentUpscale height", + "LatentUpscale crop", + "SaveImage filename_prefix", + ]); + }); + test("adds output for external links when converting to group", async () => { + const { ez, graph, app } = await start(); + const img = ez.EmptyLatentImage(); + let decode = ez.VAEDecode(...img.outputs); + const preview1 = ez.PreviewImage(...decode.outputs); + const preview2 = ez.PreviewImage(...decode.outputs); + + const group = await convertToGroup(app, graph, "test", [img, decode, preview1]); + + // Ensure we have an output connected to the 2nd preview node + expect(group.outputs.length).toBe(1); + expect(group.outputs[0].connections.length).toBe(1); + expect(group.outputs[0].connections[0].targetNode.id).toBe(preview2.id); + + // Convert back and ensure bothe previews are still connected + group.menu["Convert to nodes"].call(); + decode = graph.find(decode); + expect(decode.outputs[0].connections.length).toBe(2); + expect(decode.outputs[0].connections[0].targetNode.id).toBe(preview1.id); + expect(decode.outputs[0].connections[1].targetNode.id).toBe(preview2.id); + }); }); diff --git a/web/extensions/core/groupNode.js b/web/extensions/core/groupNode.js index 3dd1becdd..af06aede1 100644 --- a/web/extensions/core/groupNode.js +++ b/web/extensions/core/groupNode.js @@ -41,11 +41,13 @@ export async function registerGroupNodes(groupNodes, source, prefix, missingNode function getOutputs(config) { const map = {}; - for (const ext of config.external) { - if (!map[ext[0]]) { - map[ext[0]] = { [ext[1]]: true }; - } else { - map[ext[0]][ext[1]] = true; + if (config.external) { + for (const ext of config.external) { + if (!map[ext[0]]) { + map[ext[0]] = { [ext[1]]: true }; + } else { + map[ext[0]][ext[1]] = true; + } } } return map; @@ -247,9 +249,20 @@ class ConvertToGroupAction { } async register(name) { + // We want to generate the copied nodes in execution order so the internal node widgets show in a sensible order + const nodes = app.graph.computeExecutionOrder(false); + const selectedIds = Object.keys(app.canvas.selected_nodes); + const ordered = selectedIds + .map((id) => { + const node = app.graph.getNodeById(id); + return { index: nodes.indexOf(node), node }; + }) + .sort((a, b) => a.index - b.index || a.node.id - b.node.id) + .map(({ node }) => node); + // Use the built in copyToClipboard function to generate the node data we need const backup = localStorage.getItem("litegrapheditor_clipboard"); - app.canvas.copyToClipboard(); + app.canvas.copyToClipboard(ordered); const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard")); localStorage.setItem("litegrapheditor_clipboard", backup); @@ -261,7 +274,6 @@ class ConvertToGroupAction { } // Check for external links to add extra outputs - const selectedIds = Object.keys(app.canvas.selected_nodes); for (let i = 0; i < selectedIds.length; i++) { const id = selectedIds[i]; const node = app.graph.getNodeById(id); @@ -289,7 +301,7 @@ class ConvertToGroupAction { const def = buildNodeDef(config, name, globalDefs); await app.registerNodeDef("workflow/" + name, def); - return { config, def }; + return { config, def, nodes: ordered }; } findOutput(slots, link, index) { @@ -330,7 +342,7 @@ class ConvertToGroupAction { } } - convert(name, config, def) { + convert(name, config, def, nodes) { const newNode = LiteGraph.createNode("workflow/" + name); app.graph.add(newNode); @@ -339,8 +351,7 @@ class ConvertToGroupAction { let index = 0; const slots = def[GROUP_SLOTS]; newNode[GROUP_IDS] = {}; - for (const id in app.canvas.selected_nodes) { - const node = app.graph.getNodeById(id); + for (const node of nodes) { if (left == null || node.pos[0] < left) { left = node.pos[0]; } @@ -351,7 +362,7 @@ class ConvertToGroupAction { this.linkOutputs(newNode, node, slots, index++); // Store the original ID so the node is reused in this session - newNode[GROUP_IDS][node._relative_id] = id; + newNode[GROUP_IDS][node._relative_id] = node.id; app.graph.remove(node); } @@ -376,10 +387,10 @@ class ConvertToGroupAction { let groupNodes = extra.groupNodes; if (!groupNodes) extra.groupNodes = groupNodes = {}; - const { config, def } = await this.register(name); + const { config, def, nodes } = await this.register(name); groupNodes[name] = config; - return this.convert(name, config, def); + return this.convert(name, config, def, nodes); }, }); } @@ -593,7 +604,9 @@ const ext = { let top; let left; const slots = def[GROUP_SLOTS]; - const selectedIds = Object.keys(app.canvas.selected_nodes); + const selectedIds = node[GROUP_IDS] + ? Object.values(node[GROUP_IDS]) + : Object.keys(app.canvas.selected_nodes); const newNodes = []; for (let i = 0; i < selectedIds.length; i++) { const id = selectedIds[i];