Skip to content
Closed
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
79 changes: 79 additions & 0 deletions browser_tests/assets/vueNodes/collapsed-stacking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"id": "407522cb-b7cb-491d-b152-3c2de19dc51d",
"revision": 0,
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "VAEDecode",
"pos": [140, 100],
"size": [193.25, 107],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "samples",
"name": "samples",
"type": "LATENT",
"link": null
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": { "Node name for S&R": "VAEDecode" }
},
{
"id": 2,
"type": "CLIPTextEncode",
"pos": [100, 100],
"size": [240, 155],
"flags": { "collapsed": true },
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "clip",
"name": "clip",
"type": "CLIP",
"link": null
},
{
"localized_name": "text",
"name": "text",
"type": "STRING",
"widget": { "name": "text" },
"link": null
}
],
"outputs": [
{
"localized_name": "CONDITIONING",
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": null
}
],
"properties": { "Node name for S&R": "CLIPTextEncode" },
"widgets_values": [""]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
14 changes: 14 additions & 0 deletions browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ export class VueNodeHelpers {
})
}

async getNodeCenter(title: string): Promise<{ x: number; y: number }> {
const box = await this.getNodeByTitle(title).boundingBox()
if (!box) throw new Error(`Node "${title}" not found`)
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
}

async getNodePaintOrder(title: string): Promise<number> {
return await this.getNodeByTitle(title).evaluate((node) =>
node.parentElement
? Array.from(node.parentElement.children).indexOf(node)
: -1
)
}

/**
* Get total count of Vue nodes in the DOM
*/
Expand Down
71 changes: 27 additions & 44 deletions browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'

test.describe(
Expand All @@ -15,37 +14,12 @@ test.describe(
await fitToViewInstant(comfyPage)
})

/**
* Helper to get the z-index of a node by its title
*/
async function getNodeZIndex(
comfyPage: ComfyPage,
title: string
): Promise<number> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const style = await node.getAttribute('style')
const match = style?.match(/z-index:\s*(\d+)/)
return match ? parseInt(match[1], 10) : Number.NaN
}

/**
* Helper to get the bounding box center of a node
*/
async function getNodeCenter(
comfyPage: ComfyPage,
title: string
): Promise<{ x: number; y: number }> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const box = await node.boundingBox()
if (!box) throw new Error(`Node "${title}" not found`)
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
}

