mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 13:59:28 +00:00
Compare commits
1 Commits
proxy-widg
...
test/curve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd6eb57199 |
27
browser_tests/assets/vueNodes/widgets/curve_widget.json
Normal file
27
browser_tests/assets/vueNodes/widgets/curve_widget.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [100, 100],
|
||||
"size": [450, 450],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"title": "Curve",
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [""]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { SettingsHelper } from '@e2e/fixtures/helpers/SettingsHelper'
|
||||
import { SubgraphHelper } from '@e2e/fixtures/helpers/SubgraphHelper'
|
||||
import { ToastHelper } from '@e2e/fixtures/helpers/ToastHelper'
|
||||
import { WorkflowHelper } from '@e2e/fixtures/helpers/WorkflowHelper'
|
||||
import { CurveWidgetHelper } from './helpers/CurveWidgetHelper'
|
||||
import type { WorkspaceStore } from '../types/globals'
|
||||
|
||||
dotenvConfig()
|
||||
@@ -269,21 +270,30 @@ export class ComfyPage {
|
||||
return this.toast.visibleToasts
|
||||
}
|
||||
|
||||
async setupUser(username: string) {
|
||||
async setupUser(username: string): Promise<string> {
|
||||
const res = await this.request.get(`${this.url}/api/users`)
|
||||
if (res.status() !== 200)
|
||||
throw new Error(`Failed to retrieve users: ${await res.text()}`)
|
||||
|
||||
const apiRes = await res.json()
|
||||
const user = Object.entries(apiRes?.users ?? {}).find(
|
||||
([, name]) => name === username
|
||||
)
|
||||
const users = apiRes?.users ?? {}
|
||||
const user = Object.entries(users).find(([, name]) => name === username)
|
||||
const id = user?.[0]
|
||||
|
||||
return id ? id : await this.createUser(username)
|
||||
if (id) return id
|
||||
|
||||
try {
|
||||
return await this.createUser(username)
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes('Duplicate username')) {
|
||||
const fallbackName = `${username}-${Math.floor(Math.random() * 9999)}`
|
||||
return await this.createUser(fallbackName)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async createUser(username: string) {
|
||||
async createUser(username: string): Promise<string> {
|
||||
const resp = await this.request.post(`${this.url}/api/users`, {
|
||||
data: { username }
|
||||
})
|
||||
@@ -438,6 +448,7 @@ export const testComfySnapToGridGridSize = 50
|
||||
export const comfyPageFixture = base.extend<{
|
||||
comfyPage: ComfyPage
|
||||
comfyMouse: ComfyMouse
|
||||
curveWidget: (nodeTitleOrId: string | number) => CurveWidgetHelper
|
||||
}>({
|
||||
comfyPage: async ({ page, request }, use, testInfo) => {
|
||||
const comfyPage = new ComfyPage(page, request)
|
||||
@@ -450,25 +461,17 @@ export const comfyPageFixture = base.extend<{
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Top',
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||
// Disable toast warning about version compatibility, as they may or
|
||||
// may not appear - depending on upstream ComfyUI dependencies
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true,
|
||||
// Disable errors tab to prevent missing model detection from
|
||||
// rendering error indicators on nodes during unrelated tests.
|
||||
'Comfy.RightSidePanel.ShowErrorsTab': false
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -488,6 +491,15 @@ export const comfyPageFixture = base.extend<{
|
||||
comfyMouse: async ({ comfyPage }, use) => {
|
||||
const comfyMouse = new ComfyMouse(comfyPage)
|
||||
await use(comfyMouse)
|
||||
},
|
||||
curveWidget: async ({ comfyPage, page }, use) => {
|
||||
await use((nodeTitleOrId: string | number) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(nodeTitleOrId as string)
|
||||
const svg = node.locator('svg').filter({
|
||||
has: page.locator('path[data-testid="curve-path"]')
|
||||
})
|
||||
return new CurveWidgetHelper(page, svg)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
74
browser_tests/fixtures/helpers/CurveWidgetHelper.ts
Normal file
74
browser_tests/fixtures/helpers/CurveWidgetHelper.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class CurveWidgetHelper {
|
||||
constructor(
|
||||
public readonly page: Page,
|
||||
public readonly svgLocator: Locator
|
||||
) {}
|
||||
|
||||
async clickAt(curveX: number, curveY: number): Promise<void> {
|
||||
const box = await this.svgLocator.boundingBox()
|
||||
if (!box) throw new Error('SVG not found')
|
||||
const viewBoxExtent = 1.08
|
||||
const padFraction = 0.04 / viewBoxExtent
|
||||
const usableSize = box.width / viewBoxExtent
|
||||
const screenX = box.x + box.width * padFraction + curveX * usableSize
|
||||
const screenY = box.y + box.height * padFraction + (1 - curveY) * usableSize
|
||||
await this.page.mouse.click(screenX, screenY)
|
||||
}
|
||||
|
||||
async dragPoint(
|
||||
pointIndex: number,
|
||||
toCurveX: number,
|
||||
toCurveY: number
|
||||
): Promise<void> {
|
||||
const circle = this.svgLocator.locator('circle').nth(pointIndex)
|
||||
const circleBox = await circle.boundingBox()
|
||||
if (!circleBox) throw new Error('Circle not found')
|
||||
const fromX = circleBox.x + circleBox.width / 2
|
||||
const fromY = circleBox.y + circleBox.height / 2
|
||||
|
||||
const svgBox = await this.svgLocator.boundingBox()
|
||||
if (!svgBox) throw new Error('SVG not found')
|
||||
const viewBoxExtent = 1.08
|
||||
const padFraction = 0.04 / viewBoxExtent
|
||||
const usableSize = svgBox.width / viewBoxExtent
|
||||
const toScreenX =
|
||||
svgBox.x + svgBox.width * padFraction + toCurveX * usableSize
|
||||
const toScreenY =
|
||||
svgBox.y + svgBox.height * padFraction + (1 - toCurveY) * usableSize
|
||||
|
||||
await this.page.mouse.move(fromX, fromY)
|
||||
await this.page.mouse.down()
|
||||
const steps = 10
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
await this.page.mouse.move(
|
||||
fromX + ((toScreenX - fromX) * i) / steps,
|
||||
fromY + ((toScreenY - fromY) * i) / steps
|
||||
)
|
||||
}
|
||||
await this.page.mouse.up()
|
||||
}
|
||||
|
||||
async rightClickPoint(pointIndex: number): Promise<void> {
|
||||
const circle = this.svgLocator.locator('circle').nth(pointIndex)
|
||||
await circle.dispatchEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 2
|
||||
})
|
||||
}
|
||||
|
||||
async getCurveData(): Promise<
|
||||
{ points: [number, number][]; interpolation: string } | undefined
|
||||
> {
|
||||
return this.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) =>
|
||||
n.widgets?.some((w) => w.type === 'CURVE')
|
||||
)
|
||||
return node?.widgets?.find((w) => w.type === 'CURVE')?.value as
|
||||
| { points: [number, number][]; interpolation: string }
|
||||
| undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
288
browser_tests/tests/vueNodes/widgets/curve/curveWidget.spec.ts
Normal file
288
browser_tests/tests/vueNodes/widgets/curve/curveWidget.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { TestGraphAccess } from '../../../../types/globals'
|
||||
|
||||
test.describe('Curve Widget', { tag: ['@widget', '@smoke'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.workflow.loadWorkflow('vueNodes/widgets/curve_widget')
|
||||
await comfyPage.page.waitForFunction(() => {
|
||||
const g = window.graph as unknown as TestGraphAccess
|
||||
return g?._nodes_by_id?.['1'] !== undefined
|
||||
})
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const g = window.app!.graph as unknown as TestGraphAccess
|
||||
const node = g._nodes_by_id['1']
|
||||
if (!node.widgets?.some((w) => w.type === 'curve')) {
|
||||
node.addWidget(
|
||||
'curve',
|
||||
'tone_curve',
|
||||
{
|
||||
points: [
|
||||
[0, 0],
|
||||
[1, 1]
|
||||
],
|
||||
interpolation: 'monotone_cubic'
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
})
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await expect(
|
||||
comfyPage.vueNodes
|
||||
.getNodeByTitle('Curve')
|
||||
.locator('svg')
|
||||
.filter({
|
||||
has: comfyPage.page.locator('path[data-testid="curve-path"]')
|
||||
})
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Rendering', () => {
|
||||
test('renders with default diagonal', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
const svg = node.locator('svg').filter({
|
||||
has: comfyPage.page.locator('path[data-testid="curve-path"]')
|
||||
})
|
||||
await expect(svg).toBeVisible()
|
||||
await expect(svg.locator('[data-testid="curve-path"]')).toHaveAttribute(
|
||||
'd',
|
||||
/\S+/
|
||||
)
|
||||
await expect(svg.locator('circle')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('interpolation selector shows Smooth by default', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
await expect(node.getByRole('combobox')).toContainText('Smooth')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Adding control points', () => {
|
||||
test('click adds a new control point', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(2)
|
||||
await helper.clickAt(0.5, 0.8)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(3)
|
||||
})
|
||||
|
||||
test('multiple clicks add multiple points', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.clickAt(0.25, 0.3)
|
||||
await helper.clickAt(0.5, 0.6)
|
||||
await helper.clickAt(0.75, 0.4)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(5)
|
||||
})
|
||||
|
||||
test('Ctrl+click does not add points', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
const svg = node.locator('svg').filter({
|
||||
has: comfyPage.page.locator('path[data-testid="curve-path"]')
|
||||
})
|
||||
const box = await svg.boundingBox()
|
||||
const viewBoxExtent = 1.08
|
||||
const pad = 0.04 / viewBoxExtent
|
||||
const usable = box!.width / viewBoxExtent
|
||||
const x = box!.x + box!.width * pad + 0.5 * usable
|
||||
const y = box!.y + box!.height * pad + 0.5 * usable
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.mouse.click(x, y)
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await expect(svg.locator('circle')).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Dragging control points', () => {
|
||||
test('dragging a point updates curve', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
const path = helper.svgLocator.locator('[data-testid="curve-path"]')
|
||||
await helper.clickAt(0.5, 0.5)
|
||||
const d1 = await path.getAttribute('d')
|
||||
await helper.dragPoint(1, 0.5, 0.8)
|
||||
await expect.poll(() => path.getAttribute('d')).not.toBe(d1)
|
||||
})
|
||||
|
||||
test('no points when disabled @widget @smoke', async ({
|
||||
comfyPage,
|
||||
curveWidget
|
||||
}) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await comfyPage.page.evaluate((title) => {
|
||||
interface TestNode {
|
||||
widgets: Array<{
|
||||
options?: {
|
||||
disabled?: boolean
|
||||
}
|
||||
}>
|
||||
}
|
||||
interface TestGraph {
|
||||
findNodesByTitle: (t: string) => TestNode[]
|
||||
}
|
||||
|
||||
const graph = window.graph as unknown as TestGraph
|
||||
const node = graph.findNodesByTitle(title)[0]
|
||||
if (node) {
|
||||
const widget = node.widgets[0]
|
||||
if (widget) {
|
||||
if (!widget.options) widget.options = {}
|
||||
widget.options.disabled = true
|
||||
}
|
||||
}
|
||||
}, 'Curve')
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('drag clamps to [0, 1]', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.dragPoint(0, -0.5, 1.5)
|
||||
const data = await helper.getCurveData()
|
||||
expect(data?.points[0][0]).toBeGreaterThanOrEqual(-0.001)
|
||||
expect(data?.points[0][0]).toBeLessThanOrEqual(1.001)
|
||||
expect(data?.points[0][1]).toBeGreaterThanOrEqual(-0.001)
|
||||
expect(data?.points[0][1]).toBeLessThanOrEqual(1.001)
|
||||
})
|
||||
|
||||
test('drag maintains x-order', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.clickAt(0.5, 0.5)
|
||||
await helper.dragPoint(1, 0.1, 0.5)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(3)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Deleting control points', () => {
|
||||
test('right-click deletes point', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.clickAt(0.5, 0.5)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(3)
|
||||
await helper.rightClickPoint(1)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('Ctrl+click deletes point', async ({ comfyPage, curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.clickAt(0.5, 0.5)
|
||||
const circle = helper.svgLocator.locator('circle').nth(1)
|
||||
const cbox = await circle.boundingBox()
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.mouse.click(
|
||||
cbox!.x + cbox!.width / 2,
|
||||
cbox!.y + cbox!.height / 2
|
||||
)
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('minimum 2 points limit', async ({ curveWidget }) => {
|
||||
const helper = curveWidget('Curve')
|
||||
await helper.rightClickPoint(0)
|
||||
await expect(helper.svgLocator.locator('circle')).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Interpolation', () => {
|
||||
test('Smooth to Linear changes path', async ({
|
||||
comfyPage,
|
||||
curveWidget
|
||||
}) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
const helper = curveWidget('Curve')
|
||||
const path = helper.svgLocator.locator('[data-testid="curve-path"]')
|
||||
await helper.clickAt(0.5, 0.8)
|
||||
const d1 = await path.getAttribute('d')
|
||||
await node.getByRole('combobox').click()
|
||||
await comfyPage.page.getByRole('option', { name: 'Linear' }).click()
|
||||
await expect.poll(() => path.getAttribute('d')).not.toBe(d1)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Histogram', () => {
|
||||
test('absent before execution', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
await expect(
|
||||
node.locator('[data-testid="histogram-path"]')
|
||||
).not.toBeAttached()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Disabled state', () => {
|
||||
test('no points when disabled', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.title === 'Curve')!
|
||||
node.widgets![0].disabled = true
|
||||
})
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
await expect(node.locator('circle')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Screenshots', { tag: '@screenshot' }, () => {
|
||||
test('default curve matches baseline', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('curve-default.png', {
|
||||
maxDiffPixelRatio: 0.01
|
||||
})
|
||||
})
|
||||
|
||||
test('linear curve matches baseline', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Curve')
|
||||
await node.getByRole('combobox').click()
|
||||
await comfyPage.page.getByRole('option', { name: 'Linear' }).click()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('curve-linear.png', {
|
||||
maxDiffPixelRatio: 0.01
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Persistence', { tag: '@workflow' }, () => {
|
||||
test('data persists after save/reload', async ({
|
||||
comfyPage,
|
||||
curveWidget
|
||||
}) => {
|
||||
await curveWidget('Curve').clickAt(0.5, 0.8)
|
||||
await comfyPage.workflow.loadWorkflow('vueNodes/widgets/curve_widget')
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const graph = window.graph as TestGraphAccess
|
||||
const node = graph._nodes_by_id['1']
|
||||
node.addWidget(
|
||||
'curve',
|
||||
'tone_curve',
|
||||
{
|
||||
points: [
|
||||
[0, 0],
|
||||
[0.5, 0.8],
|
||||
[1, 1]
|
||||
],
|
||||
interpolation: 'monotone_cubic'
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
})
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('Curve').locator('circle')
|
||||
).toHaveCount(3)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Edge cases', () => {
|
||||
test('20 points', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.title === 'Curve')!
|
||||
const w = node.widgets!.find((w) => w.type === 'curve')!
|
||||
;(w.value as { points: [number, number][] }).points = Array.from(
|
||||
{ length: 20 },
|
||||
(_, i) => [i / 19, i / 19]
|
||||
)
|
||||
})
|
||||
await expect(
|
||||
comfyPage.vueNodes.getNodeByTitle('Curve').locator('circle')
|
||||
).toHaveCount(20)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,10 +16,7 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
@@ -77,7 +74,6 @@ export interface SafeWidgetData {
|
||||
advanced?: boolean
|
||||
hidden?: boolean
|
||||
read_only?: boolean
|
||||
values?: IWidgetOptions['values']
|
||||
}
|
||||
/** Input specification from node definition */
|
||||
spec?: InputSpec
|
||||
@@ -226,8 +222,7 @@ function safeWidgetMapper(
|
||||
canvasOnly: widget.options.canvasOnly,
|
||||
advanced: widget.options?.advanced ?? widget.advanced,
|
||||
hidden: widget.options.hidden,
|
||||
read_only: widget.options.read_only,
|
||||
values: widget.options.values
|
||||
read_only: widget.options.read_only
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,6 @@ const proxyWidgetTupleSchema = z.union([
|
||||
const proxyWidgetsPropertySchema = z.array(proxyWidgetTupleSchema)
|
||||
type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
|
||||
export interface ProxyWidgetSelector {
|
||||
name?: string
|
||||
selected: string
|
||||
options: {
|
||||
label: string
|
||||
widgets?: string[][]
|
||||
}[]
|
||||
}
|
||||
|
||||
export function parseProxyWidgets(
|
||||
property: NodeProperty | undefined
|
||||
): ProxyWidgetsProperty {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
supportsVirtualCanvasImagePreview
|
||||
} from '@/composables/node/canvasImagePreviewTypes'
|
||||
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
|
||||
import type { ProxyWidgetSelector } from '@/core/schemas/promotionSchema'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import {
|
||||
makePromotionEntryKey,
|
||||
@@ -117,7 +116,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
hasMissingBoundSourceWidget: boolean
|
||||
views: PromotedWidgetView[]
|
||||
}
|
||||
private _selectorWidget: IBaseWidget | null = null
|
||||
|
||||
// Declared as accessor via Object.defineProperty in constructor.
|
||||
// TypeScript doesn't allow overriding a property with get/set syntax,
|
||||
@@ -299,69 +297,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
return views
|
||||
}
|
||||
|
||||
private _getWidgetsWithSelector(): IBaseWidget[] {
|
||||
const views = this._getPromotedViews()
|
||||
const selector = this.properties.proxyWidgetSelector as
|
||||
| ProxyWidgetSelector
|
||||
| undefined
|
||||
if (!selector?.options?.length || !this._selectorWidget) return views
|
||||
|
||||
const selectedOption =
|
||||
selector.options.find((opt) => opt.label === selector.selected) ??
|
||||
selector.options[0]
|
||||
if (!selectedOption?.widgets) return [this._selectorWidget, ...views]
|
||||
|
||||
const allGroupedKeys = new Set(
|
||||
selector.options.flatMap((opt) =>
|
||||
(opt.widgets ?? []).map(([nid, wn]) => `${nid}:${wn}`)
|
||||
)
|
||||
)
|
||||
const selectedKeys = new Set(
|
||||
selectedOption.widgets.map(([nid, wn]) => `${nid}:${wn}`)
|
||||
)
|
||||
|
||||
const filteredViews = views.filter((v) => {
|
||||
const key = `${v.sourceNodeId}:${v.sourceWidgetName}`
|
||||
return !allGroupedKeys.has(key) || selectedKeys.has(key)
|
||||
})
|
||||
|
||||
return [this._selectorWidget, ...filteredViews]
|
||||
}
|
||||
|
||||
private _initSelectorWidget(): void {
|
||||
const selector = this.properties.proxyWidgetSelector as
|
||||
| ProxyWidgetSelector
|
||||
| undefined
|
||||
if (!selector?.options?.length) {
|
||||
this._selectorWidget = null
|
||||
return
|
||||
}
|
||||
|
||||
const validLabels = selector.options.map((o) => o.label)
|
||||
if (!validLabels.includes(selector.selected)) {
|
||||
selector.selected = validLabels[0]
|
||||
}
|
||||
|
||||
this._selectorWidget = {
|
||||
name: selector.name ?? 'selector',
|
||||
type: 'combo',
|
||||
value: selector.selected,
|
||||
y: 0,
|
||||
serialize: false,
|
||||
options: {
|
||||
values: validLabels
|
||||
},
|
||||
callback: (value: unknown) => {
|
||||
selector.selected = String(value)
|
||||
this._selectorWidget!.value = String(value)
|
||||
this._invalidatePromotedViewsCache()
|
||||
const minSize = this.computeSize()
|
||||
this.setSize([this.size[0], minSize[1]])
|
||||
this.graph?.setDirtyCanvas(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _invalidatePromotedViewsCache(): void {
|
||||
this._cacheVersion++
|
||||
}
|
||||
@@ -760,7 +695,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
// Synthetic widgets getter — SubgraphNodes have no native widgets.
|
||||
Object.defineProperty(this, 'widgets', {
|
||||
get: () => this._getWidgetsWithSelector(),
|
||||
get: () => this._getPromotedViews(),
|
||||
set: () => {
|
||||
if (import.meta.env.DEV)
|
||||
console.warn(
|
||||
@@ -1162,8 +1097,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this.properties.proxyWidgets = serialized
|
||||
}
|
||||
|
||||
this._initSelectorWidget()
|
||||
|
||||
// Check all inputs for connected widgets
|
||||
for (const input of this.inputs) {
|
||||
const subgraphInput = input._subgraphSlot
|
||||
|
||||
Reference in New Issue
Block a user