mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Add Litegraph multi-select & group nesting (#1416)
* Allow nested groups Pass all selected items (new litegraph feature) instead of just selected nodes. * Allow nested groups - context menus * Update litegraph * Update litegraph (Select all / Delete selected) * Add playwright test * nit * Update test expectations [skip ci] --------- Co-authored-by: huchenlei <huchenlei@proton.me> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
227
browser_tests/assets/mixed_graph_items.json
Normal file
227
browser_tests/assets/mixed_graph_items.json
Normal file
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 420,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 820,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 30,
|
||||
"1": 130
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 0,
|
||||
"title": "Group",
|
||||
"bounding": [
|
||||
406.9701232910156,
|
||||
59.079444885253906,
|
||||
335,
|
||||
345.6000061035156
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Group Parent",
|
||||
"bounding": [
|
||||
796.9703979492188,
|
||||
14.796443939208984,
|
||||
355,
|
||||
399.20001220703125
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Group Child",
|
||||
"bounding": [
|
||||
806.9703979492188,
|
||||
58.39643096923828,
|
||||
335,
|
||||
345.6000061035156
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
|
||||
await comfyPage.canvas.press('Delete')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node Interaction', () => {
|
||||
test('Can enter prompt', async ({ comfyPage }) => {
|
||||
const textBox = comfyPage.widgetTextBox
|
||||
@@ -12,7 +22,7 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
|
||||
test.describe('Node Selection', () => {
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta']
|
||||
const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const
|
||||
|
||||
multiSelectModifiers.forEach((modifier) => {
|
||||
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
8
package-lock.json
generated
8
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.3.31",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/litegraph": "^0.8.15",
|
||||
"@comfyorg/litegraph": "^0.8.17",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
@@ -1911,9 +1911,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.15",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.15.tgz",
|
||||
"integrity": "sha512-Wq3QxgBNs93cR7gN8aboKbsvmVoWmj9kIxuI1wm1xZUMEI3PfpwGVhRLjABxk5AMtxFOOMiQIcJvbYTAoW+itQ==",
|
||||
"version": "0.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.17.tgz",
|
||||
"integrity": "sha512-koNvj7Bei1MWELlLC9OuvGb2TGTzfqxExYbl/m0bK6taoLk82Xu3mnHz2T9iuxZMih+jwnriI6nNjCFvu1jTfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/litegraph": "^0.8.15",
|
||||
"@comfyorg/litegraph": "^0.8.17",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"axios": "^1.7.4",
|
||||
|
||||
@@ -1,66 +1,17 @@
|
||||
// @ts-strict-ignore
|
||||
import { LGraphGroup } from '@comfyorg/litegraph'
|
||||
import { app } from '../../scripts/app'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
function setNodeMode(node, mode) {
|
||||
function setNodeMode(node: LGraphNode, mode: number) {
|
||||
node.mode = mode
|
||||
node.graph.change()
|
||||
node.graph?.change()
|
||||
}
|
||||
|
||||
function addNodesToGroup(group, nodes = []) {
|
||||
var x1, y1, x2, y2
|
||||
var nx1, ny1, nx2, ny2
|
||||
var node
|
||||
|
||||
x1 = y1 = x2 = y2 = -1
|
||||
nx1 = ny1 = nx2 = ny2 = -1
|
||||
|
||||
for (var n of [group.nodes, nodes]) {
|
||||
for (var i in n) {
|
||||
node = n[i]
|
||||
|
||||
nx1 = node.pos[0]
|
||||
ny1 = node.pos[1]
|
||||
nx2 = node.pos[0] + node.size[0]
|
||||
ny2 = node.pos[1] + node.size[1]
|
||||
|
||||
if (node.type != 'Reroute') {
|
||||
ny1 -= LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
if (node.flags?.collapsed) {
|
||||
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
if (node?._collapsed_width) {
|
||||
nx2 = nx1 + Math.round(node._collapsed_width)
|
||||
}
|
||||
}
|
||||
|
||||
if (x1 == -1 || nx1 < x1) {
|
||||
x1 = nx1
|
||||
}
|
||||
|
||||
if (y1 == -1 || ny1 < y1) {
|
||||
y1 = ny1
|
||||
}
|
||||
|
||||
if (x2 == -1 || nx2 > x2) {
|
||||
x2 = nx2
|
||||
}
|
||||
|
||||
if (y2 == -1 || ny2 > y2) {
|
||||
y2 = ny2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var padding = 10
|
||||
|
||||
y1 = y1 - Math.round(group.font_size * 1.4)
|
||||
|
||||
group.pos = [x1 - padding, y1 - padding]
|
||||
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]
|
||||
function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
|
||||
group.resizeTo([...group.children, ...items])
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
@@ -68,7 +19,9 @@ app.registerExtension({
|
||||
setup() {
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
// graph_mouse
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function (
|
||||
this: LGraphCanvas
|
||||
) {
|
||||
const options = orig.apply(this, arguments)
|
||||
const group = this.graph.getGroupOnPos(
|
||||
this.graph_mouse[0],
|
||||
@@ -77,11 +30,11 @@ app.registerExtension({
|
||||
if (!group) {
|
||||
options.push({
|
||||
content: 'Add Group For Selected Nodes',
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
disabled: !this.selectedItems?.size,
|
||||
callback: () => {
|
||||
const group = new LGraphGroup()
|
||||
addNodesToGroup(group, this.selected_nodes)
|
||||
app.canvas.graph.add(group)
|
||||
addNodesToGroup(group, this.selectedItems)
|
||||
this.graph.add(group)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
@@ -95,9 +48,9 @@ app.registerExtension({
|
||||
|
||||
options.push({
|
||||
content: 'Add Selected Nodes To Group',
|
||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||
disabled: !this.selectedItems?.size,
|
||||
callback: () => {
|
||||
addNodesToGroup(group, this.selected_nodes)
|
||||
addNodesToGroup(group, this.selectedItems)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
@@ -122,7 +75,8 @@ app.registerExtension({
|
||||
options.push({
|
||||
content: 'Fit Group To Nodes',
|
||||
callback: () => {
|
||||
addNodesToGroup(group)
|
||||
group.recomputeInsideNodes()
|
||||
group.resizeTo(group.children)
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -361,14 +361,13 @@ export const useCommandStore = defineStore('command', () => {
|
||||
label: 'Group Selected Nodes',
|
||||
versionAdded: '1.3.7',
|
||||
function: () => {
|
||||
if (
|
||||
!app.canvas.selected_nodes ||
|
||||
Object.keys(app.canvas.selected_nodes).length === 0
|
||||
) {
|
||||
const { canvas } = app
|
||||
if (!canvas.selectedItems?.size) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: 'No nodes selected',
|
||||
detail: 'Please select nodes to group',
|
||||
summary: 'Nothing to group',
|
||||
detail:
|
||||
'Please select the nodes (or other groups) to create a group for',
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
@@ -377,8 +376,8 @@ export const useCommandStore = defineStore('command', () => {
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.addNodes(Object.values(app.canvas.selected_nodes), padding)
|
||||
app.canvas.graph.add(group)
|
||||
group.resizeTo(canvas.selectedItems, padding)
|
||||
canvas.graph.add(group)
|
||||
useTitleEditorStore().titleEditorTarget = group
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user