diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png index a8fed2ac0a..55b5f11c7a 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png index f4bd4d3f93..d19acb13fb 100644 Binary files a/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png and b/browser_tests/tests/nodeSearchBox.spec.ts-snapshots/added-node-no-connection-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index 8a0fb4f928..38543c496e 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts new file mode 100644 index 0000000000..6e20f9e367 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts @@ -0,0 +1,144 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' +import type { ComfyPage } from '../../../../fixtures/ComfyPage' +import { fitToViewInstant } from '../../../../helpers/fitToView' + +test.describe('Vue Node Bring to Front', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await fitToViewInstant(comfyPage) + }) + + /** + * Helper to get the z-index of a node by its title + */ + async function getNodeZIndex( + comfyPage: ComfyPage, + title: string + ): Promise { + const node = comfyPage.vueNodes.getNodeByTitle(title) + const style = await node.getAttribute('style') + if (!style) { + throw new Error( + `Node "${title}" has no style attribute (observed: ${style})` + ) + } + const match = style.match(/z-index:\s*(\d+)/) + if (!match) { + throw new Error( + `Node "${title}" has no z-index in style (observed: "${style}")` + ) + } + return parseInt(match[1], 10) + } + + /** + * 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 ksamplerHeader = await comfyPage.page + .getByText('KSampler') + .boundingBox() + if (!ksamplerHeader) throw new Error('KSampler header not found') + + // Drag KSampler on top of CLIP Text Encode + await comfyPage.dragAndDrop( + { x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 }, + clipCenter + ) + await comfyPage.nextFrame() + + // Screenshot showing KSampler on top of CLIP + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-overlapped-before.png' + ) + + // KSampler should be on top (higher z-index) after being dragged + const ksamplerZIndexBefore = await getNodeZIndex(comfyPage, 'KSampler') + const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + expect(ksamplerZIndexBefore).toBeGreaterThan(clipZIndexBefore) + + // Click on CLIP Text Encode (underneath) - need to click on a visible part + // Since KSampler is on top, we click on the edge of CLIP that should still be visible + const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode') + const clipBox = await clipNode.boundingBox() + if (!clipBox) throw new Error('CLIP node not found') + + // Click on a visible edge of CLIP + 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 + const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + const ksamplerZIndexAfter = await getNodeZIndex(comfyPage, 'KSampler') + expect(clipZIndexAfter).toBeGreaterThan(ksamplerZIndexAfter) + + // Screenshot showing CLIP now on top + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-overlapped-after.png' + ) + }) + + test('should bring overlapped node to front when clicking on its widget', async ({ + comfyPage + }) => { + // Get CLIP Text Encode position (it has a text widget) + const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode') + + // Get VAE Decode position and drag it on top of CLIP + const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox() + if (!vaeHeader) throw new Error('VAE Decode header not found') + + await comfyPage.dragAndDrop( + { x: vaeHeader.x + 50, y: vaeHeader.y + 10 }, + { x: clipCenter.x - 50, y: clipCenter.y } + ) + await comfyPage.nextFrame() + + // VAE should be on top after drag + const vaeZIndexBefore = await getNodeZIndex(comfyPage, 'VAE Decode') + const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + expect(vaeZIndexBefore).toBeGreaterThan(clipZIndexBefore) + + // Screenshot showing VAE on top + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-widget-overlapped-before.png' + ) + + // Click on the text widget of CLIP Text Encode + const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode') + const clipBox = await clipNode.boundingBox() + if (!clipBox) throw new Error('CLIP node not found') + 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 + const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + const vaeZIndexAfter = await getNodeZIndex(comfyPage, 'VAE Decode') + expect(clipZIndexAfter).toBeGreaterThan(vaeZIndexAfter) + + // Screenshot showing CLIP now on top after widget click + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-widget-overlapped-after.png' + ) + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-darwin.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-darwin.png new file mode 100644 index 0000000000..85e3ce697d Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-darwin.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-darwin.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-darwin.png new file mode 100644 index 0000000000..3869d7f80d Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-darwin.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-darwin.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-darwin.png new file mode 100644 index 0000000000..7716b53d53 Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-darwin.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-darwin.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-darwin.png new file mode 100644 index 0000000000..a1263564c7 Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-darwin.png differ diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 5fa1e378b6..9133145659 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -15,6 +15,7 @@ :style="{ 'grid-template-rows': gridTemplateRows }" + @pointerdown.capture="handleBringToFront" @pointerdown="handleWidgetPointerEvent" @pointermove="handleWidgetPointerEvent" @pointerup="handleWidgetPointerEvent" @@ -78,6 +79,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling' import { st } from '@/i18n' import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' +import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue' // Import widget components directly import WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue' @@ -99,12 +101,20 @@ const { nodeData } = defineProps() const { shouldHandleNodePointerEvents, forwardEventToCanvas } = useCanvasInteractions() +const { bringNodeToFront } = useNodeZIndex() + function handleWidgetPointerEvent(event: PointerEvent) { if (shouldHandleNodePointerEvents.value) return event.stopPropagation() forwardEventToCanvas(event) } +function handleBringToFront() { + if (nodeData?.id != null) { + bringNodeToFront(String(nodeData.id)) + } +} + // Error boundary implementation const renderError = ref(null) diff --git a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts index d79f5af177..abed566ec1 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts @@ -134,6 +134,10 @@ function useNodeEventHandlersIndividual() { canvasStore.canvas.deselectAll() canvasStore.canvas.select(node) canvasStore.updateSelectedItems() + // Bring node to front when selected (unless pinned) + if (!node.flags?.pinned) { + bringNodeToFront(nodeId) + } return } @@ -141,6 +145,10 @@ function useNodeEventHandlersIndividual() { canvasStore.canvas.deselect(node) } else { canvasStore.canvas.select(node) + // Bring node to front when selected (unless pinned) + if (!node.flags?.pinned) { + bringNodeToFront(nodeId) + } } canvasStore.updateSelectedItems()