Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dbd76a0
fix: make ensureCorrectLayoutScale idempotent to prevent node layout …
DrJKL Mar 9, 2026
98b77db
refactor: simplify ensureCorrectLayoutScale to one-time normalizer
DrJKL Mar 9, 2026
9e72a6e
fix: sync layout change notifications synchronously
DrJKL Mar 9, 2026
751cd9b
refactor: remove unused project* exports and simplify review findings
DrJKL Mar 10, 2026
56829f2
fix: extract MIN_NODE_WIDTH constant and sync inner wrapper min-width
DrJKL Mar 10, 2026
31b8178
fix: skip ResizeObserver updates for collapsed nodes
DrJKL Mar 10, 2026
22f383e
docs: add release process guide (#9548)
christian-byrne Mar 9, 2026
6b8ad37
fix: show load widget inputs in media dropdown (#9670)
DrJKL Mar 9, 2026
68689d5
fix: add isGraphReady guard to prevent premature graph access error l…
jaeone94 Mar 9, 2026
1a6788d
Restore hiding of linked inputs in app mode (#9671)
AustinMroz Mar 9, 2026
cce5daa
fix: dispatch cloud build on synchronize for preview-labeled PRs (#9636)
huntcsg Mar 9, 2026
7d603a5
Use preview downscaling in fewer places (#9678)
AustinMroz Mar 9, 2026
9478582
fix: reduce vue drag sync fan-out and per-frame overhead
DrJKL Mar 10, 2026
860f4b6
fix: skip no-op vue resize and slot sync work
DrJKL Mar 10, 2026
ff9e0e5
fix: simplify and harden vue resize no-op guards
DrJKL Mar 10, 2026
f06c9cd
fix: resolve coderabbit normalization and slot sync threads
DrJKL Mar 10, 2026
cd8e82b
Merge branch 'main' into drjkl/dark-energy
DrJKL Mar 10, 2026
033dfe4
fix: reduce layout and ui scheduling overhead on interaction paths
DrJKL Mar 10, 2026
48ba8b4
Merge branch 'main' into drjkl/dark-energy
DrJKL Mar 11, 2026
b86949b
Merge branch 'main' into drjkl/dark-energy
DrJKL Mar 12, 2026
b70eb1f
[automated] Apply ESLint and Oxfmt fixes
actions-user Mar 12, 2026
eefdebd
Merge branch 'main' into drjkl/dark-energy
DrJKL Mar 12, 2026
516e012
types: We need to fix all the Parameters utility usages. That's silly
DrJKL Mar 12, 2026
f6b7f5c
Function Declarations and avoiding stale flushes
DrJKL Mar 12, 2026
afd9445
fix: address review feedback on layout normalization
DrJKL Mar 13, 2026
8842d76
fix: address layout sync lint and node id lookup
DrJKL Mar 13, 2026
d157906
test: stabilize renderer toggle position assertions
DrJKL Mar 13, 2026
e5cc5db
Merge branch 'main' into drjkl/dark-energy
DrJKL Mar 13, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

import type {
LGraph,
LGraphExtra,
RendererType
} from '@/lib/litegraph/src/LGraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { Point, Rect } from '@/lib/litegraph/src/interfaces'

const mockState = vi.hoisted(() => ({
autoScaleEnabled: true,
shouldRenderVueNodes: true,
app: {
canvas: undefined as Partial<LGraphCanvas> | undefined
}
}))

const mockLayoutStore = vi.hoisted(() => ({
setSource: vi.fn(),
batchUpdateNodeBounds: vi.fn()
}))

vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: (key: string) =>
key === 'Comfy.VueNodes.AutoScaleLayout'
? mockState.autoScaleEnabled
: undefined
})
}))

vi.mock('@/composables/useVueFeatureFlags', () => ({
useVueFeatureFlags: () => ({
shouldRenderVueNodes: {
get value() {
return mockState.shouldRenderVueNodes
}
}
})
}))

vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
layoutStore: mockLayoutStore
}))

vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
useLayoutMutations: () => ({
moveReroute: vi.fn()
})
}))

vi.mock('@/scripts/app', () => ({
app: mockState.app
}))

import { ensureCorrectLayoutScale } from './ensureCorrectLayoutScale'

function createNode(id: string, x: number, y: number, w: number, h: number) {
return {
id,
pos: [x, y] as Point,
size: [w, h] as Point,
get width() {
return this.size[0]
},
set width(v: number) {
this.size[0] = v
},
get boundingRect(): Rect {
return [this.pos[0], this.pos[1], this.size[0], this.size[1]]
}
}
}

type MockNode = ReturnType<typeof createNode>

function createMockGraph(
nodes: MockNode[],
extra: LGraphExtra = {}
): Partial<LGraph> {
const graph: Partial<LGraph> = {
id: crypto.randomUUID(),
nodes: nodes as unknown as LGraph['nodes'],
groups: [],
reroutes: new Map() as LGraph['reroutes'],
extra
}
Object.defineProperty(graph, 'rootGraph', { get: () => graph })
return graph
}

function createMockCanvas(graph: Partial<LGraph>): Partial<LGraphCanvas> {
let scale = 1
return {
graph: graph as LGraph,
ds: {
get scale() {
return scale
},
set scale(v: number) {
scale = v
},
convertOffsetToCanvas: (point: Point) => point,
changeScale: (v: number) => {
scale = v
}
} as unknown as LGraphCanvas['ds']
}
}

