mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 05:02:17 +00:00
feat: reactive upstream value display for disabled curve and imagecrop widgets (#9851)
Replacement for https://github.com/Comfy-Org/ComfyUI_frontend/pull/9364 ## Summary Add a generic mechanism for widgets to reactively display upstream linked values when disabled, with concrete implementations for curve and imagecrop widgets. ## Changes - What: When a widget input is linked to an upstream node, the widget enters a disabled state and displays the upstream node's current value reactively. This is built as a two-layer system: - Infrastructure layer: Resolve link origin info (originNodeId, originOutputName) in buildSlotMetadata, pass it through SimplifiedWidget.linkedUpstream, and provide a generic useUpstreamValue composable that reads upstream values from widgetValueStore. - Widget layer: Each widget type provides its own ValueExtractor to interpret upstream data. singleValueExtractor handles simple type-matched values (e.g. CurvePoint[]); boundsExtractor composes a Bounds object from either a single upstream widget or four individual x/y/width/height number widgets. - Curve widget: shows upstream curve points in read-only mode - ImageCrop widget: shows upstream bounding box with disabled crop handles and number inputs - CurveEditor and WidgetBoundingBox: gain disabled prop support ## Adapting future widgets The system is designed so that any widget needing upstream value display only needs to: 1. Accept widget: SimplifiedWidget as a prop (provides linkedUpstream automatically) 2. Call useUpstreamValue(() => widget.linkedUpstream, extractor) with a suitable extractor 3. Use singleValueExtractor(typeGuard) for single-value types, or write a custom ValueExtractor for composite cases like boundsExtractor 4. Compute an effectiveValue that switches between upstream and local based on disabled state No infrastructure changes are needed — linkedUpstream is already populated for all widget types that have a corresponding input slot. ## Review Focus - The buildSlotMetadata helper is shared between extractVueNodeData and refreshNodeSlots — verify the graph ref is reliably available in both paths - boundsExtractor composing from 4 individual number widgets (x/y/width/height) — this handles the BBox→ImageCrop case where upstream exposes separate widgets rather than a single Bounds object ## Screenshots https://github.com/user-attachments/assets/dbc57a44-c5df-44f0-acce-d347797ee8fb ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9851-feat-reactive-upstream-value-display-for-disabled-curve-and-imagecrop-widgets-3226d73d36508134b386ddc9b9f1266b) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -41,6 +41,8 @@ import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'
|
||||
export interface WidgetSlotMetadata {
|
||||
index: number
|
||||
linked: boolean
|
||||
originNodeId?: string
|
||||
originOutputName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,6 +357,36 @@ function safeWidgetMapper(
|
||||
}
|
||||
}
|
||||
|
||||
function buildSlotMetadata(
|
||||
inputs: INodeInputSlot[] | undefined,
|
||||
graphRef: LGraph | null | undefined
|
||||
): Map<string, WidgetSlotMetadata> {
|
||||
const metadata = new Map<string, WidgetSlotMetadata>()
|
||||
inputs?.forEach((input, index) => {
|
||||
let originNodeId: string | undefined
|
||||
let originOutputName: string | undefined
|
||||
|
||||
if (input.link != null && graphRef) {
|
||||
const link = graphRef.getLink(input.link)
|
||||
if (link) {
|
||||
originNodeId = String(link.origin_id)
|
||||
const originNode = graphRef.getNodeById(link.origin_id)
|
||||
originOutputName = originNode?.outputs?.[link.origin_slot]?.name
|
||||
}
|
||||
}
|
||||
|
||||
const slotInfo: WidgetSlotMetadata = {
|
||||
index,
|
||||
linked: input.link != null,
|
||||
originNodeId,
|
||||
originOutputName
|
||||
}
|
||||
if (input.name) metadata.set(input.name, slotInfo)
|
||||
if (input.widget?.name) metadata.set(input.widget.name, slotInfo)
|
||||
})
|
||||
return metadata
|
||||
}
|
||||
|
||||
// Extract safe data from LiteGraph node for Vue consumption
|
||||
export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
// Determine subgraph ID - null for root graph, string for subgraphs
|
||||
@@ -427,15 +459,11 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {
|
||||
const widgetsSnapshot = node.widgets ?? []
|
||||
|
||||
const freshMetadata = buildSlotMetadata(node.inputs, node.graph)
|
||||
slotMetadata.clear()
|
||||
node.inputs?.forEach((input, index) => {
|
||||
const slotInfo = {
|
||||
index,
|
||||
linked: input.link != null
|
||||
}
|
||||
if (input.name) slotMetadata.set(input.name, slotInfo)
|
||||
if (input.widget?.name) slotMetadata.set(input.widget.name, slotInfo)
|
||||
})
|
||||
for (const [key, value] of freshMetadata) {
|
||||
slotMetadata.set(key, value)
|
||||
}
|
||||
return widgetsSnapshot.map(safeWidgetMapper(node, slotMetadata))
|
||||
})
|
||||
|
||||
@@ -488,17 +516,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
|
||||
if (!nodeRef || !currentData) return
|
||||
|
||||
// Only extract slot-related data instead of full node re-extraction
|
||||
const slotMetadata = new Map<string, WidgetSlotMetadata>()
|
||||
|
||||
nodeRef.inputs?.forEach((input, index) => {
|
||||
const slotInfo = {
|
||||
index,
|
||||
linked: input.link != null
|
||||
}
|
||||
if (input.name) slotMetadata.set(input.name, slotInfo)
|
||||
if (input.widget?.name) slotMetadata.set(input.widget.name, slotInfo)
|
||||
})
|
||||
const slotMetadata = buildSlotMetadata(nodeRef.inputs, graph)
|
||||
|
||||
// Update only widgets with new slot metadata, keeping other widget data intact
|
||||
for (const widget of currentData.widgets ?? []) {
|
||||
|
||||
Reference in New Issue
Block a user