mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 21:22:07 +00:00
fix: subgraph promoted widget input label rename (#10195)
## Summary Promoted primitive subgraph inputs (String, Int) render their link anchor at the header position instead of the widget row. Renaming subgraph input labels breaks the match entirely, causing connections to detach from their widgets visually. ## Changes - **What**: Fix widget-input slot positioning for promoted subgraph inputs in both LiteGraph and Vue (Nodes 2.0) rendering modes - `_arrangeWidgetInputSlots`: Removed Vue mode branch that skipped setting `input.pos`. Promoted widget inputs aren't rendered as `<InputSlot>` Vue components (NodeSlots filters them out), so `input.pos` is the only position fallback - `drawConnections`: Added pre-pass to arrange nodes with unpositioned widget-input slots before link rendering. The background canvas renders before the foreground canvas calls `arrange()`, so positions weren't set on the first frame - `SubgraphNode`: Sync `input.widget.name` with the display name on label rename and initial setup. The `IWidgetLocator` name diverged from `PromotedWidgetView.name` after rename, breaking all name-based slot↔widget matching (`_arrangeWidgetInputSlots`, `getWidgetFromSlot`, `getSlotFromWidget`) ## Review Focus - The `_arrangeWidgetInputSlots` rewrite iterates `_concreteInputs` directly instead of building a spread-copy map — simpler and avoids the stale index issue - `input.widget.name` is now kept in sync with the display name (`input.label ?? subgraphInput.name`). This is a semantic shift from using the raw internal name, but it's required for all name-based matching to work after renames. The value is overwritten on deserialize by `_setWidget` anyway - The `_widget` fallback in `_arrangeWidgetInputSlots` is a safety net for edge cases where the name still doesn't match (e.g., stale cache) Fixes #9998 ## Screenshots <img width="847" height="476" alt="Screenshot 2026-03-17 at 3 05 32 PM" src="https://github.com/user-attachments/assets/38f10563-f0bc-44dd-a1a5-f4a7832575d0" /> <img width="804" height="471" alt="Screenshot 2026-03-17 at 3 05 23 PM" src="https://github.com/user-attachments/assets/3237a7ee-f3e5-4084-b330-371def3415bd" /> <img width="974" height="571" alt="Screenshot 2026-03-17 at 3 05 16 PM" src="https://github.com/user-attachments/assets/cafdca46-8d9b-40e1-8561-02cbb25ee8f2" /> <img width="967" height="558" alt="Screenshot 2026-03-17 at 3 05 06 PM" src="https://github.com/user-attachments/assets/fc03ce43-906c-474d-b3bc-ddf08eb37c75" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10195-fix-subgraph-promoted-widget-input-slot-positions-after-label-rename-3266d73d365081dfa623dd94dd87c718) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: jaeone94 <jaeone.prt@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f37b0daa3a
commit
657ae6a6c3
@@ -295,6 +295,12 @@ export class LGraphNode
|
||||
*/
|
||||
freeWidgetSpace?: number
|
||||
|
||||
/**
|
||||
* Set to true when widget-backed input slot positions need recalculation.
|
||||
* Cleared after arrange() runs. Avoids per-frame O(N) scans in drawConnections.
|
||||
*/
|
||||
_widgetSlotsDirty = false
|
||||
|
||||
locked?: boolean
|
||||
|
||||
/** Execution order, automatically computed during run @see {@link LGraph.computeExecutionOrder} */
|
||||
@@ -1992,6 +1998,7 @@ export class LGraphNode
|
||||
this.widgets ||= []
|
||||
const widget = toConcreteWidget(custom_widget, this, false) ?? custom_widget
|
||||
this.widgets.push(widget)
|
||||
this._widgetSlotsDirty = true
|
||||
|
||||
// Only register with store if node has a valid ID (is already in a graph).
|
||||
// If the node isn't in a graph yet (id === -1), registration happens
|
||||
@@ -2031,9 +2038,11 @@ export class LGraphNode
|
||||
if (input._widget === widget) {
|
||||
input._widget = undefined
|
||||
input.widget = undefined
|
||||
input.pos = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
this._widgetSlotsDirty = true
|
||||
|
||||
widget.onRemove?.()
|
||||
this.widgets.splice(widgetIndex, 1)
|
||||
@@ -4206,40 +4215,29 @@ export class LGraphNode
|
||||
* Arranges the layout of the node's widget input slots.
|
||||
*/
|
||||
private _arrangeWidgetInputSlots(): void {
|
||||
if (!this.widgets) return
|
||||
if (!this.widgets?.length) return
|
||||
|
||||
const slotByWidgetName = new Map<
|
||||
string,
|
||||
INodeInputSlot & { index: number }
|
||||
>()
|
||||
// Build a name→widget map for fast lookup.
|
||||
const widgetByName = new Map<string, IBaseWidget>()
|
||||
for (const w of this.widgets) widgetByName.set(w.name, w)
|
||||
|
||||
for (const [i, slot] of this.inputs.entries()) {
|
||||
// Set widget-backed slot positions from widget Y coordinates.
|
||||
// In Vue mode, promoted widget inputs are not rendered as <InputSlot>
|
||||
// components (NodeSlots filters them out), so they have no DOM-registered
|
||||
// position. input.pos serves as the fallback for getSlotPosition().
|
||||
for (const [i, slot] of this._concreteInputs.entries()) {
|
||||
if (!isWidgetInputSlot(slot)) continue
|
||||
|
||||
slotByWidgetName.set(slot.widget.name, { ...slot, index: i })
|
||||
}
|
||||
if (!slotByWidgetName.size) return
|
||||
// Prefer the slot's direct _widget binding (1:1 for promoted inputs).
|
||||
// Fall back to name-map lookup for regular nodes without _widget set.
|
||||
// Note: the name-map is ambiguous if two promoted inputs share a label;
|
||||
// _widget avoids this since it is a direct reference.
|
||||
const widget = slot._widget ?? widgetByName.get(slot.widget.name)
|
||||
if (!widget) continue
|
||||
|
||||
// Only set custom pos if not using Vue positioning
|
||||
// Vue positioning calculates widget slot positions dynamically
|
||||
if (!LiteGraph.vueNodesMode) {
|
||||
for (const widget of this.widgets) {
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
const actualSlot = this._concreteInputs[slot.index]
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
actualSlot.pos = [offset, widget.y + offset]
|
||||
this._measureSlot(actualSlot, slot.index, true)
|
||||
}
|
||||
} else {
|
||||
// For Vue positioning, just measure the slots without setting pos
|
||||
for (const widget of this.widgets) {
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
this._measureSlot(this._concreteInputs[slot.index], slot.index, true)
|
||||
}
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
slot.pos = [offset, widget.y + offset]
|
||||
this._measureSlot(slot, i, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4269,6 +4267,7 @@ export class LGraphNode
|
||||
: 0
|
||||
this._arrangeWidgets(widgetStartY)
|
||||
this._arrangeWidgetInputSlots()
|
||||
this._widgetSlotsDirty = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user