Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
eca48a8
[docs] Add Vue node system architecture and implementation plans
christian-byrne Jun 25, 2025
0ec98e3
[feat] Add slot type color definitions
christian-byrne Jun 25, 2025
a041f40
[feat] Implement Vue-based node rendering components
christian-byrne Jun 25, 2025
065e292
[feat] Add TransformPane for Vue node coordinate synchronization
christian-byrne Jun 30, 2025
992d79b
[feat] Vue node lifecycle management implementation
christian-byrne Jul 2, 2025
3dc7686
[feat] Add widget renderer composable
christian-byrne Jul 2, 2025
95c291d
[fix] Fix WidgetSelect component for combo widgets
christian-byrne Jul 2, 2025
6bbd285
[feat] Update NodeWidgets to use safe widget data
christian-byrne Jul 2, 2025
39603dd
[feat] Update Vue node components with proper typing
christian-byrne Jul 2, 2025
04e9a79
[feat] Update GraphCanvas with VueNodeData typing
christian-byrne Jul 2, 2025
222a52d
[feat] Add feature flags and utility updates
christian-byrne Jul 2, 2025
124db59
[feat] Implement callback-driven widget updates
christian-byrne Jul 3, 2025
cd3296f
[feat] Add viewport debug overlay for TransformPane
christian-byrne Jul 3, 2025
122170f
[fix] Fix widget value synchronization between Vue and LiteGraph
christian-byrne Jul 4, 2025
0de3b8a
[feat] Add QuadTree spatial data structure for node indexing
christian-byrne Jul 4, 2025
a23d8be
[feat] Add Vue-based node rendering system with widget support
christian-byrne Jul 5, 2025
c3023e4
[fix] Remove FPS tracking to prevent memory leaks
christian-byrne Jul 5, 2025
cdd940e
[fix] Add proper cleanup for nodeManager to prevent memory leaks
christian-byrne Jul 5, 2025
95ab702
[test] Add test helper utilities for Vue node system testing
christian-byrne Jul 5, 2025
a58a354
[test] Add comprehensive tests for transform and spatial composables
christian-byrne Jul 5, 2025
32ddf72
[test] Add TransformPane component tests
christian-byrne Jul 5, 2025
c246326
[test] Add performance tests for transform operations
christian-byrne Jul 5, 2025
71c3c72
[feat] Implement LOD (Level of Detail) system for Vue nodes
christian-byrne Jul 5, 2025
290906e
[refactor] Improve type safety across Vue node widget system
christian-byrne Jul 5, 2025
5cb9ba1
[feat] Add CSS LOD classes for Vue node rendering optimization
christian-byrne Jul 5, 2025
d6315a1
[feat] Add debug type definitions for spatial indexing system
christian-byrne Jul 5, 2025
7d7dc09
[perf] Optimize TransformPane interaction tracking for better perform…
christian-byrne Jul 5, 2025
d29ce21
[refactor] Remove unused variables in GraphCanvas to fix TypeScript w…
christian-byrne Jul 5, 2025
57b09da
[test] Add missing useWidgetValue import in test file
christian-byrne Jul 5, 2025
18854d7
[cleanup] Remove temporary documentation and planning files
christian-byrne Jul 5, 2025
555e806
[perf] Optimize widget rendering performance
christian-byrne Jul 6, 2025
9a93764
[refactor] Extract canvas transform sync to dedicated composables
christian-byrne Jul 6, 2025
4304bb3
[test] Relocate and update test files
christian-byrne Jul 6, 2025
30728c1
[cleanup] Remove unused viewport culling settings and variables
christian-byrne Jul 6, 2025
32c8d0c
[refactor] Move QuadTreeBenchmark to test directory
christian-byrne Jul 6, 2025
1098d3b
[feat] Enhanced LOD system with component-driven approach (#4371)
christian-byrne Jul 7, 2025
108e54c
[perf] Global CSS optimizations for LOD system (#4379)
christian-byrne Jul 8, 2025
c6422da
Vue Node Body (#4387)
benceruleanlu Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[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
  • Loading branch information
christian-byrne committed Jul 5, 2025
commit a23d8be77b20b5c2c1b5e34e79c8f46cab50e745
File renamed without changes.
124 changes: 85 additions & 39 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,11 @@ watch(
)

// Transform state for viewport culling
const { isNodeInViewport } = useTransformState()
const { syncWithCanvas } = useTransformState()

// Viewport culling settings - use feature flags as defaults but allow debug override
const viewportCullingEnabled = ref(false) // Debug override, starts false for testing
const cullingMargin = ref(0.2) // Debug override
const viewportCullingEnabled = ref(true) // Enable viewport culling
const cullingMargin = ref(0.2) // 20% margin outside viewport

// Initialize from feature flags
watch(
Expand All @@ -467,47 +467,63 @@ watch(
// Replace problematic computed property with proper reactive system
const nodesToRender = computed(() => {
// Access performanceMetrics to trigger on RAF updates
const updateCount = performanceMetrics.updateTime

console.log(
'[GraphCanvas] Computing nodesToRender. renderAllNodes:',
renderAllNodes.value,
'vueNodeData size:',
vueNodeData.value.size,
'updateCount:',
updateCount,
'transformPaneEnabled:',
transformPaneEnabled.value,
'shouldRenderVueNodes:',
shouldRenderVueNodes.value
)
if (!renderAllNodes.value || !comfyApp.graph) {
console.log(
'[GraphCanvas] Early return - renderAllNodes:',
renderAllNodes.value,
'graph:',
!!comfyApp.graph
)
void performanceMetrics.updateTime

if (!renderAllNodes.value || !comfyApp.graph || !transformPaneEnabled.value) {
return []
}

const allNodes = Array.from(vueNodeData.value.values())

// Apply viewport culling
if (viewportCullingEnabled.value && nodeManager) {
// Apply viewport culling - check if node bounds intersect with viewport
if (
viewportCullingEnabled.value &&
nodeManager &&
canvasStore.canvas &&
comfyApp.canvas
) {
const canvas = canvasStore.canvas
const manager = nodeManager

// Ensure transform is synced before checking visibility
syncWithCanvas(comfyApp.canvas)

const ds = canvas.ds

// Access transform time to make this reactive to transform changes
void lastTransformTime.value

// 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 originalNode = nodeManager?.getNode(nodeData.id)
if (!originalNode) return false

const inViewport = isNodeInViewport(
originalNode.pos,
originalNode.size,
canvasViewport.value,
cullingMargin.value
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 inViewport
return isVisible
})

return filtered
}

Expand All @@ -530,9 +546,42 @@ watch(
}
)

// Update performance metrics when node counts change
watch(
() => [vueNodeData.value.size, nodesToRender.value.length],
([totalNodes, visibleNodes]) => {
performanceMetrics.nodeCount = totalNodes
performanceMetrics.culledCount = totalNodes - visibleNodes
}
)

// Integrate change detection with TransformPane RAF
// Track previous transform to detect changes
let lastScale = 1
let lastOffsetX = 0
let lastOffsetY = 0

const handleTransformUpdate = (time: number) => {
lastTransformTime.value = time

// 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 ||
currentOffsetX !== lastOffsetX ||
currentOffsetY !== lastOffsetY
) {
syncWithCanvas(comfyApp.canvas)
lastScale = currentScale
lastOffsetX = currentOffsetX
lastOffsetY = currentOffsetY
}
}

// Detect node changes during transform updates
detectChangesInRAF()

Expand Down Expand Up @@ -706,7 +755,7 @@ const loadCustomNodesI18n = async () => {
i18n.global.mergeLocaleMessage(locale, message)
})
} catch (error) {
console.error('Failed to load custom nodes i18n', error)
// Ignore i18n loading errors - not critical
}
}

Expand Down Expand Up @@ -735,9 +784,6 @@ onMounted(async () => {
await settingStore.loadSettingValues()
} catch (error) {
if (error instanceof UnauthorizedError) {
console.log(
'Failed loading user settings, user unauthorized, cleaning local Comfy.userId'
)
localStorage.removeItem('Comfy.userId')
localStorage.removeItem('Comfy.userName')
window.location.reload()
Expand Down
109 changes: 109 additions & 0 deletions src/components/graph/debug/QuadTreeDebugSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<template>
<div class="pt-2 border-t border-surface-200 dark-theme:border-surface-700">
<h4 class="font-semibold mb-1">QuadTree Spatial Index</h4>

<!-- Enable/Disable Toggle -->
<div class="mb-2">
<label class="flex items-center gap-2">
<input
:checked="enabled"
type="checkbox"
@change="$emit('toggle', ($event.target as HTMLInputElement).checked)"
/>
<span>Enable Spatial Indexing</span>
</label>
</div>

<!-- Status Message -->
<p v-if="!enabled" class="text-muted text-xs italic">
{{ statusMessage }}
</p>

<!-- Metrics when enabled -->
<template v-if="enabled && metrics">
<p class="text-muted">Strategy: {{ strategy }}</p>
<p class="text-muted">Total Nodes: {{ metrics.totalNodes }}</p>
<p class="text-muted">Visible Nodes: {{ metrics.visibleNodes }}</p>
<p class="text-muted">Query Time: {{ metrics.queryTime.toFixed(2) }}ms</p>
<p class="text-muted">Tree Depth: {{ metrics.treeDepth }}</p>
<p class="text-muted">Culling Efficiency: {{ cullingEfficiency }}</p>
<p class="text-muted">Rebuilds: {{ metrics.rebuildCount }}</p>

<!-- Show debug visualization toggle -->
<div class="mt-2">
<label class="flex items-center gap-2">
<input
:checked="showVisualization"
type="checkbox"
@change="
$emit(
'toggle-visualization',
($event.target as HTMLInputElement).checked
)
"
/>
<span>Show QuadTree Boundaries</span>
</label>
</div>
</template>

<!-- Performance Comparison -->
<template v-if="enabled && performanceComparison">
<div class="mt-2 text-xs">
<p class="text-muted font-semibold">Performance vs Linear:</p>
<p class="text-muted">Speedup: {{ performanceComparison.speedup }}x</p>
<p class="text-muted">
Break-even: ~{{ performanceComparison.breakEvenNodeCount }} nodes
</p>
</div>
</template>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
enabled: boolean
metrics?: {
totalNodes: number
visibleNodes: number
queryTime: number
treeDepth: number
rebuildCount: number
}
strategy?: string
threshold?: number
showVisualization?: boolean
performanceComparison?: {
speedup: number
breakEvenNodeCount: number
}
}

const props = withDefaults(defineProps<Props>(), {
strategy: 'quadtree',
threshold: 100,
showVisualization: false
})

defineEmits<{
toggle: [enabled: boolean]
'toggle-visualization': [show: boolean]
}>()

const statusMessage = computed(() => {
if (!props.enabled && props.metrics) {
return `Disabled (threshold: ${props.threshold} nodes, current: ${props.metrics.totalNodes})`
}
return `Spatial indexing will enable at ${props.threshold}+ nodes`
})

const cullingEfficiency = computed(() => {
if (!props.metrics || props.metrics.totalNodes === 0) return 'N/A'

const culled = props.metrics.totalNodes - props.metrics.visibleNodes
const percentage = ((culled / props.metrics.totalNodes) * 100).toFixed(1)
return `${culled} nodes (${percentage}%)`
})
</script>
112 changes: 112 additions & 0 deletions src/components/graph/debug/QuadTreeVisualization.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<template>
<svg
v-if="visible && debugInfo"
:width="svgSize.width"
:height="svgSize.height"
:style="svgStyle"
class="quadtree-visualization"
>
<!-- QuadTree boundaries -->
<g v-for="(node, index) in flattenedNodes" :key="`quad-${index}`">
<rect
:x="node.bounds.x"
:y="node.bounds.y"
:width="node.bounds.width"
:height="node.bounds.height"
:stroke="getDepthColor(node.depth)"
:stroke-width="getStrokeWidth(node.depth)"
fill="none"
:opacity="0.3 + node.depth * 0.05"
/>
</g>

<!-- Viewport bounds (optional) -->
<rect
v-if="viewportBounds"
:x="viewportBounds.x"
:y="viewportBounds.y"
:width="viewportBounds.width"
:height="viewportBounds.height"
stroke="#00ff00"
stroke-width="3"
fill="none"
stroke-dasharray="10,5"
opacity="0.8"
/>
</svg>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

import type { Bounds } from '@/utils/spatial/QuadTree'

interface Props {
visible: boolean
debugInfo: any | null
transformStyle: any
viewportBounds?: Bounds
}

const props = defineProps<Props>()

// Flatten the tree structure for rendering
const flattenedNodes = computed(() => {
if (!props.debugInfo?.tree) return []

const nodes: any[] = []
const traverse = (node: any, depth = 0) => {
nodes.push({
bounds: node.bounds,
depth,
itemCount: node.itemCount,
divided: node.divided
})

if (node.children) {
node.children.forEach((child: any) => traverse(child, depth + 1))
}
}

traverse(props.debugInfo.tree)
return nodes
})

// SVG size (matches the transform pane size)
const svgSize = ref({ width: 20000, height: 20000 })

// Apply the same transform as the TransformPane
const svgStyle = computed(() => ({
...props.transformStyle,
position: 'absolute',
top: 0,
left: 0,
pointerEvents: 'none'
}))

// Color based on depth
const getDepthColor = (depth: number): string => {
const colors = [
'#ff6b6b', // Red
'#ffa500', // Orange
'#ffd93d', // Yellow
'#6bcf7f', // Green
'#4da6ff', // Blue
'#a78bfa' // Purple
]
return colors[depth % colors.length]
}

// Stroke width based on depth
const getStrokeWidth = (depth: number): number => {
return Math.max(0.5, 2 - depth * 0.3)
}
</script>

<style scoped>
.quadtree-visualization {
position: absolute;
overflow: visible;
z-index: 10; /* Above nodes but below UI */
}
</style>
Loading