fix(vueNodes): sync node size changes from extensions to Vue components

This commit is contained in:
Terry Jia
2026-01-24 08:53:17 -05:00
parent 1b1356951e
commit cda9f52fb5
6 changed files with 94 additions and 63 deletions

View File

@@ -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'
@@ -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)

View File

@@ -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]
@@ -1681,7 +1683,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)
@@ -1848,8 +1853,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
}

View File

@@ -4,10 +4,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'
@@ -2354,8 +2352,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) => {
@@ -4032,29 +4029,29 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Adjust positions
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
}
}
// 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)
@@ -6244,7 +6241,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
@@ -8532,27 +8529,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
*/
@@ -8600,7 +8584,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
deltaX: number,
deltaY: number
) {
const mutations = this.initLayoutMutations()
const nodesInMovingGroups = this.collectNodesInGroups(allItems)
const nodesToMove: NewNodePosition[] = []
@@ -8626,11 +8609,10 @@ 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)
}
}

View File

@@ -472,6 +472,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() {
@@ -483,6 +494,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]
})
}
/**
@@ -1993,8 +2011,7 @@ export class LGraphNode
return
}
this.pos[0] += deltaX
this.pos[1] += deltaY
this.pos = [this._pos[0] + deltaX, this._pos[1] + deltaY]
}
/**

View File

@@ -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
}

View File

@@ -164,6 +164,7 @@ import {
nextTick,
onErrorCaptured,
onMounted,
onUnmounted,
ref,
watch
} from 'vue'
@@ -186,6 +187,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'
@@ -322,15 +324,8 @@ 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
@@ -343,6 +338,44 @@ function initSizeStyles() {
el.style.setProperty(`--node-height${suffix}`, `${height}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
el.style.setProperty('--node-width', `${newSize.width}px`)
el.style.setProperty('--node-height', `${newSize.height}px`)
}
let unsubscribeLayoutChange: (() => void) | null = null
onMounted(() => {
initSizeStyles()
unsubscribeLayoutChange = layoutStore.onChange(handleLayoutChange)
})
onUnmounted(() => {
unsubscribeLayoutChange?.()
})
const baseResizeHandleClasses =
'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'