Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions src/components/rightSidePanel/settings/SetNodeState.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,56 @@ import { useI18n } from 'vue-i18n'

import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import FormSelectButton from '@/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue'

import LayoutField from './LayoutField.vue'

/**
* Good design limits dependencies and simplifies the interface of the abstraction layer.
* Here, we only care about the mode method,
* Here, we only care about the node id,
* and do not concern ourselves with other methods.
*/
type PickedNode = Pick<LGraphNode, 'mode'>
type PickedNode = Pick<LGraphNode, 'id'>

const { nodes } = defineProps<{ nodes: PickedNode[] }>()
const emit = defineEmits<{ (e: 'changed'): void }>()

const { t } = useI18n()

const nodeIds = computed(() => nodes.map((node) => node.id.toString()))

/**
* Retrieves layout references for all selected nodes from the store.
*/
const nodeRefs = computed(() =>
nodeIds.value.map((nodeId) => layoutStore.getNodeLayoutRef(nodeId))
)

/**
* Manages the execution mode state for selected nodes.
* When getting: returns the common mode if all nodes share the same mode, null otherwise.
* When setting: applies the new mode to all selected nodes via the layout store.
*/
const nodeState = computed({
get() {
let mode: LGraphNode['mode'] | null = null
if (nodeIds.value.length === 0) return null

if (nodes.length === 0) return null
const modes = nodeRefs.value
.map((nodeRef) => nodeRef.value?.mode)
.filter((mode): mode is number => mode !== undefined && mode !== null)

// For multiple nodes, if all nodes have the same mode, return that mode, otherwise return null
if (nodes.length > 1) {
mode = nodes[0].mode
if (!nodes.every((node) => node.mode === mode)) {
mode = null
}
} else {
mode = nodes[0].mode
}
if (modes.length === 0) return null

return mode
const firstMode = modes[0]
const allSame = modes.every((mode) => mode === firstMode)

return allSame ? firstMode : null
},
set(value: LGraphNode['mode']) {
nodes.forEach((node) => {
node.mode = value
})
if (value === null || value === undefined) return

layoutStore.setNodesMode(nodeIds.value, value)
emit('changed')
}
})
Expand Down
120 changes: 100 additions & 20 deletions src/composables/canvas/useSelectedLiteGraphItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'

