mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 21:22:07 +00:00
## 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>
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import { createSharedComposable, whenever } from '@vueuse/core'
|
|
import { shallowRef, watch } from 'vue'
|
|
|
|
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
|
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
|
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
|
import { app as comfyApp } from '@/scripts/app'
|
|
|
|
function useVueNodeLifecycleIndividual() {
|
|
const canvasStore = useCanvasStore()
|
|
const layoutMutations = useLayoutMutations()
|
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
|
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
|
const { startSync, stopSync } = useLayoutSync()
|
|
|
|
const initializeNodeManager = () => {
|
|
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
|
const activeGraph = comfyApp.canvas?.graph
|
|
if (!activeGraph || nodeManager.value) return
|
|
|
|
// Initialize the core node manager
|
|
const manager = useGraphNodeManager(activeGraph)
|
|
nodeManager.value = manager
|
|
|
|
// Initialize layout system with existing nodes from active graph
|
|
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
|
id: node.id.toString(),
|
|
pos: [node.pos[0], node.pos[1]] as [number, number],
|
|
size: [node.size[0], node.size[1]] as [number, number]
|
|
}))
|
|
layoutStore.initializeFromLiteGraph(nodes)
|
|
|
|
// Seed reroutes into the Layout Store so hit-testing uses the new path
|
|
for (const reroute of activeGraph.reroutes.values()) {
|
|
const [x, y] = reroute.pos
|
|
const parent = reroute.parentId ?? undefined
|
|
const linkIds = Array.from(reroute.linkIds)
|
|
layoutMutations.createReroute(reroute.id, { x, y }, parent, linkIds)
|
|
}
|
|
|
|
// Seed existing links into the Layout Store (topology only)
|
|
for (const link of activeGraph._links.values()) {
|
|
layoutMutations.createLink(
|
|
link.id,
|
|
link.origin_id,
|
|
link.origin_slot,
|
|
link.target_id,
|
|
link.target_slot
|
|
)
|
|
}
|
|
|
|
// Start sync AFTER seeding so bootstrap operations don't trigger
|
|
// the Layout→LiteGraph writeback loop redundantly.
|
|
startSync(canvasStore.canvas)
|
|
}
|
|
|
|
const disposeNodeManagerAndSyncs = () => {
|
|
stopSync()
|
|
if (!nodeManager.value) return
|
|
|
|
try {
|
|
nodeManager.value.cleanup()
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
nodeManager.value = null
|
|
}
|
|
|
|
// Watch for Vue nodes enabled state changes
|
|
watch(
|
|
() => shouldRenderVueNodes.value && Boolean(comfyApp.canvas?.graph),
|
|
(enabled) => {
|
|
if (enabled) {
|
|
initializeNodeManager()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
whenever(
|
|
() => !shouldRenderVueNodes.value,
|
|
() => {
|
|
disposeNodeManagerAndSyncs()
|
|
|
|
// Force arrange() on all nodes so input.pos is computed before
|
|
// the first legacy drawConnections frame (which may run before
|
|
// drawNode on the foreground canvas).
|
|
const graph = comfyApp.canvas?.graph
|
|
if (!graph) {
|
|
comfyApp.canvas?.setDirty(true, true)
|
|
return
|
|
}
|
|
for (const node of graph._nodes) {
|
|
if (node.flags.collapsed) continue
|
|
try {
|
|
node.arrange()
|
|
} catch {
|
|
/* skip nodes not fully initialized */
|
|
}
|
|
}
|
|
|
|
comfyApp.canvas?.setDirty(true, true)
|
|
}
|
|
)
|
|
|
|
// Clear stale slot layouts when switching modes
|
|
watch(
|
|
() => shouldRenderVueNodes.value,
|
|
() => {
|
|
layoutStore.clearAllSlotLayouts()
|
|
}
|
|
)
|
|
|
|
// Handle case where Vue nodes are enabled but graph starts empty
|
|
const setupEmptyGraphListener = () => {
|
|
const activeGraph = comfyApp.canvas?.graph
|
|
if (
|
|
!shouldRenderVueNodes.value ||
|
|
nodeManager.value ||
|
|
activeGraph?._nodes.length !== 0
|
|
) {
|
|
return
|
|
}
|
|
const originalOnNodeAdded = activeGraph.onNodeAdded
|
|
activeGraph.onNodeAdded = function (node: LGraphNode) {
|
|
// Restore original handler
|
|
activeGraph.onNodeAdded = originalOnNodeAdded
|
|
|
|
// Initialize node manager if needed
|
|
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
|
initializeNodeManager()
|
|
}
|
|
|
|
// Call original handler
|
|
if (originalOnNodeAdded) {
|
|
originalOnNodeAdded.call(this, node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cleanup function for component unmounting
|
|
const cleanup = () => {
|
|
if (nodeManager.value) {
|
|
nodeManager.value.cleanup()
|
|
nodeManager.value = null
|
|
}
|
|
}
|
|
|
|
return {
|
|
nodeManager,
|
|
|
|
// Lifecycle methods
|
|
initializeNodeManager,
|
|
disposeNodeManagerAndSyncs,
|
|
setupEmptyGraphListener,
|
|
cleanup
|
|
}
|
|
}
|
|
|
|
export const useVueNodeLifecycle = createSharedComposable(
|
|
useVueNodeLifecycleIndividual
|
|
)
|