mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 09:19:43 +00:00
Compare commits
2 Commits
cloud/mode
...
refactor/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aee2366496 | ||
|
|
4dd4334287 |
@@ -38,7 +38,7 @@
|
|||||||
backgroundColor: nodeBodyBackgroundColor,
|
backgroundColor: nodeBodyBackgroundColor,
|
||||||
opacity: nodeOpacity
|
opacity: nodeOpacity
|
||||||
},
|
},
|
||||||
dragStyle
|
{ cursor: nodeStyle.cursor }
|
||||||
]"
|
]"
|
||||||
v-bind="pointerHandlers"
|
v-bind="pointerHandlers"
|
||||||
@wheel="handleWheel"
|
@wheel="handleWheel"
|
||||||
@@ -262,8 +262,10 @@ onErrorCaptured((error) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Use layout system for node position and dragging
|
// Use layout system for node position and dragging
|
||||||
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id)
|
const { position, size, zIndex, resize, nodeStyle } = useNodeLayout(
|
||||||
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
|
() => nodeData.id
|
||||||
|
)
|
||||||
|
const { pointerHandlers, isDragging } = useNodePointerInteractions(
|
||||||
() => nodeData,
|
() => nodeData,
|
||||||
handleNodeSelect
|
handleNodeSelect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { nextTick, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||||
|
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||||
|
|
||||||
// Mock the dependencies
|
// Mock the dependencies
|
||||||
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
||||||
@@ -12,19 +13,27 @@ vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock the layout system
|
||||||
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
|
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
|
||||||
useNodeLayout: () => ({
|
useNodeLayout: vi.fn()
|
||||||
startDrag: vi.fn(),
|
|
||||||
endDrag: vi.fn().mockResolvedValue(undefined),
|
|
||||||
handleDrag: vi.fn().mockResolvedValue(undefined)
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
const createBaseNodeLayoutMock = () => ({
|
||||||
layoutStore: {
|
isDragging: ref(false),
|
||||||
isDraggingVueNodes: ref(false)
|
startDrag: vi.fn(() => true),
|
||||||
}
|
endDrag: vi.fn(() => Promise.resolve()),
|
||||||
}))
|
handleDrag: vi.fn(),
|
||||||
|
layoutRef: ref(null),
|
||||||
|
position: computed(() => ({ x: 0, y: 0 })),
|
||||||
|
size: computed(() => ({ width: 200, height: 100 })),
|
||||||
|
bounds: computed(() => ({ x: 0, y: 0, width: 200, height: 100 })),
|
||||||
|
isVisible: computed(() => true),
|
||||||
|
zIndex: computed(() => 0),
|
||||||
|
moveTo: vi.fn(),
|
||||||
|
resize: vi.fn(),
|
||||||
|
nodeStyle: computed(() => ({ position: 'absolute' as const }))
|
||||||
|
})
|
||||||
|
vi.mock('@/renderer/core/layout/store/layoutStore')
|
||||||
|
|
||||||
const createMockVueNodeData = (
|
const createMockVueNodeData = (
|
||||||
overrides: Partial<VueNodeData> = {}
|
overrides: Partial<VueNodeData> = {}
|
||||||
@@ -42,28 +51,38 @@ const createMockVueNodeData = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
const createPointerEvent = (
|
const createPointerEvent = (
|
||||||
eventType: string,
|
type: string,
|
||||||
overrides: Partial<PointerEventInit> = {}
|
overrides: Partial<PointerEvent> = {}
|
||||||
): PointerEvent => {
|
): PointerEvent => {
|
||||||
return new PointerEvent(eventType, {
|
const event = new PointerEvent(type, {
|
||||||
pointerId: 1,
|
pointerId: 1,
|
||||||
button: 0,
|
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
|
button: 0,
|
||||||
...overrides
|
...overrides
|
||||||
})
|
})
|
||||||
|
Object.defineProperty(event, 'target', {
|
||||||
|
value: {
|
||||||
|
setPointerCapture: vi.fn(),
|
||||||
|
releasePointerCapture: vi.fn()
|
||||||
|
},
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
const createMouseEvent = (
|
const createMouseEvent = (
|
||||||
eventType: string,
|
type: string,
|
||||||
overrides: Partial<MouseEventInit> = {}
|
overrides: Partial<MouseEvent> = {}
|
||||||
): MouseEvent => {
|
): MouseEvent => {
|
||||||
return new MouseEvent(eventType, {
|
const event = new MouseEvent(type, {
|
||||||
button: 2, // Right click
|
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
|
button: 0,
|
||||||
...overrides
|
...overrides
|
||||||
})
|
})
|
||||||
|
event.preventDefault = vi.fn()
|
||||||
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('useNodePointerInteractions', () => {
|
describe('useNodePointerInteractions', () => {
|
||||||
@@ -71,144 +90,241 @@ describe('useNodePointerInteractions', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should only start drag on left-click', async () => {
|
afterEach(() => {
|
||||||
const mockNodeData = createMockVueNodeData()
|
vi.restoreAllMocks()
|
||||||
const mockOnPointerUp = vi.fn()
|
|
||||||
|
|
||||||
const { pointerHandlers } = useNodePointerInteractions(
|
|
||||||
ref(mockNodeData),
|
|
||||||
mockOnPointerUp
|
|
||||||
)
|
|
||||||
|
|
||||||
// Right-click should not start drag
|
|
||||||
const rightClickEvent = createPointerEvent('pointerdown', { button: 2 })
|
|
||||||
pointerHandlers.onPointerdown(rightClickEvent)
|
|
||||||
|
|
||||||
expect(mockOnPointerUp).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
// Left-click should start drag and emit callback
|
|
||||||
const leftClickEvent = createPointerEvent('pointerdown', { button: 0 })
|
|
||||||
pointerHandlers.onPointerdown(leftClickEvent)
|
|
||||||
|
|
||||||
const pointerUpEvent = createPointerEvent('pointerup')
|
|
||||||
pointerHandlers.onPointerup(pointerUpEvent)
|
|
||||||
|
|
||||||
expect(mockOnPointerUp).toHaveBeenCalledWith(
|
|
||||||
pointerUpEvent,
|
|
||||||
mockNodeData,
|
|
||||||
false // wasDragging = false (same position)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should distinguish drag from click based on distance threshold', async () => {
|
describe('DrJKL single source of truth architecture', () => {
|
||||||
const mockNodeData = createMockVueNodeData()
|
it('should use useNodeLayout.isDragging as the single authority', () => {
|
||||||
const mockOnPointerUp = vi.fn()
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
const { pointerHandlers } = useNodePointerInteractions(
|
const mockIsDragging = ref(false)
|
||||||
ref(mockNodeData),
|
const testMock = createBaseNodeLayoutMock()
|
||||||
mockOnPointerUp
|
testMock.isDragging = mockIsDragging
|
||||||
)
|
|
||||||
|
|
||||||
// Test drag (distance > 4px)
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
pointerHandlers.onPointerdown(
|
|
||||||
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
|
|
||||||
)
|
|
||||||
|
|
||||||
const dragUpEvent = createPointerEvent('pointerup', {
|
const { isDragging } = useNodePointerInteractions(
|
||||||
clientX: 200,
|
ref(mockNodeData),
|
||||||
clientY: 200
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(isDragging).toBe(mockIsDragging)
|
||||||
})
|
})
|
||||||
pointerHandlers.onPointerup(dragUpEvent)
|
|
||||||
|
|
||||||
expect(mockOnPointerUp).toHaveBeenCalledWith(
|
it('should eliminate coordination object entirely', () => {
|
||||||
dragUpEvent,
|
const mockNodeData = createMockVueNodeData()
|
||||||
mockNodeData,
|
const mockOnPointerUp = vi.fn()
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
mockOnPointerUp.mockClear()
|
vi.mocked(useNodeLayout).mockReturnValueOnce(createBaseNodeLayoutMock())
|
||||||
|
|
||||||
// Test click (same position)
|
const result = useNodePointerInteractions(
|
||||||
const samePos = { clientX: 100, clientY: 100 }
|
ref(mockNodeData),
|
||||||
pointerHandlers.onPointerdown(createPointerEvent('pointerdown', samePos))
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
const clickUpEvent = createPointerEvent('pointerup', samePos)
|
expect(result.isDragging).toBeDefined()
|
||||||
pointerHandlers.onPointerup(clickUpEvent)
|
expect(result.pointerHandlers).toBeDefined()
|
||||||
|
expect(result.stopWatcher).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
expect(mockOnPointerUp).toHaveBeenCalledWith(
|
it('should use pure Vue reactivity for global state sync', async () => {
|
||||||
clickUpEvent,
|
const mockNodeData = createMockVueNodeData()
|
||||||
mockNodeData,
|
const mockOnPointerUp = vi.fn()
|
||||||
false
|
|
||||||
)
|
const mockStartDrag = vi.fn(() => true)
|
||||||
|
const testMock = createBaseNodeLayoutMock()
|
||||||
|
testMock.startDrag = mockStartDrag
|
||||||
|
|
||||||
|
vi.mocked(useNodeLayout).mockReturnValue(testMock)
|
||||||
|
|
||||||
|
const { pointerHandlers, isDragging } = useNodePointerInteractions(
|
||||||
|
ref(mockNodeData),
|
||||||
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
|
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
||||||
|
|
||||||
|
expect(mockStartDrag).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'pointerdown',
|
||||||
|
button: 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(isDragging).toBe(testMock.isDragging)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make startDrag return success boolean to fix race condition', () => {
|
||||||
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
|
const mockIsDragging = ref(false)
|
||||||
|
const mockStartDrag = vi.fn().mockImplementation(() => {
|
||||||
|
mockIsDragging.value = true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const testMock = createBaseNodeLayoutMock()
|
||||||
|
testMock.isDragging = mockIsDragging
|
||||||
|
testMock.startDrag = mockStartDrag
|
||||||
|
|
||||||
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
|
|
||||||
|
const { pointerHandlers } = useNodePointerInteractions(
|
||||||
|
ref(mockNodeData),
|
||||||
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test 1: startDrag should be called and return success
|
||||||
|
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
||||||
|
expect(mockStartDrag).toHaveBeenCalled()
|
||||||
|
expect(mockStartDrag).toHaveReturnedWith(true)
|
||||||
|
|
||||||
|
// Reset for next test
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockIsDragging.value = false
|
||||||
|
|
||||||
|
// Test 2: If startDrag fails, drag state shouldn't be set
|
||||||
|
mockStartDrag.mockReturnValue(false)
|
||||||
|
|
||||||
|
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
||||||
|
expect(mockStartDrag).toHaveBeenCalled()
|
||||||
|
expect(mockIsDragging.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have clean Vue-native implementation without manual sync', () => {
|
||||||
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
|
const mockIsDragging = ref(false)
|
||||||
|
const testMock = createBaseNodeLayoutMock()
|
||||||
|
testMock.isDragging = mockIsDragging
|
||||||
|
|
||||||
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
|
|
||||||
|
const result = useNodePointerInteractions(
|
||||||
|
ref(mockNodeData),
|
||||||
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.isDragging).toBeDefined()
|
||||||
|
expect(result.pointerHandlers).toBeDefined()
|
||||||
|
expect(result.stopWatcher).toBeDefined()
|
||||||
|
|
||||||
|
expect(result.isDragging.value).toBe(false)
|
||||||
|
mockIsDragging.value = true
|
||||||
|
expect(result.isDragging.value).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle drag termination via cancel and context menu', async () => {
|
// Essential integration tests
|
||||||
const mockNodeData = createMockVueNodeData()
|
describe('basic functionality', () => {
|
||||||
const mockOnPointerUp = vi.fn()
|
it('should only start drag on left-click', async () => {
|
||||||
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
const { pointerHandlers } = useNodePointerInteractions(
|
const mockStartDrag = vi.fn()
|
||||||
ref(mockNodeData),
|
const testMock = createBaseNodeLayoutMock()
|
||||||
mockOnPointerUp
|
testMock.startDrag = mockStartDrag
|
||||||
)
|
|
||||||
|
|
||||||
// Test pointer cancel
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
|
||||||
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
|
|
||||||
|
|
||||||
// Should not emit callback on cancel
|
const { pointerHandlers } = useNodePointerInteractions(
|
||||||
expect(mockOnPointerUp).not.toHaveBeenCalled()
|
ref(mockNodeData),
|
||||||
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
// Test context menu during drag prevents default
|
// Right-click should not start drag
|
||||||
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
const rightClick = createPointerEvent('pointerdown', { button: 2 })
|
||||||
|
pointerHandlers.onPointerdown(rightClick)
|
||||||
|
expect(mockStartDrag).not.toHaveBeenCalled()
|
||||||
|
|
||||||
const contextMenuEvent = createMouseEvent('contextmenu')
|
// Left-click should start drag
|
||||||
const preventDefaultSpy = vi.spyOn(contextMenuEvent, 'preventDefault')
|
const leftClick = createPointerEvent('pointerdown', { button: 0 })
|
||||||
|
pointerHandlers.onPointerdown(leftClick)
|
||||||
|
expect(mockStartDrag).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
pointerHandlers.onContextmenu(contextMenuEvent)
|
it('should distinguish drag from click based on distance threshold', async () => {
|
||||||
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
expect(preventDefaultSpy).toHaveBeenCalled()
|
const mockIsDragging = ref(false)
|
||||||
})
|
const testMock = createBaseNodeLayoutMock()
|
||||||
|
testMock.isDragging = mockIsDragging
|
||||||
|
|
||||||
it('should not emit callback when nodeData becomes null', async () => {
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
const mockNodeData = createMockVueNodeData()
|
|
||||||
const mockOnPointerUp = vi.fn()
|
|
||||||
const nodeDataRef = ref<VueNodeData | null>(mockNodeData)
|
|
||||||
|
|
||||||
const { pointerHandlers } = useNodePointerInteractions(
|
const { pointerHandlers } = useNodePointerInteractions(
|
||||||
nodeDataRef,
|
ref(mockNodeData),
|
||||||
mockOnPointerUp
|
mockOnPointerUp
|
||||||
)
|
)
|
||||||
|
|
||||||
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
// Start at 100, 100
|
||||||
|
pointerHandlers.onPointerdown(
|
||||||
|
createPointerEvent('pointerdown', { clientX: 100, clientY: 100 })
|
||||||
|
)
|
||||||
|
|
||||||
// Clear nodeData before pointerup
|
// Move just 2 pixels (below threshold)
|
||||||
nodeDataRef.value = null
|
pointerHandlers.onPointerup(
|
||||||
|
createPointerEvent('pointerup', { clientX: 102, clientY: 102 })
|
||||||
|
)
|
||||||
|
|
||||||
pointerHandlers.onPointerup(createPointerEvent('pointerup'))
|
// Should be considered a click, not drag
|
||||||
|
expect(mockOnPointerUp).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
mockNodeData,
|
||||||
|
false // wasDragging = false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
expect(mockOnPointerUp).not.toHaveBeenCalled()
|
it('should handle drag termination via cancel and context menu', async () => {
|
||||||
})
|
const mockNodeData = createMockVueNodeData()
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
it('should integrate with layout store dragging state', async () => {
|
const mockIsDragging = ref(true)
|
||||||
const mockNodeData = createMockVueNodeData()
|
const testMock = createBaseNodeLayoutMock()
|
||||||
const mockOnPointerUp = vi.fn()
|
testMock.isDragging = mockIsDragging
|
||||||
const { layoutStore } = await import(
|
|
||||||
'@/renderer/core/layout/store/layoutStore'
|
|
||||||
)
|
|
||||||
|
|
||||||
const { pointerHandlers } = useNodePointerInteractions(
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
ref(mockNodeData),
|
|
||||||
mockOnPointerUp
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start drag
|
const { pointerHandlers } = useNodePointerInteractions(
|
||||||
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
ref(mockNodeData),
|
||||||
await nextTick()
|
mockOnPointerUp
|
||||||
expect(layoutStore.isDraggingVueNodes.value).toBe(true)
|
)
|
||||||
|
|
||||||
// End drag
|
const contextMenu = createMouseEvent('contextmenu')
|
||||||
pointerHandlers.onPointercancel(createPointerEvent('pointercancel'))
|
pointerHandlers.onContextmenu(contextMenu)
|
||||||
await nextTick()
|
|
||||||
expect(layoutStore.isDraggingVueNodes.value).toBe(false)
|
expect(contextMenu.preventDefault).toHaveBeenCalled()
|
||||||
|
mockIsDragging.value = false
|
||||||
|
const contextMenuNotDragging = createMouseEvent('contextmenu')
|
||||||
|
pointerHandlers.onContextmenu(contextMenuNotDragging)
|
||||||
|
|
||||||
|
expect(contextMenuNotDragging.preventDefault).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not emit callback when nodeData becomes null', async () => {
|
||||||
|
const mockOnPointerUp = vi.fn()
|
||||||
|
|
||||||
|
const mockIsDragging = ref(false)
|
||||||
|
const testMock = createBaseNodeLayoutMock()
|
||||||
|
testMock.isDragging = mockIsDragging
|
||||||
|
|
||||||
|
vi.mocked(useNodeLayout).mockReturnValueOnce(testMock)
|
||||||
|
|
||||||
|
// Start with null nodeData
|
||||||
|
const { pointerHandlers } = useNodePointerInteractions(
|
||||||
|
ref(null),
|
||||||
|
mockOnPointerUp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should not crash or call callback
|
||||||
|
pointerHandlers.onPointerdown(createPointerEvent('pointerdown'))
|
||||||
|
pointerHandlers.onPointerup(createPointerEvent('pointerup'))
|
||||||
|
|
||||||
|
expect(mockOnPointerUp).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { type MaybeRefOrGetter, computed, onUnmounted, ref, toValue } from 'vue'
|
import { type MaybeRefOrGetter, computed, ref, toValue, watch } from 'vue'
|
||||||
|
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
// Treat tiny pointer jitter as a click, not a drag
|
// Treat tiny pointer jitter as a click, not a drag
|
||||||
const DRAG_THRESHOLD_PX = 4
|
const DRAG_THRESHOLD_PX = 4
|
||||||
|
const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX
|
||||||
|
|
||||||
export function useNodePointerInteractions(
|
export function useNodePointerInteractions(
|
||||||
nodeDataMaybe: MaybeRefOrGetter<VueNodeData | null>,
|
nodeDataMaybe: MaybeRefOrGetter<VueNodeData | null>,
|
||||||
@@ -29,68 +35,50 @@ export function useNodePointerInteractions(
|
|||||||
|
|
||||||
// Avoid potential null access during component initialization
|
// Avoid potential null access during component initialization
|
||||||
const nodeIdComputed = computed(() => nodeData.value?.id ?? '')
|
const nodeIdComputed = computed(() => nodeData.value?.id ?? '')
|
||||||
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeIdComputed)
|
const { startDrag, endDrag, handleDrag, isDragging } =
|
||||||
|
useNodeLayout(nodeIdComputed)
|
||||||
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||||
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
|
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
|
||||||
useCanvasInteractions()
|
useCanvasInteractions()
|
||||||
|
|
||||||
// Drag state for styling
|
const startPosition = ref<Position>({ x: 0, y: 0 })
|
||||||
const isDragging = ref(false)
|
|
||||||
const dragStyle = computed(() => {
|
const stopWatcher = watch(
|
||||||
if (nodeData.value?.flags?.pinned) {
|
isDragging,
|
||||||
return { cursor: 'default' }
|
(dragging) => {
|
||||||
}
|
layoutStore.isDraggingVueNodes.value = dragging
|
||||||
return { cursor: isDragging.value ? 'grabbing' : 'grab' }
|
},
|
||||||
})
|
{ immediate: true }
|
||||||
const startPosition = ref({ x: 0, y: 0 })
|
)
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
if (!nodeData.value) {
|
if (!nodeData.value) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'LGraphNode: nodeData is null/undefined in handlePointerDown'
|
'useNodePointerInteractions: nodeData is null in handlePointerDown'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only start drag on left-click (button 0)
|
if (event.button !== 0) return
|
||||||
if (event.button !== 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
|
||||||
if (!shouldHandleNodePointerEvents.value) {
|
if (!shouldHandleNodePointerEvents.value) {
|
||||||
forwardEventToCanvas(event)
|
forwardEventToCanvas(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow dragging if node is pinned (but still record position for selection)
|
|
||||||
startPosition.value = { x: event.clientX, y: event.clientY }
|
startPosition.value = { x: event.clientX, y: event.clientY }
|
||||||
if (nodeData.value.flags?.pinned) {
|
|
||||||
|
const dragStarted = startDrag(event)
|
||||||
|
if (!dragStarted) {
|
||||||
|
startPosition.value = { x: 0, y: 0 }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start drag using layout system
|
|
||||||
isDragging.value = true
|
|
||||||
|
|
||||||
// Set Vue node dragging state for selection toolbox
|
|
||||||
layoutStore.isDraggingVueNodes.value = true
|
|
||||||
|
|
||||||
startDrag(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
if (isDragging.value) {
|
if (!isDragging.value) return
|
||||||
void handleDrag(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
handleDrag(event)
|
||||||
* Centralized cleanup function for drag state
|
|
||||||
* Ensures consistent cleanup across all drag termination scenarios
|
|
||||||
*/
|
|
||||||
const cleanupDragState = () => {
|
|
||||||
isDragging.value = false
|
|
||||||
layoutStore.isDraggingVueNodes.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,10 +88,13 @@ export function useNodePointerInteractions(
|
|||||||
const safeDragEnd = async (event: PointerEvent): Promise<void> => {
|
const safeDragEnd = async (event: PointerEvent): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await endDrag(event)
|
await endDrag(event)
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error during endDrag:', error)
|
const errorMessage =
|
||||||
} finally {
|
error instanceof Error ? error.message : String(error)
|
||||||
cleanupDragState()
|
console.error(
|
||||||
|
'useNodePointerInteractions: Error during endDrag -',
|
||||||
|
errorMessage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +102,13 @@ export function useNodePointerInteractions(
|
|||||||
* Common drag termination handler with fallback cleanup
|
* Common drag termination handler with fallback cleanup
|
||||||
*/
|
*/
|
||||||
const handleDragTermination = (event: PointerEvent, errorContext: string) => {
|
const handleDragTermination = (event: PointerEvent, errorContext: string) => {
|
||||||
safeDragEnd(event).catch((error) => {
|
safeDragEnd(event).catch((error: unknown) => {
|
||||||
console.error(`Failed to complete ${errorContext}:`, error)
|
const errorMessage =
|
||||||
cleanupDragState() // Fallback cleanup
|
error instanceof Error ? error.message : String(error)
|
||||||
|
console.error(
|
||||||
|
`useNodePointerInteractions: Failed to complete ${errorContext} -`,
|
||||||
|
errorMessage
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,18 +117,18 @@ export function useNodePointerInteractions(
|
|||||||
handleDragTermination(event, 'drag end')
|
handleDragTermination(event, 'drag end')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
|
|
||||||
if (!shouldHandleNodePointerEvents.value) {
|
if (!shouldHandleNodePointerEvents.value) {
|
||||||
forwardEventToCanvas(event)
|
forwardEventToCanvas(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nodeData?.value) return
|
||||||
|
|
||||||
// Emit node-click for selection handling in GraphCanvas
|
// Emit node-click for selection handling in GraphCanvas
|
||||||
const dx = event.clientX - startPosition.value.x
|
const dx = event.clientX - startPosition.value.x
|
||||||
const dy = event.clientY - startPosition.value.y
|
const dy = event.clientY - startPosition.value.y
|
||||||
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
|
const wasDragging = dx * dx + dy * dy > DRAG_THRESHOLD_SQUARED
|
||||||
|
|
||||||
if (!nodeData?.value) return
|
|
||||||
onPointerUp(event, nodeData.value, wasDragging)
|
onPointerUp(event, nodeData.value, wasDragging)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +138,7 @@ export function useNodePointerInteractions(
|
|||||||
*/
|
*/
|
||||||
const handlePointerCancel = (event: PointerEvent) => {
|
const handlePointerCancel = (event: PointerEvent) => {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
handleDragTermination(event, 'drag cancellation')
|
handleDragTermination(event, 'drag cancellation')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,16 +150,8 @@ export function useNodePointerInteractions(
|
|||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// Simply cleanup state without calling endDrag to avoid synthetic event creation
|
|
||||||
cleanupDragState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount to prevent resource leaks
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (!isDragging.value) return
|
|
||||||
cleanupDragState()
|
|
||||||
})
|
|
||||||
|
|
||||||
const pointerHandlers = {
|
const pointerHandlers = {
|
||||||
onPointerdown: handlePointerDown,
|
onPointerdown: handlePointerDown,
|
||||||
onPointermove: handlePointerMove,
|
onPointermove: handlePointerMove,
|
||||||
@@ -174,7 +162,7 @@ export function useNodePointerInteractions(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isDragging,
|
isDragging,
|
||||||
dragStyle,
|
pointerHandlers,
|
||||||
pointerHandlers
|
stopWatcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,10 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start dragging the node
|
* Start dragging the node
|
||||||
|
* @returns {boolean} True if drag started successfully, false otherwise
|
||||||
*/
|
*/
|
||||||
function startDrag(event: PointerEvent) {
|
function startDrag(event: PointerEvent): boolean {
|
||||||
if (!layoutRef.value || !transformState) return
|
if (!layoutRef.value || !transformState) return false
|
||||||
|
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
dragStartPos = { ...position.value }
|
dragStartPos = { ...position.value }
|
||||||
@@ -88,8 +89,10 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
|
|||||||
mutations.setSource(LayoutSource.Vue)
|
mutations.setSource(LayoutSource.Vue)
|
||||||
|
|
||||||
// Capture pointer
|
// Capture pointer
|
||||||
if (!(event.target instanceof HTMLElement)) return
|
if (!(event.target instanceof HTMLElement)) return false
|
||||||
event.target.setPointerCapture(event.pointerId)
|
event.target.setPointerCapture(event.pointerId)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,14 +49,47 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
|||||||
|
|
||||||
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
|
vi.mock('@/renderer/extensions/vueNodes/layout/useNodeLayout', () => ({
|
||||||
useNodeLayout: () => ({
|
useNodeLayout: () => ({
|
||||||
position: { x: 100, y: 50 },
|
layoutRef: computed(() => null),
|
||||||
size: { width: 200, height: 100 },
|
position: computed(() => ({ x: 100, y: 50 })),
|
||||||
|
size: computed(() => ({ width: 200, height: 100 })),
|
||||||
|
bounds: computed(() => ({ x: 100, y: 50, width: 200, height: 100 })),
|
||||||
|
isVisible: computed(() => true),
|
||||||
|
zIndex: computed(() => 0),
|
||||||
|
moveTo: vi.fn(),
|
||||||
|
resize: vi.fn(),
|
||||||
startDrag: vi.fn(),
|
startDrag: vi.fn(),
|
||||||
handleDrag: vi.fn(),
|
handleDrag: vi.fn(),
|
||||||
endDrag: vi.fn()
|
endDrag: vi.fn(),
|
||||||
|
isDragging: computed(() => false),
|
||||||
|
nodeStyle: computed(() => ({ cursor: 'grab' }))
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock(
|
||||||
|
'@/renderer/extensions/vueNodes/composables/useNodePointerInteractions',
|
||||||
|
() => ({
|
||||||
|
useNodePointerInteractions: (nodeDataMaybe: any, onPointerUp: any) => ({
|
||||||
|
isDragging: computed(() => false),
|
||||||
|
pointerHandlers: {
|
||||||
|
onPointerdown: vi.fn(),
|
||||||
|
onPointermove: vi.fn(),
|
||||||
|
onPointerup: (event: PointerEvent) => {
|
||||||
|
const nodeData =
|
||||||
|
typeof nodeDataMaybe === 'function'
|
||||||
|
? nodeDataMaybe()
|
||||||
|
: nodeDataMaybe
|
||||||
|
if (nodeData && onPointerUp) {
|
||||||
|
onPointerUp(event, nodeData, false) // false = wasDragging
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointercancel: vi.fn(),
|
||||||
|
onContextmenu: vi.fn()
|
||||||
|
},
|
||||||
|
stopWatcher: vi.fn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
vi.mock(
|
vi.mock(
|
||||||
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
|
'@/renderer/extensions/vueNodes/execution/useNodeExecutionState',
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user