mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 14:09:59 +00:00
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:
193
browser_tests/assets/batch_move_links.json
Normal file
193
browser_tests/assets/batch_move_links.json
Normal 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
|
||||
}
|
||||
@@ -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 |
@@ -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
30
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
74
src/types/litegraphTypes.ts
Normal file
74
src/types/litegraphTypes.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user