diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue index fc9b71d10..71ee6194f 100644 --- a/src/components/rightSidePanel/parameters/SectionWidgets.vue +++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue @@ -2,6 +2,7 @@ import { computed, provide } from 'vue' import { useI18n } from 'vue-i18n' +import { useReactiveWidgetValue } from '@/composables/graph/useGraphNodeManager' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' @@ -71,7 +72,7 @@ const displayLabel = computed( void } { + if (widget.vueTrack) return + + customRef((track, trigger) => { + widget.callback = useChainCallback(widget.callback, trigger) + widget.vueTrack = track + return { get() {}, set() {} } + }) +} +export function useReactiveWidgetValue(widget: IBaseWidget) { + widgetWithVueTrack(widget) + widget.vueTrack() + return widget.value +} + function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined { const cagWidget = widget.linkedWidgets?.find( (w) => w.name == 'control_after_generate' @@ -106,6 +123,37 @@ function getNodeType(node: LGraphNode, widget: IBaseWidget) { return subNode?.type } +/** + * Validates that a value is a valid WidgetValue type + */ +const normalizeWidgetValue = (value: unknown): WidgetValue => { + if (value === null || value === undefined || value === void 0) { + return undefined + } + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value + } + if (typeof value === 'object') { + // Check if it's a File array + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item): item is File => item instanceof File) + ) { + return value + } + // Otherwise it's a generic object + return value + } + // If none of the above, return undefined + console.warn(`Invalid widget value type: ${typeof value}`, value) + return undefined +} + export function safeWidgetMapper( node: LGraphNode, slotMetadata: Map @@ -113,19 +161,6 @@ export function safeWidgetMapper( const nodeDefStore = useNodeDefStore() return function (widget) { try { - // TODO: Use widget.getReactiveData() once TypeScript types are updated - let value = widget.value - - // For combo widgets, if value is undefined, use the first option as default - if ( - value === undefined && - widget.type === 'combo' && - widget.options?.values && - Array.isArray(widget.options.values) && - widget.options.values.length > 0 - ) { - value = widget.options.values[0] - } const spec = nodeDefStore.getInputSpecForWidget(node, widget.name) const slotInfo = slotMetadata.get(widget.name) const borderStyle = widget.promoted @@ -133,13 +168,18 @@ export function safeWidgetMapper( : widget.advanced ? 'ring ring-component-node-widget-advanced' : undefined + const callback = (v: unknown) => { + const value = normalizeWidgetValue(v) + widget.value = value ?? undefined + widget.callback?.(value) + } return { name: widget.name, type: widget.type, - value: value, + value: useReactiveWidgetValue(widget), borderStyle, - callback: widget.callback, + callback, controlWidget: getControlWidget(widget), isDOMWidget: isDOMWidget(widget), label: widget.label, @@ -286,128 +326,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { return nodeRefs.get(id) } - /** - * Validates that a value is a valid WidgetValue type - */ - const validateWidgetValue = (value: unknown): WidgetValue => { - if (value === null || value === undefined || value === void 0) { - return undefined - } - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return value - } - if (typeof value === 'object') { - // Check if it's a File array - if ( - Array.isArray(value) && - value.length > 0 && - value.every((item): item is File => item instanceof File) - ) { - return value - } - // Otherwise it's a generic object - return value - } - // If none of the above, return undefined - console.warn(`Invalid widget value type: ${typeof value}`, value) - return undefined - } - - /** - * Updates Vue state when widget values change - */ - const updateVueWidgetState = ( - nodeId: string, - widgetName: string, - value: unknown - ): void => { - try { - const currentData = vueNodeData.get(nodeId) - if (!currentData?.widgets) return - - const updatedWidgets = currentData.widgets.map((w) => - w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w - ) - // Create a completely new object to ensure Vue reactivity triggers - const updatedData = { - ...currentData, - widgets: updatedWidgets - } - - vueNodeData.set(nodeId, updatedData) - } catch (error) { - // Ignore widget update errors to prevent cascade failures - } - } - - /** - * Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync - */ - const createWrappedWidgetCallback = ( - widget: IBaseWidget, // LiteGraph widget with minimal typing - originalCallback: ((value: unknown) => void) | undefined, - nodeId: string - ) => { - let updateInProgress = false - - return (value: unknown) => { - if (updateInProgress) return - updateInProgress = true - - try { - // 1. Update the widget value in LiteGraph (critical for LiteGraph state) - // Validate that the value is of an acceptable type - if ( - value !== null && - value !== undefined && - typeof value !== 'string' && - typeof value !== 'number' && - typeof value !== 'boolean' && - typeof value !== 'object' - ) { - console.warn(`Invalid widget value type: ${typeof value}`) - updateInProgress = false - return - } - - // Always update widget.value to ensure sync - widget.value = value ?? undefined - - // 2. Call the original callback if it exists - if (originalCallback && widget.type !== 'asset') { - originalCallback.call(widget, value) - } - - // 3. Update Vue state to maintain synchronization - updateVueWidgetState(nodeId, widget.name, value) - } finally { - updateInProgress = false - } - } - } - - /** - * Sets up widget callbacks for a node - */ - const setupNodeWidgetCallbacks = (node: LGraphNode) => { - if (!node.widgets) return - - const nodeId = String(node.id) - - node.widgets.forEach((widget) => { - const originalCallback = widget.callback - widget.callback = createWrappedWidgetCallback( - widget, - originalCallback, - nodeId - ) - }) - } - const syncWithGraph = () => { if (!graph?._nodes) return @@ -428,9 +346,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { // Store non-reactive reference nodeRefs.set(id, node) - // Set up widget callbacks BEFORE extracting data (critical order) - setupNodeWidgetCallbacks(node) - // Extract and store safe data for Vue vueNodeData.set(id, extractVueNodeData(node)) }) @@ -449,9 +364,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { // Store non-reactive reference to original node nodeRefs.set(id, node) - // Set up widget callbacks BEFORE extracting data (critical order) - setupNodeWidgetCallbacks(node) - // Extract initial data for Vue (may be incomplete during graph configure) vueNodeData.set(id, extractVueNodeData(node)) diff --git a/src/core/graph/subgraph/proxyWidget.ts b/src/core/graph/subgraph/proxyWidget.ts index ef04bd76b..60f6d456a 100644 --- a/src/core/graph/subgraph/proxyWidget.ts +++ b/src/core/graph/subgraph/proxyWidget.ts @@ -211,11 +211,9 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) { } return Reflect.get(redirectedTarget, property, redirectedReceiver) }, - set(_t: IBaseWidget, property: string, value: unknown, receiver: object) { + set(_t: IBaseWidget, property: string, value: unknown) { let redirectedTarget: object = backingWidget - let redirectedReceiver = receiver - if (property == 'value') redirectedReceiver = backingWidget - else if (property == 'computedHeight') { + if (property == 'computedHeight') { if (overlay.widgetName.startsWith('$$') && linkedNode) { updatePreviews(linkedNode) } @@ -228,9 +226,8 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) { } if (Object.prototype.hasOwnProperty.call(overlay, property)) { redirectedTarget = overlay - redirectedReceiver = overlay } - return Reflect.set(redirectedTarget, property, value, redirectedReceiver) + return Reflect.set(redirectedTarget, property, value, redirectedTarget) }, getPrototypeOf() { return Reflect.getPrototypeOf(backingWidget) diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index f383f9404..1cfb323d8 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -284,6 +284,7 @@ export interface IBaseWidget< /** Widget type (see {@link TWidgetType}) */ type: TType value?: TValue + vueTrack?: () => void /** * Whether the widget value should be serialized on node serialization.