Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 1 addition & 5 deletions src/composables/graph/useVueNodeLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { app as comfyApp } from '@/scripts/app'

Expand All @@ -33,10 +32,7 @@ function useVueNodeLifecycleIndividual() {
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
id: node.id.toString(),
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [
number,
number
]
size: [node.size[0], node.size[1]] as [number, number]
}))
layoutStore.initializeFromLiteGraph(nodes)

Expand Down
14 changes: 9 additions & 5 deletions src/lib/litegraph/src/LGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,8 +755,10 @@ export class LGraph
let max_size = 100
let y = margin + LiteGraph.NODE_TITLE_HEIGHT
for (const node of column) {
node.pos[0] = layout == LiteGraph.VERTICAL_LAYOUT ? y : x
node.pos[1] = layout == LiteGraph.VERTICAL_LAYOUT ? x : y
node.setPos(
layout == LiteGraph.VERTICAL_LAYOUT ? y : x,
layout == LiteGraph.VERTICAL_LAYOUT ? x : y
)
const max_size_index = layout == LiteGraph.VERTICAL_LAYOUT ? 1 : 0
if (node.size[max_size_index] > max_size) {
max_size = node.size[max_size_index]
Expand Down Expand Up @@ -1691,7 +1693,10 @@ export class LGraph
)

//Correct for title height. It's included in bounding box, but not _posSize
subgraphNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2
subgraphNode.setPos(
subgraphNode.pos[0],
subgraphNode.pos[1] + LiteGraph.NODE_TITLE_HEIGHT / 2
)

// Add the subgraph node to the graph
this.add(subgraphNode)
Expand Down Expand Up @@ -1858,8 +1863,7 @@ export class LGraph

this.add(node, true)
node.configure(n_info)
node.pos[0] += offsetX
node.pos[1] += offsetY
node.setPos(node.pos[0] + offsetX, node.pos[1] + offsetY)
for (const input of node.inputs) {
input.link = null
}
Expand Down
65 changes: 23 additions & 42 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'

import { CanvasPointer } from './CanvasPointer'
Expand Down Expand Up @@ -2370,8 +2368,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const cloned = items?.created[0] as LGraphNode | undefined
if (!cloned) return

cloned.pos[0] += 5
cloned.pos[1] += 5
cloned.setPos(cloned.pos[0] + 5, cloned.pos[1] + 5)

if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
Expand Down Expand Up @@ -4055,31 +4052,30 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}

// Adjust positions
// Adjust positions - use move/setPos to ensure layout store is updated
const dx = position[0] - offsetX
const dy = position[1] - offsetY
for (const item of created) {
item.pos[0] += position[0] - offsetX
item.pos[1] += position[1] - offsetY
if (item instanceof LGraphNode) {
item.setPos(item.pos[0] + dx, item.pos[1] + dy)
} else if (item instanceof Reroute) {
item.move(dx, dy)
}
Comment on lines +4055 to +4063
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pasted groups aren’t offset with the paste position.
created includes LGraphGroup, but the offset loop only moves nodes and reroutes, so pasted groups will stay at the original coordinates. Please move groups alongside nodes/reroutes.

🔧 Suggested fix
for (const item of created) {
  if (item instanceof LGraphNode) {
    item.setPos(item.pos[0] + dx, item.pos[1] + dy)
+  } else if (item instanceof LGraphGroup) {
+    item.move(dx, dy, true)
  } else if (item instanceof Reroute) {
    item.move(dx, dy)
  }
}
🤖 Prompt for AI Agents
In `@src/lib/litegraph/src/LGraphCanvas.ts` around lines 4030 - 4038, The paste
offset loop in LGraphCanvas.ts moves only LGraphNode and Reroute items, so
LGraphGroup instances in created remain at original coordinates; update the loop
to also handle LGraphGroup by detecting items instanceof LGraphGroup and
shifting their position the same way as nodes (e.g., call the group's position
setter like setPos(item.pos[0] + dx, item.pos[1] + dy) or the appropriate move
method on LGraphGroup) so pasted groups are translated by dx/dy along with nodes
and reroutes.

}
Comment on lines 4058 to 4064
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine Reroute class definition
echo "=== Searching for Reroute class ==="
fd -t f "Reroute" src/lib/litegraph/src/

echo -e "\n=== Checking Reroute.ts for move() method ==="
rg -n "^\s*(move|setPos)\s*\(" src/lib/litegraph/src/Reroute.ts -A 5 2>/dev/null || echo "File not found or no matches"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 495


🏁 Script executed:

#!/bin/bash
# Find and examine LGraphGroup class definition
echo "=== Searching for LGraphGroup class ==="
fd -t f "LGraphGroup" src/lib/litegraph/src/

echo -e "\n=== Checking LGraphGroup.ts for move() method ==="
rg -n "^\s*(move|setPos)\s*\(" src/lib/litegraph/src/LGraphGroup.ts -A 5 2>/dev/null || echo "File not found or no matches"

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 523


🏁 Script executed:

