Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
72 changes: 72 additions & 0 deletions browser_tests/tests/rendererToggleStability.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import type { Position } from '../fixtures/types'

type NodeSnapshot = { id: number } & Position

type ComfyPage = Parameters<Parameters<typeof test>[2]>[0]['comfyPage']

async function getAllNodePositions(
comfyPage: ComfyPage
): Promise<NodeSnapshot[]> {
return comfyPage.page.evaluate(() =>
window.app!.graph.nodes.map((n) => ({
Comment thread
christian-byrne marked this conversation as resolved.
id: n.id as number,
x: n.pos[0],
y: n.pos[1]
}))
)
}

async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
if (enabled) {
await comfyPage.vueNodes.waitForNodes()
}
await comfyPage.nextFrame()
}

test.describe(
'Renderer toggle stability',
{ tag: ['@node', '@canvas'] },
() => {
test('node positions do not drift when toggling between Vue and LiteGraph renderers', async ({
comfyPage
}) => {
const TOGGLE_COUNT = 5

const initialPositions = await getAllNodePositions(comfyPage)
expect(initialPositions.length).toBeGreaterThan(0)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for (let i = 0; i < TOGGLE_COUNT; i++) {
await setVueMode(comfyPage, true)
const afterVue = await getAllNodePositions(comfyPage)

for (const initial of initialPositions) {
const current = afterVue.find((n) => n.id === initial.id)
expect(
current,
`node ${initial.id} missing after Vue toggle ${i + 1}`
).toBeDefined()
expect(current!.x).toBeCloseTo(initial.x, 1)
expect(current!.y).toBeCloseTo(initial.y, 1)
Comment thread
christian-byrne marked this conversation as resolved.
Outdated
}

await setVueMode(comfyPage, false)
const afterLG = await getAllNodePositions(comfyPage)

for (const initial of initialPositions) {
const current = afterLG.find((n) => n.id === initial.id)
expect(
current,
`node ${initial.id} missing after LG toggle ${i + 1}`
).toBeDefined()
expect(current!.x).toBeCloseTo(initial.x, 1)
expect(current!.y).toBeCloseTo(initial.y, 1)
}
}
})
}
)
7 changes: 0 additions & 7 deletions src/composables/graph/useVueNodeLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { app as comfyApp } from '@/scripts/app'

