feat: drop nodes when click the canvas

This commit is contained in:
Yourz
2026-02-08 00:56:38 +08:00
parent f80b87b46c
commit 70b514a48d
4 changed files with 314 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
<template>
<div class="flex h-full flex-col overflow-hidden">
<div id="node-library-node-preview-container-v2" />
<NodeDragPreview />
<!-- Fixed header -->
<div class="shrink-0 px-4 pt-2 pb-1">
<h2 class="m-0 mb-1 text-sm font-bold leading-8">
@@ -70,7 +71,7 @@ import { computed, nextTick, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchBox from '@/components/common/SearchBoxV2.vue'
import { useLitegraphService } from '@/services/litegraphService'
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
import {
DEFAULT_TAB_ID,
nodeOrganizationService
@@ -86,6 +87,7 @@ import type {
import AllNodesPanel from './nodeLibrary/AllNodesPanel.vue'
import CustomNodesPanel from './nodeLibrary/CustomNodesPanel.vue'
import EssentialNodesPanel from './nodeLibrary/EssentialNodesPanel.vue'
import NodeDragPreview from './nodeLibrary/NodeDragPreview.vue'
const selectedTab = useLocalStorage<TabId>(
'Comfy.NodeLibrary.Tab',
@@ -109,7 +111,7 @@ const expandedKeys = computed({
})
const nodeDefStore = useNodeDefStore()
const litegraphService = useLitegraphService()
const { startDrag } = useNodeDragToCanvas()
const filteredNodeDefs = computed(() => {
if (searchQuery.value.length === 0) {
@@ -222,7 +224,7 @@ function collectFolderKeys(node: TreeNode): string[] {
function handleNodeClick(node: RenderedTreeExplorerNode<ComfyNodeDefImpl>) {
if (node.type === 'node' && node.data) {
litegraphService.addNodeOnGraph(node.data)
startDrag(node.data)
}
if (node.type === 'folder') {
const index = expandedKeys.value.indexOf(node.key)

View File

@@ -0,0 +1,27 @@
<template>
<Teleport to="body">
<div
v-if="isDragging && draggedNode"
class="pointer-events-none fixed z-[10000] rounded-md bg-comfy-menu-bg px-3 py-2 text-sm font-medium text-foreground shadow-lg border border-border-default"
:style="{
left: `${cursorPosition.x + 12}px`,
top: `${cursorPosition.y + 12}px`
}"
>
{{ draggedNode.display_name }}
</div>
</Teleport>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
const { isDragging, draggedNode, cursorPosition, setupGlobalListeners } =
useNodeDragToCanvas()
onMounted(() => {
setupGlobalListeners()
})
</script>

View File

@@ -0,0 +1,207 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import type { useNodeDragToCanvas as UseNodeDragToCanvasType } from './useNodeDragToCanvas'
const mockAddNodeOnGraph = vi.fn()
const mockConvertEventToCanvasOffset = vi.fn()
const mockCanvas = {
canvas: {
getBoundingClientRect: vi.fn()
},
convertEventToCanvasOffset: mockConvertEventToCanvasOffset
}
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn(() => ({
canvas: mockCanvas
}))
}))
vi.mock('@/services/litegraphService', () => ({
useLitegraphService: vi.fn(() => ({
addNodeOnGraph: mockAddNodeOnGraph
}))
}))
describe('useNodeDragToCanvas', () => {
let useNodeDragToCanvas: typeof UseNodeDragToCanvasType
const mockNodeDef = {
name: 'TestNode',
display_name: 'Test Node'
} as ComfyNodeDefImpl
beforeEach(async () => {
vi.resetModules()
vi.clearAllMocks()
const module = await import('./useNodeDragToCanvas')
useNodeDragToCanvas = module.useNodeDragToCanvas
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('startDrag', () => {
it('should set isDragging to true and store the node definition', () => {
const { isDragging, draggedNode, startDrag } = useNodeDragToCanvas()
expect(isDragging.value).toBe(false)
expect(draggedNode.value).toBeNull()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
expect(draggedNode.value).toBe(mockNodeDef)
})
})
describe('cancelDrag', () => {
it('should reset isDragging and draggedNode', () => {
const { isDragging, draggedNode, startDrag, cancelDrag } =
useNodeDragToCanvas()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
cancelDrag()
expect(isDragging.value).toBe(false)
expect(draggedNode.value).toBeNull()
})
})
describe('setupGlobalListeners', () => {
it('should add event listeners to document', () => {
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
const { setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function)
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
true
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'keydown',
expect.any(Function)
)
})
it('should only setup listeners once', () => {
const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
const { setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
const callCount = addEventListenerSpy.mock.calls.length
setupGlobalListeners()
expect(addEventListenerSpy.mock.calls.length).toBe(callCount)
})
})
describe('cursorPosition', () => {
it('should update on pointermove', () => {
const { cursorPosition, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
const pointerEvent = new PointerEvent('pointermove', {
clientX: 100,
clientY: 200
})
document.dispatchEvent(pointerEvent)
expect(cursorPosition.value).toEqual({ x: 100, y: 200 })
})
})
describe('endDrag behavior', () => {
it('should add node when pointer is over canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const pointerEvent = new PointerEvent('pointerup', {
clientX: 250,
clientY: 250,
bubbles: true
})
document.dispatchEvent(pointerEvent)
expect(mockAddNodeOnGraph).toHaveBeenCalledWith(mockNodeDef, {
pos: [150, 150]
})
})
it('should not add node when pointer is outside canvas', () => {
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
left: 0,
right: 500,
top: 0,
bottom: 500
})
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const pointerEvent = new PointerEvent('pointerup', {
clientX: 600,
clientY: 250,
bubbles: true
})
document.dispatchEvent(pointerEvent)
expect(mockAddNodeOnGraph).not.toHaveBeenCalled()
expect(isDragging.value).toBe(false)
})
it('should cancel drag on Escape key', () => {
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
expect(isDragging.value).toBe(true)
const keyEvent = new KeyboardEvent('keydown', { key: 'Escape' })
document.dispatchEvent(keyEvent)
expect(isDragging.value).toBe(false)
})
it('should not cancel drag on other keys', () => {
const { startDrag, setupGlobalListeners, isDragging } =
useNodeDragToCanvas()
setupGlobalListeners()
startDrag(mockNodeDef)
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' })
document.dispatchEvent(keyEvent)
expect(isDragging.value).toBe(true)
})
})
})

View File

@@ -0,0 +1,75 @@
import { ref, shallowRef } from 'vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLitegraphService } from '@/services/litegraphService'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
const isDragging = ref(false)
const draggedNode = shallowRef<ComfyNodeDefImpl | null>(null)
const cursorPosition = ref({ x: 0, y: 0 })
let listenersSetup = false
function updatePosition(e: PointerEvent) {
cursorPosition.value = { x: e.clientX, y: e.clientY }
}
function cancelDrag() {
isDragging.value = false
draggedNode.value = null
}
function endDrag(e: PointerEvent) {
if (!isDragging.value || !draggedNode.value) return
const canvasStore = useCanvasStore()
const canvas = canvasStore.canvas
if (!canvas) {
cancelDrag()
return
}
const canvasElement = canvas.canvas as HTMLCanvasElement
const rect = canvasElement.getBoundingClientRect()
const isOverCanvas =
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
if (isOverCanvas) {
const pos = canvas.convertEventToCanvasOffset(e)
const litegraphService = useLitegraphService()
litegraphService.addNodeOnGraph(draggedNode.value, { pos })
}
cancelDrag()
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') cancelDrag()
}
function setupGlobalListeners() {
if (listenersSetup) return
listenersSetup = true
document.addEventListener('pointermove', updatePosition)
document.addEventListener('pointerup', endDrag, true)
document.addEventListener('keydown', handleKeydown)
}
export function useNodeDragToCanvas() {
function startDrag(nodeDef: ComfyNodeDefImpl) {
isDragging.value = true
draggedNode.value = nodeDef
}
return {
isDragging,
draggedNode,
cursorPosition,
startDrag,
cancelDrag,
setupGlobalListeners
}
}