#!/bin/bash
# Search for layoutStore usage related to reroutes
echo "=== Searching for layoutStore and queryRerouteAtPoint ==="
rg -n "layoutStore\.|queryRerouteAtPoint|updateReroute" src/lib/litegraph/src --max-count 20

echo -e "\n=== Checking for move() implementations in all litegraph source files ==="
rg -n "^\s*move\s*\(" src/lib/litegraph/src -A 3 --max-count 30

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3304


🏁 Script executed:

#!/bin/bash
# Get full Reroute.move() implementation
echo "=== Full Reroute.move() implementation ==="
sed -n '413,450p' src/lib/litegraph/src/Reroute.ts

echo -e "\n=== Full LGraphNode.move() implementation ==="
sed -n '2002,2050p' src/lib/litegraph/src/LGraphNode.ts

echo -e "\n=== Full LGraphGroup.move() implementation ==="
sed -n '227,280p' src/lib/litegraph/src/LGraphGroup.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4515


🏁 Script executed:

#!/bin/bash
# Check how layoutStore is updated after pos changes
echo "=== Search for updateReroute or similar in layoutStore context ==="
rg -n "updateReroute|invalidate.*[Rr]eroute|setReroutePos" src/lib/litegraph/src/

echo -e "\n=== Check Reroute setter/getter for pos ==="
rg -n "^\s*(get|set)\s+pos\|^\s*pos\s*=" src/lib/litegraph/src/Reroute.ts -A 3

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 383


🏁 Script executed:

#!/bin/bash
# Check the current code at lines 4013-4022 in context to see how items are used after positioning
echo "=== Context around lines 4013-4022 in LGraphCanvas.ts ==="
sed -n '4000,4050p' src/lib/litegraph/src/LGraphCanvas.ts

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1616


Use item.move(dx, dy, true) for all items to ensure layout store is updated.
Direct mutation of pos[0]/pos[1] bypasses the move() method, which for Reroute calls layoutMutations.moveReroute() to sync the layout store. This leaves stale reroute layout entries that break layoutStore.queryRerouteAtPoint(...) used for hit-testing and rendering.

Proposed change (routes all items through their own movement API)
-    for (const item of created) {
-      const newX = item.pos[0] + position[0] - offsetX
-      const newY = item.pos[1] + position[1] - offsetY
-      if (item instanceof LGraphNode) {
-        item.setPos(newX, newY)
-      } else {
-        item.pos[0] = newX
-        item.pos[1] = newY
-      }
-    }
+    const deltaX = position[0] - offsetX
+    const deltaY = position[1] - offsetY
+    for (const item of created) {
+      item.move(deltaX, deltaY, true)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const item of created) {
item.pos[0] += position[0] - offsetX
item.pos[1] += position[1] - offsetY
const newX = item.pos[0] + position[0] - offsetX
const newY = item.pos[1] + position[1] - offsetY
if (item instanceof LGraphNode) {
item.setPos(newX, newY)
} else {
item.pos[0] = newX
item.pos[1] = newY
}
}
const deltaX = position[0] - offsetX
const deltaY = position[1] - offsetY
for (const item of created) {
item.move(deltaX, deltaY, true)
}
🤖 Prompt for AI Agents
In @src/lib/litegraph/src/LGraphCanvas.ts around lines 4013 - 4022, The loop
currently sets positions directly (using LGraphNode.setPos(...) and direct pos
mutation) which bypasses item.move and leaves reroute layout entries stale;
replace both branches to compute dx = newX - item.pos[0], dy = newY -
item.pos[1] and call item.move(dx, dy, true) for every item (including instances
of LGraphNode and Reroute) so the movement API and layoutMutations.moveReroute()
are invoked and the layout store stays in sync.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jtydhr88 Does this comment seem plausible?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


// TODO: Report failures, i.e. `failedNodes`

const newPositions = created
.filter((item): item is LGraphNode => item instanceof LGraphNode)
.map((node) => {
const fullHeight = node.size?.[1] ?? 200
const layoutHeight = LiteGraph.vueNodesMode
? removeNodeTitleHeight(fullHeight)
: fullHeight
return {
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: layoutHeight
}
.map((node) => ({
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: node.size?.[1] ?? 200
}
})
}))

if (newPositions.length) layoutStore.setSource(LayoutSource.Canvas)
layoutStore.batchUpdateNodeBounds(newPositions)
Expand Down Expand Up @@ -6281,7 +6277,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
options
)
) {
node.pos[0] -= node.size[0] * 0.5
node.setPos(node.pos[0] - node.size[0] * 0.5, node.pos[1])
}
})
break
Expand Down Expand Up @@ -8569,27 +8565,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Apply batched node position updates
*/
private applyNodePositionUpdates(
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>,
mutations: ReturnType<typeof useLayoutMutations>
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>
): void {
for (const { node, newPos } of nodesToMove) {
// Update LiteGraph position first so next drag uses correct base position
node.pos[0] = newPos.x
node.pos[1] = newPos.y
// Then update layout store which will update Vue nodes
mutations.moveNode(node.id, newPos)
// setPos automatically syncs to layout store
node.setPos(newPos.x, newPos.y)
}
}

