From 6cf0357b3e54d6e28ab7e68669b22b32291c0597 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 12 Feb 2026 05:38:18 -0500 Subject: [PATCH] fix(vueNodes): sync node size changes from extensions to Vue components (#7993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When extensions like KJNodes call node.setSize(), the Vue component now properly updates its CSS variables to reflect the new size. ## Changes: - LGraphNode pos/size setters now always sync to layoutStore with Canvas source - LGraphNode.vue listens to layoutStore changes and updates CSS variables - Fixed height calculation to account for NODE_TITLE_HEIGHT difference - Removed _syncToLayoutStore flag (simplified - layoutStore ignores non-existent nodes) - Use setPos() helper method instead of direct pos[0]/pos[1] assignment ## Screenshots (if applicable) before https://github.com/user-attachments/assets/236a173a-e41d-485b-8c63-5c28ef1c69bf after https://github.com/user-attachments/assets/5fc3f7e4-35c7-40e1-81ac-38a35ee0ac1b ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7993-fix-vueNodes-sync-node-size-changes-from-extensions-to-Vue-components-2e76d73d3650815799c5f2d9d8c7dcbf) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- src/lib/litegraph/src/LGraph.ts | 14 ++-- .../src/LGraphCanvas.slotHitDetection.test.ts | 4 +- src/lib/litegraph/src/LGraphCanvas.ts | 80 +++++++------------ src/lib/litegraph/src/LGraphNode.ts | 21 ++++- src/lib/litegraph/src/utils/arrange.ts | 3 +- .../vueNodes/components/LGraphNode.vue | 56 ++++++++++--- 6 files changed, 104 insertions(+), 74 deletions(-) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index a51599e4c1..8e772d6335 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -776,8 +776,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] @@ -1759,7 +1761,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) @@ -1926,8 +1931,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 } diff --git a/src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts b/src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts index f61a56b87f..faf878b0cc 100644 --- a/src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts +++ b/src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts @@ -13,7 +13,9 @@ vi.mock('@/renderer/core/layout/store/layoutStore', () => ({ querySlotAtPoint: vi.fn(), queryRerouteAtPoint: vi.fn(), getNodeLayoutRef: vi.fn(() => ({ value: null })), - getSlotLayout: vi.fn() + getSlotLayout: vi.fn(), + setSource: vi.fn(), + batchUpdateNodeBounds: vi.fn() } })) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 72e776644b..3579aba9df 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -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' @@ -2396,8 +2394,7 @@ export class LGraphCanvas implements CustomEventDispatcher 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) => { @@ -3581,19 +3578,14 @@ export class LGraphCanvas implements CustomEventDispatcher if (dragEvent) { this.adjustMouseEvent(dragEvent) const e = dragEvent as CanvasPointerEvent - node.pos[0] = e.canvasX - node.size[0] / 2 - node.pos[1] = e.canvasY + 10 + node.setPos(e.canvasX - node.size[0] / 2, e.canvasY + 10) // Update last_mouse to prevent jump on first drag move this.last_mouse = [e.clientX, e.clientY] } else { - node.pos[0] = this.graph_mouse[0] - node.size[0] / 2 - node.pos[1] = this.graph_mouse[1] + 10 - } - - // Sync position to layout store for Vue node rendering - if (LiteGraph.vueNodesMode) { - const mutations = this.initLayoutMutations() - mutations.moveNode(node.id, { x: node.pos[0], y: node.pos[1] }) + node.setPos( + this.graph_mouse[0] - node.size[0] / 2, + this.graph_mouse[1] + 10 + ) } this.state.ghostNodeId = node.id @@ -4162,31 +4154,30 @@ export class LGraphCanvas implements CustomEventDispatcher } } - // 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) + } } // 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) @@ -6407,7 +6398,7 @@ export class LGraphCanvas implements CustomEventDispatcher options ) ) { - node.pos[0] -= node.size[0] * 0.5 + node.setPos(node.pos[0] - node.size[0] * 0.5, node.pos[1]) } }) break @@ -8695,27 +8686,14 @@ export class LGraphCanvas implements CustomEventDispatcher * Apply batched node position updates */ private applyNodePositionUpdates( - nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>, - mutations: ReturnType + 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 { - const mutations = useLayoutMutations() - mutations.setSource(LayoutSource.Canvas) - return mutations - } - /** * Collect all nodes that are children of groups in the selection */ @@ -8763,7 +8741,6 @@ export class LGraphCanvas implements CustomEventDispatcher deltaX: number, deltaY: number ) { - const mutations = this.initLayoutMutations() const nodesInMovingGroups = this.collectNodesInGroups(allItems) const nodesToMove: NewNodePosition[] = [] @@ -8789,12 +8766,11 @@ export class LGraphCanvas implements CustomEventDispatcher } // 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) } /** diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index c329518234..8f2d11340c 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -490,6 +490,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() { @@ -501,6 +512,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] + }) } /** @@ -2032,8 +2050,7 @@ export class LGraphNode return } - this.pos[0] += deltaX - this.pos[1] += deltaY + this.pos = [this._pos[0] + deltaX, this._pos[1] + deltaY] } /** diff --git a/src/lib/litegraph/src/utils/arrange.ts b/src/lib/litegraph/src/utils/arrange.ts index 9a7e7ab8c5..7408fa3a68 100644 --- a/src/lib/litegraph/src/utils/arrange.ts +++ b/src/lib/litegraph/src/utils/arrange.ts @@ -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 } diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 9abeac96e7..ca88eec192 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -199,6 +199,7 @@ import { nextTick, onErrorCaptured, onMounted, + onUnmounted, ref, watch } from 'vue' @@ -223,6 +224,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue' +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' @@ -362,15 +364,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 @@ -378,14 +373,51 @@ function initSizeStyles() { 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 + LiteGraph.NODE_TITLE_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'