mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
Compare commits
29 Commits
v1.39.2
...
drjkl/shh-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53b1170bd2 | ||
|
|
50c1754da6 | ||
|
|
be7c34e28b | ||
|
|
a807986835 | ||
|
|
679288500a | ||
|
|
34d42311ea | ||
|
|
c4e5fc8dbf | ||
|
|
a64c561a5f | ||
|
|
59c58379fe | ||
|
|
6c14ae6f90 | ||
|
|
ee600a8951 | ||
|
|
e7d3bc7285 | ||
|
|
b4649bc96d | ||
|
|
d7ec24abc3 | ||
|
|
1ac214a1cd | ||
|
|
985d024a6e | ||
|
|
ee4a205d32 | ||
|
|
d784d4982b | ||
|
|
47113b117e | ||
|
|
13311a46ea | ||
|
|
067d80c4ed | ||
|
|
82bacb82a7 | ||
|
|
80ccc13659 | ||
|
|
dce6fd1040 | ||
|
|
e2625a4055 | ||
|
|
a4cf9a1ca8 | ||
|
|
d437b96238 | ||
|
|
f1cf8073d6 | ||
|
|
d7654baebf |
1
.github/workflows/ci-lint-format.yaml
vendored
1
.github/workflows/ci-lint-format.yaml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
2
.github/workflows/i18n-update-core.yaml
vendored
2
.github/workflows/i18n-update-core.yaml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
|
||||
# Setup playwright environment
|
||||
- name: Setup ComfyUI Frontend
|
||||
|
||||
1
.github/workflows/release-draft-create.yaml
vendored
1
.github/workflows/release-draft-create.yaml
vendored
@@ -50,6 +50,7 @@ jobs:
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
|
||||
@@ -98,12 +98,10 @@ const config: StorybookConfig = {
|
||||
},
|
||||
build: {
|
||||
rolldownOptions: {
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true
|
||||
keepNames: true,
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { KeyCombo } from '../../src/platform/keybindings'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { webSocketFixture } from '../fixtures/ws.ts'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('Actionbar', () => {
|
||||
test.describe('Actionbar', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Bottom Panel Shortcuts', () => {
|
||||
test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Browser tab title', () => {
|
||||
test.describe('Browser tab title', { tag: '@smoke' }, () => {
|
||||
test.describe('Beta Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
@@ -19,7 +19,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Change Tracker', () => {
|
||||
test.describe('Change Tracker', { tag: '@workflow' }, () => {
|
||||
test.describe('Undo/Redo', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
@@ -151,7 +151,7 @@ const customColorPalettes: Record<string, Palette> = {
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Color Palette', () => {
|
||||
test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
|
||||
test('Can show custom color palette', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
|
||||
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
|
||||
@@ -194,104 +194,110 @@ test.describe('Color Palette', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node Color Adjustments', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
})
|
||||
|
||||
test('should adjust opacity via node opacity setting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
|
||||
// Drag mouse to force canvas to redraw
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
|
||||
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
|
||||
await comfyPage.page.mouse.move(8, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
|
||||
})
|
||||
|
||||
test('should persist color adjustments when changing themes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.2-arc-theme.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should not serialize color adjustments in workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
const parsed = await (
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const workflow = localStorage.getItem('workflow')
|
||||
if (!workflow) return null
|
||||
try {
|
||||
const data = JSON.parse(workflow)
|
||||
return Array.isArray(data?.nodes) ? data : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
).jsonValue()
|
||||
expect(parsed.nodes).toBeDefined()
|
||||
expect(Array.isArray(parsed.nodes)).toBe(true)
|
||||
for (const node of parsed.nodes) {
|
||||
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
|
||||
if (node.color) expect(node.color).not.toMatch(/hsla/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should lighten node colors when switching to light theme', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
|
||||
})
|
||||
|
||||
test.describe('Context menu color adjustments', () => {
|
||||
test.describe(
|
||||
'Node Color Adjustments',
|
||||
{ tag: ['@screenshot', '@settings'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
})
|
||||
|
||||
test('should adjust opacity via node opacity setting', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
|
||||
// Drag mouse to force canvas to redraw
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
|
||||
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
|
||||
|
||||
await comfyPage.page.mouse.move(8, 8)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
|
||||
})
|
||||
|
||||
test('should persist color adjustments when changing themes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.mouse.move(0, 0)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.2-arc-theme.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should not serialize color adjustments in workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
|
||||
const node = await comfyPage.getFirstNodeRef()
|
||||
await node?.clickContextMenuOption('Colors')
|
||||
await comfyPage.nextFrame()
|
||||
const parsed = await (
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => {
|
||||
const workflow = localStorage.getItem('workflow')
|
||||
if (!workflow) return null
|
||||
try {
|
||||
const data = JSON.parse(workflow)
|
||||
return Array.isArray(data?.nodes) ? data : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
)
|
||||
).jsonValue()
|
||||
expect(parsed.nodes).toBeDefined()
|
||||
expect(Array.isArray(parsed.nodes)).toBe(true)
|
||||
for (const node of parsed.nodes) {
|
||||
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
|
||||
if (node.color) expect(node.color).not.toMatch(/hsla/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should persist color adjustments when changing custom node colors', async ({
|
||||
test('should lighten node colors when switching to light theme', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page
|
||||
.locator('.litemenu-entry.submenu span:has-text("red")')
|
||||
.click()
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.3-color-changed.png'
|
||||
'node-lightened-colors.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should persist color adjustments when removing custom node color', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page
|
||||
.locator('.litemenu-entry.submenu span:has-text("No color")')
|
||||
.click()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.3-color-removed.png'
|
||||
)
|
||||
test.describe('Context menu color adjustments', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
|
||||
const node = await comfyPage.getFirstNodeRef()
|
||||
await node?.clickContextMenuOption('Colors')
|
||||
})
|
||||
|
||||
test('should persist color adjustments when changing custom node colors', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page
|
||||
.locator('.litemenu-entry.submenu span:has-text("red")')
|
||||
.click()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.3-color-changed.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should persist color adjustments when removing custom node color', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page
|
||||
.locator('.litemenu-entry.submenu span:has-text("No color")')
|
||||
.click()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-opacity-0.3-color-removed.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test.describe('Keybindings', { tag: '@keyboard' }, () => {
|
||||
test('Should execute command', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', () => {
|
||||
window['foo'] = true
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Copy Paste', () => {
|
||||
test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
|
||||
test('Can copy and paste node', async ({ comfyPage }) => {
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
|
||||
@@ -22,7 +22,7 @@ async function verifyCustomIconSvg(iconElement: Locator) {
|
||||
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
|
||||
}
|
||||
|
||||
test.describe('Custom Icons', () => {
|
||||
test.describe('Custom Icons', { tag: '@settings' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import type { Keybinding } from '../../src/platform/keybindings'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
test.describe('Load workflow warning', { tag: '@ui' }, () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('DOM Widget', () => {
|
||||
test.describe('DOM Widget', { tag: '@widget' }, () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
|
||||
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
|
||||
@@ -29,12 +29,16 @@ test.describe('DOM Widget', () => {
|
||||
await expect(lastMultiline).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Position update when entering focus mode', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
})
|
||||
test(
|
||||
'Position update when entering focus mode',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
|
||||
}
|
||||
)
|
||||
|
||||
// No DOM widget should be created by creation of interim LGraphNode objects.
|
||||
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {
|
||||
|
||||
@@ -6,40 +6,48 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Execution', () => {
|
||||
test('Report error on unconnected slot', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyPage.clickEmptySpace()
|
||||
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
|
||||
test(
|
||||
'Report error on unconnected slot',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyPage.clickEmptySpace()
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
|
||||
await comfyPage.page.locator('.p-dialog-close-button').click()
|
||||
await comfyPage.page.locator('.comfy-error-report').waitFor({
|
||||
state: 'hidden'
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'execution-error-unconnected-slot.png'
|
||||
)
|
||||
})
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
|
||||
await comfyPage.page.locator('.p-dialog-close-button').click()
|
||||
await comfyPage.page.locator('.comfy-error-report').waitFor({
|
||||
state: 'hidden'
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'execution-error-unconnected-slot.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Execute to selected output nodes', () => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
await expect(async () => {
|
||||
test.describe(
|
||||
'Execute to selected output nodes',
|
||||
{ tag: ['@smoke', '@workflow'] },
|
||||
() => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
}).toPass({ timeout: 2_000 })
|
||||
})
|
||||
})
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
await expect(async () => {
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
}).toPass({ timeout: 2_000 })
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Feature Flags', () => {
|
||||
test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
|
||||
test('Client and server exchange feature flags on connection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Graph', () => {
|
||||
test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
|
||||
// Should be able to fix link input slot index after swap the input order
|
||||
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
|
||||
test('Fix link input slots', async ({ comfyPage }) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Graph Canvas Menu', () => {
|
||||
test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Set link render mode to spline to make sure it's not affected by other tests'
|
||||
// side effects.
|
||||
@@ -15,29 +15,33 @@ test.describe('Graph Canvas Menu', () => {
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
})
|
||||
|
||||
test('Can toggle link visibility', async ({ comfyPage }) => {
|
||||
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-hidden-links.png'
|
||||
)
|
||||
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
|
||||
return window['LiteGraph'].HIDDEN_LINK
|
||||
})
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
test(
|
||||
'Can toggle link visibility',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-hidden-links.png'
|
||||
)
|
||||
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
|
||||
return window['LiteGraph'].HIDDEN_LINK
|
||||
})
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-visible-links.png'
|
||||
)
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
})
|
||||
await button.click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'canvas-with-visible-links.png'
|
||||
)
|
||||
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
|
||||
hiddenLinkRenderMode
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Toggle minimap button is clickable and has correct test id', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.describe('Group Node', { tag: '@node' }, () => {
|
||||
test.describe('Node library sidebar', () => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
const groupNodeCategory = 'group nodes>workflow'
|
||||
@@ -89,16 +89,20 @@ test.describe('Group Node', () => {
|
||||
// does not have a v-model on the query, so we cannot observe the raw
|
||||
// query update, and thus cannot set the spinning state between the raw query
|
||||
// update and the debounced search update.
|
||||
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'group-node-copy-added-from-search.png'
|
||||
)
|
||||
})
|
||||
test.skip(
|
||||
'Can be added to canvas using search',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'group-node-copy-added-from-search.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
|
||||
@@ -13,7 +13,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('groups/mixed_graph_items')
|
||||
await comfyPage.canvas.press('Control+a')
|
||||
@@ -60,13 +60,17 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('@2x Can highlight selected', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
||||
await comfyPage.clickTextEncodeNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
})
|
||||
test(
|
||||
'@2x Can highlight selected',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNode1()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
||||
await comfyPage.clickTextEncodeNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
||||
}
|
||||
)
|
||||
|
||||
const dragSelectNodes = async (
|
||||
comfyPage: ComfyPage,
|
||||
@@ -150,12 +154,12 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Can drag node', async ({ comfyPage }) => {
|
||||
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.dragNode2()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
|
||||
})
|
||||
|
||||
test.describe('Edge Interaction', () => {
|
||||
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
|
||||
@@ -222,12 +226,18 @@ test.describe('Node Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('Can adjust widget value', async ({ comfyPage }) => {
|
||||
await comfyPage.adjustWidgetValue()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png')
|
||||
})
|
||||
test(
|
||||
'Can adjust widget value',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.adjustWidgetValue()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'adjusted-widget-value.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Link snap to slot', async ({ comfyPage }) => {
|
||||
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('links/snap_to_slot')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
|
||||
|
||||
@@ -244,57 +254,67 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
|
||||
})
|
||||
|
||||
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
|
||||
test(
|
||||
'Can batch move links by drag with shift',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
}
|
||||
const outputSlot2Pos = {
|
||||
x: 307,
|
||||
y: 310
|
||||
}
|
||||
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'batch_move_links_moved.png'
|
||||
)
|
||||
}
|
||||
const outputSlot2Pos = {
|
||||
x: 307,
|
||||
y: 310
|
||||
)
|
||||
|
||||
test(
|
||||
'Can batch disconnect links with ctrl+alt+click',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const loadCheckpointClipSlotPos = {
|
||||
x: 332,
|
||||
y: 508
|
||||
}
|
||||
await comfyPage.canvas.click({
|
||||
modifiers: ['Control', 'Alt'],
|
||||
position: loadCheckpointClipSlotPos
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'batch-disconnect-links-disconnected.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'batch_move_links_moved.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can batch disconnect links with ctrl+alt+click', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointClipSlotPos = {
|
||||
x: 332,
|
||||
y: 508
|
||||
test(
|
||||
'Can toggle dom widget node open/closed',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-off.png'
|
||||
)
|
||||
await comfyPage.delay(1000)
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-back-open.png'
|
||||
)
|
||||
}
|
||||
await comfyPage.canvas.click({
|
||||
modifiers: ['Control', 'Alt'],
|
||||
position: loadCheckpointClipSlotPos
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'batch-disconnect-links-disconnected.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-off.png'
|
||||
)
|
||||
await comfyPage.delay(1000)
|
||||
await comfyPage.clickTextEncodeNodeToggler()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'text-encode-toggled-back-open.png'
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
test('Can close prompt dialog with canvas click (number widget)', async ({
|
||||
comfyPage
|
||||
@@ -341,19 +361,23 @@ test.describe('Node Interaction', () => {
|
||||
await expect(legacyPrompt).toBeHidden()
|
||||
})
|
||||
|
||||
test('Can double click node title to edit', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
|
||||
})
|
||||
test(
|
||||
'Can double click node title to edit',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: 50,
|
||||
y: 10
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
await comfyPage.page.keyboard.type('Hello World')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
|
||||
}
|
||||
)
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
@@ -369,29 +393,41 @@ test.describe('Node Interaction', () => {
|
||||
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
|
||||
})
|
||||
|
||||
test('Can group selected nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
|
||||
await comfyPage.select2Nodes()
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.press('KeyG')
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await comfyPage.nextFrame()
|
||||
// Confirm group title
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
|
||||
})
|
||||
test(
|
||||
'Can group selected nodes',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
|
||||
await comfyPage.select2Nodes()
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.press('KeyG')
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
await comfyPage.nextFrame()
|
||||
// Confirm group title
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'group-selected-nodes.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Can fit group to contents', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('groups/oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
|
||||
})
|
||||
test(
|
||||
'Can fit group to contents',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('groups/oversized_group')
|
||||
await comfyPage.ctrlA()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'group-fit-to-contents.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Can pin/unpin nodes', async ({ comfyPage }) => {
|
||||
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
|
||||
await comfyPage.nextFrame()
|
||||
@@ -401,20 +437,22 @@ test.describe('Node Interaction', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
|
||||
})
|
||||
|
||||
test('Can bypass/unbypass nodes with keyboard shortcut', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await comfyPage.canvas.press('Control+b')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
|
||||
await comfyPage.canvas.press('Control+b')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
|
||||
})
|
||||
test(
|
||||
'Can bypass/unbypass nodes with keyboard shortcut',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await comfyPage.canvas.press('Control+b')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
|
||||
await comfyPage.canvas.press('Control+b')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Group Interaction', () => {
|
||||
test.describe('Group Interaction', { tag: '@screenshot' }, () => {
|
||||
test('Can double click group title to edit', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('groups/single_group')
|
||||
await comfyPage.canvas.dblclick({
|
||||
@@ -430,7 +468,7 @@ test.describe('Group Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Canvas Interaction', () => {
|
||||
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
|
||||
test('Can zoom in/out', async ({ comfyPage }) => {
|
||||
await comfyPage.zoom(-100)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
|
||||
@@ -632,7 +670,7 @@ test.describe('Widget Interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load workflow', () => {
|
||||
test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
test('Can load workflow with string node id', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/string_node_id')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
|
||||
@@ -824,7 +862,7 @@ test.describe('Viewport settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Canvas Navigation', () => {
|
||||
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
||||
test.describe('Legacy Mode', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test.describe('Keybindings', { tag: '@keyboard' }, () => {
|
||||
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ function listenForEvent(): Promise<Event> {
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('Canvas Event', () => {
|
||||
test.describe('Canvas Event', { tag: '@canvas' }, () => {
|
||||
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
|
||||
const eventPromise = comfyPage.page.evaluate(listenForEvent)
|
||||
const disconnectPromise = comfyPage.disconnectEdge()
|
||||
|
||||
@@ -6,46 +6,50 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Load Workflow in Media', () => {
|
||||
const fileNames = [
|
||||
'workflow.webp',
|
||||
'edited_workflow.webp',
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow_prompt_parameters.png',
|
||||
'workflow.webm',
|
||||
// Skipped due to 3d widget unstable visual result.
|
||||
// 3d widget shows grid after fully loaded.
|
||||
// 'workflow.glb',
|
||||
'workflow.mp4',
|
||||
'workflow.mov',
|
||||
'workflow.m4v',
|
||||
'workflow.svg'
|
||||
// TODO: Re-enable after fixing test asset to use core nodes only
|
||||
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
|
||||
// 'workflow.avif'
|
||||
]
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
test.describe(
|
||||
'Load Workflow in Media',
|
||||
{ tag: ['@screenshot', '@workflow'] },
|
||||
() => {
|
||||
const fileNames = [
|
||||
'workflow.webp',
|
||||
'edited_workflow.webp',
|
||||
'no_workflow.webp',
|
||||
'large_workflow.webp',
|
||||
'workflow_prompt_parameters.png',
|
||||
'workflow.webm',
|
||||
// Skipped due to 3d widget unstable visual result.
|
||||
// 3d widget shows grid after fully loaded.
|
||||
// 'workflow.glb',
|
||||
'workflow.mp4',
|
||||
'workflow.mov',
|
||||
'workflow.m4v',
|
||||
'workflow.svg'
|
||||
// TODO: Re-enable after fixing test asset to use core nodes only
|
||||
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
|
||||
// 'workflow.avif'
|
||||
]
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const urls = [
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
|
||||
]
|
||||
urls.forEach(async (url) => {
|
||||
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropURL(url)
|
||||
const readableName = url.split('/').pop()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`dropped_workflow_url_${readableName}.png`
|
||||
)
|
||||
const urls = [
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
|
||||
]
|
||||
urls.forEach(async (url) => {
|
||||
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.dragAndDropURL(url)
|
||||
const readableName = url.split('/').pop()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`dropped_workflow_url_${readableName}.png`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('LOD Threshold', () => {
|
||||
test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
|
||||
test('Should switch to low quality mode at correct zoom threshold', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
@@ -149,53 +149,55 @@ test.describe('LOD Threshold', () => {
|
||||
expect(state.scale).toBeLessThan(0.2) // Very zoomed out
|
||||
})
|
||||
|
||||
test('Should show visual difference between LOD on and off', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load a workflow with text-heavy nodes for clear visual difference
|
||||
await comfyPage.loadWorkflow('default')
|
||||
test(
|
||||
'Should show visual difference between LOD on and off',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
// Load a workflow with text-heavy nodes for clear visual difference
|
||||
await comfyPage.loadWorkflow('default')
|
||||
|
||||
// Set zoom level clearly below the threshold to ensure LOD activates
|
||||
const targetZoom = 0.4 // Well below default threshold of ~0.571
|
||||
// Set zoom level clearly below the threshold to ensure LOD activates
|
||||
const targetZoom = 0.4 // Well below default threshold of ~0.571
|
||||
|
||||
// Zoom to target level
|
||||
await comfyPage.page.evaluate((zoom) => {
|
||||
window['app'].canvas.ds.scale = zoom
|
||||
window['app'].canvas.setDirty(true, true)
|
||||
}, targetZoom)
|
||||
await comfyPage.nextFrame()
|
||||
// Zoom to target level
|
||||
await comfyPage.page.evaluate((zoom) => {
|
||||
window['app'].canvas.ds.scale = zoom
|
||||
window['app'].canvas.setDirty(true, true)
|
||||
}, targetZoom)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Take snapshot with LOD active (default 8px setting)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'lod-comparison-low-quality.png'
|
||||
)
|
||||
// Take snapshot with LOD active (default 8px setting)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'lod-comparison-low-quality.png'
|
||||
)
|
||||
|
||||
const lowQualityState = await comfyPage.page.evaluate(() => {
|
||||
const canvas = window['app'].canvas
|
||||
return {
|
||||
lowQuality: canvas.low_quality,
|
||||
scale: canvas.ds.scale
|
||||
}
|
||||
})
|
||||
expect(lowQualityState.lowQuality).toBe(true)
|
||||
const lowQualityState = await comfyPage.page.evaluate(() => {
|
||||
const canvas = window['app'].canvas
|
||||
return {
|
||||
lowQuality: canvas.low_quality,
|
||||
scale: canvas.ds.scale
|
||||
}
|
||||
})
|
||||
expect(lowQualityState.lowQuality).toBe(true)
|
||||
|
||||
// Disable LOD to see high quality at same zoom
|
||||
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
|
||||
await comfyPage.nextFrame()
|
||||
// Disable LOD to see high quality at same zoom
|
||||
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Take snapshot with LOD disabled (full quality at same zoom)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'lod-comparison-high-quality.png'
|
||||
)
|
||||
// Take snapshot with LOD disabled (full quality at same zoom)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'lod-comparison-high-quality.png'
|
||||
)
|
||||
|
||||
const highQualityState = await comfyPage.page.evaluate(() => {
|
||||
const canvas = window['app'].canvas
|
||||
return {
|
||||
lowQuality: canvas.low_quality,
|
||||
scale: canvas.ds.scale
|
||||
}
|
||||
})
|
||||
expect(highQualityState.lowQuality).toBe(false)
|
||||
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
|
||||
})
|
||||
const highQualityState = await comfyPage.page.evaluate(() => {
|
||||
const canvas = window['app'].canvas
|
||||
return {
|
||||
lowQuality: canvas.low_quality,
|
||||
scale: canvas.ds.scale
|
||||
}
|
||||
})
|
||||
expect(highQualityState.lowQuality).toBe(false)
|
||||
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Menu', () => {
|
||||
test.describe('Menu', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Minimap', () => {
|
||||
test.describe('Minimap', { tag: '@canvas' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Minimap.Visible', true)
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
test.describe('Mobile Baseline Snapshots', () => {
|
||||
test('@mobile empty canvas', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ConfirmClear', false)
|
||||
await comfyPage.executeCommand('Comfy.ClearWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 256 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
|
||||
})
|
||||
test.describe(
|
||||
'Mobile Baseline Snapshots',
|
||||
{ tag: ['@mobile', '@screenshot'] },
|
||||
() => {
|
||||
test('@mobile empty canvas', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.ConfirmClear', false)
|
||||
await comfyPage.executeCommand('Comfy.ClearWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 256 })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
|
||||
})
|
||||
|
||||
test('@mobile default workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'mobile-default-workflow.png'
|
||||
)
|
||||
})
|
||||
test('@mobile default workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'mobile-default-workflow.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('@mobile settings dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.nextFrame()
|
||||
test('@mobile settings dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
|
||||
'mobile-settings-dialog.png',
|
||||
{
|
||||
mask: [
|
||||
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
|
||||
]
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
|
||||
'mobile-settings-dialog.png',
|
||||
{
|
||||
mask: [
|
||||
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
|
||||
]
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Node Badge', () => {
|
||||
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
|
||||
test('Can add badge', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const LGraphBadge = window['LGraphBadge']
|
||||
@@ -66,50 +66,60 @@ test.describe('Node Badge', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Node source badge', () => {
|
||||
Object.values(NodeBadgeMode).forEach(async (mode) => {
|
||||
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
|
||||
// Execution error workflow has both custom node and core node.
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.resetView()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`)
|
||||
test.describe(
|
||||
'Node source badge',
|
||||
{ tag: ['@screenshot', '@smoke', '@node'] },
|
||||
() => {
|
||||
Object.values(NodeBadgeMode).forEach(async (mode) => {
|
||||
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
|
||||
// Execution error workflow has both custom node and core node.
|
||||
await comfyPage.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
|
||||
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.resetView()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`node-badge-${mode}.png`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Node badge color', () => {
|
||||
test('Can show node badge with unknown color palette', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
NodeBadgeMode.ShowAll
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
|
||||
await comfyPage.nextFrame()
|
||||
// Click empty space to trigger canvas re-render.
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-badge-unknown-color-palette.png'
|
||||
)
|
||||
})
|
||||
test.describe(
|
||||
'Node badge color',
|
||||
{ tag: ['@screenshot', '@smoke', '@node'] },
|
||||
() => {
|
||||
test('Can show node badge with unknown color palette', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
NodeBadgeMode.ShowAll
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
|
||||
await comfyPage.nextFrame()
|
||||
// Click empty space to trigger canvas re-render.
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-badge-unknown-color-palette.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can show node badge with light color palette', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
NodeBadgeMode.ShowAll
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
// Click empty space to trigger canvas re-render.
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-badge-light-color-palette.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
test('Can show node badge with light color palette', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode',
|
||||
NodeBadgeMode.ShowAll
|
||||
)
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.nextFrame()
|
||||
// Click empty space to trigger canvas re-render.
|
||||
await comfyPage.clickEmptySpace()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'node-badge-light-color-palette.png'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
// If an input is optional by node definition, it should be shown as
|
||||
// a hollow circle no matter what shape it was defined in the workflow JSON.
|
||||
test.describe('Optional input', () => {
|
||||
test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
test('No shape specified', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
|
||||
|
||||
@@ -23,7 +23,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
|
||||
await nodeRef.click('title')
|
||||
}
|
||||
|
||||
test.describe('Node Help', () => {
|
||||
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Node search box', () => {
|
||||
test.describe('Node search box', { tag: '@node' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
|
||||
@@ -46,14 +46,14 @@ test.describe('Node search box', () => {
|
||||
await expect(comfyPage.searchBox.input).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
|
||||
})
|
||||
|
||||
test('Can auto link node', async ({ comfyPage }) => {
|
||||
test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
// Select the second item as the first item is always reroute
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
|
||||
@@ -62,41 +62,47 @@ test.describe('Node search box', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
|
||||
})
|
||||
|
||||
test('Can auto link batch moved node', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
test(
|
||||
'Can auto link batch moved node',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('links/batch_move_links')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
}
|
||||
const emptySpacePos = {
|
||||
x: 5,
|
||||
y: 5
|
||||
}
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
// Select the second item as the first item is always reroute
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
|
||||
suggestionIndex: 0
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'auto-linked-node-batch.png'
|
||||
)
|
||||
}
|
||||
const emptySpacePos = {
|
||||
x: 5,
|
||||
y: 5
|
||||
)
|
||||
|
||||
test(
|
||||
'Link release connecting to node with no slots',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await comfyPage.page.locator('.p-chip-remove-icon').click()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'added-node-no-connection.png'
|
||||
)
|
||||
}
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
// Select the second item as the first item is always reroute
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
|
||||
suggestionIndex: 0
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'auto-linked-node-batch.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Link release connecting to node with no slots', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await comfyPage.page.locator('.p-chip-remove-icon').click()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'added-node-no-connection.png'
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
|
||||
const node = 'Load Checkpoint'
|
||||
@@ -251,40 +257,45 @@ test.describe('Node search box', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Release context menu', () => {
|
||||
test.describe('Release context menu', { tag: '@node' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
|
||||
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
|
||||
})
|
||||
|
||||
test('Can trigger on link release', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
const contextMenu = comfyPage.page.locator('.litecontextmenu')
|
||||
// Wait for context menu with correct title (slot name | slot type)
|
||||
// The title shows the output slot name and type from the disconnected link
|
||||
await expect(contextMenu.locator('.litemenu-title')).toContainText(
|
||||
'CLIP | CLIP'
|
||||
)
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'link-release-context-menu.png'
|
||||
)
|
||||
})
|
||||
test(
|
||||
'Can trigger on link release',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
const contextMenu = comfyPage.page.locator('.litecontextmenu')
|
||||
// Wait for context menu with correct title (slot name | slot type)
|
||||
// The title shows the output slot name and type from the disconnected link
|
||||
await expect(contextMenu.locator('.litemenu-title')).toContainText(
|
||||
'CLIP | CLIP'
|
||||
)
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'link-release-context-menu.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Can search and add node from context menu', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyMouse.move({ x: 10, y: 10 })
|
||||
await comfyPage.clickContextMenuItem('Search')
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'link-context-menu-search.png'
|
||||
)
|
||||
})
|
||||
test(
|
||||
'Can search and add node from context menu',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage, comfyMouse }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyMouse.move({ x: 10, y: 10 })
|
||||
await comfyPage.clickContextMenuItem('Search')
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'link-context-menu-search.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -6,8 +6,8 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Note Node', () => {
|
||||
test('Can load node nodes', async ({ comfyPage }) => {
|
||||
test.describe('Note Node', { tag: '@node' }, () => {
|
||||
test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/note_nodes')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Primitive Node', () => {
|
||||
test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
|
||||
test('Can load with correct size', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('primitive/primitive_node')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', () => {
|
||||
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
|
||||
test('should add a record audio node and take a screenshot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Remote COMBO Widget', () => {
|
||||
test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
|
||||
const addRemoteWidgetNode = async (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Reroute Node', () => {
|
||||
test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
@@ -38,92 +38,96 @@ test.describe('Reroute Node', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('LiteGraph Native Reroute Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
|
||||
})
|
||||
test.describe(
|
||||
'LiteGraph Native Reroute Node',
|
||||
{ tag: ['@screenshot', '@node'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
|
||||
})
|
||||
|
||||
test('loads from workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('reroute/native_reroute')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
|
||||
})
|
||||
test('loads from workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('reroute/native_reroute')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
|
||||
})
|
||||
|
||||
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = (
|
||||
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
|
||||
)[0]
|
||||
const clipEncodeNode = (
|
||||
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
||||
)[0]
|
||||
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = (
|
||||
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
|
||||
)[0]
|
||||
const clipEncodeNode = (
|
||||
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
||||
)[0]
|
||||
|
||||
const slot1 = await loadCheckpointNode.getOutput(1)
|
||||
const slot2 = await clipEncodeNode.getInput(0)
|
||||
const middlePoint = getMiddlePoint(
|
||||
await slot1.getPosition(),
|
||||
await slot2.getPosition()
|
||||
)
|
||||
const slot1 = await loadCheckpointNode.getOutput(1)
|
||||
const slot2 = await clipEncodeNode.getInput(0)
|
||||
const middlePoint = getMiddlePoint(
|
||||
await slot1.getPosition(),
|
||||
await slot2.getPosition()
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_alt_click.png'
|
||||
)
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_alt_click.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can add reroute by clicking middle of link context menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = (
|
||||
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
|
||||
)[0]
|
||||
const clipEncodeNode = (
|
||||
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
||||
)[0]
|
||||
test('Can add reroute by clicking middle of link context menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = (
|
||||
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
|
||||
)[0]
|
||||
const clipEncodeNode = (
|
||||
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
||||
)[0]
|
||||
|
||||
const slot1 = await loadCheckpointNode.getOutput(1)
|
||||
const slot2 = await clipEncodeNode.getInput(0)
|
||||
const middlePoint = getMiddlePoint(
|
||||
await slot1.getPosition(),
|
||||
await slot2.getPosition()
|
||||
)
|
||||
const slot1 = await loadCheckpointNode.getOutput(1)
|
||||
const slot2 = await clipEncodeNode.getInput(0)
|
||||
const middlePoint = getMiddlePoint(
|
||||
await slot1.getPosition(),
|
||||
await slot2.getPosition()
|
||||
)
|
||||
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
|
||||
.click()
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_context_menu.png'
|
||||
)
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_context_menu.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can delete link that is connected to two reroutes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
|
||||
await comfyPage.loadWorkflow(
|
||||
'reroute/single-native-reroute-default-workflow'
|
||||
)
|
||||
test('Can delete link that is connected to two reroutes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
|
||||
await comfyPage.loadWorkflow(
|
||||
'reroute/single-native-reroute-default-workflow'
|
||||
)
|
||||
|
||||
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
|
||||
// since the link is a bezier curve and not a straight line.
|
||||
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
|
||||
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
|
||||
// since the link is a bezier curve and not a straight line.
|
||||
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
|
||||
|
||||
// Click the middle point of the link to open the context menu.
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
// Click the middle point of the link to open the context menu.
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
|
||||
// Click the "Delete" context menu option.
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
|
||||
.click()
|
||||
// Click the "Delete" context menu option.
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_delete_from_midpoint_context_menu.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_delete_from_midpoint_context_menu.png'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,43 +7,49 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Canvas Right Click Menu', () => {
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Node').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('loaders').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('Load VAE').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
|
||||
})
|
||||
test.describe(
|
||||
'Canvas Right Click Menu',
|
||||
{ tag: ['@screenshot', '@ui'] },
|
||||
() => {
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Node').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('loaders').click()
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.getByText('Load VAE').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
|
||||
})
|
||||
|
||||
test('Can add group', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Group', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
||||
})
|
||||
test('Can add group', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
|
||||
await comfyPage.page.getByText('Add Group', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'add-group-group-added.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
|
||||
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
|
||||
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-group-node.png'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Node Right Click Menu', () => {
|
||||
test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
test('Can open properties panel', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
|
||||
@@ -10,7 +10,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
const BLUE_COLOR = 'rgb(51, 51, 85)'
|
||||
const RED_COLOR = 'rgb(85, 51, 51)'
|
||||
|
||||
test.describe('Selection Toolbox', () => {
|
||||
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
})
|
||||
|
||||
@@ -7,178 +7,190 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Selection Toolbox - More Options Submenus', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
const openMoreOptions = async (comfyPage: ComfyPage) => {
|
||||
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
|
||||
// Drag the KSampler to the center of the screen
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
const viewportSize = comfyPage.page.viewportSize()
|
||||
const centerX = viewportSize.width / 3
|
||||
const centerY = viewportSize.height / 2
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: nodePos.x, y: nodePos.y },
|
||||
{ x: centerX, y: centerY }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await ksamplerNodes[0].click('title')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisible = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisibleAfterClick) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Could not open More Options menu - popover not showing')
|
||||
}
|
||||
|
||||
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
|
||||
exact: true
|
||||
})
|
||||
await expect(nodeInfoButton).toBeVisible()
|
||||
await nodeInfoButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialShape = await nodeRef.getProperty<number>('shape')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).hover()
|
||||
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
await comfyPage.page.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newShape = await nodeRef.getProperty<number>('shape')
|
||||
expect(newShape).not.toBe(initialShape)
|
||||
expect(newShape).toBe(1)
|
||||
})
|
||||
|
||||
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
expect(newColor).toBe('#223')
|
||||
if (initialColor) {
|
||||
expect(newColor).not.toBe(initialColor)
|
||||
}
|
||||
})
|
||||
|
||||
test('renames a node using Rename action', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page
|
||||
.getByText('Rename', { exact: true })
|
||||
.click({ force: true })
|
||||
const input = comfyPage.page.locator(
|
||||
'.group-title-editor.node-title-editor .editable-text input'
|
||||
)
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill('RenamedNode')
|
||||
await input.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
const newTitle = await nodeRef.getProperty<string>('title')
|
||||
expect(newTitle).toBe('RenamedNode')
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking outside', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
|
||||
await expect(renameItem).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
|
||||
for (let i = 0; i < 30; i++) {
|
||||
test.describe(
|
||||
'Selection Toolbox - More Options Submenus',
|
||||
{ tag: '@ui' },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
const openMoreOptions = async (comfyPage: ComfyPage) => {
|
||||
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
|
||||
// Drag the KSampler to the center of the screen
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
const viewportSize = comfyPage.page.viewportSize()
|
||||
const centerX = viewportSize.width / 3
|
||||
const centerY = viewportSize.height / 2
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: nodePos.x, y: nodePos.y },
|
||||
{ x: centerX, y: centerY }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await ksamplerNodes[0].click('title')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisible = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisibleAfterClick) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Could not open More Options menu - popover not showing')
|
||||
}
|
||||
|
||||
await comfyPage.page
|
||||
.locator('#graph-canvas')
|
||||
.click({ position: { x: 0, y: 50 }, force: true })
|
||||
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
|
||||
exact: true
|
||||
})
|
||||
await expect(nodeInfoButton).toBeVisible()
|
||||
await nodeInfoButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialShape = await nodeRef.getProperty<number>('shape')
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).hover()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Box', { exact: true })
|
||||
).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
await comfyPage.page.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const btn = document.querySelector('[data-testid="more-options-button"]')
|
||||
if (btn) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1
|
||||
})
|
||||
btn.dispatchEvent(event)
|
||||
const newShape = await nodeRef.getProperty<number>('shape')
|
||||
expect(newShape).not.toBe(initialShape)
|
||||
expect(newShape).toBe(1)
|
||||
})
|
||||
|
||||
test('changes node color via Color submenu swatch', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialColor = await nodeRef.getProperty<string | undefined>(
|
||||
'color'
|
||||
)
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
expect(newColor).toBe('#223')
|
||||
if (initialColor) {
|
||||
expect(newColor).not.toBe(initialColor)
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
test('renames a node using Rename action', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page
|
||||
.getByText('Rename', { exact: true })
|
||||
.click({ force: true })
|
||||
const input = comfyPage.page.locator(
|
||||
'.group-title-editor.node-title-editor .editable-text input'
|
||||
)
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill('RenamedNode')
|
||||
await input.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
const newTitle = await nodeRef.getProperty<string>('title')
|
||||
expect(newTitle).toBe('RenamedNode')
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking outside', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
|
||||
await expect(renameItem).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
await comfyPage.page
|
||||
.locator('#graph-canvas')
|
||||
.click({ position: { x: 0, y: 50 }, force: true })
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const btn = document.querySelector(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
if (btn) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1
|
||||
})
|
||||
btn.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ const SELECTORS = {
|
||||
promptDialog: '.graphdialog input'
|
||||
} as const
|
||||
|
||||
test.describe('Subgraph Slot Rename Dialog', () => {
|
||||
test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ const SELECTORS = {
|
||||
domWidget: '.comfy-multiline-input'
|
||||
} as const
|
||||
|
||||
test.describe('Subgraph Operations', () => {
|
||||
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ async function checkTemplateFileExists(
|
||||
return response.ok()
|
||||
}
|
||||
|
||||
test.describe('Templates', () => {
|
||||
test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
|
||||
@@ -207,109 +207,114 @@ test.describe('Templates', () => {
|
||||
await expect(nav).toBeVisible() // Nav should be visible at tablet size
|
||||
})
|
||||
|
||||
test('template cards descriptions adjust height dynamically', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Setup test by intercepting templates response to inject cards with varying description lengths
|
||||
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
|
||||
const response = [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Test Templates',
|
||||
type: 'image',
|
||||
templates: [
|
||||
test(
|
||||
'template cards descriptions adjust height dynamically',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
// Setup test by intercepting templates response to inject cards with varying description lengths
|
||||
await comfyPage.page.route(
|
||||
'**/templates/index.json',
|
||||
async (route, _) => {
|
||||
const response = [
|
||||
{
|
||||
name: 'short-description',
|
||||
title: 'Short Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'This is a short description.'
|
||||
},
|
||||
{
|
||||
name: 'medium-description',
|
||||
title: 'Medium Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'This is a medium length description that should take up two lines on most displays.'
|
||||
},
|
||||
{
|
||||
name: 'long-description',
|
||||
title: 'Long Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
|
||||
moduleName: 'default',
|
||||
title: 'Test Templates',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'short-description',
|
||||
title: 'Short Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'This is a short description.'
|
||||
},
|
||||
{
|
||||
name: 'medium-description',
|
||||
title: 'Medium Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'This is a medium length description that should take up two lines on most displays.'
|
||||
},
|
||||
{
|
||||
name: 'long-description',
|
||||
title: 'Long Description',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(response),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(response),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
)
|
||||
|
||||
// Mock the thumbnail images to avoid 404s
|
||||
await comfyPage.page.route('**/templates/**.webp', async (route) => {
|
||||
const headers = {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
path: 'browser_tests/assets/example.webp',
|
||||
headers
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Mock the thumbnail images to avoid 404s
|
||||
await comfyPage.page.route('**/templates/**.webp', async (route) => {
|
||||
const headers = {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
path: 'browser_tests/assets/example.webp',
|
||||
headers
|
||||
})
|
||||
})
|
||||
// Open templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
// Open templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
// Wait for cards to load
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Wait for cards to load
|
||||
await expect(
|
||||
comfyPage.page.locator(
|
||||
// Verify all three cards with different descriptions are visible
|
||||
const shortDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
const mediumDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-medium-description"]'
|
||||
)
|
||||
const longDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-long-description"]'
|
||||
)
|
||||
|
||||
// Verify all three cards with different descriptions are visible
|
||||
const shortDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
const mediumDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-medium-description"]'
|
||||
)
|
||||
const longDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-long-description"]'
|
||||
)
|
||||
await expect(shortDescCard).toBeVisible()
|
||||
await expect(mediumDescCard).toBeVisible()
|
||||
await expect(longDescCard).toBeVisible()
|
||||
|
||||
await expect(shortDescCard).toBeVisible()
|
||||
await expect(mediumDescCard).toBeVisible()
|
||||
await expect(longDescCard).toBeVisible()
|
||||
// Verify descriptions are visible and have line-clamp class
|
||||
// The description is in a p tag with text-muted class
|
||||
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
|
||||
|
||||
// Verify descriptions are visible and have line-clamp class
|
||||
// The description is in a p tag with text-muted class
|
||||
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
|
||||
await expect(shortDesc).toContainText('short description')
|
||||
await expect(mediumDesc).toContainText('medium length description')
|
||||
await expect(longDesc).toContainText('much longer description')
|
||||
|
||||
await expect(shortDesc).toContainText('short description')
|
||||
await expect(mediumDesc).toContainText('medium length description')
|
||||
await expect(longDesc).toContainText('much longer description')
|
||||
|
||||
// Verify grid layout maintains consistency
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot(
|
||||
'template-grid-varying-content.png'
|
||||
)
|
||||
})
|
||||
// Verify grid layout maintains consistency
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot(
|
||||
'template-grid-varying-content.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Settings Search functionality', () => {
|
||||
test.describe('Settings Search functionality', { tag: '@settings' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Register test settings to verify hidden/deprecated filtering
|
||||
await comfyPage.page.evaluate(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { userSelectPageFixture as test } from '../fixtures/UserSelectPage'
|
||||
/**
|
||||
* Expects ComfyUI backend to be launched with `--multi-user` flag.
|
||||
*/
|
||||
test.describe('User Select View', () => {
|
||||
test.describe('User Select View', { tag: '@settings' }, () => {
|
||||
test.beforeEach(async ({ userSelectPage, page }) => {
|
||||
await page.goto(userSelectPage.url)
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
|
||||
import type { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Version Mismatch Warnings', () => {
|
||||
test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
|
||||
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
|
||||
const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Viewport', () => {
|
||||
test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => {
|
||||
test('Fits view to nodes when saved viewport position is offscreen', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
const CREATE_GROUP_HOTKEY = 'Control+g'
|
||||
|
||||
test.describe('Vue Node Groups', () => {
|
||||
test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)
|
||||
|
||||
@@ -9,10 +9,14 @@ test.describe('Vue Nodes Canvas Pan', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('@mobile Can pan with touch', async ({ comfyPage }) => {
|
||||
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-paned-with-touch.png'
|
||||
)
|
||||
})
|
||||
test(
|
||||
'@mobile Can pan with touch',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-paned-with-touch.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -10,25 +10,30 @@ test.describe('Vue Nodes Zoom', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should not capture drag while zooming with ctrl+shift+drag', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBoundingBox = await checkpointNode.boundingBox()
|
||||
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
|
||||
test(
|
||||
'should not capture drag while zooming with ctrl+shift+drag',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBoundingBox = await checkpointNode.boundingBox()
|
||||
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
|
||||
|
||||
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
|
||||
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
|
||||
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
|
||||
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
|
||||
|
||||
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
|
||||
// the node. The node should not capture the drag while drag-zooming.
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: 200, y: 300 },
|
||||
{ x: nodeMidpointX, y: nodeMidpointY }
|
||||
)
|
||||
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
|
||||
// the node. The node should not capture the drag while drag-zooming.
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: 200, y: 300 },
|
||||
{ x: nodeMidpointX, y: nodeMidpointY }
|
||||
)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'zoomed-in-ctrl-shift.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -98,7 +98,7 @@ async function connectSlots(
|
||||
await nextFrame()
|
||||
}
|
||||
|
||||
test.describe('Vue Node Link Interaction', () => {
|
||||
test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
test.describe('Vue Node Bring to Front', () => {
|
||||
test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import type { Position } from '../../../../fixtures/types'
|
||||
|
||||
test.describe('Vue Node Moving', () => {
|
||||
@@ -29,39 +29,47 @@ test.describe('Vue Node Moving', () => {
|
||||
expect(diffY).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
|
||||
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
|
||||
x: 256,
|
||||
y: 256
|
||||
})
|
||||
test(
|
||||
'should allow moving nodes by dragging',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
const loadCheckpointHeaderPos =
|
||||
await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
|
||||
x: 256,
|
||||
y: 256
|
||||
})
|
||||
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
|
||||
}
|
||||
)
|
||||
|
||||
test('@mobile should allow moving nodes by dragging on touch devices', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Disable minimap (gets in way of the node on small screens)
|
||||
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
|
||||
test(
|
||||
'@mobile should allow moving nodes by dragging on touch devices',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
// Disable minimap (gets in way of the node on small screens)
|
||||
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
|
||||
|
||||
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.panWithTouch(
|
||||
{
|
||||
x: 64,
|
||||
y: 64
|
||||
},
|
||||
loadCheckpointHeaderPos
|
||||
)
|
||||
const loadCheckpointHeaderPos =
|
||||
await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.panWithTouch(
|
||||
{
|
||||
x: 64,
|
||||
y: 64
|
||||
},
|
||||
loadCheckpointHeaderPos
|
||||
)
|
||||
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-moved-node-touch.png'
|
||||
)
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-moved-node-touch.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -15,22 +15,25 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
test(
|
||||
'should allow toggling bypass on a selected node with hotkey',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
}
|
||||
)
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Custom Colors', () => {
|
||||
test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
|
||||
@@ -12,19 +12,24 @@ test.describe('Vue Node Mute', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling mute on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
test(
|
||||
'should allow toggling mute on a selected node with hotkey',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png')
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-muted-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
|
||||
})
|
||||
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
|
||||
}
|
||||
)
|
||||
|
||||
test('should allow toggling mute on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -9,13 +9,17 @@ test.describe('Vue Upload Widgets', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('widgets/all_load_widgets')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
test(
|
||||
'should hide canvas-only upload buttons',
|
||||
{ tag: '@screenshot' },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('widgets/all_load_widgets')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-upload-widgets.png'
|
||||
)
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-nodes-upload-widgets.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Combo text widget', () => {
|
||||
test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Truncates text when resized', async ({ comfyPage }) => {
|
||||
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
@@ -79,7 +79,7 @@ test.describe('Combo text widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Boolean widget', () => {
|
||||
test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can toggle', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/boolean_widget')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
|
||||
@@ -92,7 +92,7 @@ test.describe('Boolean widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Slider widget', () => {
|
||||
test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can drag adjust value', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('inputs/simple_slider')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
@@ -113,7 +113,7 @@ test.describe('Slider widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Number widget', () => {
|
||||
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can drag adjust value', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/seed_widget')
|
||||
|
||||
@@ -134,22 +134,28 @@ test.describe('Number widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Dynamic widget manipulation', () => {
|
||||
test('Auto expand node when widget is added dynamically', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
test.describe(
|
||||
'Dynamic widget manipulation',
|
||||
{ tag: ['@screenshot', '@widget'] },
|
||||
() => {
|
||||
test('Auto expand node when widget is added dynamically', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
|
||||
window['graph'].setDirtyCanvas(true, true)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
|
||||
window['graph'].setDirtyCanvas(true, true)
|
||||
})
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'ksampler_widget_added.png'
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Image widget', () => {
|
||||
test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can load image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_image_widget')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
|
||||
@@ -236,99 +242,103 @@ test.describe('Image widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Animated image widget', () => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
|
||||
test.skip('Shows preview of uploaded animated image', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
test.describe(
|
||||
'Animated image widget',
|
||||
{ tag: ['@screenshot', '@widget'] },
|
||||
() => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
|
||||
test.skip('Shows preview of uploaded animated image', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
|
||||
// Expect the image preview to change automatically
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped.png'
|
||||
)
|
||||
|
||||
// Move mouse and click on canvas to trigger render
|
||||
await comfyPage.page.mouse.click(64, 64)
|
||||
|
||||
// Expect the image preview to change to the next frame of the animation
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped_next_frame.png'
|
||||
)
|
||||
})
|
||||
|
||||
// Expect the image preview to change automatically
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped.png'
|
||||
)
|
||||
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
|
||||
// Move mouse and click on canvas to trigger render
|
||||
await comfyPage.page.mouse.click(64, 64)
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Expect the image preview to change to the next frame of the animation
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_drag_and_dropped_next_frame.png'
|
||||
)
|
||||
})
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y },
|
||||
waitForUpload: true
|
||||
})
|
||||
|
||||
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const nodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = nodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y },
|
||||
waitForUpload: true
|
||||
// Expect the filename combo value to be updated
|
||||
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
|
||||
const filename = await fileComboWidget.getValue()
|
||||
expect(filename).toContain('animated_webp.webp')
|
||||
})
|
||||
|
||||
// Expect the filename combo value to be updated
|
||||
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
|
||||
const filename = await fileComboWidget.getValue()
|
||||
expect(filename).toContain('animated_webp.webp')
|
||||
})
|
||||
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/save_animated_webp')
|
||||
|
||||
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/save_animated_webp')
|
||||
// Get position of the load animated webp node
|
||||
const loadNodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = loadNodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
|
||||
// Get position of the load animated webp node
|
||||
const loadNodes = await comfyPage.getNodeRefsByType(
|
||||
'DevToolsLoadAnimatedImageTest'
|
||||
)
|
||||
const loadAnimatedWebpNode = loadNodes[0]
|
||||
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Drag and drop image file onto the load animated webp node
|
||||
await comfyPage.dragAndDropFile('animated_webp.webp', {
|
||||
dropPosition: { x, y }
|
||||
// Get the SaveAnimatedWEBP node
|
||||
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
|
||||
const saveAnimatedWebpNode = saveNodes[0]
|
||||
if (!saveAnimatedWebpNode)
|
||||
throw new Error('SaveAnimatedWEBP node not found')
|
||||
|
||||
// Simulate the graph executing
|
||||
await comfyPage.page.evaluate(
|
||||
([loadId, saveId]) => {
|
||||
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
|
||||
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
|
||||
app.canvas.setDirty(true)
|
||||
},
|
||||
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
||||
)
|
||||
await expect(
|
||||
comfyPage.page.locator('.dom-widget').locator('img')
|
||||
).toHaveCount(2)
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
)
|
||||
|
||||
// Get the SaveAnimatedWEBP node
|
||||
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
|
||||
const saveAnimatedWebpNode = saveNodes[0]
|
||||
if (!saveAnimatedWebpNode)
|
||||
throw new Error('SaveAnimatedWEBP node not found')
|
||||
|
||||
// Simulate the graph executing
|
||||
await comfyPage.page.evaluate(
|
||||
([loadId, saveId]) => {
|
||||
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
|
||||
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
|
||||
app.canvas.setDirty(true)
|
||||
},
|
||||
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
||||
)
|
||||
await expect(
|
||||
comfyPage.page.locator('.dom-widget').locator('img')
|
||||
).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load audio widget', () => {
|
||||
test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can load audio', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/load_audio_widget')
|
||||
// Wait for the audio widget to be rendered in the DOM
|
||||
@@ -338,7 +348,7 @@ test.describe('Load audio widget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Unserialized widgets', () => {
|
||||
test.describe('Unserialized widgets', { tag: '@widget' }, () => {
|
||||
test('Unserialized widgets values do not mark graph as modified', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Workflow Tab Thumbnails', () => {
|
||||
test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.39.2",
|
||||
"version": "1.39.3",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.8"
|
||||
"vite": "^8.0.0-beta.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
567
pnpm-lock.yaml
generated
567
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -62,8 +62,8 @@ catalog:
|
||||
happy-dom: ^20.0.11
|
||||
husky: ^9.1.7
|
||||
jiti: 2.6.1
|
||||
jsonata: ^2.1.0
|
||||
jsdom: ^27.4.0
|
||||
jsonata: ^2.1.0
|
||||
knip: ^5.75.1
|
||||
lint-staged: ^16.2.7
|
||||
markdown-table: ^3.0.4
|
||||
@@ -92,7 +92,7 @@ catalog:
|
||||
unplugin-icons: ^22.5.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^30.0.0
|
||||
vite: ^8.0.0-beta.8
|
||||
vite: 8.0.0-beta.11
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
vite-plugin-vue-devtools: ^8.0.0
|
||||
|
||||
@@ -46,6 +46,8 @@ onMounted(() => {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
// Handle preload errors that occur during dynamic imports (e.g., stale chunks after deployment)
|
||||
// See: https://vite.dev/guide/build#load-error-handling
|
||||
window.addEventListener('vite:preloadError', (event) => {
|
||||
event.preventDefault()
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
@@ -772,7 +772,7 @@ useIntersectionObserver(loadTrigger, () => {
|
||||
// Reset pagination when filters change
|
||||
watch(
|
||||
[
|
||||
searchQuery,
|
||||
filteredTemplates,
|
||||
selectedNavItem,
|
||||
sortBy,
|
||||
selectedModels,
|
||||
|
||||
@@ -150,13 +150,11 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
@@ -265,18 +263,15 @@ function cancelEdit() {
|
||||
}
|
||||
|
||||
async function saveKeybinding() {
|
||||
if (currentEditingCommand.value && newBindingKeyCombo.value) {
|
||||
const updated = keybindingStore.updateKeybindingOnCommand(
|
||||
new KeybindingImpl({
|
||||
commandId: currentEditingCommand.value.id,
|
||||
combo: newBindingKeyCombo.value
|
||||
})
|
||||
)
|
||||
if (updated) {
|
||||
await keybindingService.persistUserKeybindings()
|
||||
}
|
||||
}
|
||||
const commandId = currentEditingCommand.value?.id
|
||||
const combo = newBindingKeyCombo.value
|
||||
cancelEdit()
|
||||
if (!combo || commandId == undefined) return
|
||||
|
||||
const updated = keybindingStore.updateKeybindingOnCommand(
|
||||
new KeybindingImpl({ commandId, combo })
|
||||
)
|
||||
if (updated) await keybindingService.persistUserKeybindings()
|
||||
}
|
||||
|
||||
async function resetKeybinding(commandData: ICommandData) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { KeyComboImpl } from '@/stores/keybindingStore'
|
||||
import type { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
|
||||
const { keyCombo, isModified = false } = defineProps<{
|
||||
keyCombo: KeyComboImpl
|
||||
|
||||
@@ -70,7 +70,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Active Jobs Grid -->
|
||||
<div
|
||||
v-if="activeJobItems.length"
|
||||
v-if="isQueuePanelV2Enabled && activeJobItems.length"
|
||||
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
|
||||
:style="gridStyle"
|
||||
>
|
||||
@@ -65,6 +65,7 @@ import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const {
|
||||
assets,
|
||||
@@ -90,6 +91,11 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
|
||||
type AssetGridItem = { key: string; asset: AssetItem }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div
|
||||
v-if="activeJobItems.length"
|
||||
v-if="isQueuePanelV2Enabled && activeJobItems.length"
|
||||
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
|
||||
>
|
||||
<AssetsListItem
|
||||
@@ -133,6 +133,7 @@ import {
|
||||
} from '@/utils/formatUtil'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const {
|
||||
assets,
|
||||
@@ -154,6 +155,11 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredAssetId = ref<string | null>(null)
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ describe('useSelectedLiteGraphItems', () => {
|
||||
expect(selectedNodes).toContainEqual(subNode2)
|
||||
})
|
||||
|
||||
it('toggleSelectedNodesMode should apply unified state to subgraph children', () => {
|
||||
it('toggleSelectedNodesMode should not apply state to subgraph children', () => {
|
||||
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
|
||||
const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
|
||||
const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode
|
||||
@@ -294,9 +294,8 @@ describe('useSelectedLiteGraphItems', () => {
|
||||
// regularNode: BYPASS -> NEVER (since BYPASS != NEVER)
|
||||
expect(regularNode.mode).toBe(LGraphEventMode.NEVER)
|
||||
|
||||
// Subgraph children get unified state (same as their parent):
|
||||
// Both children should now be NEVER, regardless of their previous states
|
||||
expect(subNode1.mode).toBe(LGraphEventMode.NEVER) // was ALWAYS, now NEVER
|
||||
// Subgraph children do not change state
|
||||
expect(subNode1.mode).toBe(LGraphEventMode.ALWAYS) // was ALWAYS, stays ALWAYS
|
||||
expect(subNode2.mode).toBe(LGraphEventMode.NEVER) // was NEVER, stays NEVER
|
||||
})
|
||||
|
||||
@@ -317,9 +316,9 @@ describe('useSelectedLiteGraphItems', () => {
|
||||
// Selected subgraph should toggle to ALWAYS (since it was already NEVER)
|
||||
expect(subgraphNode.mode).toBe(LGraphEventMode.ALWAYS)
|
||||
|
||||
// All children should also get ALWAYS (unified with parent's new state)
|
||||
// All children should be unchanged
|
||||
expect(subNode1.mode).toBe(LGraphEventMode.ALWAYS)
|
||||
expect(subNode2.mode).toBe(LGraphEventMode.ALWAYS)
|
||||
expect(subNode2.mode).toBe(LGraphEventMode.BYPASS)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import {
|
||||
collectFromNodes,
|
||||
traverseNodesDepthFirst
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { collectFromNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling selected LiteGraph items filtering and operations.
|
||||
@@ -97,16 +94,10 @@ export function useSelectedLiteGraphItems() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the execution mode of all selected nodes with unified subgraph behavior.
|
||||
* Toggle the execution mode of all selected nodes
|
||||
*
|
||||
* Top-level behavior (selected nodes): Standard toggle logic
|
||||
* - If the selected node is already in the specified mode → set to ALWAYS
|
||||
* - Otherwise → set to the specified mode
|
||||
*
|
||||
* Subgraph behavior (children of selected subgraph nodes): Unified state application
|
||||
* - All children inherit the same mode that their parent subgraph node was set to
|
||||
* - This creates predictable behavior: if you toggle a subgraph to "mute",
|
||||
* ALL nodes inside become muted, regardless of their previous individual states
|
||||
* - If any nodes are not already the specified node mode → all are set to specified mode
|
||||
* - Otherwise → set all nodes to ALWAYS
|
||||
*
|
||||
* @param mode - The LGraphEventMode to toggle to (e.g., NEVER for mute, BYPASS for bypass)
|
||||
*/
|
||||
@@ -124,27 +115,8 @@ export function useSelectedLiteGraphItems() {
|
||||
)
|
||||
const newModeForSelectedNode = allNodesMatch ? LGraphEventMode.ALWAYS : mode
|
||||
|
||||
// Process each selected node independently to determine its target state and apply to children
|
||||
selectedNodeArray.forEach((selectedNode) => {
|
||||
// Apply standard toggle logic to the selected node itself
|
||||
|
||||
for (const selectedNode of selectedNodeArray)
|
||||
selectedNode.mode = newModeForSelectedNode
|
||||
|
||||
// If this selected node is a subgraph, apply the same mode uniformly to all its children
|
||||
// This ensures predictable behavior: all children get the same state as their parent
|
||||
if (selectedNode.isSubgraphNode?.() && selectedNode.subgraph) {
|
||||
traverseNodesDepthFirst([selectedNode], {
|
||||
visitor: (node) => {
|
||||
// Skip the parent node since we already handled it above
|
||||
if (node === selectedNode) return undefined
|
||||
|
||||
// Apply the parent's new mode to all children uniformly
|
||||
node.mode = newModeForSelectedNode
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -862,7 +862,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
userEmail: userEmail.value,
|
||||
userId: resolvedUserInfo.value?.id
|
||||
})
|
||||
window.open(supportUrl, '_blank')
|
||||
window.open(supportUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { refThrottled, watchDebounced } from '@vueuse/core'
|
||||
import { refDebounced, watchDebounced } from '@vueuse/core'
|
||||
import Fuse from 'fuse.js'
|
||||
import type { IFuseOptions } from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
@@ -119,7 +119,7 @@ export function useTemplateFiltering(
|
||||
)
|
||||
})
|
||||
|
||||
const debouncedSearchQuery = refThrottled(searchQuery, 50)
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 150)
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
|
||||
@@ -557,7 +557,7 @@ function withComfyAutogrow(node: LGraphNode): asserts node is AutogrowNode {
|
||||
if (!autogrowGroup) return
|
||||
if (app.configuringGraph && input.widget)
|
||||
ensureWidgetForInput(node, input)
|
||||
if (iscon && linf) {
|
||||
if (iscon) {
|
||||
if (swappingConnection || !linf) return
|
||||
autogrowInputConnected(slot, this)
|
||||
} else {
|
||||
|
||||
@@ -292,10 +292,10 @@ export class GroupNodeConfig {
|
||||
this.processNode(node, seenInputs, seenOutputs)
|
||||
}
|
||||
|
||||
for (const p of this.#convertedToProcess) {
|
||||
for (const p of this._convertedToProcess) {
|
||||
p()
|
||||
}
|
||||
this.#convertedToProcess = []
|
||||
this._convertedToProcess = []
|
||||
if (!this.nodeDef) return
|
||||
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
|
||||
useNodeDefStore().addNodeDef(this.nodeDef)
|
||||
@@ -773,7 +773,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#convertedToProcess: (() => void)[] = []
|
||||
private _convertedToProcess: (() => void)[] = []
|
||||
processNodeInputs(
|
||||
node: GroupNodeData,
|
||||
seenInputs: Record<string, number>,
|
||||
@@ -804,7 +804,7 @@ export class GroupNodeConfig {
|
||||
)
|
||||
|
||||
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
|
||||
this.#convertedToProcess.push(() =>
|
||||
this._convertedToProcess.push(() =>
|
||||
this.processConvertedWidgets(
|
||||
inputs,
|
||||
node,
|
||||
|
||||
@@ -13,7 +13,9 @@ import './imageCompare'
|
||||
import './imageCrop'
|
||||
import './load3d'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
if (!isCloud) {
|
||||
await import('./nodeTemplates')
|
||||
}
|
||||
import './noteNode'
|
||||
import './previewAny'
|
||||
import './rerouteNode'
|
||||
|
||||
@@ -103,7 +103,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
override onAfterGraphConfigured() {
|
||||
if (this.outputs[0].links?.length && !this.widgets?.length) {
|
||||
this.#onFirstConnection()
|
||||
this._onFirstConnection()
|
||||
|
||||
// Populate widget values from config data
|
||||
if (this.widgets && this.widgets_values) {
|
||||
@@ -116,7 +116,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
// Merge values if required
|
||||
this.#mergeWidgetConfig()
|
||||
this._mergeWidgetConfig()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,11 +133,11 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const links = this.outputs[0].links
|
||||
if (connected) {
|
||||
if (links?.length && !this.widgets?.length) {
|
||||
this.#onFirstConnection()
|
||||
this._onFirstConnection()
|
||||
}
|
||||
} else {
|
||||
// We may have removed a link that caused the constraints to change
|
||||
this.#mergeWidgetConfig()
|
||||
this._mergeWidgetConfig()
|
||||
|
||||
if (!links?.length) {
|
||||
this.onLastDisconnect()
|
||||
@@ -159,7 +159,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
if (this.outputs[slot].links?.length) {
|
||||
const valid = this.#isValidConnection(input)
|
||||
const valid = this._isValidConnection(input)
|
||||
if (valid) {
|
||||
// On connect of additional outputs, copy our value to their widget
|
||||
this.applyToGraph([{ target_id: target_node.id, target_slot } as LLink])
|
||||
@@ -170,7 +170,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
return true
|
||||
}
|
||||
|
||||
#onFirstConnection(recreating?: boolean) {
|
||||
private _onFirstConnection(recreating?: boolean) {
|
||||
// First connection can fire before the graph is ready on initial load so random things can be missing
|
||||
if (!this.outputs[0].links || !this.graph) {
|
||||
this.onLastDisconnect()
|
||||
@@ -204,7 +204,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
this.outputs[0].name = type
|
||||
this.outputs[0].widget = widget
|
||||
|
||||
this.#createWidget(
|
||||
this._createWidget(
|
||||
widget[CONFIG] ?? config,
|
||||
theirNode,
|
||||
widget.name,
|
||||
@@ -213,7 +213,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
)
|
||||
}
|
||||
|
||||
#createWidget(
|
||||
private _createWidget(
|
||||
inputData: InputSpec,
|
||||
node: LGraphNode,
|
||||
widgetName: string,
|
||||
@@ -307,8 +307,8 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
recreateWidget() {
|
||||
const values = this.widgets?.map((w) => w.value)
|
||||
this.#removeWidgets()
|
||||
this.#onFirstConnection(true)
|
||||
this._removeWidgets()
|
||||
this._onFirstConnection(true)
|
||||
if (values?.length && this.widgets) {
|
||||
for (let i = 0; i < this.widgets.length; i++)
|
||||
this.widgets[i].value = values[i]
|
||||
@@ -316,7 +316,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
return this.widgets?.[0]
|
||||
}
|
||||
|
||||
#mergeWidgetConfig() {
|
||||
private _mergeWidgetConfig() {
|
||||
// Merge widget configs if the node has multiple outputs
|
||||
const output = this.outputs[0]
|
||||
const links = output.links ?? []
|
||||
@@ -348,11 +348,11 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const theirInput = theirNode.inputs[link.target_slot]
|
||||
|
||||
// Call is valid connection so it can merge the configs when validating
|
||||
this.#isValidConnection(theirInput, hasConfig)
|
||||
this._isValidConnection(theirInput, hasConfig)
|
||||
}
|
||||
}
|
||||
|
||||
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
|
||||
private _isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
|
||||
// Only allow connections where the configs match
|
||||
const output = this.outputs?.[0]
|
||||
const config2 = (input.widget?.[GET_CONFIG] as () => InputSpec)?.()
|
||||
@@ -367,7 +367,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
)
|
||||
}
|
||||
|
||||
#removeWidgets() {
|
||||
private _removeWidgets() {
|
||||
if (this.widgets) {
|
||||
// Allow widgets to cleanup
|
||||
for (const w of this.widgets) {
|
||||
@@ -398,7 +398,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
this.outputs[0].name = 'connect to widget input'
|
||||
delete this.outputs[0].widget
|
||||
|
||||
this.#removeWidgets()
|
||||
this._removeWidgets()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@ export class CanvasPointer {
|
||||
|
||||
/** Maximum offset from click location */
|
||||
static get maxClickDrift() {
|
||||
return this.#maxClickDrift
|
||||
return this._maxClickDrift
|
||||
}
|
||||
|
||||
static set maxClickDrift(value) {
|
||||
this.#maxClickDrift = value
|
||||
this.#maxClickDrift2 = value * value
|
||||
this._maxClickDrift = value
|
||||
this._maxClickDrift2 = value * value
|
||||
}
|
||||
|
||||
static #maxClickDrift = 6
|
||||
private static _maxClickDrift = 6
|
||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||
private static _maxClickDrift2 = this._maxClickDrift ** 2
|
||||
|
||||
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
|
||||
static trackpadThreshold = 60
|
||||
@@ -153,18 +153,18 @@ export class CanvasPointer {
|
||||
* Therefore, simply setting this value twice will execute the first callback.
|
||||
*/
|
||||
get finally() {
|
||||
return this.#finally
|
||||
return this._finally
|
||||
}
|
||||
|
||||
set finally(value) {
|
||||
try {
|
||||
this.#finally?.()
|
||||
this._finally?.()
|
||||
} finally {
|
||||
this.#finally = value
|
||||
this._finally = value
|
||||
}
|
||||
}
|
||||
|
||||
#finally?: () => unknown
|
||||
private _finally?: () => unknown
|
||||
|
||||
constructor(element: Element) {
|
||||
this.element = element
|
||||
@@ -197,7 +197,7 @@ export class CanvasPointer {
|
||||
|
||||
// Primary button released - treat as pointerup.
|
||||
if (!(e.buttons & eDown.buttons)) {
|
||||
this.#completeClick(e)
|
||||
this._completeClick(e)
|
||||
this.reset()
|
||||
return
|
||||
}
|
||||
@@ -209,8 +209,8 @@ export class CanvasPointer {
|
||||
|
||||
const longerThanBufferTime =
|
||||
e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
|
||||
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
|
||||
this.#setDragStarted(e)
|
||||
if (longerThanBufferTime || !this._hasSamePosition(e, eDown)) {
|
||||
this._setDragStarted(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,13 +221,13 @@ export class CanvasPointer {
|
||||
up(e: CanvasPointerEvent): boolean {
|
||||
if (e.button !== this.eDown?.button) return false
|
||||
|
||||
this.#completeClick(e)
|
||||
this._completeClick(e)
|
||||
const { dragStarted } = this
|
||||
this.reset()
|
||||
return !dragStarted
|
||||
}
|
||||
|
||||
#completeClick(e: CanvasPointerEvent): void {
|
||||
private _completeClick(e: CanvasPointerEvent): void {
|
||||
const { eDown } = this
|
||||
if (!eDown) return
|
||||
|
||||
@@ -236,11 +236,11 @@ export class CanvasPointer {
|
||||
if (this.dragStarted) {
|
||||
// A move event already started drag
|
||||
this.onDragEnd?.(e)
|
||||
} else if (!this.#hasSamePosition(e, eDown)) {
|
||||
} else if (!this._hasSamePosition(e, eDown)) {
|
||||
// Teleport without a move event (e.g. tab out, move, tab back)
|
||||
this.#setDragStarted()
|
||||
this._setDragStarted()
|
||||
this.onDragEnd?.(e)
|
||||
} else if (this.onDoubleClick && this.#isDoubleClick()) {
|
||||
} else if (this.onDoubleClick && this._isDoubleClick()) {
|
||||
// Double-click event
|
||||
this.onDoubleClick(e)
|
||||
this.eLastDown = undefined
|
||||
@@ -258,10 +258,10 @@ export class CanvasPointer {
|
||||
* @param tolerance2 The maximum distance (squared) before the positions are considered different
|
||||
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
|
||||
*/
|
||||
#hasSamePosition(
|
||||
private _hasSamePosition(
|
||||
a: PointerEvent,
|
||||
b: PointerEvent,
|
||||
tolerance2 = CanvasPointer.#maxClickDrift2
|
||||
tolerance2 = CanvasPointer._maxClickDrift2
|
||||
): boolean {
|
||||
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
|
||||
return drift <= tolerance2
|
||||
@@ -271,21 +271,21 @@ export class CanvasPointer {
|
||||
* Checks whether the pointer is currently past the max click drift threshold.
|
||||
* @returns `true` if the latest pointer event is past the the click drift threshold
|
||||
*/
|
||||
#isDoubleClick(): boolean {
|
||||
private _isDoubleClick(): boolean {
|
||||
const { eDown, eLastDown } = this
|
||||
if (!eDown || !eLastDown) return false
|
||||
|
||||
// Use thrice the drift distance for double-click gap
|
||||
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
|
||||
const tolerance2 = (3 * CanvasPointer._maxClickDrift) ** 2
|
||||
const diff = eDown.timeStamp - eLastDown.timeStamp
|
||||
return (
|
||||
diff > 0 &&
|
||||
diff < CanvasPointer.doubleClickTime &&
|
||||
this.#hasSamePosition(eDown, eLastDown, tolerance2)
|
||||
this._hasSamePosition(eDown, eLastDown, tolerance2)
|
||||
)
|
||||
}
|
||||
|
||||
#setDragStarted(eMove?: CanvasPointerEvent): void {
|
||||
private _setDragStarted(eMove?: CanvasPointerEvent): void {
|
||||
this.dragStarted = true
|
||||
this.onDragStart?.(this, eMove)
|
||||
delete this.onDragStart
|
||||
@@ -303,14 +303,14 @@ export class CanvasPointer {
|
||||
const timeSinceLastEvent = Math.max(0, now - this.lastWheelEventTime)
|
||||
this.lastWheelEventTime = now
|
||||
|
||||
if (this.#isHighResWheelEvent(e, now)) {
|
||||
if (this._isHighResWheelEvent(e, now)) {
|
||||
this.detectedDevice = 'mouse'
|
||||
} else if (this.#isWithinCooldown(timeSinceLastEvent)) {
|
||||
if (this.#shouldBufferLinuxEvent(e)) {
|
||||
this.#bufferLinuxEvent(e, now)
|
||||
} else if (this._isWithinCooldown(timeSinceLastEvent)) {
|
||||
if (this._shouldBufferLinuxEvent(e)) {
|
||||
this._bufferLinuxEvent(e, now)
|
||||
}
|
||||
} else {
|
||||
this.#updateDeviceMode(e, now)
|
||||
this._updateDeviceMode(e, now)
|
||||
this.hasReceivedWheelEvent = true
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ export class CanvasPointer {
|
||||
* Validates buffered high res wheel events and switches to mouse mode if pattern matches.
|
||||
* @returns `true` if switched to mouse mode
|
||||
*/
|
||||
#isHighResWheelEvent(event: WheelEvent, now: number): boolean {
|
||||
private _isHighResWheelEvent(event: WheelEvent, now: number): boolean {
|
||||
if (!this.bufferedLinuxEvent || this.bufferedLinuxEventTime <= 0) {
|
||||
return false
|
||||
}
|
||||
@@ -329,15 +329,15 @@ export class CanvasPointer {
|
||||
const timeSinceBuffer = now - this.bufferedLinuxEventTime
|
||||
|
||||
if (timeSinceBuffer > CanvasPointer.maxHighResBufferTime) {
|
||||
this.#clearLinuxBuffer()
|
||||
this._clearLinuxBuffer()
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
event.deltaX === 0 &&
|
||||
this.#isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
|
||||
this._isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
|
||||
) {
|
||||
this.#clearLinuxBuffer()
|
||||
this._clearLinuxBuffer()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ export class CanvasPointer {
|
||||
/**
|
||||
* Checks if we're within the cooldown period where mode switching is disabled.
|
||||
*/
|
||||
#isWithinCooldown(timeSinceLastEvent: number): boolean {
|
||||
private _isWithinCooldown(timeSinceLastEvent: number): boolean {
|
||||
const isFirstEvent = !this.hasReceivedWheelEvent
|
||||
const cooldownExpired = timeSinceLastEvent >= CanvasPointer.trackpadMaxGap
|
||||
return !isFirstEvent && !cooldownExpired
|
||||
@@ -356,23 +356,23 @@ export class CanvasPointer {
|
||||
/**
|
||||
* Updates the device mode based on event patterns.
|
||||
*/
|
||||
#updateDeviceMode(event: WheelEvent, now: number): void {
|
||||
if (this.#isTrackpadPattern(event)) {
|
||||
private _updateDeviceMode(event: WheelEvent, now: number): void {
|
||||
if (this._isTrackpadPattern(event)) {
|
||||
this.detectedDevice = 'trackpad'
|
||||
} else if (this.#isMousePattern(event)) {
|
||||
} else if (this._isMousePattern(event)) {
|
||||
this.detectedDevice = 'mouse'
|
||||
} else if (
|
||||
this.detectedDevice === 'trackpad' &&
|
||||
this.#shouldBufferLinuxEvent(event)
|
||||
this._shouldBufferLinuxEvent(event)
|
||||
) {
|
||||
this.#bufferLinuxEvent(event, now)
|
||||
this._bufferLinuxEvent(event, now)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the buffered Linux wheel event and associated timer.
|
||||
*/
|
||||
#clearLinuxBuffer(): void {
|
||||
private _clearLinuxBuffer(): void {
|
||||
this.bufferedLinuxEvent = undefined
|
||||
this.bufferedLinuxEventTime = 0
|
||||
if (this.linuxBufferTimeoutId !== undefined) {
|
||||
@@ -385,7 +385,7 @@ export class CanvasPointer {
|
||||
* Checks if the event matches trackpad input patterns.
|
||||
* @param event The wheel event to check
|
||||
*/
|
||||
#isTrackpadPattern(event: WheelEvent): boolean {
|
||||
private _isTrackpadPattern(event: WheelEvent): boolean {
|
||||
// Two-finger panning: non-zero deltaX AND deltaY
|
||||
if (event.deltaX !== 0 && event.deltaY !== 0) return true
|
||||
|
||||
@@ -399,7 +399,7 @@ export class CanvasPointer {
|
||||
* Checks if the event matches mouse wheel input patterns.
|
||||
* @param event The wheel event to check
|
||||
*/
|
||||
#isMousePattern(event: WheelEvent): boolean {
|
||||
private _isMousePattern(event: WheelEvent): boolean {
|
||||
const absoluteDeltaY = Math.abs(event.deltaY)
|
||||
|
||||
// Primary threshold for switching from trackpad to mouse
|
||||
@@ -417,7 +417,7 @@ export class CanvasPointer {
|
||||
* Checks if the event should be buffered as a potential Linux wheel event.
|
||||
* @param event The wheel event to check
|
||||
*/
|
||||
#shouldBufferLinuxEvent(event: WheelEvent): boolean {
|
||||
private _shouldBufferLinuxEvent(event: WheelEvent): boolean {
|
||||
const absoluteDeltaY = Math.abs(event.deltaY)
|
||||
const isInLinuxRange = absoluteDeltaY >= 10 && absoluteDeltaY < 60
|
||||
const isVerticalOnly = event.deltaX === 0
|
||||
@@ -436,7 +436,7 @@ export class CanvasPointer {
|
||||
* @param event The event to buffer
|
||||
* @param now The current timestamp
|
||||
*/
|
||||
#bufferLinuxEvent(event: WheelEvent, now: number): void {
|
||||
private _bufferLinuxEvent(event: WheelEvent, now: number): void {
|
||||
if (this.linuxBufferTimeoutId !== undefined) {
|
||||
clearTimeout(this.linuxBufferTimeoutId)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ export class CanvasPointer {
|
||||
|
||||
// Set timeout to clear buffer after 10ms
|
||||
this.linuxBufferTimeoutId = setTimeout(() => {
|
||||
this.#clearLinuxBuffer()
|
||||
this._clearLinuxBuffer()
|
||||
}, CanvasPointer.maxHighResBufferTime)
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ export class CanvasPointer {
|
||||
* @param deltaY1 The first deltaY value
|
||||
* @param deltaY2 The second deltaY value
|
||||
*/
|
||||
#isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
|
||||
private _isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
|
||||
const absolute1 = Math.abs(deltaY1)
|
||||
const absolute2 = Math.abs(deltaY2)
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DragAndScale {
|
||||
* Returns `true` if the current state has changed from the previous state.
|
||||
* @returns `true` if the current state has changed from the previous state, otherwise `false`.
|
||||
*/
|
||||
#stateHasChanged(): boolean {
|
||||
private _stateHasChanged(): boolean {
|
||||
const current = this.state
|
||||
const previous = this.lastState
|
||||
|
||||
@@ -95,7 +95,7 @@ export class DragAndScale {
|
||||
computeVisibleArea(viewport: Rect | undefined): void {
|
||||
const { scale, offset, visible_area } = this
|
||||
|
||||
if (this.#stateHasChanged()) {
|
||||
if (this._stateHasChanged()) {
|
||||
this.onChanged?.(scale, offset)
|
||||
copyState(this.state, this.lastState)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraphData,
|
||||
createTestSubgraphNode
|
||||
} from './subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
@@ -206,6 +210,70 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Subgraph Definition Garbage Collection', () => {
|
||||
function createSubgraphWithNodes(rootGraph: LGraph, nodeCount: number) {
|
||||
const subgraph = rootGraph.createSubgraph(createTestSubgraphData())
|
||||
|
||||
const innerNodes: LGraphNode[] = []
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
const node = new LGraphNode(`Inner Node ${i}`)
|
||||
subgraph.add(node)
|
||||
innerNodes.push(node)
|
||||
}
|
||||
|
||||
return { subgraph, innerNodes }
|
||||
}
|
||||
|
||||
it('removing SubgraphNode fires onRemoved for inner nodes', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const { subgraph, innerNodes } = createSubgraphWithNodes(rootGraph, 2)
|
||||
const removedNodeIds = new Set<string>()
|
||||
|
||||
for (const node of innerNodes) {
|
||||
node.onRemoved = () => removedNodeIds.add(String(node.id))
|
||||
}
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
expect(subgraph.nodes.length).toBe(2)
|
||||
|
||||
rootGraph.remove(subgraphNode)
|
||||
|
||||
expect(removedNodeIds.size).toBe(2)
|
||||
})
|
||||
|
||||
it('removing SubgraphNode fires onNodeRemoved callback', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const { subgraph } = createSubgraphWithNodes(rootGraph, 2)
|
||||
const graphRemovedNodeIds = new Set<string>()
|
||||
|
||||
subgraph.onNodeRemoved = (node) => graphRemovedNodeIds.add(String(node.id))
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
rootGraph.remove(subgraphNode)
|
||||
|
||||
expect(graphRemovedNodeIds.size).toBe(2)
|
||||
})
|
||||
|
||||
it('subgraph definition is removed when SubgraphNode is removed', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const { subgraph } = createSubgraphWithNodes(rootGraph, 1)
|
||||
const subgraphId = subgraph.id
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
expect(rootGraph.subgraphs.has(subgraphId)).toBe(true)
|
||||
|
||||
rootGraph.remove(subgraphNode)
|
||||
|
||||
expect(rootGraph.subgraphs.has(subgraphId)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Legacy LGraph Compatibility Layer', () => {
|
||||
test('can be extended via prototype', ({ expect, minimalGraph }) => {
|
||||
// @ts-expect-error Should always be an error.
|
||||
|
||||
@@ -244,7 +244,7 @@ export class LGraph
|
||||
}
|
||||
|
||||
/** Internal only. Not required for serialisation; calculated on deserialise. */
|
||||
#lastFloatingLinkId: number = 0
|
||||
private _lastFloatingLinkId: number = 0
|
||||
|
||||
private readonly floatingLinksInternal: Map<LinkId, LLink> = new Map()
|
||||
get floatingLinks(): ReadonlyMap<LinkId, LLink> {
|
||||
@@ -365,7 +365,7 @@ export class LGraph
|
||||
this.reroutes.clear()
|
||||
this.floatingLinksInternal.clear()
|
||||
|
||||
this.#lastFloatingLinkId = 0
|
||||
this._lastFloatingLinkId = 0
|
||||
|
||||
// other scene stuff
|
||||
this._groups = []
|
||||
@@ -992,6 +992,16 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
// Subgraph cleanup (use local const to avoid type narrowing affecting node.graph assignment)
|
||||
const subgraphNode = node.isSubgraphNode() ? node : null
|
||||
if (subgraphNode) {
|
||||
for (const innerNode of subgraphNode.subgraph.nodes) {
|
||||
innerNode.onRemoved?.()
|
||||
subgraphNode.subgraph.onNodeRemoved?.(innerNode)
|
||||
}
|
||||
this.rootGraph.subgraphs.delete(subgraphNode.subgraph.id)
|
||||
}
|
||||
|
||||
// callback
|
||||
node.onRemoved?.()
|
||||
|
||||
@@ -1294,7 +1304,7 @@ export class LGraph
|
||||
|
||||
addFloatingLink(link: LLink): LLink {
|
||||
if (link.id === -1) {
|
||||
link.id = ++this.#lastFloatingLinkId
|
||||
link.id = ++this._lastFloatingLinkId
|
||||
}
|
||||
this.floatingLinksInternal.set(link.id, link)
|
||||
|
||||
@@ -2165,8 +2175,16 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom JSON serialization to prevent circular reference errors.
|
||||
* Called automatically by JSON.stringify().
|
||||
*/
|
||||
toJSON(): ISerialisedGraph {
|
||||
return this.serialize()
|
||||
}
|
||||
|
||||
/** @returns The drag and scale state of the first attached canvas, otherwise `undefined`. */
|
||||
#getDragAndScale(): DragAndScaleState | undefined {
|
||||
private _getDragAndScale(): DragAndScaleState | undefined {
|
||||
const ds = this.list_of_graphcanvas?.at(0)?.ds
|
||||
if (ds) return { scale: ds.scale, offset: ds.offset }
|
||||
}
|
||||
@@ -2206,7 +2224,7 @@ export class LGraph
|
||||
|
||||
// Save scale and offset
|
||||
const extra = { ...this.extra }
|
||||
if (LiteGraph.saveViewportWithGraph) extra.ds = this.#getDragAndScale()
|
||||
if (LiteGraph.saveViewportWithGraph) extra.ds = this._getDragAndScale()
|
||||
if (!extra.ds) delete extra.ds
|
||||
|
||||
const data: ReturnType<typeof this.asSerialisable> = {
|
||||
@@ -2396,8 +2414,8 @@ export class LGraph
|
||||
const floatingLink = LLink.create(linkData)
|
||||
this.addFloatingLink(floatingLink)
|
||||
|
||||
if (floatingLink.id > this.#lastFloatingLinkId)
|
||||
this.#lastFloatingLinkId = floatingLink.id
|
||||
if (floatingLink.id > this._lastFloatingLinkId)
|
||||
this._lastFloatingLinkId = floatingLink.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2448,13 +2466,13 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
#canvas?: LGraphCanvas
|
||||
private _canvas?: LGraphCanvas
|
||||
get primaryCanvas(): LGraphCanvas | undefined {
|
||||
return this.rootGraph.#canvas
|
||||
return this.rootGraph._canvas
|
||||
}
|
||||
|
||||
set primaryCanvas(canvas: LGraphCanvas) {
|
||||
this.rootGraph.#canvas = canvas
|
||||
this.rootGraph._canvas = canvas
|
||||
}
|
||||
|
||||
load(url: string | Blob | URL | File, callback: () => void) {
|
||||
@@ -2523,9 +2541,9 @@ export class Subgraph
|
||||
/** A list of node widgets displayed in the parent graph, on the subgraph object. */
|
||||
readonly widgets: ExposedWidget[] = []
|
||||
|
||||
#rootGraph: LGraph
|
||||
private _rootGraph: LGraph
|
||||
override get rootGraph(): LGraph {
|
||||
return this.#rootGraph
|
||||
return this._rootGraph
|
||||
}
|
||||
|
||||
constructor(rootGraph: LGraph, data: ExportedSubgraph) {
|
||||
@@ -2533,11 +2551,11 @@ export class Subgraph
|
||||
|
||||
super()
|
||||
|
||||
this.#rootGraph = rootGraph
|
||||
this._rootGraph = rootGraph
|
||||
|
||||
const cloned = structuredClone(data)
|
||||
this._configureBase(cloned)
|
||||
this.#configureSubgraph(cloned)
|
||||
this._configureSubgraph(cloned)
|
||||
}
|
||||
|
||||
getIoNodeOnPos(
|
||||
@@ -2549,7 +2567,7 @@ export class Subgraph
|
||||
if (outputNode.containsPoint([x, y])) return outputNode
|
||||
}
|
||||
|
||||
#configureSubgraph(
|
||||
private _configureSubgraph(
|
||||
data:
|
||||
| (ISerialisedGraph & ExportedSubgraph)
|
||||
| (SerialisableGraph & ExportedSubgraph)
|
||||
@@ -2593,7 +2611,7 @@ export class Subgraph
|
||||
): boolean | undefined {
|
||||
const r = super.configure(data, keep_old)
|
||||
|
||||
this.#configureSubgraph(data)
|
||||
this._configureSubgraph(data)
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
@@ -316,17 +316,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
selectionChanged: false
|
||||
}
|
||||
|
||||
#subgraph?: Subgraph
|
||||
private _subgraph?: Subgraph
|
||||
get subgraph(): Subgraph | undefined {
|
||||
return this.#subgraph
|
||||
return this._subgraph
|
||||
}
|
||||
|
||||
set subgraph(value: Subgraph | undefined) {
|
||||
if (value !== this.#subgraph) {
|
||||
this.#subgraph = value
|
||||
if (value !== this._subgraph) {
|
||||
this._subgraph = value
|
||||
if (value)
|
||||
this.dispatch('litegraph:set-graph', {
|
||||
oldGraph: this.#subgraph,
|
||||
oldGraph: this._subgraph,
|
||||
newGraph: value
|
||||
})
|
||||
}
|
||||
@@ -361,7 +361,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.canvas.dispatchEvent(new CustomEvent(type, { detail }))
|
||||
}
|
||||
|
||||
#updateCursorStyle() {
|
||||
private _updateCursorStyle() {
|
||||
if (!this.state.shouldSetCursor) return
|
||||
|
||||
const crosshairItems =
|
||||
@@ -398,7 +398,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
set read_only(value: boolean) {
|
||||
this.state.readOnly = value
|
||||
this.#updateCursorStyle()
|
||||
this._updateCursorStyle()
|
||||
}
|
||||
|
||||
get isDragging(): boolean {
|
||||
@@ -415,7 +415,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
set hoveringOver(value: CanvasItem) {
|
||||
this.state.hoveringOver = value
|
||||
this.#updateCursorStyle()
|
||||
this._updateCursorStyle()
|
||||
}
|
||||
|
||||
/** @deprecated Replace all references with {@link pointer}.{@link CanvasPointer.isDown isDown}. */
|
||||
@@ -435,7 +435,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
set dragging_canvas(value: boolean) {
|
||||
this.state.draggingCanvas = value
|
||||
this.#updateCursorStyle()
|
||||
this._updateCursorStyle()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,16 +450,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
|
||||
}
|
||||
|
||||
#maximumFrameGap = 0
|
||||
private _maximumFrameGap = 0
|
||||
/** Maximum frames per second to render. 0: unlimited. Default: 0 */
|
||||
public get maximumFps() {
|
||||
return this.#maximumFrameGap > Number.EPSILON
|
||||
? this.#maximumFrameGap / 1000
|
||||
return this._maximumFrameGap > Number.EPSILON
|
||||
? this._maximumFrameGap / 1000
|
||||
: 0
|
||||
}
|
||||
|
||||
public set maximumFps(value) {
|
||||
this.#maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
|
||||
this._maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,12 +660,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* The IDs of the nodes that are currently visible on the canvas. More
|
||||
* performant than {@link visible_nodes} for visibility checks.
|
||||
*/
|
||||
#visible_node_ids: Set<NodeId> = new Set()
|
||||
private _visible_node_ids: Set<NodeId> = new Set()
|
||||
node_over?: LGraphNode
|
||||
node_capturing_input?: LGraphNode | null
|
||||
highlighted_links: Dictionary<boolean> = {}
|
||||
|
||||
#visibleReroutes: Set<Reroute> = new Set()
|
||||
private _visibleReroutes: Set<Reroute> = new Set()
|
||||
|
||||
dirty_canvas: boolean = true
|
||||
dirty_bgcanvas: boolean = true
|
||||
@@ -725,9 +725,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
NODEPANEL_IS_OPEN?: boolean
|
||||
|
||||
/** Once per frame check of snap to grid value. @todo Update on change. */
|
||||
#snapToGrid?: number
|
||||
private _snapToGrid?: number
|
||||
/** Set on keydown, keyup. @todo */
|
||||
#shiftDown: boolean = false
|
||||
private _shiftDown: boolean = false
|
||||
|
||||
/** Link rendering adapter for litegraph-to-canvas integration */
|
||||
linkRenderer: LitegraphLinkAdapter | null = null
|
||||
@@ -735,7 +735,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
/** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */
|
||||
dragZoomEnabled: boolean = false
|
||||
/** The start position of the drag zoom and original read-only state. */
|
||||
#dragZoomStart: { pos: Point; scale: number; readOnly: boolean } | null = null
|
||||
private _dragZoomStart: {
|
||||
pos: Point
|
||||
scale: number
|
||||
readOnly: boolean
|
||||
} | null = null
|
||||
|
||||
/** If true, enable live selection during drag. Nodes are selected/deselected in real-time. */
|
||||
liveSelection: boolean = false
|
||||
@@ -810,7 +814,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
this.linkConnector.events.addEventListener('link-created', () =>
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
)
|
||||
|
||||
// @deprecated Workaround: Keep until connecting_links is removed.
|
||||
@@ -1808,7 +1812,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
this.dragging_canvas = false
|
||||
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
this.dirty_area = null
|
||||
|
||||
this.node_in_panel = null
|
||||
@@ -1836,7 +1840,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.linkRenderer = new LitegraphLinkAdapter(false)
|
||||
|
||||
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
}
|
||||
|
||||
openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
|
||||
@@ -1873,7 +1877,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @returns The canvas element
|
||||
* @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element
|
||||
*/
|
||||
#validateCanvas(
|
||||
private _validateCanvas(
|
||||
canvas: string | HTMLCanvasElement
|
||||
): HTMLCanvasElement & { data?: LGraphCanvas } {
|
||||
if (typeof canvas === 'string') {
|
||||
@@ -1892,7 +1896,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param skip_events If true, events on the previous canvas will not be removed. Has no effect on the first invocation.
|
||||
*/
|
||||
setCanvas(canvas: string | HTMLCanvasElement, skip_events?: boolean) {
|
||||
const element = this.#validateCanvas(canvas)
|
||||
const element = this._validateCanvas(canvas)
|
||||
if (element === this.canvas) return
|
||||
// maybe detach events from old_canvas
|
||||
if (!element && this.canvas && !skip_events) this.unbindEvents()
|
||||
@@ -1905,7 +1909,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
// TODO: classList.add
|
||||
element.className += ' lgraphcanvas'
|
||||
element.data = this
|
||||
Object.defineProperty(element, 'data', {
|
||||
value: this,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false
|
||||
})
|
||||
|
||||
// Background canvas: To render objects behind nodes (background, links, groups)
|
||||
this.bgcanvas = document.createElement('canvas')
|
||||
@@ -2026,12 +2035,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
/** Marks the entire canvas as dirty. */
|
||||
#dirty(): void {
|
||||
private _dirty(): void {
|
||||
this.dirty_canvas = true
|
||||
this.dirty_bgcanvas = true
|
||||
}
|
||||
|
||||
#linkConnectorDrop(): void {
|
||||
private _linkConnectorDrop(): void {
|
||||
const { graph, linkConnector, pointer } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
@@ -2070,10 +2079,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
const window = this.getCanvasWindow()
|
||||
if (this.is_rendering) {
|
||||
if (this.#maximumFrameGap > 0) {
|
||||
if (this._maximumFrameGap > 0) {
|
||||
// Manual FPS limit
|
||||
const gap =
|
||||
this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
|
||||
this._maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
|
||||
setTimeout(renderFrame.bind(this), Math.max(1, gap))
|
||||
} else {
|
||||
// FPS limited by refresh rate
|
||||
@@ -2161,7 +2170,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
!e.altKey &&
|
||||
e.buttons
|
||||
) {
|
||||
this.#dragZoomStart = {
|
||||
this._dragZoomStart = {
|
||||
pos: [e.x, e.y],
|
||||
scale: this.ds.scale,
|
||||
readOnly: this.read_only
|
||||
@@ -2208,9 +2217,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
// left button mouse / single finger
|
||||
if (e.button === 0 && !pointer.isDouble) {
|
||||
this.#processPrimaryButton(e, node)
|
||||
this._processPrimaryButton(e, node)
|
||||
} else if (e.button === 1) {
|
||||
this.#processMiddleButton(e, node)
|
||||
this._processMiddleButton(e, node)
|
||||
} else if (
|
||||
(e.button === 2 || pointer.isDouble) &&
|
||||
this.allow_interaction &&
|
||||
@@ -2246,7 +2255,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
reroute = graph.getRerouteOnPos(
|
||||
e.canvasX,
|
||||
e.canvasY,
|
||||
this.#visibleReroutes
|
||||
this._visibleReroutes
|
||||
)
|
||||
}
|
||||
if (reroute) {
|
||||
@@ -2302,18 +2311,24 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param y The y coordinate in canvas space
|
||||
* @returns The positionable item or undefined
|
||||
*/
|
||||
#getPositionableOnPos(x: number, y: number): Positionable | undefined {
|
||||
private _getPositionableOnPos(
|
||||
x: number,
|
||||
y: number
|
||||
): Positionable | undefined {
|
||||
const ioNode = this.subgraph?.getIoNodeOnPos(x, y)
|
||||
if (ioNode) return ioNode
|
||||
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
for (const reroute of this._visibleReroutes) {
|
||||
if (reroute.containsPoint([x, y])) return reroute
|
||||
}
|
||||
|
||||
return this.graph?.getGroupTitlebarOnPos(x, y)
|
||||
}
|
||||
|
||||
#processPrimaryButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
|
||||
private _processPrimaryButton(
|
||||
e: CanvasPointerEvent,
|
||||
node: LGraphNode | undefined
|
||||
) {
|
||||
const { pointer, graph, linkConnector, subgraph } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
@@ -2329,7 +2344,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
!e.altKey &&
|
||||
LiteGraph.leftMouseClickBehavior === 'panning'
|
||||
) {
|
||||
this.#setupNodeSelectionDrag(e, pointer, node)
|
||||
this._setupNodeSelectionDrag(e, pointer, node)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -2360,16 +2375,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
if (this.allow_dragnodes) {
|
||||
pointer.onDragStart = (pointer) => {
|
||||
this.#startDraggingItems(cloned, pointer)
|
||||
this._startDraggingItems(cloned, pointer)
|
||||
}
|
||||
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
|
||||
pointer.onDragEnd = (e) => this._processDraggedItems(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Node clicked
|
||||
if (node && (this.allow_interaction || node.flags.allow_interaction)) {
|
||||
this.#processNodeClick(e, ctrlOrMeta, node)
|
||||
this._processNodeClick(e, ctrlOrMeta, node)
|
||||
} else {
|
||||
// Subgraph IO nodes
|
||||
if (subgraph) {
|
||||
@@ -2387,8 +2402,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
ioNode.onPointerDown(e, pointer, linkConnector)
|
||||
pointer.onClick ??= () => canvas.processSelect(ioNode, e)
|
||||
pointer.onDragStart ??= () =>
|
||||
canvas.#startDraggingItems(ioNode, pointer, true)
|
||||
pointer.onDragEnd ??= (eUp) => canvas.#processDraggedItems(eUp)
|
||||
canvas._startDraggingItems(ioNode, pointer, true)
|
||||
pointer.onDragEnd ??= (eUp) => canvas._processDraggedItems(eUp)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2404,7 +2419,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// Fallback to checking visible reroutes directly
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
for (const reroute of this._visibleReroutes) {
|
||||
const overReroute =
|
||||
foundReroute === reroute || reroute.containsPoint([x, y])
|
||||
if (!reroute.isSlotHovered && !overReroute) continue
|
||||
@@ -2413,19 +2428,19 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
pointer.onClick = () => this.processSelect(reroute, e)
|
||||
if (!e.shiftKey) {
|
||||
pointer.onDragStart = (pointer) =>
|
||||
this.#startDraggingItems(reroute, pointer, true)
|
||||
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
|
||||
this._startDraggingItems(reroute, pointer, true)
|
||||
pointer.onDragEnd = (e) => this._processDraggedItems(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (reroute.isOutputHovered || (overReroute && e.shiftKey)) {
|
||||
linkConnector.dragFromReroute(graph, reroute)
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
}
|
||||
|
||||
if (reroute.isInputHovered) {
|
||||
linkConnector.dragFromRerouteToOutput(graph, reroute)
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
}
|
||||
|
||||
reroute.hideSlots()
|
||||
@@ -2470,14 +2485,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
if (e.shiftKey && !e.altKey) {
|
||||
linkConnector.dragFromLinkSegment(graph, linkSegment)
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
|
||||
return
|
||||
} else if (e.altKey && !e.shiftKey) {
|
||||
const newReroute = graph.createReroute([x, y], linkSegment)
|
||||
pointer.onDragStart = (pointer) =>
|
||||
this.#startDraggingItems(newReroute, pointer)
|
||||
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
|
||||
this._startDraggingItems(newReroute, pointer)
|
||||
pointer.onDragEnd = (e) => this._processDraggedItems(e)
|
||||
return
|
||||
}
|
||||
} else if (
|
||||
@@ -2519,7 +2534,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
eMove.canvasY - group.pos[1] - offsetY
|
||||
]
|
||||
// Unless snapping.
|
||||
if (this.#snapToGrid) snapPoint(pos, this.#snapToGrid)
|
||||
if (this._snapToGrid) snapPoint(pos, this._snapToGrid)
|
||||
|
||||
const resized = group.resize(pos[0], pos[1])
|
||||
if (resized) this.dirty_bgcanvas = true
|
||||
@@ -2542,9 +2557,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
pointer.onClick = () => this.processSelect(group, e)
|
||||
pointer.onDragStart = (pointer) => {
|
||||
group.recomputeInsideNodes()
|
||||
this.#startDraggingItems(group, pointer, true)
|
||||
this._startDraggingItems(group, pointer, true)
|
||||
}
|
||||
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
|
||||
pointer.onDragEnd = (e) => this._processDraggedItems(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2582,12 +2597,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
pointer.finally = () => (this.dragging_canvas = false)
|
||||
this.dragging_canvas = true
|
||||
} else {
|
||||
this.#setupNodeSelectionDrag(e, pointer)
|
||||
this._setupNodeSelectionDrag(e, pointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setupNodeSelectionDrag(
|
||||
private _setupNodeSelectionDrag(
|
||||
e: CanvasPointerEvent,
|
||||
pointer: CanvasPointer,
|
||||
node?: LGraphNode | undefined
|
||||
@@ -2602,7 +2617,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
pointer.onClick = (eUp) => {
|
||||
// Click, not drag
|
||||
const clickedItem =
|
||||
node ?? this.#getPositionableOnPos(eUp.canvasX, eUp.canvasY)
|
||||
node ?? this._getPositionableOnPos(eUp.canvasX, eUp.canvasY)
|
||||
this.processSelect(clickedItem, eUp)
|
||||
}
|
||||
pointer.onDragStart = () => (this.dragging_rectangle = dragRect)
|
||||
@@ -2617,7 +2632,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
} else {
|
||||
// Classic mode: select only when drag ends
|
||||
pointer.onDragEnd = (upEvent) =>
|
||||
this.#handleMultiSelect(upEvent, dragRect)
|
||||
this._handleMultiSelect(upEvent, dragRect)
|
||||
}
|
||||
|
||||
pointer.finally = () => (this.dragging_rectangle = null)
|
||||
@@ -2629,7 +2644,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param ctrlOrMeta Ctrl or meta key is pressed
|
||||
* @param node The node to process a click event for
|
||||
*/
|
||||
#processNodeClick(
|
||||
private _processNodeClick(
|
||||
e: CanvasPointerEvent,
|
||||
ctrlOrMeta: boolean,
|
||||
node: LGraphNode
|
||||
@@ -2685,13 +2700,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// Drag multiple output links
|
||||
if (e.shiftKey && hasRelevantOutputLinks(output, graph)) {
|
||||
linkConnector.moveOutputLink(graph, output)
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
return
|
||||
}
|
||||
|
||||
// New output link
|
||||
linkConnector.dragNewFromOutput(graph, node, output)
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
|
||||
if (LiteGraph.shift_click_do_break_link_from) {
|
||||
if (e.shiftKey) {
|
||||
@@ -2744,7 +2759,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
linkConnector.dragNewFromInput(graph, node, input)
|
||||
}
|
||||
|
||||
this.#linkConnectorDrop()
|
||||
this._linkConnectorDrop()
|
||||
this.dirty_bgcanvas = true
|
||||
|
||||
return
|
||||
@@ -2874,7 +2889,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// Apply snapping to position changes
|
||||
if (this.#snapToGrid) {
|
||||
if (this._snapToGrid) {
|
||||
if (
|
||||
resizeDirection.includes('N') ||
|
||||
resizeDirection.includes('W')
|
||||
@@ -2882,7 +2897,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const originalX = newBounds.x
|
||||
const originalY = newBounds.y
|
||||
|
||||
snapPoint(newBounds.pos, this.#snapToGrid)
|
||||
snapPoint(newBounds.pos, this._snapToGrid)
|
||||
|
||||
// Adjust size to compensate for snapped position
|
||||
if (resizeDirection.includes('N')) {
|
||||
@@ -2893,7 +2908,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
}
|
||||
|
||||
snapPoint(newBounds.size, this.#snapToGrid)
|
||||
snapPoint(newBounds.size, this._snapToGrid)
|
||||
}
|
||||
|
||||
// Apply snapping to size changes
|
||||
@@ -2918,11 +2933,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
node.pos = newBounds.pos
|
||||
node.setSize(newBounds.size)
|
||||
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
}
|
||||
|
||||
pointer.onDragEnd = () => {
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
graph.afterChange(node)
|
||||
}
|
||||
pointer.finally = () => {
|
||||
@@ -2938,8 +2953,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
// Drag node
|
||||
pointer.onDragStart = (pointer) =>
|
||||
this.#startDraggingItems(node, pointer, true)
|
||||
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
|
||||
this._startDraggingItems(node, pointer, true)
|
||||
pointer.onDragEnd = (e) => this._processDraggedItems(e)
|
||||
}
|
||||
|
||||
this.dirty_canvas = true
|
||||
@@ -3008,7 +3023,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param e The pointerdown event
|
||||
* @param node The node to process a click event for
|
||||
*/
|
||||
#processMiddleButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
|
||||
private _processMiddleButton(
|
||||
e: CanvasPointerEvent,
|
||||
node: LGraphNode | undefined
|
||||
) {
|
||||
const { pointer } = this
|
||||
|
||||
if (
|
||||
@@ -3105,14 +3123,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
}
|
||||
|
||||
#processDragZoom(e: PointerEvent): void {
|
||||
private _processDragZoom(e: PointerEvent): void {
|
||||
// stop canvas zoom action
|
||||
if (!e.buttons) {
|
||||
this.#finishDragZoom()
|
||||
this._finishDragZoom()
|
||||
return
|
||||
}
|
||||
|
||||
const start = this.#dragZoomStart
|
||||
const start = this._dragZoomStart
|
||||
if (!start) throw new TypeError('Drag-zoom state object was null')
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
|
||||
@@ -3126,10 +3144,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.graph.change()
|
||||
}
|
||||
|
||||
#finishDragZoom(): void {
|
||||
const start = this.#dragZoomStart
|
||||
private _finishDragZoom(): void {
|
||||
const start = this._dragZoomStart
|
||||
if (!start) return
|
||||
this.#dragZoomStart = null
|
||||
this._dragZoomStart = null
|
||||
this.read_only = start.readOnly
|
||||
}
|
||||
|
||||
@@ -3141,9 +3159,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.dragZoomEnabled &&
|
||||
e.ctrlKey &&
|
||||
e.shiftKey &&
|
||||
this.#dragZoomStart
|
||||
this._dragZoomStart
|
||||
) {
|
||||
this.#processDragZoom(e)
|
||||
this._processDragZoom(e)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3210,7 +3228,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
} else if (this.dragging_canvas) {
|
||||
this.ds.offset[0] += delta[0] / this.ds.scale
|
||||
this.ds.offset[1] += delta[1] / this.ds.scale
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
} else if (
|
||||
(this.allow_interaction || node?.flags.allow_interaction) &&
|
||||
!this.read_only
|
||||
@@ -3258,7 +3276,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.node_over = node
|
||||
this.dirty_canvas = true
|
||||
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
for (const reroute of this._visibleReroutes) {
|
||||
reroute.hideSlots()
|
||||
this.dirty_bgcanvas = true
|
||||
}
|
||||
@@ -3382,10 +3400,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
} else {
|
||||
// Reroutes
|
||||
underPointer = this.#updateReroutes(underPointer)
|
||||
underPointer = this._updateReroutes(underPointer)
|
||||
|
||||
// Not over a node
|
||||
const segment = this.#getLinkCentreOnPos(e)
|
||||
const segment = this._getLinkCentreOnPos(e)
|
||||
if (this.over_link_center !== segment) {
|
||||
underPointer |= CanvasItem.Link
|
||||
this.over_link_center = segment
|
||||
@@ -3435,7 +3453,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
}
|
||||
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3449,14 +3467,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* Updates the hover / snap state of all visible reroutes.
|
||||
* @returns The original value of {@link underPointer}, with any found reroute items added.
|
||||
*/
|
||||
#updateReroutes(underPointer: CanvasItem): CanvasItem {
|
||||
private _updateReroutes(underPointer: CanvasItem): CanvasItem {
|
||||
const { graph, pointer, linkConnector } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
// Update reroute hover state
|
||||
if (!pointer.isDown) {
|
||||
let anyChanges = false
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
for (const reroute of this._visibleReroutes) {
|
||||
anyChanges ||= reroute.updateVisibility(this.graph_mouse)
|
||||
|
||||
if (reroute.isSlotHovered) underPointer |= CanvasItem.RerouteSlot
|
||||
@@ -3464,7 +3482,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (anyChanges) this.dirty_bgcanvas = true
|
||||
} else if (linkConnector.isConnecting) {
|
||||
// Highlight the reroute that the mouse is over
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
for (const reroute of this._visibleReroutes) {
|
||||
if (reroute.containsPoint(this.graph_mouse)) {
|
||||
if (linkConnector.isRerouteValidDrop(reroute)) {
|
||||
linkConnector.overReroute = reroute
|
||||
@@ -3489,7 +3507,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param pointer The pointer event that initiated the drag, e.g. pointerdown
|
||||
* @param sticky If `true`, the item is added to the selection - see {@link processSelect}
|
||||
*/
|
||||
#startDraggingItems(
|
||||
private _startDraggingItems(
|
||||
item: Positionable,
|
||||
pointer: CanvasPointer,
|
||||
sticky = false
|
||||
@@ -3511,7 +3529,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* Handles shared clean up and placement after items have been dragged.
|
||||
* @param e The event that completed the drag, e.g. pointerup, pointermove
|
||||
*/
|
||||
#processDraggedItems(e: CanvasPointerEvent): void {
|
||||
private _processDraggedItems(e: CanvasPointerEvent): void {
|
||||
const { graph } = this
|
||||
if (e.shiftKey || LiteGraph.alwaysSnapToGrid)
|
||||
graph?.snapToGrid(this.selectedItems)
|
||||
@@ -3533,7 +3551,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const { graph, pointer } = this
|
||||
if (!graph) return
|
||||
|
||||
this.#finishDragZoom()
|
||||
this._finishDragZoom()
|
||||
|
||||
LGraphCanvas.active_canvas = this
|
||||
|
||||
@@ -3677,7 +3695,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return
|
||||
}
|
||||
|
||||
#noItemsSelected(): void {
|
||||
private _noItemsSelected(): void {
|
||||
const event = new CustomEvent('litegraph:no-items-selected', {
|
||||
bubbles: true
|
||||
})
|
||||
@@ -3688,7 +3706,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* process a key event
|
||||
*/
|
||||
processKey(e: KeyboardEvent): void {
|
||||
this.#shiftDown = e.shiftKey
|
||||
this._shiftDown = e.shiftKey
|
||||
|
||||
const { graph } = this
|
||||
if (!graph) return
|
||||
@@ -3735,7 +3753,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// @ts-expect-error EventTarget.localName is not in standard types
|
||||
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
|
||||
if (this.selectedItems.size === 0) {
|
||||
this.#noItemsSelected()
|
||||
this._noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4097,7 +4115,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param dragRect The drag rectangle to normalize (modified in place)
|
||||
* @returns The normalized rectangle
|
||||
*/
|
||||
#normalizeDragRect(dragRect: Rect): Rect {
|
||||
private _normalizeDragRect(dragRect: Rect): Rect {
|
||||
const w = Math.abs(dragRect[2])
|
||||
const h = Math.abs(dragRect[3])
|
||||
if (dragRect[2] < 0) dragRect[0] -= w
|
||||
@@ -4112,7 +4130,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param rect The rectangle to check against
|
||||
* @returns Set of positionable items that overlap with the rectangle
|
||||
*/
|
||||
#getItemsInRect(rect: Rect): Set<Positionable> {
|
||||
private _getItemsInRect(rect: Rect): Set<Positionable> {
|
||||
const { graph, subgraph } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
@@ -4166,9 +4184,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
dragRect[2],
|
||||
dragRect[3]
|
||||
]
|
||||
this.#normalizeDragRect(normalizedRect)
|
||||
this._normalizeDragRect(normalizedRect)
|
||||
|
||||
const itemsInRect = this.#getItemsInRect(normalizedRect)
|
||||
const itemsInRect = this._getItemsInRect(normalizedRect)
|
||||
|
||||
const desired = new Set<Positionable>()
|
||||
if (e.shiftKey && !e.altKey) {
|
||||
@@ -4215,16 +4233,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @param e The pointer up event
|
||||
* @param dragRect The drag rectangle
|
||||
*/
|
||||
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
|
||||
private _handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
|
||||
const normalizedRect: Rect = [
|
||||
dragRect[0],
|
||||
dragRect[1],
|
||||
dragRect[2],
|
||||
dragRect[3]
|
||||
]
|
||||
this.#normalizeDragRect(normalizedRect)
|
||||
this._normalizeDragRect(normalizedRect)
|
||||
|
||||
const itemsInRect = this.#getItemsInRect(normalizedRect)
|
||||
const itemsInRect = this._getItemsInRect(normalizedRect)
|
||||
const { selectedItems } = this
|
||||
|
||||
if (e.shiftKey) {
|
||||
@@ -4588,7 +4606,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
*/
|
||||
setZoom(value: number, zooming_center: Point) {
|
||||
this.ds.changeScale(value, zooming_center)
|
||||
this.#dirty()
|
||||
this._dirty()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4671,7 +4689,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* @returns `true` if the node is visible, otherwise `false`
|
||||
*/
|
||||
isNodeVisible(node: LGraphNode): boolean {
|
||||
return this.#visible_node_ids.has(node.id)
|
||||
return this._visible_node_ids.has(node.id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4692,7 +4710,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (this.dirty_canvas || force_canvas) {
|
||||
this.computeVisibleNodes(undefined, this.visible_nodes)
|
||||
// Update visible node IDs
|
||||
this.#visible_node_ids = new Set(
|
||||
this._visible_node_ids = new Set(
|
||||
this.visible_nodes.map((node) => node.id)
|
||||
)
|
||||
|
||||
@@ -4746,8 +4764,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// TODO: Set snapping value when changed instead of once per frame
|
||||
this.#snapToGrid =
|
||||
this.#shiftDown || LiteGraph.alwaysSnapToGrid
|
||||
this._snapToGrid =
|
||||
this._shiftDown || LiteGraph.alwaysSnapToGrid
|
||||
? this.graph?.getSnapToGridSize()
|
||||
: undefined
|
||||
|
||||
@@ -4789,7 +4807,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// draw nodes
|
||||
const { visible_nodes } = this
|
||||
const drawSnapGuides =
|
||||
this.#snapToGrid &&
|
||||
this._snapToGrid &&
|
||||
(this.isDragging || layoutStore.isDraggingVueNodes.value)
|
||||
|
||||
for (const node of visible_nodes) {
|
||||
@@ -4829,7 +4847,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (linkConnector.isConnecting) {
|
||||
// current connection (the one being dragged by the mouse)
|
||||
const { renderLinks } = linkConnector
|
||||
const highlightPos = this.#getHighlightPosition()
|
||||
const highlightPos = this._getHighlightPosition()
|
||||
ctx.lineWidth = this.connections_width
|
||||
|
||||
for (const renderLink of renderLinks) {
|
||||
@@ -4883,7 +4901,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// Gradient half-border over target node
|
||||
this.#renderSnapHighlight(ctx, highlightPos)
|
||||
this._renderSnapHighlight(ctx, highlightPos)
|
||||
}
|
||||
|
||||
// on top of link center
|
||||
@@ -4909,7 +4927,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
/** @returns If the pointer is over a link centre marker, the link segment it belongs to. Otherwise, `undefined`. */
|
||||
#getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
|
||||
private _getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
|
||||
// Skip hit detection if center markers are disabled
|
||||
if (this.linkMarkerShape === LinkMarkerShape.None) {
|
||||
return undefined
|
||||
@@ -4928,7 +4946,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
/** Get the target snap / highlight point in graph space */
|
||||
#getHighlightPosition(): Readonly<Point> {
|
||||
private _getHighlightPosition(): Readonly<Point> {
|
||||
return LiteGraph.snaps_for_comfy
|
||||
? (this.linkConnector.state.snapLinksPos ??
|
||||
this._highlight_pos ??
|
||||
@@ -4941,7 +4959,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* Partial border over target node and a highlight over the slot itself.
|
||||
* @param ctx Canvas 2D context
|
||||
*/
|
||||
#renderSnapHighlight(
|
||||
private _renderSnapHighlight(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
highlightPos: Readonly<Point>
|
||||
): void {
|
||||
@@ -5592,7 +5610,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// Normalise boundingRect to pos to snap
|
||||
snapGuide[0] += offsetX
|
||||
snapGuide[1] += offsetY
|
||||
if (this.#snapToGrid) snapPoint(snapGuide, this.#snapToGrid)
|
||||
if (this._snapToGrid) snapPoint(snapGuide, this._snapToGrid)
|
||||
snapGuide[0] -= offsetX
|
||||
snapGuide[1] -= offsetY
|
||||
|
||||
@@ -5672,7 +5690,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const output = start_node.outputs[outputId]
|
||||
if (!output) continue
|
||||
|
||||
this.#renderAllLinkSegments(
|
||||
this._renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
startPos,
|
||||
@@ -5701,7 +5719,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
? getSlotPosition(inputNode, link.target_slot, true)
|
||||
: inputNode.getInputPos(link.target_slot)
|
||||
|
||||
this.#renderAllLinkSegments(
|
||||
this._renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
output.pos,
|
||||
@@ -5728,7 +5746,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
? getSlotPosition(outputNode, link.origin_slot, false)
|
||||
: outputNode.getOutputPos(link.origin_slot)
|
||||
|
||||
this.#renderAllLinkSegments(
|
||||
this._renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
startPos,
|
||||
@@ -5742,10 +5760,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
if (graph.floatingLinks.size > 0) {
|
||||
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
|
||||
this._renderFloatingLinks(ctx, graph, visibleReroutes, now)
|
||||
}
|
||||
|
||||
const rerouteSet = this.#visibleReroutes
|
||||
const rerouteSet = this._visibleReroutes
|
||||
rerouteSet.clear()
|
||||
|
||||
// Render reroutes, ordered by number of non-floating links
|
||||
@@ -5754,7 +5772,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
rerouteSet.add(reroute)
|
||||
|
||||
if (
|
||||
this.#snapToGrid &&
|
||||
this._snapToGrid &&
|
||||
this.isDragging &&
|
||||
this.selectedItems.has(reroute)
|
||||
) {
|
||||
@@ -5776,7 +5794,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
: this.editor_alpha
|
||||
}
|
||||
|
||||
#renderFloatingLinks(
|
||||
private _renderFloatingLinks(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
graph: LGraph,
|
||||
visibleReroutes: Reroute[],
|
||||
@@ -5805,7 +5823,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const endDirection = node.inputs[link.target_slot]?.dir
|
||||
|
||||
firstReroute._dragging = true
|
||||
this.#renderAllLinkSegments(
|
||||
this._renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
startPos,
|
||||
@@ -5827,7 +5845,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const startDirection = node.outputs[link.origin_slot]?.dir
|
||||
|
||||
link._dragging = true
|
||||
this.#renderAllLinkSegments(
|
||||
this._renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
startPos,
|
||||
@@ -5843,7 +5861,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
ctx.globalAlpha = globalAlpha
|
||||
}
|
||||
|
||||
#renderAllLinkSegments(
|
||||
private _renderAllLinkSegments(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LLink,
|
||||
startPos: Point,
|
||||
@@ -6146,7 +6164,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
ctx.save()
|
||||
ctx.globalAlpha = 0.5 * this.editor_alpha
|
||||
const drawSnapGuides =
|
||||
this.#snapToGrid &&
|
||||
this._snapToGrid &&
|
||||
(this.isDragging || layoutStore.isDraggingVueNodes.value)
|
||||
|
||||
for (const group of groups) {
|
||||
@@ -6513,7 +6531,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
},
|
||||
optPass || {}
|
||||
)
|
||||
const dirty = () => this.#dirty()
|
||||
const dirty = () => this._dirty()
|
||||
|
||||
const that = this
|
||||
const { graph } = this
|
||||
@@ -7522,7 +7540,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
function inner() {
|
||||
setValue(input?.value)
|
||||
}
|
||||
const dirty = () => this.#dirty()
|
||||
const dirty = () => this._dirty()
|
||||
|
||||
function setValue(value: string | number | undefined) {
|
||||
if (
|
||||
@@ -8356,7 +8374,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
reroute = this.graph.getRerouteOnPos(
|
||||
event.canvasX,
|
||||
event.canvasY,
|
||||
this.#visibleReroutes
|
||||
this._visibleReroutes
|
||||
)
|
||||
}
|
||||
if (reroute) {
|
||||
@@ -8646,4 +8664,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const mutations = this.initLayoutMutations()
|
||||
this.applyNodePositionUpdates(nodesToReposition, mutations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom JSON serialization to prevent circular reference errors.
|
||||
* LGraphCanvas should not be serialized directly - serialize the graph instead.
|
||||
*/
|
||||
toJSON(): { ds: { scale: number; offset: [number, number] } } {
|
||||
return {
|
||||
ds: {
|
||||
scale: this.ds.scale,
|
||||
offset: [...this.ds.offset] as [number, number]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ export class LGraphNode
|
||||
inputs: INodeInputSlot[] = []
|
||||
outputs: INodeOutputSlot[] = []
|
||||
|
||||
#concreteInputs: NodeInputSlot[] = []
|
||||
#concreteOutputs: NodeOutputSlot[] = []
|
||||
private _concreteInputs: NodeInputSlot[] = []
|
||||
private _concreteOutputs: NodeOutputSlot[] = []
|
||||
|
||||
properties: Dictionary<NodeProperty | undefined> = {}
|
||||
properties_info: INodePropertyInfo[] = []
|
||||
@@ -438,24 +438,24 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link renderArea} */
|
||||
#renderArea = new Rectangle()
|
||||
private _renderArea = new Rectangle()
|
||||
/**
|
||||
* Rect describing the node area, including shadows and any protrusions.
|
||||
* Determines if the node is visible. Calculated once at the start of every frame.
|
||||
*/
|
||||
get renderArea(): ReadOnlyRect {
|
||||
return this.#renderArea
|
||||
return this._renderArea
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link boundingRect} */
|
||||
#boundingRect: Rectangle = new Rectangle()
|
||||
private _boundingRect: Rectangle = new Rectangle()
|
||||
/**
|
||||
* Cached node position & area as `x, y, width, height`. Includes changes made by {@link onBounding}, if present.
|
||||
*
|
||||
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
|
||||
*/
|
||||
get boundingRect(): ReadOnlyRectangle {
|
||||
return this.#boundingRect
|
||||
return this._boundingRect
|
||||
}
|
||||
|
||||
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
|
||||
@@ -753,7 +753,9 @@ export class LGraphNode
|
||||
onPropertyChange?(this: LGraphNode): void
|
||||
updateOutputData?(this: LGraphNode, origin_slot: number): void
|
||||
|
||||
#getErrorStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
|
||||
private _getErrorStrokeStyle(
|
||||
this: LGraphNode
|
||||
): IDrawBoundingOptions | undefined {
|
||||
if (this.has_errors) {
|
||||
return {
|
||||
padding: 12,
|
||||
@@ -763,7 +765,9 @@ export class LGraphNode
|
||||
}
|
||||
}
|
||||
|
||||
#getSelectedStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
|
||||
private _getSelectedStrokeStyle(
|
||||
this: LGraphNode
|
||||
): IDrawBoundingOptions | undefined {
|
||||
if (this.selected) {
|
||||
return {
|
||||
padding: this.has_errors ? 20 : undefined
|
||||
@@ -778,8 +782,8 @@ export class LGraphNode
|
||||
this.size = [LiteGraph.NODE_WIDTH, 60]
|
||||
this.pos = [10, 10]
|
||||
this.strokeStyles = {
|
||||
error: this.#getErrorStrokeStyle,
|
||||
selected: this.#getSelectedStrokeStyle
|
||||
error: this._getErrorStrokeStyle,
|
||||
selected: this._getSelectedStrokeStyle
|
||||
}
|
||||
// Initialize property manager with tracked properties
|
||||
this.changeTracker = new LGraphNodeProperties(this)
|
||||
@@ -2067,11 +2071,11 @@ export class LGraphNode
|
||||
* Called automatically at the start of every frame.
|
||||
*/
|
||||
updateArea(ctx?: CanvasRenderingContext2D): void {
|
||||
const bounds = this.#boundingRect
|
||||
const bounds = this._boundingRect
|
||||
this.measure(bounds, ctx)
|
||||
this.onBounding?.(bounds)
|
||||
|
||||
const renderArea = this.#renderArea
|
||||
const renderArea = this._renderArea
|
||||
renderArea.set(bounds)
|
||||
// 4 offset for collapsed node connection points
|
||||
renderArea[0] -= 4
|
||||
@@ -2293,7 +2297,7 @@ export class LGraphNode
|
||||
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
|
||||
): INodeInputSlot | -1
|
||||
findInputSlotFree(optsIn?: FindFreeSlotOptions) {
|
||||
return this.#findFreeSlot(this.inputs, optsIn)
|
||||
return this._findFreeSlot(this.inputs, optsIn)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2308,14 +2312,14 @@ export class LGraphNode
|
||||
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
|
||||
): INodeOutputSlot | -1
|
||||
findOutputSlotFree(optsIn?: FindFreeSlotOptions) {
|
||||
return this.#findFreeSlot(this.outputs, optsIn)
|
||||
return this._findFreeSlot(this.outputs, optsIn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next free slot
|
||||
* @param slots The slots to search, i.e. this.inputs or this.outputs
|
||||
*/
|
||||
#findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
private _findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
slots: TSlot[],
|
||||
options?: FindFreeSlotOptions
|
||||
): TSlot | number {
|
||||
@@ -2357,7 +2361,7 @@ export class LGraphNode
|
||||
preferFreeSlot?: boolean,
|
||||
doNotUseOccupied?: boolean
|
||||
) {
|
||||
return this.#findSlotByType(
|
||||
return this._findSlotByType(
|
||||
this.inputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2387,7 +2391,7 @@ export class LGraphNode
|
||||
preferFreeSlot?: boolean,
|
||||
doNotUseOccupied?: boolean
|
||||
) {
|
||||
return this.#findSlotByType(
|
||||
return this._findSlotByType(
|
||||
this.outputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2433,14 +2437,14 @@ export class LGraphNode
|
||||
doNotUseOccupied?: boolean
|
||||
): number | INodeOutputSlot | INodeInputSlot {
|
||||
return input
|
||||
? this.#findSlotByType(
|
||||
? this._findSlotByType(
|
||||
this.inputs,
|
||||
type,
|
||||
returnObj,
|
||||
preferFreeSlot,
|
||||
doNotUseOccupied
|
||||
)
|
||||
: this.#findSlotByType(
|
||||
: this._findSlotByType(
|
||||
this.outputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2461,7 +2465,7 @@ export class LGraphNode
|
||||
* @see {findInputSlotByType}
|
||||
* @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1
|
||||
*/
|
||||
#findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
private _findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
slots: TSlot[],
|
||||
type: ISlotType,
|
||||
returnObj?: boolean,
|
||||
@@ -3310,8 +3314,8 @@ export class LGraphNode
|
||||
// default vertical slots
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
const slotIndex = is_input
|
||||
? this.#defaultVerticalInputs.indexOf(this.inputs[slot_number])
|
||||
: this.#defaultVerticalOutputs.indexOf(this.outputs[slot_number])
|
||||
? this._defaultVerticalInputs.indexOf(this.inputs[slot_number])
|
||||
: this._defaultVerticalOutputs.indexOf(this.outputs[slot_number])
|
||||
|
||||
out[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset
|
||||
out[1] =
|
||||
@@ -3324,7 +3328,7 @@ export class LGraphNode
|
||||
/**
|
||||
* @internal The inputs that are not positioned with absolute coordinates.
|
||||
*/
|
||||
get #defaultVerticalInputs() {
|
||||
private get _defaultVerticalInputs() {
|
||||
return this.inputs.filter(
|
||||
(slot) => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot))
|
||||
)
|
||||
@@ -3333,7 +3337,7 @@ export class LGraphNode
|
||||
/**
|
||||
* @internal The outputs that are not positioned with absolute coordinates.
|
||||
*/
|
||||
get #defaultVerticalOutputs() {
|
||||
private get _defaultVerticalOutputs() {
|
||||
return this.outputs.filter((slot: INodeOutputSlot) => !slot.pos)
|
||||
}
|
||||
|
||||
@@ -3341,7 +3345,7 @@ export class LGraphNode
|
||||
* Get the context needed for slot position calculations
|
||||
* @internal
|
||||
*/
|
||||
#getSlotPositionContext(): SlotPositionContext {
|
||||
private _getSlotPositionContext(): SlotPositionContext {
|
||||
return {
|
||||
nodeX: this.pos[0],
|
||||
nodeY: this.pos[1],
|
||||
@@ -3373,7 +3377,7 @@ export class LGraphNode
|
||||
* @returns Position of the centre of the input slot in graph co-ordinates.
|
||||
*/
|
||||
getInputSlotPos(input: INodeInputSlot): Point {
|
||||
return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input)
|
||||
return calculateInputSlotPosFromSlot(this._getSlotPositionContext(), input)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3916,13 +3920,13 @@ export class LGraphNode
|
||||
*/
|
||||
drawCollapsedSlots(ctx: CanvasRenderingContext2D): void {
|
||||
// Render the first connected slot only.
|
||||
for (const slot of this.#concreteInputs) {
|
||||
for (const slot of this._concreteInputs) {
|
||||
if (slot.link != null) {
|
||||
slot.drawCollapsed(ctx)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (const slot of this.#concreteOutputs) {
|
||||
for (const slot of this._concreteOutputs) {
|
||||
if (slot.links?.length) {
|
||||
slot.drawCollapsed(ctx)
|
||||
break
|
||||
@@ -3934,7 +3938,7 @@ export class LGraphNode
|
||||
return [...this.inputs, ...this.outputs]
|
||||
}
|
||||
|
||||
#measureSlot(
|
||||
private _measureSlot(
|
||||
slot: NodeInputSlot | NodeOutputSlot,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
@@ -3951,27 +3955,27 @@ export class LGraphNode
|
||||
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
#measureSlots(): ReadOnlyRect | null {
|
||||
private _measureSlots(): ReadOnlyRect | null {
|
||||
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
|
||||
|
||||
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
|
||||
for (const [slotIndex, slot] of this._concreteInputs.entries()) {
|
||||
// Unrecognized nodes (Nodes with error) has inputs but no widgets. Treat
|
||||
// converted inputs as normal inputs.
|
||||
/** Widget input slots are handled in {@link layoutWidgetInputSlots} */
|
||||
if (this.widgets?.length && isWidgetInputSlot(slot)) continue
|
||||
|
||||
this.#measureSlot(slot, slotIndex, true)
|
||||
this._measureSlot(slot, slotIndex, true)
|
||||
slots.push(slot)
|
||||
}
|
||||
for (const [slotIndex, slot] of this.#concreteOutputs.entries()) {
|
||||
this.#measureSlot(slot, slotIndex, false)
|
||||
for (const [slotIndex, slot] of this._concreteOutputs.entries()) {
|
||||
this._measureSlot(slot, slotIndex, false)
|
||||
slots.push(slot)
|
||||
}
|
||||
|
||||
return slots.length ? createBounds(slots, 0) : null
|
||||
}
|
||||
|
||||
#getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
|
||||
private _getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
|
||||
const isInput = isINodeInputSlot(slot)
|
||||
const mouseOverId = this.mouseOver?.[isInput ? 'inputId' : 'outputId'] ?? -1
|
||||
if (mouseOverId === -1) {
|
||||
@@ -3980,11 +3984,11 @@ export class LGraphNode
|
||||
return isInput ? this.inputs[mouseOverId] : this.outputs[mouseOverId]
|
||||
}
|
||||
|
||||
#isMouseOverSlot(slot: INodeSlot): boolean {
|
||||
return this.#getMouseOverSlot(slot) === slot
|
||||
private _isMouseOverSlot(slot: INodeSlot): boolean {
|
||||
return this._getMouseOverSlot(slot) === slot
|
||||
}
|
||||
|
||||
#isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
|
||||
private _isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
|
||||
if (!widget) return false
|
||||
return this.mouseOver?.overWidget === widget
|
||||
}
|
||||
@@ -4016,9 +4020,9 @@ export class LGraphNode
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions
|
||||
) {
|
||||
for (const slot of [...this.#concreteInputs, ...this.#concreteOutputs]) {
|
||||
for (const slot of [...this._concreteInputs, ...this._concreteOutputs]) {
|
||||
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
|
||||
const isMouseOverSlot = this.#isMouseOverSlot(slot)
|
||||
const isMouseOverSlot = this._isMouseOverSlot(slot)
|
||||
|
||||
// change opacity of incompatible slots when dragging a connection
|
||||
const isValid = !fromSlot || isValidTarget
|
||||
@@ -4033,7 +4037,7 @@ export class LGraphNode
|
||||
isMouseOverSlot ||
|
||||
isValidTarget ||
|
||||
!slot.isWidgetInputSlot ||
|
||||
this.#isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
slot.isConnected ||
|
||||
slot.alwaysVisible
|
||||
) {
|
||||
@@ -4054,7 +4058,7 @@ export class LGraphNode
|
||||
* - {@link IBaseWidget.y}
|
||||
* @param widgetStartY The y-coordinate of the first widget
|
||||
*/
|
||||
#arrangeWidgets(widgetStartY: number): void {
|
||||
private _arrangeWidgets(widgetStartY: number): void {
|
||||
if (!this.widgets || !this.widgets.length) return
|
||||
|
||||
const bodyHeight = this.bodyHeight
|
||||
@@ -4132,7 +4136,7 @@ export class LGraphNode
|
||||
/**
|
||||
* Arranges the layout of the node's widget input slots.
|
||||
*/
|
||||
#arrangeWidgetInputSlots(): void {
|
||||
private _arrangeWidgetInputSlots(): void {
|
||||
if (!this.widgets) return
|
||||
|
||||
const slotByWidgetName = new Map<
|
||||
@@ -4154,10 +4158,10 @@ export class LGraphNode
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
const actualSlot = this.#concreteInputs[slot.index]
|
||||
const actualSlot = this._concreteInputs[slot.index]
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
actualSlot.pos = [offset, widget.y + offset]
|
||||
this.#measureSlot(actualSlot, slot.index, true)
|
||||
this._measureSlot(actualSlot, slot.index, true)
|
||||
}
|
||||
} else {
|
||||
// For Vue positioning, just measure the slots without setting pos
|
||||
@@ -4165,7 +4169,7 @@ export class LGraphNode
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
this.#measureSlot(this.#concreteInputs[slot.index], slot.index, true)
|
||||
this._measureSlot(this._concreteInputs[slot.index], slot.index, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4178,10 +4182,10 @@ export class LGraphNode
|
||||
* have been removed from the ecosystem.
|
||||
*/
|
||||
_setConcreteSlots(): void {
|
||||
this.#concreteInputs = this.inputs.map((slot) =>
|
||||
this._concreteInputs = this.inputs.map((slot) =>
|
||||
toClass(NodeInputSlot, slot, this)
|
||||
)
|
||||
this.#concreteOutputs = this.outputs.map((slot) =>
|
||||
this._concreteOutputs = this.outputs.map((slot) =>
|
||||
toClass(NodeOutputSlot, slot, this)
|
||||
)
|
||||
}
|
||||
@@ -4190,12 +4194,12 @@ export class LGraphNode
|
||||
* Arranges node elements in preparation for rendering (slots & widgets).
|
||||
*/
|
||||
arrange(): void {
|
||||
const slotsBounds = this.#measureSlots()
|
||||
const slotsBounds = this._measureSlots()
|
||||
const widgetStartY = slotsBounds
|
||||
? slotsBounds[1] + slotsBounds[3] - this.pos[1]
|
||||
: 0
|
||||
this.#arrangeWidgets(widgetStartY)
|
||||
this.#arrangeWidgetInputSlots()
|
||||
this._arrangeWidgets(widgetStartY)
|
||||
this._arrangeWidgetInputSlots()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,24 +25,24 @@ export class LGraphNodeProperties {
|
||||
node: LGraphNode
|
||||
|
||||
/** Set of property paths that have been instrumented */
|
||||
#instrumentedPaths = new Set<string>()
|
||||
private _instrumentedPaths = new Set<string>()
|
||||
|
||||
constructor(node: LGraphNode) {
|
||||
this.node = node
|
||||
|
||||
this.#setupInstrumentation()
|
||||
this._setupInstrumentation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up property instrumentation for all tracked properties
|
||||
*/
|
||||
#setupInstrumentation(): void {
|
||||
private _setupInstrumentation(): void {
|
||||
for (const path of DEFAULT_TRACKED_PROPERTIES) {
|
||||
this.#instrumentProperty(path)
|
||||
this._instrumentProperty(path)
|
||||
}
|
||||
}
|
||||
|
||||
#resolveTargetObject(parts: string[]): {
|
||||
private _resolveTargetObject(parts: string[]): {
|
||||
targetObject: Record<string, unknown>
|
||||
propertyName: string
|
||||
} {
|
||||
@@ -73,14 +73,14 @@ export class LGraphNodeProperties {
|
||||
/**
|
||||
* Instruments a single property to track changes
|
||||
*/
|
||||
#instrumentProperty(path: string): void {
|
||||
private _instrumentProperty(path: string): void {
|
||||
const parts = path.split('.')
|
||||
|
||||
if (parts.length > 1) {
|
||||
this.#ensureNestedPath(path)
|
||||
this._ensureNestedPath(path)
|
||||
}
|
||||
|
||||
const { targetObject, propertyName } = this.#resolveTargetObject(parts)
|
||||
const { targetObject, propertyName } = this._resolveTargetObject(parts)
|
||||
|
||||
const hasProperty = Object.prototype.hasOwnProperty.call(
|
||||
targetObject,
|
||||
@@ -96,7 +96,7 @@ export class LGraphNodeProperties {
|
||||
set: (newValue: unknown) => {
|
||||
const oldValue = value
|
||||
value = newValue
|
||||
this.#emitPropertyChange(path, oldValue, newValue)
|
||||
this._emitPropertyChange(path, oldValue, newValue)
|
||||
|
||||
// Update enumerable: true for non-undefined values, false for undefined
|
||||
const shouldBeEnumerable = newValue !== undefined
|
||||
@@ -121,24 +121,24 @@ export class LGraphNodeProperties {
|
||||
Object.defineProperty(
|
||||
targetObject,
|
||||
propertyName,
|
||||
this.#createInstrumentedDescriptor(path, currentValue)
|
||||
this._createInstrumentedDescriptor(path, currentValue)
|
||||
)
|
||||
}
|
||||
|
||||
this.#instrumentedPaths.add(path)
|
||||
this._instrumentedPaths.add(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a property descriptor that emits change events
|
||||
*/
|
||||
#createInstrumentedDescriptor(
|
||||
private _createInstrumentedDescriptor(
|
||||
propertyPath: string,
|
||||
initialValue: unknown
|
||||
): PropertyDescriptor {
|
||||
return this.#createInstrumentedDescriptorTyped(propertyPath, initialValue)
|
||||
return this._createInstrumentedDescriptorTyped(propertyPath, initialValue)
|
||||
}
|
||||
|
||||
#createInstrumentedDescriptorTyped<TValue>(
|
||||
private _createInstrumentedDescriptorTyped<TValue>(
|
||||
propertyPath: string,
|
||||
initialValue: TValue
|
||||
): PropertyDescriptor {
|
||||
@@ -149,7 +149,7 @@ export class LGraphNodeProperties {
|
||||
set: (newValue: TValue) => {
|
||||
const oldValue = value
|
||||
value = newValue
|
||||
this.#emitPropertyChange(propertyPath, oldValue, newValue)
|
||||
this._emitPropertyChange(propertyPath, oldValue, newValue)
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
@@ -159,15 +159,15 @@ export class LGraphNodeProperties {
|
||||
/**
|
||||
* Emits a property change event if the node is connected to a graph
|
||||
*/
|
||||
#emitPropertyChange(
|
||||
private _emitPropertyChange(
|
||||
propertyPath: string,
|
||||
oldValue: unknown,
|
||||
newValue: unknown
|
||||
): void {
|
||||
this.#emitPropertyChangeTyped(propertyPath, oldValue, newValue)
|
||||
this._emitPropertyChangeTyped(propertyPath, oldValue, newValue)
|
||||
}
|
||||
|
||||
#emitPropertyChangeTyped<TValue>(
|
||||
private _emitPropertyChangeTyped<TValue>(
|
||||
propertyPath: string,
|
||||
oldValue: TValue,
|
||||
newValue: TValue
|
||||
@@ -183,7 +183,7 @@ export class LGraphNodeProperties {
|
||||
/**
|
||||
* Ensures parent objects exist for nested properties
|
||||
*/
|
||||
#ensureNestedPath(path: string): void {
|
||||
private _ensureNestedPath(path: string): void {
|
||||
const parts = path.split('.')
|
||||
// LGraphNode supports dynamic property access at runtime
|
||||
let current: Record<string, unknown> = this.node as unknown as Record<
|
||||
@@ -208,7 +208,7 @@ export class LGraphNodeProperties {
|
||||
* Checks if a property is being tracked
|
||||
*/
|
||||
isTracked(path: string): boolean {
|
||||
return this.#instrumentedPaths.has(path)
|
||||
return this._instrumentedPaths.has(path)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,14 +119,14 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
/** @inheritdoc */
|
||||
_dragging?: boolean
|
||||
|
||||
#color?: CanvasColour | null
|
||||
private _color?: CanvasColour | null
|
||||
/** Custom colour for this link only */
|
||||
public get color(): CanvasColour | null | undefined {
|
||||
return this.#color
|
||||
return this._color
|
||||
}
|
||||
|
||||
public set color(value: CanvasColour) {
|
||||
this.#color = value === '' ? null : value
|
||||
this._color = value === '' ? null : value
|
||||
}
|
||||
|
||||
public get isFloatingOutput(): boolean {
|
||||
|
||||
@@ -1,301 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
|
||||
LGraph {
|
||||
"_groups": [
|
||||
LGraphGroup {
|
||||
"_bounding": Rectangle [
|
||||
{
|
||||
"config": {},
|
||||
"definitions": undefined,
|
||||
"extra": {
|
||||
"reroutes": undefined,
|
||||
},
|
||||
"floatingLinks": undefined,
|
||||
"groups": [
|
||||
{
|
||||
"bounding": [
|
||||
20,
|
||||
20,
|
||||
1,
|
||||
3,
|
||||
],
|
||||
"_children": Set {},
|
||||
"_nodes": [],
|
||||
"_pos": Float64Array [
|
||||
20,
|
||||
20,
|
||||
],
|
||||
"_size": Float64Array [
|
||||
1,
|
||||
3,
|
||||
],
|
||||
"color": "#6029aa",
|
||||
"flags": {},
|
||||
"font": undefined,
|
||||
"font_size": 14,
|
||||
"graph": [Circular],
|
||||
"id": 123,
|
||||
"selected": undefined,
|
||||
"setDirtyCanvas": [Function],
|
||||
"title": "A group to test with",
|
||||
},
|
||||
],
|
||||
"_input_nodes": undefined,
|
||||
"_last_trigger_time": undefined,
|
||||
"_links": Map {},
|
||||
"_nodes": [
|
||||
LGraphNode {
|
||||
"_collapsed_width": undefined,
|
||||
"_level": undefined,
|
||||
"_pos": Float64Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"_posSize": Rectangle [
|
||||
10,
|
||||
10,
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"_relative_id": undefined,
|
||||
"_shape": undefined,
|
||||
"_size": Float64Array [
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"action_call": undefined,
|
||||
"action_triggered": undefined,
|
||||
"badgePosition": "top-left",
|
||||
"badges": [],
|
||||
"bgcolor": undefined,
|
||||
"block_delete": undefined,
|
||||
"boxcolor": undefined,
|
||||
"changeTracker": undefined,
|
||||
"clip_area": undefined,
|
||||
"clonable": undefined,
|
||||
"color": undefined,
|
||||
"console": undefined,
|
||||
"exec_version": undefined,
|
||||
"execute_triggered": undefined,
|
||||
"flags": {},
|
||||
"freeWidgetSpace": undefined,
|
||||
"gotFocusAt": undefined,
|
||||
"graph": [Circular],
|
||||
"has_errors": true,
|
||||
"id": 1,
|
||||
"ignore_remove": undefined,
|
||||
"inputs": [],
|
||||
"last_serialization": {
|
||||
"id": 1,
|
||||
},
|
||||
"locked": undefined,
|
||||
"lostFocusAt": undefined,
|
||||
"mode": 0,
|
||||
"mouseOver": undefined,
|
||||
"order": 0,
|
||||
"outputs": [],
|
||||
"progress": undefined,
|
||||
"properties": {},
|
||||
"properties_info": [],
|
||||
"redraw_on_mouse": undefined,
|
||||
"removable": undefined,
|
||||
"resizable": undefined,
|
||||
"selected": undefined,
|
||||
"serialize_widgets": undefined,
|
||||
"showAdvanced": undefined,
|
||||
"strokeStyles": {
|
||||
"error": [Function],
|
||||
"selected": [Function],
|
||||
},
|
||||
"title": undefined,
|
||||
"title_buttons": [],
|
||||
"type": "",
|
||||
"widgets": undefined,
|
||||
"widgets_start_y": undefined,
|
||||
"widgets_up": undefined,
|
||||
},
|
||||
],
|
||||
"_nodes_by_id": {
|
||||
"1": LGraphNode {
|
||||
"_collapsed_width": undefined,
|
||||
"_level": undefined,
|
||||
"_pos": Float64Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"_posSize": Rectangle [
|
||||
10,
|
||||
10,
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"_relative_id": undefined,
|
||||
"_shape": undefined,
|
||||
"_size": Float64Array [
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"action_call": undefined,
|
||||
"action_triggered": undefined,
|
||||
"badgePosition": "top-left",
|
||||
"badges": [],
|
||||
"bgcolor": undefined,
|
||||
"block_delete": undefined,
|
||||
"boxcolor": undefined,
|
||||
"changeTracker": undefined,
|
||||
"clip_area": undefined,
|
||||
"clonable": undefined,
|
||||
"color": undefined,
|
||||
"console": undefined,
|
||||
"exec_version": undefined,
|
||||
"execute_triggered": undefined,
|
||||
"flags": {},
|
||||
"freeWidgetSpace": undefined,
|
||||
"gotFocusAt": undefined,
|
||||
"graph": [Circular],
|
||||
"has_errors": true,
|
||||
"id": 1,
|
||||
"ignore_remove": undefined,
|
||||
"inputs": [],
|
||||
"last_serialization": {
|
||||
"id": 1,
|
||||
},
|
||||
"locked": undefined,
|
||||
"lostFocusAt": undefined,
|
||||
"mode": 0,
|
||||
"mouseOver": undefined,
|
||||
"order": 0,
|
||||
"outputs": [],
|
||||
"progress": undefined,
|
||||
"properties": {},
|
||||
"properties_info": [],
|
||||
"redraw_on_mouse": undefined,
|
||||
"removable": undefined,
|
||||
"resizable": undefined,
|
||||
"selected": undefined,
|
||||
"serialize_widgets": undefined,
|
||||
"showAdvanced": undefined,
|
||||
"strokeStyles": {
|
||||
"error": [Function],
|
||||
"selected": [Function],
|
||||
},
|
||||
"title": undefined,
|
||||
"title_buttons": [],
|
||||
"type": "",
|
||||
"widgets": undefined,
|
||||
"widgets_start_y": undefined,
|
||||
"widgets_up": undefined,
|
||||
},
|
||||
},
|
||||
"_nodes_executable": [],
|
||||
"_nodes_in_order": [
|
||||
LGraphNode {
|
||||
"_collapsed_width": undefined,
|
||||
"_level": undefined,
|
||||
"_pos": Float64Array [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
"_posSize": Rectangle [
|
||||
10,
|
||||
10,
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"_relative_id": undefined,
|
||||
"_shape": undefined,
|
||||
"_size": Float64Array [
|
||||
140,
|
||||
60,
|
||||
],
|
||||
"action_call": undefined,
|
||||
"action_triggered": undefined,
|
||||
"badgePosition": "top-left",
|
||||
"badges": [],
|
||||
"bgcolor": undefined,
|
||||
"block_delete": undefined,
|
||||
"boxcolor": undefined,
|
||||
"changeTracker": undefined,
|
||||
"clip_area": undefined,
|
||||
"clonable": undefined,
|
||||
"color": undefined,
|
||||
"console": undefined,
|
||||
"exec_version": undefined,
|
||||
"execute_triggered": undefined,
|
||||
"flags": {},
|
||||
"freeWidgetSpace": undefined,
|
||||
"gotFocusAt": undefined,
|
||||
"graph": [Circular],
|
||||
"has_errors": true,
|
||||
"id": 1,
|
||||
"ignore_remove": undefined,
|
||||
"inputs": [],
|
||||
"last_serialization": {
|
||||
"id": 1,
|
||||
},
|
||||
"locked": undefined,
|
||||
"lostFocusAt": undefined,
|
||||
"mode": 0,
|
||||
"mouseOver": undefined,
|
||||
"order": 0,
|
||||
"outputs": [],
|
||||
"progress": undefined,
|
||||
"properties": {},
|
||||
"properties_info": [],
|
||||
"redraw_on_mouse": undefined,
|
||||
"removable": undefined,
|
||||
"resizable": undefined,
|
||||
"selected": undefined,
|
||||
"serialize_widgets": undefined,
|
||||
"showAdvanced": undefined,
|
||||
"strokeStyles": {
|
||||
"error": [Function],
|
||||
"selected": [Function],
|
||||
},
|
||||
"title": undefined,
|
||||
"title_buttons": [],
|
||||
"type": "",
|
||||
"widgets": undefined,
|
||||
"widgets_start_y": undefined,
|
||||
"widgets_up": undefined,
|
||||
},
|
||||
],
|
||||
"_subgraphs": Map {},
|
||||
"_version": 3,
|
||||
"catch_errors": true,
|
||||
"config": {},
|
||||
"elapsed_time": 0.01,
|
||||
"errors_in_execution": undefined,
|
||||
"events": CustomEventTarget {
|
||||
Symbol(listeners): {
|
||||
"bubbling": Map {},
|
||||
"capturing": Map {},
|
||||
},
|
||||
Symbol(listenerOptions): {
|
||||
"bubbling": Map {},
|
||||
"capturing": Map {},
|
||||
},
|
||||
},
|
||||
"execution_time": undefined,
|
||||
"execution_timer_id": undefined,
|
||||
"extra": {},
|
||||
"filter": undefined,
|
||||
"fixedtime": 0,
|
||||
"fixedtime_lapse": 0.01,
|
||||
"floatingLinksInternal": Map {},
|
||||
"globaltime": 0,
|
||||
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
|
||||
"iteration": 0,
|
||||
"last_update_time": 0,
|
||||
"links": Map {},
|
||||
"list_of_graphcanvas": null,
|
||||
"nodes_actioning": [],
|
||||
"nodes_executedAction": [],
|
||||
"nodes_executing": [],
|
||||
"onTrigger": undefined,
|
||||
"reroutesInternal": Map {},
|
||||
"last_link_id": 0,
|
||||
"last_node_id": 1,
|
||||
"links": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"mode": 0,
|
||||
"pos": [
|
||||
10,
|
||||
10,
|
||||
],
|
||||
},
|
||||
],
|
||||
"revision": 0,
|
||||
"runningtime": 0,
|
||||
"starttime": 0,
|
||||
"state": {
|
||||
"lastGroupId": 123,
|
||||
"lastLinkId": 0,
|
||||
"lastNodeId": 1,
|
||||
"lastRerouteId": 0,
|
||||
},
|
||||
"status": 1,
|
||||
"vars": {},
|
||||
"version": 0.4,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -51,11 +51,11 @@ export class InputIndicators implements Disposable {
|
||||
const element = canvas.canvas
|
||||
const options = { capture: true, signal } satisfies AddEventListenerOptions
|
||||
|
||||
element.addEventListener('pointerdown', this.#onPointerDownOrMove, options)
|
||||
element.addEventListener('pointermove', this.#onPointerDownOrMove, options)
|
||||
element.addEventListener('pointerup', this.#onPointerUp, options)
|
||||
element.addEventListener('keydown', this.#onKeyDownOrUp, options)
|
||||
document.addEventListener('keyup', this.#onKeyDownOrUp, options)
|
||||
element.addEventListener('pointerdown', this._onPointerDownOrMove, options)
|
||||
element.addEventListener('pointermove', this._onPointerDownOrMove, options)
|
||||
element.addEventListener('pointerup', this._onPointerUp, options)
|
||||
element.addEventListener('keydown', this._onKeyDownOrUp, options)
|
||||
document.addEventListener('keyup', this._onKeyDownOrUp, options)
|
||||
|
||||
const origDrawFrontCanvas = canvas.drawFrontCanvas.bind(canvas)
|
||||
signal.addEventListener('abort', () => {
|
||||
@@ -68,7 +68,7 @@ export class InputIndicators implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerDownOrMove = this.onPointerDownOrMove.bind(this)
|
||||
private _onPointerDownOrMove = this.onPointerDownOrMove.bind(this)
|
||||
onPointerDownOrMove(e: MouseEvent): void {
|
||||
this.mouse0Down = (e.buttons & 1) === 1
|
||||
this.mouse1Down = (e.buttons & 4) === 4
|
||||
@@ -80,14 +80,14 @@ export class InputIndicators implements Disposable {
|
||||
this.canvas.setDirty(true)
|
||||
}
|
||||
|
||||
#onPointerUp = this.onPointerUp.bind(this)
|
||||
private _onPointerUp = this.onPointerUp.bind(this)
|
||||
onPointerUp(): void {
|
||||
this.mouse0Down = false
|
||||
this.mouse1Down = false
|
||||
this.mouse2Down = false
|
||||
}
|
||||
|
||||
#onKeyDownOrUp = this.onKeyDownOrUp.bind(this)
|
||||
private _onKeyDownOrUp = this.onKeyDownOrUp.bind(this)
|
||||
onKeyDownOrUp(e: KeyboardEvent): void {
|
||||
this.ctrlDown = e.ctrlKey
|
||||
this.altDown = e.altKey
|
||||
|
||||
@@ -115,10 +115,10 @@ export class LinkConnector {
|
||||
/** The reroute beneath the pointer, if it is a valid connection target. */
|
||||
overReroute?: Reroute
|
||||
|
||||
readonly #setConnectingLinks: (value: ConnectingLink[]) => void
|
||||
private readonly _setConnectingLinks: (value: ConnectingLink[]) => void
|
||||
|
||||
constructor(setConnectingLinks: (value: ConnectingLink[]) => void) {
|
||||
this.#setConnectingLinks = setConnectingLinks
|
||||
this._setConnectingLinks = setConnectingLinks
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
@@ -253,7 +253,7 @@ export class LinkConnector {
|
||||
state.connectingTo = 'input'
|
||||
state.draggingExistingLinks = true
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
}
|
||||
|
||||
/** Drag all links from an output to a new output. */
|
||||
@@ -364,7 +364,7 @@ export class LinkConnector {
|
||||
state.multi = true
|
||||
state.connectingTo = 'output'
|
||||
|
||||
this.#setLegacyLinks(true)
|
||||
this._setLegacyLinks(true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,7 +387,7 @@ export class LinkConnector {
|
||||
|
||||
state.connectingTo = 'input'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,7 +410,7 @@ export class LinkConnector {
|
||||
|
||||
state.connectingTo = 'output'
|
||||
|
||||
this.#setLegacyLinks(true)
|
||||
this._setLegacyLinks(true)
|
||||
}
|
||||
|
||||
dragNewFromSubgraphInput(
|
||||
@@ -431,7 +431,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'input'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
}
|
||||
|
||||
dragNewFromSubgraphOutput(
|
||||
@@ -452,7 +452,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'output'
|
||||
|
||||
this.#setLegacyLinks(true)
|
||||
this._setLegacyLinks(true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,7 +489,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'input'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -516,7 +516,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'input'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,7 +553,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'output'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ export class LinkConnector {
|
||||
|
||||
this.state.connectingTo = 'output'
|
||||
|
||||
this.#setLegacyLinks(true)
|
||||
this._setLegacyLinks(true)
|
||||
}
|
||||
|
||||
dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void {
|
||||
@@ -603,7 +603,7 @@ export class LinkConnector {
|
||||
|
||||
state.connectingTo = 'input'
|
||||
|
||||
this.#setLegacyLinks(false)
|
||||
this._setLegacyLinks(false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -754,7 +754,7 @@ export class LinkConnector {
|
||||
const output = node.getOutputOnPos([canvasX, canvasY])
|
||||
|
||||
if (output) {
|
||||
this.#dropOnOutput(node, output)
|
||||
this._dropOnOutput(node, output)
|
||||
} else {
|
||||
this.connectToNode(node, event)
|
||||
}
|
||||
@@ -765,7 +765,7 @@ export class LinkConnector {
|
||||
|
||||
// Input slot
|
||||
if (inputOrSocket) {
|
||||
this.#dropOnInput(node, inputOrSocket)
|
||||
this._dropOnInput(node, inputOrSocket)
|
||||
} else {
|
||||
// Node background / title
|
||||
this.connectToNode(node, event)
|
||||
@@ -911,7 +911,7 @@ export class LinkConnector {
|
||||
return
|
||||
}
|
||||
|
||||
this.#dropOnOutput(node, output)
|
||||
this._dropOnOutput(node, output)
|
||||
} else if (connectingTo === 'input') {
|
||||
// Dropping new input link
|
||||
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
|
||||
@@ -922,11 +922,11 @@ export class LinkConnector {
|
||||
return
|
||||
}
|
||||
|
||||
this.#dropOnInput(node, input)
|
||||
this._dropOnInput(node, input)
|
||||
}
|
||||
}
|
||||
|
||||
#dropOnInput(node: LGraphNode, input: INodeInputSlot): void {
|
||||
private _dropOnInput(node: LGraphNode, input: INodeInputSlot): void {
|
||||
for (const link of this.renderLinks) {
|
||||
if (!link.canConnectToInput(node, input)) continue
|
||||
|
||||
@@ -934,7 +934,7 @@ export class LinkConnector {
|
||||
}
|
||||
}
|
||||
|
||||
#dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
|
||||
private _dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
|
||||
for (const link of this.renderLinks) {
|
||||
if (!link.canConnectToOutput(node, output)) {
|
||||
if (
|
||||
@@ -1014,7 +1014,7 @@ export class LinkConnector {
|
||||
}
|
||||
|
||||
/** Sets connecting_links, used by some extensions still. */
|
||||
#setLegacyLinks(fromSlotIsInput: boolean): void {
|
||||
private _setLegacyLinks(fromSlotIsInput: boolean): void {
|
||||
const links = this.renderLinks.map((link) => {
|
||||
const input = fromSlotIsInput ? (link.fromSlot as INodeInputSlot) : null
|
||||
const output = fromSlotIsInput ? null : (link.fromSlot as INodeOutputSlot)
|
||||
@@ -1033,7 +1033,7 @@ export class LinkConnector {
|
||||
afterRerouteId
|
||||
} satisfies ConnectingLink
|
||||
})
|
||||
this.#setConnectingLinks(links)
|
||||
this._setConnectingLinks(links)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { ReadOnlyRect, Size } from '@/lib/litegraph/src/interfaces'
|
||||
* - Width and height are then updated, clamped to min/max values
|
||||
*/
|
||||
export class ConstrainedSize {
|
||||
#width: number = 0
|
||||
#height: number = 0
|
||||
#desiredWidth: number = 0
|
||||
#desiredHeight: number = 0
|
||||
private _width: number = 0
|
||||
private _height: number = 0
|
||||
private _desiredWidth: number = 0
|
||||
private _desiredHeight: number = 0
|
||||
|
||||
minWidth: number = 0
|
||||
minHeight: number = 0
|
||||
@@ -21,29 +21,29 @@ export class ConstrainedSize {
|
||||
maxHeight: number = Infinity
|
||||
|
||||
get width() {
|
||||
return this.#width
|
||||
return this._width
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this.#height
|
||||
return this._height
|
||||
}
|
||||
|
||||
get desiredWidth() {
|
||||
return this.#desiredWidth
|
||||
return this._desiredWidth
|
||||
}
|
||||
|
||||
set desiredWidth(value: number) {
|
||||
this.#desiredWidth = value
|
||||
this.#width = clamp(value, this.minWidth, this.maxWidth)
|
||||
this._desiredWidth = value
|
||||
this._width = clamp(value, this.minWidth, this.maxWidth)
|
||||
}
|
||||
|
||||
get desiredHeight() {
|
||||
return this.#desiredHeight
|
||||
return this._desiredHeight
|
||||
}
|
||||
|
||||
set desiredHeight(value: number) {
|
||||
this.#desiredHeight = value
|
||||
this.#height = clamp(value, this.minHeight, this.maxHeight)
|
||||
this._desiredHeight = value
|
||||
this._height = clamp(value, this.minHeight, this.maxHeight)
|
||||
}
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
@@ -70,6 +70,6 @@ export class ConstrainedSize {
|
||||
}
|
||||
|
||||
toSize(): Size {
|
||||
return [this.#width, this.#height]
|
||||
return [this._width, this._height]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import { isInRectangle } from '@/lib/litegraph/src/measure'
|
||||
* - {@link size}: The size of the rectangle.
|
||||
*/
|
||||
export class Rectangle extends Float64Array {
|
||||
#pos: Float64Array<ArrayBuffer> | undefined
|
||||
#size: Float64Array<ArrayBuffer> | undefined
|
||||
private _pos: Float64Array<ArrayBuffer> | undefined
|
||||
private _size: Float64Array<ArrayBuffer> | undefined
|
||||
|
||||
constructor(
|
||||
x: number = 0,
|
||||
@@ -78,8 +78,8 @@ export class Rectangle extends Float64Array {
|
||||
* Updating the values of the returned object will update this rectangle.
|
||||
*/
|
||||
get pos(): Point {
|
||||
this.#pos ??= this.subarray(0, 2)
|
||||
return this.#pos! as unknown as Point
|
||||
this._pos ??= this.subarray(0, 2)
|
||||
return this._pos! as unknown as Point
|
||||
}
|
||||
|
||||
set pos(value: Readonly<Point>) {
|
||||
@@ -93,8 +93,8 @@ export class Rectangle extends Float64Array {
|
||||
* Updating the values of the returned object will update this rectangle.
|
||||
*/
|
||||
get size(): Size {
|
||||
this.#size ??= this.subarray(2, 4)
|
||||
return this.#size! as unknown as Size
|
||||
this._size ??= this.subarray(2, 4)
|
||||
return this._size! as unknown as Size
|
||||
}
|
||||
|
||||
set size(value: Readonly<Size>) {
|
||||
|
||||
@@ -23,15 +23,15 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
||||
return !!this.widget
|
||||
}
|
||||
|
||||
#widget: WeakRef<IBaseWidget> | undefined
|
||||
private _widgetRef: WeakRef<IBaseWidget> | undefined
|
||||
|
||||
/** Internal use only; API is not finalised and may change at any time. */
|
||||
get _widget(): IBaseWidget | undefined {
|
||||
return this.#widget?.deref()
|
||||
return this._widgetRef?.deref()
|
||||
}
|
||||
|
||||
set _widget(widget: IBaseWidget | undefined) {
|
||||
this.#widget = widget ? new WeakRef(widget) : undefined
|
||||
this._widgetRef = widget ? new WeakRef(widget) : undefined
|
||||
}
|
||||
|
||||
get collapsedPos(): Readonly<Point> {
|
||||
@@ -79,4 +79,12 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
||||
|
||||
ctx.textAlign = textAlign
|
||||
}
|
||||
|
||||
override toJSON(): INodeInputSlot {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
link: this.link,
|
||||
widget: this.widget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput
|
||||
import { isSubgraphOutput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
|
||||
|
||||
export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
#node: LGraphNode
|
||||
|
||||
links: LinkId[] | null
|
||||
_data?: unknown
|
||||
slot_index?: number
|
||||
@@ -27,7 +25,7 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
|
||||
get collapsedPos(): Readonly<Point> {
|
||||
return [
|
||||
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
|
||||
this._node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
|
||||
LiteGraph.NODE_TITLE_HEIGHT * -0.5
|
||||
]
|
||||
}
|
||||
@@ -40,7 +38,6 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
this.links = slot.links
|
||||
this._data = slot._data
|
||||
this.slot_index = slot.slot_index
|
||||
this.#node = node
|
||||
}
|
||||
|
||||
override isValidTarget(
|
||||
@@ -78,4 +75,12 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
ctx.textAlign = textAlign
|
||||
ctx.strokeStyle = strokeStyle
|
||||
}
|
||||
|
||||
override toJSON(): INodeOutputSlot {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
links: this.links,
|
||||
slot_index: this.slot_index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
pos?: Point
|
||||
|
||||
/** The offset from the parent node to the centre point of this slot. */
|
||||
get #centreOffset(): Readonly<Point> {
|
||||
private get _centreOffset(): Readonly<Point> {
|
||||
const nodePos = this.node.pos
|
||||
const { boundingRect } = this
|
||||
|
||||
@@ -55,9 +55,9 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
/** The center point of this slot when the node is collapsed. */
|
||||
abstract get collapsedPos(): Readonly<Point>
|
||||
|
||||
#node: LGraphNode
|
||||
protected _node: LGraphNode
|
||||
get node(): LGraphNode {
|
||||
return this.#node
|
||||
return this._node
|
||||
}
|
||||
|
||||
get highlightColor(): CanvasColour {
|
||||
@@ -89,7 +89,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
super(name, type, rectangle)
|
||||
|
||||
Object.assign(this, rest)
|
||||
this.#node = node
|
||||
this._node = node
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +126,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
? this.highlightColor
|
||||
: LiteGraph.NODE_TEXT_COLOR
|
||||
|
||||
const pos = this.#centreOffset
|
||||
const pos = this._centreOffset
|
||||
const slot_type = this.type
|
||||
const slot_shape = (
|
||||
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
|
||||
@@ -260,6 +260,25 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
ctx.lineWidth = originalLineWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom JSON serialization to prevent circular reference errors.
|
||||
* Returns only serializable slot properties without the node back-reference.
|
||||
*/
|
||||
toJSON(): INodeSlot {
|
||||
return {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
label: this.label,
|
||||
color_on: this.color_on,
|
||||
color_off: this.color_off,
|
||||
shape: this.shape,
|
||||
dir: this.dir,
|
||||
localized_name: this.localized_name,
|
||||
pos: this.pos,
|
||||
boundingRect: [...this.boundingRect] as [number, number, number, number]
|
||||
}
|
||||
}
|
||||
|
||||
drawCollapsed(ctx: CanvasRenderingContext2D) {
|
||||
const [x, y] = this.collapsedPos
|
||||
|
||||
|
||||
131
src/lib/litegraph/src/serialization.test.ts
Normal file
131
src/lib/litegraph/src/serialization.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode, LiteGraph } from './litegraph'
|
||||
import type { NodeInputSlot } from './node/NodeInputSlot'
|
||||
import type { NodeOutputSlot } from './node/NodeOutputSlot'
|
||||
|
||||
class TestNode extends LGraphNode {
|
||||
static override title = 'TestNode'
|
||||
constructor() {
|
||||
super('TestNode')
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType('test/TestNode', TestNode)
|
||||
|
||||
describe('Serialization - Circular Reference Prevention', () => {
|
||||
describe('LGraph.toJSON()', () => {
|
||||
it('should serialize without circular reference errors', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
expect(() => JSON.stringify(graph)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should return serialize() output from toJSON()', () => {
|
||||
const graph = new LGraph()
|
||||
const serialized = graph.serialize()
|
||||
const jsonOutput = graph.toJSON()
|
||||
|
||||
expect(jsonOutput).toEqual(serialized)
|
||||
})
|
||||
|
||||
it('should not include list_of_graphcanvas in JSON output', () => {
|
||||
const graph = new LGraph()
|
||||
const json = JSON.stringify(graph)
|
||||
const parsed = JSON.parse(json)
|
||||
|
||||
expect(parsed.list_of_graphcanvas).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeSlot.toJSON()', () => {
|
||||
it('NodeInputSlot should serialize without circular reference errors', () => {
|
||||
const graph = new LGraph()
|
||||
const node = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node)
|
||||
node.addInput('test_input', 'TEST')
|
||||
|
||||
const inputSlot = node.inputs[0]
|
||||
|
||||
expect(() => JSON.stringify(inputSlot)).not.toThrow()
|
||||
})
|
||||
|
||||
it('NodeOutputSlot should serialize without circular reference errors', () => {
|
||||
const graph = new LGraph()
|
||||
const node = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node)
|
||||
node.addOutput('test_output', 'TEST')
|
||||
|
||||
const outputSlot = node.outputs[0]
|
||||
|
||||
expect(() => JSON.stringify(outputSlot)).not.toThrow()
|
||||
})
|
||||
|
||||
it('NodeInputSlot.toJSON() should not include _node reference', () => {
|
||||
const graph = new LGraph()
|
||||
const node = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node)
|
||||
node.addInput('test_input', 'TEST')
|
||||
|
||||
const inputSlot = node.inputs[0] as NodeInputSlot
|
||||
const json = inputSlot.toJSON()
|
||||
|
||||
expect('_node' in json).toBe(false)
|
||||
expect('node' in json).toBe(false)
|
||||
expect(json.name).toBe('test_input')
|
||||
expect(json.type).toBe('TEST')
|
||||
})
|
||||
|
||||
it('NodeOutputSlot.toJSON() should not include _node reference', () => {
|
||||
const graph = new LGraph()
|
||||
const node = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node)
|
||||
node.addOutput('test_output', 'TEST')
|
||||
|
||||
const outputSlot = node.outputs[0] as NodeOutputSlot
|
||||
const json = outputSlot.toJSON()
|
||||
|
||||
expect('_node' in json).toBe(false)
|
||||
expect('node' in json).toBe(false)
|
||||
expect(json.name).toBe('test_output')
|
||||
expect(json.type).toBe('TEST')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Full graph with nodes - no circular references', () => {
|
||||
it('should serialize a graph with connected nodes', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
const node1 = LiteGraph.createNode('test/TestNode')!
|
||||
const node2 = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node1)
|
||||
graph.add(node2)
|
||||
|
||||
node1.addOutput('out', 'TEST')
|
||||
node2.addInput('in', 'TEST')
|
||||
|
||||
node1.connect(0, node2, 0)
|
||||
|
||||
expect(() => JSON.stringify(graph)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should serialize graph.serialize() output without errors', () => {
|
||||
const graph = new LGraph()
|
||||
|
||||
const node1 = LiteGraph.createNode('test/TestNode')!
|
||||
const node2 = LiteGraph.createNode('test/TestNode')!
|
||||
graph.add(node1)
|
||||
graph.add(node2)
|
||||
|
||||
node1.addOutput('out', 'TEST')
|
||||
node2.addInput('in', 'TEST')
|
||||
|
||||
node1.connect(0, node2, 0)
|
||||
|
||||
const serialized = graph.serialize()
|
||||
|
||||
expect(() => JSON.stringify(serialized)).not.toThrow()
|
||||
expect(() => JSON.parse(JSON.stringify(serialized))).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -50,7 +50,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
inputs: { linkId: number | null; name: string; type: ISlotType }[]
|
||||
|
||||
/** Backing field for {@link id}. */
|
||||
#id: ExecutionId
|
||||
private _id: ExecutionId
|
||||
|
||||
/**
|
||||
* The path to the actual node through subgraph instances, represented as a list of all subgraph node IDs (instances),
|
||||
@@ -62,7 +62,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
* - `3` is the node ID of the actual node in the subgraph definition
|
||||
*/
|
||||
get id() {
|
||||
return this.#id
|
||||
return this._id
|
||||
}
|
||||
|
||||
get type() {
|
||||
@@ -106,7 +106,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
if (!node.graph) throw new NullGraphError()
|
||||
|
||||
// Set the internal ID of the DTO
|
||||
this.#id = [...this.subgraphNodePath, this.node.id].join(':')
|
||||
this._id = [...this.subgraphNodePath, this.node.id].join(':')
|
||||
this.graph = node.graph
|
||||
this.inputs = this.node.inputs.map((x) => ({
|
||||
linkId: x.link,
|
||||
@@ -265,7 +265,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
// Upstreamed: Bypass nodes are bypassed using the first input with matching type
|
||||
if (this.mode === LGraphEventMode.BYPASS) {
|
||||
// Bypass nodes by finding first input with matching type
|
||||
const matchingIndex = this.#getBypassSlotIndex(slot, type)
|
||||
const matchingIndex = this._getBypassSlotIndex(slot, type)
|
||||
|
||||
// No input types match - bypass not possible
|
||||
if (matchingIndex === -1) {
|
||||
@@ -281,7 +281,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
|
||||
const { node } = this
|
||||
if (node.isSubgraphNode())
|
||||
return this.#resolveSubgraphOutput(slot, type, visited)
|
||||
return this._resolveSubgraphOutput(slot, type, visited)
|
||||
|
||||
if (node.isVirtualNode) {
|
||||
const virtualLink = this.node.getInputLink(slot)
|
||||
@@ -321,7 +321,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
* @param type The type of the final target input (so type list matches are accurate)
|
||||
* @returns The index of the input slot on this node, otherwise `-1`.
|
||||
*/
|
||||
#getBypassSlotIndex(slot: number, type: ISlotType) {
|
||||
private _getBypassSlotIndex(slot: number, type: ISlotType) {
|
||||
const { inputs } = this
|
||||
const oppositeInput = inputs[slot]
|
||||
const outputType = this.node.outputs[slot].type
|
||||
@@ -358,7 +358,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
* @param visited A set of unique IDs to guard against infinite recursion. See {@link resolveInput}.
|
||||
* @returns A DTO for the node, and the origin ID / slot index of the output.
|
||||
*/
|
||||
#resolveSubgraphOutput(
|
||||
private _resolveSubgraphOutput(
|
||||
slot: number,
|
||||
type: ISlotType,
|
||||
visited: Set<string>
|
||||
|
||||
@@ -38,12 +38,12 @@ export abstract class SubgraphIONodeBase<
|
||||
static minWidth = 100
|
||||
static roundedRadius = 10
|
||||
|
||||
readonly #boundingRect: Rectangle = new Rectangle()
|
||||
private readonly _boundingRect: Rectangle = new Rectangle()
|
||||
|
||||
abstract readonly id: NodeId
|
||||
|
||||
get boundingRect(): Rectangle {
|
||||
return this.#boundingRect
|
||||
return this._boundingRect
|
||||
}
|
||||
|
||||
selected: boolean = false
|
||||
@@ -181,7 +181,7 @@ export abstract class SubgraphIONodeBase<
|
||||
): void {
|
||||
// Only allow renaming non-empty slots
|
||||
if (slot !== this.emptySlot) {
|
||||
this.#promptForSlotRename(slot, event)
|
||||
this._promptForSlotRename(slot, event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,14 +191,14 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param event The event that triggered the context menu.
|
||||
*/
|
||||
protected showSlotContextMenu(slot: TSlot, event: CanvasPointerEvent): void {
|
||||
const options: (IContextMenuValue | null)[] = this.#getSlotMenuOptions(slot)
|
||||
const options: (IContextMenuValue | null)[] = this._getSlotMenuOptions(slot)
|
||||
if (!(options.length > 0)) return
|
||||
|
||||
new LiteGraph.ContextMenu(options, {
|
||||
event,
|
||||
title: slot.name || 'Subgraph Output',
|
||||
callback: (item: IContextMenuValue) => {
|
||||
this.#onSlotMenuAction(item, slot, event)
|
||||
this._onSlotMenuAction(item, slot, event)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param slot The slot to get the context menu options for.
|
||||
* @returns The context menu options.
|
||||
*/
|
||||
#getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
|
||||
private _getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
|
||||
const options: (IContextMenuValue | null)[] = []
|
||||
|
||||
// Disconnect option if slot has connections
|
||||
@@ -239,7 +239,7 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param slot The slot
|
||||
* @param event The event that triggered the context menu.
|
||||
*/
|
||||
#onSlotMenuAction(
|
||||
private _onSlotMenuAction(
|
||||
selectedItem: IContextMenuValue,
|
||||
slot: TSlot,
|
||||
event: CanvasPointerEvent
|
||||
@@ -260,7 +260,7 @@ export abstract class SubgraphIONodeBase<
|
||||
// Rename the slot
|
||||
case 'rename':
|
||||
if (slot !== this.emptySlot) {
|
||||
this.#promptForSlotRename(slot, event)
|
||||
this._promptForSlotRename(slot, event)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -273,7 +273,7 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param slot The slot to rename.
|
||||
* @param event The event that triggered the rename.
|
||||
*/
|
||||
#promptForSlotRename(slot: TSlot, event: CanvasPointerEvent): void {
|
||||
private _promptForSlotRename(slot: TSlot, event: CanvasPointerEvent): void {
|
||||
this.subgraph.canvasAction((c) =>
|
||||
c.prompt(
|
||||
'Slot name',
|
||||
@@ -362,7 +362,7 @@ export abstract class SubgraphIONodeBase<
|
||||
}
|
||||
|
||||
configure(data: ExportedSubgraphIONode): void {
|
||||
this.#boundingRect.set(data.bounding)
|
||||
this._boundingRect.set(data.bounding)
|
||||
this.pinned = data.pinned ?? false
|
||||
}
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ export class SubgraphInput extends SubgraphSlot {
|
||||
events = new CustomEventTarget<SubgraphInputEventMap>()
|
||||
|
||||
/** The linked widget that this slot is connected to. */
|
||||
#widgetRef?: WeakRef<IBaseWidget>
|
||||
private _widgetRef?: WeakRef<IBaseWidget>
|
||||
|
||||
get _widget() {
|
||||
return this.#widgetRef?.deref()
|
||||
return this._widgetRef?.deref()
|
||||
}
|
||||
|
||||
set _widget(widget) {
|
||||
this.#widgetRef = widget ? new WeakRef(widget) : undefined
|
||||
this._widgetRef = widget ? new WeakRef(widget) : undefined
|
||||
}
|
||||
|
||||
override connect(
|
||||
@@ -187,7 +187,7 @@ export class SubgraphInput extends SubgraphSlot {
|
||||
* @returns `true` if the connection is valid, otherwise `false`.
|
||||
*/
|
||||
matchesWidget(otherWidget: IBaseWidget): boolean {
|
||||
const widget = this.#widgetRef?.deref()
|
||||
const widget = this._widgetRef?.deref()
|
||||
if (!widget) return true
|
||||
|
||||
if (
|
||||
|
||||
@@ -63,7 +63,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
override widgets: IBaseWidget[] = []
|
||||
|
||||
/** Manages lifecycle of all subgraph event listeners */
|
||||
#eventAbortController = new AbortController()
|
||||
private _eventAbortController = new AbortController()
|
||||
|
||||
constructor(
|
||||
/** The (sub)graph that contains this subgraph instance. */
|
||||
@@ -76,7 +76,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
// Update this node when the subgraph input / output slots are changed
|
||||
const subgraphEvents = this.subgraph.events
|
||||
const { signal } = this.#eventAbortController
|
||||
const { signal } = this._eventAbortController
|
||||
|
||||
subgraphEvents.addEventListener(
|
||||
'input-added',
|
||||
@@ -89,12 +89,12 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
|
||||
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
|
||||
if (widget)
|
||||
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
|
||||
this._setWidget(subgraphInput, existingInput, widget, input?.widget)
|
||||
return
|
||||
}
|
||||
const input = this.addInput(name, type)
|
||||
|
||||
this.#addSubgraphInputListeners(subgraphInput, input)
|
||||
this._addSubgraphInputListeners(subgraphInput, input)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
@@ -179,7 +179,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
}
|
||||
|
||||
#addSubgraphInputListeners(
|
||||
private _addSubgraphInputListeners(
|
||||
subgraphInput: SubgraphInput,
|
||||
input: INodeInputSlot & Partial<ISubgraphInput>
|
||||
) {
|
||||
@@ -201,7 +201,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
if (!widget) return
|
||||
|
||||
const widgetLocator = e.detail.input.widget
|
||||
this.#setWidget(subgraphInput, input, widget, widgetLocator)
|
||||
this._setWidget(subgraphInput, input, widget, widgetLocator)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
@@ -288,7 +288,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
continue
|
||||
}
|
||||
|
||||
this.#addSubgraphInputListeners(subgraphInput, input)
|
||||
this._addSubgraphInputListeners(subgraphInput, input)
|
||||
|
||||
// Find the first widget that this slot is connected to
|
||||
for (const linkId of subgraphInput.linkIds) {
|
||||
@@ -318,13 +318,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const widget = inputNode.getWidgetFromSlot(targetInput)
|
||||
if (!widget) continue
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget, targetInput.widget)
|
||||
this._setWidget(subgraphInput, input, widget, targetInput.widget)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setWidget(
|
||||
private _setWidget(
|
||||
subgraphInput: Readonly<SubgraphInput>,
|
||||
input: INodeInputSlot,
|
||||
widget: Readonly<IBaseWidget>,
|
||||
@@ -553,7 +553,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
override onRemoved(): void {
|
||||
// Clean up all subgraph event listeners
|
||||
this.#eventAbortController.abort()
|
||||
this._eventAbortController.abort()
|
||||
|
||||
// Clean up all promoted widgets
|
||||
for (const widget of this.widgets) {
|
||||
|
||||
@@ -46,7 +46,7 @@ export abstract class SubgraphSlot
|
||||
return LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
readonly #pos: Point = [0, 0]
|
||||
private readonly _pos: Point = [0, 0]
|
||||
|
||||
readonly measurement: ConstrainedSize = new ConstrainedSize(
|
||||
SubgraphSlot.defaultHeight,
|
||||
@@ -67,14 +67,14 @@ export abstract class SubgraphSlot
|
||||
)
|
||||
|
||||
override get pos() {
|
||||
return this.#pos
|
||||
return this._pos
|
||||
}
|
||||
|
||||
override set pos(value) {
|
||||
if (!value || value.length < 2) return
|
||||
|
||||
this.#pos[0] = value[0]
|
||||
this.#pos[1] = value[1]
|
||||
this._pos[0] = value[0]
|
||||
this._pos[1] = value[1]
|
||||
}
|
||||
|
||||
/** Whether this slot is connected to another slot. */
|
||||
|
||||
@@ -57,10 +57,10 @@ export abstract class BaseWidget<
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
#node: LGraphNode
|
||||
private _node: LGraphNode
|
||||
/** The node that this widget belongs to. */
|
||||
get node() {
|
||||
return this.#node
|
||||
return this._node
|
||||
}
|
||||
|
||||
linkedWidgets?: IBaseWidget[]
|
||||
@@ -97,20 +97,20 @@ export abstract class BaseWidget<
|
||||
canvas: LGraphCanvas
|
||||
): boolean
|
||||
|
||||
#value?: TWidget['value']
|
||||
private _value?: TWidget['value']
|
||||
get value(): TWidget['value'] {
|
||||
return this.#value
|
||||
return this._value
|
||||
}
|
||||
|
||||
set value(value: TWidget['value']) {
|
||||
this.#value = value
|
||||
this._value = value
|
||||
}
|
||||
|
||||
constructor(widget: TWidget & { node: LGraphNode })
|
||||
constructor(widget: TWidget, node: LGraphNode)
|
||||
constructor(widget: TWidget & { node: LGraphNode }, node?: LGraphNode) {
|
||||
// Private fields
|
||||
this.#node = node ?? widget.node
|
||||
this._node = node ?? widget.node
|
||||
|
||||
// The set and get functions for DOM widget values are hacked on to the options object;
|
||||
// attempting to set value before options will throw.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user