mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +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:
@@ -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(
|
||||
@@ -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
|
||||
void 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
|
||||
)
|
||||
if (!renderAllNodes.value || !comfyApp.graph || !transformPaneEnabled.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allNodes = Array.from(vueNodeData.value.values())
|
||||
|
||||
// Apply viewport culling
|
||||
if (viewportCullingEnabled.value && nodeManager) {
|
||||
const filtered = allNodes.filter((nodeData) => {
|
||||
const originalNode = nodeManager?.getNode(nodeData.id)
|
||||
if (!originalNode) return false
|
||||
// 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
|
||||
|
||||
const inViewport = isNodeInViewport(
|
||||
originalNode.pos,
|
||||
originalNode.size,
|
||||
canvasViewport.value,
|
||||
cullingMargin.value
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
109
src/components/graph/debug/QuadTreeDebugSection.vue
Normal file
109
src/components/graph/debug/QuadTreeDebugSection.vue
Normal file
@@ -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
src/components/graph/debug/QuadTreeVisualization.vue
Normal file
112
src/components/graph/debug/QuadTreeVisualization.vue
Normal file
@@ -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>
|
||||
@@ -58,56 +58,29 @@ const widgets = computed((): SafeWidgetData[] => {
|
||||
const info = nodeInfo.value
|
||||
if (!info?.widgets) return []
|
||||
|
||||
console.log('[NodeWidgets] Raw widgets from nodeInfo:', info.widgets)
|
||||
|
||||
const filtered = (info.widgets as SafeWidgetData[]).filter(
|
||||
(w: SafeWidgetData) => !w.options?.hidden
|
||||
)
|
||||
console.log('[NodeWidgets] Filtered widgets:', filtered)
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// Only render widgets that have Vue component support
|
||||
const supportedWidgets = computed((): SafeWidgetData[] => {
|
||||
const allWidgets = widgets.value
|
||||
console.log('[NodeWidgets] All widgets:', allWidgets)
|
||||
|
||||
const supported = allWidgets.filter((widget: SafeWidgetData) => {
|
||||
const isSupported = shouldRenderAsVue(widget)
|
||||
console.log(
|
||||
'[NodeWidgets] Widget:',
|
||||
widget.name,
|
||||
'type:',
|
||||
widget.type,
|
||||
'supported:',
|
||||
isSupported
|
||||
)
|
||||
return isSupported
|
||||
return shouldRenderAsVue(widget)
|
||||
})
|
||||
|
||||
console.log('[NodeWidgets] Supported widgets:', supported)
|
||||
return supported
|
||||
})
|
||||
|
||||
// Get Vue component for widget
|
||||
const getVueComponent = (widget: SafeWidgetData) => {
|
||||
const componentName = getWidgetComponent(widget.type)
|
||||
console.log(
|
||||
'[NodeWidgets] Widget type:',
|
||||
widget.type,
|
||||
'Component name:',
|
||||
componentName
|
||||
)
|
||||
|
||||
const component = widgetTypeToComponent[componentName]
|
||||
console.log('[NodeWidgets] Resolved component:', component)
|
||||
|
||||
return component || WidgetInputText // Fallback to text input
|
||||
}
|
||||
|
||||
const getWidgetValue = (widget: SafeWidgetData): unknown => {
|
||||
console.log('[NodeWidgets] Widget value for', widget.name, ':', widget.value)
|
||||
return widget.value
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,53 @@
|
||||
/**
|
||||
* Composable for managing transform state synchronized with LiteGraph canvas
|
||||
* Provides reactive transform state and coordinate conversion utilities
|
||||
*
|
||||
* This composable is a critical part of the hybrid rendering architecture that
|
||||
* allows Vue components to render in perfect alignment with LiteGraph's canvas.
|
||||
*
|
||||
* ## Core Concept
|
||||
*
|
||||
* LiteGraph uses a canvas for rendering connections, grid, and handling interactions.
|
||||
* Vue components need to render nodes on top of this canvas. The challenge is
|
||||
* synchronizing the coordinate systems:
|
||||
*
|
||||
* - LiteGraph: Uses canvas coordinates with its own transform matrix
|
||||
* - Vue/DOM: Uses screen coordinates with CSS transforms
|
||||
*
|
||||
* ## Solution: Transform Container Pattern
|
||||
*
|
||||
* Instead of transforming individual nodes (O(n) complexity), we:
|
||||
* 1. Mirror LiteGraph's transform matrix to a single CSS container
|
||||
* 2. Place all Vue nodes as children with simple absolute positioning
|
||||
* 3. Achieve O(1) transform updates regardless of node count
|
||||
*
|
||||
* ## Coordinate Systems
|
||||
*
|
||||
* - **Canvas coordinates**: LiteGraph's internal coordinate system
|
||||
* - **Screen coordinates**: Browser's viewport coordinate system
|
||||
* - **Transform sync**: camera.x/y/z mirrors canvas.ds.offset/scale
|
||||
*
|
||||
* ## Performance Benefits
|
||||
*
|
||||
* - GPU acceleration via CSS transforms
|
||||
* - No layout thrashing (only transform changes)
|
||||
* - Efficient viewport culling calculations
|
||||
* - Scales to 1000+ nodes while maintaining 60 FPS
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { camera, transformStyle, canvasToScreen } = useTransformState()
|
||||
*
|
||||
* // In template
|
||||
* <div :style="transformStyle">
|
||||
* <NodeComponent
|
||||
* v-for="node in nodes"
|
||||
* :style="{ left: node.x + 'px', top: node.y + 'px' }"
|
||||
* />
|
||||
* </div>
|
||||
*
|
||||
* // Convert coordinates
|
||||
* const screenPos = canvasToScreen({ x: nodeX, y: nodeY })
|
||||
* ```
|
||||
*/
|
||||
import type { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
@@ -30,16 +77,36 @@ export const useTransformState = () => {
|
||||
transformOrigin: '0 0'
|
||||
}))
|
||||
|
||||
// Sync with LiteGraph during draw cycle
|
||||
/**
|
||||
* Synchronizes Vue's reactive camera state with LiteGraph's canvas transform
|
||||
*
|
||||
* Called every frame via RAF to ensure Vue components stay aligned with canvas.
|
||||
* This is the heart of the hybrid rendering system - it bridges the gap between
|
||||
* LiteGraph's canvas transforms and Vue's reactive system.
|
||||
*
|
||||
* @param canvas - LiteGraph canvas instance with DragAndScale (ds) transform state
|
||||
*/
|
||||
const syncWithCanvas = (canvas: LGraphCanvas) => {
|
||||
if (!canvas || !canvas.ds) return
|
||||
|
||||
// Mirror LiteGraph's transform state to Vue's reactive state
|
||||
// ds.offset = pan offset, ds.scale = zoom level
|
||||
camera.x = canvas.ds.offset[0]
|
||||
camera.y = canvas.ds.offset[1]
|
||||
camera.z = canvas.ds.scale || 1
|
||||
}
|
||||
|
||||
// Convert canvas coordinates to screen coordinates
|
||||
/**
|
||||
* Converts canvas coordinates to screen coordinates
|
||||
*
|
||||
* Applies the same transform that LiteGraph uses for rendering.
|
||||
* Essential for positioning Vue components to align with canvas elements.
|
||||
*
|
||||
* Formula: screen = canvas * scale + offset
|
||||
*
|
||||
* @param point - Point in canvas coordinate system
|
||||
* @returns Point in screen coordinate system
|
||||
*/
|
||||
const canvasToScreen = (point: Point): Point => {
|
||||
return {
|
||||
x: point.x * camera.z + camera.x,
|
||||
@@ -47,7 +114,17 @@ export const useTransformState = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert screen coordinates to canvas coordinates
|
||||
/**
|
||||
* Converts screen coordinates to canvas coordinates
|
||||
*
|
||||
* Inverse of canvasToScreen. Useful for hit testing and converting
|
||||
* mouse events back to canvas space.
|
||||
*
|
||||
* Formula: canvas = (screen - offset) / scale
|
||||
*
|
||||
* @param point - Point in screen coordinate system
|
||||
* @returns Point in canvas coordinate system
|
||||
*/
|
||||
const screenToCanvas = (point: Point): Point => {
|
||||
return {
|
||||
x: (point.x - camera.x) / camera.z,
|
||||
@@ -110,6 +187,28 @@ export const useTransformState = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Get viewport bounds in canvas coordinates (for spatial index queries)
|
||||
const getViewportBounds = (
|
||||
viewport: { width: number; height: number },
|
||||
margin: number = 0.2
|
||||
) => {
|
||||
const marginX = viewport.width * margin
|
||||
const marginY = viewport.height * margin
|
||||
|
||||
const topLeft = screenToCanvas({ x: -marginX, y: -marginY })
|
||||
const bottomRight = screenToCanvas({
|
||||
x: viewport.width + marginX,
|
||||
y: viewport.height + marginY
|
||||
})
|
||||
|
||||
return {
|
||||
x: topLeft.x,
|
||||
y: topLeft.y,
|
||||
width: bottomRight.x - topLeft.x,
|
||||
height: bottomRight.y - topLeft.y
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
camera: readonly(camera),
|
||||
transformStyle,
|
||||
@@ -117,6 +216,7 @@ export const useTransformState = () => {
|
||||
canvasToScreen,
|
||||
screenToCanvas,
|
||||
getNodeScreenBounds,
|
||||
isNodeInViewport
|
||||
isNodeInViewport,
|
||||
getViewportBounds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
210
src/composables/graph/useSpatialIndex.ts
Normal file
210
src/composables/graph/useSpatialIndex.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Composable for spatial indexing of nodes using QuadTree
|
||||
* Integrates with useGraphNodeManager for efficient viewport culling
|
||||
*/
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
import { type Bounds, QuadTree } from '@/utils/spatial/QuadTree'
|
||||
|
||||
export interface SpatialIndexOptions {
|
||||
worldBounds?: Bounds
|
||||
maxDepth?: number
|
||||
maxItemsPerNode?: number
|
||||
enableDebugVisualization?: boolean
|
||||
updateDebounceMs?: number
|
||||
}
|
||||
|
||||
interface SpatialMetrics {
|
||||
queryTime: number
|
||||
totalNodes: number
|
||||
visibleNodes: number
|
||||
treeDepth: number
|
||||
rebuildCount: number
|
||||
}
|
||||
|
||||
export const useSpatialIndex = (options: SpatialIndexOptions = {}) => {
|
||||
// Default world bounds (can be expanded dynamically)
|
||||
const defaultBounds: Bounds = {
|
||||
x: -10000,
|
||||
y: -10000,
|
||||
width: 20000,
|
||||
height: 20000
|
||||
}
|
||||
|
||||
// QuadTree instance
|
||||
const quadTree = ref<QuadTree<string> | null>(null)
|
||||
|
||||
// Performance metrics
|
||||
const metrics = reactive<SpatialMetrics>({
|
||||
queryTime: 0,
|
||||
totalNodes: 0,
|
||||
visibleNodes: 0,
|
||||
treeDepth: 0,
|
||||
rebuildCount: 0
|
||||
})
|
||||
|
||||
// Debug visualization data (unused for now but may be used in future)
|
||||
// const debugBounds = ref<Bounds[]>([])
|
||||
|
||||
// Initialize QuadTree
|
||||
const initialize = (bounds: Bounds = defaultBounds) => {
|
||||
quadTree.value = new QuadTree<string>(bounds, {
|
||||
maxDepth: options.maxDepth ?? 6,
|
||||
maxItemsPerNode: options.maxItemsPerNode ?? 4
|
||||
})
|
||||
metrics.rebuildCount++
|
||||
}
|
||||
|
||||
// Add or update node in spatial index
|
||||
const updateNode = (
|
||||
nodeId: string,
|
||||
position: { x: number; y: number },
|
||||
size: { width: number; height: number }
|
||||
) => {
|
||||
if (!quadTree.value) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
const bounds: Bounds = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: size.width,
|
||||
height: size.height
|
||||
}
|
||||
|
||||
quadTree.value!.update(nodeId, bounds)
|
||||
metrics.totalNodes = quadTree.value!.size
|
||||
}
|
||||
|
||||
// Batch update for multiple nodes
|
||||
const batchUpdate = (
|
||||
updates: Array<{
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
size: { width: number; height: number }
|
||||
}>
|
||||
) => {
|
||||
if (!quadTree.value) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
for (const update of updates) {
|
||||
const bounds: Bounds = {
|
||||
x: update.position.x,
|
||||
y: update.position.y,
|
||||
width: update.size.width,
|
||||
height: update.size.height
|
||||
}
|
||||
quadTree.value!.update(update.id, bounds)
|
||||
}
|
||||
|
||||
metrics.totalNodes = quadTree.value!.size
|
||||
}
|
||||
|
||||
// Remove node from spatial index
|
||||
const removeNode = (nodeId: string) => {
|
||||
if (!quadTree.value) return
|
||||
|
||||
quadTree.value.remove(nodeId)
|
||||
metrics.totalNodes = quadTree.value.size
|
||||
}
|
||||
|
||||
// Query nodes within viewport bounds
|
||||
const queryViewport = (viewportBounds: Bounds): string[] => {
|
||||
if (!quadTree.value) return []
|
||||
|
||||
const startTime = performance.now()
|
||||
const nodeIds = quadTree.value.query(viewportBounds)
|
||||
const queryTime = performance.now() - startTime
|
||||
|
||||
metrics.queryTime = queryTime
|
||||
metrics.visibleNodes = nodeIds.length
|
||||
|
||||
return nodeIds
|
||||
}
|
||||
|
||||
// Get nodes within a radius (for proximity queries)
|
||||
const queryRadius = (
|
||||
center: { x: number; y: number },
|
||||
radius: number
|
||||
): string[] => {
|
||||
if (!quadTree.value) return []
|
||||
|
||||
const bounds: Bounds = {
|
||||
x: center.x - radius,
|
||||
y: center.y - radius,
|
||||
width: radius * 2,
|
||||
height: radius * 2
|
||||
}
|
||||
|
||||
return quadTree.value.query(bounds)
|
||||
}
|
||||
|
||||
// Clear all nodes
|
||||
const clear = () => {
|
||||
if (!quadTree.value) return
|
||||
|
||||
quadTree.value.clear()
|
||||
metrics.totalNodes = 0
|
||||
metrics.visibleNodes = 0
|
||||
}
|
||||
|
||||
// Rebuild tree (useful after major layout changes)
|
||||
const rebuild = (
|
||||
nodes: Map<
|
||||
string,
|
||||
{
|
||||
position: { x: number; y: number }
|
||||
size: { width: number; height: number }
|
||||
}
|
||||
>
|
||||
) => {
|
||||
initialize()
|
||||
|
||||
const updates = Array.from(nodes.entries()).map(([id, data]) => ({
|
||||
id,
|
||||
position: data.position,
|
||||
size: data.size
|
||||
}))
|
||||
|
||||
batchUpdate(updates)
|
||||
}
|
||||
|
||||
// Get debug visualization data
|
||||
const getDebugVisualization = () => {
|
||||
if (!quadTree.value || !options.enableDebugVisualization) return null
|
||||
|
||||
return quadTree.value.getDebugInfo()
|
||||
}
|
||||
|
||||
// Debounced update for performance
|
||||
const debouncedUpdateNode = useDebounceFn(
|
||||
updateNode,
|
||||
options.updateDebounceMs ?? 16
|
||||
)
|
||||
|
||||
return {
|
||||
// Core functions
|
||||
initialize,
|
||||
updateNode,
|
||||
batchUpdate,
|
||||
removeNode,
|
||||
queryViewport,
|
||||
queryRadius,
|
||||
clear,
|
||||
rebuild,
|
||||
|
||||
// Debounced version for high-frequency updates
|
||||
debouncedUpdateNode,
|
||||
|
||||
// Metrics
|
||||
metrics: computed(() => metrics),
|
||||
|
||||
// Debug
|
||||
getDebugVisualization,
|
||||
|
||||
// Direct access to QuadTree (for advanced usage)
|
||||
quadTree: computed(() => quadTree.value)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,15 @@ export const useWidgetRenderer = () => {
|
||||
image: WidgetType.IMAGE,
|
||||
IMAGE: WidgetType.IMAGE,
|
||||
file: WidgetType.FILEUPLOAD,
|
||||
FILEUPLOAD: WidgetType.FILEUPLOAD
|
||||
FILEUPLOAD: WidgetType.FILEUPLOAD,
|
||||
|
||||
// Button widget
|
||||
button: WidgetType.BUTTON,
|
||||
BUTTON: WidgetType.BUTTON,
|
||||
|
||||
// Text-based widgets that don't have dedicated components yet
|
||||
MARKDOWN: WidgetType.TEXTAREA, // Markdown should use textarea for now
|
||||
customtext: WidgetType.TEXTAREA // Custom text widgets use textarea for multiline
|
||||
}
|
||||
|
||||
// Get mapped enum key
|
||||
@@ -55,13 +63,6 @@ export const useWidgetRenderer = () => {
|
||||
return enumKey
|
||||
}
|
||||
|
||||
// Log unmapped widget types for debugging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(
|
||||
`[useWidgetRenderer] Unknown widget type: ${widgetType}, falling back to WidgetInputText`
|
||||
)
|
||||
}
|
||||
|
||||
return WidgetType.STRING // Return enum key for WidgetInputText
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user