diff --git a/browser_tests/assets/batch_move_links.json b/browser_tests/assets/batch_move_links.json new file mode 100644 index 0000000000..a2d8a54e73 --- /dev/null +++ b/browser_tests/assets/batch_move_links.json @@ -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 +} \ No newline at end of file diff --git a/browser_tests/interaction.spec.ts b/browser_tests/interaction.spec.ts index 9a9dffae25..b126eee031 100644 --- a/browser_tests/interaction.spec.ts +++ b/browser_tests/interaction.spec.ts @@ -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', () => { diff --git a/browser_tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png new file mode 100644 index 0000000000..427306564d Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/batch-move-links-chromium-linux.png differ diff --git a/browser_tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png new file mode 100644 index 0000000000..0e0efadad1 Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/batch-move-links-moved-chromium-linux.png differ diff --git a/browser_tests/nodeSearchBox.spec.ts b/browser_tests/nodeSearchBox.spec.ts index 8676d9a98c..f05693e2b0 100644 --- a/browser_tests/nodeSearchBox.spec.ts +++ b/browser_tests/nodeSearchBox.spec.ts @@ -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' + ) + }) }) diff --git a/browser_tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png b/browser_tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png new file mode 100644 index 0000000000..e178207e9b Binary files /dev/null and b/browser_tests/nodeSearchBox.spec.ts-snapshots/auto-linked-node-batch-chromium-linux.png differ diff --git a/package-lock.json b/package-lock.json index b7ac1c0af9..8ef0fe8c17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 62852ece64..4c1bf37b37 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/NodeSearchBoxPopover.vue b/src/components/NodeSearchBoxPopover.vue index 74f3d1d20d..6025debdea 100644 --- a/src/components/NodeSearchBoxPopover.vue +++ b/src/components/NodeSearchBoxPopover.vue @@ -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 diff --git a/src/types/litegraphTypes.ts b/src/types/litegraphTypes.ts new file mode 100644 index 0000000000..d2089dc27b --- /dev/null +++ b/src/types/litegraphTypes.ts @@ -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) + } + } +}