From 26c682f44f078cd0c15fc33e18b01f91a2796ae3 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sat, 7 Feb 2026 14:06:37 -0500 Subject: [PATCH] fix(vueNodes): sync node size changes from extensions to Vue components --- 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 d26734664..ca3ef81fa 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -765,8 +765,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] @@ -1733,7 +1735,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) @@ -1900,8 +1905,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 f61a56b87..faf878b0c 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 130a27809..878cc2284 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 baf6a4419..b7c0f62c5 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -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() { @@ -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] + }) } /** @@ -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] } /** diff --git a/src/lib/litegraph/src/utils/arrange.ts b/src/lib/litegraph/src/utils/arrange.ts index 9a7e7ab8c..7408fa3a6 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 e459865ab..693e00357 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -167,6 +167,7 @@ import { nextTick, onErrorCaptured, onMounted, + onUnmounted, ref, watch } from 'vue' @@ -189,6 +190,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' @@ -327,15 +329,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,14 +338,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'