Skip to content

Commit f629d32

Browse files
simula-ractions-userJakeSchroeder
authored
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 <img width="200" height="220" alt="image" src="https://github.com/user-attachments/assets/eef0117b-7e02-407d-98ab-c610fd1ec54c" /> Vue Node Ksampler node: 445w x 430h <img width="445" height="430" alt="image" src="https://github.com/user-attachments/assets/e78d9d45-5b32-4e8d-bf1c-bce1c699037f" /> 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: <img width="1510" height="726" alt="image" src="https://github.com/user-attachments/assets/3b7ae9d2-6057-49b2-968e-c531a969fac4" /> <img width="1475" height="850" alt="image" src="https://github.com/user-attachments/assets/ea10f361-09bd-4daa-97f1-6b45b5dde389" /> ### 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. <img width="2173" height="1096" alt="image" src="https://github.com/user-attachments/assets/7817d866-4051-47bb-a589-69ca77a0bfd3" /> ### 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 <[email protected]> Co-authored-by: JakeSchroeder <[email protected]>
1 parent 38525d8 commit f629d32

File tree

6 files changed

+123
-1
lines changed

6 files changed

+123
-1
lines changed

browser_tests/fixtures/ComfyPage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1657,7 +1657,8 @@ export const comfyPageFixture = base.extend<{
16571657
'Comfy.userId': userId,
16581658
// Set tutorial completed to true to avoid loading the tutorial workflow.
16591659
'Comfy.TutorialCompleted': true,
1660-
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
1660+
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
1661+
'Comfy.VueNodes.AutoScaleLayout': false
16611662
})
16621663
} catch (e) {
16631664
console.error(e)

src/composables/graph/useVueNodeLifecycle.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
99
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
1010
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
1111
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
12+
import { scaleLayoutForVueNodes } from '@/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes'
1213
import { app as comfyApp } from '@/scripts/app'
1314

1415
function useVueNodeLifecycleIndividual() {
@@ -77,6 +78,15 @@ function useVueNodeLifecycleIndividual() {
7778
(enabled) => {
7879
if (enabled) {
7980
initializeNodeManager()
81+
82+
const graph = comfyApp.canvas.graph
83+
if (graph && !graph.extra) {
84+
graph.extra = {}
85+
}
86+
if (graph && !graph.extra.vueNodesScaled) {
87+
scaleLayoutForVueNodes()
88+
graph.extra.vueNodesScaled = true
89+
}
8090
} else {
8191
disposeNodeManagerAndSyncs()
8292
}

src/platform/settings/constants/coreSettings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,16 @@ export const CORE_SETTINGS: SettingParams[] = [
10651065
experimental: true,
10661066
versionAdded: '1.27.1'
10671067
},
1068+
{
1069+
id: 'Comfy.VueNodes.AutoScaleLayout',
1070+
name: 'Auto-scale layout for Vue nodes',
1071+
tooltip:
1072+
'Automatically scale node positions when switching to Vue rendering to prevent overlap',
1073+
type: 'boolean',
1074+
experimental: true,
1075+
defaultValue: false,
1076+
versionAdded: '1.30.3'
1077+
},
10681078
{
10691079
id: 'Comfy.Assets.UseAssetAPI',
10701080
name: 'Use Asset API for model library',
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { Rect } from '@/lib/litegraph/src/interfaces'
2+
import { createBounds } from '@/lib/litegraph/src/measure'
3+
import { useSettingStore } from '@/platform/settings/settingStore'
4+
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
5+
import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
6+
import { app as comfyApp } from '@/scripts/app'
7+
8+
const SCALE_FACTOR = 1.75
9+
10+
export function scaleLayoutForVueNodes() {
11+
const settingStore = useSettingStore()
12+
13+
const autoScaleLayoutSetting = settingStore.get(
14+
'Comfy.VueNodes.AutoScaleLayout'
15+
)
16+
17+
if (autoScaleLayoutSetting === false) {
18+
return
19+
}
20+
21+
const canvas = comfyApp.canvas
22+
const graph = canvas.graph
23+
24+
if (!graph || !graph.nodes) return
25+
26+
const lgBounds = createBounds(graph.nodes)
27+
28+
if (!lgBounds) return
29+
30+
const allVueNodes = layoutStore.getAllNodes().value
31+
32+
const lgBoundsCenterX = lgBounds![0] + lgBounds![2] / 2
33+
const lgBoundsCenterY = lgBounds![1] + lgBounds![3] / 2
34+
35+
const lgNodesById = new Map(
36+
graph.nodes.map((node) => [String(node.id), node])
37+
)
38+
39+
const yjsMoveNodeUpdates: NodeBoundsUpdate[] = []
40+
const scaledNodesForBounds: Array<{ boundingRect: Rect }> = []
41+
42+
for (const vueNode of allVueNodes.values()) {
43+
const lgNode = lgNodesById.get(String(vueNode.id))
44+
if (!lgNode) continue
45+
46+
const vectorX = lgNode.pos[0] - lgBoundsCenterX
47+
const vectorY = lgNode.pos[1] - lgBoundsCenterY
48+
const newX = lgBoundsCenterX + vectorX * SCALE_FACTOR
49+
const newY = lgBoundsCenterY + vectorY * SCALE_FACTOR
50+
51+
yjsMoveNodeUpdates.push({
52+
nodeId: vueNode.id,
53+
bounds: {
54+
x: newX,
55+
y: newY,
56+
width: vueNode.bounds.width,
57+
height: vueNode.bounds.height
58+
}
59+
})
60+
61+
scaledNodesForBounds.push({
62+
boundingRect: [newX, newY, vueNode.bounds.width, vueNode.bounds.height]
63+
})
64+
}
65+
66+
layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
67+
68+
const scaledLgBounds = createBounds(scaledNodesForBounds)
69+
70+
graph.groups.forEach((group) => {
71+
const vectorX = group.pos[0] - lgBoundsCenterX
72+
const vectorY = group.pos[1] - lgBoundsCenterY
73+
74+
group.pos = [
75+
lgBoundsCenterX + vectorX * SCALE_FACTOR,
76+
lgBoundsCenterY + vectorY * SCALE_FACTOR
77+
]
78+
group.size = [group.size[0] * SCALE_FACTOR, group.size[1] * SCALE_FACTOR]
79+
})
80+
81+
if (scaledLgBounds) {
82+
canvas.ds.fitToBounds(scaledLgBounds, {
83+
zoom: 0.5 //Makes it so the fit to view is slightly zoomed out and not edge to edge.
84+
})
85+
}
86+
}

src/schemas/apiSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ const zSettings = z.object({
469469
'Comfy.Canvas.LeftMouseClickBehavior': z.string(),
470470
'Comfy.Canvas.MouseWheelScroll': z.string(),
471471
'Comfy.VueNodes.Enabled': z.boolean(),
472+
'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
472473
'Comfy.Assets.UseAssetAPI': z.boolean(),
473474
'Comfy-Desktop.AutoUpdate': z.boolean(),
474475
'Comfy-Desktop.SendStatistics': z.boolean(),

src/scripts/app.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { reactive, unref } from 'vue'
55
import { shallowRef } from 'vue'
66

77
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
8+
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
89
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
910
import { st, t } from '@/i18n'
1011
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
@@ -98,6 +99,7 @@ import { $el, ComfyUI } from './ui'
9899
import { ComfyAppMenu } from './ui/menu/index'
99100
import { clone } from './utils'
100101
import { type ComfyWidgetConstructor } from './widgets'
102+
import { scaleLayoutForVueNodes } from '@/renderer/extensions/vueNodes/layout/scaleLayoutForVueNodes'
101103

102104
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
103105

@@ -1181,6 +1183,18 @@ export class ComfyApp {
11811183
try {
11821184
// @ts-expect-error Discrepancies between zod and litegraph - in progress
11831185
this.graph.configure(graphData)
1186+
1187+
const vueMode = useVueFeatureFlags().shouldRenderVueNodes.value
1188+
1189+
if (!this.graph.extra) {
1190+
this.graph.extra = {}
1191+
}
1192+
1193+
if (vueMode && !this.graph.extra.vueNodesScaled) {
1194+
scaleLayoutForVueNodes()
1195+
this.graph.extra.vueNodesScaled = true
1196+
}
1197+
11841198
if (
11851199
restore_view &&
11861200
useSettingStore().get('Comfy.EnableWorkflowViewRestore')

0 commit comments

Comments
 (0)