[fix] Preserve per-workflow subgraph navigation state (#4616)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-07-31 19:37:17 -07:00
committed by GitHub
parent baea47c493
commit eae4b954d0
7 changed files with 230 additions and 76 deletions

View File

@@ -115,6 +115,21 @@ export function useMinimap() {
() => workflowStore.activeSubgraph,
() => {
graph.value = app.canvas?.graph
// Force viewport update when switching subgraphs
if (initialized.value && visible.value) {
updateViewport()
}
}
)
// Update viewport when switching workflows
watch(
() => workflowStore.activeWorkflow,
() => {
// Force viewport update when switching workflows
if (initialized.value && visible.value) {
updateViewport()
}
}
)

View File

@@ -73,7 +73,8 @@ export class ChangeTracker {
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
}
const navigation = useSubgraphNavigationStore().exportState()
this.subgraphState = navigation.length ? { navigation } : undefined
// Always store the navigation state, even if empty (root level)
this.subgraphState = { navigation }
}
restore() {
@@ -90,8 +91,14 @@ export class ChangeTracker {
const activeId = navigation.at(-1)
if (activeId) {
// Navigate to the saved subgraph
const subgraph = app.graph.subgraphs.get(activeId)
if (subgraph) app.canvas.setGraph(subgraph)
if (subgraph) {
app.canvas.setGraph(subgraph)
}
} else {
// Empty navigation array means root level
app.canvas.setGraph(app.graph)
}
}
}

View File

@@ -2,9 +2,10 @@ import QuickLRU from '@alloc/quick-lru'
import type { Subgraph } from '@comfyorg/litegraph'
import type { DragAndScaleState } from '@comfyorg/litegraph/dist/DragAndScale'
import { defineStore } from 'pinia'
import { computed, shallowReactive, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { app } from '@/scripts/app'
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
import { isNonNullish } from '@/utils/typeGuardUtil'
import { useCanvasStore } from './graphStore'
@@ -25,7 +26,7 @@ export const useSubgraphNavigationStore = defineStore(
const activeSubgraph = shallowRef<Subgraph>()
/** The stack of subgraph IDs from the root graph to the currently opened subgraph. */
const idStack = shallowReactive<string[]>([])
const idStack = ref<string[]>([])
/** LRU cache for viewport states. Key: subgraph ID or 'root' for root graph */
const viewportCache = new QuickLRU<string, DragAndScaleState>({
@@ -37,7 +38,9 @@ export const useSubgraphNavigationStore = defineStore(
* the current opened subgraph.
*/
const navigationStack = computed(() =>
idStack.map((id) => app.graph.subgraphs.get(id)).filter(isNonNullish)
idStack.value
.map((id) => app.graph.subgraphs.get(id))
.filter(isNonNullish)
)
/**
@@ -46,8 +49,8 @@ export const useSubgraphNavigationStore = defineStore(
* @see exportState
*/
const restoreState = (subgraphIds: string[]) => {
idStack.length = 0
for (const id of subgraphIds) idStack.push(id)
idStack.value.length = 0
for (const id of subgraphIds) idStack.value.push(id)
}
/**
@@ -55,7 +58,7 @@ export const useSubgraphNavigationStore = defineStore(
* @returns The list of subgraph IDs, ending with the currently active subgraph.
* @see restoreState
*/
const exportState = () => [...idStack]
const exportState = () => [...idStack.value]
/**
* Get the current viewport state.
@@ -99,47 +102,49 @@ export const useSubgraphNavigationStore = defineStore(
canvas.setDirty(true, true)
}
// Reset on workflow change
watch(
() => workflowStore.activeWorkflow,
() => {
idStack.length = 0
/**
* Update the navigation stack when the active subgraph changes.
* @param subgraph The new active subgraph.
* @param prevSubgraph The previous active subgraph.
*/
const onNavigated = (
subgraph: Subgraph | undefined,
prevSubgraph: Subgraph | undefined
) => {
// Save viewport state for the graph we're leaving
if (prevSubgraph) {
// Leaving a subgraph
saveViewport(prevSubgraph.id)
} else if (!prevSubgraph && subgraph) {
// Leaving root graph to enter a subgraph
saveViewport('root')
}
)
// Update navigation stack when opened subgraph changes
const isInRootGraph = !subgraph
if (isInRootGraph) {
idStack.value.length = 0
restoreViewport('root')
return
}
const path = findSubgraphPathById(subgraph.rootGraph, subgraph.id)
const isInReachableSubgraph = !!path
if (isInReachableSubgraph) {
idStack.value = [...path]
} else {
// Treat as if opening a new subgraph
idStack.value = [subgraph.id]
}
// Always try to restore viewport for the target subgraph
restoreViewport(subgraph.id)
}
// Update navigation stack when opened subgraph changes (also triggers when switching workflows)
watch(
() => workflowStore.activeSubgraph,
(subgraph, prevSubgraph) => {
// Save viewport state for the graph we're leaving
if (prevSubgraph) {
// Leaving a subgraph
saveViewport(prevSubgraph.id)
} else if (!prevSubgraph && subgraph) {
// Leaving root graph to enter a subgraph
saveViewport('root')
}
// Navigated back to the root graph
if (!subgraph) {
idStack.length = 0
restoreViewport('root')
return
}
const index = idStack.lastIndexOf(subgraph.id)
const lastIndex = idStack.length - 1
if (index === -1) {
// Opened a new subgraph
idStack.push(subgraph.id)
} else if (index !== lastIndex) {
// Navigated to a different subgraph
idStack.splice(index + 1, lastIndex - index)
}
// Always try to restore viewport for the target subgraph
restoreViewport(subgraph.id)
(newValue, oldValue) => {
onNavigated(newValue, oldValue)
}
)

View File

@@ -205,7 +205,7 @@ export function findSubgraphByUuid(
targetUuid: string
): Subgraph | null {
// Check all nodes in the current graph
for (const node of graph._nodes) {
for (const node of graph.nodes) {
if (node.isSubgraphNode?.() && node.subgraph) {
if (node.subgraph.id === targetUuid) {
return node.subgraph
@@ -218,6 +218,42 @@ export function findSubgraphByUuid(
return null
}
/**
* Iteratively finds the path of subgraph IDs to a target subgraph.
* @param rootGraph The graph to start searching from.
* @param targetId The ID of the subgraph to find.
* @returns An array of subgraph IDs representing the path, or `null` if not found.
*/
export function findSubgraphPathById(
rootGraph: LGraph,
targetId: string
): string[] | null {
const stack: { graph: LGraph | Subgraph; path: string[] }[] = [
{ graph: rootGraph, path: [] }
]
while (stack.length > 0) {
const { graph, path } = stack.pop()!
// Check if graph exists and has _nodes property
if (!graph || !graph._nodes || !Array.isArray(graph._nodes)) {
continue
}
for (const node of graph._nodes) {
if (node.isSubgraphNode?.() && node.subgraph) {
const newPath = [...path, String(node.subgraph.id)]
if (node.subgraph.id === targetId) {
return newPath
}
stack.push({ graph: node.subgraph, path: newPath })
}
}
}
return null
}
/**
* Get a node by its execution ID from anywhere in the graph hierarchy.
* Execution IDs use hierarchical format like "123:456:789" for nested nodes.