Compare commits

...

14 Commits

Author SHA1 Message Date
bymyself
ce9beb5371 fix: zoom out more in mask editor test 2025-12-13 19:34:17 -08:00
bymyself
21aeb5a96b fix: use toBeHidden and getByLabel in mask editor test 2025-12-13 19:07:44 -08:00
bymyself
cd486a3ce5 fix: zoom out in mask editor test to keep toolbox in viewport 2025-12-13 03:51:32 -08:00
bymyself
b93c7a55c4 fix: add missing afterEach import 2025-12-13 03:24:32 -08:00
bymyself
62d278c5e6 fix: add error handling and race condition guard to syncNodeImgs 2025-12-13 02:41:19 -08:00
bymyself
45ec11cdbe fix: only sync node.imgs in Vue nodes mode 2025-12-13 01:49:29 -08:00
bymyself
9a9b3f286b refactor: move node.imgs sync from component to store
Move syncNodeImgs logic from ImagePreview.vue to imagePreviewStore.ts
for proper separation of concerns. Store handles backwards compatibility
sync when setting node outputs.
2025-12-12 22:09:14 -08:00
bymyself
9ffced4b30 fix: use existing workflow image instead of widget interaction 2025-12-12 21:00:59 -08:00
bymyself
7049946721 fix: use combo widget instead of drag-drop in mask editor test 2025-12-12 20:39:18 -08:00
Christian Byrne
aa47ed33c4 Merge branch 'main' into image-output-backwards-compat 2025-12-12 19:05:36 -08:00
bymyself
9d4974e4ec fix: sync node.imgs in Vue nodes for Copy Image compatibility
- Add syncNodeImgs() to ImagePreview.vue to sync node.imgs on image load
- Simplify setupNodeForMaskEditor() to reuse sync logic
- Add aria-label to MaskEditorButton for accessibility
- Add browser test for mask editor button functionality
- Add unit test for node.imgs sync behavior

Fixes copy image and other legacy features that rely on node.imgs
2025-12-12 18:58:21 -08:00
github-actions
db7beb6e9f [automated] Update test expectations 2025-12-12 21:13:08 +00:00
bymyself
1311d3b428 simplify 2025-12-12 03:50:50 -08:00
bymyself
54c865e82f assign node images for backwards compat 2025-12-12 01:00:03 -08:00
7 changed files with 89 additions and 13 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -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()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -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"

View File

@@ -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')
}

View File

@@ -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<NodeLocatorId, { stop: () => void }> = {}
const latestOutput = ref<string[]>([])
const nodeLoadIds = new WeakMap<LGraphNode, number>()
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(