test('should bring overlapped node to front when clicking on it', async ({
comfyPage
}) => {
// Get initial positions
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
const clipCenter =
await comfyPage.vueNodes.getNodeCenter('CLIP Text Encode')
const ksamplerHeader = await comfyPage.page
.getByText('KSampler')
.boundingBox()
Expand All @@ -63,12 +37,14 @@ test.describe(
'bring-to-front-overlapped-before.png'
)

// KSampler should be on top (higher z-index) after being dragged
// KSampler should be on top after being dragged
await expect
.poll(async () => {
const ksamplerZ = await getNodeZIndex(comfyPage, 'KSampler')
const clipZ = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
return ksamplerZ - clipZ
const ksamplerOrder =
await comfyPage.vueNodes.getNodePaintOrder('KSampler')
const clipOrder =
await comfyPage.vueNodes.getNodePaintOrder('CLIP Text Encode')
return ksamplerOrder - clipOrder
})
.toBeGreaterThan(0)

Expand All @@ -82,12 +58,14 @@ test.describe(
await comfyPage.page.mouse.click(clipBox.x + 30, clipBox.y + 10)
await comfyPage.nextFrame()

// CLIP should now be on top - compare post-action z-indices
// CLIP should now be on top
await expect
.poll(async () => {
const clipZ = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const ksamplerZ = await getNodeZIndex(comfyPage, 'KSampler')
return clipZ - ksamplerZ
const clipOrder =
await comfyPage.vueNodes.getNodePaintOrder('CLIP Text Encode')
const ksamplerOrder =
await comfyPage.vueNodes.getNodePaintOrder('KSampler')
return clipOrder - ksamplerOrder
})
.toBeGreaterThan(0)

Expand All @@ -101,7 +79,8 @@ test.describe(
comfyPage
}) => {
// Get CLIP Text Encode position (it has a text widget)
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
const clipCenter =
await comfyPage.vueNodes.getNodeCenter('CLIP Text Encode')

// Get VAE Decode position and drag it on top of CLIP
const vaeHeader = await comfyPage.page
Expand All @@ -118,9 +97,11 @@ test.describe(
// VAE should be on top after drag
await expect
.poll(async () => {
const vaeZ = await getNodeZIndex(comfyPage, 'VAE Decode')
const clipZ = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
return vaeZ - clipZ
const vaeOrder =
await comfyPage.vueNodes.getNodePaintOrder('VAE Decode')
const clipOrder =
await comfyPage.vueNodes.getNodePaintOrder('CLIP Text Encode')
return vaeOrder - clipOrder
})
.toBeGreaterThan(0)

Expand All @@ -136,12 +117,14 @@ test.describe(
await comfyPage.page.mouse.click(clipBox.x + 170, clipBox.y + 80)
await comfyPage.nextFrame()

// CLIP should now be on top - compare post-action z-indices
// CLIP should now be on top
await expect
.poll(async () => {
const clipZ = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const vaeZ = await getNodeZIndex(comfyPage, 'VAE Decode')
return clipZ - vaeZ
const clipOrder =
await comfyPage.vueNodes.getNodePaintOrder('CLIP Text Encode')
const vaeOrder =
await comfyPage.vueNodes.getNodePaintOrder('VAE Decode')
return clipOrder - vaeOrder
})
.toBeGreaterThan(0)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
import { toNodeId } from '@/types/nodeId'

test.describe('Collapsed Vue node stacking', { tag: '@vue-nodes' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.workflow.loadWorkflow('vueNodes/collapsed-stacking')
await fitToViewInstant(comfyPage)
})

test('updates pointer hit order for selection and collapse state', async ({
comfyPage
}) => {
const collapsed =
await comfyPage.vueNodes.getFixtureByTitle('CLIP Text Encode')
const expanded = await comfyPage.vueNodes.getFixtureByTitle('VAE Decode')
const collapsedId = toNodeId(2)
const expandedId = toNodeId(1)
const collapsedBox = await collapsed.boundingBox()
const expandedBox = await expanded.boundingBox()
if (!collapsedBox || !expandedBox) throw new Error('Nodes are not visible')

const overlap = {
x: Math.max(collapsedBox.x, expandedBox.x) + 20,
y: Math.max(collapsedBox.y, expandedBox.y) + 12
}
const exposedCollapsed = {
x: collapsedBox.x + 10,
y: collapsedBox.y + 12
}
await comfyPage.page.mouse.click(overlap.x, overlap.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([expandedId])

await expanded.header.click({ modifiers: ['Control'] })
await expect.poll(() => comfyPage.nodeOps.getSelectedNodeIds()).toEqual([])
await comfyPage.page.mouse.click(exposedCollapsed.x, exposedCollapsed.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([collapsedId])

await comfyPage.page.mouse.click(overlap.x, overlap.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([collapsedId])

await collapsed.header.click({ modifiers: ['Control'] })
await expect.poll(() => comfyPage.nodeOps.getSelectedNodeIds()).toEqual([])
await comfyPage.page.mouse.click(overlap.x, overlap.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([expandedId])

await expanded.header.click({ modifiers: ['Control'] })
await expect.poll(() => comfyPage.nodeOps.getSelectedNodeIds()).toEqual([])
await comfyPage.page.mouse.click(exposedCollapsed.x, exposedCollapsed.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([collapsedId])
await comfyPage.command.executeCommand(
'Comfy.Canvas.ToggleSelectedNodes.Collapse'
)
await expect(collapsed.root).not.toHaveAttribute('data-collapsed', 'true')
await comfyPage.page.mouse.click(overlap.x, overlap.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([collapsedId])

await comfyPage.command.executeCommand(
'Comfy.Canvas.ToggleSelectedNodes.Collapse'
)
await expect(collapsed.root).toHaveAttribute('data-collapsed', 'true')
await collapsed.header.click({ modifiers: ['Control'] })
await expect.poll(() => comfyPage.nodeOps.getSelectedNodeIds()).toEqual([])
await comfyPage.page.mouse.click(overlap.x, overlap.y)
await expect
.poll(() => comfyPage.nodeOps.getSelectedNodeIds())
.toEqual([expandedId])
})
})
32 changes: 30 additions & 2 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
>
<!-- Vue nodes rendered based on graph nodes -->
<LGraphNode
v-for="nodeData in allNodes"
v-for="nodeData in displayedNodes"
:key="nodeData.id"
:node-data="nodeData"
:error="
Expand Down Expand Up @@ -178,16 +178,19 @@ import { useWorkflowPersistenceV2 as useWorkflowPersistence } from '@/platform/w
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { NodeLayout } from '@/renderer/core/layout/types'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { requestSlotLayoutSyncForAllNodes } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { orderNodesForPainting } from '@/renderer/extensions/vueNodes/utils/nodePaintOrder'
import { UnauthorizedError } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useNewUserService } from '@/services/useNewUserService'
import type { NodeId } from '@/types/nodeId'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
import { storeToRefs } from 'pinia'

Expand Down Expand Up @@ -220,7 +223,7 @@ const workspaceStore = useWorkspaceStore()
const { isBuilderMode } = useAppMode()
const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore()
const { linearMode } = storeToRefs(canvasStore)
const { linearMode, selectedNodeIds } = storeToRefs(canvasStore)
const executionStore = useExecutionStore()
const executionErrorStore = useExecutionErrorStore()
const toastStore = useToastStore()
Expand Down Expand Up @@ -293,6 +296,30 @@ watch(
const allNodes = computed((): VueNodeData[] =>
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
)
const nodeLayouts = shallowRef<ReadonlyMap<NodeId, NodeLayout>>(new Map())

function updateNodeLayouts() {
nodeLayouts.value = layoutStore.getAllNodes().value
}

watch(allNodes, updateNodeLayouts, { immediate: true })
const stopNodeLayoutWatcher = layoutStore.onChange((change) => {
if (
change.operation.type !== 'setNodeZIndex' &&
change.operation.type !== 'createNode' &&
change.operation.type !== 'deleteNode'
)
return

updateNodeLayouts()
})
const displayedNodes = computed(() =>
orderNodesForPainting(
allNodes.value,
nodeLayouts.value,
selectedNodeIds.value
)
)
watch(
() => linearMode.value,
(isLinearMode) => {
Expand Down Expand Up @@ -585,6 +612,7 @@ onMounted(async () => {
})

onUnmounted(() => {
stopNodeLayoutWatcher()
cleanupErrorHooks?.()
cleanupErrorHooks = null
vueNodeLifecycle.cleanup()
Expand Down
Loading
Loading