mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-31 01:35:51 +00:00
Compare commits
14 Commits
coderabbit
...
image-outp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9beb5371 | ||
|
|
21aeb5a96b | ||
|
|
cd486a3ce5 | ||
|
|
b93c7a55c4 | ||
|
|
62d278c5e6 | ||
|
|
45ec11cdbe | ||
|
|
9a9b3f286b | ||
|
|
9ffced4b30 | ||
|
|
7049946721 | ||
|
|
aa47ed33c4 | ||
|
|
9d4974e4ec | ||
|
|
db7beb6e9f | ||
|
|
1311d3b428 | ||
|
|
54c865e82f |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 105 KiB |
52
browser_tests/tests/vueNodes/maskEditor.spec.ts
Normal file
52
browser_tests/tests/vueNodes/maskEditor.spec.ts
Normal 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 |
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user