Expand Down Expand Up @@ -224,8 +225,19 @@ describe('useSelectedLiteGraphItems', () => {

it('toggleSelectedNodesMode should toggle node modes correctly', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode
const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode
const node1 = { id: '1', mode: LGraphEventMode.ALWAYS } as LGraphNode
const node2 = { id: '2', mode: LGraphEventMode.NEVER } as LGraphNode

// Initialize nodes in layoutStore
layoutStore.initializeFromLiteGraph([
{
id: '1',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.ALWAYS
},
{ id: '2', pos: [0, 0], size: [100, 100], mode: LGraphEventMode.NEVER }
])

app.canvas.selected_nodes = { '0': node1, '1': node2 }

Expand All @@ -234,21 +246,32 @@ describe('useSelectedLiteGraphItems', () => {

// node1 should change from ALWAYS to NEVER
// node2 should stay NEVER (since a selected node exists which is not NEVER)
expect(node1.mode).toBe(LGraphEventMode.NEVER)
expect(node2.mode).toBe(LGraphEventMode.NEVER)
expect(layoutStore.getNodeLayoutRef('1').value?.mode).toBe(
LGraphEventMode.NEVER
)
expect(layoutStore.getNodeLayoutRef('2').value?.mode).toBe(
LGraphEventMode.NEVER
)
})

it('toggleSelectedNodesMode should set mode to ALWAYS when already in target mode', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
const node = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode
const node = { id: '1', mode: LGraphEventMode.BYPASS } as LGraphNode

// Initialize node in layoutStore
layoutStore.initializeFromLiteGraph([
{ id: '1', pos: [0, 0], size: [100, 100], mode: LGraphEventMode.BYPASS }
])

app.canvas.selected_nodes = { '0': node }

// Toggle to BYPASS mode (node is already BYPASS)
toggleSelectedNodesMode(LGraphEventMode.BYPASS)

// Should change to ALWAYS
expect(node.mode).toBe(LGraphEventMode.ALWAYS)
expect(layoutStore.getNodeLayoutRef('1').value?.mode).toBe(
LGraphEventMode.ALWAYS
)
})

it('getSelectedNodes should include nodes from subgraphs', () => {
Expand Down Expand Up @@ -277,17 +300,43 @@ describe('useSelectedLiteGraphItems', () => {

it('toggleSelectedNodesMode should apply unified state to subgraph children', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode
const subNode1 = { id: '11', mode: LGraphEventMode.ALWAYS } as LGraphNode
const subNode2 = { id: '12', mode: LGraphEventMode.NEVER } as LGraphNode
const subgraphNode = {
id: 1,
id: '1',
mode: LGraphEventMode.ALWAYS,
isSubgraphNode: () => true,
subgraph: {
nodes: [subNode1, subNode2]
}
} as unknown as LGraphNode
const regularNode = { id: 2, mode: LGraphEventMode.BYPASS } as LGraphNode
const regularNode = {
id: '2',
mode: LGraphEventMode.BYPASS
} as LGraphNode

// Initialize all nodes in layoutStore
layoutStore.initializeFromLiteGraph([
{
id: '1',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.ALWAYS
},
{
id: '2',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.BYPASS
},
{
id: '11',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.ALWAYS
},
{ id: '12', pos: [0, 0], size: [100, 100], mode: LGraphEventMode.NEVER }
])

app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode }

Expand All @@ -296,40 +345,71 @@ describe('useSelectedLiteGraphItems', () => {

// Selected nodes follow standard toggle logic:
// subgraphNode: ALWAYS -> NEVER (since ALWAYS != NEVER)
expect(subgraphNode.mode).toBe(LGraphEventMode.NEVER)
expect(layoutStore.getNodeLayoutRef('1').value?.mode).toBe(
LGraphEventMode.NEVER
)
// regularNode: BYPASS -> NEVER (since BYPASS != NEVER)
expect(regularNode.mode).toBe(LGraphEventMode.NEVER)
expect(layoutStore.getNodeLayoutRef('2').value?.mode).toBe(
LGraphEventMode.NEVER
)

// Subgraph children get unified state (same as their parent):
// Both children should now be NEVER, regardless of their previous states
expect(subNode1.mode).toBe(LGraphEventMode.NEVER) // was ALWAYS, now NEVER
expect(subNode2.mode).toBe(LGraphEventMode.NEVER) // was NEVER, stays NEVER
expect(layoutStore.getNodeLayoutRef('11').value?.mode).toBe(
LGraphEventMode.NEVER
) // was ALWAYS, now NEVER
expect(layoutStore.getNodeLayoutRef('12').value?.mode).toBe(
LGraphEventMode.NEVER
) // was NEVER, stays NEVER
})

it('toggleSelectedNodesMode should toggle to ALWAYS when subgraph is already in target mode', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
const subNode2 = { id: 12, mode: LGraphEventMode.BYPASS } as LGraphNode
const subNode1 = { id: '11', mode: LGraphEventMode.ALWAYS } as LGraphNode
const subNode2 = { id: '12', mode: LGraphEventMode.BYPASS } as LGraphNode
const subgraphNode = {
id: 1,
id: '1',
mode: LGraphEventMode.NEVER, // Already in NEVER mode
isSubgraphNode: () => true,
subgraph: {
nodes: [subNode1, subNode2]
}
} as unknown as LGraphNode

// Initialize all nodes in layoutStore
layoutStore.initializeFromLiteGraph([
{ id: '1', pos: [0, 0], size: [100, 100], mode: LGraphEventMode.NEVER },
{
id: '11',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.ALWAYS
},
{
id: '12',
pos: [0, 0],
size: [100, 100],
mode: LGraphEventMode.BYPASS
}
])

app.canvas.selected_nodes = { '0': subgraphNode }

// Toggle to NEVER mode (but subgraphNode is already NEVER)
toggleSelectedNodesMode(LGraphEventMode.NEVER)

// Selected subgraph should toggle to ALWAYS (since it was already NEVER)
expect(subgraphNode.mode).toBe(LGraphEventMode.ALWAYS)
expect(layoutStore.getNodeLayoutRef('1').value?.mode).toBe(
LGraphEventMode.ALWAYS
)

// All children should also get ALWAYS (unified with parent's new state)
expect(subNode1.mode).toBe(LGraphEventMode.ALWAYS)
expect(subNode2.mode).toBe(LGraphEventMode.ALWAYS)
expect(layoutStore.getNodeLayoutRef('11').value?.mode).toBe(
LGraphEventMode.ALWAYS
)
expect(layoutStore.getNodeLayoutRef('12').value?.mode).toBe(
LGraphEventMode.ALWAYS
)
})
})

Expand Down
18 changes: 12 additions & 6 deletions src/composables/canvas/useSelectedLiteGraphItems.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import {
Expand Down Expand Up @@ -119,16 +120,21 @@ export function useSelectedLiteGraphItems() {
for (const i in selectedNodes) {
selectedNodeArray.push(selectedNodes[i])
}
const allNodesMatch = !selectedNodeArray.some(
(selectedNode) => selectedNode.mode !== mode
)

// Check if all selected nodes are already in the target mode
const allNodesMatch = !selectedNodeArray.some((selectedNode) => {
const nodeRef = layoutStore.getNodeLayoutRef(selectedNode.id.toString())
return nodeRef.value?.mode !== mode
})
const newModeForSelectedNode = allNodesMatch ? LGraphEventMode.ALWAYS : mode

// Process each selected node independently to determine its target state and apply to children
selectedNodeArray.forEach((selectedNode) => {
// Apply standard toggle logic to the selected node itself

selectedNode.mode = newModeForSelectedNode
layoutStore.setNodeMode(
selectedNode.id.toString(),
newModeForSelectedNode
)

// If this selected node is a subgraph, apply the same mode uniformly to all its children
// This ensures predictable behavior: all children get the same state as their parent
Expand All @@ -139,7 +145,7 @@ export function useSelectedLiteGraphItems() {
if (node === selectedNode) return undefined

// Apply the parent's new mode to all children uniformly
node.mode = newModeForSelectedNode
layoutStore.setNodeMode(node.id.toString(), newModeForSelectedNode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider batch mode updates for subgraph children.

When traversing subgraph nodes, each child triggers a separate layoutStore.setNodeMode call. For large subgraphs, using layoutStore.setNodesMode with a pre-collected map of node IDs to modes could be more efficient.

♻️ Optional: Collect child nodes and batch update
       // If this selected node is a subgraph, apply the same mode uniformly to all its children
       // This ensures predictable behavior: all children get the same state as their parent
       if (selectedNode.isSubgraphNode?.() && selectedNode.subgraph) {
+        const childNodeModes: Record<string, number> = {}
         traverseNodesDepthFirst([selectedNode], {
           visitor: (node) => {
             // Skip the parent node since we already handled it above
             if (node === selectedNode) return undefined
 
-            // Apply the parent's new mode to all children uniformly
-            layoutStore.setNodeMode(node.id.toString(), newModeForSelectedNode)
+            childNodeModes[node.id.toString()] = newModeForSelectedNode
             return undefined
           }
         })
+        if (Object.keys(childNodeModes).length > 0) {
+          layoutStore.setNodesMode(childNodeModes)
+        }
       }
🤖 Prompt for AI Agents
In `@src/composables/canvas/useSelectedLiteGraphItems.ts` at line 148, The loop
currently calls layoutStore.setNodeMode(node.id.toString(),
newModeForSelectedNode) for each child which is inefficient for large subgraphs;
modify the traversal in useSelectedLiteGraphItems.ts to collect a map/object of
child node IDs to newModeForSelectedNode (use node.id.toString() as the key) and
after traversal call layoutStore.setNodesMode(collectedMap) once; keep the
existing per-node logic to determine newModeForSelectedNode but replace
individual setNodeMode calls with building the map and a single batch
setNodesMode call.

return undefined
}
})
Expand Down
7 changes: 6 additions & 1 deletion src/composables/graph/useVueNodeLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 { patchLGraphNodeMode } from '@/renderer/core/layout/sync/patchLGraphNodeMode'
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
Expand Down Expand Up @@ -36,7 +37,8 @@ function useVueNodeLifecycleIndividual() {
size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [
number,
number
]
],
mode: node.mode
}))
layoutStore.initializeFromLiteGraph(nodes)

Expand All @@ -59,6 +61,9 @@ function useVueNodeLifecycleIndividual() {
)
}

// Patch LGraphNode.changeMode to sync with layoutStore
patchLGraphNodeMode()

// Initialize layout sync (one-way: Layout Store → LiteGraph)
startSync(canvasStore.canvas)
}
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/core/groupOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
LGraphGroup,
type LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ComfyExtension } from '@/types/comfy'

