mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-18 20:39:28 +00:00
Compare commits
6 Commits
uy/node-se
...
austin/nod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ac026fc8 | ||
|
|
0988cb1d87 | ||
|
|
00eade5e18 | ||
|
|
f58f0ce2d3 | ||
|
|
a4749501b1 | ||
|
|
5ed7948745 |
@@ -1270,3 +1270,38 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('Spacebar pan', { tag: '@vue-nodes' }, async ({ comfyPage }) => {
|
||||
const initialOffset = await comfyPage.canvasOps.getOffset()
|
||||
|
||||
await test.step('Setup link drag', async () => {
|
||||
await comfyPage.searchBoxV2.addNode('Load Diffusion')
|
||||
const loadNode = await comfyPage.vueNodes.getFixtureByTitle('Load Diff')
|
||||
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
|
||||
await loadNode.getSlot('MODEL').hover()
|
||||
await comfyPage.page.mouse.down()
|
||||
await ksampler.getSlot('model').hover()
|
||||
expect(await comfyPage.canvasOps.getOffset()).toEqual(initialOffset)
|
||||
})
|
||||
|
||||
await test.step('Holding space initiates a pan', async () => {
|
||||
await comfyPage.page.keyboard.down(' ')
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.page.keyboard.up(' ')
|
||||
await expect
|
||||
.poll(() => comfyPage.canvasOps.getOffset())
|
||||
.not.toEqual(initialOffset)
|
||||
})
|
||||
|
||||
await test.step('Mouse remains over model after pan', async () => {
|
||||
await comfyPage.page.mouse.up()
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(
|
||||
() => graph?.nodes?.at(-1)?.outputs?.[0]?.links?.length === 1
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
@pointerdown.capture="forwardPointerDownPanEvent"
|
||||
@pointerup.capture="forwardPointerUpPanEvent"
|
||||
@pointermove.capture="forwardPointerMovePanEvent"
|
||||
@keydown.space="canvasInteractions.forwardEventToCanvas"
|
||||
>
|
||||
<!-- Vue nodes rendered based on graph nodes -->
|
||||
<LGraphNode
|
||||
|
||||
@@ -410,8 +410,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
set read_only(value: boolean) {
|
||||
const changed = this.state.readOnly !== value
|
||||
this.state.readOnly = value
|
||||
this._updateCursorStyle()
|
||||
if (changed) {
|
||||
this.dispatchEvent('litegraph:read-only-changed', { readOnly: value })
|
||||
}
|
||||
}
|
||||
|
||||
get isDragging(): boolean {
|
||||
@@ -3972,7 +3976,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (this._previously_dragging_canvas === null) {
|
||||
this._previously_dragging_canvas = this.dragging_canvas
|
||||
}
|
||||
this.dragging_canvas = this.pointer.isDown
|
||||
this.dragging_canvas =
|
||||
this.pointer.isDown || !!this.linkConnector.renderLinks.length
|
||||
block_default = true
|
||||
} else if (e.key === 'Escape') {
|
||||
// esc
|
||||
|
||||
@@ -59,4 +59,9 @@ export interface LGraphCanvasEventMap {
|
||||
active: boolean
|
||||
nodeId: NodeId
|
||||
}
|
||||
|
||||
/** The canvas read-only state has changed. */
|
||||
'litegraph:read-only-changed': {
|
||||
readOnly: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
@@ -36,6 +38,30 @@ vi.mock('@/scripts/app', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as Record<string, unknown>),
|
||||
useEventListener: vi.fn(
|
||||
(
|
||||
target: EventTarget,
|
||||
event: string,
|
||||
handler: EventListenerOrEventListenerObject
|
||||
) => {
|
||||
target.addEventListener(event, handler)
|
||||
return () => target.removeEventListener(event, handler)
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function createMockCanvas(readOnly = false): LGraphCanvas {
|
||||
return {
|
||||
read_only: readOnly,
|
||||
canvas: document.createElement('canvas')
|
||||
} as unknown as LGraphCanvas
|
||||
}
|
||||
|
||||
describe('useCanvasStore', () => {
|
||||
let store: ReturnType<typeof useCanvasStore>
|
||||
|
||||
@@ -90,4 +116,42 @@ describe('useCanvasStore', () => {
|
||||
|
||||
expect(store.selectedNodeIds).toHaveLength(0)
|
||||
})
|
||||
|
||||
describe('isReadOnly', () => {
|
||||
it('syncs initial read_only value when canvas is set', async () => {
|
||||
const mockCanvas = createMockCanvas(true)
|
||||
|
||||
store.canvas = mockCanvas as unknown as LGraphCanvas
|
||||
await nextTick()
|
||||
|
||||
expect(store.isReadOnly).toBe(true)
|
||||
})
|
||||
|
||||
it('updates isReadOnly when litegraph:read-only-changed event fires', async () => {
|
||||
const mockCanvas = createMockCanvas(false)
|
||||
|
||||
store.canvas = mockCanvas as unknown as LGraphCanvas
|
||||
await nextTick()
|
||||
|
||||
expect(store.isReadOnly).toBe(false)
|
||||
|
||||
// Simulate space key press → LGraphCanvas sets read_only = true
|
||||
mockCanvas.canvas.dispatchEvent(
|
||||
new CustomEvent('litegraph:read-only-changed', {
|
||||
detail: { readOnly: true }
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.isReadOnly).toBe(true)
|
||||
|
||||
// Simulate space key release
|
||||
mockCanvas.canvas.dispatchEvent(
|
||||
new CustomEvent('litegraph:read-only-changed', {
|
||||
detail: { readOnly: false }
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.isReadOnly).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,6 +56,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
setMode(val ? 'app' : 'graph')
|
||||
}
|
||||
})
|
||||
const isReadOnly = ref(false)
|
||||
|
||||
// Set up scale synchronization when canvas is available
|
||||
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
|
||||
@@ -131,6 +132,16 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
whenever(
|
||||
() => canvas.value,
|
||||
(newCanvas) => {
|
||||
isReadOnly.value = newCanvas.read_only
|
||||
|
||||
useEventListener(
|
||||
newCanvas.canvas,
|
||||
'litegraph:read-only-changed',
|
||||
(event: CustomEvent<{ readOnly: boolean }>) => {
|
||||
isReadOnly.value = event.detail.readOnly
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(
|
||||
newCanvas.canvas,
|
||||
'litegraph:set-graph',
|
||||
@@ -176,6 +187,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
rerouteSelected,
|
||||
appScalePercentage,
|
||||
linearMode,
|
||||
isReadOnly,
|
||||
updateSelectedItems,
|
||||
getCanvas,
|
||||
setAppZoomFromPercentage,
|
||||
|
||||
@@ -13,7 +13,8 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => {
|
||||
return {
|
||||
useCanvasStore: vi.fn(() => ({
|
||||
getCanvas,
|
||||
setCursorStyle
|
||||
setCursorStyle,
|
||||
isReadOnly: false
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,9 +27,7 @@ export function useCanvasInteractions() {
|
||||
* Whether Vue node components should handle pointer events.
|
||||
* Returns false when canvas is in read-only/panning mode (e.g., space key held for panning).
|
||||
*/
|
||||
const shouldHandleNodePointerEvents = computed(
|
||||
() => !(canvasStore.canvas?.read_only ?? false)
|
||||
)
|
||||
const shouldHandleNodePointerEvents = computed(() => !canvasStore.isReadOnly)
|
||||
|
||||
/**
|
||||
* Returns true if the wheel event target is inside an element that should
|
||||
|
||||
@@ -52,6 +52,10 @@ vi.mock('@/renderer/core/canvas/useAutoPan', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({ isReadOnly: false })
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
resolveNodeSurfaceSlotCandidate,
|
||||
resolveSlotTargetCandidate
|
||||
} from '@/renderer/core/canvas/links/linkDropOrchestrator'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
||||
import type { SlotDropCandidate } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
@@ -122,6 +123,7 @@ export function useSlotLinkInteraction({
|
||||
setCompatibleForKey,
|
||||
clearCompatible
|
||||
} = useSlotLinkDragUIState()
|
||||
const canvasStore = useCanvasStore()
|
||||
const conversion = useSharedCanvasPositionConversion()
|
||||
const pointerSession = createPointerSession()
|
||||
let activeAdapter: LinkConnectorAdapter | null = null
|
||||
@@ -414,9 +416,11 @@ export function useSlotLinkInteraction({
|
||||
const canvas = app.canvas
|
||||
const node = canvas.graph?.getNodeById(nodeId)
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (!pointerSession.matches(event)) return
|
||||
if (!pointerSession.matches(event) || canvasStore.isReadOnly) return
|
||||
|
||||
event.stopPropagation()
|
||||
|
||||
app.canvas.last_mouse = [event.clientX, event.clientY]
|
||||
autoPan?.updatePointer(event.clientX, event.clientY)
|
||||
|
||||
if (canvas.subgraph && node) {
|
||||
|
||||
@@ -199,6 +199,19 @@ function useNodeDragIndividual() {
|
||||
if (!dragStartPos || !dragStartMouse) {
|
||||
return
|
||||
}
|
||||
if (canvasStore.isReadOnly) {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const delta = [event.clientX - lastPointerX, event.clientY - lastPointerY]
|
||||
|
||||
canvas.ds.offset[0] += delta[0] / canvas.ds.scale
|
||||
canvas.ds.offset[1] += delta[1] / canvas.ds.scale
|
||||
canvas.setDirty(true, true)
|
||||
lastPointerX = event.clientX
|
||||
lastPointerY = event.clientY
|
||||
dragStartMouse.x += delta[0]
|
||||
dragStartMouse.y += delta[1]
|
||||
return
|
||||
}
|
||||
|
||||
// Throttle position updates using requestAnimationFrame for better performance
|
||||
if (rafId !== null) return // Skip if frame already scheduled
|
||||
|
||||
@@ -193,14 +193,13 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
|
||||
let unwatchReadOnly: (() => void) | undefined
|
||||
function enforceReadOnly(inSelect: boolean) {
|
||||
const { state } = getCanvas()
|
||||
if (!state) return
|
||||
state.readOnly = inSelect
|
||||
const canvas = getCanvas()
|
||||
canvas.read_only = inSelect
|
||||
unwatchReadOnly?.()
|
||||
if (inSelect)
|
||||
unwatchReadOnly = watch(
|
||||
() => state.readOnly,
|
||||
() => (state.readOnly = true)
|
||||
() => canvas.read_only,
|
||||
() => (canvas.read_only = true)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user