diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 5c9d6ecb2..4b0b90823 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -46,7 +46,6 @@ :position="nodePositions.get(nodeData.id)" :size="nodeSizes.get(nodeData.id)" :readonly="false" - :executing="executionStore.executingNodeId === nodeData.id" :error=" executionStore.lastExecutionError?.node_id === nodeData.id ? 'Execution error' @@ -118,6 +117,7 @@ import TransformPane from '@/renderer/core/layout/TransformPane.vue' import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue' import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue' import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers' +import { useExecutionStateProvider } from '@/renderer/extensions/vueNodes/execution/useExecutionStateProvider' import { UnauthorizedError, api } from '@/scripts/api' import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' @@ -205,6 +205,9 @@ const selectedNodeIds = computed( ) provide(SelectedNodeIdsKey, selectedNodeIds) +// Provide execution state to all Vue nodes +useExecutionStateProvider() + watchEffect(() => { nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated') }) diff --git a/src/renderer/core/canvas/injectionKeys.ts b/src/renderer/core/canvas/injectionKeys.ts index 4134846c3..5c850c100 100644 --- a/src/renderer/core/canvas/injectionKeys.ts +++ b/src/renderer/core/canvas/injectionKeys.ts @@ -1,8 +1,25 @@ import type { InjectionKey, Ref } from 'vue' +import type { NodeProgressState } from '@/schemas/apiSchema' + /** * Injection key for providing selected node IDs to Vue node components. * Contains a reactive Set of selected node IDs (as strings). */ export const SelectedNodeIdsKey: InjectionKey>> = Symbol('selectedNodeIds') + +/** + * Injection key for providing executing node IDs to Vue node components. + * Contains a reactive Set of currently executing node IDs (as strings). + */ +export const ExecutingNodeIdsKey: InjectionKey>> = + Symbol('executingNodeIds') + +/** + * Injection key for providing node progress states to Vue node components. + * Contains a reactive Record of node IDs to their current progress state. + */ +export const NodeProgressStatesKey: InjectionKey< + Ref> +> = Symbol('nodeProgressStates') diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 2fa74d573..b0802df93 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -12,7 +12,7 @@ 'lg-node absolute rounded-2xl', // border 'border border-solid border-sand-100 dark-theme:border-charcoal-300', - !!executing && 'border-blue-500 dark-theme:border-blue-500', + !!executing && 'border-blue-100 dark-theme:border-blue-100', !!(error || nodeData.hasErrors) && 'border-error', // hover 'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', @@ -20,7 +20,7 @@ 'outline-transparent -outline-offset-2 outline-2', !!isSelected && 'outline-black dark-theme:outline-white', !!(isSelected && executing) && - 'outline-blue-500 dark-theme:outline-blue-500', + 'outline-blue-100 dark-theme:outline-blue-100', !!(isSelected && (error || nodeData.hasErrors)) && 'outline-error', { 'animate-pulse': executing, @@ -141,6 +141,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useErrorHandling } from '@/composables/useErrorHandling' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' +import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState' import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout' import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD' import { ExecutedWsMessage } from '@/schemas/apiSchema' @@ -162,8 +163,6 @@ interface LGraphNodeProps { position?: { x: number; y: number } size?: { width: number; height: number } readonly?: boolean - executing?: boolean - progress?: number error?: string | null zoomLevel?: number } @@ -202,6 +201,9 @@ const isSelected = computed(() => { return selectedNodeIds.value.has(props.nodeData.id) }) +// Use execution state composable +const { executing, progress } = useNodeExecutionState(props.nodeData.id) + // LOD (Level of Detail) system based on zoom level const zoomRef = toRef(() => props.zoomLevel ?? 1) const { diff --git a/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts b/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts new file mode 100644 index 000000000..aae08298a --- /dev/null +++ b/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts @@ -0,0 +1,36 @@ +import { storeToRefs } from 'pinia' +import { computed, provide } from 'vue' + +import { + ExecutingNodeIdsKey, + NodeProgressStatesKey +} from '@/renderer/core/canvas/injectionKeys' +import { useExecutionStore } from '@/stores/executionStore' + +/** + * Composable for providing execution state to Vue node children + * + * This composable sets up the execution state providers that can be injected + * by child Vue nodes using useNodeExecutionState. + * + * Should be used in the parent component that manages Vue nodes (e.g., GraphCanvas). + */ +export const useExecutionStateProvider = () => { + const executionStore = useExecutionStore() + const { executingNodeIds: storeExecutingNodeIds, nodeProgressStates } = + storeToRefs(executionStore) + + // Convert execution store data to the format expected by Vue nodes + const executingNodeIds = computed( + () => new Set(storeExecutingNodeIds.value.map(String)) + ) + + // Provide the execution state to all child Vue nodes + provide(ExecutingNodeIdsKey, executingNodeIds) + provide(NodeProgressStatesKey, nodeProgressStates) + + return { + executingNodeIds, + nodeProgressStates + } +} diff --git a/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts b/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts new file mode 100644 index 000000000..8f03e29e1 --- /dev/null +++ b/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts @@ -0,0 +1,54 @@ +import { computed, inject, ref } from 'vue' + +import { + ExecutingNodeIdsKey, + NodeProgressStatesKey +} from '@/renderer/core/canvas/injectionKeys' +import type { NodeProgressState } from '@/schemas/apiSchema' + +/** + * Composable for managing execution state of Vue-based nodes + * + * Provides reactive access to execution state and progress for a specific node + * by injecting execution data from the parent GraphCanvas provider. + * + * @param nodeId - The ID of the node to track execution state for + * @returns Object containing reactive execution state and progress + */ +export const useNodeExecutionState = (nodeId: string) => { + const executingNodeIds = inject(ExecutingNodeIdsKey, ref(new Set())) + const nodeProgressStates = inject( + NodeProgressStatesKey, + ref>({}) + ) + + const executing = computed(() => { + return executingNodeIds.value.has(nodeId) + }) + + const progress = computed(() => { + const state = nodeProgressStates.value[nodeId] + return state?.max > 0 ? state.value / state.max : undefined + }) + + const progressState = computed(() => nodeProgressStates.value[nodeId]) + + const progressPercentage = computed(() => { + const prog = progress.value + return prog !== undefined ? Math.round(prog * 100) : undefined + }) + + const executionState = computed(() => { + const state = progressState.value + if (!state) return 'idle' + return state.state + }) + + return { + executing, + progress, + progressPercentage, + progressState, + executionState + } +} diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index 44bb13d2d..b2b11e8a1 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -132,7 +132,7 @@ export const useExecutionStore = defineStore('execution', () => { // Easily access all currently executing node IDs const executingNodeIds = computed(() => { - return Object.entries(nodeProgressStates) + return Object.entries(nodeProgressStates.value) .filter(([_, state]) => state.state === 'running') .map(([nodeId, _]) => nodeId) }) diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts index d8df32f34..6e34e2450 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts @@ -1,13 +1,14 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { createI18n } from 'vue-i18n' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue' import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking' +import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState' vi.mock( '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking', @@ -42,6 +43,19 @@ vi.mock('@/renderer/extensions/vueNodes/lod/useLOD', () => ({ LODLevel: { MINIMAL: 0 } })) +vi.mock( + '@/renderer/extensions/vueNodes/execution/useNodeExecutionState', + () => ({ + useNodeExecutionState: vi.fn(() => ({ + executing: computed(() => false), + progress: computed(() => undefined), + progressPercentage: computed(() => undefined), + progressState: computed(() => undefined as any), + executionState: computed(() => 'idle' as const) + })) + }) +) + const i18n = createI18n({ legacy: false, locale: 'en', @@ -92,6 +106,14 @@ describe('LGraphNode', () => { beforeEach(() => { vi.clearAllMocks() + // Reset to default mock + vi.mocked(useNodeExecutionState).mockReturnValue({ + executing: computed(() => false), + progress: computed(() => undefined), + progressPercentage: computed(() => undefined), + progressState: computed(() => undefined as any), + executionState: computed(() => 'idle' as const) + }) }) it('should call resize tracking composable with node ID', () => { @@ -143,7 +165,16 @@ describe('LGraphNode', () => { }) it('should apply executing animation when executing prop is true', () => { - const wrapper = mountLGraphNode({ nodeData: mockNodeData, executing: true }) + // Mock the execution state to return executing: true + vi.mocked(useNodeExecutionState).mockReturnValue({ + executing: computed(() => true), + progress: computed(() => undefined), + progressPercentage: computed(() => undefined), + progressState: computed(() => undefined as any), + executionState: computed(() => 'running' as const) + }) + + const wrapper = mountLGraphNode({ nodeData: mockNodeData }) expect(wrapper.classes()).toContain('animate-pulse') })