function distanceBetweenNodes(nodes: MockNode[]): number {
const [a, b] = nodes
return Math.hypot(b.pos[0] - a.pos[0], b.pos[1] - a.pos[1])
}

function twoNodeLayout(): MockNode[] {
return [
createNode('1', 100, 100, 120, 80),
createNode('2', 320, 140, 100, 80)
]
}

describe('ensureCorrectLayoutScale', () => {
beforeEach(() => {
mockState.autoScaleEnabled = true
mockState.shouldRenderVueNodes = true
mockState.app.canvas = undefined
vi.clearAllMocks()
})

it('is idempotent — calling multiple times does not compound spacing', () => {
const nodes = twoNodeLayout()
const graph = createMockGraph(nodes, { workflowRendererVersion: 'LG' })
mockState.app.canvas = createMockCanvas(graph)

const beforeDistance = distanceBetweenNodes(nodes)

ensureCorrectLayoutScale('LG', graph as LGraph)
const firstDistance = distanceBetweenNodes(nodes)

ensureCorrectLayoutScale('LG', graph as LGraph)
const secondDistance = distanceBetweenNodes(nodes)

expect(firstDistance / beforeDistance).toBeCloseTo(1.2, 5)
expect(secondDistance / firstDistance).toBeCloseTo(1, 5)
})

it('stays stable across reloads even when renderer metadata is lost', () => {
const distances: number[] = []
let savedNodes = twoNodeLayout()
let savedExtra: LGraphExtra = {}

for (let i = 0; i < 3; i++) {
const nodes = twoNodeLayout().map((n, j) => {
n.pos = [...savedNodes[j].pos] as Point
n.size = [...savedNodes[j].size] as Point
return n
})
const graph = createMockGraph(nodes, { ...savedExtra })
mockState.app.canvas = createMockCanvas(graph)

ensureCorrectLayoutScale(undefined, graph as LGraph)

distances.push(distanceBetweenNodes(nodes))

// Persist positions but drop renderer metadata
savedNodes = nodes
savedExtra = {}
}

// First load scales up, subsequent loads should not compound
expect(distances[1] / distances[0]).toBeCloseTo(1, 5)
expect(distances[2] / distances[1]).toBeCloseTo(1, 5)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

it('stays stable across reloads when renderer metadata is preserved', () => {
const distances: number[] = []
let savedNodes = twoNodeLayout()
let savedRenderer: RendererType | undefined = 'Vue'

for (let i = 0; i < 3; i++) {
const nodes = twoNodeLayout().map((n, j) => {
n.pos = [...savedNodes[j].pos] as Point
n.size = [...savedNodes[j].size] as Point
return n
})
const extra: LGraphExtra = savedRenderer
? { workflowRendererVersion: savedRenderer }
: {}
const graph = createMockGraph(nodes, extra)
mockState.app.canvas = createMockCanvas(graph)

ensureCorrectLayoutScale(savedRenderer, graph as LGraph)

distances.push(distanceBetweenNodes(nodes))

savedNodes = nodes
savedRenderer = graph.extra?.workflowRendererVersion
}

expect(distances[1] / distances[0]).toBeCloseTo(1, 5)
expect(distances[2] / distances[1]).toBeCloseTo(1, 5)
})

it('scales up then down to return to original positions', () => {
const nodes = twoNodeLayout()
const graph = createMockGraph(nodes, { workflowRendererVersion: 'LG' })
mockState.app.canvas = createMockCanvas(graph)

const beforeDistance = distanceBetweenNodes(nodes)

ensureCorrectLayoutScale('LG', graph as LGraph)

mockState.shouldRenderVueNodes = false
ensureCorrectLayoutScale('Vue', graph as LGraph)

const finalDistance = distanceBetweenNodes(nodes)
expect(finalDistance / beforeDistance).toBeCloseTo(1, 5)
})

it('does nothing when renderer is unknown and graph has no metadata', () => {
const nodes = twoNodeLayout()
const graph = createMockGraph(nodes)
mockState.app.canvas = createMockCanvas(graph)

const beforeDistance = distanceBetweenNodes(nodes)

ensureCorrectLayoutScale(undefined, graph as LGraph)

expect(distanceBetweenNodes(nodes)).toBe(beforeDistance)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOu
const SCALE_FACTOR = 1.2

export function ensureCorrectLayoutScale(
renderer: RendererType = 'LG',
renderer?: RendererType,
targetGraph?: LGraph
) {
const autoScaleLayoutSetting = useSettingStore().get(
Expand All @@ -29,14 +29,13 @@ export function ensureCorrectLayoutScale(

const { shouldRenderVueNodes } = useVueFeatureFlags()

const needsUpscale = renderer === 'LG' && shouldRenderVueNodes.value
const needsDownscale = renderer === 'Vue' && !shouldRenderVueNodes.value
const savedRenderer = graph.extra.workflowRendererVersion ?? renderer
if (!savedRenderer) return

if (!needsUpscale && !needsDownscale) {
// Don't scale, but ensure workflowRendererVersion is set for future checks
graph.extra.workflowRendererVersion ??= renderer
return
}
const needsUpscale = savedRenderer === 'LG' && shouldRenderVueNodes.value
const needsDownscale = savedRenderer === 'Vue' && !shouldRenderVueNodes.value

if (!needsUpscale && !needsDownscale) return

const lgBounds = createBounds(graph.nodes)

Expand Down
Loading