import { app } from '../../scripts/app'

function setNodeMode(node: LGraphNode, mode: number) {
node.mode = mode
node.graph?.change()
layoutStore.setNodeMode(node.id.toString(), mode)
}
Comment on lines 16 to 18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good refactor to centralized mode management.

The function now correctly delegates to layoutStore.setNodeMode. However, consider using layoutStore.setNodesMode for batch updates when changing mode for all nodes in a group, as this could reduce redundant store operations.

♻️ Optional: Use batch mode update
 function setNodeMode(node: LGraphNode, mode: number) {
   layoutStore.setNodeMode(node.id.toString(), mode)
 }
+
+function setNodesModeForGroup(nodes: LGraphNode[], mode: number) {
+  const nodeIdModes: Record<string, number> = {}
+  for (const node of nodes) {
+    nodeIdModes[node.id.toString()] = mode
+  }
+  layoutStore.setNodesMode(nodeIdModes)
+}

Then update the callbacks to use the batch function instead of looping.

🤖 Prompt for AI Agents
In `@src/extensions/core/groupOptions.ts` around lines 16 - 18, The current
setNodeMode(node: LGraphNode, mode: number) delegates to layoutStore.setNodeMode
for single-node updates; when you change mode for an entire group, replace the
per-node loop with a single batch call to layoutStore.setNodesMode(idsArray,
mode) to avoid repetitive store writes—keep setNodeMode for individual updates
but modify the group callbacks (where you currently iterate nodes and call
setNodeMode) to collect node.id strings into an array and call
layoutStore.setNodesMode once with that array and the new mode.


function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,9 @@ export class LGraphNode
case LGraphEventMode.ALWAYS:
break

case LGraphEventMode.BYPASS:
break

// @ts-expect-error Not impl.
case LiteGraph.ON_REQUEST:
break
Expand Down
1 change: 1 addition & 0 deletions src/renderer/core/layout/operations/layoutMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function useLayoutMutations(): LayoutMutations {
size: layout.size ?? { width: 200, height: 100 },
zIndex: layout.zIndex ?? 0,
visible: layout.visible ?? true,
mode: layout.mode ?? 0, // Default to ALWAYS
bounds: {
x: layout.position?.x ?? 0,
y: layout.position?.y ?? 0,
Expand Down
1 change: 1 addition & 0 deletions src/renderer/core/layout/store/layoutStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('layoutStore CRDT operations', () => {
size: { width: 200, height: 100 },
zIndex: 0,
visible: true,
mode: 0,
bounds: { x: 100, y: 100, width: 200, height: 100 }
})

Expand Down
Loading
Loading