Update litegraph (Batch link move with shift + drag) (#268)

* Refactor based on new event data format

* nit

* Add playwright tests

* Update test expectations [skip ci]

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2024-07-30 19:06:58 -04:00
committed by GitHub
parent 7d2d6df57b
commit 0e3590d017
10 changed files with 352 additions and 49 deletions

View File

@@ -0,0 +1,193 @@
{
"last_node_id": 10,
"last_link_id": 9,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
0,
92
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"3Guofeng3_v32Light.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
460,
92
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
460,
368
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": [
0,
276
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"3Guofeng3_v32Light.safetensors"
]
}
],
"links": [
[
3,
4,
1,
6,
0,
"CLIP"
],
[
5,
4,
1,
7,
0,
"CLIP"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -58,6 +58,28 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
x: 304,
y: 127
}
const outputSlot2Pos = {
x: 307,
y: 310
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
})
})
test.describe('Canvas Interaction', () => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -32,4 +32,26 @@ test.describe('Node search box', () => {
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode')
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('batch_move_links')
const outputSlot1Pos = {
x: 304,
y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint')
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

30
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.2.6",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@comfyorg/litegraph": "^0.7.31",
"@comfyorg/litegraph": "^0.7.34",
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"class-transformer": "^0.5.1",
@@ -54,6 +54,28 @@
"zip-dir": "^2.0.0"
}
},
"../litegraph.js": {
"name": "@comfyorg/litegraph",
"version": "0.7.32",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@types/jest": "^28.1.3",
"eslint": "^8.37.0 ",
"eslint-plugin-jest": "^27.2.1",
"express": "^4.17.1",
"google-closure-compiler": "^20230411.0.0",
"grunt": "^1.1.0",
"grunt-cli": "^1.2.0",
"grunt-closure-tools": "^1.0.0",
"grunt-contrib-concat": "^2.1.0",
"jest": "^28.1.3",
"jest-cli": "^28.1.3",
"nodemon": "^2.0.22",
"rimraf": "^5.0.0",
"yuidocjs": "^0.10.2"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1830,9 +1852,9 @@
"dev": true
},
"node_modules/@comfyorg/litegraph": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.31.tgz",
"integrity": "sha512-SfsswAUA9AmMYLPqMXVdJFI9eRdjrPHhJ1NrD//SNEDxz2HAAivyf3XzRnzxBcWCFmlMsyi+ybg4wiITwI3r3A==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.34.tgz",
"integrity": "sha512-PLQ9QDTbsVzQE6qr64HWs3z+U8wGHC4GgFeLvEbXL70LLZ1yiWVfNVZI3TsKuVD2jKdsBpDJ8vAdVUW5BwqGyQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -47,7 +47,7 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
"@comfyorg/litegraph": "^0.7.31",
"@comfyorg/litegraph": "^0.7.34",
"@primevue/themes": "^4.0.0-rc.2",
"@vitejs/plugin-vue": "^5.0.5",
"class-transformer": "^0.5.1",

View File

@@ -25,14 +25,10 @@ import { app } from '@/scripts/app'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import NodeSearchBox from './NodeSearchBox.vue'
import Dialog from 'primevue/dialog'
import {
INodeSlot,
LiteGraphCanvasEvent,
LGraphNode,
LinkReleaseContext
} from '@comfyorg/litegraph'
import { LiteGraphCanvasEvent, ConnectingLink } from '@comfyorg/litegraph'
import { FilterAndValue } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
interface LiteGraphPointerEvent extends Event {
canvasX: number
@@ -67,36 +63,7 @@ const clearFilters = () => {
const closeDialog = () => {
visible.value = false
}
const connectNodeOnLinkRelease = (
node: LGraphNode,
context: LinkReleaseContext
) => {
const destIsInput = context.node_from !== undefined
const srcNode = (
destIsInput ? context.node_from : context.node_to
) as LGraphNode
const srcSlotIndex: number = context.slot_from.slot_index
const linkDataType = destIsInput
? context.type_filter_in
: context.type_filter_out
const destSlots = destIsInput ? node.inputs : node.outputs
const destSlotIndex = destSlots.findIndex(
(slot: INodeSlot) => slot.type === linkDataType
)
if (destSlotIndex === -1) {
console.warn(
`Could not find slot with type ${linkDataType} on node ${node.title}`
)
return
}
if (destIsInput) {
srcNode.connect(srcSlotIndex, node, destSlotIndex)
} else {
node.connect(destSlotIndex, srcNode, srcSlotIndex)
}
}
const addNode = (nodeDef: ComfyNodeDefImpl) => {
closeDialog()
@@ -104,7 +71,9 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
const eventDetail = triggerEvent.value.detail
if (eventDetail.subType === 'empty-release') {
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext)
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
})
}
}
@@ -117,16 +86,17 @@ const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
}
if (e.detail.subType === 'empty-release') {
const destIsInput = e.detail.linkReleaseContext.node_from !== undefined
const context = e.detail.linkReleaseContext
if (context.links.length === 0) {
console.warn('Empty release with no links! This should never happen')
return
}
const firstLink = ConnectingLinkImpl.createFromPlainObject(context.links[0])
const filter = useNodeDefStore().nodeSearchService.getFilterById(
destIsInput ? 'input' : 'output'
firstLink.releaseSlotType
)
const value = destIsInput
? e.detail.linkReleaseContext.type_filter_in
: e.detail.linkReleaseContext.type_filter_out
addFilter([filter, value])
const dataType = firstLink.type
addFilter([filter, dataType])
}
triggerEvent.value = e
visible.value = true

View File

@@ -0,0 +1,74 @@
import {
ConnectingLink,
LGraphNode,
Vector2,
INodeInputSlot,
INodeOutputSlot,
INodeSlot
} from '@comfyorg/litegraph'
export class ConnectingLinkImpl implements ConnectingLink {
node: LGraphNode
slot: number
input: INodeInputSlot | null
output: INodeOutputSlot | null
pos: Vector2
constructor(
node: LGraphNode,
slot: number,
input: INodeInputSlot | null,
output: INodeOutputSlot | null,
pos: Vector2
) {
this.node = node
this.slot = slot
this.input = input
this.output = output
this.pos = pos
}
static createFromPlainObject(obj: ConnectingLink) {
return new ConnectingLinkImpl(
obj.node,
obj.slot,
obj.input,
obj.output,
obj.pos
)
}
get type(): string | null {
const result = this.input ? this.input.type : this.output.type
return result === -1 ? null : result
}
/**
* Which slot type is release and need to be reconnected.
* - 'output' means we need a new node's outputs slot to connect with this link
*/
get releaseSlotType(): 'input' | 'output' {
return this.output ? 'input' : 'output'
}
connectTo(newNode: LGraphNode) {
const newNodeSlots =
this.releaseSlotType === 'output' ? newNode.outputs : newNode.inputs
const newNodeSlot = newNodeSlots.findIndex(
(slot: INodeSlot) => slot.type === this.type
)
if (newNodeSlot === -1) {
console.warn(
`Could not find slot with type ${this.type} on node ${newNode.title}. This should never happen`
)
return
}
if (this.releaseSlotType === 'input') {
this.node.connect(this.slot, newNode, newNodeSlot)
} else {
newNode.connect(newNodeSlot, this.node, this.slot)
}
}
}