Skip to content
Open
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
9a18d37
Add dragging test
benceruleanlu Sep 19, 2025
939cbe0
Litegraph? Never heard if it
benceruleanlu Sep 19, 2025
e3e1d2e
Add more test cases
benceruleanlu Sep 19, 2025
5c6c21c
Update test expectations [skip ci]
invalid-email-address Sep 19, 2025
f624940
Merge branch 'bl-tests' into bl-more-slots
benceruleanlu Sep 19, 2025
48f5087
More test cases v1
benceruleanlu Sep 19, 2025
369da53
review comments
benceruleanlu Sep 19, 2025
22a1c61
Merge remote-tracking branch 'origin/bl-tests' into bl-more-slots
benceruleanlu Sep 19, 2025
70651dc
Allow moving links and support reroutes
benceruleanlu Sep 21, 2025
c227d60
Add dragging input to input drags existing link test
benceruleanlu Sep 21, 2025
9d668a1
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 21, 2025
b99d70d
nit
benceruleanlu Sep 21, 2025
19c538c
Support dragging from output to output
benceruleanlu Sep 22, 2025
263b280
Add snapshot
benceruleanlu Sep 22, 2025
bef712e
o-o shift test
benceruleanlu Sep 22, 2025
e7f0ee4
Update test expectation
benceruleanlu Sep 22, 2025
20d136d
Switch to adapter approach
benceruleanlu Sep 22, 2025
3f4a806
clean up onPointerDown
benceruleanlu Sep 23, 2025
e136b89
Add reroute anchor tests
benceruleanlu Sep 23, 2025
6685e00
Fix double links
benceruleanlu Sep 23, 2025
0aa971b
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 23, 2025
9d32b4c
temp screenshots
benceruleanlu Sep 23, 2025
e879bd5
improve typing
benceruleanlu Sep 23, 2025
f348902
nit
benceruleanlu Sep 23, 2025
d780296
cleanup unused
benceruleanlu Sep 23, 2025
8eec7fb
huh?
benceruleanlu Sep 23, 2025
f99d8c1
I am the one who knocks
benceruleanlu Sep 23, 2025
65ec322
Those who type
benceruleanlu Sep 23, 2025
a2be36a
fix bad fallback and remove logging
benceruleanlu Sep 23, 2025
88cd60f
nit
benceruleanlu Sep 23, 2025
381d97a
nit
benceruleanlu Sep 23, 2025
e9ffce4
nit
benceruleanlu Sep 23, 2025
99aaa4e
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 23, 2025
57810b9
nit
benceruleanlu Sep 24, 2025
9b39835
refactor linkInteraction.spec.ts
benceruleanlu Sep 24, 2025
c050115
those who know
benceruleanlu Sep 24, 2025
839d8a5
sure
benceruleanlu Sep 25, 2025
4f6eaea
get nodeid and slotkey
benceruleanlu Sep 25, 2025
76c718e
Visually snap to node
benceruleanlu Sep 25, 2025
0f46452
Remove debug logging
benceruleanlu Sep 26, 2025
23f3e17
Try connecting to snapped first
benceruleanlu Sep 26, 2025
0627a71
those who know cont
benceruleanlu Sep 26, 2025
1ca3d75
nit
benceruleanlu Sep 26, 2025
ecc5bed
type
benceruleanlu Sep 26, 2025
0e33672
Merge remote-tracking branch 'origin/bl-more-slots' into bl-snap
benceruleanlu Sep 26, 2025
9de27ad
fix stale
benceruleanlu Sep 26, 2025
18b4f56
refactor candidatefromnodetarget
benceruleanlu Sep 26, 2025
4b95ef9
Implement caching and rAF
benceruleanlu Sep 26, 2025
8da5ae3
Add tests
benceruleanlu Sep 26, 2025
247e395
knip
benceruleanlu Sep 26, 2025
1c11dcc
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 27, 2025
f13a45c
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 27, 2025
21cc208
Merge remote-tracking branch 'origin/main' into bl-more-slots
benceruleanlu Sep 28, 2025
066a755
Update test expectations [skip ci]
invalid-email-address Sep 28, 2025
5e3c91f
Merge remote-tracking branch 'origin/bl-more-slots' into bl-snap
benceruleanlu Sep 28, 2025
a71b99d
Update test expectations [skip ci]
invalid-email-address Sep 29, 2025
d5a5621
Merge remote-tracking branch 'origin/main' into bl-snap
benceruleanlu Sep 30, 2025
d18a604
Merge remote-tracking branch 'origin/main' into bl-snap
benceruleanlu Sep 30, 2025
87e410d
Remove duplicated playwright snapshots
benceruleanlu Sep 30, 2025
35bc2f9
Update test expectations [skip ci]
invalid-email-address Sep 30, 2025
aed6a4e
ci sanity check
benceruleanlu Sep 30, 2025
768b6e5
Align links to slots in subgraphs (#5876)
benceruleanlu Oct 1, 2025
4d7c0d7
Merge origin/main into bl-snap: resolve conflicts
benceruleanlu Oct 3, 2025
d1f2734
[automated] Update test expectations
invalid-email-address Oct 3, 2025
4c985f7
benceruleanlu Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export class VueNodeHelpers {
return this.page.locator('[data-node-id]')
}

/**
* Get locator for a Vue node by its NodeId
*/
getNodeLocator(nodeId: string): Locator {
return this.page.locator(`[data-node-id="${nodeId}"]`)
}

/**
* Get locator for selected Vue node components (using visual selection indicators)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,4 +693,99 @@ test.describe('Vue Node Link Interaction', () => {
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
})

test('should snap to node center while dragging and link on drop', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()

// Start drag from CLIP output[0]
const clipOutputCenter = await getSlotCenter(
comfyPage.page,
clipNode.id,
0,
false
)

// Drag to the visual center of the KSampler Vue node (not a slot)
const samplerVue = comfyPage.vueNodes.getNodeLocator(String(samplerNode.id))
await expect(samplerVue).toBeVisible()
const samplerCenter = await getCenter(samplerVue)

await comfyMouse.move(clipOutputCenter)
await comfyMouse.drag(samplerCenter)

// During drag, the preview should snap/highlight a compatible input on KSampler
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-node.png')

// Drop to create the link
await comfyMouse.drop()
await comfyPage.nextFrame()

// Validate a link was created to one of KSampler's compatible inputs (1 or 2)
const linkOnInput1 = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
1
)
const linkOnInput2 = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
2
)

const linked = linkOnInput1 ?? linkOnInput2
expect(linked).not.toBeNull()
expect(linked?.originId).toBe(clipNode.id)
expect(linked?.targetId).toBe(samplerNode.id)
})

test('should snap to a specific compatible slot when targeting it', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()

// Drag from CLIP output[0] to KSampler input[2] (third slot) which is the
// second compatible input for CLIP
const clipOutputCenter = await getSlotCenter(
comfyPage.page,
clipNode.id,
0,
false
)
const samplerInput3Center = await getSlotCenter(
comfyPage.page,
samplerNode.id,
2,
true
)

await comfyMouse.move(clipOutputCenter)
await comfyMouse.drag(samplerInput3Center)

// Expect the preview to show snapping to the targeted slot
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-slot.png')

// Finish the connection
await comfyMouse.drop()
await comfyPage.nextFrame()

const linkDetails = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
2
)
expect(linkDetails).not.toBeNull()
expect(linkDetails).toMatchObject({
originId: clipNode.id,
targetId: samplerNode.id,
targetSlot: 2
})
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 6 additions & 2 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5617,7 +5617,9 @@ export class LGraphCanvas
const { link, inputNode, input } = resolved
if (!inputNode || !input) continue

const endPos = inputNode.getInputPos(link.target_slot)
const endPos = LiteGraph.vueNodesMode
? getSlotPosition(inputNode, link.target_slot, true)
: inputNode.getInputPos(link.target_slot)

this.#renderAllLinkSegments(
ctx,
Expand All @@ -5642,7 +5644,9 @@ export class LGraphCanvas
const { link, outputNode, output } = resolved
if (!outputNode || !output) continue

const startPos = outputNode.getOutputPos(link.origin_slot)
const startPos = LiteGraph.vueNodesMode
? getSlotPosition(outputNode, link.origin_slot, false)
: outputNode.getOutputPos(link.origin_slot)

this.#renderAllLinkSegments(
ctx,
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
const renderLinks = createLinkConnectorAdapter()?.renderLinks
if (!renderLinks || renderLinks.length === 0) return

const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
const to: ReadOnlyPoint = state.candidate?.compatible
? [state.candidate.layout.position.x, state.candidate.layout.position.y]
: [pointer.canvas.x, pointer.canvas.y]
ctx.save()
for (const link of renderLinks) {
const startDir = link.fromDirection ?? LinkDirection.RIGHT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { SlotLayout } from '@/renderer/core/layout/types'

interface PendingMoveData {
clientX: number
clientY: number
target: EventTarget | null
}

interface SlotLinkDragSession {
compatCache: Map<string, boolean>
nodePreferred: Map<
number,
{ index: number; key: string; layout: SlotLayout } | null
>
lastHoverSlotKey: string | null
lastHoverNodeId: number | null
lastCandidateKey: string | null
pendingMove: PendingMoveData | null
reset: () => void
dispose: () => void
}

export function createSlotLinkDragSession(): SlotLinkDragSession {
const state: SlotLinkDragSession = {
compatCache: new Map(),
nodePreferred: new Map(),
lastHoverSlotKey: null,
lastHoverNodeId: null,
lastCandidateKey: null,
pendingMove: null,
reset: () => {
state.compatCache = new Map()
state.nodePreferred = new Map()
state.lastHoverSlotKey = null
state.lastHoverNodeId = null
state.lastCandidateKey = null
state.pendingMove = null
},
dispose: () => {
state.reset()
}
Comment on lines +31 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this persist and be reset instead of just being dropped and garbage collected?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's because it will be alive for the lifetime of the composable

it's called as const dragSession = createSlotLinkDragSession()

}

return state
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,17 @@ import {
isSizeEqual
} from '@/renderer/core/layout/utils/geometry'
import { useNodeSlotRegistryStore } from '@/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore'
import { createRafBatch } from '@/utils/rafBatch'

// RAF batching
const pendingNodes = new Set<string>()
let rafId: number | null = null
const raf = createRafBatch(() => {
flushScheduledSlotLayoutSync()
})

function scheduleSlotLayoutSync(nodeId: string) {
pendingNodes.add(nodeId)
if (rafId == null) {
rafId = requestAnimationFrame(() => {
rafId = null
flushScheduledSlotLayoutSync()
})
}
raf.schedule()
}

function flushScheduledSlotLayoutSync() {
Expand Down
Loading