Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 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
47781c7
Add more ts files to allowDefaultProject
benceruleanlu Oct 2, 2025
f198e13
Temporarily increase playwright timeout
benceruleanlu Oct 2, 2025
6cc0a1e
Temporarily regenerate screenshot expectations
benceruleanlu Oct 2, 2025
634dec5
Export interface SlotLinkDragSession
benceruleanlu Oct 2, 2025
bae44be
Remove slotLinkPreviewRenderer
benceruleanlu Oct 2, 2025
f271fed
Add more test cases
benceruleanlu Oct 2, 2025
e04bf8a
Make linkconnector use the canonical linkconnector
benceruleanlu Oct 2, 2025
d2f3051
add canvasPointerEvent helper
benceruleanlu Oct 2, 2025
fafd374
Add linkDropOrchestrator
benceruleanlu Oct 2, 2025
d9a283b
Add canvas drop to useSlotLinkInteraction.ts
benceruleanlu Oct 3, 2025
b00e429
Revert temporary playwright timeout increase
benceruleanlu Oct 3, 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
2ddb8f3
Merge remote-tracking branch 'origin/bl-snap' into bl-linkslot-refactor
benceruleanlu Oct 3, 2025
89a2315
[automated] Update test expectations
invalid-email-address Oct 3, 2025
8f438d6
Fix reroute links not snapping correctly (#5903)
benceruleanlu Oct 7, 2025
871da43
Merge remote-tracking branch 'origin/main' into bl-linkslot-refactor
benceruleanlu Oct 7, 2025
fd22e3f
Renamings for clarity, nodeID to standardized string, and cache guard…
benceruleanlu Oct 7, 2025
1b97b4e
[automated] Update test expectations
invalid-email-address Oct 7, 2025
2aca7c8
Rename and extractions for clarity
benceruleanlu Oct 7, 2025
3f84bb0
Merge remote-tracking branch 'origin/main' into bl-linkslot-refactor
benceruleanlu Oct 8, 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
Original file line number Diff line number Diff line change
Expand Up @@ -788,4 +788,171 @@ test.describe('Vue Node Link Interaction', () => {
targetSlot: 2
})
})

test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)

const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()

const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)

const dropPos = { x: outputCenter.x + 180, y: outputCenter.y - 140 }

await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}

// Context menu should be visible
const contextMenu = comfyPage.page.locator('.litecontextmenu')
await expect(contextMenu).toBeVisible()

// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()

// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
})

test('Context menu -> Search pre-filters by link type and connects after selection', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')

const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()

const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 200, y: outputCenter.y - 120 }

await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}

// Open Search from the context menu
await comfyPage.clickContextMenuItem('Search')

// Search box opens with prefilled type filter based on link type (LATENT)
await expect(comfyPage.searchBox.input).toBeVisible()
const chips = comfyPage.searchBox.filterChips
// Ensure at least one filter chip exists and it matches the link type
const chipCount = await chips.count()
expect(chipCount).toBeGreaterThan(0)
await expect(chips.first()).toContainText('LATENT')

// Choose a compatible node and verify it auto-connects
await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode')
await comfyPage.nextFrame()

// KSampler output should now have an outgoing link
const samplerOutput = await samplerNode.getOutput(0)
expect(await samplerOutput.getLinkCount()).toBe(1)

// One of the VAEDecode nodes should have an incoming link on input[0]
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
if (details) {
expect(details.originId).toBe(samplerNode.id)
linked = true
break
}
}
expect(linked).toBe(true)
})

