From f629d325b2ac14891d0f52205e3c0a93deb1ea82 Mon Sep 17 00:00:00 2001
From: Simula_r <18093452+simula-r@users.noreply.github.com>
Date: Tue, 28 Oct 2025 14:14:39 -0700
Subject: [PATCH] Feat/vue nodes arrange alg (#6212)
## Summary
### Problem:
The Vue nodes renderer/feature introduces new designs for each node i.e.
the equivalent Litegraph node design is smaller and the vue node design
is non uniformly larger.
### Example:
Litegraph Ksampler node: 200w x 220h
Vue Node Ksampler node: 445w x 430h
This means if users load a workflow in Litegraph and then switches to
Vue nodes renderer the nodes are using the same Litegraph positions
which would cause a visual overlap and overall look broken.
### Example:
### Solution:
Scale the positions of the nodes in lite graph radially from the center
of the bounds of all nodes. And then simply move the Vue nodes to those
new positions.
1. Get the `center of the bounds of all LG nodes`.
2. Get the `xy of each LG node`.
3. Get the vector from `center of the bounds of all LG nodes` `-` `xy of
each LG node`.
4. Scale it by a factor (e.g. 1.75x which is the average Vue node size
increase plus some visual padding.)
5. Move each Vue node to the scaled `xy of each LG node`.
Result: The nodes are spaced apart removing overlaps while keeping the
spatial layout intact.
### Further concerns.
This vector scaling algorithm needs to run once per workflow when in vue
nodes. This means when in Litegraph and switching to Vue nodes, it needs
to run before the nodes render. And then now that the entire app is in
vue nodes, we need to run it each time we load a workflow. However, once
its run, we do not need to run it again. Therefore we must persist a
flag that it has run somewhere. This PR also adds that feature by
leveraging the `extra` field in the workflow schema.
---------
Co-authored-by: GitHub Action
Co-authored-by: JakeSchroeder
---
browser_tests/fixtures/ComfyPage.ts | 3 +-
src/composables/graph/useVueNodeLifecycle.ts | 10 +++
.../settings/constants/coreSettings.ts | 10 +++
.../vueNodes/layout/scaleLayoutForVueNodes.ts | 86 +++++++++++++++++++
src/schemas/apiSchema.ts | 1 +
src/scripts/app.ts | 14 +++
6 files changed, 123 insertions(+), 1 deletion(-)
create mode 100644 src/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes.ts
diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts
index b01de101e5..8059fb4cb4 100644
--- a/browser_tests/fixtures/ComfyPage.ts
+++ b/browser_tests/fixtures/ComfyPage.ts
@@ -1657,7 +1657,8 @@ export const comfyPageFixture = base.extend<{
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
- 'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
+ 'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
+ 'Comfy.VueNodes.AutoScaleLayout': false
})
} catch (e) {
console.error(e)
diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts
index 2b32e51bf3..6ebac700fd 100644
--- a/src/composables/graph/useVueNodeLifecycle.ts
+++ b/src/composables/graph/useVueNodeLifecycle.ts
@@ -9,6 +9,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 { scaleLayoutForVueNodes } from '@/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes'
import { app as comfyApp } from '@/scripts/app'
function useVueNodeLifecycleIndividual() {
@@ -77,6 +78,15 @@ function useVueNodeLifecycleIndividual() {
(enabled) => {
if (enabled) {
initializeNodeManager()
+
+ const graph = comfyApp.canvas.graph
+ if (graph && !graph.extra) {
+ graph.extra = {}
+ }
+ if (graph && !graph.extra.vueNodesScaled) {
+ scaleLayoutForVueNodes()
+ graph.extra.vueNodesScaled = true
+ }
} else {
disposeNodeManagerAndSyncs()
}
diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts
index c4fda0ef92..2b8eba44c9 100644
--- a/src/platform/settings/constants/coreSettings.ts
+++ b/src/platform/settings/constants/coreSettings.ts
@@ -1065,6 +1065,16 @@ export const CORE_SETTINGS: SettingParams[] = [
experimental: true,
versionAdded: '1.27.1'
},
+ {
+ id: 'Comfy.VueNodes.AutoScaleLayout',
+ name: 'Auto-scale layout for Vue nodes',
+ tooltip:
+ 'Automatically scale node positions when switching to Vue rendering to prevent overlap',
+ type: 'boolean',
+ experimental: true,
+ defaultValue: false,
+ versionAdded: '1.30.3'
+ },
{
id: 'Comfy.Assets.UseAssetAPI',
name: 'Use Asset API for model library',
diff --git a/src/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes.ts b/src/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes.ts
new file mode 100644
index 0000000000..873a18c15d
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes.ts
@@ -0,0 +1,86 @@
+import type { Rect } from '@/lib/litegraph/src/interfaces'
+import { createBounds } from '@/lib/litegraph/src/measure'
+import { useSettingStore } from '@/platform/settings/settingStore'
+import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
+import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
+import { app as comfyApp } from '@/scripts/app'
+
+const SCALE_FACTOR = 1.75
+
+export function scaleLayoutForVueNodes() {
+ const settingStore = useSettingStore()
+
+ const autoScaleLayoutSetting = settingStore.get(
+ 'Comfy.VueNodes.AutoScaleLayout'
+ )
+
+ if (autoScaleLayoutSetting === false) {
+ return
+ }
+
+ const canvas = comfyApp.canvas
+ const graph = canvas.graph
+
+ if (!graph || !graph.nodes) return
+
+ const lgBounds = createBounds(graph.nodes)
+
+ if (!lgBounds) return
+
+ const allVueNodes = layoutStore.getAllNodes().value
+
+ const lgBoundsCenterX = lgBounds![0] + lgBounds![2] / 2
+ const lgBoundsCenterY = lgBounds![1] + lgBounds![3] / 2
+
+ const lgNodesById = new Map(
+ graph.nodes.map((node) => [String(node.id), node])
+ )
+
+ const yjsMoveNodeUpdates: NodeBoundsUpdate[] = []
+ const scaledNodesForBounds: Array<{ boundingRect: Rect }> = []
+
+ for (const vueNode of allVueNodes.values()) {
+ const lgNode = lgNodesById.get(String(vueNode.id))
+ if (!lgNode) continue
+
+ const vectorX = lgNode.pos[0] - lgBoundsCenterX
+ const vectorY = lgNode.pos[1] - lgBoundsCenterY
+ const newX = lgBoundsCenterX + vectorX * SCALE_FACTOR
+ const newY = lgBoundsCenterY + vectorY * SCALE_FACTOR
+
+ yjsMoveNodeUpdates.push({
+ nodeId: vueNode.id,
+ bounds: {
+ x: newX,
+ y: newY,
+ width: vueNode.bounds.width,
+ height: vueNode.bounds.height
+ }
+ })
+
+ scaledNodesForBounds.push({
+ boundingRect: [newX, newY, vueNode.bounds.width, vueNode.bounds.height]
+ })
+ }
+
+ layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
+
+ const scaledLgBounds = createBounds(scaledNodesForBounds)
+
+ graph.groups.forEach((group) => {
+ const vectorX = group.pos[0] - lgBoundsCenterX
+ const vectorY = group.pos[1] - lgBoundsCenterY
+
+ group.pos = [
+ lgBoundsCenterX + vectorX * SCALE_FACTOR,
+ lgBoundsCenterY + vectorY * SCALE_FACTOR
+ ]
+ group.size = [group.size[0] * SCALE_FACTOR, group.size[1] * SCALE_FACTOR]
+ })
+
+ if (scaledLgBounds) {
+ canvas.ds.fitToBounds(scaledLgBounds, {
+ zoom: 0.5 //Makes it so the fit to view is slightly zoomed out and not edge to edge.
+ })
+ }
+}
diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts
index df1b102d11..30b0e57eba 100644
--- a/src/schemas/apiSchema.ts
+++ b/src/schemas/apiSchema.ts
@@ -469,6 +469,7 @@ const zSettings = z.object({
'Comfy.Canvas.LeftMouseClickBehavior': z.string(),
'Comfy.Canvas.MouseWheelScroll': z.string(),
'Comfy.VueNodes.Enabled': z.boolean(),
+ 'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
'Comfy.Assets.UseAssetAPI': z.boolean(),
'Comfy-Desktop.AutoUpdate': z.boolean(),
'Comfy-Desktop.SendStatistics': z.boolean(),
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index 24f6124dc8..9858156503 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -5,6 +5,7 @@ import { reactive, unref } from 'vue'
import { shallowRef } from 'vue'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
+import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
import { st, t } from '@/i18n'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
@@ -98,6 +99,7 @@ import { $el, ComfyUI } from './ui'
import { ComfyAppMenu } from './ui/menu/index'
import { clone } from './utils'
import { type ComfyWidgetConstructor } from './widgets'
+import { scaleLayoutForVueNodes } from '@/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
@@ -1181,6 +1183,18 @@ export class ComfyApp {
try {
// @ts-expect-error Discrepancies between zod and litegraph - in progress
this.graph.configure(graphData)
+
+ const vueMode = useVueFeatureFlags().shouldRenderVueNodes.value
+
+ if (!this.graph.extra) {
+ this.graph.extra = {}
+ }
+
+ if (vueMode && !this.graph.extra.vueNodesScaled) {
+ scaleLayoutForVueNodes()
+ this.graph.extra.vueNodesScaled = true
+ }
+
if (
restore_view &&
useSettingStore().get('Comfy.EnableWorkflowViewRestore')