Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions browser_tests/tests/vueNodes/nodeStates/pin.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
10 changes: 10 additions & 0 deletions src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface VueNodeData {
hasErrors?: boolean
flags?: {
collapsed?: boolean
pinned?: boolean
}
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supremely happy that this is no longer necessary~

}

unpin(): void {
Expand Down
1 change: 1 addition & 0 deletions src/lib/litegraph/src/LGraphNodeProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { LGraphNode } from './LGraphNode'
const DEFAULT_TRACKED_PROPERTIES: string[] = [
'title',
'flags.collapsed',
'flags.pinned',
'mode'
]

Expand Down
3 changes: 1 addition & 2 deletions src/renderer/core/canvas/useCanvasInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
</template>
<!-- Header only updates on title/color changes -->
<NodeHeader
v-memo="[nodeData.title, isCollapsed]"
v-memo="[nodeData.title, isCollapsed, nodeData.flags?.pinned]"
:node-data="nodeData"
:readonly="readonly"
:collapsed="isCollapsed"
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/extensions/vueNodes/components/NodeHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<!-- Node Title -->
<div
v-tooltip.top="tooltipConfig"
class="text-sm font-bold truncate flex-1 lod-toggle"
class="text-sm font-bold truncate flex-1 lod-toggle flex items-center gap-2"
data-testid="node-title"
>
<EditableText
Expand All @@ -36,6 +36,11 @@
@edit="handleTitleEdit"
@cancel="handleTitleCancel"
/>
<i-lucide:pin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use the class based icon, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better? Or just for consistency?

v-if="isPinned"
class="w-5 h-5 text-stone-200 dark-theme:text-slate-300"
data-testid="node-pin-indicator"
/>
</div>
<LODFallback />
</div>
Expand Down Expand Up @@ -141,6 +146,8 @@ watch(
}
)

const isPinned = computed(() => Boolean(nodeData?.flags?.pinned))

// Subgraph detection
const isSubgraphNode = computed(() => {
if (!nodeData?.id) return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -60,14 +63,19 @@ 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

// Set Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = true

startDrag(event)
startPosition.value = { x: event.clientX, y: event.clientY }
}

const handlePointerMove = (event: PointerEvent) => {
Expand Down