test('Search box opens on Shift-drop and connects after selection', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')

const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(samplerNode).toBeTruthy()

const outputCenter = await getSlotCenter(
comfyPage.page,
samplerNode.id,
0,
false
)
const dropPos = { x: outputCenter.x + 140, y: outputCenter.y - 100 }

await comfyMouse.move(outputCenter)
await comfyPage.page.keyboard.down('Shift')
try {
await comfyMouse.drag(dropPos)
await comfyMouse.drop()
} finally {
await comfyPage.page.keyboard.up('Shift').catch(() => {})
}

// Search box should open directly
await expect(comfyPage.searchBox.input).toBeVisible()
await expect(comfyPage.searchBox.filterChips.first()).toContainText(
'LATENT'
)

// Select a compatible node and verify connection
await comfyPage.searchBox.fillAndSelectFirstNode('VAEDecode')
await comfyPage.nextFrame()

const samplerOutput = await samplerNode.getOutput(0)
expect(await samplerOutput.getLinkCount()).toBe(1)

const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
if (details) {
expect(details.originId).toBe(samplerNode.id)
linked = true
break
}
}
expect(linked).toBe(true)
})
})
})
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { attachSlotLinkPreviewRenderer } from '@/renderer/core/canvas/links/slotLinkPreviewRenderer'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
Expand Down Expand Up @@ -401,7 +400,6 @@ onMounted(async () => {

// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
attachSlotLinkPreviewRenderer(comfyApp.canvas)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false
Expand Down
26 changes: 21 additions & 5 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3318,7 +3318,15 @@ export class LGraphCanvas

if (slot && linkConnector.isInputValidDrop(node, slot)) {
highlightInput = slot
highlightPos = node.getInputSlotPos(slot)
if (LiteGraph.vueNodesMode) {
const idx = node.inputs.indexOf(slot)
highlightPos =
idx !== -1
? getSlotPosition(node, idx, true)
: node.getInputSlotPos(slot)
} else {
highlightPos = node.getInputSlotPos(slot)
}
linkConnector.overWidget = overWidget
}
}
Expand All @@ -3330,7 +3338,9 @@ export class LGraphCanvas
const result = node.findInputByType(firstLink.fromSlot.type)
if (result) {
highlightInput = result.slot
highlightPos = node.getInputSlotPos(result.slot)
highlightPos = LiteGraph.vueNodesMode
? getSlotPosition(node, result.index, true)
: node.getInputSlotPos(result.slot)
}
} else if (
inputId != -1 &&
Expand All @@ -3355,7 +3365,9 @@ export class LGraphCanvas
if (inputId === -1 && outputId === -1) {
const result = node.findOutputByType(firstLink.fromSlot.type)
if (result) {
highlightPos = node.getOutputPos(result.index)
highlightPos = LiteGraph.vueNodesMode
? getSlotPosition(node, result.index, false)
: node.getOutputPos(result.index)
}
} else {
// check if I have a slot below de mouse
Expand Down Expand Up @@ -5727,7 +5739,9 @@ export class LGraphCanvas
if (!node) continue

const startPos = firstReroute.pos
const endPos = node.getInputPos(link.target_slot)
const endPos: Point = LiteGraph.vueNodesMode
? getSlotPosition(node, link.target_slot, true)
: node.getInputPos(link.target_slot)
const endDirection = node.inputs[link.target_slot]?.dir

firstReroute._dragging = true
Expand All @@ -5746,7 +5760,9 @@ export class LGraphCanvas
const node = graph.getNodeById(link.origin_id)
if (!node) continue

const startPos = node.getOutputPos(link.origin_slot)
const startPos: Point = LiteGraph.vueNodesMode
? getSlotPosition(node, link.origin_slot, false)
: node.getOutputPos(link.origin_slot)
const endPos = reroute.pos
const startDirection = node.outputs[link.origin_slot]?.dir

Expand Down
9 changes: 6 additions & 3 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3290,11 +3290,14 @@ export class LGraphNode
* Gets the position of an output slot, in graph co-ordinates.
*
* This method is preferred over the legacy {@link getConnectionPos} method.
* @param slot Output slot index
* @param outputSlotIndex Output slot index
* @returns Position of the output slot
*/
getOutputPos(slot: number): Point {
return calculateOutputSlotPos(this.#getSlotPositionContext(), slot)
getOutputPos(outputSlotIndex: number): Point {
return calculateOutputSlotPos(
this.#getSlotPositionContext(),
outputSlotIndex
)
}

/** @inheritdoc */
Expand Down
74 changes: 74 additions & 0 deletions src/renderer/core/canvas/interaction/canvasPointerEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import type {
CanvasPointerEvent,
CanvasPointerExtensions
} from '@/lib/litegraph/src/types/events'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'

type PointerOffsets = {
x: number
y: number
}

const pointerHistory = new Map<number, PointerOffsets>()

const defineEnhancements = (
event: PointerEvent,
enhancement: CanvasPointerExtensions
) => {
Object.defineProperties(event, {
canvasX: { value: enhancement.canvasX, configurable: true, writable: true },
canvasY: { value: enhancement.canvasY, configurable: true, writable: true },
deltaX: { value: enhancement.deltaX, configurable: true, writable: true },
deltaY: { value: enhancement.deltaY, configurable: true, writable: true },
safeOffsetX: {
value: enhancement.safeOffsetX,
configurable: true,
writable: true
},
safeOffsetY: {
value: enhancement.safeOffsetY,
configurable: true,
writable: true
}
})
}

const createEnhancement = (event: PointerEvent): CanvasPointerExtensions => {
const conversion = useSharedCanvasPositionConversion()
conversion.update()

const [canvasX, canvasY] = conversion.clientPosToCanvasPos([
event.clientX,
event.clientY
])

const canvas = useCanvasStore().getCanvas()
const { offset, scale } = canvas.ds

const [originClientX, originClientY] = conversion.canvasPosToClientPos([0, 0])
const left = originClientX - offset[0] * scale
const top = originClientY - offset[1] * scale

const safeOffsetX = event.clientX - left
const safeOffsetY = event.clientY - top

const previous = pointerHistory.get(event.pointerId)
const deltaX = previous ? safeOffsetX - previous.x : 0
const deltaY = previous ? safeOffsetY - previous.y : 0
pointerHistory.set(event.pointerId, { x: safeOffsetX, y: safeOffsetY })

return { canvasX, canvasY, deltaX, deltaY, safeOffsetX, safeOffsetY }
}

export const toCanvasPointerEvent = <T extends PointerEvent>(
event: T
): T & CanvasPointerEvent => {
const enhancement = createEnhancement(event)
defineEnhancements(event, enhancement)
return event as T & CanvasPointerEvent
}

export const clearCanvasPointerHistory = (pointerId: number) => {
pointerHistory.delete(pointerId)
}
Loading
Loading