/**
* Initialize layout mutations with Canvas source
*/
private initLayoutMutations(): ReturnType<typeof useLayoutMutations> {
const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
return mutations
}

/**
* Collect all nodes that are children of groups in the selection
*/
Expand Down Expand Up @@ -8637,7 +8620,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
deltaX: number,
deltaY: number
) {
const mutations = this.initLayoutMutations()
const nodesInMovingGroups = this.collectNodesInGroups(allItems)
const nodesToMove: NewNodePosition[] = []

Expand All @@ -8663,12 +8645,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}

// Now apply all the node moves at once
this.applyNodePositionUpdates(nodesToMove, mutations)
this.applyNodePositionUpdates(nodesToMove)
}

repositionNodesVueMode(nodesToReposition: NewNodePosition[]) {
const mutations = this.initLayoutMutations()
this.applyNodePositionUpdates(nodesToReposition, mutations)
this.applyNodePositionUpdates(nodesToReposition)
}

/**
Expand Down
21 changes: 19 additions & 2 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,17 @@ export class LGraphNode

this._pos[0] = value[0]
this._pos[1] = value[1]

const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
mutations.moveNode(String(this.id), { x: value[0], y: value[1] })
}

/**
* Set the node position to an absolute location.
*/
setPos(x: number, y: number): void {
this.pos = [x, y]
}

public get size() {
Expand All @@ -497,6 +508,13 @@ export class LGraphNode

this._size[0] = value[0]
this._size[1] = value[1]

const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
mutations.resizeNode(String(this.id), {
width: value[0],
height: value[1]
})
}

/**
Expand Down Expand Up @@ -2020,8 +2038,7 @@ export class LGraphNode
return
}

this.pos[0] += deltaX
this.pos[1] += deltaY
this.pos = [this._pos[0] + deltaX, this._pos[1] + deltaY]
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/lib/litegraph/src/utils/arrange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ export function alignNodes(
})

for (const { node, newPos } of nodePositions) {
node.pos[0] = newPos.x
node.pos[1] = newPos.y
node.setPos(newPos.x, newPos.y)
}
return nodePositions
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('LGraphNode', () => {

expect(wrapper.element.style.getPropertyValue('--node-height')).toBe('')
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe(
'100px'
'130px'
)
})

Expand All @@ -219,7 +219,7 @@ describe('LGraphNode', () => {
})

expect(wrapper.element.style.getPropertyValue('--node-height')).toBe(
'100px'
'130px'
)
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe('')
})
Expand Down
53 changes: 44 additions & 9 deletions src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ import {
nextTick,
onErrorCaptured,
onMounted,
onUnmounted,
ref,
watch
} from 'vue'
Expand All @@ -188,6 +189,7 @@ import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
Expand Down Expand Up @@ -324,27 +326,60 @@ const handleContextMenu = (event: MouseEvent) => {
showNodeOptions(event)
}

onMounted(() => {
initSizeStyles()
})

/**
* Set initial DOM size from layout store, but respect intrinsic content minimum.
* Important: nodes can mount in a collapsed state, and the collapse watcher won't
* run initially. Match the collapsed runtime behavior by writing to the correct
* CSS variables on mount.
* Set initial DOM size from layout store.
*/
function initSizeStyles() {
const el = nodeContainerRef.value
const { width, height } = size.value
if (!el) return

const suffix = isCollapsed.value ? '-x' : ''
const fullHeight = height + LiteGraph.NODE_TITLE_HEIGHT

el.style.setProperty(`--node-width${suffix}`, `${width}px`)
el.style.setProperty(`--node-height${suffix}`, `${height}px`)
el.style.setProperty(`--node-height${suffix}`, `${fullHeight}px`)
}

/**
* Handle external size changes (e.g., from extensions calling node.setSize()).
* Updates CSS variables when layoutStore changes from Canvas/External source.
*/
function handleLayoutChange(change: {
source: LayoutSource
nodeIds: string[]
}) {
// Only handle Canvas or External source (extensions calling setSize)
if (
change.source !== LayoutSource.Canvas &&
change.source !== LayoutSource.External
)
return

if (!change.nodeIds.includes(nodeData.id)) return
if (layoutStore.isResizingVueNodes.value) return
if (isCollapsed.value) return

const el = nodeContainerRef.value
if (!el) return

const newSize = size.value
const fullHeight = newSize.height + LiteGraph.NODE_TITLE_HEIGHT
el.style.setProperty('--node-width', `${newSize.width}px`)
el.style.setProperty('--node-height', `${fullHeight}px`)
}

let unsubscribeLayoutChange: (() => void) | null = null

onMounted(() => {
initSizeStyles()
unsubscribeLayoutChange = layoutStore.onChange(handleLayoutChange)
})

onUnmounted(() => {
unsubscribeLayoutChange?.()
})

const baseResizeHandleClasses =
'absolute h-5 w-5 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'

Expand Down
Loading