diff --git a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts new file mode 100644 index 000000000..27f1ad1ac --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts @@ -0,0 +1,85 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const PIN_HOTKEY = 'p' +const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' + +test.describe('Vue Node Pin', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling pin on a selected node with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const pinIndicator = checkpointNode.locator(PIN_INDICATOR) + + await expect(pinIndicator).toBeVisible() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator).not.toBeVisible() + }) + + test('should allow toggling pin on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) + + const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint') + const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler') + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + const pinIndicator1 = checkpointNode.locator(PIN_INDICATOR) + await expect(pinIndicator1).toBeVisible() + const pinIndicator2 = ksamplerNode.locator(PIN_INDICATOR) + await expect(pinIndicator2).toBeVisible() + + await comfyPage.page.keyboard.press(PIN_HOTKEY) + await expect(pinIndicator1).not.toBeVisible() + await expect(pinIndicator2).not.toBeVisible() + }) + + test('should not allow dragging pinned nodes', async ({ comfyPage }) => { + const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint') + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node + const headerPos = await checkpointNodeHeader.boundingBox() + if (!headerPos) throw new Error('Failed to get header position') + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is not dragged (same position before and after click-and-drag) + const headerPosAfterDrag = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag).toEqual(headerPos) + + // Unpin the node with the hotkey + await checkpointNodeHeader.click() + await comfyPage.page.keyboard.press(PIN_HOTKEY) + + // Try to drag the node again + await comfyPage.dragAndDrop( + { x: headerPos.x, y: headerPos.y }, + { x: headerPos.x + 256, y: headerPos.y + 256 } + ) + + // Verify the node is dragged + const headerPosAfterDrag2 = await checkpointNodeHeader.boundingBox() + if (!headerPosAfterDrag2) + throw new Error('Failed to get header position after drag') + expect(headerPosAfterDrag2).not.toEqual(headerPos) + }) +}) diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 701e14891..beaca8d4c 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -39,6 +39,7 @@ export interface VueNodeData { hasErrors?: boolean flags?: { collapsed?: boolean + pinned?: boolean } } @@ -434,6 +435,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { } }) break + case 'flags.pinned': + vueNodeData.set(nodeId, { + ...currentData, + flags: { + ...currentData.flags, + pinned: Boolean(event.newValue) + } + }) + break case 'mode': vueNodeData.set(nodeId, { ...currentData, diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 20e26e211..a1eeb8ae2 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -3398,9 +3398,7 @@ export class LGraphNode this.graph._version++ this.flags.pinned = v ?? !this.flags.pinned this.resizable = !this.pinned - // Delete the flag if unpinned, so that we don't get unnecessary - // flags.pinned = false in serialized object. - if (!this.pinned) delete this.flags.pinned + if (!this.pinned) this.flags.pinned = undefined } unpin(): void { diff --git a/src/lib/litegraph/src/LGraphNodeProperties.ts b/src/lib/litegraph/src/LGraphNodeProperties.ts index 41bb1f673..271d98e1e 100644 --- a/src/lib/litegraph/src/LGraphNodeProperties.ts +++ b/src/lib/litegraph/src/LGraphNodeProperties.ts @@ -6,6 +6,7 @@ import type { LGraphNode } from './LGraphNode' const DEFAULT_TRACKED_PROPERTIES: string[] = [ 'title', 'flags.collapsed', + 'flags.pinned', 'mode' ] diff --git a/src/renderer/core/canvas/useCanvasInteractions.ts b/src/renderer/core/canvas/useCanvasInteractions.ts index cddc50d08..a32d5e864 100644 --- a/src/renderer/core/canvas/useCanvasInteractions.ts +++ b/src/renderer/core/canvas/useCanvasInteractions.ts @@ -6,8 +6,7 @@ import { app } from '@/scripts/app' /** * Composable for handling canvas interactions from Vue components. - * This provides a unified way to forward events to the LiteGraph canvas - * and will be the foundation for migrating canvas interactions to Vue. + * This provides a unified way to forward events to the LiteGraph canvas. */ export function useCanvasInteractions() { const settingStore = useSettingStore() diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 9e4ecefd6..a86d8de3a 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -49,7 +49,7 @@
+
@@ -141,6 +146,8 @@ watch( } ) +const isPinned = computed(() => Boolean(nodeData?.flags?.pinned)) + // Subgraph detection const isSubgraphNode = computed(() => { if (!nodeData?.id) return false diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts index ce63dcadd..e00e77c24 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -36,9 +36,12 @@ export function useNodePointerInteractions( // Drag state for styling const isDragging = ref(false) - const dragStyle = computed(() => ({ - cursor: isDragging.value ? 'grabbing' : 'grab' - })) + const dragStyle = computed(() => { + if (nodeData.value?.flags?.pinned) { + return { cursor: 'default' } + } + return { cursor: isDragging.value ? 'grabbing' : 'grab' } + }) const startPosition = ref({ x: 0, y: 0 }) const handlePointerDown = (event: PointerEvent) => { @@ -60,6 +63,12 @@ export function useNodePointerInteractions( return } + // Don't allow dragging if node is pinned (but still record position for selection) + startPosition.value = { x: event.clientX, y: event.clientY } + if (nodeData.value.flags?.pinned) { + return + } + // Start drag using layout system isDragging.value = true @@ -67,7 +76,6 @@ export function useNodePointerInteractions( layoutStore.isDraggingVueNodes.value = true startDrag(event) - startPosition.value = { x: event.clientX, y: event.clientY } } const handlePointerMove = (event: PointerEvent) => {