diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index c6eb86404..ee9bea3ba 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -96,7 +96,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu import SideToolbar from '@/components/sidebar/SideToolbar.vue' import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue' import { useChainCallback } from '@/composables/functional/useChainCallback' -import { useNodeEventHandlers } from '@/composables/graph/useNodeEventHandlers' import { useViewportCulling } from '@/composables/graph/useViewportCulling' import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useNodeBadge } from '@/composables/node/useNodeBadge' @@ -116,6 +115,7 @@ import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' 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 { UnauthorizedError, api } from '@/scripts/api' import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index bcd2a3636..a77f764f1 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -22,13 +22,14 @@ 'border-red-500 bg-red-50': error, 'will-change-transform': isDragging }, - lodCssClass + lodCssClass, + 'pointer-events-auto' ) " :style="[ { transform: `translate(${layoutPosition.x ?? position?.x ?? 0}px, ${(layoutPosition.y ?? position?.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`, - pointerEvents: 'auto' + zIndex: zIndex }, dragStyle ]" @@ -192,6 +193,7 @@ onErrorCaptured((error) => { // Use layout system for node position and dragging const { position: layoutPosition, + zIndex, startDrag, handleDrag: handleLayoutDrag, endDrag diff --git a/src/composables/graph/useNodeEventHandlers.ts b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts similarity index 93% rename from src/composables/graph/useNodeEventHandlers.ts rename to src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts index e00f67b5b..5b354bf25 100644 --- a/src/composables/graph/useNodeEventHandlers.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts @@ -11,8 +11,7 @@ import type { Ref } from 'vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' -import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' -import { LayoutSource } from '@/renderer/core/layout/types' +import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' import { useCanvasStore } from '@/stores/graphStore' interface NodeManager { @@ -21,7 +20,7 @@ interface NodeManager { export function useNodeEventHandlers(nodeManager: Ref) { const canvasStore = useCanvasStore() - const layoutMutations = useLayoutMutations() + const { bringNodeToFront } = useNodeZIndex() /** * Handle node selection events @@ -51,8 +50,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { // Bring node to front when clicked (similar to LiteGraph behavior) // Skip if node is pinned to avoid unwanted movement if (!node.flags?.pinned) { - layoutMutations.setSource(LayoutSource.Vue) - layoutMutations.bringNodeToFront(nodeData.id) + bringNodeToFront(nodeData.id) } // Update canvas selection tracking @@ -171,14 +169,13 @@ export function useNodeEventHandlers(nodeManager: Ref) { if (!canvasStore.canvas || !nodeManager.value) return if (!addToSelection) { - canvasStore.canvas.deselectAllNodes() + canvasStore.canvas.deselectAll() } nodeIds.forEach((nodeId) => { const node = nodeManager.value?.getNode(nodeId) if (node && canvasStore.canvas) { - canvasStore.canvas.selectNode(node) - node.selected = true + canvasStore.canvas.select(node) } }) diff --git a/src/renderer/extensions/vueNodes/composables/useNodeZIndex.ts b/src/renderer/extensions/vueNodes/composables/useNodeZIndex.ts new file mode 100644 index 000000000..3a6e71077 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useNodeZIndex.ts @@ -0,0 +1,36 @@ +/** + * Node Z-Index Management Composable + * + * Provides focused functionality for managing node layering through z-index. + * Integrates with the layout system to ensure proper visual ordering. + */ +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' +import type { NodeId } from '@/schemas/comfyWorkflowSchema' + +interface NodeZIndexOptions { + /** + * Layout source for z-index mutations + * @default LayoutSource.Vue + */ + layoutSource?: LayoutSource +} + +export function useNodeZIndex(options: NodeZIndexOptions = {}) { + const { layoutSource = LayoutSource.Vue } = options + const layoutMutations = useLayoutMutations() + + /** + * Bring node to front (highest z-index) + * @param nodeId - The node to bring to front + * @param source - Optional source override + */ + function bringNodeToFront(nodeId: NodeId, source?: LayoutSource) { + layoutMutations.setSource(source ?? layoutSource) + layoutMutations.bringNodeToFront(nodeId) + } + + return { + bringNodeToFront + } +} diff --git a/tests-ui/tests/composables/graph/useNodeEventHandlers.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts similarity index 96% rename from tests-ui/tests/composables/graph/useNodeEventHandlers.test.ts rename to tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts index d98a2134f..51a0ae235 100644 --- a/tests-ui/tests/composables/graph/useNodeEventHandlers.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeEventHandlers.test.ts @@ -1,13 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' -import type { - VueNodeData, - useGraphNodeManager -} from '@/composables/graph/useGraphNodeManager' -import { useNodeEventHandlers } from '@/composables/graph/useNodeEventHandlers' +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers' import { useCanvasStore } from '@/stores/graphStore' vi.mock('@/stores/graphStore', () => ({ diff --git a/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts new file mode 100644 index 000000000..f9c669cd4 --- /dev/null +++ b/tests-ui/tests/renderer/extensions/vueNodes/composables/useNodeZIndex.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' +import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' + +// Mock the layout mutations module +vi.mock('@/renderer/core/layout/operations/layoutMutations') + +const mockedUseLayoutMutations = vi.mocked(useLayoutMutations) + +describe('useNodeZIndex', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('bringNodeToFront', () => { + it('should bring node to front with default source', () => { + const mockSetSource = vi.fn() + const mockBringNodeToFront = vi.fn() + + mockedUseLayoutMutations.mockReturnValue({ + setSource: mockSetSource, + bringNodeToFront: mockBringNodeToFront + } as Partial> as ReturnType< + typeof useLayoutMutations + >) + + const { bringNodeToFront } = useNodeZIndex() + + bringNodeToFront('node1') + + expect(mockSetSource).toHaveBeenCalledWith(LayoutSource.Vue) + expect(mockBringNodeToFront).toHaveBeenCalledWith('node1') + }) + + it('should bring node to front with custom source', () => { + const mockSetSource = vi.fn() + const mockBringNodeToFront = vi.fn() + + mockedUseLayoutMutations.mockReturnValue({ + setSource: mockSetSource, + bringNodeToFront: mockBringNodeToFront + } as Partial> as ReturnType< + typeof useLayoutMutations + >) + + const { bringNodeToFront } = useNodeZIndex() + + bringNodeToFront('node2', LayoutSource.Canvas) + + expect(mockSetSource).toHaveBeenCalledWith(LayoutSource.Canvas) + expect(mockBringNodeToFront).toHaveBeenCalledWith('node2') + }) + + it('should use custom layout source from options', () => { + const mockSetSource = vi.fn() + const mockBringNodeToFront = vi.fn() + + mockedUseLayoutMutations.mockReturnValue({ + setSource: mockSetSource, + bringNodeToFront: mockBringNodeToFront + } as Partial> as ReturnType< + typeof useLayoutMutations + >) + + const { bringNodeToFront } = useNodeZIndex({ + layoutSource: LayoutSource.External + }) + + bringNodeToFront('node3') + + expect(mockSetSource).toHaveBeenCalledWith(LayoutSource.External) + expect(mockBringNodeToFront).toHaveBeenCalledWith('node3') + }) + + it('should override layout source with explicit source parameter', () => { + const mockSetSource = vi.fn() + const mockBringNodeToFront = vi.fn() + + mockedUseLayoutMutations.mockReturnValue({ + setSource: mockSetSource, + bringNodeToFront: mockBringNodeToFront + } as Partial> as ReturnType< + typeof useLayoutMutations + >) + + const { bringNodeToFront } = useNodeZIndex({ + layoutSource: LayoutSource.External + }) + + bringNodeToFront('node4', LayoutSource.Canvas) + + expect(mockSetSource).toHaveBeenCalledWith(LayoutSource.Canvas) + expect(mockBringNodeToFront).toHaveBeenCalledWith('node4') + }) + }) +})