Implement fit-to-view for Vue nodes (#5782)

## Summary

Implemented fit-to-view functionality for Vue nodes with bounds
calculation and viewport animation support.


https://github.com/user-attachments/assets/2ec221f1-9194-4564-95f9-ad4da80f190a

## Changes

- **What**: Added Vue nodes support to fit-to-view command with [bounds
calculation](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
and LiteGraph integration
- **Dependencies**: Added dependency on `layoutStore` and
`selectionBounds` utility

## Review Focus

Bounds calculation accuracy for complex node layouts and animation
performance with large node selections. Verify proper fallback to legacy
LiteGraph behavior when Vue nodes disabled.

```mermaid
graph TD
    A[Fit to View Command] --> B{Vue Nodes Enabled?}
    B -->|Yes| C[Get Selected Nodes]
    B -->|No| D[Legacy LiteGraph Method]
    C --> E{Nodes Selected?}
    E -->|Yes| F[Calculate Selected Bounds]
    E -->|No| G[Calculate All Nodes Bounds]
    F --> H[Convert to LiteGraph Format]
    G --> H
    H --> I[Animate to Bounds]
    D --> J[Canvas fitViewToSelectionAnimated]
    
    style A fill:#f9f9f9,stroke:#333,color:#333
    style I fill:#f9f9f9,stroke:#333,color:#333
    style J fill:#f9f9f9,stroke:#333,color:#333
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5782-Implement-fit-to-view-for-Vue-nodes-27a6d73d365081cb822cd93f557e77b2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
This commit is contained in:
Christian Byrne
2025-09-26 21:32:31 -07:00
committed by GitHub
parent da332ed75d
commit 46ad1318e5
3 changed files with 154 additions and 9 deletions

View File

@@ -25,6 +25,8 @@ import {
useCanvasStore,
useTitleEditorStore
} from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { selectionBounds } from '@/renderer/core/layout/utils/layoutMath'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -316,15 +318,53 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'Zoom to fit',
category: 'view-controls' as const,
function: () => {
if (app.canvas.empty) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
const vueNodesEnabled = useSettingStore().get('Comfy.VueNodes.Enabled')
if (vueNodesEnabled) {
// Get nodes from Vue stores
const canvasStore = useCanvasStore()
const selectedNodeIds = canvasStore.selectedNodeIds
const allNodes = layoutStore.getAllNodes().value
// Get nodes to fit - selected if any, otherwise all
const nodesToFit =
selectedNodeIds.size > 0
? Array.from(selectedNodeIds)
.map((id) => allNodes.get(id))
.filter((node) => node != null)
: Array.from(allNodes.values())
// Use Vue nodes bounds calculation
const bounds = selectionBounds(nodesToFit)
if (!bounds) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
}
// Convert to LiteGraph format and animate
const lgBounds = [
bounds.x,
bounds.y,
bounds.width,
bounds.height
] as const
const setDirty = () => app.canvas.setDirty(true, true)
app.canvas.ds.animateToBounds(lgBounds, setDirty)
} else {
if (app.canvas.empty) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
}
app.canvas.fitViewToSelectionAnimated()
}
app.canvas.fitViewToSelectionAnimated()
}
},
{

View File

@@ -1,4 +1,4 @@
import type { Bounds, Point } from '@/renderer/core/layout/types'
import type { Bounds, NodeLayout, Point } from '@/renderer/core/layout/types'
export const REROUTE_RADIUS = 8
@@ -19,3 +19,35 @@ export function boundsIntersect(a: Bounds, b: Bounds): boolean {
b.y + b.height < a.y
)
}
export function calculateBounds(nodes: NodeLayout[]): Bounds {
let minX = Infinity,
minY = Infinity
let maxX = -Infinity,
maxY = -Infinity
for (const node of nodes) {
const bounds = node.bounds
minX = Math.min(minX, bounds.x)
minY = Math.min(minY, bounds.y)
maxX = Math.max(maxX, bounds.x + bounds.width)
maxY = Math.max(maxY, bounds.y + bounds.height)
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
}
/**
* Calculate combined bounds for Vue nodes selection
* @param nodes Array of NodeLayout objects to calculate bounds for
* @returns Bounds of the nodes or null if no nodes provided
*/
export function selectionBounds(nodes: NodeLayout[]): Bounds | null {
if (nodes.length === 0) return null
return calculateBounds(nodes)
}