mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 01:50:08 +00:00
[feat] Add Vue-based node rendering system with widget support
Complete Vue node lifecycle management system that safely extracts data from LiteGraph and renders nodes with working widgets in Vue components. Key features: - Safe data extraction pattern to avoid Vue proxy issues with LiteGraph private fields - Event-driven lifecycle management using onNodeAdded/onNodeRemoved hooks - Widget system integration with functional dropdowns, inputs, and controls - Performance optimizations including viewport culling and RAF batching - Transform container pattern for O(1) scaling regardless of node count - QuadTree spatial indexing for efficient visibility queries - Debug tools and performance monitoring - Feature flag system for safe rollout Architecture: - LiteGraph remains source of truth for all graph logic and data - Vue components render nodes positioned over canvas using CSS transforms - Widget updates flow through LiteGraph callbacks to maintain consistency - Reactive state separated from node references to prevent proxy overhead Components: - useGraphNodeManager: Core lifecycle management with safe data extraction - TransformPane: Performance-optimized viewport container - LGraphNode.vue: Vue node component with widget rendering - Widget system: PrimeVue-based components for all widget types
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
import type { LGraph, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { nextTick, reactive, readonly } from 'vue'
|
||||
import { QuadTree, type Bounds } from '@/utils/spatial/QuadTree'
|
||||
|
||||
import { type Bounds, QuadTree } from '../../utils/spatial/QuadTree'
|
||||
|
||||
export interface NodeState {
|
||||
visible: boolean
|
||||
@@ -52,10 +53,8 @@ export interface VueNodeData {
|
||||
}
|
||||
|
||||
export interface SpatialMetrics {
|
||||
strategy: 'linear' | 'quadtree'
|
||||
queryTime: number
|
||||
nodesInIndex: number
|
||||
threshold: number
|
||||
}
|
||||
|
||||
export interface GraphNodeManager {
|
||||
@@ -86,14 +85,12 @@ export interface GraphNodeManager {
|
||||
// Performance
|
||||
performanceMetrics: PerformanceMetrics
|
||||
spatialMetrics: SpatialMetrics
|
||||
|
||||
|
||||
// Debug
|
||||
getSpatialIndexDebugInfo(): any | null
|
||||
}
|
||||
|
||||
export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
console.log('[useGraphNodeManager] Initializing with graph:', graph)
|
||||
|
||||
// Safe reactive data extracted from LiteGraph nodes
|
||||
const vueNodeData = reactive(new Map<string, VueNodeData>())
|
||||
const nodeState = reactive(new Map<string, NodeState>())
|
||||
@@ -120,21 +117,17 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
adaptiveQuality: false
|
||||
})
|
||||
|
||||
// Spatial indexing for large graphs (auto-enables at 100+ nodes)
|
||||
const SPATIAL_INDEX_THRESHOLD = 100
|
||||
// Spatial indexing using QuadTree
|
||||
const spatialIndex = new QuadTree<string>(
|
||||
{ x: -10000, y: -10000, width: 20000, height: 20000 },
|
||||
{ maxDepth: 6, maxItemsPerNode: 4 }
|
||||
)
|
||||
let spatialIndexEnabled = false
|
||||
let lastSpatialQueryTime = 0
|
||||
|
||||
// Spatial metrics
|
||||
const spatialMetrics = reactive<SpatialMetrics>({
|
||||
strategy: 'linear',
|
||||
queryTime: 0,
|
||||
nodesInIndex: 0,
|
||||
threshold: SPATIAL_INDEX_THRESHOLD
|
||||
nodesInIndex: 0
|
||||
})
|
||||
|
||||
// FPS tracking state
|
||||
@@ -146,7 +139,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
frameCount++
|
||||
const now = performance.now()
|
||||
const elapsed = now - lastFrameTime
|
||||
|
||||
|
||||
if (elapsed >= 1000) {
|
||||
performanceMetrics.fps = Math.round((frameCount * 1000) / elapsed)
|
||||
performanceMetrics.frameTime = elapsed / frameCount
|
||||
@@ -157,7 +150,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
const startFPSTracking = () => {
|
||||
if (fpsUpdateInterval) return
|
||||
|
||||
|
||||
const trackFrame = () => {
|
||||
updateFPS()
|
||||
fpsUpdateInterval = requestAnimationFrame(trackFrame)
|
||||
@@ -221,12 +214,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
callback: widget.callback
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[useGraphNodeManager] Error extracting widget data for',
|
||||
widget.name,
|
||||
':',
|
||||
error
|
||||
)
|
||||
return {
|
||||
name: widget.name || 'unknown',
|
||||
type: widget.type || 'text',
|
||||
@@ -257,36 +244,35 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
||||
const nodeId = String(node.id)
|
||||
|
||||
node.widgets?.forEach(widget => {
|
||||
|
||||
node.widgets?.forEach((widget) => {
|
||||
// Create a new callback that updates the widget value AND the Vue state
|
||||
const originalCallback = widget.callback
|
||||
widget.callback = (value: string | number | boolean | object | undefined, ...args: unknown[]) => {
|
||||
widget.callback = (value: unknown) => {
|
||||
// 1. Update the widget value in LiteGraph
|
||||
widget.value = value
|
||||
|
||||
// Widget value can be various types, cast appropriately
|
||||
widget.value = value as string | number | boolean | object | undefined
|
||||
|
||||
// 2. Call the original callback if it exists
|
||||
if (originalCallback) {
|
||||
originalCallback.call(widget, value as Parameters<typeof originalCallback>[0], ...args)
|
||||
originalCallback.call(widget, value)
|
||||
}
|
||||
|
||||
|
||||
// 3. Update Vue state
|
||||
try {
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
if (currentData?.widgets) {
|
||||
const updatedWidgets = currentData.widgets.map(w =>
|
||||
w.name === widget.name
|
||||
? { ...w, value: value }
|
||||
: w
|
||||
const updatedWidgets = currentData.widgets.map((w) =>
|
||||
w.name === widget.name ? { ...w, value: value } : w
|
||||
)
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
widgets: updatedWidgets
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
widgets: updatedWidgets
|
||||
})
|
||||
}
|
||||
performanceMetrics.callbackUpdateCount++
|
||||
} catch (error) {
|
||||
console.warn(`[useGraphNodeManager] Failed to update Vue state for widget ${widget.name}:`, error)
|
||||
// Ignore widget update errors to prevent cascade failures
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -363,7 +349,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))
|
||||
|
||||
// Remove deleted nodes
|
||||
for (const [id] of vueNodeData) {
|
||||
for (const id of Array.from(vueNodeData.keys())) {
|
||||
if (!currentNodes.has(id)) {
|
||||
nodeRefs.delete(id)
|
||||
vueNodeData.delete(id)
|
||||
@@ -371,6 +357,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
nodePositions.delete(id)
|
||||
nodeSizes.delete(id)
|
||||
lastNodesSnapshot.delete(id)
|
||||
spatialIndex.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +381,15 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||
attachMetadata(node)
|
||||
|
||||
// Add to spatial index
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size[0],
|
||||
height: node.size[1]
|
||||
}
|
||||
spatialIndex.insert(id, bounds, id)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -405,61 +401,28 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
}
|
||||
|
||||
// Most performant: Direct position sync without re-setting entire node
|
||||
// Query visible nodes using spatial index when available
|
||||
// Query visible nodes using QuadTree spatial index
|
||||
const getVisibleNodeIds = (viewportBounds: Bounds): Set<string> => {
|
||||
const startTime = performance.now()
|
||||
|
||||
let visibleIds: Set<string>
|
||||
|
||||
if (spatialIndexEnabled) {
|
||||
// Use QuadTree for fast spatial query
|
||||
const results = spatialIndex.query(viewportBounds)
|
||||
visibleIds = new Set(results)
|
||||
} else {
|
||||
// Use simple linear search for small graphs
|
||||
visibleIds = new Set<string>()
|
||||
for (const [id, pos] of nodePositions) {
|
||||
const size = nodeSizes.get(id)
|
||||
if (!size) continue
|
||||
|
||||
// Simple bounds check
|
||||
if (!(
|
||||
pos.x + size.width < viewportBounds.x ||
|
||||
pos.x > viewportBounds.x + viewportBounds.width ||
|
||||
pos.y + size.height < viewportBounds.y ||
|
||||
pos.y > viewportBounds.y + viewportBounds.height
|
||||
)) {
|
||||
visibleIds.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Use QuadTree for fast spatial query
|
||||
const results: string[] = spatialIndex.query(viewportBounds)
|
||||
const visibleIds = new Set(results)
|
||||
|
||||
lastSpatialQueryTime = performance.now() - startTime
|
||||
spatialMetrics.queryTime = lastSpatialQueryTime
|
||||
|
||||
|
||||
return visibleIds
|
||||
}
|
||||
|
||||
const detectChangesInRAF = () => {
|
||||
const startTime = performance.now()
|
||||
|
||||
|
||||
if (!graph?._nodes) return
|
||||
|
||||
let positionUpdates = 0
|
||||
let sizeUpdates = 0
|
||||
|
||||
// Check if we should enable/disable spatial indexing
|
||||
const nodeCount = graph._nodes.length
|
||||
const shouldUseSpatialIndex = nodeCount >= SPATIAL_INDEX_THRESHOLD
|
||||
|
||||
if (shouldUseSpatialIndex !== spatialIndexEnabled) {
|
||||
spatialIndexEnabled = shouldUseSpatialIndex
|
||||
if (!spatialIndexEnabled) {
|
||||
spatialIndex.clear()
|
||||
}
|
||||
spatialMetrics.strategy = spatialIndexEnabled ? 'quadtree' : 'linear'
|
||||
}
|
||||
|
||||
// Update reactive positions and sizes
|
||||
for (const node of graph._nodes) {
|
||||
const id = String(node.id)
|
||||
@@ -489,8 +452,8 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
sizeChanged = true
|
||||
}
|
||||
|
||||
// Update spatial index if enabled and position/size changed
|
||||
if (spatialIndexEnabled && (posChanged || sizeChanged)) {
|
||||
// Update spatial index if position/size changed
|
||||
if (posChanged || sizeChanged) {
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
@@ -506,10 +469,10 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
performanceMetrics.updateTime = endTime - startTime
|
||||
performanceMetrics.nodeCount = vueNodeData.size
|
||||
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
|
||||
state => state.culled
|
||||
(state) => state.culled
|
||||
).length
|
||||
spatialMetrics.nodesInIndex = spatialIndexEnabled ? spatialIndex.size : 0
|
||||
|
||||
spatialMetrics.nodesInIndex = spatialIndex.size
|
||||
|
||||
if (positionUpdates > 0 || sizeUpdates > 0) {
|
||||
performanceMetrics.rafUpdateCount++
|
||||
}
|
||||
@@ -522,12 +485,11 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
// Override callbacks
|
||||
graph.onNodeAdded = (node: LGraphNode) => {
|
||||
console.log('[useGraphNodeManager] onNodeAdded:', node.id)
|
||||
const id = String(node.id)
|
||||
|
||||
// Store non-reactive reference to original node
|
||||
nodeRefs.set(id, node)
|
||||
|
||||
|
||||
// Set up widget callbacks BEFORE extracting data
|
||||
setupNodeWidgetCallbacks(node)
|
||||
|
||||
@@ -544,18 +506,16 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||
attachMetadata(node)
|
||||
|
||||
// Add to spatial index if enabled
|
||||
if (spatialIndexEnabled) {
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size[0],
|
||||
height: node.size[1]
|
||||
}
|
||||
spatialIndex.insert(id, bounds, id)
|
||||
|
||||
// Add to spatial index
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size[0],
|
||||
height: node.size[1]
|
||||
}
|
||||
|
||||
spatialIndex.insert(id, bounds, id)
|
||||
|
||||
if (originalOnNodeAdded) {
|
||||
void originalOnNodeAdded(node)
|
||||
}
|
||||
@@ -563,12 +523,10 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
graph.onNodeRemoved = (node: LGraphNode) => {
|
||||
const id = String(node.id)
|
||||
|
||||
// Remove from spatial index if enabled
|
||||
if (spatialIndexEnabled) {
|
||||
spatialIndex.remove(id)
|
||||
}
|
||||
|
||||
|
||||
// Remove from spatial index
|
||||
spatialIndex.remove(id)
|
||||
|
||||
nodeRefs.delete(id)
|
||||
vueNodeData.delete(id)
|
||||
nodeState.delete(id)
|
||||
@@ -580,7 +538,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
// Initial sync
|
||||
syncWithGraph()
|
||||
|
||||
|
||||
// Start FPS tracking
|
||||
startFPSTracking()
|
||||
|
||||
@@ -595,7 +553,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
clearTimeout(batchTimeoutId)
|
||||
batchTimeoutId = null
|
||||
}
|
||||
|
||||
|
||||
// Stop FPS tracking
|
||||
stopFPSTracking()
|
||||
|
||||
@@ -616,6 +574,15 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
// Set up event listeners immediately
|
||||
const cleanup = setupEventListeners()
|
||||
|
||||
// Process any existing nodes after event listeners are set up
|
||||
if (graph._nodes && graph._nodes.length > 0) {
|
||||
graph._nodes.forEach((node: LGraphNode) => {
|
||||
if (graph.onNodeAdded) {
|
||||
graph.onNodeAdded(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
vueNodeData: readonly(vueNodeData) as ReadonlyMap<string, VueNodeData>,
|
||||
nodeState: readonly(nodeState) as ReadonlyMap<string, NodeState>,
|
||||
@@ -636,6 +603,6 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
getVisibleNodeIds,
|
||||
performanceMetrics,
|
||||
spatialMetrics: readonly(spatialMetrics),
|
||||
getSpatialIndexDebugInfo: () => spatialIndexEnabled ? spatialIndex.getDebugInfo() : null
|
||||
getSpatialIndexDebugInfo: () => spatialIndex.getDebugInfo()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user