diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts index 648427a51..2157f008f 100644 --- a/src/composables/graph/useVueNodeLifecycle.ts +++ b/src/composables/graph/useVueNodeLifecycle.ts @@ -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) diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 49ad35101..44ea53b35 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -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 } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 40893174f..9c9c9455d 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -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 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 // 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 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 * 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 */ @@ -8600,7 +8584,6 @@ export class LGraphCanvas implements CustomEventDispatcher 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 } // 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 1ff503cdc..f9205bfc5 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -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] } /** 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 d602e4062..e07a653ff 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -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'