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 fa23bba3ba..4db4171f41 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/maskEditor.spec.ts b/browser_tests/tests/vueNodes/maskEditor.spec.ts new file mode 100644 index 0000000000..684098c8d8 --- /dev/null +++ b/browser_tests/tests/vueNodes/maskEditor.spec.ts @@ -0,0 +1,52 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.describe('Vue Nodes Mask Editor', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + }) + + test('opens mask editor from toolbox and image overlay buttons', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.vueNodes.waitForNodes() + await comfyPage.zoom(100, 15) + + const imagePreview = comfyPage.page.locator('.image-preview img') + await expect(imagePreview).toBeVisible() + + const maskEditorDialog = comfyPage.page.locator('.maskEditor-dialog-root') + + // Test 1: Open from toolbox button + await comfyPage.selectNodes(['Load Image']) + await expect(comfyPage.selectionToolbox).toBeVisible() + + const toolboxMaskButton = comfyPage.selectionToolbox.getByRole('button', { + name: /mask editor/i + }) + await expect(toolboxMaskButton).toBeVisible() + await toolboxMaskButton.click() + + await expect(maskEditorDialog).toBeVisible() + + // Close mask editor + await comfyPage.page.keyboard.press('Escape') + await expect(maskEditorDialog).toBeHidden() + + // Test 2: Open from image overlay button + const imageWrapper = comfyPage.page.locator('.image-preview [role="img"]') + await imageWrapper.hover() + + const overlayMaskButton = comfyPage.page + .locator('.image-preview') + .getByLabel('Edit or mask image') + await expect(overlayMaskButton).toBeVisible() + await overlayMaskButton.click() + + await expect(maskEditorDialog).toBeVisible() + }) +}) diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index dd25a81213..e95ed22041 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png index a2bfb22d71..a20459ec57 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/load-image-widget-chromium-linux.png differ diff --git a/src/components/graph/selectionToolbox/MaskEditorButton.vue b/src/components/graph/selectionToolbox/MaskEditorButton.vue index cf71483568..8228e77467 100644 --- a/src/components/graph/selectionToolbox/MaskEditorButton.vue +++ b/src/components/graph/selectionToolbox/MaskEditorButton.vue @@ -5,6 +5,7 @@ value: $t('commands.Comfy_MaskEditor_OpenMaskEditor.label'), showDelay: 1000 }" + :aria-label="$t('commands.Comfy_MaskEditor_OpenMaskEditor.label')" severity="secondary" text @click="openMaskEditor" diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue index 538a5a5d34..c0304161ea 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -125,7 +125,6 @@ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { downloadFile } from '@/base/common/downloadUtil' -import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' @@ -207,18 +206,7 @@ const handleImageError = () => { actualDimensions.value = null } -// In vueNodes mode, we need to set them manually before opening the mask editor. -const setupNodeForMaskEditor = () => { - if (!props.nodeId || !currentImageEl.value) return - const node = app.rootGraph?.getNodeById(props.nodeId) - if (!node) return - node.imageIndex = currentIndex.value - node.imgs = [currentImageEl.value] - app.canvas?.select(node) -} - const handleEditMask = () => { - setupNodeForMaskEditor() void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor') } diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index ee8ae759bd..b984151f93 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -2,6 +2,7 @@ import { useTimeoutFn } from '@vueuse/core' import { defineStore } from 'pinia' import { ref } from 'vue' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import type { @@ -37,10 +38,12 @@ interface SetOutputOptions { } export const useNodeOutputStore = defineStore('nodeOutput', () => { - const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore() + const { nodeIdToNodeLocatorId, nodeToNodeLocatorId, nodeLocatorIdToNodeId } = + useWorkflowStore() const { executionIdToNodeLocatorId } = useExecutionStore() const scheduledRevoke: Record void }> = {} const latestOutput = ref([]) + const nodeLoadIds = new WeakMap() function scheduleRevoke(locator: NodeLocatorId, cb: () => void) { scheduledRevoke[locator]?.stop() @@ -156,6 +159,38 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { }) ?? [] app.nodeOutputs[nodeLocatorId] = outputs nodeOutputs.value[nodeLocatorId] = outputs + + syncNodeImgs(nodeLocatorId, latestOutput.value) + } + + /** + * Sync node.imgs for backwards compatibility with legacy systems (e.g., Copy Image). + * Only needed in Vue nodes mode since legacy nodes already populate node.imgs. + */ + function syncNodeImgs(nodeLocatorId: NodeLocatorId, imageUrls: string[]) { + if (!LiteGraph.vueNodesMode) return + if (!imageUrls.length) return + const nodeId = nodeLocatorIdToNodeId(nodeLocatorId) + if (nodeId === null) return + const node = app.canvas?.graph?.getNodeById(nodeId) + if (!node) return + + const loadId = (nodeLoadIds.get(node) ?? 0) + 1 + nodeLoadIds.set(node, loadId) + + const img = new Image() + img.onload = () => { + if (nodeLoadIds.get(node) !== loadId) return + node.imgs = [img] + node.imageIndex = 0 + } + img.onerror = () => { + if (nodeLoadIds.get(node) !== loadId) return + node.imgs = [] + node.imageIndex = 0 + console.warn(`[ImagePreview] Failed to load image for node ${nodeId}`) + } + img.src = imageUrls[0] } function setNodeOutputs(