Skip to content

Commit cd7310c

Browse files
benceruleanlugithub-actions
andauthored
feat(vue-nodes): snap link preview; connect on drop (#5780)
## Summary Snap link preview to the nearest compatible slot while dragging in Vue Nodes mode, and complete the connection on drop using the snapped target. Mirrors LiteGraph’s first-compatible-slot logic for node-level snapping and reuses the computed candidate for performance. ## Changes - Snap preview end to compatible slot - slot under cursor via `data-slot-key` fast-path - node under cursor via `findInputByType` / `findOutputByType` - Render path - `slotLinkPreviewRenderer.ts` now renders to `state.candidate.layout.position` - Complete on drop - Prefer `state.candidate` (no re-hit-testing) - Fallbacks: DOM slot → node first-compatible → reroute - Disconnects moving input link when dropped on canvas ## Review Focus - UX feel of snapping and drop completion (both directions) - Performance on large graphs (mousemove path is O(1) with dataset + single validation) - Edge cases: reroutes, moving existing links, collapsed nodes ## Screenshots (if applicable) https://github.com/user-attachments/assets/fbed0ae2-2231-473b-a05a-9aaf68e3f820 #5780 (snapping) <-- #5898 (drop on canvas + linkconnectoradapter refactor) <-- #5903 (fix reroute snapping) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5780-feat-vue-nodes-snap-link-preview-connect-on-drop-27a6d73d365081d89c8cf570e2049c89) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <[email protected]>
1 parent 4e7e6e2 commit cd7310c

21 files changed

+365
-36
lines changed

browser_tests/fixtures/VueNodeHelpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export class VueNodeHelpers {
1313
return this.page.locator('[data-node-id]')
1414
}
1515

16+
/**
17+
* Get locator for a Vue node by its NodeId
18+
*/
19+
getNodeLocator(nodeId: string): Locator {
20+
return this.page.locator(`[data-node-id="${nodeId}"]`)
21+
}
22+
1623
/**
1724
* Get locator for selected Vue node components (using visual selection indicators)
1825
*/
52 Bytes
Loading

browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,4 +693,99 @@ test.describe('Vue Node Link Interaction', () => {
693693
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
694694
}
695695
})
696+
697+
test('should snap to node center while dragging and link on drop', async ({
698+
comfyPage,
699+
comfyMouse
700+
}) => {
701+
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
702+
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
703+
expect(clipNode && samplerNode).toBeTruthy()
704+
705+
// Start drag from CLIP output[0]
706+
const clipOutputCenter = await getSlotCenter(
707+
comfyPage.page,
708+
clipNode.id,
709+
0,
710+
false
711+
)
712+
713+
// Drag to the visual center of the KSampler Vue node (not a slot)
714+
const samplerVue = comfyPage.vueNodes.getNodeLocator(String(samplerNode.id))
715+
await expect(samplerVue).toBeVisible()
716+
const samplerCenter = await getCenter(samplerVue)
717+
718+
await comfyMouse.move(clipOutputCenter)
719+
await comfyMouse.drag(samplerCenter)
720+
721+
// During drag, the preview should snap/highlight a compatible input on KSampler
722+
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-node.png')
723+
724+
// Drop to create the link
725+
await comfyMouse.drop()
726+
await comfyPage.nextFrame()
727+
728+
// Validate a link was created to one of KSampler's compatible inputs (1 or 2)
729+
const linkOnInput1 = await getInputLinkDetails(
730+
comfyPage.page,
731+
samplerNode.id,
732+
1
733+
)
734+
const linkOnInput2 = await getInputLinkDetails(
735+
comfyPage.page,
736+
samplerNode.id,
737+
2
738+
)
739+
740+
const linked = linkOnInput1 ?? linkOnInput2
741+
expect(linked).not.toBeNull()
742+
expect(linked?.originId).toBe(clipNode.id)
743+
expect(linked?.targetId).toBe(samplerNode.id)
744+
})
745+
746+
test('should snap to a specific compatible slot when targeting it', async ({
747+
comfyPage,
748+
comfyMouse
749+
}) => {
750+
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
751+
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
752+
expect(clipNode && samplerNode).toBeTruthy()
753+
754+
// Drag from CLIP output[0] to KSampler input[2] (third slot) which is the
755+
// second compatible input for CLIP
756+
const clipOutputCenter = await getSlotCenter(
757+
comfyPage.page,
758+
clipNode.id,
759+
0,
760+
false
761+
)
762+
const samplerInput3Center = await getSlotCenter(
763+
comfyPage.page,
764+
samplerNode.id,
765+
2,
766+
true
767+
)
768+
769+
await comfyMouse.move(clipOutputCenter)
770+
await comfyMouse.drag(samplerInput3Center)
771+
772+
// Expect the preview to show snapping to the targeted slot
773+
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-slot.png')
774+
775+
// Finish the connection
776+
await comfyMouse.drop()
777+
await comfyPage.nextFrame()
778+
779+
const linkDetails = await getInputLinkDetails(
780+
comfyPage.page,
781+
samplerNode.id,
782+
2
783+
)
784+
expect(linkDetails).not.toBeNull()
785+
expect(linkDetails).toMatchObject({
786+
originId: clipNode.id,
787+
targetId: samplerNode.id,
788+
targetSlot: 2
789+
})
790+
})
696791
})
-4 Bytes
Loading
-2 Bytes
Loading
-26 Bytes
Loading
-45 Bytes
Loading
49.2 KB
Loading
48.3 KB
Loading
-53.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)