Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/composables/graph/useVueNodeLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 19 additions & 9 deletions src/lib/litegraph/src/LGraphCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions src/renderer/core/layout/store/layoutStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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[] = []
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/core/layout/sync/useLayoutSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
}
}

Expand Down
1 change: 1 addition & 0 deletions src/renderer/core/layout/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ComputedRef, Ref } from 'vue'
export enum LayoutSource {
Canvas = 'canvas',
Vue = 'vue',
DOM = 'dom',
External = 'external'
}

Expand Down
7 changes: 7 additions & 0 deletions src/renderer/core/layout/utils/nodeSizeUtil.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: I'd rather have fewer of these one-off utility modules, if we can consolidate things like this into named domain utilities.

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
112 changes: 107 additions & 5 deletions tests-ui/tests/renderer/core/layout/layoutStore.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading