diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index d19fe37089..c642e6bed4 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index 137f58ea5b..2180922d9a 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index cea0dd1bdb..b3faaced03 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 6d3cd70e6f..b66692cdb2 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 1831a64652..adf81b55c4 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 6af9325a82..9ded7395f6 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index d145d88903..36cdb4ffba 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 53162e19f5..31a6219b4b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 5890591363..4951feb36c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index cac66743b2..fdfd695397 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 70daf6c8e9..2df57ff550 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 5a8a3b9255..3a004a7b86 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 2aa7952dde..3049bf7192 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index 49535a3ed2..73c8450986 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index 4fcd2d7c6c..cf91eb87b2 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts index 263df3b792..dfc2ac06aa 100644 --- a/src/composables/graph/useVueNodeLifecycle.ts +++ b/src/composables/graph/useVueNodeLifecycle.ts @@ -10,6 +10,7 @@ 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' import { useToastStore } from '@/platform/updates/common/toastStore' @@ -38,7 +39,10 @@ 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], node.size[1]] as [number, number] + size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [ + number, + number + ] })) layoutStore.initializeFromLiteGraph(nodes) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index fb21e72b53..d1d2062c94 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -7,6 +7,7 @@ import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculatio 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 { CanvasPointer } from './CanvasPointer' import type { ContextMenu } from './ContextMenu' @@ -4043,16 +4044,25 @@ export class LGraphCanvas // TODO: Report failures, i.e. `failedNodes` - const newPositions = created.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 - } - })) + 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 + } + } + }) + if (newPositions.length) layoutStore.setSource(LayoutSource.Canvas) layoutStore.batchUpdateNodeBounds(newPositions) this.selectItems(created) diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index a3449ce823..aeb21f32bb 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -9,6 +9,8 @@ import { computed, customRef, ref } from 'vue' import type { ComputedRef, Ref } from 'vue' import * as Y from 'yjs' +import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' + import { ACTOR_CONFIG } from '@/renderer/core/layout/constants' import { LayoutSource } from '@/renderer/core/layout/types' import type { @@ -1414,8 +1416,8 @@ class LayoutStoreImpl implements LayoutStore { batchUpdateNodeBounds(updates: NodeBoundsUpdate[]): void { if (updates.length === 0) return - // Set source to Vue for these DOM-driven updates const originalSource = this.currentSource + const shouldNormalizeHeights = originalSource === LayoutSource.DOM this.currentSource = LayoutSource.Vue const nodeIds: NodeId[] = [] @@ -1426,8 +1428,15 @@ class LayoutStoreImpl implements LayoutStore { if (!ynode) continue const currentLayout = yNodeToLayout(ynode) + const normalizedBounds = shouldNormalizeHeights + ? { + ...bounds, + height: removeNodeTitleHeight(bounds.height) + } + : bounds + boundsRecord[nodeId] = { - bounds, + bounds: normalizedBounds, previousBounds: currentLayout.bounds } nodeIds.push(nodeId) diff --git a/src/renderer/core/layout/sync/useLayoutSync.ts b/src/renderer/core/layout/sync/useLayoutSync.ts index c3cbb9ebd7..b51aeafeee 100644 --- a/src/renderer/core/layout/sync/useLayoutSync.ts +++ b/src/renderer/core/layout/sync/useLayoutSync.ts @@ -8,6 +8,7 @@ import { onUnmounted, ref } from 'vue' import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { addNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' /** * Composable for syncing LiteGraph with the Layout system @@ -43,12 +44,13 @@ export function useLayoutSync() { liteNode.pos[1] = layout.position.y } + const targetHeight = addNodeTitleHeight(layout.size.height) if ( liteNode.size[0] !== layout.size.width || - liteNode.size[1] !== layout.size.height + liteNode.size[1] !== targetHeight ) { // Use setSize() to trigger onResize callback - liteNode.setSize([layout.size.width, layout.size.height]) + liteNode.setSize([layout.size.width, targetHeight]) } } diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index 0af4112c71..4332059f5e 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -10,6 +10,7 @@ import type { ComputedRef, Ref } from 'vue' export enum LayoutSource { Canvas = 'canvas', Vue = 'vue', + DOM = 'dom', External = 'external' } diff --git a/src/renderer/core/layout/utils/nodeSizeUtil.ts b/src/renderer/core/layout/utils/nodeSizeUtil.ts new file mode 100644 index 0000000000..811240cd09 --- /dev/null +++ b/src/renderer/core/layout/utils/nodeSizeUtil.ts @@ -0,0 +1,7 @@ +import { LiteGraph } from '@/lib/litegraph/src/litegraph' + +export const removeNodeTitleHeight = (height: number) => + Math.max(0, height - (LiteGraph.NODE_TITLE_HEIGHT || 0)) + +export const addNodeTitleHeight = (height: number) => + height + LiteGraph.NODE_TITLE_HEIGHT diff --git a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts index 9b18e1582c..4d4e274d3c 100644 --- a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts @@ -107,7 +107,7 @@ const resizeObserver = new ResizeObserver((entries) => { x: topLeftCanvas.x, y: topLeftCanvas.y + LiteGraph.NODE_TITLE_HEIGHT, width: Math.max(0, width), - height: Math.max(0, height - LiteGraph.NODE_TITLE_HEIGHT) + height: Math.max(0, height) } let updates = updatesByType.get(elementType) @@ -123,8 +123,7 @@ const resizeObserver = new ResizeObserver((entries) => { } } - // Set source to Vue before processing DOM-driven updates - layoutStore.setSource(LayoutSource.Vue) + layoutStore.setSource(LayoutSource.DOM) // Flush per-type for (const [type, updates] of updatesByType) { diff --git a/tests-ui/tests/renderer/core/layout/layoutStore.test.ts b/tests-ui/tests/renderer/core/layout/layoutStore.test.ts index 993bde8664..ff7ee1fa06 100644 --- a/tests-ui/tests/renderer/core/layout/layoutStore.test.ts +++ b/tests-ui/tests/renderer/core/layout/layoutStore.test.ts @@ -1,11 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import { - type LayoutChange, - LayoutSource, - type NodeLayout -} from '@/renderer/core/layout/types' +import { LayoutSource } from '@/renderer/core/layout/types' +import type { LayoutChange, NodeLayout } from '@/renderer/core/layout/types' describe('layoutStore CRDT operations', () => { beforeEach(() => { @@ -304,4 +302,108 @@ describe('layoutStore CRDT operations', () => { expect(recentOps.length).toBeGreaterThanOrEqual(1) expect(recentOps[0].type).toBe('moveNode') }) + + it('normalizes DOM-sourced heights before storing', () => { + const nodeId = 'dom-node' + const layout = createTestNode(nodeId) + + layoutStore.applyOperation({ + type: 'createNode', + entity: 'node', + nodeId, + layout, + timestamp: Date.now(), + source: LayoutSource.External, + actor: 'test' + }) + + layoutStore.setSource(LayoutSource.DOM) + layoutStore.batchUpdateNodeBounds([ + { + nodeId, + bounds: { + x: layout.bounds.x, + y: layout.bounds.y, + width: layout.size.width, + height: layout.size.height + LiteGraph.NODE_TITLE_HEIGHT + } + } + ]) + + const nodeRef = layoutStore.getNodeLayoutRef(nodeId) + expect(nodeRef.value?.size.height).toBe(layout.size.height) + expect(nodeRef.value?.size.width).toBe(layout.size.width) + expect(nodeRef.value?.position).toEqual(layout.position) + }) + + it('normalizes very small DOM-sourced heights safely', () => { + const nodeId = 'small-dom-node' + const layout = createTestNode(nodeId) + layout.size.height = 10 + + layoutStore.applyOperation({ + type: 'createNode', + entity: 'node', + nodeId, + layout, + timestamp: Date.now(), + source: LayoutSource.External, + actor: 'test' + }) + + layoutStore.setSource(LayoutSource.DOM) + layoutStore.batchUpdateNodeBounds([ + { + nodeId, + bounds: { + x: layout.bounds.x, + y: layout.bounds.y, + width: layout.size.width, + height: layout.size.height + LiteGraph.NODE_TITLE_HEIGHT + } + } + ]) + + const nodeRef = layoutStore.getNodeLayoutRef(nodeId) + expect(nodeRef.value?.size.height).toBeGreaterThanOrEqual(0) + }) + + it('handles undefined NODE_TITLE_HEIGHT without NaN results', () => { + const nodeId = 'undefined-title-height' + const layout = createTestNode(nodeId) + + layoutStore.applyOperation({ + type: 'createNode', + entity: 'node', + nodeId, + layout, + timestamp: Date.now(), + source: LayoutSource.External, + actor: 'test' + }) + + const originalTitleHeight = LiteGraph.NODE_TITLE_HEIGHT + // @ts-expect-error – intentionally simulate undefined runtime value + LiteGraph.NODE_TITLE_HEIGHT = undefined + + try { + layoutStore.setSource(LayoutSource.DOM) + layoutStore.batchUpdateNodeBounds([ + { + nodeId, + bounds: { + x: layout.bounds.x, + y: layout.bounds.y, + width: layout.size.width, + height: layout.size.height + } + } + ]) + + const nodeRef = layoutStore.getNodeLayoutRef(nodeId) + expect(nodeRef.value?.size.height).toBe(layout.size.height) + } finally { + LiteGraph.NODE_TITLE_HEIGHT = originalTitleHeight + } + }) }) diff --git a/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts index f9c669cd4e..e22ec69703 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts @@ -5,7 +5,9 @@ import { LayoutSource } from '@/renderer/core/layout/types' import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' // Mock the layout mutations module -vi.mock('@/renderer/core/layout/operations/layoutMutations') +vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({ + useLayoutMutations: vi.fn() +})) const mockedUseLayoutMutations = vi.mocked(useLayoutMutations)