From c4c0e52e642f53c79c7e3c2a687b15c8b2f1b814 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Sat, 20 Sep 2025 22:14:30 -0700 Subject: [PATCH] Refactor: Let LGraphNode handle more events itself (#5709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Don't route events up through GraphCanvas if the component itself can handle the changes ## Changes - **What**: Reduce the indirect access or action dispatch to composables/stores. ## Review Focus The behavior should be either equivalent or a little snappier than before. Also, the local state in LGraphNode has (almost) all been removed in favor of reacting to the nodeData prop. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5709-Refactor-Let-LGraphNode-handle-more-events-itself-2756d73d365081e6a88ce6241bceecc0) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- eslint.config.ts | 6 + src/components/graph/GraphCanvas.vue | 12 -- src/components/graph/SelectionToolbox.vue | 2 +- src/lib/litegraph/src/litegraph.ts | 1 - src/renderer/core/canvas/injectionKeys.ts | 18 --- src/renderer/core/layout/injectionKeys.ts | 30 +--- .../vueNodes/components/LGraphNode.vue | 126 ++++----------- .../vueNodes/components/NodeHeader.vue | 2 - .../vueNodes/components/NodeSlots.vue | 2 - .../composables/useNodeEventHandlers.ts | 1 + .../composables/useVueNodeResizeTracking.ts | 11 +- .../execution/useExecutionStateProvider.ts | 36 ----- .../execution/useNodeExecutionState.ts | 25 ++- .../vueNodes/layout/useNodeLayout.ts | 5 +- .../extensions/vueNodes/lod/useLOD.ts | 5 +- .../vueNodes/preview/useNodePreviewState.ts | 5 +- src/stores/executionStore.ts | 153 +++++++----------- src/types/litegraph-augmentation.d.ts | 2 +- .../vueNodes/components/LGraphNode.spec.ts | 29 +++- 19 files changed, 159 insertions(+), 312 deletions(-) delete mode 100644 src/renderer/core/canvas/injectionKeys.ts delete mode 100644 src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts diff --git a/eslint.config.ts b/eslint.config.ts index 3073948f2..04f4b2578 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -77,6 +77,12 @@ export default defineConfig([ '@typescript-eslint/prefer-as-const': 'off', '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'always' + } + ], 'unused-imports/no-unused-imports': 'error', 'vue/no-v-html': 'off', // Enforce dark-theme: instead of dark: prefix diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index c5839e1f1..73020bb2e 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -53,9 +53,6 @@ " :zoom-level="canvasStore.canvas?.ds?.scale || 1" :data-node-id="nodeData.id" - @node-click="handleNodeSelect" - @update:collapsed="handleNodeCollapse" - @update:title="handleNodeTitleUpdate" /> @@ -121,8 +118,6 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti import TransformPane from '@/renderer/core/layout/transform/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' @@ -173,7 +168,6 @@ const { shouldRenderVueNodes } = useVueFeatureFlags() // Vue node system const vueNodeLifecycle = useVueNodeLifecycle() const viewportCulling = useViewportCulling() -const nodeEventHandlers = useNodeEventHandlers() const handleVueNodeLifecycleReset = async () => { if (shouldRenderVueNodes.value) { @@ -204,12 +198,6 @@ const handleTransformUpdate = () => { // TODO: Fix paste position sync in separate PR vueNodeLifecycle.detectChangesInRAF.value() } -const handleNodeSelect = nodeEventHandlers.handleNodeSelect -const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse -const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate - -// Provide execution state to all Vue nodes -useExecutionStateProvider() watchEffect(() => { nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated') diff --git a/src/components/graph/SelectionToolbox.vue b/src/components/graph/SelectionToolbox.vue index 067b04346..f0a18ed3e 100644 --- a/src/components/graph/SelectionToolbox.vue +++ b/src/components/graph/SelectionToolbox.vue @@ -11,7 +11,7 @@ :style="`backgroundColor: ${containerStyles.backgroundColor};`" :pt="{ header: 'hidden', - content: 'px-1 py-1 h-10 px-1 flex flex-row gap-1' + content: 'p-1 h-10 flex flex-row gap-1' }" @wheel="canvasInteractions.handleWheel" > diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index 46b094af0..098c30e7a 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -48,7 +48,6 @@ export interface LinkReleaseContextExtended { links: ConnectingLink[] } -// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface LiteGraphCanvasEvent extends CustomEvent {} export interface LGraphNodeConstructor { diff --git a/src/renderer/core/canvas/injectionKeys.ts b/src/renderer/core/canvas/injectionKeys.ts deleted file mode 100644 index 9c0d25733..000000000 --- a/src/renderer/core/canvas/injectionKeys.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { InjectionKey, Ref } from 'vue' - -import type { NodeProgressState } from '@/schemas/apiSchema' - -/** - * 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/core/layout/injectionKeys.ts b/src/renderer/core/layout/injectionKeys.ts index dd6efda21..8e0e0e1d6 100644 --- a/src/renderer/core/layout/injectionKeys.ts +++ b/src/renderer/core/layout/injectionKeys.ts @@ -1,6 +1,6 @@ import type { InjectionKey } from 'vue' -import type { Point } from '@/renderer/core/layout/types' +import type { useTransformState } from '@/renderer/core/layout/transform/useTransformState' /** * Lightweight, injectable transform state used by layout-aware components. @@ -21,29 +21,11 @@ import type { Point } from '@/renderer/core/layout/types' * const state = inject(TransformStateKey)! * const screen = state.canvasToScreen({ x: 100, y: 50 }) */ -interface TransformState { - /** Convert a screen-space point (CSS pixels) to canvas space. */ - screenToCanvas: (p: Point) => Point - /** Convert a canvas-space point to screen space (CSS pixels). */ - canvasToScreen: (p: Point) => Point - /** Current pan/zoom; `x`/`y` are offsets, `z` is scale. */ - camera?: { x: number; y: number; z: number } - /** - * Test whether a node's rectangle intersects the (expanded) viewport. - * Handy for viewport culling and lazy work. - * - * @param nodePos Top-left in canvas space `[x, y]` - * @param nodeSize Size in canvas units `[width, height]` - * @param viewport Screen-space viewport `{ width, height }` - * @param margin Optional fractional margin (e.g. `0.2` = 20%) - */ - isNodeInViewport?: ( - nodePos: ArrayLike, - nodeSize: ArrayLike, - viewport: { width: number; height: number }, - margin?: number - ) => boolean -} +interface TransformState + extends Pick< + ReturnType, + 'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport' + > {} export const TransformStateKey: InjectionKey = Symbol('transformState') diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index ce318e82e..56a0984fb 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -54,7 +54,7 @@ :lod-level="lodLevel" :collapsed="isCollapsed" @collapse="handleCollapse" - @update:title="handleTitleUpdate" + @update:title="handleHeaderTitleUpdate" @enter-subgraph="handleEnterSubgraph" /> @@ -101,7 +101,6 @@ :node-data="nodeData" :readonly="readonly" :lod-level="lodLevel" - @slot-click="handleSlotClick" /> @@ -140,15 +139,7 @@ diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index 3bd75024c..40b8a7fe0 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -63,7 +63,6 @@ import EditableText from '@/components/common/EditableText.vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useErrorHandling } from '@/composables/useErrorHandling' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' -import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD' import { app } from '@/scripts/app' import { getLocatorIdFromNodeData, @@ -73,7 +72,6 @@ import { interface NodeHeaderProps { nodeData?: VueNodeData readonly?: boolean - lodLevel?: LODLevel collapsed?: boolean } diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.vue b/src/renderer/extensions/vueNodes/components/NodeSlots.vue index 931cacb4d..26187899d 100644 --- a/src/renderer/extensions/vueNodes/components/NodeSlots.vue +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.vue @@ -35,7 +35,6 @@ import { computed, onErrorCaptured, ref } from 'vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useErrorHandling } from '@/composables/useErrorHandling' import type { INodeSlot } from '@/lib/litegraph/src/litegraph' -import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD' import { isSlotObject } from '@/utils/typeGuardUtil' import InputSlot from './InputSlot.vue' @@ -44,7 +43,6 @@ import OutputSlot from './OutputSlot.vue' interface NodeSlotsProps { nodeData?: VueNodeData readonly?: boolean - lodLevel?: LODLevel } const { nodeData = null, readonly } = defineProps() diff --git a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts index 1d090af84..8ed854a0c 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts @@ -82,6 +82,7 @@ function useNodeEventHandlersIndividual() { const currentCollapsed = node.flags?.collapsed ?? false if (currentCollapsed !== collapsed) { node.collapse() + nodeManager.value.scheduleUpdate(nodeId, 'critical') } } diff --git a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts index 4ce9f8e62..e8c38164d 100644 --- a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts @@ -8,7 +8,13 @@ * Supports different element types (nodes, slots, widgets, etc.) with * customizable data attributes and update handlers. */ -import { getCurrentInstance, onMounted, onUnmounted } from 'vue' +import { + type MaybeRefOrGetter, + getCurrentInstance, + onMounted, + onUnmounted, + toValue +} from 'vue' import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' import { LiteGraph } from '@/lib/litegraph/src/litegraph' @@ -154,9 +160,10 @@ const resizeObserver = new ResizeObserver((entries) => { * ``` */ export function useVueElementTracking( - appIdentifier: string, + appIdentifierMaybe: MaybeRefOrGetter, trackingType: string ) { + const appIdentifier = toValue(appIdentifierMaybe) onMounted(() => { const element = getCurrentInstance()?.proxy?.$el if (!(element instanceof HTMLElement) || !appIdentifier) return diff --git a/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts b/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts deleted file mode 100644 index aae08298a..000000000 --- a/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 index 8f03e29e1..aa4867db9 100644 --- a/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts +++ b/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts @@ -1,10 +1,7 @@ -import { computed, inject, ref } from 'vue' +import { storeToRefs } from 'pinia' +import { type MaybeRefOrGetter, computed, toValue } from 'vue' -import { - ExecutingNodeIdsKey, - NodeProgressStatesKey -} from '@/renderer/core/canvas/injectionKeys' -import type { NodeProgressState } from '@/schemas/apiSchema' +import { useExecutionStore } from '@/stores/executionStore' /** * Composable for managing execution state of Vue-based nodes @@ -12,18 +9,18 @@ import type { NodeProgressState } from '@/schemas/apiSchema' * 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 + * @param nodeIdMaybe - 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>({}) - ) +export const useNodeExecutionState = ( + nodeIdMaybe: MaybeRefOrGetter +) => { + const nodeId = toValue(nodeIdMaybe) + const { uniqueExecutingNodeIdStrings, nodeProgressStates } = + storeToRefs(useExecutionStore()) const executing = computed(() => { - return executingNodeIds.value.has(nodeId) + return uniqueExecutingNodeIdStrings.value.has(nodeId) }) const progress = computed(() => { diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts index 3274d342d..515ebb04e 100644 --- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -5,7 +5,7 @@ import { storeToRefs } from 'pinia' * Uses customRef for shared write access with Canvas renderer. * Provides dragging functionality and reactive layout state. */ -import { computed, inject } from 'vue' +import { type MaybeRefOrGetter, computed, inject, toValue } from 'vue' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' @@ -17,7 +17,8 @@ import { LayoutSource, type Point } from '@/renderer/core/layout/types' * Composable for individual Vue node components * Uses customRef for shared write access with Canvas renderer */ -export function useNodeLayout(nodeId: string) { +export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter) { + const nodeId = toValue(nodeIdMaybe) const mutations = useLayoutMutations() const { selectedNodeIds } = storeToRefs(useCanvasStore()) diff --git a/src/renderer/extensions/vueNodes/lod/useLOD.ts b/src/renderer/extensions/vueNodes/lod/useLOD.ts index 584e21f9a..87c1bb865 100644 --- a/src/renderer/extensions/vueNodes/lod/useLOD.ts +++ b/src/renderer/extensions/vueNodes/lod/useLOD.ts @@ -27,7 +27,7 @@ * * ``` */ -import { type Ref, computed, readonly } from 'vue' +import { type MaybeRefOrGetter, computed, readonly, toRef } from 'vue' export enum LODLevel { MINIMAL = 'minimal', // zoom <= 0.4 @@ -78,7 +78,8 @@ const LOD_CONFIGS: Record = { * @param zoomRef - Reactive reference to current zoom level (camera.z) * @returns LOD state and configuration */ -export function useLOD(zoomRef: Ref) { +export function useLOD(zoomRefMaybe: MaybeRefOrGetter) { + const zoomRef = toRef(zoomRefMaybe) // Continuous LOD score (0-1) for smooth transitions const lodScore = computed(() => { const zoom = zoomRef.value diff --git a/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts index 427cf20c0..8fc82147a 100644 --- a/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts +++ b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts @@ -1,16 +1,17 @@ import { storeToRefs } from 'pinia' -import { type Ref, computed } from 'vue' +import { type MaybeRefOrGetter, type Ref, computed, toValue } from 'vue' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' export const useNodePreviewState = ( - nodeId: string, + nodeIdMaybe: MaybeRefOrGetter, options?: { isMinimalLOD?: Ref isCollapsed?: Ref } ) => { + const nodeId = toValue(nodeIdMaybe) const workflowStore = useWorkflowStore() const { nodePreviewImages } = storeToRefs(useNodeOutputStore()) diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index cfc7a2dd4..8791ab4e1 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -43,6 +43,57 @@ interface QueuedPrompt { workflow?: ComfyWorkflow } +const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => { + const node = graph.getNodeById(id) + if (node?.isSubgraphNode()) return node.subgraph +} + +/** + * Recursively get the subgraph objects for the given subgraph instance IDs + * @param currentGraph The current graph + * @param subgraphNodeIds The instance IDs + * @param subgraphs The subgraphs + * @returns The subgraphs that correspond to each of the instance IDs. + */ +function getSubgraphsFromInstanceIds( + currentGraph: LGraph | Subgraph, + subgraphNodeIds: string[], + subgraphs: Subgraph[] = [] +): Subgraph[] { + // Last segment is the node portion; nothing to do. + if (subgraphNodeIds.length === 1) return subgraphs + + const currentPart = subgraphNodeIds.shift() + if (currentPart === undefined) return subgraphs + + const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph) + if (!subgraph) throw new Error(`Subgraph not found: ${currentPart}`) + + subgraphs.push(subgraph) + return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs) +} + +/** + * Convert execution context node IDs to NodeLocatorIds + * @param nodeId The node ID from execution context (could be execution ID) + * @returns The NodeLocatorId + */ +function executionIdToNodeLocatorId(nodeId: string | number): NodeLocatorId { + const nodeIdStr = String(nodeId) + + if (!nodeIdStr.includes(':')) { + // It's a top-level node ID + return nodeIdStr + } + + // It's an execution node ID + const parts = nodeIdStr.split(':') + const localNodeId = parts[parts.length - 1] + const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts) + const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId) + return nodeLocatorId +} + export const useExecutionStore = defineStore('execution', () => { const workflowStore = useWorkflowStore() const canvasStore = useCanvasStore() @@ -55,29 +106,6 @@ export const useExecutionStore = defineStore('execution', () => { // This is the progress of all nodes in the currently executing workflow const nodeProgressStates = ref>({}) - /** - * Convert execution context node IDs to NodeLocatorIds - * @param nodeId The node ID from execution context (could be execution ID) - * @returns The NodeLocatorId - */ - const executionIdToNodeLocatorId = ( - nodeId: string | number - ): NodeLocatorId => { - const nodeIdStr = String(nodeId) - - if (!nodeIdStr.includes(':')) { - // It's a top-level node ID - return nodeIdStr - } - - // It's an execution node ID - const parts = nodeIdStr.split(':') - const localNodeId = parts[parts.length - 1] - const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts) - const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId) - return nodeLocatorId - } - const mergeExecutionProgressStates = ( currentState: NodeProgressState | undefined, newState: NodeProgressState @@ -139,9 +167,13 @@ export const useExecutionStore = defineStore('execution', () => { // @deprecated For backward compatibility - stores the primary executing node ID const executingNodeId = computed(() => { - return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null + return executingNodeIds.value[0] ?? null }) + const uniqueExecutingNodeIdStrings = computed( + () => new Set(executingNodeIds.value.map(String)) + ) + // For backward compatibility - returns the primary executing node const executingNode = computed(() => { if (!executingNodeId.value) return null @@ -159,36 +191,6 @@ export const useExecutionStore = defineStore('execution', () => { ) }) - const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => { - const node = graph.getNodeById(id) - if (node?.isSubgraphNode()) return node.subgraph - } - - /** - * Recursively get the subgraph objects for the given subgraph instance IDs - * @param currentGraph The current graph - * @param subgraphNodeIds The instance IDs - * @param subgraphs The subgraphs - * @returns The subgraphs that correspond to each of the instance IDs. - */ - const getSubgraphsFromInstanceIds = ( - currentGraph: LGraph | Subgraph, - subgraphNodeIds: string[], - subgraphs: Subgraph[] = [] - ): Subgraph[] => { - // Last segment is the node portion; nothing to do. - if (subgraphNodeIds.length === 1) return subgraphs - - const currentPart = subgraphNodeIds.shift() - if (currentPart === undefined) return subgraphs - - const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph) - if (!subgraph) throw new Error(`Subgraph not found: ${currentPart}`) - - subgraphs.push(subgraph) - return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs) - } - // This is the progress of the currently executing node (for backward compatibility) const _executingNodeProgress = ref(null) const executingNodeProgress = computed(() => @@ -423,66 +425,25 @@ export const useExecutionStore = defineStore('execution', () => { return { isIdle, clientId, - /** - * The id of the prompt that is currently being executed - */ activePromptId, - /** - * The queued prompts - */ queuedPrompts, - /** - * The node errors from the previous execution. - */ lastNodeErrors, - /** - * The error from the previous execution. - */ lastExecutionError, - /** - * Local node ID for the most recent execution error. - */ lastExecutionErrorNodeId, - /** - * The id of the node that is currently being executed (backward compatibility) - */ executingNodeId, - /** - * The list of all nodes that are currently executing - */ executingNodeIds, - /** - * The prompt that is currently being executed - */ activePrompt, - /** - * The total number of nodes to execute - */ totalNodesToExecute, - /** - * The number of nodes that have been executed - */ nodesExecuted, - /** - * The progress of the execution - */ executionProgress, - /** - * The node that is currently being executed (backward compatibility) - */ executingNode, - /** - * The progress of the executing node (backward compatibility) - */ executingNodeProgress, - /** - * All node progress states from progress_state events - */ nodeProgressStates, nodeLocationProgressStates, bindExecutionEvents, unbindExecutionEvents, storePrompt, + uniqueExecutingNodeIdStrings, // Raw executing progress data for backward compatibility in ComfyApp. _executingNodeProgress, // NodeLocatorId conversion helpers diff --git a/src/types/litegraph-augmentation.d.ts b/src/types/litegraph-augmentation.d.ts index d404ff88f..0f5dee17d 100644 --- a/src/types/litegraph-augmentation.d.ts +++ b/src/types/litegraph-augmentation.d.ts @@ -82,7 +82,7 @@ declare module '@/lib/litegraph/src/litegraph' { } // Add interface augmentations into the class itself - // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface BaseWidget extends IBaseWidget {} interface LGraphNode { 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 42d16569a..0615e9b9a 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/components/LGraphNode.spec.ts @@ -1,12 +1,13 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { computed } from 'vue' +import { computed, toValue } from 'vue' import type { ComponentProps } from 'vue-component-type-helpers' import { createI18n } from 'vue-i18n' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue' +import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers' import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking' const mockData = vi.hoisted(() => ({ @@ -25,6 +26,14 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => { } }) +vi.mock( + '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers', + () => { + const handleNodeSelect = vi.fn() + return { useNodeEventHandlers: () => ({ handleNodeSelect }) } + } +) + vi.mock( '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking', () => ({ @@ -130,7 +139,13 @@ describe('LGraphNode', () => { it('should call resize tracking composable with node ID', () => { mountLGraphNode({ nodeData: mockNodeData }) - expect(useVueElementTracking).toHaveBeenCalledWith('test-node-123', 'node') + expect(useVueElementTracking).toHaveBeenCalledWith( + expect.any(Function), + 'node' + ) + const idArg = vi.mocked(useVueElementTracking).mock.calls[0]?.[0] + const id = toValue(idArg) + expect(id).toEqual('test-node-123') }) it('should render with data-node-id attribute', () => { @@ -179,12 +194,16 @@ describe('LGraphNode', () => { }) it('should emit node-click event on pointer up', async () => { + const { handleNodeSelect } = useNodeEventHandlers() const wrapper = mountLGraphNode({ nodeData: mockNodeData }) await wrapper.trigger('pointerup') - expect(wrapper.emitted('node-click')).toHaveLength(1) - expect(wrapper.emitted('node-click')?.[0]).toHaveLength(3) - expect(wrapper.emitted('node-click')?.[0][1]).toEqual(mockNodeData) + expect(handleNodeSelect).toHaveBeenCalledOnce() + expect(handleNodeSelect).toHaveBeenCalledWith( + expect.any(PointerEvent), + mockNodeData, + expect.any(Boolean) + ) }) })