Fix/vue nodes auto scale (#6664)
## Summary **Problem:** ensureCorrectLayoutScale scales up LG -> Vue. But doesn't scale down from Vue -> LG. **Solution:** Bi directional scaling. **Bonus:** fix edge cases such as subgraphs, groups, and reroutes. Also, set auto scale: true now that we 'preserve' LG scale. **IMPORTANT:** useVueNodeResizeTracking.ts sets vue node height - Litegraph.NODE_TITLE_HEIGHT on workflow load using a resize observer. Reloading the page (loading a workflow) in Vue mode, will subtract height each time. This can look like a problem caused by ensureCorrectLayoutScale. It is not. Need to fix. Here was an attempt by [removing the Litegraph.NODE_TITLE_HEIGHT entirely](https://github.com/Comfy-Org/ComfyUI_frontend/pull/6643). ## Review Focus Full lifecycle of loading workflows and switching between vue and lg. Race conditions could be present. For example switching the mode using keybind very fast. ## Screenshots (if applicable) https://github.com/user-attachments/assets/5576b760-13a8-45b9-b8f7-64e1caf443c1 https://github.com/user-attachments/assets/46d6f870-df76-4084-968a-53cb629fc123 --------- Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import { createSharedComposable } from '@vueuse/core'
|
import { createSharedComposable, whenever } from '@vueuse/core'
|
||||||
import { shallowRef, watch } from 'vue'
|
import { shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
@@ -82,8 +82,9 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
(enabled, wasEnabled) => {
|
(enabled, wasEnabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
initializeNodeManager()
|
initializeNodeManager()
|
||||||
ensureCorrectLayoutScale()
|
ensureCorrectLayoutScale(
|
||||||
|
comfyApp.canvas?.graph?.extra.workflowRendererVersion
|
||||||
|
)
|
||||||
if (!wasEnabled && !isVueNodeToastDismissed.value) {
|
if (!wasEnabled && !isVueNodeToastDismissed.value) {
|
||||||
useToastStore().add({
|
useToastStore().add({
|
||||||
group: 'vue-nodes-migration',
|
group: 'vue-nodes-migration',
|
||||||
@@ -91,14 +92,22 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
life: 0
|
life: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
comfyApp.canvas?.setDirty(true, true)
|
|
||||||
disposeNodeManagerAndSyncs()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => !shouldRenderVueNodes.value,
|
||||||
|
() => {
|
||||||
|
ensureCorrectLayoutScale(
|
||||||
|
comfyApp.canvas?.graph?.extra.workflowRendererVersion
|
||||||
|
)
|
||||||
|
disposeNodeManagerAndSyncs()
|
||||||
|
comfyApp.canvas?.setDirty(true, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Consolidated watch for slot layout sync management
|
// Consolidated watch for slot layout sync management
|
||||||
watch(
|
watch(
|
||||||
() => shouldRenderVueNodes.value,
|
() => shouldRenderVueNodes.value,
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export type {
|
|||||||
LGraphTriggerParam
|
LGraphTriggerParam
|
||||||
} from './types/graphTriggers'
|
} from './types/graphTriggers'
|
||||||
|
|
||||||
|
export type rendererType = 'LG' | 'Vue'
|
||||||
|
|
||||||
export interface LGraphState {
|
export interface LGraphState {
|
||||||
lastGroupId: number
|
lastGroupId: number
|
||||||
lastNodeId: number
|
lastNodeId: number
|
||||||
@@ -104,6 +106,7 @@ export interface LGraphExtra extends Dictionary<unknown> {
|
|||||||
reroutes?: SerialisableReroute[]
|
reroutes?: SerialisableReroute[]
|
||||||
linkExtensions?: { id: number; parentId: number | undefined }[]
|
linkExtensions?: { id: number; parentId: number | undefined }[]
|
||||||
ds?: DragAndScaleState
|
ds?: DragAndScaleState
|
||||||
|
workflowRendererVersion?: rendererType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseLGraph {
|
export interface BaseLGraph {
|
||||||
|
|||||||
@@ -1101,7 +1101,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
'Automatically scale node positions when switching to Vue rendering to prevent overlap',
|
'Automatically scale node positions when switching to Vue rendering to prevent overlap',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
experimental: true,
|
experimental: true,
|
||||||
defaultValue: false,
|
defaultValue: true,
|
||||||
versionAdded: '1.30.3'
|
versionAdded: '1.30.3'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -86,15 +86,16 @@ const resizeObserver = new ResizeObserver((entries) => {
|
|||||||
|
|
||||||
if (!elementType || !elementId) continue
|
if (!elementType || !elementId) continue
|
||||||
|
|
||||||
// Use contentBoxSize when available; fall back to contentRect for older engines/tests
|
// Use borderBoxSize when available; fall back to contentRect for older engines/tests
|
||||||
const contentBox = Array.isArray(entry.contentBoxSize)
|
// Border box is the border included FULL wxh DOM value.
|
||||||
? entry.contentBoxSize[0]
|
const borderBox = Array.isArray(entry.borderBoxSize)
|
||||||
|
? entry.borderBoxSize[0]
|
||||||
: {
|
: {
|
||||||
inlineSize: entry.contentRect.width,
|
inlineSize: entry.contentRect.width,
|
||||||
blockSize: entry.contentRect.height
|
blockSize: entry.contentRect.height
|
||||||
}
|
}
|
||||||
const width = contentBox.inlineSize
|
const width = borderBox.inlineSize
|
||||||
const height = contentBox.blockSize
|
const height = borderBox.blockSize
|
||||||
|
|
||||||
// Screen-space rect
|
// Screen-space rect
|
||||||
const rect = element.getBoundingClientRect()
|
const rect = element.getBoundingClientRect()
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
|
import type { LGraph, rendererType } from '@/lib/litegraph/src/LGraph'
|
||||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
import { createBounds } from '@/lib/litegraph/src/measure'
|
import { createBounds } from '@/lib/litegraph/src/measure'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
|
import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
|
import type { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode'
|
||||||
|
import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode'
|
||||||
|
|
||||||
const SCALE_FACTOR = 1.75
|
const SCALE_FACTOR = 1.75
|
||||||
|
|
||||||
export function ensureCorrectLayoutScale() {
|
export function ensureCorrectLayoutScale(
|
||||||
|
renderer?: rendererType,
|
||||||
|
targetGraph?: LGraph
|
||||||
|
) {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const autoScaleLayoutSetting = settingStore.get(
|
const autoScaleLayoutSetting = settingStore.get(
|
||||||
@@ -18,77 +26,178 @@ export function ensureCorrectLayoutScale() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
|
|
||||||
const canvas = comfyApp.canvas
|
const canvas = comfyApp.canvas
|
||||||
const graph = canvas?.graph
|
const graph = targetGraph ?? canvas?.graph
|
||||||
|
|
||||||
if (!graph || !graph.nodes) return
|
if (!graph || !graph.nodes) return
|
||||||
|
|
||||||
if (graph.extra?.vueNodesScaled === true) {
|
// Use renderer from graph, default to 'LG' for the check (but don't modify graph yet)
|
||||||
|
if (!renderer) {
|
||||||
|
// Always assume legacy LG format when unknown (pre-dates this feature)
|
||||||
|
renderer = 'LG'
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesntNeedScale =
|
||||||
|
(renderer === 'LG' && shouldRenderVueNodes.value === false) ||
|
||||||
|
(renderer === 'Vue' && shouldRenderVueNodes.value === true)
|
||||||
|
|
||||||
|
if (doesntNeedScale) {
|
||||||
|
// Don't scale, but ensure workflowRendererVersion is set for future checks
|
||||||
|
if (!graph.extra.workflowRendererVersion) {
|
||||||
|
graph.extra.workflowRendererVersion = renderer
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const vueNodesEnabled = settingStore.get('Comfy.VueNodes.Enabled')
|
const needsUpscale = renderer === 'LG' && shouldRenderVueNodes.value === true
|
||||||
if (!vueNodesEnabled) {
|
const needsDownscale =
|
||||||
return
|
renderer === 'Vue' && shouldRenderVueNodes.value === false
|
||||||
}
|
|
||||||
|
|
||||||
const lgBounds = createBounds(graph.nodes)
|
const lgBounds = createBounds(graph.nodes)
|
||||||
|
|
||||||
if (!lgBounds) return
|
if (!lgBounds) return
|
||||||
|
|
||||||
const allVueNodes = layoutStore.getAllNodes().value
|
|
||||||
|
|
||||||
const originX = lgBounds[0]
|
const originX = lgBounds[0]
|
||||||
const originY = lgBounds[1]
|
const originY = lgBounds[1]
|
||||||
|
|
||||||
const lgNodesById = new Map(
|
const lgNodesById = new Map(graph.nodes.map((node) => [node.id, node]))
|
||||||
graph.nodes.map((node) => [String(node.id), node])
|
|
||||||
)
|
|
||||||
|
|
||||||
const yjsMoveNodeUpdates: NodeBoundsUpdate[] = []
|
const yjsMoveNodeUpdates: NodeBoundsUpdate[] = []
|
||||||
|
|
||||||
for (const vueNode of allVueNodes.values()) {
|
const scaleFactor = needsUpscale
|
||||||
const lgNode = lgNodesById.get(String(vueNode.id))
|
? SCALE_FACTOR
|
||||||
|
: needsDownscale
|
||||||
|
? 1 / SCALE_FACTOR
|
||||||
|
: 1
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
const lgNode = lgNodesById.get(node.id)
|
||||||
if (!lgNode) continue
|
if (!lgNode) continue
|
||||||
|
|
||||||
const lgBodyY = lgNode.pos[1] - LiteGraph.NODE_TITLE_HEIGHT
|
const lgBodyY = lgNode.pos[1]
|
||||||
|
|
||||||
const relativeX = lgNode.pos[0] - originX
|
const relativeX = lgNode.pos[0] - originX
|
||||||
const relativeY = lgBodyY - originY
|
const relativeY = lgBodyY - originY
|
||||||
const newX = originX + relativeX * SCALE_FACTOR
|
const newX = originX + relativeX * scaleFactor
|
||||||
const newY = originY + relativeY * SCALE_FACTOR
|
const newY = originY + relativeY * scaleFactor
|
||||||
const newWidth = lgNode.width * SCALE_FACTOR
|
const newWidth = lgNode.width * scaleFactor
|
||||||
const newHeight = lgNode.height * SCALE_FACTOR
|
const newHeight = lgNode.height * scaleFactor
|
||||||
|
|
||||||
yjsMoveNodeUpdates.push({
|
// Directly update LiteGraph node to ensure immediate consistency
|
||||||
nodeId: vueNode.id,
|
// Dont need to reference vue directly because the pos and dims are already in yjs
|
||||||
bounds: {
|
lgNode.pos[0] = newX
|
||||||
x: newX,
|
lgNode.pos[1] = newY
|
||||||
y: newY,
|
lgNode.size[0] = newWidth
|
||||||
width: newWidth,
|
lgNode.size[1] =
|
||||||
height: newHeight
|
newHeight - (needsDownscale ? LiteGraph.NODE_TITLE_HEIGHT : 0)
|
||||||
}
|
|
||||||
})
|
// Track updates for layout store (only if this is the active graph)
|
||||||
|
if (!targetGraph || targetGraph === canvas?.graph) {
|
||||||
|
yjsMoveNodeUpdates.push({
|
||||||
|
nodeId: String(lgNode.id),
|
||||||
|
bounds: {
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight - (needsDownscale ? LiteGraph.NODE_TITLE_HEIGHT : 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
|
if (
|
||||||
|
(!targetGraph || targetGraph === canvas?.graph) &&
|
||||||
|
yjsMoveNodeUpdates.length > 0
|
||||||
|
) {
|
||||||
|
layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reroute of graph.reroutes.values()) {
|
||||||
|
const oldX = reroute.pos[0]
|
||||||
|
const oldY = reroute.pos[1]
|
||||||
|
|
||||||
|
const relativeX = oldX - originX
|
||||||
|
const relativeY = oldY - originY
|
||||||
|
const newX = originX + relativeX * scaleFactor
|
||||||
|
const newY = originY + relativeY * scaleFactor
|
||||||
|
|
||||||
|
reroute.pos = [newX, newY]
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!targetGraph || targetGraph === canvas?.graph) &&
|
||||||
|
shouldRenderVueNodes.value
|
||||||
|
) {
|
||||||
|
const layoutMutations = useLayoutMutations()
|
||||||
|
layoutMutations.moveReroute(
|
||||||
|
reroute.id,
|
||||||
|
{ x: newX, y: newY },
|
||||||
|
{ x: oldX, y: oldY }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('inputNode' in graph && 'outputNode' in graph) {
|
||||||
|
const ioNodes = [
|
||||||
|
graph.inputNode as SubgraphInputNode,
|
||||||
|
graph.outputNode as SubgraphOutputNode
|
||||||
|
]
|
||||||
|
for (const ioNode of ioNodes) {
|
||||||
|
const oldX = ioNode.pos[0]
|
||||||
|
const oldY = ioNode.pos[1]
|
||||||
|
const oldWidth = ioNode.size[0]
|
||||||
|
const oldHeight = ioNode.size[1]
|
||||||
|
|
||||||
|
const relativeX = oldX - originX
|
||||||
|
const relativeY = oldY - originY
|
||||||
|
const newX = originX + relativeX * scaleFactor
|
||||||
|
const newY = originY + relativeY * scaleFactor
|
||||||
|
const newWidth = oldWidth * scaleFactor
|
||||||
|
const newHeight = oldHeight * scaleFactor
|
||||||
|
|
||||||
|
ioNode.pos = [newX, newY]
|
||||||
|
ioNode.size = [newWidth, newHeight]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
graph.groups.forEach((group) => {
|
graph.groups.forEach((group) => {
|
||||||
const groupBodyY = group.pos[1] - LiteGraph.NODE_TITLE_HEIGHT
|
const originalPosX = group.pos[0]
|
||||||
|
const originalPosY = group.pos[1]
|
||||||
|
const originalWidth = group.size[0]
|
||||||
|
const originalHeight = group.size[1]
|
||||||
|
|
||||||
const relativeX = group.pos[0] - originX
|
const adjustedY = needsDownscale
|
||||||
const relativeY = groupBodyY - originY
|
? originalPosY - LiteGraph.NODE_TITLE_HEIGHT
|
||||||
|
: originalPosY
|
||||||
|
|
||||||
const newPosY =
|
const relativeX = originalPosX - originX
|
||||||
originY + relativeY * SCALE_FACTOR + LiteGraph.NODE_TITLE_HEIGHT
|
const relativeY = adjustedY - originY
|
||||||
|
|
||||||
group.pos = [originX + relativeX * SCALE_FACTOR, newPosY]
|
const newWidth = originalWidth * scaleFactor
|
||||||
group.size = [group.size[0] * SCALE_FACTOR, group.size[1] * SCALE_FACTOR]
|
const newHeight = originalHeight * scaleFactor
|
||||||
|
|
||||||
|
const scaledX = originX + relativeX * scaleFactor
|
||||||
|
const scaledY = originY + relativeY * scaleFactor
|
||||||
|
|
||||||
|
const finalY = needsUpscale
|
||||||
|
? scaledY + LiteGraph.NODE_TITLE_HEIGHT
|
||||||
|
: scaledY
|
||||||
|
|
||||||
|
group.pos = [scaledX, finalY]
|
||||||
|
group.size = [newWidth, newHeight]
|
||||||
})
|
})
|
||||||
|
|
||||||
const originScreen = canvas.ds.convertOffsetToCanvas([originX, originY])
|
if ((!targetGraph || targetGraph === canvas?.graph) && canvas) {
|
||||||
canvas.ds.changeScale(canvas.ds.scale / SCALE_FACTOR, originScreen)
|
const originScreen = canvas.ds.convertOffsetToCanvas([originX, originY])
|
||||||
|
canvas.ds.changeScale(canvas.ds.scale / scaleFactor, originScreen)
|
||||||
|
}
|
||||||
|
|
||||||
if (!graph.extra) graph.extra = {}
|
if (needsUpscale) {
|
||||||
graph.extra.vueNodesScaled = true
|
graph.extra.workflowRendererVersion = 'Vue'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsDownscale) {
|
||||||
|
graph.extra.workflowRendererVersion = 'LG'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,6 +825,23 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Ensure subgraphs are scaled when entering them
|
||||||
|
this.canvas.canvas.addEventListener<'litegraph:set-graph'>(
|
||||||
|
'litegraph:set-graph',
|
||||||
|
(e) => {
|
||||||
|
const { newGraph, oldGraph } = e.detail
|
||||||
|
// Only scale when switching between graphs (not during initial setup)
|
||||||
|
// oldGraph is null/undefined during initial setup, so skip scaling then
|
||||||
|
if (oldGraph) {
|
||||||
|
ensureCorrectLayoutScale(
|
||||||
|
newGraph.extra.workflowRendererVersion,
|
||||||
|
newGraph
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
registerProxyWidgets(this.canvas)
|
registerProxyWidgets(this.canvas)
|
||||||
|
|
||||||
this.graph.start()
|
this.graph.start()
|
||||||
@@ -1177,7 +1194,20 @@ export class ComfyApp {
|
|||||||
// @ts-expect-error Discrepancies between zod and litegraph - in progress
|
// @ts-expect-error Discrepancies between zod and litegraph - in progress
|
||||||
this.graph.configure(graphData)
|
this.graph.configure(graphData)
|
||||||
|
|
||||||
ensureCorrectLayoutScale()
|
// Save original renderer version before scaling (it gets modified during scaling)
|
||||||
|
const originalMainGraphRenderer = this.graph.extra.workflowRendererVersion
|
||||||
|
|
||||||
|
// Scale main graph
|
||||||
|
ensureCorrectLayoutScale(originalMainGraphRenderer)
|
||||||
|
|
||||||
|
// Scale all subgraphs that were loaded with the workflow
|
||||||
|
// Use original main graph renderer as fallback (not the modified one)
|
||||||
|
for (const subgraph of this.graph.subgraphs.values()) {
|
||||||
|
ensureCorrectLayoutScale(
|
||||||
|
subgraph.extra.workflowRendererVersion || originalMainGraphRenderer,
|
||||||
|
subgraph
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
restore_view &&
|
restore_view &&
|
||||||
|
|||||||