mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Fix/vue nodes viewport culling (#5510)
* debug: disable culling flag * fix: vue nodes LOD * fix: viewport culling and improve perf * fix: PR feedback and more perf improvements * refactor: forEach to for of * fix: PR feedback * fix: PR feedback * fix: PR feedback * fix: PR feedback --------- Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com>
This commit is contained in:
@@ -40,7 +40,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Vue nodes rendered based on graph nodes -->
|
<!-- Vue nodes rendered based on graph nodes -->
|
||||||
<VueGraphNode
|
<VueGraphNode
|
||||||
v-for="nodeData in nodesToRender"
|
v-for="nodeData in allNodes"
|
||||||
:key="nodeData.id"
|
:key="nodeData.id"
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:position="nodePositions.get(nodeData.id)"
|
:position="nodePositions.get(nodeData.id)"
|
||||||
@@ -183,12 +183,12 @@ const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
|
|||||||
|
|
||||||
const nodePositions = vueNodeLifecycle.nodePositions
|
const nodePositions = vueNodeLifecycle.nodePositions
|
||||||
const nodeSizes = vueNodeLifecycle.nodeSizes
|
const nodeSizes = vueNodeLifecycle.nodeSizes
|
||||||
const nodesToRender = viewportCulling.nodesToRender
|
const allNodes = viewportCulling.allNodes
|
||||||
|
|
||||||
const handleTransformUpdate = () => {
|
const handleTransformUpdate = () => {
|
||||||
viewportCulling.handleTransformUpdate(
|
viewportCulling.handleTransformUpdate()
|
||||||
vueNodeLifecycle.detectChangesInRAF.value
|
// TODO: Fix paste position sync in separate PR
|
||||||
)
|
vueNodeLifecycle.detectChangesInRAF.value()
|
||||||
}
|
}
|
||||||
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
|
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
|
||||||
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Viewport Culling Composable
|
* Vue Nodes Viewport Culling
|
||||||
*
|
*
|
||||||
* Handles viewport culling optimization for Vue nodes including:
|
* Principles:
|
||||||
* - Transform state synchronization
|
* 1. Query DOM directly using data attributes (no cache to maintain)
|
||||||
* - Visible node calculation with screen space transforms
|
* 2. Set display none on element to avoid cascade resolution overhead
|
||||||
* - Adaptive margin computation based on zoom level
|
* 3. Only run when transform changes (event driven)
|
||||||
* - Performance optimizations for large graphs
|
|
||||||
*/
|
*/
|
||||||
import { type Ref, computed, readonly, ref } from 'vue'
|
import { type Ref, computed } from 'vue'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useTransformState } from '@/renderer/core/layout/useTransformState'
|
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
@@ -25,188 +23,84 @@ export function useViewportCulling(
|
|||||||
nodeManager: Ref<NodeManager | null>
|
nodeManager: Ref<NodeManager | null>
|
||||||
) {
|
) {
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const { syncWithCanvas } = useTransformState()
|
|
||||||
|
|
||||||
// Transform tracking for performance optimization
|
const allNodes = computed(() => {
|
||||||
const lastScale = ref(1)
|
if (!isVueNodesEnabled.value) return []
|
||||||
const lastOffsetX = ref(0)
|
void nodeDataTrigger.value // Force re-evaluation when nodeManager initializes
|
||||||
const lastOffsetY = ref(0)
|
return Array.from(vueNodeData.value.values())
|
||||||
|
|
||||||
// Current transform state
|
|
||||||
const currentTransformState = computed(() => ({
|
|
||||||
scale: lastScale.value,
|
|
||||||
offsetX: lastOffsetX.value,
|
|
||||||
offsetY: lastOffsetY.value
|
|
||||||
}))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property that returns nodes visible in the current viewport
|
|
||||||
* Implements sophisticated culling algorithm with adaptive margins
|
|
||||||
*/
|
|
||||||
const nodesToRender = computed(() => {
|
|
||||||
if (!isVueNodesEnabled.value) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access trigger to force re-evaluation after nodeManager initialization
|
|
||||||
void nodeDataTrigger.value
|
|
||||||
|
|
||||||
if (!comfyApp.graph) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const allNodes = Array.from(vueNodeData.value.values())
|
|
||||||
|
|
||||||
// Apply viewport culling - check if node bounds intersect with viewport
|
|
||||||
// TODO: use quadtree
|
|
||||||
if (nodeManager.value && canvasStore.canvas && comfyApp.canvas) {
|
|
||||||
const canvas = canvasStore.canvas
|
|
||||||
const manager = nodeManager.value
|
|
||||||
|
|
||||||
// Ensure transform is synced before checking visibility
|
|
||||||
syncWithCanvas(comfyApp.canvas)
|
|
||||||
|
|
||||||
const ds = canvas.ds
|
|
||||||
|
|
||||||
// Work in screen space - viewport is simply the canvas element size
|
|
||||||
const viewport_width = canvas.canvas.width
|
|
||||||
const viewport_height = canvas.canvas.height
|
|
||||||
|
|
||||||
// Add margin that represents a constant distance in canvas space
|
|
||||||
// Convert canvas units to screen pixels by multiplying by scale
|
|
||||||
const canvasMarginDistance = 200 // Fixed margin in canvas units
|
|
||||||
const margin_x = canvasMarginDistance * ds.scale
|
|
||||||
const margin_y = canvasMarginDistance * ds.scale
|
|
||||||
|
|
||||||
const filtered = allNodes.filter((nodeData) => {
|
|
||||||
const node = manager.getNode(nodeData.id)
|
|
||||||
if (!node) return false
|
|
||||||
|
|
||||||
// Transform node position to screen space (same as DOM widgets)
|
|
||||||
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
|
||||||
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
|
||||||
const screen_width = node.size[0] * ds.scale
|
|
||||||
const screen_height = node.size[1] * ds.scale
|
|
||||||
|
|
||||||
// Check if node bounds intersect with expanded viewport (in screen space)
|
|
||||||
const isVisible = !(
|
|
||||||
screen_x + screen_width < -margin_x ||
|
|
||||||
screen_x > viewport_width + margin_x ||
|
|
||||||
screen_y + screen_height < -margin_y ||
|
|
||||||
screen_y > viewport_height + margin_y
|
|
||||||
)
|
|
||||||
|
|
||||||
return isVisible
|
|
||||||
})
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
return allNodes
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle transform updates with performance optimization
|
* Update visibility of all nodes based on viewport
|
||||||
* Only syncs when transform actually changes to avoid unnecessary reflows
|
* Queries DOM directly - no cache maintenance needed
|
||||||
*/
|
*/
|
||||||
const handleTransformUpdate = (detectChangesInRAF: () => void) => {
|
const updateVisibility = () => {
|
||||||
// Skip all work if Vue nodes are disabled
|
if (!nodeManager.value || !canvasStore.canvas || !comfyApp.canvas) return
|
||||||
if (!isVueNodesEnabled.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync transform state only when it changes (avoids reflows)
|
|
||||||
if (comfyApp.canvas?.ds) {
|
|
||||||
const currentScale = comfyApp.canvas.ds.scale
|
|
||||||
const currentOffsetX = comfyApp.canvas.ds.offset[0]
|
|
||||||
const currentOffsetY = comfyApp.canvas.ds.offset[1]
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentScale !== lastScale.value ||
|
|
||||||
currentOffsetX !== lastOffsetX.value ||
|
|
||||||
currentOffsetY !== lastOffsetY.value
|
|
||||||
) {
|
|
||||||
syncWithCanvas(comfyApp.canvas)
|
|
||||||
lastScale.value = currentScale
|
|
||||||
lastOffsetX.value = currentOffsetX
|
|
||||||
lastOffsetY.value = currentOffsetY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect node changes during transform updates
|
|
||||||
detectChangesInRAF()
|
|
||||||
|
|
||||||
// Trigger reactivity for nodesToRender
|
|
||||||
void nodesToRender.value.length
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate if a specific node is visible in viewport
|
|
||||||
* Useful for individual node visibility checks
|
|
||||||
*/
|
|
||||||
const isNodeVisible = (nodeData: VueNodeData): boolean => {
|
|
||||||
if (!nodeManager.value || !canvasStore.canvas || !comfyApp.canvas) {
|
|
||||||
return true // Default to visible if culling not available
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = canvasStore.canvas
|
const canvas = canvasStore.canvas
|
||||||
const node = nodeManager.value.getNode(nodeData.id)
|
const manager = nodeManager.value
|
||||||
if (!node) return false
|
|
||||||
|
|
||||||
syncWithCanvas(comfyApp.canvas)
|
|
||||||
const ds = canvas.ds
|
const ds = canvas.ds
|
||||||
|
|
||||||
|
// Viewport bounds
|
||||||
const viewport_width = canvas.canvas.width
|
const viewport_width = canvas.canvas.width
|
||||||
const viewport_height = canvas.canvas.height
|
const viewport_height = canvas.canvas.height
|
||||||
const canvasMarginDistance = 200
|
const margin = 500 * ds.scale
|
||||||
const margin_x = canvasMarginDistance * ds.scale
|
|
||||||
const margin_y = canvasMarginDistance * ds.scale
|
|
||||||
|
|
||||||
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
// Get all node elements at once
|
||||||
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
const nodeElements = document.querySelectorAll('[data-node-id]')
|
||||||
const screen_width = node.size[0] * ds.scale
|
|
||||||
const screen_height = node.size[1] * ds.scale
|
|
||||||
|
|
||||||
return !(
|
// Update each element's visibility
|
||||||
screen_x + screen_width < -margin_x ||
|
for (const element of nodeElements) {
|
||||||
screen_x > viewport_width + margin_x ||
|
const nodeId = element.getAttribute('data-node-id')
|
||||||
screen_y + screen_height < -margin_y ||
|
if (!nodeId) continue
|
||||||
screen_y > viewport_height + margin_y
|
|
||||||
)
|
const node = manager.getNode(nodeId)
|
||||||
|
if (!node) continue
|
||||||
|
|
||||||
|
// Calculate if node is outside viewport
|
||||||
|
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
||||||
|
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
||||||
|
const screen_width = node.size[0] * ds.scale
|
||||||
|
const screen_height = node.size[1] * ds.scale
|
||||||
|
|
||||||
|
const isNodeOutsideViewport =
|
||||||
|
screen_x + screen_width < -margin ||
|
||||||
|
screen_x > viewport_width + margin ||
|
||||||
|
screen_y + screen_height < -margin ||
|
||||||
|
screen_y > viewport_height + margin
|
||||||
|
|
||||||
|
// Setting display none directly avoid potential cascade resolution
|
||||||
|
if (element instanceof HTMLElement) {
|
||||||
|
element.style.display = isNodeOutsideViewport ? 'none' : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RAF throttling for smooth updates during continuous panning
|
||||||
|
let rafId: number | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get viewport bounds information for debugging
|
* Handle transform update - called by TransformPane event
|
||||||
|
* Uses RAF to batch updates for smooth performance
|
||||||
*/
|
*/
|
||||||
const getViewportInfo = () => {
|
const handleTransformUpdate = () => {
|
||||||
if (!canvasStore.canvas || !comfyApp.canvas) {
|
if (!isVueNodesEnabled.value) return
|
||||||
return null
|
|
||||||
|
// Cancel previous RAF if still pending
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = canvasStore.canvas
|
// Schedule update in next animation frame
|
||||||
const ds = canvas.ds
|
rafId = requestAnimationFrame(() => {
|
||||||
|
updateVisibility()
|
||||||
return {
|
rafId = null
|
||||||
viewport_width: canvas.canvas.width,
|
})
|
||||||
viewport_height: canvas.canvas.height,
|
|
||||||
scale: ds.scale,
|
|
||||||
offset: [ds.offset[0], ds.offset[1]],
|
|
||||||
margin_distance: 200,
|
|
||||||
margin_x: 200 * ds.scale,
|
|
||||||
margin_y: 200 * ds.scale
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodesToRender,
|
allNodes,
|
||||||
handleTransformUpdate,
|
handleTransformUpdate,
|
||||||
isNodeVisible,
|
updateVisibility
|
||||||
getViewportInfo,
|
|
||||||
|
|
||||||
// Transform state
|
|
||||||
currentTransformState: readonly(currentTransformState),
|
|
||||||
lastScale: readonly(lastScale),
|
|
||||||
lastOffsetX: readonly(lastOffsetX),
|
|
||||||
lastOffsetY: readonly(lastOffsetY)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user