function useVueNodeLifecycleIndividual() {
Expand Down Expand Up @@ -76,9 +75,6 @@ function useVueNodeLifecycleIndividual() {
(enabled) => {
if (enabled) {
initializeNodeManager()
ensureCorrectLayoutScale(
comfyApp.canvas?.graph?.extra.workflowRendererVersion
)
}
},
{ immediate: true }
Expand All @@ -87,9 +83,6 @@ function useVueNodeLifecycleIndividual() {
whenever(
() => !shouldRenderVueNodes.value,
() => {
ensureCorrectLayoutScale(
comfyApp.canvas?.graph?.extra.workflowRendererVersion
)
disposeNodeManagerAndSyncs()
comfyApp.canvas?.setDirty(true, true)
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/LGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export type {
LGraphTriggerParam
} from './types/graphTriggers'

export type RendererType = 'LG' | 'Vue'
export type RendererType = 'LG' | 'Vue' | 'Vue-corrected'

export interface LGraphState {
lastGroupId: number
Expand Down
6 changes: 5 additions & 1 deletion src/platform/workflow/validation/schemas/workflowSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type { SafeParseReturnType } from 'zod'
import { fromZodError } from 'zod-validation-error'
import type { RendererType } from '@/lib/litegraph/src/LGraph'

const zRendererType = z.enum(['LG', 'Vue']) satisfies z.ZodType<RendererType>
const zRendererType = z.enum([
'LG',
'Vue',
'Vue-corrected'
]) satisfies z.ZodType<RendererType>

// GroupNode is hacking node id to be a string, so we need to allow that.
// innerNode.id = `${this.node.id}:${i}`
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/core/layout/store/layoutStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,8 +917,9 @@ class LayoutStoreImpl implements LayoutStore {
}
})

// Notify listeners (after transaction completes)
setTimeout(() => this.notifyChange(change), 0)
// Notify listeners synchronously so Layout→LiteGraph sync
// (useLayoutSync) pushes positions within the same frame.
this.notifyChange(change)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/**
Expand Down
103 changes: 103 additions & 0 deletions src/renderer/core/layout/transform/graphRenderTransform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest'

import {
RENDER_SCALE_FACTOR,
getGraphRenderAnchor,
projectBounds,
projectPoint,
unprojectBounds,
unprojectPoint
} from './graphRenderTransform'

const anchor = { x: 100, y: 100 }

describe('graphRenderTransform', () => {
describe('projectPoint / unprojectPoint', () => {
it('round-trips correctly', () => {
const point = { x: 320, y: 140 }
const projected = projectPoint(point, anchor, RENDER_SCALE_FACTOR)
const restored = unprojectPoint(projected, anchor, RENDER_SCALE_FACTOR)
expect(restored.x).toBeCloseTo(point.x, 10)
expect(restored.y).toBeCloseTo(point.y, 10)
})

it('is identity when scale is 1', () => {
const point = { x: 250, y: 300 }
expect(projectPoint(point, anchor, 1)).toEqual(point)
expect(unprojectPoint(point, anchor, 1)).toEqual(point)
})

it('scales relative to anchor', () => {
const point = { x: 200, y: 200 }
const projected = projectPoint(point, anchor, RENDER_SCALE_FACTOR)
expect(projected.x).toBeCloseTo(100 + 100 * RENDER_SCALE_FACTOR)
expect(projected.y).toBeCloseTo(100 + 100 * RENDER_SCALE_FACTOR)
})

it('leaves anchor point unchanged', () => {
const projected = projectPoint(anchor, anchor, RENDER_SCALE_FACTOR)
expect(projected).toEqual(anchor)
})
})

describe('projectBounds / unprojectBounds', () => {
it('round-trips correctly', () => {
const bounds = { x: 200, y: 150, width: 120, height: 80 }
const projected = projectBounds(bounds, anchor, RENDER_SCALE_FACTOR)
const restored = unprojectBounds(projected, anchor, RENDER_SCALE_FACTOR)
expect(restored.x).toBeCloseTo(bounds.x, 10)
expect(restored.y).toBeCloseTo(bounds.y, 10)
expect(restored.width).toBeCloseTo(bounds.width, 10)
expect(restored.height).toBeCloseTo(bounds.height, 10)
})

it('scales width and height by scale factor', () => {
const bounds = { x: 100, y: 100, width: 100, height: 50 }
const projected = projectBounds(bounds, anchor, RENDER_SCALE_FACTOR)
expect(projected.width).toBeCloseTo(100 * RENDER_SCALE_FACTOR)
expect(projected.height).toBeCloseTo(50 * RENDER_SCALE_FACTOR)
})
})

describe('getGraphRenderAnchor', () => {
it('returns cached anchor on subsequent calls', () => {
const mockGraph = {
nodes: [
{
pos: [100, 200],
size: [120, 80],
get width() {
return this.size[0]
},
get boundingRect() {
return [this.pos[0], this.pos[1], this.size[0], this.size[1]]
}
},
{
pos: [300, 400],
size: [100, 60],
get width() {
return this.size[0]
},
get boundingRect() {
return [this.pos[0], this.pos[1], this.size[0], this.size[1]]
}
}
]
}

const anchor1 = getGraphRenderAnchor(mockGraph as never)
// Mutate positions — anchor should stay frozen
mockGraph.nodes[0].pos = [500, 600]
const anchor2 = getGraphRenderAnchor(mockGraph as never)

expect(anchor1).toBe(anchor2)
})

it('returns origin for empty graph', () => {
const mockGraph = { nodes: [] }
const anchor = getGraphRenderAnchor(mockGraph as never)
expect(anchor).toEqual({ x: 0, y: 0 })
})
})
})
69 changes: 69 additions & 0 deletions src/renderer/core/layout/transform/graphRenderTransform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import { createBounds } from '@/lib/litegraph/src/measure'
import type { Bounds, Point } from '@/renderer/core/layout/types'

export const RENDER_SCALE_FACTOR = 1.2

const anchorCache = new WeakMap<LGraph, Point>()
Comment thread
christian-byrne marked this conversation as resolved.

export function getGraphRenderAnchor(graph: LGraph): Point {
const cached = anchorCache.get(graph)
if (cached) return cached

const bounds = graph.nodes?.length ? createBounds(graph.nodes) : undefined
const anchor = bounds ? { x: bounds[0], y: bounds[1] } : { x: 0, y: 0 }
anchorCache.set(graph, anchor)
return anchor
}

export function projectPoint(
point: Point,
anchor: Point,
scale: number
): Point {
if (scale === 1) return point
return {
x: anchor.x + (point.x - anchor.x) * scale,
y: anchor.y + (point.y - anchor.y) * scale
}
}

export function unprojectPoint(
point: Point,
anchor: Point,
scale: number
): Point {
if (scale === 1) return point
return {
x: anchor.x + (point.x - anchor.x) / scale,
y: anchor.y + (point.y - anchor.y) / scale
}
}

export function projectBounds(
bounds: Bounds,
anchor: Point,
scale: number
): Bounds {
const topLeft = projectPoint({ x: bounds.x, y: bounds.y }, anchor, scale)
return {
x: topLeft.x,
y: topLeft.y,
width: bounds.width * scale,
height: bounds.height * scale
}
}

export function unprojectBounds(
bounds: Bounds,
anchor: Point,
scale: number
): Bounds {
const topLeft = unprojectPoint({ x: bounds.x, y: bounds.y }, anchor, scale)
return {
x: topLeft.x,
y: topLeft.y,
width: bounds.width / scale,
height: bounds.height / scale
}
}
Loading
Loading