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 <action@github.com>
Co-authored-by: JakeSchroeder <jake@axiom.co>
This commit is contained in:
Simula_r
2025-10-28 14:14:39 -07:00
committed by GitHub
parent 38525d8f3a
commit f629d325b2
6 changed files with 123 additions and 1 deletions

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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',

View File

@@ -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.
})
}
}

View File

@@ -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(),

View File

@@ -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')