fix: suppress link rendering during slot sync after graph reconfigure

After graph reconfiguration (e.g., undo/redo), there's a timing gap where
canvas renders links before Vue components have mounted and synced slot
positions to layoutStore.

This adds a pendingSlotSync flag to layoutStore that:
- Is set to true at start of graph.onConfigure()
- Is cleared after flushScheduledSlotLayoutSync() completes
- Causes drawConnections() to skip link rendering when true

This prevents links from rendering with stale/missing positions during the
brief window between configure() completing and Vue components syncing.

Amp-Thread-ID: https://ampcode.com/threads/T-019c084c-49dd-764b-8125-8938c42612c8
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Subagent 5
2026-01-28 23:24:53 -08:00
parent dd3e4d3edc
commit 299386aa03
4 changed files with 38 additions and 2 deletions

View File

@@ -5614,6 +5614,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.renderedPaths.clear()
if (this.links_render_mode === LinkRenderType.HIDDEN_LINK) return
// Skip link rendering while waiting for slot positions to sync after reconfigure
if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) return
const { graph, subgraph } = this
if (!graph) throw new NullGraphError()

View File

@@ -141,6 +141,20 @@ class LayoutStoreImpl implements LayoutStore {
// Vue resizing state to prevent drag from activating during resize
public isResizingVueNodes = ref(false)
/**
* Flag indicating slot positions are pending sync after graph reconfiguration.
* When true, link rendering should be skipped to avoid drawing with stale positions.
*/
private _pendingSlotSync = false
get pendingSlotSync(): boolean {
return this._pendingSlotSync
}
setPendingSlotSync(value: boolean): void {
this._pendingSlotSync = value
}
constructor() {
// Initialize Yjs data structures
this.ynodes = this.ydoc.getMap('nodes')

View File

@@ -31,13 +31,19 @@ function scheduleSlotLayoutSync(nodeId: string) {
raf.schedule()
}
function flushScheduledSlotLayoutSync() {
if (pendingNodes.size === 0) return
export function flushScheduledSlotLayoutSync() {
if (pendingNodes.size === 0) {
// Even if no pending nodes, clear the flag (e.g., graph with no nodes)
layoutStore.setPendingSlotSync(false)
return
}
const conv = useSharedCanvasPositionConversion()
for (const nodeId of Array.from(pendingNodes)) {
pendingNodes.delete(nodeId)
syncNodeSlotLayoutsFromDOM(nodeId, conv)
}
// Clear the pending sync flag - slots are now synced
layoutStore.setPendingSlotSync(false)
}
export function syncNodeSlotLayoutsFromDOM(

View File

@@ -5,6 +5,8 @@ import { reactive, unref } from 'vue'
import { shallowRef } from 'vue'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
import { st, t } from '@/i18n'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
@@ -730,6 +732,11 @@ export class ComfyApp {
private addAfterConfigureHandler(graph: LGraph) {
const { onConfigure } = graph
graph.onConfigure = function (...args) {
// Set pending sync flag to suppress link rendering until slots are synced
if (LiteGraph.vueNodesMode) {
layoutStore.setPendingSlotSync(true)
}
fixLinkInputSlots(this)
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
@@ -740,6 +747,12 @@ export class ComfyApp {
// Fire after onConfigure, used by primitives to generate widget using input nodes config
triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured')
// Flush pending slot layout syncs to fix link alignment after undo/redo
if (LiteGraph.vueNodesMode) {
flushScheduledSlotLayoutSync()
app.canvas?.setDirty(true, true)
}
return r
}
}