mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
## Summary Replicate the alt+drag to clone behavior present in litegraph. ## Changes - **What**: Simplify the interaction/drag handling, now with less state! - **What**: Alt+Click+Drag a node to clone it ## Screenshots (if applicable) https://github.com/user-attachments/assets/469e33c2-de0c-4e64-a344-1e9d9339d528 <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6789-WIP-Alt-Drag-to-clone-Vue-Nodes-2b16d73d36508102a871ffe97ed2831f) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
169 lines
4.9 KiB
TypeScript
169 lines
4.9 KiB
TypeScript
import { onScopeDispose, ref, toValue } from 'vue'
|
|
import type { MaybeRefOrGetter } from 'vue'
|
|
|
|
import { isMiddlePointerInput } from '@/base/pointerUtils'
|
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
|
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
|
import { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'
|
|
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
|
|
|
|
export function useNodePointerInteractions(
|
|
nodeIdRef: MaybeRefOrGetter<string>
|
|
) {
|
|
const { startDrag, endDrag, handleDrag } = useNodeDrag()
|
|
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
|
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
|
|
useCanvasInteractions()
|
|
const { handleNodeSelect, toggleNodeSelectionAfterPointerUp } =
|
|
useNodeEventHandlers()
|
|
const { nodeManager } = useVueNodeLifecycle()
|
|
|
|
const forwardMiddlePointerIfNeeded = (event: PointerEvent) => {
|
|
if (!isMiddlePointerInput(event)) return false
|
|
forwardEventToCanvas(event)
|
|
return true
|
|
}
|
|
|
|
const startPosition = ref({ x: 0, y: 0 })
|
|
|
|
const DRAG_THRESHOLD = 3 // pixels
|
|
|
|
function onPointerdown(event: PointerEvent) {
|
|
if (forwardMiddlePointerIfNeeded(event)) return
|
|
|
|
// Only start drag on left-click (button 0)
|
|
if (event.button !== 0) return
|
|
|
|
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
|
if (!shouldHandleNodePointerEvents.value) {
|
|
forwardEventToCanvas(event)
|
|
return
|
|
}
|
|
|
|
const nodeId = toValue(nodeIdRef)
|
|
if (!nodeId) {
|
|
console.warn(
|
|
'LGraphNode: nodeData is null/undefined in handlePointerDown'
|
|
)
|
|
return
|
|
}
|
|
|
|
// IMPORTANT: Read from actual LGraphNode to get correct state
|
|
if (nodeManager.value?.getNode(nodeId)?.flags?.pinned) {
|
|
return
|
|
}
|
|
|
|
startPosition.value = { x: event.clientX, y: event.clientY }
|
|
|
|
startDrag(event, nodeId)
|
|
}
|
|
|
|
function onPointermove(event: PointerEvent) {
|
|
if (forwardMiddlePointerIfNeeded(event)) return
|
|
|
|
const nodeId = toValue(nodeIdRef)
|
|
|
|
if (nodeManager.value?.getNode(nodeId)?.flags?.pinned) {
|
|
return
|
|
}
|
|
|
|
const multiSelect = isMultiSelectKey(event)
|
|
|
|
const lmbDown = event.buttons & 1
|
|
if (lmbDown && multiSelect && !layoutStore.isDraggingVueNodes.value) {
|
|
layoutStore.isDraggingVueNodes.value = true
|
|
handleNodeSelect(event, nodeId)
|
|
startDrag(event, nodeId)
|
|
return
|
|
}
|
|
// Check if we should start dragging (pointer moved beyond threshold)
|
|
if (lmbDown && !layoutStore.isDraggingVueNodes.value) {
|
|
const dx = event.clientX - startPosition.value.x
|
|
const dy = event.clientY - startPosition.value.y
|
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
if (distance > DRAG_THRESHOLD) {
|
|
layoutStore.isDraggingVueNodes.value = true
|
|
handleNodeSelect(event, nodeId)
|
|
}
|
|
}
|
|
|
|
if (layoutStore.isDraggingVueNodes.value) {
|
|
handleDrag(event, nodeId)
|
|
}
|
|
}
|
|
|
|
function cleanupDragState() {
|
|
layoutStore.isDraggingVueNodes.value = false
|
|
}
|
|
|
|
function safeDragEnd(event: PointerEvent) {
|
|
try {
|
|
const nodeId = toValue(nodeIdRef)
|
|
endDrag(event, nodeId)
|
|
} catch (error) {
|
|
console.error('Error during endDrag:', error)
|
|
} finally {
|
|
cleanupDragState()
|
|
}
|
|
}
|
|
|
|
function onPointerup(event: PointerEvent) {
|
|
if (forwardMiddlePointerIfNeeded(event)) return
|
|
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
|
const canHandlePointer = shouldHandleNodePointerEvents.value
|
|
if (!canHandlePointer) {
|
|
forwardEventToCanvas(event)
|
|
return
|
|
}
|
|
const wasDragging = layoutStore.isDraggingVueNodes.value
|
|
|
|
if (wasDragging) {
|
|
safeDragEnd(event)
|
|
return
|
|
}
|
|
const multiSelect = isMultiSelectKey(event)
|
|
|
|
const nodeId = toValue(nodeIdRef)
|
|
if (nodeId) {
|
|
toggleNodeSelectionAfterPointerUp(nodeId, multiSelect)
|
|
}
|
|
}
|
|
|
|
function onPointercancel(event: PointerEvent) {
|
|
if (!layoutStore.isDraggingVueNodes.value) return
|
|
safeDragEnd(event)
|
|
}
|
|
|
|
/**
|
|
* Handles right-click during drag operations
|
|
* Cancels the current drag to prevent context menu from appearing while dragging
|
|
*/
|
|
function onContextmenu(event: MouseEvent) {
|
|
if (!layoutStore.isDraggingVueNodes.value) return
|
|
|
|
event.preventDefault()
|
|
// Simply cleanup state without calling endDrag to avoid synthetic event creation
|
|
cleanupDragState()
|
|
}
|
|
|
|
// Cleanup on unmount to prevent resource leaks
|
|
onScopeDispose(() => {
|
|
cleanupDragState()
|
|
})
|
|
|
|
const pointerHandlers = {
|
|
onPointerdown,
|
|
onPointermove,
|
|
onPointerup,
|
|
onPointercancel,
|
|
onContextmenu
|
|
} as const
|
|
|
|
return {
|
|
pointerHandlers
|
|
}
|
|
}
|