diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 08cdd5cce..2f51533ce 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -3,7 +3,7 @@ import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' import * as fs from 'fs' -import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph' +import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' import type { KeyCombo } from '../../src/schemas/keyBindingSchema' import type { useWorkspaceStore } from '../../src/stores/workspaceStore' @@ -1591,14 +1591,29 @@ export class ComfyPage { return window['app'].graph.nodes }) } - async getNodeRefsByType(type: string): Promise { + async waitForGraphNodes(count: number) { + await this.page.waitForFunction((count) => { + return window['app']?.canvas.graph?.nodes?.length === count + }, count) + } + async getNodeRefsByType( + type: string, + includeSubgraph: boolean = false + ): Promise { return Promise.all( ( - await this.page.evaluate((type) => { - return window['app'].graph.nodes - .filter((n: LGraphNode) => n.type === type) - .map((n: LGraphNode) => n.id) - }, type) + await this.page.evaluate( + ({ type, includeSubgraph }) => { + const graph = ( + includeSubgraph ? window['app'].canvas.graph : window['app'].graph + ) as LGraph + const nodes = graph.nodes + return nodes + .filter((n: LGraphNode) => n.type === type) + .map((n: LGraphNode) => n.id) + }, + { type, includeSubgraph } + ) ).map((id: NodeId) => this.getNodeRefById(id)) ) } diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index 6c84af9cd..3c11cfda2 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -163,4 +163,14 @@ export class VueNodeHelpers { incrementButton: widget.getByTestId('increment') } } + + /** + * Enter the subgraph of a node. + * @param nodeId - The ID of the node to enter the subgraph of. If not provided, the first matched subgraph will be entered. + */ + async enterSubgraph(nodeId?: string): Promise { + const locator = nodeId ? this.getNodeLocator(nodeId) : this.page + const editButton = locator.getByTestId('subgraph-enter-button') + await editButton.click() + } } diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 09a78fb6e..315758f5c 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -102,7 +102,7 @@ test.describe('Vue Node Link Interaction', () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setup() + // await comfyPage.setup() await comfyPage.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) @@ -993,4 +993,51 @@ test.describe('Vue Node Link Interaction', () => { expect(linked).toBe(true) }) }) + + test('Dragging from subgraph input connects to correct slot', async ({ + comfyPage, + comfyMouse + }) => { + // Setup workflow with a KSampler node + await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.waitForGraphNodes(0) + await comfyPage.executeCommand('Workspace.SearchBox.Toggle') + await comfyPage.nextFrame() + await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') + await comfyPage.waitForGraphNodes(1) + + // Convert the KSampler node to a subgraph + let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0] + await comfyPage.vueNodes.selectNode(String(ksamplerNode.id)) + await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph') + + // Enter the subgraph + await comfyPage.vueNodes.enterSubgraph() + await fitToViewInstant(comfyPage) + + // Get the KSampler node inside the subgraph + ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0] + const positiveInput = await ksamplerNode.getInput(1) + const negativeInput = await ksamplerNode.getInput(2) + + const positiveInputPos = await getSlotCenter( + comfyPage.page, + ksamplerNode.id, + 1, + true + ) + + const sourceSlot = await comfyPage.getSubgraphInputSlot() + const calculatedSourcePos = await sourceSlot.getOpenSlotPosition() + + await comfyMouse.move(calculatedSourcePos) + await comfyMouse.drag(positiveInputPos) + await comfyMouse.drop() + + // Verify connection went to the correct slot + const positiveLinks = await positiveInput.getLinkCount() + const negativeLinks = await negativeInput.getLinkCount() + expect(positiveLinks).toBe(1) + expect(negativeLinks).toBe(0) + }) }) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 78ddf2836..16a48ace3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 804e16c0d..56f48867c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 9b8d4d153..1ff503cdc 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,8 +1,6 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' import { - calculateInputSlotPos, calculateInputSlotPosFromSlot, - calculateOutputSlotPos, getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' @@ -3349,7 +3347,7 @@ export class LGraphNode * @returns Position of the input slot */ getInputPos(slot: number): Point { - return calculateInputSlotPos(this.#getSlotPositionContext(), slot) + return getSlotPosition(this, slot, true) } /** @@ -3369,10 +3367,7 @@ export class LGraphNode * @returns Position of the output slot */ getOutputPos(outputSlotIndex: number): Point { - return calculateOutputSlotPos( - this.#getSlotPositionContext(), - outputSlotIndex - ) + return getSlotPosition(this, outputSlotIndex, false) } /** diff --git a/src/renderer/core/canvas/litegraph/slotCalculations.ts b/src/renderer/core/canvas/litegraph/slotCalculations.ts index 8b66b68a8..32560441e 100644 --- a/src/renderer/core/canvas/litegraph/slotCalculations.ts +++ b/src/renderer/core/canvas/litegraph/slotCalculations.ts @@ -45,7 +45,7 @@ export interface SlotPositionContext { * @param slot The input slot index * @returns Position of the input slot center in graph coordinates */ -export function calculateInputSlotPos( +function calculateInputSlotPos( context: SlotPositionContext, slot: number ): Point { @@ -93,7 +93,7 @@ export function calculateInputSlotPosFromSlot( * @param slot The output slot index * @returns Position of the output slot center in graph coordinates */ -export function calculateOutputSlotPos( +function calculateOutputSlotPos( context: SlotPositionContext, slot: number ): Point { @@ -138,38 +138,41 @@ export function getSlotPosition( slotIndex: number, isInput: boolean ): Point { - // Try to get precise position from slot layout (DOM-registered) - const slotKey = getSlotKey(String(node.id), slotIndex, isInput) - const slotLayout = layoutStore.getSlotLayout(slotKey) - if (slotLayout) { - return [slotLayout.position.x, slotLayout.position.y] - } - - // Fallback: derive position from node layout tree and slot model - const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value - - if (nodeLayout) { - // Create context from layout tree data - const context: SlotPositionContext = { - nodeX: nodeLayout.position.x, - nodeY: nodeLayout.position.y, - nodeWidth: nodeLayout.size.width, - nodeHeight: nodeLayout.size.height, - collapsed: node.flags.collapsed || false, - collapsedWidth: node._collapsed_width, - slotStartY: node.constructor.slot_start_y, - inputs: node.inputs, - outputs: node.outputs, - widgets: node.widgets + // Only use DOM-registered slot positions when Vue nodes mode is enabled + if (LiteGraph.vueNodesMode) { + // Try to get precise position from slot layout (DOM-registered) + const slotKey = getSlotKey(String(node.id), slotIndex, isInput) + const slotLayout = layoutStore.getSlotLayout(slotKey) + if (slotLayout) { + return [slotLayout.position.x, slotLayout.position.y] } - // Use helper to calculate position - return isInput - ? calculateInputSlotPos(context, slotIndex) - : calculateOutputSlotPos(context, slotIndex) + // Fallback: derive position from node layout tree and slot model + const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value + + if (nodeLayout) { + // Create context from layout tree data + const context: SlotPositionContext = { + nodeX: nodeLayout.position.x, + nodeY: nodeLayout.position.y, + nodeWidth: nodeLayout.size.width, + nodeHeight: nodeLayout.size.height, + collapsed: node.flags.collapsed || false, + collapsedWidth: node._collapsed_width, + slotStartY: node.constructor.slot_start_y, + inputs: node.inputs, + outputs: node.outputs, + widgets: node.widgets + } + + // Use helper to calculate position + return isInput + ? calculateInputSlotPos(context, slotIndex) + : calculateOutputSlotPos(context, slotIndex) + } } - // Fallback: calculate directly from node properties if layout not available + // Fallback: calculate directly from node properties (legacy litegraph behavior) const context: SlotPositionContext = { nodeX: node.pos[0], nodeY: node.pos[1],