mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
[fix] Fix widget value synchronization between Vue and LiteGraph
- Widget callbacks now explicitly set widget.value (fixes empty LiteGraph callbacks) - Created useWidgetValue composable for consistent widget patterns - Updated all widget components to use proper props/emit pattern - Callbacks are set up before data extraction to ensure proper state sync
This commit is contained in:
@@ -123,12 +123,11 @@ const simplifiedWidget = (widget: SafeWidgetData): SimplifiedWidget => {
|
||||
|
||||
// Handle widget value updates
|
||||
const handleWidgetUpdate = (widget: SafeWidgetData, value: unknown) => {
|
||||
widget.value = value
|
||||
|
||||
// Call LiteGraph callback to update the authoritative state
|
||||
// The callback will trigger the chained callback in useGraphNodeManager
|
||||
// which will update the Vue state automatically
|
||||
if (widget.callback) {
|
||||
widget.callback(value)
|
||||
}
|
||||
|
||||
// TODO: Implement proper widget change handling to sync back to LiteGraph
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<InputText v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
<InputText
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,14 +21,25 @@ import {
|
||||
INPUT_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<string>({ required: true })
|
||||
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
modelValue: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// Use the composable for consistent widget value handling
|
||||
const { localValue, onChange } = useStringWidgetValue(
|
||||
props.widget,
|
||||
props.modelValue,
|
||||
emit
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
widget.name
|
||||
}}</label>
|
||||
<Select
|
||||
v-model="value"
|
||||
v-model="localValue"
|
||||
:options="selectOptions"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,14 +22,26 @@ import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<any>({ required: true })
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<any>
|
||||
modelValue: any
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
}>()
|
||||
|
||||
// Use the composable for consistent widget value handling
|
||||
const { localValue, onChange } = useWidgetValue({
|
||||
widget: props.widget,
|
||||
modelValue: props.modelValue,
|
||||
defaultValue: props.widget.options?.values?.[0] || '',
|
||||
emit
|
||||
})
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, PANEL_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<Slider v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
<Slider
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,14 +21,25 @@ import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<number>({ required: true })
|
||||
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
modelValue: number
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
// Use the composable for consistent widget value handling
|
||||
const { localValue, onChange } = useNumberWidgetValue(
|
||||
props.widget,
|
||||
props.modelValue,
|
||||
emit
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<label v-if="widget.name" class="text-sm opacity-80">{{
|
||||
widget.name
|
||||
}}</label>
|
||||
<ToggleSwitch v-model="value" v-bind="filteredProps" :disabled="readonly" />
|
||||
<ToggleSwitch
|
||||
v-model="localValue"
|
||||
v-bind="filteredProps"
|
||||
:disabled="readonly"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,14 +21,25 @@ import {
|
||||
STANDARD_EXCLUDED_PROPS,
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
import { useBooleanWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<boolean>
|
||||
modelValue: boolean
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
// Use the composable for consistent widget value handling
|
||||
const { localValue, onChange } = useBooleanWidgetValue(
|
||||
props.widget,
|
||||
props.modelValue,
|
||||
emit
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import type { LGraph, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { nextTick, reactive, readonly } from 'vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { QuadTree, type Bounds } from '@/utils/spatial/QuadTree'
|
||||
|
||||
export interface NodeState {
|
||||
visible: boolean
|
||||
@@ -51,6 +51,13 @@ export interface VueNodeData {
|
||||
outputs?: unknown[]
|
||||
}
|
||||
|
||||
export interface SpatialMetrics {
|
||||
strategy: 'linear' | 'quadtree'
|
||||
queryTime: number
|
||||
nodesInIndex: number
|
||||
threshold: number
|
||||
}
|
||||
|
||||
export interface GraphNodeManager {
|
||||
// Reactive state - safe data extracted from LiteGraph nodes
|
||||
vueNodeData: ReadonlyMap<string, VueNodeData>
|
||||
@@ -73,8 +80,15 @@ export interface GraphNodeManager {
|
||||
forceSync(): void
|
||||
detectChangesInRAF(): void
|
||||
|
||||
// Spatial queries
|
||||
getVisibleNodeIds(viewportBounds: Bounds): Set<string>
|
||||
|
||||
// Performance
|
||||
performanceMetrics: PerformanceMetrics
|
||||
spatialMetrics: SpatialMetrics
|
||||
|
||||
// Debug
|
||||
getSpatialIndexDebugInfo(): any | null
|
||||
}
|
||||
|
||||
export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
@@ -106,6 +120,23 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
adaptiveQuality: false
|
||||
})
|
||||
|
||||
// Spatial indexing for large graphs (auto-enables at 100+ nodes)
|
||||
const SPATIAL_INDEX_THRESHOLD = 100
|
||||
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
|
||||
})
|
||||
|
||||
// FPS tracking state
|
||||
let lastFrameTime = performance.now()
|
||||
let frameCount = 0
|
||||
@@ -228,13 +259,24 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
const nodeId = String(node.id)
|
||||
|
||||
node.widgets?.forEach(widget => {
|
||||
widget.callback = useChainCallback(widget.callback, () => {
|
||||
// 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[]) => {
|
||||
// 1. Update the widget value in LiteGraph
|
||||
widget.value = value
|
||||
|
||||
// 2. Call the original callback if it exists
|
||||
if (originalCallback) {
|
||||
originalCallback.call(widget, value as Parameters<typeof originalCallback>[0], ...args)
|
||||
}
|
||||
|
||||
// 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: widget.value }
|
||||
? { ...w, value: value }
|
||||
: w
|
||||
)
|
||||
vueNodeData.set(nodeId, {
|
||||
@@ -246,7 +288,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
} catch (error) {
|
||||
console.warn(`[useGraphNodeManager] Failed to update Vue state for widget ${widget.name}:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -363,6 +405,41 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
}
|
||||
|
||||
// Most performant: Direct position sync without re-setting entire node
|
||||
// Query visible nodes using spatial index when available
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastSpatialQueryTime = performance.now() - startTime
|
||||
spatialMetrics.queryTime = lastSpatialQueryTime
|
||||
|
||||
return visibleIds
|
||||
}
|
||||
|
||||
const detectChangesInRAF = () => {
|
||||
const startTime = performance.now()
|
||||
|
||||
@@ -371,12 +448,27 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
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)
|
||||
const currentPos = nodePositions.get(id)
|
||||
const currentSize = nodeSizes.get(id)
|
||||
|
||||
let posChanged = false
|
||||
let sizeChanged = false
|
||||
|
||||
if (
|
||||
!currentPos ||
|
||||
currentPos.x !== node.pos[0] ||
|
||||
@@ -384,6 +476,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
) {
|
||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||
positionUpdates++
|
||||
posChanged = true
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -393,6 +486,18 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
) {
|
||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||
sizeUpdates++
|
||||
sizeChanged = true
|
||||
}
|
||||
|
||||
// Update spatial index if enabled and position/size changed
|
||||
if (spatialIndexEnabled && (posChanged || sizeChanged)) {
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size[0],
|
||||
height: node.size[1]
|
||||
}
|
||||
spatialIndex.update(id, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +508,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
performanceMetrics.culledCount = Array.from(nodeState.values()).filter(
|
||||
state => state.culled
|
||||
).length
|
||||
spatialMetrics.nodesInIndex = spatialIndexEnabled ? spatialIndex.size : 0
|
||||
|
||||
if (positionUpdates > 0 || sizeUpdates > 0) {
|
||||
performanceMetrics.rafUpdateCount++
|
||||
@@ -421,8 +527,11 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
// Store non-reactive reference to original node
|
||||
nodeRefs.set(id, node)
|
||||
|
||||
// Set up widget callbacks BEFORE extracting data
|
||||
setupNodeWidgetCallbacks(node)
|
||||
|
||||
// Extract safe data for Vue
|
||||
// Extract safe data for Vue (now with proper callbacks)
|
||||
vueNodeData.set(id, extractVueNodeData(node))
|
||||
|
||||
// Set up reactive tracking
|
||||
@@ -436,7 +545,16 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||
attachMetadata(node)
|
||||
|
||||
setupNodeWidgetCallbacks(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)
|
||||
}
|
||||
|
||||
if (originalOnNodeAdded) {
|
||||
void originalOnNodeAdded(node)
|
||||
@@ -445,6 +563,12 @@ 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)
|
||||
}
|
||||
|
||||
nodeRefs.delete(id)
|
||||
vueNodeData.delete(id)
|
||||
nodeState.delete(id)
|
||||
@@ -485,6 +609,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
pendingUpdates.clear()
|
||||
criticalUpdates.clear()
|
||||
lowPriorityUpdates.clear()
|
||||
spatialIndex.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +633,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
scheduleUpdate,
|
||||
forceSync: syncWithGraph,
|
||||
detectChangesInRAF,
|
||||
performanceMetrics
|
||||
getVisibleNodeIds,
|
||||
performanceMetrics,
|
||||
spatialMetrics: readonly(spatialMetrics),
|
||||
getSpatialIndexDebugInfo: () => spatialIndexEnabled ? spatialIndex.getDebugInfo() : null
|
||||
}
|
||||
}
|
||||
|
||||
138
src/composables/graph/useWidgetValue.ts
Normal file
138
src/composables/graph/useWidgetValue.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Composable for managing widget value synchronization between Vue and LiteGraph
|
||||
* Provides consistent pattern for immediate UI updates and LiteGraph callbacks
|
||||
*/
|
||||
import { ref, watch, type Ref } from 'vue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
export interface UseWidgetValueOptions<T, U = T> {
|
||||
/** The widget configuration from LiteGraph */
|
||||
widget: SimplifiedWidget<T>
|
||||
/** The current value from parent component */
|
||||
modelValue: T
|
||||
/** Default value if modelValue is null/undefined */
|
||||
defaultValue: T
|
||||
/** Emit function from component setup */
|
||||
emit: (event: 'update:modelValue', value: T) => void
|
||||
/** Optional value transformer before sending to LiteGraph */
|
||||
transform?: (value: U) => T
|
||||
}
|
||||
|
||||
export interface UseWidgetValueReturn<T, U = T> {
|
||||
/** Local value for immediate UI updates */
|
||||
localValue: Ref<T>
|
||||
/** Handler for user interactions */
|
||||
onChange: (newValue: U) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages widget value synchronization with LiteGraph
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* const { localValue, onChange } = useWidgetValue({
|
||||
* widget: props.widget,
|
||||
* modelValue: props.modelValue,
|
||||
* defaultValue: ''
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function useWidgetValue<T, U = T>({
|
||||
widget,
|
||||
modelValue,
|
||||
defaultValue,
|
||||
emit,
|
||||
transform
|
||||
}: UseWidgetValueOptions<T, U>): UseWidgetValueReturn<T, U> {
|
||||
// Local value for immediate UI updates
|
||||
const localValue = ref<T>(modelValue ?? defaultValue)
|
||||
|
||||
// Handle user changes
|
||||
const onChange = (newValue: U) => {
|
||||
// Handle different PrimeVue component signatures
|
||||
let processedValue: T
|
||||
if (transform) {
|
||||
processedValue = transform(newValue)
|
||||
} else {
|
||||
processedValue = newValue as unknown as T
|
||||
}
|
||||
|
||||
// 1. Update local state for immediate UI feedback
|
||||
localValue.value = processedValue
|
||||
|
||||
// 2. Emit to parent component
|
||||
emit('update:modelValue', processedValue)
|
||||
|
||||
// 3. Call LiteGraph callback to update authoritative state
|
||||
if (widget.callback) {
|
||||
widget.callback(processedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for external updates from LiteGraph
|
||||
watch(() => modelValue, (newValue) => {
|
||||
localValue.value = newValue ?? defaultValue
|
||||
})
|
||||
|
||||
return {
|
||||
localValue: localValue as Ref<T>,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-specific helper for string widgets
|
||||
*/
|
||||
export function useStringWidgetValue(
|
||||
widget: SimplifiedWidget<string>,
|
||||
modelValue: string,
|
||||
emit: (event: 'update:modelValue', value: string) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
widget,
|
||||
modelValue,
|
||||
defaultValue: '',
|
||||
emit,
|
||||
transform: (value: string | undefined) => String(value || '') // Handle undefined from PrimeVue
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-specific helper for number widgets
|
||||
*/
|
||||
export function useNumberWidgetValue(
|
||||
widget: SimplifiedWidget<number>,
|
||||
modelValue: number,
|
||||
emit: (event: 'update:modelValue', value: number) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
widget,
|
||||
modelValue,
|
||||
defaultValue: 0,
|
||||
emit,
|
||||
transform: (value: number | number[]) => {
|
||||
// Handle PrimeVue Slider which can emit number | number[]
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] || 0
|
||||
}
|
||||
return Number(value) || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-specific helper for boolean widgets
|
||||
*/
|
||||
export function useBooleanWidgetValue(
|
||||
widget: SimplifiedWidget<boolean>,
|
||||
modelValue: boolean,
|
||||
emit: (event: 'update:modelValue', value: boolean) => void
|
||||
) {
|
||||
return useWidgetValue({
|
||||
widget,
|
||||
modelValue,
|
||||
defaultValue: false,
|
||||
emit,
|
||||
transform: (value: boolean) => Boolean(value)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user