Compare commits
15 Commits
test/node-
...
BulkDownlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5441f70cd5 | ||
|
|
0e3314bbd3 | ||
|
|
8f301ec94b | ||
|
|
17c1b1f989 | ||
|
|
a4b725b85e | ||
|
|
8283438ee6 | ||
|
|
d05e4eac58 | ||
|
|
7f509cc018 | ||
|
|
23c8757447 | ||
|
|
7d3d00858a | ||
|
|
478cfc0b5e | ||
|
|
90a701dd67 | ||
|
|
7f81e1afac | ||
|
|
e26283e754 | ||
|
|
1ca6e57ac4 |
@@ -109,7 +109,7 @@ jobs:
|
||||
# Run sharded tests with snapshot updates (browsers pre-installed in container)
|
||||
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright-tests
|
||||
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
run: pnpm exec playwright test --update-snapshots --grep @screenshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stage changed snapshot files
|
||||
|
||||
@@ -60,11 +60,6 @@
|
||||
{
|
||||
"name": "primevue/sidebar",
|
||||
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
|
||||
},
|
||||
{
|
||||
"name": "@/i18n--to-enable",
|
||||
"importNames": ["st", "t", "te", "d"],
|
||||
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
import { nextTick, provide } from 'vue'
|
||||
import type { ElectronWindow } from '@/utils/envUtil'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import InstallView from './InstallView.vue'
|
||||
@@ -42,16 +44,21 @@ const meta: Meta<typeof InstallView> = {
|
||||
const router = createMockRouter()
|
||||
|
||||
// Mock electron API
|
||||
;(window as any).electronAPI = {
|
||||
;(window as ElectronWindow).electronAPI = {
|
||||
getPlatform: () => 'darwin',
|
||||
Config: {
|
||||
getDetectedGpu: () => Promise.resolve('mps')
|
||||
},
|
||||
Events: {
|
||||
trackEvent: (_eventName: string, _data?: any) => {}
|
||||
trackEvent: (
|
||||
_eventName: string,
|
||||
_data?: Record<string, unknown>
|
||||
) => {}
|
||||
},
|
||||
installComfyUI: (_options: any) => {},
|
||||
changeTheme: (_theme: any) => {},
|
||||
installComfyUI: (
|
||||
_options: Parameters<ElectronAPI['installComfyUI']>[0]
|
||||
) => {},
|
||||
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
|
||||
getSystemPaths: () =>
|
||||
Promise.resolve({
|
||||
defaultInstallPath: '/Users/username/ComfyUI'
|
||||
@@ -240,8 +247,8 @@ export const DesktopSettings: Story = {
|
||||
export const WindowsPlatform: Story = {
|
||||
render: () => {
|
||||
// Override the platform to Windows
|
||||
;(window as any).electronAPI.getPlatform = () => 'win32'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
|
||||
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('nvidia')
|
||||
|
||||
return {
|
||||
@@ -259,8 +266,8 @@ export const MacOSPlatform: Story = {
|
||||
name: 'macOS Platform',
|
||||
render: () => {
|
||||
// Override the platform to macOS
|
||||
;(window as any).electronAPI.getPlatform = () => 'darwin'
|
||||
;(window as any).electronAPI.Config.getDetectedGpu = () =>
|
||||
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
|
||||
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
|
||||
Promise.resolve('mps')
|
||||
|
||||
return {
|
||||
@@ -327,7 +334,7 @@ export const ManualInstall: Story = {
|
||||
export const ErrorState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return an error
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: false,
|
||||
exists: false,
|
||||
@@ -375,7 +382,7 @@ export const ErrorState: Story = {
|
||||
export const WarningState: Story = {
|
||||
render: () => {
|
||||
// Override validation to return a warning about non-default drive
|
||||
;(window as any).electronAPI.validateInstallPath = () =>
|
||||
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
|
||||
Promise.resolve({
|
||||
isValid: true,
|
||||
exists: false,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
interface TestWindow extends Window {
|
||||
__ws__?: Record<string, WebSocket>
|
||||
}
|
||||
|
||||
export const webSocketFixture = base.extend<{
|
||||
ws: { trigger(data: unknown, url?: string): Promise<void> }
|
||||
}>({
|
||||
|
||||
162
browser_tests/tests/nodeGhostPlacement.spec.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
type ComfyPage = Parameters<Parameters<typeof test>[2]>[0]['comfyPage']
|
||||
|
||||
async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
|
||||
if (enabled) {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
}
|
||||
|
||||
async function addGhostAtCenter(comfyPage: ComfyPage) {
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const viewport = comfyPage.page.viewportSize()!
|
||||
const centerX = Math.round(viewport.width / 2)
|
||||
const centerY = Math.round(viewport.height / 2)
|
||||
|
||||
await comfyPage.page.mouse.move(centerX, centerY)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const nodeId = await comfyPage.page.evaluate(
|
||||
([clientX, clientY]) => {
|
||||
const node = window.LiteGraph!.createNode('VAEDecode')!
|
||||
const event = new MouseEvent('click', { clientX, clientY })
|
||||
window.app!.graph.add(node, { ghost: true, dragEvent: event })
|
||||
return node.id
|
||||
},
|
||||
[centerX, centerY] as const
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
return { nodeId, centerX, centerY }
|
||||
}
|
||||
|
||||
function getNodeById(comfyPage: ComfyPage, nodeId: number | string) {
|
||||
return comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph.getNodeById(id)
|
||||
if (!node) return null
|
||||
return { ghost: !!node.flags.ghost }
|
||||
}, nodeId)
|
||||
}
|
||||
|
||||
for (const mode of ['litegraph', 'vue'] as const) {
|
||||
test.describe(`Ghost node placement (${mode} mode)`, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await setVueMode(comfyPage, mode === 'vue')
|
||||
})
|
||||
|
||||
test('positions ghost node at cursor', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const viewport = comfyPage.page.viewportSize()!
|
||||
const centerX = Math.round(viewport.width / 2)
|
||||
const centerY = Math.round(viewport.height / 2)
|
||||
|
||||
await comfyPage.page.mouse.move(centerX, centerY)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const result = await comfyPage.page.evaluate(
|
||||
([clientX, clientY]) => {
|
||||
const node = window.LiteGraph!.createNode('VAEDecode')!
|
||||
const event = new MouseEvent('click', { clientX, clientY })
|
||||
window.app!.graph.add(node, { ghost: true, dragEvent: event })
|
||||
|
||||
const canvas = window.app!.canvas
|
||||
const rect = canvas.canvas.getBoundingClientRect()
|
||||
const cursorCanvasX =
|
||||
(clientX - rect.left) / canvas.ds.scale - canvas.ds.offset[0]
|
||||
const cursorCanvasY =
|
||||
(clientY - rect.top) / canvas.ds.scale - canvas.ds.offset[1]
|
||||
|
||||
return {
|
||||
diffX: node.pos[0] + node.size[0] / 2 - cursorCanvasX,
|
||||
diffY: node.pos[1] - 10 - cursorCanvasY
|
||||
}
|
||||
},
|
||||
[centerX, centerY] as const
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(Math.abs(result.diffX)).toBeLessThan(5)
|
||||
expect(Math.abs(result.diffY)).toBeLessThan(5)
|
||||
})
|
||||
|
||||
test('left-click confirms ghost placement', async ({ comfyPage }) => {
|
||||
const { nodeId, centerX, centerY } = await addGhostAtCenter(comfyPage)
|
||||
|
||||
const before = await getNodeById(comfyPage, nodeId)
|
||||
expect(before).not.toBeNull()
|
||||
expect(before!.ghost).toBe(true)
|
||||
|
||||
await comfyPage.page.mouse.click(centerX, centerY)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await getNodeById(comfyPage, nodeId)
|
||||
expect(after).not.toBeNull()
|
||||
expect(after!.ghost).toBe(false)
|
||||
})
|
||||
|
||||
test('Escape cancels ghost placement', async ({ comfyPage }) => {
|
||||
const { nodeId } = await addGhostAtCenter(comfyPage)
|
||||
|
||||
const before = await getNodeById(comfyPage, nodeId)
|
||||
expect(before).not.toBeNull()
|
||||
expect(before!.ghost).toBe(true)
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await getNodeById(comfyPage, nodeId)
|
||||
expect(after).toBeNull()
|
||||
})
|
||||
|
||||
test('Delete cancels ghost placement', async ({ comfyPage }) => {
|
||||
const { nodeId } = await addGhostAtCenter(comfyPage)
|
||||
|
||||
const before = await getNodeById(comfyPage, nodeId)
|
||||
expect(before).not.toBeNull()
|
||||
expect(before!.ghost).toBe(true)
|
||||
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await getNodeById(comfyPage, nodeId)
|
||||
expect(after).toBeNull()
|
||||
})
|
||||
|
||||
test('Backspace cancels ghost placement', async ({ comfyPage }) => {
|
||||
const { nodeId } = await addGhostAtCenter(comfyPage)
|
||||
|
||||
const before = await getNodeById(comfyPage, nodeId)
|
||||
expect(before).not.toBeNull()
|
||||
expect(before!.ghost).toBe(true)
|
||||
|
||||
await comfyPage.page.keyboard.press('Backspace')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await getNodeById(comfyPage, nodeId)
|
||||
expect(after).toBeNull()
|
||||
})
|
||||
|
||||
test('right-click cancels ghost placement', async ({ comfyPage }) => {
|
||||
const { nodeId, centerX, centerY } = await addGhostAtCenter(comfyPage)
|
||||
|
||||
const before = await getNodeById(comfyPage, nodeId)
|
||||
expect(before).not.toBeNull()
|
||||
expect(before!.ghost).toBe(true)
|
||||
|
||||
await comfyPage.page.mouse.click(centerX, centerY, { button: 'right' })
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const after = await getNodeById(comfyPage, nodeId)
|
||||
expect(after).toBeNull()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 103 KiB |
@@ -31,7 +31,12 @@ test.describe('Vue Integer Widget', () => {
|
||||
await expect(seedWidget).toBeVisible()
|
||||
|
||||
// Delete the node that is linked to the slot (freeing up the widget)
|
||||
await comfyPage.vueNodes.getNodeByTitle('Int').click()
|
||||
// Click on the header to select the node (clicking center may land on
|
||||
// the widget area where pointerdown.stop prevents node selection)
|
||||
await comfyPage.vueNodes
|
||||
.getNodeByTitle('Int')
|
||||
.locator('.lg-node-header')
|
||||
.click()
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Test widget works when unlinked
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
@@ -279,5 +279,46 @@ export default defineConfig([
|
||||
'import-x/no-duplicates': 'off',
|
||||
'import-x/consistent-type-specifier-style': 'off'
|
||||
}
|
||||
},
|
||||
|
||||
// i18n import enforcement
|
||||
// Vue components must use the useI18n() composable, not the global t/d/st/te
|
||||
{
|
||||
files: ['**/*.vue'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@/i18n',
|
||||
importNames: ['t', 'd', 'st', 'te'],
|
||||
message:
|
||||
"In Vue components, use `const { t } = useI18n()` instead of importing from '@/i18n'."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
// Non-composable .ts files must use the global t/d/st/te, not useI18n()
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
ignores: ['**/use[A-Z]*.ts', '**/*.test.ts', 'src/i18n.ts'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'vue-i18n',
|
||||
importNames: ['useI18n'],
|
||||
message:
|
||||
"useI18n() requires Vue setup context. Use `import { t } from '@/i18n'` instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.39.7",
|
||||
"version": "1.39.8",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
1
packages/design-system/src/icons/extensions-blocks.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path d="M6.66667 14V5.33333C6.66667 5.15652 6.59643 4.98695 6.4714 4.86193C6.34638 4.7369 6.17681 4.66667 6 4.66667H2.66667C2.48986 4.66667 2.32029 4.7369 2.19526 4.86193C2.07024 4.98695 2 5.15652 2 5.33333V13.3333C2 13.5101 2.07024 13.6797 2.19526 13.8047C2.32029 13.9298 2.48986 14 2.66667 14H10.6667C10.8435 14 11.013 13.9298 11.1381 13.8047C11.2631 13.6797 11.3333 13.5101 11.3333 13.3333V10C11.3333 9.82319 11.2631 9.65362 11.1381 9.5286C11.013 9.40357 10.8435 9.33333 10.6667 9.33333H2M10 2H13.3333C13.7015 2 14 2.29848 14 2.66667V6C14 6.36819 13.7015 6.66667 13.3333 6.66667H10C9.63181 6.66667 9.33333 6.36819 9.33333 6V2.66667C9.33333 2.29848 9.63181 2 10 2Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 837 B |
@@ -2,10 +2,7 @@
|
||||
import { execSync } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import { globSync } from 'glob'
|
||||
|
||||
interface LocaleData {
|
||||
[key: string]: any
|
||||
}
|
||||
import type { LocaleData } from './i18n-types'
|
||||
|
||||
// Configuration
|
||||
const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
|
||||
@@ -45,7 +42,7 @@ function getStagedLocaleFiles(): string[] {
|
||||
}
|
||||
|
||||
// Extract all keys from a nested object
|
||||
function extractKeys(obj: any, prefix = ''): string[] {
|
||||
function extractKeys(obj: LocaleData, prefix = ''): string[] {
|
||||
const keys: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
@@ -166,17 +163,17 @@ async function checkNewUnusedKeys() {
|
||||
|
||||
// Report results
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.log('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
console.warn('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
console.log(` - ${key}`)
|
||||
console.warn(` - ${key}`)
|
||||
}
|
||||
|
||||
console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
|
||||
console.log(
|
||||
console.warn(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
|
||||
console.warn(
|
||||
'\nThese keys were added but are not used anywhere in the codebase.'
|
||||
)
|
||||
console.log('Consider using them or removing them in a future update.')
|
||||
console.warn('Consider using them or removing them in a future update.')
|
||||
|
||||
// Changed from process.exit(1) to process.exit(0) for warning only
|
||||
process.exit(0)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
writeFileSync
|
||||
} from 'fs'
|
||||
import { dirname, join } from 'path'
|
||||
import type { LocaleData } from './i18n-types'
|
||||
|
||||
// Ensure directories exist
|
||||
function ensureDir(dir: string) {
|
||||
@@ -41,8 +42,8 @@ function getAllJsonFiles(dir: string): string[] {
|
||||
}
|
||||
|
||||
// Find additions in new object compared to base
|
||||
function findAdditions(base: any, updated: any): Record<string, any> {
|
||||
const additions: Record<string, any> = {}
|
||||
function findAdditions(base: LocaleData, updated: LocaleData): LocaleData {
|
||||
const additions: LocaleData = {}
|
||||
|
||||
for (const key in updated) {
|
||||
if (!(key in base)) {
|
||||
@@ -74,7 +75,7 @@ function capture(srcLocaleDir: string, tempBaseDir: string) {
|
||||
ensureDir(dirname(targetPath))
|
||||
writeFileSync(targetPath, readFileSync(file, 'utf8'))
|
||||
}
|
||||
console.log('Captured current locale files to temp/base/')
|
||||
console.warn('Captured current locale files to temp/base/')
|
||||
}
|
||||
|
||||
// Diff command
|
||||
@@ -94,7 +95,7 @@ function diff(srcLocaleDir: string, tempBaseDir: string, tempDiffDir: string) {
|
||||
if (Object.keys(additions).length > 0) {
|
||||
ensureDir(dirname(diffPath))
|
||||
writeFileSync(diffPath, JSON.stringify(additions, null, 2))
|
||||
console.log(`Wrote diff to ${diffPath}`)
|
||||
console.warn(`Wrote diff to ${diffPath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,9 +117,9 @@ switch (command) {
|
||||
// Remove temp directory recursively
|
||||
if (existsSync('temp')) {
|
||||
rmSync('temp', { recursive: true, force: true })
|
||||
console.log('Removed temp directory')
|
||||
console.warn('Removed temp directory')
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.log('Please specify either "capture" or "diff" command')
|
||||
console.error('Please specify either "capture" or "diff" command')
|
||||
}
|
||||
|
||||
5
scripts/i18n-types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Shared types for i18n-related scripts
|
||||
*/
|
||||
|
||||
export type LocaleData = { [key: string]: string | LocaleData }
|
||||
@@ -19,12 +19,14 @@
|
||||
<Button
|
||||
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('menu.customNodesManager')"
|
||||
:aria-label="t('menu.manageExtensions')"
|
||||
class="relative"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[lucide--puzzle] size-4" />
|
||||
<i class="icon-[comfy--extensions-blocks] size-4" />
|
||||
<span class="not-md:hidden">
|
||||
{{ t('menu.manageExtensions') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="shouldShowRedDot"
|
||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||
@@ -220,7 +222,7 @@ const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
)
|
||||
const customNodesManagerTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.customNodesManager'))
|
||||
buildTooltipConfig(t('menu.manageExtensions'))
|
||||
)
|
||||
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
const queueContextMenuItems = computed<MenuItem[]>(() => [
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<script setup lang="ts">
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useMediaCache } from '@/services/mediaCacheService'
|
||||
@@ -55,7 +56,7 @@ const {
|
||||
alt?: string
|
||||
containerClass?: ClassValue
|
||||
imageClass?: ClassValue
|
||||
imageStyle?: Record<string, any>
|
||||
imageStyle?: StyleValue
|
||||
rootMargin?: string
|
||||
}>()
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async function showTooltip(tooltip: string | null | undefined) {
|
||||
function onIdle() {
|
||||
const { canvas } = comfyApp
|
||||
const node = canvas?.node_over
|
||||
if (!node) return
|
||||
if (!node || node.flags?.ghost) return
|
||||
|
||||
const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type ?? '']
|
||||
|
||||
@@ -131,7 +131,7 @@ const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
|
||||
onModelDrop: async (file) => {
|
||||
await viewer.handleModelDrop(file)
|
||||
},
|
||||
disabled: viewer.isPreview.value || isStandaloneMode
|
||||
disabled: viewer.isPreview.value || !!isStandaloneMode
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -158,7 +158,7 @@ export const Queued: Story = {
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
return { args: { ...args, jobId } }
|
||||
},
|
||||
@@ -217,7 +217,7 @@ export const QueuedParallel: Story = {
|
||||
prompt_id: 'p2'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
return { args: { ...args, jobId } }
|
||||
},
|
||||
@@ -258,7 +258,7 @@ export const Running: Story = {
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
return { args: { ...args, jobId } }
|
||||
},
|
||||
@@ -303,7 +303,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
return { args: { ...args, jobId } }
|
||||
},
|
||||
@@ -360,7 +360,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
|
||||
prompt_id: 'p2'
|
||||
}
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
return { args: { ...args, jobId } }
|
||||
},
|
||||
|
||||
@@ -138,7 +138,6 @@ describe('flatAndCategorizeSelectedItems', () => {
|
||||
expect(result.nodes).toEqual([testNode1])
|
||||
expect(result.groups).toEqual([testGroup1, testGroup2])
|
||||
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup2)
|
||||
expect(result.nodeToParentGroup.has(testGroup2 as any)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle mixed selection of nodes and groups', () => {
|
||||
|
||||
@@ -230,7 +230,7 @@ const extraMenuItems = computed(() => [
|
||||
{
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'icon-[lucide--puzzle]',
|
||||
icon: 'icon-[comfy--extensions-blocks]',
|
||||
command: showManageExtensions
|
||||
}
|
||||
])
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface SafeWidgetData {
|
||||
isDOMWidget?: boolean
|
||||
label?: string
|
||||
nodeType?: string
|
||||
options?: IWidgetOptions<unknown>
|
||||
options?: IWidgetOptions
|
||||
spec?: InputSpec
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
}
|
||||
@@ -70,6 +70,7 @@ export interface VueNodeData {
|
||||
color?: string
|
||||
flags?: {
|
||||
collapsed?: boolean
|
||||
ghost?: boolean
|
||||
pinned?: boolean
|
||||
}
|
||||
hasErrors?: boolean
|
||||
@@ -145,7 +146,7 @@ interface SharedWidgetEnhancements {
|
||||
/** Widget label */
|
||||
label?: string
|
||||
/** Widget options */
|
||||
options?: Record<string, any>
|
||||
options?: IWidgetOptions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +171,7 @@ export function getSharedWidgetEnhancements(
|
||||
? 'ring ring-component-node-widget-advanced'
|
||||
: undefined,
|
||||
label: widget.label,
|
||||
options: widget.options
|
||||
options: widget.options as IWidgetOptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +433,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
} else {
|
||||
// Not during workflow loading - initialize layout immediately
|
||||
// This handles individual node additions during normal operation
|
||||
requestAnimationFrame(initializeVueNodeLayout)
|
||||
initializeVueNodeLayout()
|
||||
}
|
||||
|
||||
// Call original callback if provided
|
||||
@@ -526,6 +527,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'flags.ghost':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
flags: {
|
||||
...currentData.flags,
|
||||
ghost: Boolean(propertyEvent.newValue)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'flags.pinned':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
||||
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
|
||||
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
|
||||
@@ -33,10 +32,7 @@ function useVueNodeLifecycleIndividual() {
|
||||
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
||||
id: node.id.toString(),
|
||||
pos: [node.pos[0], node.pos[1]] as [number, number],
|
||||
size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [
|
||||
number,
|
||||
number
|
||||
]
|
||||
size: [node.size[0], node.size[1]] as [number, number]
|
||||
}))
|
||||
layoutStore.initializeFromLiteGraph(nodes)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useComputedWithWidgetWatch = (
|
||||
const { widgetNames, triggerCanvasRedraw = false } = options
|
||||
|
||||
// Create a reactive trigger based on widget values
|
||||
const widgetValues = ref<Record<string, any>>({})
|
||||
const widgetValues = ref<Record<string, unknown>>({})
|
||||
|
||||
// Initialize widget observers
|
||||
if (node.widgets) {
|
||||
@@ -56,7 +56,7 @@ export const useComputedWithWidgetWatch = (
|
||||
: node.widgets
|
||||
|
||||
// Initialize current values
|
||||
const currentValues: Record<string, any> = {}
|
||||
const currentValues: Record<string, unknown> = {}
|
||||
widgetsToObserve.forEach((widget) => {
|
||||
currentValues[widget.name] = widget.value
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Ref } from 'vue'
|
||||
|
||||
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -33,7 +33,7 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
|
||||
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
const pos = [...basePos]
|
||||
const pos: Point = [...basePos]
|
||||
// Add an offset on y to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
@@ -102,6 +102,16 @@ export interface LGraphConfig {
|
||||
links_ontop?: boolean
|
||||
}
|
||||
|
||||
/** Options for {@link LGraph.add} method. */
|
||||
interface GraphAddOptions {
|
||||
/** If true, skip recomputing execution order after adding the node. */
|
||||
skipComputeOrder?: boolean
|
||||
/** If true, the node will be semi-transparent and follow the cursor until placed or cancelled. */
|
||||
ghost?: boolean
|
||||
/** Mouse event for ghost placement. Used to position node under cursor. */
|
||||
dragEvent?: MouseEvent
|
||||
}
|
||||
|
||||
export interface GroupNodeConfigEntry {
|
||||
input?: Record<string, { name?: string; visible?: boolean }>
|
||||
output?: Record<number, { name?: string; visible?: boolean }>
|
||||
@@ -862,12 +872,35 @@ export class LGraph
|
||||
/**
|
||||
* Adds a new node instance to this graph
|
||||
* @param node the instance of the node
|
||||
* @param options Additional options for adding the node
|
||||
*/
|
||||
add(
|
||||
node: LGraphNode | LGraphGroup,
|
||||
skip_compute_order?: boolean
|
||||
options?: GraphAddOptions
|
||||
): LGraphNode | null | undefined
|
||||
/**
|
||||
* Adds a new node instance to this graph
|
||||
* @param node the instance of the node
|
||||
* @param skipComputeOrder If true, skip recomputing execution order
|
||||
* @deprecated Use options object instead
|
||||
*/
|
||||
add(
|
||||
node: LGraphNode | LGraphGroup,
|
||||
skipComputeOrder?: boolean
|
||||
): LGraphNode | null | undefined
|
||||
add(
|
||||
node: LGraphNode | LGraphGroup,
|
||||
skipComputeOrderOrOptions?: boolean | GraphAddOptions
|
||||
): LGraphNode | null | undefined {
|
||||
if (!node) return
|
||||
|
||||
// Handle backwards compatibility: 2nd arg can be boolean or options
|
||||
const opts: GraphAddOptions =
|
||||
typeof skipComputeOrderOrOptions === 'object'
|
||||
? skipComputeOrderOrOptions
|
||||
: { skipComputeOrder: skipComputeOrderOrOptions ?? false }
|
||||
const shouldSkipComputeOrder = opts.skipComputeOrder ?? false
|
||||
|
||||
const { state } = this
|
||||
|
||||
// Ensure created items are snapped
|
||||
@@ -914,6 +947,11 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
// Set ghost flag before registration so VueNodeData picks it up
|
||||
if (opts.ghost) {
|
||||
node.flags.ghost = true
|
||||
}
|
||||
|
||||
node.graph = this
|
||||
this._version++
|
||||
|
||||
@@ -924,13 +962,17 @@ export class LGraph
|
||||
|
||||
if (this.config.align_to_grid) node.alignToGrid()
|
||||
|
||||
if (!skip_compute_order) this.updateExecutionOrder()
|
||||
if (!shouldSkipComputeOrder) this.updateExecutionOrder()
|
||||
|
||||
this.onNodeAdded?.(node)
|
||||
|
||||
this.setDirtyCanvas(true)
|
||||
this.change()
|
||||
|
||||
if (opts.ghost) {
|
||||
this.canvasAction((c) => c.startGhostPlacement(node, opts.dragEvent))
|
||||
}
|
||||
|
||||
// to chain actions
|
||||
return node
|
||||
}
|
||||
|
||||
210
src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
layoutStore: {
|
||||
querySlotAtPoint: vi.fn(),
|
||||
queryRerouteAtPoint: vi.fn(),
|
||||
getNodeLayoutRef: vi.fn(() => ({ value: null })),
|
||||
getSlotLayout: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('LGraphCanvas slot hit detection', () => {
|
||||
let graph: LGraph
|
||||
let canvas: LGraphCanvas
|
||||
let node: LGraphNode
|
||||
let canvasElement: HTMLCanvasElement
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
canvasElement = document.createElement('canvas')
|
||||
canvasElement.width = 800
|
||||
canvasElement.height = 600
|
||||
|
||||
const ctx = {
|
||||
save: vi.fn(),
|
||||
restore: vi.fn(),
|
||||
translate: vi.fn(),
|
||||
scale: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
strokeRect: vi.fn(),
|
||||
fillText: vi.fn(),
|
||||
measureText: vi.fn().mockReturnValue({ width: 50 }),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
closePath: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
rect: vi.fn(),
|
||||
clip: vi.fn(),
|
||||
clearRect: vi.fn(),
|
||||
setTransform: vi.fn(),
|
||||
roundRect: vi.fn(),
|
||||
getTransform: vi
|
||||
.fn()
|
||||
.mockReturnValue({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
|
||||
font: '',
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 1,
|
||||
globalAlpha: 1,
|
||||
textAlign: 'left' as CanvasTextAlign,
|
||||
textBaseline: 'alphabetic' as CanvasTextBaseline
|
||||
} as unknown as CanvasRenderingContext2D
|
||||
|
||||
canvasElement.getContext = vi.fn().mockReturnValue(ctx)
|
||||
canvasElement.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
graph = new LGraph()
|
||||
canvas = new LGraphCanvas(canvasElement, graph, {
|
||||
skip_render: true
|
||||
})
|
||||
|
||||
// Create a test node with an output slot
|
||||
node = new LGraphNode('Test Node')
|
||||
node.pos = [100, 100]
|
||||
node.size = [150, 80]
|
||||
node.addOutput('output', 'number')
|
||||
graph.add(node)
|
||||
|
||||
// Enable Vue nodes mode for the test
|
||||
LiteGraph.vueNodesMode = true
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
LiteGraph.vueNodesMode = false
|
||||
})
|
||||
|
||||
describe('processMouseDown slot fallback in Vue nodes mode', () => {
|
||||
it('should query layoutStore.querySlotAtPoint when clicking outside node bounds', () => {
|
||||
// Click position outside node bounds (node is at 100,100 with size 150x80)
|
||||
// So node covers x: 100-250, y: 100-180
|
||||
// Click at x=255 is outside the right edge
|
||||
const clickX = 255
|
||||
const clickY = 120
|
||||
|
||||
// Verify the click is outside the node bounds
|
||||
expect(node.isPointInside(clickX, clickY)).toBe(false)
|
||||
expect(graph.getNodeOnPos(clickX, clickY)).toBeNull()
|
||||
|
||||
// Mock the slot query to return our node's slot
|
||||
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
|
||||
nodeId: String(node.id),
|
||||
index: 0,
|
||||
type: 'output',
|
||||
position: { x: 252, y: 120 },
|
||||
bounds: { x: 246, y: 110, width: 20, height: 20 }
|
||||
})
|
||||
|
||||
// Call processMouseDown - this should trigger the slot fallback
|
||||
canvas.processMouseDown(
|
||||
new MouseEvent('pointerdown', {
|
||||
button: 1, // Middle button
|
||||
clientX: clickX,
|
||||
clientY: clickY
|
||||
})
|
||||
)
|
||||
|
||||
// The fix should query the layout store when no node is found at click position
|
||||
expect(layoutStore.querySlotAtPoint).toHaveBeenCalledWith({
|
||||
x: clickX,
|
||||
y: clickY
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT query layoutStore when node is found directly at click position', () => {
|
||||
// Initialize node's bounding rect
|
||||
node.updateArea()
|
||||
|
||||
// Populate visible_nodes (normally done during render)
|
||||
canvas.visible_nodes = [node]
|
||||
|
||||
// Click inside the node bounds
|
||||
const clickX = 150
|
||||
const clickY = 140
|
||||
|
||||
// Verify the click is inside the node bounds
|
||||
expect(node.isPointInside(clickX, clickY)).toBe(true)
|
||||
expect(graph.getNodeOnPos(clickX, clickY)).toBe(node)
|
||||
|
||||
// Call processMouseDown
|
||||
canvas.processMouseDown(
|
||||
new MouseEvent('pointerdown', {
|
||||
button: 1,
|
||||
clientX: clickX,
|
||||
clientY: clickY
|
||||
})
|
||||
)
|
||||
|
||||
// Should NOT query the layout store since node was found directly
|
||||
expect(layoutStore.querySlotAtPoint).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT query layoutStore when not in Vue nodes mode', () => {
|
||||
LiteGraph.vueNodesMode = false
|
||||
|
||||
const clickX = 255
|
||||
const clickY = 120
|
||||
|
||||
// Call processMouseDown
|
||||
canvas.processMouseDown(
|
||||
new MouseEvent('pointerdown', {
|
||||
button: 1,
|
||||
clientX: clickX,
|
||||
clientY: clickY
|
||||
})
|
||||
)
|
||||
|
||||
// Should NOT query the layout store in non-Vue mode
|
||||
expect(layoutStore.querySlotAtPoint).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should find node via slot query for input slots extending beyond left edge', () => {
|
||||
node.addInput('input', 'number')
|
||||
|
||||
// Click position left of node (node starts at x=100)
|
||||
const clickX = 95
|
||||
const clickY = 140
|
||||
|
||||
// Verify outside bounds
|
||||
expect(node.isPointInside(clickX, clickY)).toBe(false)
|
||||
|
||||
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
|
||||
nodeId: String(node.id),
|
||||
index: 0,
|
||||
type: 'input',
|
||||
position: { x: 98, y: 140 },
|
||||
bounds: { x: 88, y: 130, width: 20, height: 20 }
|
||||
})
|
||||
|
||||
canvas.processMouseDown(
|
||||
new MouseEvent('pointerdown', {
|
||||
button: 1,
|
||||
clientX: clickX,
|
||||
clientY: clickY
|
||||
})
|
||||
)
|
||||
|
||||
expect(layoutStore.querySlotAtPoint).toHaveBeenCalledWith({
|
||||
x: clickX,
|
||||
y: clickY
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -203,6 +203,9 @@ interface LGraphCanvasState {
|
||||
* Downstream consumers may reset to false once actioned.
|
||||
*/
|
||||
selectionChanged: boolean
|
||||
|
||||
/** ID of node currently in ghost placement mode (semi-transparent, following cursor). */
|
||||
ghostNodeId: NodeId | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +316,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
readOnly: false,
|
||||
hoveringOver: CanvasItem.Nothing,
|
||||
shouldSetCursor: true,
|
||||
selectionChanged: false
|
||||
selectionChanged: false,
|
||||
ghostNodeId: null
|
||||
}
|
||||
|
||||
private _subgraph?: Subgraph
|
||||
@@ -2163,6 +2167,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
processMouseDown(e: MouseEvent): void {
|
||||
if (this.state.ghostNodeId != null) {
|
||||
if (e.button === 0) this.finalizeGhostPlacement(false)
|
||||
if (e.button === 2) this.finalizeGhostPlacement(true)
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.dragZoomEnabled &&
|
||||
e.ctrlKey &&
|
||||
@@ -2197,9 +2209,21 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
if (!is_inside) return
|
||||
|
||||
const node =
|
||||
let node =
|
||||
graph.getNodeOnPos(e.canvasX, e.canvasY, this.visible_nodes) ?? undefined
|
||||
|
||||
// In Vue nodes mode, slots extend beyond node bounds due to CSS transforms.
|
||||
// If no node was found, check if the click is on a slot and use its owning node.
|
||||
if (!node && LiteGraph.vueNodesMode) {
|
||||
const slotLayout = layoutStore.querySlotAtPoint({
|
||||
x: e.canvasX,
|
||||
y: e.canvasY
|
||||
})
|
||||
if (slotLayout) {
|
||||
node = graph.getNodeById(slotLayout.nodeId) ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
this.mouse[0] = x
|
||||
this.mouse[1] = y
|
||||
this.graph_mouse[0] = e.canvasX
|
||||
@@ -3541,6 +3565,76 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.onNodeMoved?.(findFirstNode(this.selectedItems))
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts ghost placement mode for a node.
|
||||
* The node will be semi-transparent and follow the cursor until the user
|
||||
* clicks to place it, or presses Escape/right-clicks to cancel.
|
||||
* @param node The node to place
|
||||
* @param dragEvent Optional mouse event for positioning under cursor
|
||||
*/
|
||||
startGhostPlacement(node: LGraphNode, dragEvent?: MouseEvent): void {
|
||||
this.emitBeforeChange()
|
||||
this.graph?.beforeChange()
|
||||
|
||||
if (dragEvent) {
|
||||
this.adjustMouseEvent(dragEvent)
|
||||
const e = dragEvent as CanvasPointerEvent
|
||||
node.pos[0] = e.canvasX - node.size[0] / 2
|
||||
node.pos[1] = e.canvasY + 10
|
||||
// Update last_mouse to prevent jump on first drag move
|
||||
this.last_mouse = [e.clientX, e.clientY]
|
||||
} else {
|
||||
node.pos[0] = this.graph_mouse[0] - node.size[0] / 2
|
||||
node.pos[1] = this.graph_mouse[1] + 10
|
||||
}
|
||||
|
||||
// Sync position to layout store for Vue node rendering
|
||||
if (LiteGraph.vueNodesMode) {
|
||||
const mutations = this.initLayoutMutations()
|
||||
mutations.moveNode(node.id, { x: node.pos[0], y: node.pos[1] })
|
||||
}
|
||||
|
||||
this.state.ghostNodeId = node.id
|
||||
|
||||
this.deselectAll()
|
||||
this.select(node)
|
||||
this.isDragging = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes ghost placement mode.
|
||||
* @param cancelled If true, the node is removed; otherwise it's placed
|
||||
*/
|
||||
finalizeGhostPlacement(cancelled: boolean): void {
|
||||
const nodeId = this.state.ghostNodeId
|
||||
if (nodeId == null) return
|
||||
|
||||
this.state.ghostNodeId = null
|
||||
this.isDragging = false
|
||||
|
||||
const node = this.graph?.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
|
||||
if (cancelled) {
|
||||
this.deselect(node)
|
||||
this.graph?.remove(node)
|
||||
} else {
|
||||
delete node.flags.ghost
|
||||
this.graph?.trigger('node:property:changed', {
|
||||
nodeId: node.id,
|
||||
property: 'flags.ghost',
|
||||
oldValue: true,
|
||||
newValue: false
|
||||
})
|
||||
}
|
||||
|
||||
this.dirty_canvas = true
|
||||
this.dirty_bgcanvas = true
|
||||
|
||||
this.graph?.afterChange()
|
||||
this.emitAfterChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a mouse up event has to be processed
|
||||
*/
|
||||
@@ -3711,6 +3805,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const { graph } = this
|
||||
if (!graph) return
|
||||
|
||||
// Cancel ghost placement
|
||||
if (
|
||||
(e.key === 'Escape' || e.key === 'Delete' || e.key === 'Backspace') &&
|
||||
this.state.ghostNodeId != null
|
||||
) {
|
||||
this.finalizeGhostPlacement(true)
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
let block_default = false
|
||||
// @ts-expect-error EventTarget.localName is not in standard types
|
||||
if (e.target.localName == 'input') return
|
||||
@@ -5793,6 +5898,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
private getNodeModeAlpha(node: LGraphNode) {
|
||||
if (node.flags.ghost) return 0.3
|
||||
return node.mode === LGraphEventMode.BYPASS
|
||||
? 0.2
|
||||
: node.mode === LGraphEventMode.NEVER
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NeverNever, PickNevers } from '@/lib/litegraph/src/types/utility'
|
||||
|
||||
type EventListeners<T> = {
|
||||
readonly [K in keyof T]:
|
||||
| ((this: EventTarget, ev: CustomEvent<T[K]>) => any)
|
||||
| ((this: EventTarget, ev: CustomEvent<T[K]>) => unknown)
|
||||
| EventListenerObject
|
||||
| null
|
||||
}
|
||||
|
||||
@@ -332,6 +332,8 @@ export interface INodeFlags {
|
||||
collapsed?: boolean
|
||||
/** Configuration setting for {@link LGraphNode.connectInputToOutput} */
|
||||
keepAllLinksOnBypass?: boolean
|
||||
/** Node is in ghost placement mode (semi-transparent, following cursor) */
|
||||
ghost?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,7 +68,7 @@ describe.skip('subgraphUtils', () => {
|
||||
describe.skip('findUsedSubgraphIds', () => {
|
||||
it('should handle graph with no subgraphs', () => {
|
||||
const graph = new LGraph()
|
||||
const registry = new Map<UUID, any>()
|
||||
const registry = new Map<UUID, LGraph>()
|
||||
|
||||
const result = findUsedSubgraphIds(graph, registry)
|
||||
expect(result.size).toBe(0)
|
||||
@@ -87,7 +87,7 @@ describe.skip('subgraphUtils', () => {
|
||||
const node2 = createTestSubgraphNode(subgraph2)
|
||||
subgraph1.add(node2)
|
||||
|
||||
const registry = new Map<UUID, any>([
|
||||
const registry = new Map<UUID, LGraph>([
|
||||
[subgraph1.id, subgraph1],
|
||||
[subgraph2.id, subgraph2]
|
||||
])
|
||||
@@ -115,7 +115,7 @@ describe.skip('subgraphUtils', () => {
|
||||
const node3 = createTestSubgraphNode(subgraph1, { id: 3 })
|
||||
subgraph2.add(node3)
|
||||
|
||||
const registry = new Map<UUID, any>([
|
||||
const registry = new Map<UUID, LGraph>([
|
||||
[subgraph1.id, subgraph1],
|
||||
[subgraph2.id, subgraph2]
|
||||
])
|
||||
@@ -139,7 +139,7 @@ describe.skip('subgraphUtils', () => {
|
||||
rootGraph.add(node2)
|
||||
|
||||
// Only register subgraph1
|
||||
const registry = new Map<UUID, any>([[subgraph1.id, subgraph1]])
|
||||
const registry = new Map<UUID, LGraph>([[subgraph1.id, subgraph1]])
|
||||
|
||||
const result = findUsedSubgraphIds(rootGraph, registry)
|
||||
expect(result.size).toBe(2)
|
||||
|
||||
@@ -37,6 +37,14 @@ export interface IWidgetOptions<TValues = unknown[]> {
|
||||
getOptionLabel?: (value?: string | null) => string
|
||||
callback?: IWidget['callback']
|
||||
iconClass?: string
|
||||
|
||||
// Vue widget options
|
||||
disabled?: boolean
|
||||
useGrouping?: boolean
|
||||
placeholder?: string
|
||||
showThumbnails?: boolean
|
||||
showItemNavigators?: boolean
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
interface IWidgetSliderOptions extends IWidgetOptions<number[]> {
|
||||
|
||||
@@ -35,11 +35,10 @@ export class ComboWidget
|
||||
override get _displayValue() {
|
||||
if (this.computedDisabled) return ''
|
||||
|
||||
if (this.options.getOptionLabel) {
|
||||
const getOptionLabel = this.options.getOptionLabel
|
||||
if (getOptionLabel) {
|
||||
try {
|
||||
return this.options.getOptionLabel(
|
||||
this.value ? String(this.value) : null
|
||||
)
|
||||
return getOptionLabel(this.value ? String(this.value) : null)
|
||||
} catch (e) {
|
||||
console.error('Failed to map value:', e)
|
||||
return this.value ? String(this.value) : ''
|
||||
@@ -155,9 +154,12 @@ export class ComboWidget
|
||||
}
|
||||
const menu = new LiteGraph.ContextMenu([], menuOptions)
|
||||
|
||||
const getOptionLabel = this.options.getOptionLabel
|
||||
for (const value of values_list) {
|
||||
try {
|
||||
const label = this.options.getOptionLabel(String(value))
|
||||
const label = getOptionLabel
|
||||
? getOptionLabel(String(value))
|
||||
: String(value)
|
||||
menu.addItem(label, value, menuOptions)
|
||||
} catch (err) {
|
||||
console.error('Failed to map value:', err)
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "إنشاء قائمة",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "المدخلات"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "قائمة",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "إنشاء فيديو من الصور.",
|
||||
"display_name": "إنشاء فيديو",
|
||||
@@ -13940,6 +13954,9 @@
|
||||
"bpm": {
|
||||
"name": "الإيقاع (BPM)"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13949,6 +13966,10 @@
|
||||
"duration": {
|
||||
"name": "المدة"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "توليد رموز الصوت",
|
||||
"tooltip": "تفعيل نموذج اللغة الكبير (LLM) الذي يولد رموز الصوت. قد يكون بطيئًا لكنه سيزيد من جودة الصوت الناتج. قم بإيقاف هذا الخيار إذا كنت تقدم مرجعًا صوتيًا للنموذج."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "المقام الموسيقي"
|
||||
},
|
||||
@@ -13964,8 +13985,17 @@
|
||||
"tags": {
|
||||
"name": "الوسوم"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "درجة الحرارة"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "توقيع الإيقاع"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14900,6 +14930,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "فك ترميز الصوت بواسطة VAE (مجزأ)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "تداخل"
|
||||
},
|
||||
"samples": {
|
||||
"name": "عينات"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "حجم التجزئة"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "فك ترميز VAE Hunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"download": "Download",
|
||||
"downloadImage": "Download image",
|
||||
"downloadVideo": "Download video",
|
||||
"downloadAudio": "Download audio",
|
||||
"editOrMaskImage": "Edit or mask image",
|
||||
"editImage": "Edit image",
|
||||
"decrement": "Decrement",
|
||||
|
||||
@@ -2020,6 +2020,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Create List",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "inputs"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "list",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"display_name": "Create Video",
|
||||
"description": "Create a video from images.",
|
||||
@@ -11492,8 +11506,8 @@
|
||||
}
|
||||
},
|
||||
"ReferenceTimbreAudio": {
|
||||
"display_name": "ReferenceTimbreAudio",
|
||||
"description": "This node sets the reference audio for timbre (for ace step 1.5)",
|
||||
"display_name": "Reference Audio",
|
||||
"description": "This node sets the reference audio for ace step 1.5",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
"name": "conditioning"
|
||||
@@ -14057,6 +14071,22 @@
|
||||
"keyscale": {
|
||||
"name": "keyscale"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "generate_audio_codes",
|
||||
"tooltip": "Enable the LLM that generates audio codes. This can be slow but will increase the quality of the generated audio. Turn this off if you are giving the model an audio reference."
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
@@ -15060,6 +15090,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE Decode Audio (Tiled)",
|
||||
"inputs": {
|
||||
"samples": {
|
||||
"name": "samples"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "tile_size"
|
||||
},
|
||||
"overlap": {
|
||||
"name": "overlap"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Crear lista",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "entradas"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "lista",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "Crea un video a partir de imágenes.",
|
||||
"display_name": "Crear video",
|
||||
@@ -6418,9 +6432,7 @@
|
||||
"Load3D": {
|
||||
"display_name": "Cargar 3D",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
"": "limpiar"
|
||||
},
|
||||
"clear": {},
|
||||
"height": {
|
||||
"name": "alto"
|
||||
},
|
||||
@@ -6431,10 +6443,10 @@
|
||||
"name": "archivo_modelo"
|
||||
},
|
||||
"upload 3d model": {
|
||||
"": "subir modelo 3D"
|
||||
"es": "Subir modelo 3D"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"": "subir recursos adicionales"
|
||||
"es": "Subir recursos adicionales"
|
||||
},
|
||||
"width": {
|
||||
"name": "ancho"
|
||||
@@ -13946,6 +13958,9 @@
|
||||
"bpm": {
|
||||
"name": "bpm"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13955,6 +13970,10 @@
|
||||
"duration": {
|
||||
"name": "duración"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "generar_códigos_de_audio",
|
||||
"tooltip": "Activa el LLM que genera códigos de audio. Esto puede ser lento, pero aumentará la calidad del audio generado. Desactívalo si proporcionas una referencia de audio al modelo."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "escala tonal"
|
||||
},
|
||||
@@ -13970,8 +13989,17 @@
|
||||
"tags": {
|
||||
"name": "etiquetas"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperatura"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "compás"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14906,6 +14934,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE Decodificar audio (en mosaico)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "superposición"
|
||||
},
|
||||
"samples": {
|
||||
"name": "muestras"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "tamaño_de_mosaico"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2020,6 +2020,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "ایجاد لیست",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "ورودیها"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "لیست",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "ایجاد ویدیو از تصاویر.",
|
||||
"display_name": "ایجاد ویدیو",
|
||||
@@ -6427,7 +6441,9 @@
|
||||
"Load3D": {
|
||||
"display_name": "بارگذاری ۳بعدی و انیمیشن",
|
||||
"inputs": {
|
||||
"clear": {},
|
||||
"clear": {
|
||||
"": "پاکسازی"
|
||||
},
|
||||
"height": {
|
||||
"name": "ارتفاع"
|
||||
},
|
||||
@@ -6437,8 +6453,12 @@
|
||||
"model_file": {
|
||||
"name": "فایل مدل"
|
||||
},
|
||||
"upload 3d model": {},
|
||||
"upload extra resources": {},
|
||||
"upload 3d model": {
|
||||
"": "بارگذاری مدل سهبعدی"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"": "بارگذاری منابع اضافی"
|
||||
},
|
||||
"width": {
|
||||
"name": "عرض"
|
||||
}
|
||||
@@ -13953,6 +13973,9 @@
|
||||
"bpm": {
|
||||
"name": "ضرب در دقیقه (BPM)"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13962,6 +13985,10 @@
|
||||
"duration": {
|
||||
"name": "مدت زمان"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "تولید کدهای صوتی",
|
||||
"tooltip": "فعالسازی LLM برای تولید کدهای صوتی. این کار ممکن است کند باشد اما کیفیت صدای تولیدشده را افزایش میدهد. اگر به مدل یک مرجع صوتی میدهید، این گزینه را غیرفعال کنید."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "گام"
|
||||
},
|
||||
@@ -13977,8 +14004,17 @@
|
||||
"tags": {
|
||||
"name": "برچسبها"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "امضای زمانی"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14913,6 +14949,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "رمزگشایی VAE صوتی (کاشیبندیشده)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "همپوشانی"
|
||||
},
|
||||
"samples": {
|
||||
"name": "نمونهها"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "اندازه کاشی"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Créer une liste",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "entrées"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "liste",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "Créer une vidéo à partir d’images.",
|
||||
"display_name": "Créer une vidéo",
|
||||
@@ -13946,6 +13960,9 @@
|
||||
"bpm": {
|
||||
"name": "bpm"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13955,6 +13972,10 @@
|
||||
"duration": {
|
||||
"name": "durée"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "générer des codes audio",
|
||||
"tooltip": "Activez le LLM qui génère des codes audio. Cela peut être lent mais augmentera la qualité de l’audio généré. Désactivez cette option si vous fournissez une référence audio au modèle."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "tonalité"
|
||||
},
|
||||
@@ -13970,8 +13991,17 @@
|
||||
"tags": {
|
||||
"name": "tags"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "température"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "signature rythmique"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14906,6 +14936,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "Décoder Audio VAE (par tuiles)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "chevauchement"
|
||||
},
|
||||
"samples": {
|
||||
"name": "échantillons"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "taille de tuile"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "リストを作成",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "入力"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "リスト",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "画像から動画を作成します。",
|
||||
"display_name": "動画を作成",
|
||||
@@ -6418,9 +6432,7 @@
|
||||
"Load3D": {
|
||||
"display_name": "3Dを読み込む",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
"": "クリア"
|
||||
},
|
||||
"clear": "クリア",
|
||||
"height": {
|
||||
"name": "高さ"
|
||||
},
|
||||
@@ -6430,12 +6442,8 @@
|
||||
"model_file": {
|
||||
"name": "モデルファイル"
|
||||
},
|
||||
"upload 3d model": {
|
||||
"": "3Dモデルをアップロード"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"": "追加リソースをアップロード"
|
||||
},
|
||||
"upload 3d model": "3Dモデルをアップロード",
|
||||
"upload extra resources": "追加リソースをアップロード",
|
||||
"width": {
|
||||
"name": "幅"
|
||||
}
|
||||
@@ -13946,6 +13954,9 @@
|
||||
"bpm": {
|
||||
"name": "BPM"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfgスケール"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13955,6 +13966,10 @@
|
||||
"duration": {
|
||||
"name": "長さ"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "オーディオコードを生成",
|
||||
"tooltip": "オーディオコードを生成するLLMを有効にします。これにより生成されるオーディオの品質が向上しますが、処理が遅くなる場合があります。モデルにオーディオリファレンスを与える場合はオフにしてください。"
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "キー"
|
||||
},
|
||||
@@ -13970,8 +13985,17 @@
|
||||
"tags": {
|
||||
"name": "タグ"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "温度"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "拍子"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14906,6 +14930,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAEデコードオーディオ(タイル)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "オーバーラップ"
|
||||
},
|
||||
"samples": {
|
||||
"name": "サンプル"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "タイルサイズ"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "목록 만들기",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "입력값"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "목록",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "이미지로부터 비디오를 생성합니다.",
|
||||
"display_name": "비디오 생성",
|
||||
@@ -6418,7 +6432,7 @@
|
||||
"Load3D": {
|
||||
"display_name": "3D 불러오기",
|
||||
"inputs": {
|
||||
"clear": {},
|
||||
"clear": "지우기",
|
||||
"height": {
|
||||
"name": "높이"
|
||||
},
|
||||
@@ -6428,12 +6442,8 @@
|
||||
"model_file": {
|
||||
"name": "모델 파일"
|
||||
},
|
||||
"upload 3d model": {
|
||||
"ko": "3D 모델 업로드"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"ko": "추가 리소스 업로드"
|
||||
},
|
||||
"upload 3d model": "3D 모델 업로드",
|
||||
"upload extra resources": "추가 리소스 업로드",
|
||||
"width": {
|
||||
"name": "너비"
|
||||
}
|
||||
@@ -13944,6 +13954,9 @@
|
||||
"bpm": {
|
||||
"name": "BPM"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13953,6 +13966,10 @@
|
||||
"duration": {
|
||||
"name": "길이"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "오디오 코드 생성",
|
||||
"tooltip": "오디오 코드를 생성하는 LLM을 활성화합니다. 느릴 수 있지만 생성된 오디오의 품질이 향상됩니다. 모델에 오디오 참조를 제공하는 경우 이 옵션을 끄세요."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "조성"
|
||||
},
|
||||
@@ -13968,8 +13985,17 @@
|
||||
"tags": {
|
||||
"name": "태그"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "박자"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14904,6 +14930,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE 오디오 디코드 (타일)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "오버랩"
|
||||
},
|
||||
"samples": {
|
||||
"name": "샘플"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "타일 크기"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2020,6 +2020,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Criar Lista",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "entradas"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "lista",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "Crie um vídeo a partir de imagens.",
|
||||
"display_name": "Criar Vídeo",
|
||||
@@ -6427,7 +6441,9 @@
|
||||
"Load3D": {
|
||||
"display_name": "Carregar 3D & Animação",
|
||||
"inputs": {
|
||||
"clear": {},
|
||||
"clear": {
|
||||
"": "limpar"
|
||||
},
|
||||
"height": {
|
||||
"name": "altura"
|
||||
},
|
||||
@@ -6437,8 +6453,12 @@
|
||||
"model_file": {
|
||||
"name": "arquivo_do_modelo"
|
||||
},
|
||||
"upload 3d model": {},
|
||||
"upload extra resources": {},
|
||||
"upload 3d model": {
|
||||
"": "enviar modelo 3D"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"": "enviar recursos extras"
|
||||
},
|
||||
"width": {
|
||||
"name": "largura"
|
||||
}
|
||||
@@ -13953,6 +13973,9 @@
|
||||
"bpm": {
|
||||
"name": "bpm"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13962,6 +13985,10 @@
|
||||
"duration": {
|
||||
"name": "duração"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "gerar_códigos_de_áudio",
|
||||
"tooltip": "Ativa o LLM que gera códigos de áudio. Isso pode ser lento, mas aumentará a qualidade do áudio gerado. Desative se você fornecer uma referência de áudio ao modelo."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "escala tonal"
|
||||
},
|
||||
@@ -13977,8 +14004,17 @@
|
||||
"tags": {
|
||||
"name": "tags"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperatura"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "fórmula de compasso"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14913,6 +14949,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE Decodificar Áudio (Em Blocos)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "sobreposição"
|
||||
},
|
||||
"samples": {
|
||||
"name": "amostras"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "tamanho_do_bloco"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Создать список",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "входные данные"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "список",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "Создайте видео из изображений.",
|
||||
"display_name": "Создать видео",
|
||||
@@ -6418,7 +6432,9 @@
|
||||
"Load3D": {
|
||||
"display_name": "Загрузить 3D",
|
||||
"inputs": {
|
||||
"clear": "Очистить",
|
||||
"clear": {
|
||||
"": "Очистить"
|
||||
},
|
||||
"height": {
|
||||
"name": "высота"
|
||||
},
|
||||
@@ -6428,8 +6444,12 @@
|
||||
"model_file": {
|
||||
"name": "файл модели"
|
||||
},
|
||||
"upload 3d model": "Загрузить 3D-модель",
|
||||
"upload extra resources": "Загрузить дополнительные ресурсы",
|
||||
"upload 3d model": {
|
||||
"": "Загрузить 3D-модель"
|
||||
},
|
||||
"upload extra resources": {
|
||||
"": "Загрузить дополнительные ресурсы"
|
||||
},
|
||||
"width": {
|
||||
"name": "ширина"
|
||||
}
|
||||
@@ -13940,6 +13960,9 @@
|
||||
"bpm": {
|
||||
"name": "bpm"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13949,6 +13972,10 @@
|
||||
"duration": {
|
||||
"name": "длительность"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "генерировать аудиокоды",
|
||||
"tooltip": "Включить LLM, который генерирует аудиокоды. Это может быть медленно, но повысит качество сгенерированного аудио. Отключите, если вы предоставляете модели аудиореференс."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "тональность"
|
||||
},
|
||||
@@ -13964,8 +13991,17 @@
|
||||
"tags": {
|
||||
"name": "теги"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "температура"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "размер"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14900,6 +14936,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE декодирование аудио (плитками)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "перекрытие"
|
||||
},
|
||||
"samples": {
|
||||
"name": "образцы"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "размер плитки"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEDecodeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "Liste Oluştur",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "girdiler"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "liste",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "Görüntülerden bir video oluşturun.",
|
||||
"display_name": "Video Oluştur",
|
||||
@@ -13946,6 +13960,9 @@
|
||||
"bpm": {
|
||||
"name": "bpm"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg ölçeği"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13955,6 +13972,10 @@
|
||||
"duration": {
|
||||
"name": "süre"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "ses kodlarını üret",
|
||||
"tooltip": "Ses kodlarını üreten LLM'i etkinleştir. Bu işlem yavaş olabilir ancak üretilen sesin kalitesini artırır. Eğer modele bir ses referansı veriyorsanız bunu kapatın."
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "ton anahtarı"
|
||||
},
|
||||
@@ -13970,8 +13991,17 @@
|
||||
"tags": {
|
||||
"name": "etiketler"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "sıcaklık"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "zaman imzası"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14906,6 +14936,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE Sesini Çöz (Döşemeli)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "örtüşme"
|
||||
},
|
||||
"samples": {
|
||||
"name": "örnekler"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "döşeme boyutu"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAEKodÇözmeHunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2018,6 +2018,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "建立清單",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "輸入"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "清單",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "從圖片建立影片。",
|
||||
"display_name": "建立影片",
|
||||
@@ -13944,6 +13958,9 @@
|
||||
"bpm": {
|
||||
"name": "每分鐘節拍數"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13953,6 +13970,10 @@
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "產生音訊編碼",
|
||||
"tooltip": "啟用產生音訊編碼的 LLM。這可能會較慢,但能提升產生音訊的品質。如果你已提供模型音訊參考,請關閉此選項。"
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "調性"
|
||||
},
|
||||
@@ -13968,8 +13989,17 @@
|
||||
"tags": {
|
||||
"name": "標籤"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "拍號"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14904,6 +14934,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE 解碼音訊(分塊)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "重疊"
|
||||
},
|
||||
"samples": {
|
||||
"name": "樣本"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "分塊大小"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAE 解碼 Hunyuan3D",
|
||||
"inputs": {
|
||||
|
||||
@@ -2020,6 +2020,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateList": {
|
||||
"display_name": "创建列表",
|
||||
"inputs": {
|
||||
"inputs": {
|
||||
"name": "输入"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "列表",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateVideo": {
|
||||
"description": "从图像创建视频。",
|
||||
"display_name": "创建视频",
|
||||
@@ -13953,6 +13967,9 @@
|
||||
"bpm": {
|
||||
"name": "每分钟节拍数"
|
||||
},
|
||||
"cfg_scale": {
|
||||
"name": "cfg_scale"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
@@ -13962,6 +13979,10 @@
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
"generate_audio_codes": {
|
||||
"name": "生成音频代码",
|
||||
"tooltip": "启用生成音频代码的LLM。这可能较慢,但会提升生成音频的质量。如果你为模型提供了音频参考,请关闭此选项。"
|
||||
},
|
||||
"keyscale": {
|
||||
"name": "调式"
|
||||
},
|
||||
@@ -13977,8 +13998,17 @@
|
||||
"tags": {
|
||||
"name": "标签"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
"timesignature": {
|
||||
"name": "拍号"
|
||||
},
|
||||
"top_k": {
|
||||
"name": "top_k"
|
||||
},
|
||||
"top_p": {
|
||||
"name": "top_p"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14913,6 +14943,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeAudioTiled": {
|
||||
"display_name": "VAE解码音频(分块)",
|
||||
"inputs": {
|
||||
"overlap": {
|
||||
"name": "重叠"
|
||||
},
|
||||
"samples": {
|
||||
"name": "样本"
|
||||
},
|
||||
"tile_size": {
|
||||
"name": "分块大小"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VAEDecodeHunyuan3D": {
|
||||
"display_name": "VAE解码(Hunyuan3D)",
|
||||
"inputs": {
|
||||
|
||||
@@ -65,15 +65,8 @@ export function useMediaAssetActions() {
|
||||
|
||||
try {
|
||||
const filename = targetAsset.name
|
||||
let downloadUrl: string
|
||||
|
||||
// In cloud, use preview_url directly (from cloud storage)
|
||||
// In OSS/localhost, use the /view endpoint
|
||||
if (isCloud && targetAsset.preview_url) {
|
||||
downloadUrl = targetAsset.preview_url
|
||||
} else {
|
||||
downloadUrl = getAssetUrl(targetAsset)
|
||||
}
|
||||
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
|
||||
const downloadUrl = targetAsset.preview_url || getAssetUrl(targetAsset)
|
||||
|
||||
downloadFile(downloadUrl, filename)
|
||||
|
||||
@@ -103,15 +96,8 @@ export function useMediaAssetActions() {
|
||||
try {
|
||||
assets.forEach((asset) => {
|
||||
const filename = asset.name
|
||||
let downloadUrl: string
|
||||
|
||||
// In cloud, use preview_url directly (from GCS or other cloud storage)
|
||||
// In OSS/localhost, use the /view endpoint
|
||||
if (isCloud && asset.preview_url) {
|
||||
downloadUrl = asset.preview_url
|
||||
} else {
|
||||
downloadUrl = getAssetUrl(asset)
|
||||
}
|
||||
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
|
||||
const downloadUrl = asset.preview_url || getAssetUrl(asset)
|
||||
downloadFile(downloadUrl, filename)
|
||||
})
|
||||
|
||||
|
||||
@@ -487,7 +487,7 @@ function createAssetService() {
|
||||
url: string
|
||||
name: string
|
||||
tags?: string[]
|
||||
user_metadata?: Record<string, any>
|
||||
user_metadata?: Record<string, unknown>
|
||||
preview_id?: string
|
||||
}): Promise<AssetItem & { created_new: boolean }> {
|
||||
const res = await api.fetchApi(ASSETS_ENDPOINT, {
|
||||
@@ -525,7 +525,7 @@ function createAssetService() {
|
||||
data: string
|
||||
name: string
|
||||
tags?: string[]
|
||||
user_metadata?: Record<string, any>
|
||||
user_metadata?: Record<string, unknown>
|
||||
}): Promise<AssetItem & { created_new: boolean }> {
|
||||
// Validate that data is a data URL
|
||||
if (!params.data || !params.data.startsWith('data:')) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getAssetType } from './assetTypeUtil'
|
||||
|
||||
/**
|
||||
* Get the download/view URL for an asset
|
||||
* Constructs the proper URL with filename encoding and type parameter
|
||||
* Constructs the proper URL with filename encoding, type, and subfolder parameters
|
||||
*
|
||||
* @param asset The asset to get URL for
|
||||
* @param defaultType Default type if asset doesn't have tags (default: 'output')
|
||||
@@ -23,7 +23,12 @@ export function getAssetUrl(
|
||||
defaultType: 'input' | 'output' = 'output'
|
||||
): string {
|
||||
const assetType = getAssetType(asset, defaultType)
|
||||
return api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(asset.name)}&type=${assetType}`
|
||||
)
|
||||
const subfolder = asset.user_metadata?.subfolder
|
||||
const params = new URLSearchParams()
|
||||
params.set('filename', asset.name)
|
||||
params.set('type', assetType)
|
||||
if (typeof subfolder === 'string' && subfolder) {
|
||||
params.set('subfolder', subfolder)
|
||||
}
|
||||
return api.apiURL(`/view?${params}`)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
useErrorHandling: vi.fn(() => ({
|
||||
wrapWithErrorHandlingAsync: vi.fn(
|
||||
(fn, errorHandler) =>
|
||||
async (...args: any[]) => {
|
||||
async (...args: Parameters<typeof fn>) => {
|
||||
try {
|
||||
return await fn(...args)
|
||||
} catch (error) {
|
||||
|
||||
@@ -22,7 +22,8 @@ function mockSettingStore(enabled: boolean) {
|
||||
return enabled
|
||||
}
|
||||
return false
|
||||
})
|
||||
}),
|
||||
load: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -227,6 +228,16 @@ describe('useNodeReplacementStore', () => {
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not fetch when feature is disabled', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue({})
|
||||
store = createStore(false)
|
||||
|
||||
await store.load()
|
||||
|
||||
expect(fetchNodeReplacements).not.toHaveBeenCalled()
|
||||
expect(store.isLoaded).toBe(false)
|
||||
})
|
||||
|
||||
it('should not re-fetch when called twice', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
|
||||
store = createStore()
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
)
|
||||
|
||||
async function load() {
|
||||
if (isLoaded.value) return
|
||||
if (isLoaded.value || !isEnabled.value) return
|
||||
|
||||
try {
|
||||
replacements.value = await fetchNodeReplacements()
|
||||
|
||||
@@ -1198,7 +1198,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
tooltip:
|
||||
'When enabled, missing nodes can be automatically replaced with their newer equivalents if a replacement mapping exists.',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
experimental: true,
|
||||
versionAdded: '1.40.0'
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ function onChange(
|
||||
}
|
||||
|
||||
export const useSettingStore = defineStore('setting', () => {
|
||||
const settingValues = ref<Record<string, any>>({})
|
||||
const settingValues = ref<Partial<Settings>>({})
|
||||
const settingsById = ref<Record<string, SettingParams>>({})
|
||||
|
||||
const {
|
||||
@@ -87,7 +87,7 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
* @param key - The key of the setting to check.
|
||||
* @returns Whether the setting exists.
|
||||
*/
|
||||
function exists(key: string) {
|
||||
function exists<K extends keyof Settings>(key: K) {
|
||||
return settingValues.value[key] !== undefined
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
*/
|
||||
function get<K extends keyof Settings>(key: K): Settings[K] {
|
||||
// Clone the value when returning to prevent external mutations
|
||||
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
|
||||
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key)!)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -222,7 +222,7 @@ interface WorkflowStore {
|
||||
activeSubgraph: Subgraph | undefined
|
||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
executionIdToCurrentId: (id: string) => string | undefined
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeToNodeLocatorId: (node: LGraphNode) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
@@ -718,7 +718,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
|
||||
//FIXME: use existing util function
|
||||
const executionIdToCurrentId = (id: string) => {
|
||||
const executionIdToCurrentId = (id: string): string | undefined => {
|
||||
const subgraph = activeSubgraph.value
|
||||
|
||||
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as I18n from 'vue-i18n'
|
||||
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { WorkflowDraftSnapshot } from '@/platform/workflow/persistence/base/draftCache'
|
||||
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
|
||||
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
|
||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
@@ -191,7 +192,7 @@ describe('useWorkflowPersistence', () => {
|
||||
|
||||
const drafts = JSON.parse(
|
||||
localStorage.getItem('Comfy.Workflow.Drafts') ?? '{}'
|
||||
) as Record<string, any>
|
||||
) as Record<string, WorkflowDraftSnapshot>
|
||||
|
||||
expect(Object.keys(drafts).length).toBe(32)
|
||||
expect(drafts['workflows/Draft0.json']).toBeUndefined()
|
||||
|
||||
@@ -184,7 +184,7 @@ describe('LGraphNode', () => {
|
||||
|
||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||
|
||||
expect(wrapper.classes()).toContain('outline-2')
|
||||
expect(wrapper.classes()).toContain('outline-3')
|
||||
expect(wrapper.classes()).toContain('outline-node-component-outline')
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ describe('LGraphNode', () => {
|
||||
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height')).toBe('')
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe(
|
||||
'100px'
|
||||
'130px'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -219,7 +219,7 @@ describe('LGraphNode', () => {
|
||||
})
|
||||
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height')).toBe(
|
||||
'100px'
|
||||
'130px'
|
||||
)
|
||||
expect(wrapper.element.style.getPropertyValue('--node-height-x')).toBe('')
|
||||
})
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-node-component-ring',
|
||||
'outline-transparent outline-2 focus-visible:outline-node-component-outline',
|
||||
'outline-transparent outline-3 focus-visible:outline-node-component-outline',
|
||||
borderClass,
|
||||
outlineClass,
|
||||
cursorClass,
|
||||
@@ -29,7 +29,7 @@
|
||||
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
|
||||
},
|
||||
|
||||
shouldHandleNodePointerEvents
|
||||
shouldHandleNodePointerEvents && !nodeData.flags?.ghost
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none',
|
||||
!isCollapsed && ' pb-1'
|
||||
@@ -268,6 +268,8 @@ const muted = computed((): boolean => nodeData.mode === LGraphEventMode.NEVER)
|
||||
const nodeOpacity = computed(() => {
|
||||
const globalOpacity = settingStore.get('Comfy.Node.Opacity') ?? 1
|
||||
|
||||
if (nodeData.flags?.ghost) return globalOpacity * 0.3
|
||||
|
||||
// For muted/bypassed nodes, apply the 0.5 multiplier on top of global opacity
|
||||
if (bypassed.value || muted.value) {
|
||||
return globalOpacity * 0.5
|
||||
@@ -342,7 +344,10 @@ function initSizeStyles() {
|
||||
const suffix = isCollapsed.value ? '-x' : ''
|
||||
|
||||
el.style.setProperty(`--node-width${suffix}`, `${width}px`)
|
||||
el.style.setProperty(`--node-height${suffix}`, `${height}px`)
|
||||
el.style.setProperty(
|
||||
`--node-height${suffix}`,
|
||||
`${height + LiteGraph.NODE_TITLE_HEIGHT}px`
|
||||
)
|
||||
}
|
||||
|
||||
const baseResizeHandleClasses =
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import { RenderShape } from '@/lib/litegraph/src/litegraph'
|
||||
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
||||
import NodeSlots from '@/renderer/extensions/vueNodes/components/NodeSlots.vue'
|
||||
@@ -69,8 +70,11 @@ const nodeData = computed<VueNodeData>(() => {
|
||||
options: {
|
||||
hidden: input.hidden,
|
||||
advanced: input.advanced,
|
||||
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
|
||||
}
|
||||
values:
|
||||
input.type === 'COMBO' && Array.isArray(input.options)
|
||||
? input.options
|
||||
: undefined
|
||||
} satisfies IWidgetOptions
|
||||
}))
|
||||
|
||||
const inputs: INodeInputSlot[] = Object.entries(nodeDef.inputs || {})
|
||||
|
||||
@@ -4,11 +4,11 @@ import { createBounds } from '@/lib/litegraph/src/measure'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeBoundsUpdate } from '@/renderer/core/layout/types'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import type { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode'
|
||||
import type { SubgraphOutputNode } from '@/lib/litegraph/src/subgraph/SubgraphOutputNode'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const SCALE_FACTOR = 1.2
|
||||
|
||||
@@ -67,9 +67,7 @@ export function ensureCorrectLayoutScale(
|
||||
|
||||
const scaledWidth = lgNode.width * scaleFactor
|
||||
|
||||
const scaledHeight = needsUpscale
|
||||
? lgNode.size[1] * scaleFactor + LiteGraph.NODE_TITLE_HEIGHT
|
||||
: (lgNode.size[1] - LiteGraph.NODE_TITLE_HEIGHT) * scaleFactor
|
||||
const scaledHeight = lgNode.size[1] * scaleFactor
|
||||
|
||||
// Directly update LiteGraph node to ensure immediate consistency
|
||||
// Dont need to reference vue directly because the pos and dims are already in yjs
|
||||
@@ -93,6 +91,7 @@ export function ensureCorrectLayoutScale(
|
||||
}
|
||||
|
||||
if (onActiveGraph && yjsMoveNodeUpdates.length > 0) {
|
||||
layoutStore.setSource(LayoutSource.Canvas)
|
||||
layoutStore.batchUpdateNodeBounds(yjsMoveNodeUpdates)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ import type { ChartData } from 'chart.js'
|
||||
import Chart from 'primevue/chart'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']>
|
||||
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
|
||||
IWidgetOptions
|
||||
|
||||
const value = defineModel<ChartData>({ required: true })
|
||||
|
||||
|
||||
@@ -38,10 +38,12 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
type WidgetOptions = { format?: ColorFormat } & Record<string, unknown>
|
||||
type WidgetOptions = IWidgetOptions & { format?: ColorFormat }
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string, WidgetOptions>
|
||||
|
||||
@@ -67,30 +67,40 @@ function updateValue(e: UIEvent) {
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLInputElement)) return
|
||||
const parsed = evaluateInput(unformatValue(target.value))
|
||||
if (parsed !== undefined)
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, parsed)
|
||||
)
|
||||
else target.value = formattedValue.value
|
||||
if (parsed !== undefined) {
|
||||
const max = filteredProps.value.max ?? Number.MAX_VALUE
|
||||
const min = filteredProps.value.min ?? -Number.MAX_VALUE
|
||||
modelValue.value = Math.min(max, Math.max(min, parsed))
|
||||
} else target.value = formattedValue.value
|
||||
|
||||
textEdit.value = false
|
||||
}
|
||||
|
||||
const canDecrement = computed(
|
||||
() =>
|
||||
modelValue.value > filteredProps.value.min &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
const canIncrement = computed(
|
||||
() =>
|
||||
modelValue.value < filteredProps.value.max &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
interface NumericWidgetOptions {
|
||||
min: number
|
||||
max: number
|
||||
step?: number
|
||||
step2?: number
|
||||
precision?: number
|
||||
disabled?: boolean
|
||||
useGrouping?: boolean
|
||||
}
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
const filteredProps = computed(() => {
|
||||
const filtered = filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
return filtered as Partial<NumericWidgetOptions>
|
||||
})
|
||||
|
||||
const isDisabled = computed(() => props.widget.options?.disabled ?? false)
|
||||
|
||||
const canDecrement = computed(() => {
|
||||
const min = filteredProps.value.min ?? -Number.MAX_VALUE
|
||||
return modelValue.value > min && !isDisabled.value
|
||||
})
|
||||
const canIncrement = computed(() => {
|
||||
const max = filteredProps.value.max ?? Number.MAX_VALUE
|
||||
return modelValue.value < max && !isDisabled.value
|
||||
})
|
||||
|
||||
// Get the precision value for proper number formatting
|
||||
const precision = computed(() => {
|
||||
@@ -108,7 +118,7 @@ const stepValue = computed(() => {
|
||||
// Use step / 10 for custom large step values (> 10) to match litegraph behavior
|
||||
// This is important for extensions like Impact Pack that use custom step values (e.g., 640)
|
||||
// We skip default step values (1, 10) to avoid affecting normal widgets
|
||||
const step = props.widget.options?.step
|
||||
const step = props.widget.options?.step as number | undefined
|
||||
if (step !== undefined && step > 10) {
|
||||
return Number(step) / 10
|
||||
}
|
||||
@@ -140,17 +150,16 @@ const buttonsDisabled = computed(() => {
|
||||
})
|
||||
|
||||
function updateValueBy(delta: number) {
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, modelValue.value + delta)
|
||||
)
|
||||
const max = filteredProps.value.max ?? Number.MAX_VALUE
|
||||
const min = filteredProps.value.min ?? -Number.MAX_VALUE
|
||||
modelValue.value = Math.min(max, Math.max(min, modelValue.value + delta))
|
||||
}
|
||||
|
||||
const dragValue = ref<number>()
|
||||
const dragDelta = ref(0)
|
||||
function handleMouseDown(e: PointerEvent) {
|
||||
if (e.button > 0) return
|
||||
if (props.widget.options?.disabled) return
|
||||
if (isDisabled.value) return
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
target.setPointerCapture(e.pointerId)
|
||||
@@ -163,10 +172,9 @@ function handleMouseMove(e: PointerEvent) {
|
||||
const unclippedValue =
|
||||
dragValue.value + ((dragDelta.value / 10) | 0) * stepValue.value
|
||||
dragDelta.value %= 10
|
||||
dragValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, unclippedValue)
|
||||
)
|
||||
const max = filteredProps.value.max ?? Number.MAX_VALUE
|
||||
const min = filteredProps.value.min ?? -Number.MAX_VALUE
|
||||
dragValue.value = Math.min(max, Math.max(min, unclippedValue))
|
||||
}
|
||||
function handleMouseUp() {
|
||||
const newValue = dragValue.value
|
||||
@@ -248,7 +256,7 @@ const sliderWidth = computed(() => {
|
||||
:value="formattedValue"
|
||||
role="spinbutton"
|
||||
tabindex="0"
|
||||
:disabled="widget.options?.disabled"
|
||||
:disabled="isDisabled"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { InputTextProps } from 'primevue/inputtext'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetInputText from './WidgetInputText.vue'
|
||||
@@ -18,7 +19,7 @@ describe('WidgetInputText Value Binding', () => {
|
||||
name: 'test_input',
|
||||
type: 'string',
|
||||
value,
|
||||
options,
|
||||
options: options as IWidgetOptions,
|
||||
callback
|
||||
})
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const props = defineProps<Props>()
|
||||
|
||||
const modelValue = defineModel<string | undefined>({
|
||||
default(props: Props) {
|
||||
return props.widget.options?.values?.[0] || ''
|
||||
return props.widget.options?.values?.[0] ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
value: string = 'img_001.png',
|
||||
options: {
|
||||
values?: string[]
|
||||
getOptionLabel?: (value: string | null) => string
|
||||
getOptionLabel?: (value?: string | null) => string
|
||||
} = {},
|
||||
spec?: ComboInputSpec
|
||||
): SimplifiedWidget<string | undefined> => ({
|
||||
@@ -82,7 +82,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
|
||||
describe('when custom labels are provided via getOptionLabel', () => {
|
||||
it('displays custom labels while preserving original values', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (!value) return 'No file'
|
||||
const mapping: Record<string, string> = {
|
||||
'img_001.png': 'Vacation Photo',
|
||||
@@ -112,7 +112,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
})
|
||||
|
||||
it('emits original values when items with custom labels are selected', async () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (!value) return 'No file'
|
||||
return `Custom: ${value}`
|
||||
})
|
||||
@@ -134,7 +134,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
})
|
||||
|
||||
it('falls back to original value when label mapping fails', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (value === 'photo_abc.jpg') {
|
||||
throw new Error('Mapping failed')
|
||||
}
|
||||
@@ -163,7 +163,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
})
|
||||
|
||||
it('falls back to original value when label mapping returns empty string', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (value === 'photo_abc.jpg') {
|
||||
return ''
|
||||
}
|
||||
@@ -185,7 +185,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
})
|
||||
|
||||
it('falls back to original value when label mapping returns undefined', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (value === 'hash789.png') {
|
||||
return undefined as unknown as string
|
||||
}
|
||||
@@ -209,7 +209,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
|
||||
describe('output items with custom label mapping', () => {
|
||||
it('applies custom label mapping to output items from queue history', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
const getOptionLabel = vi.fn((value?: string | null) => {
|
||||
if (!value) return 'No file'
|
||||
return `Output: ${value}`
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ provide(
|
||||
|
||||
const modelValue = defineModel<string | undefined>({
|
||||
default(props: Props) {
|
||||
return props.widget.options?.values?.[0] || ''
|
||||
return props.widget.options?.values?.[0] ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -73,7 +73,8 @@ const combinedProps = computed(() => ({
|
||||
}))
|
||||
|
||||
const getAssetData = () => {
|
||||
const nodeType = props.widget.options?.nodeType ?? props.nodeType
|
||||
const nodeType: string | undefined =
|
||||
props.widget.options?.nodeType ?? props.nodeType
|
||||
if (props.isAssetMode && nodeType) {
|
||||
return useAssetWidgetData(toRef(nodeType))
|
||||
}
|
||||
@@ -134,11 +135,11 @@ const inputItems = computed<FormDropdownItem[]>(() => {
|
||||
return []
|
||||
}
|
||||
|
||||
return values.map((value: string, index: number) => ({
|
||||
return values.map((value, index) => ({
|
||||
id: `input-${index}`,
|
||||
preview_url: getMediaUrl(value, 'input'),
|
||||
name: value,
|
||||
label: getDisplayLabel(value)
|
||||
preview_url: getMediaUrl(String(value), 'input'),
|
||||
name: String(value),
|
||||
label: getDisplayLabel(String(value))
|
||||
}))
|
||||
})
|
||||
const outputItems = computed<FormDropdownItem[]>(() => {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
v-model="modelValue"
|
||||
:class="cn(WidgetInputBaseClass, 'size-full text-xs resize-none')"
|
||||
:placeholder
|
||||
:readonly="widget.options?.read_only"
|
||||
:disabled="widget.options?.read_only"
|
||||
:readonly="isReadOnly"
|
||||
:disabled="isReadOnly"
|
||||
fluid
|
||||
data-capture-wheel="true"
|
||||
@pointerdown.capture.stop
|
||||
@@ -58,4 +58,6 @@ const filteredProps = computed(() =>
|
||||
|
||||
const displayName = computed(() => widget.label || widget.name)
|
||||
const id = useId()
|
||||
|
||||
const isReadOnly = computed(() => widget.options?.read_only ?? false)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import AudioPreviewPlayer from '@/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue'
|
||||
|
||||
const mockToastAdd = vi.fn()
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: () => ({ add: mockToastAdd })
|
||||
}))
|
||||
|
||||
vi.mock('@/base/common/downloadUtil', () => ({
|
||||
downloadFile: vi.fn()
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: {} }
|
||||
})
|
||||
|
||||
function mountPlayer(modelValue?: string) {
|
||||
return mount(AudioPreviewPlayer, {
|
||||
props: {
|
||||
modelValue,
|
||||
hideWhenEmpty: false
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
components: { Button },
|
||||
stubs: {
|
||||
TieredMenu: true,
|
||||
Slider: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findDownloadButton(wrapper: ReturnType<typeof mountPlayer>) {
|
||||
return wrapper.find('[aria-label="g.downloadAudio"]')
|
||||
}
|
||||
|
||||
describe('AudioPreviewPlayer', () => {
|
||||
describe('download button', () => {
|
||||
it('shows download button when audio is loaded', () => {
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
|
||||
expect(findDownloadButton(wrapper).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('hides download button when no audio is loaded', () => {
|
||||
const wrapper = mountPlayer()
|
||||
|
||||
expect(findDownloadButton(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('calls downloadFile when download button is clicked', async () => {
|
||||
const { downloadFile } = await import('@/base/common/downloadUtil')
|
||||
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
await findDownloadButton(wrapper).trigger('click')
|
||||
|
||||
expect(downloadFile).toHaveBeenCalledWith('http://example.com/audio.mp3')
|
||||
})
|
||||
|
||||
it('shows toast on download failure', async () => {
|
||||
const { downloadFile } = await import('@/base/common/downloadUtil')
|
||||
vi.mocked(downloadFile).mockImplementation(() => {
|
||||
throw new Error('download failed')
|
||||
})
|
||||
|
||||
const wrapper = mountPlayer('http://example.com/audio.mp3')
|
||||
await findDownloadButton(wrapper).trigger('click')
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
severity: 'error'
|
||||
})
|
||||
)
|
||||
|
||||
vi.mocked(downloadFile).mockReset()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,11 +16,11 @@
|
||||
<!-- Left Actions -->
|
||||
<div class="relative flex shrink-0 items-center justify-start gap-2">
|
||||
<!-- Play/Pause Button -->
|
||||
<div
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.playPause')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="togglePlayPause"
|
||||
>
|
||||
<i
|
||||
@@ -28,7 +28,7 @@
|
||||
class="text-secondary icon-[lucide--play] size-4"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--pause] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<!-- Time Display -->
|
||||
<div class="text-sm font-normal text-nowrap text-base-foreground">
|
||||
@@ -57,11 +57,11 @@
|
||||
<!-- Right Actions -->
|
||||
<div class="relative flex shrink-0 items-center justify-start gap-2">
|
||||
<!-- Volume Button -->
|
||||
<div
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.volume')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="toggleMute"
|
||||
>
|
||||
<i
|
||||
@@ -73,19 +73,32 @@
|
||||
class="text-secondary icon-[lucide--volume-1] size-4"
|
||||
/>
|
||||
<i v-else class="text-secondary icon-[lucide--volume-x] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<!-- Download Button -->
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
size="icon-sm"
|
||||
variant="textonly"
|
||||
:aria-label="$t('g.downloadAudio')"
|
||||
:title="$t('g.downloadAudio')"
|
||||
class="size-6 hover:bg-interface-menu-component-surface-hovered"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<i class="text-secondary icon-[lucide--download] size-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Options Button -->
|
||||
<div
|
||||
<Button
|
||||
v-if="showOptionsButton"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
class="flex size-6 cursor-pointer items-center justify-center rounded hover:bg-interface-menu-component-surface-hovered"
|
||||
class="size-6 rounded"
|
||||
@click="toggleOptionsMenu"
|
||||
>
|
||||
<i class="text-secondary icon-[lucide--more-vertical] size-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Options Menu -->
|
||||
@@ -137,11 +150,16 @@ import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { whenever } from '@vueuse/core'
|
||||
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { formatTime } from '../../utils/audioUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -187,6 +205,20 @@ const togglePlayPause = () => {
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!modelValue.value) return
|
||||
try {
|
||||
downloadFile(modelValue.value)
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadFile'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMute = () => {
|
||||
if (audioRef.value) {
|
||||
isMuted.value = !isMuted.value
|
||||
|
||||
@@ -42,11 +42,11 @@ export function useTextPreviewWidget(
|
||||
widgetValue.value = typeof value === 'string' ? value : String(value)
|
||||
},
|
||||
getMinHeight: () => options.minHeight ?? 42 + PADDING,
|
||||
serialize: false,
|
||||
read_only: true
|
||||
},
|
||||
type: inputSpec.type
|
||||
})
|
||||
widget.serialize = false
|
||||
addWidget(node, widget)
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ async function getAuthHeaders() {
|
||||
return {}
|
||||
}
|
||||
|
||||
const dataCache = new Map<string, CacheEntry<any>>()
|
||||
const dataCache = new Map<string, CacheEntry<unknown>>()
|
||||
|
||||
const createCacheKey = (config: RemoteWidgetConfig): string => {
|
||||
const { route, query_params = {}, refresh = 0 } = config
|
||||
@@ -49,7 +49,9 @@ const getBackoff = (retryCount: number) =>
|
||||
Math.min(1000 * Math.pow(2, retryCount), 512)
|
||||
|
||||
const isInitialized = (entry: CacheEntry<unknown> | undefined) =>
|
||||
entry?.data && entry?.timestamp && entry.timestamp > 0
|
||||
entry?.data !== undefined &&
|
||||
entry?.timestamp !== undefined &&
|
||||
entry.timestamp > 0
|
||||
|
||||
const isStale = (entry: CacheEntry<unknown> | undefined, ttl: number) =>
|
||||
entry?.timestamp && Date.now() - entry.timestamp >= ttl
|
||||
@@ -128,9 +130,11 @@ export function useRemoteWidget<
|
||||
return !isLoaded && isInitialized(dataCache.get(cacheKey))
|
||||
}
|
||||
|
||||
const onFirstLoad = (data: T[]) => {
|
||||
const onFirstLoad = (data: T | T[]) => {
|
||||
isLoaded = true
|
||||
widget.value = data[0]
|
||||
const nextValue =
|
||||
Array.isArray(data) && data.length > 0 ? data[0] : undefined
|
||||
widget.value = nextValue ?? (Array.isArray(data) ? defaultValue : data)
|
||||
widget.callback?.(widget.value)
|
||||
node.graph?.setDirtyCanvas(true)
|
||||
}
|
||||
@@ -138,13 +142,16 @@ export function useRemoteWidget<
|
||||
const fetchValue = async () => {
|
||||
const entry = dataCache.get(cacheKey)
|
||||
|
||||
if (isFailed(entry)) return entry!.data
|
||||
if (isFailed(entry)) return entry!.data as T
|
||||
|
||||
const isValid =
|
||||
isInitialized(entry) && (isPermanent || !isStale(entry, refresh))
|
||||
if (isValid || isBackingOff(entry) || isFetching(entry)) return entry!.data
|
||||
if (isValid || isBackingOff(entry) || isFetching(entry))
|
||||
return entry!.data as T
|
||||
|
||||
const currentEntry: CacheEntry<T> = entry || { data: defaultValue }
|
||||
const currentEntry: CacheEntry<T> = (entry as
|
||||
| CacheEntry<T>
|
||||
| undefined) || { data: defaultValue }
|
||||
dataCache.set(cacheKey, currentEntry)
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
|
||||
import { type StatusWsMessageStatus } from '@/schemas/apiSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs'
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -33,17 +35,6 @@ type Props = {
|
||||
|
||||
type Children = Element[] | Element | string | string[]
|
||||
|
||||
/**
|
||||
* @deprecated Legacy queue item structure from old history API.
|
||||
* Will be removed when ComfyList is migrated to Jobs API.
|
||||
*/
|
||||
interface LegacyQueueItem {
|
||||
prompt: [unknown, string, unknown, { extra_pnginfo: { workflow: unknown } }]
|
||||
outputs?: Record<string, unknown>
|
||||
meta?: Record<string, { display_node?: string }>
|
||||
remove?: { name: string; cb: () => Promise<void> | void }
|
||||
}
|
||||
|
||||
type ElementType<K extends string> = K extends keyof HTMLElementTagNameMap
|
||||
? HTMLElementTagNameMap[K]
|
||||
: HTMLElement
|
||||
@@ -261,7 +252,10 @@ class ComfyList {
|
||||
}
|
||||
|
||||
async load() {
|
||||
const items = await api.getItems(this._type)
|
||||
const items =
|
||||
this._type === 'history'
|
||||
? { history: await api.getHistory() }
|
||||
: await api.getQueue()
|
||||
this.element.replaceChildren(
|
||||
...Object.keys(items).flatMap((section) => [
|
||||
$el('h4', {
|
||||
@@ -270,29 +264,30 @@ class ComfyList {
|
||||
$el('div.comfy-list-items', [
|
||||
// @ts-expect-error fixme ts strict error
|
||||
...(this._reverse ? items[section].reverse() : items[section]).map(
|
||||
(item: LegacyQueueItem) => {
|
||||
(item: JobListItem) => {
|
||||
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
|
||||
const removeAction = item.remove ?? {
|
||||
name: 'Delete',
|
||||
cb: () => api.deleteItem(this._type, item.prompt[1])
|
||||
}
|
||||
return $el('div', { textContent: item.prompt[0] + ': ' }, [
|
||||
const removeAction =
|
||||
section === 'Running'
|
||||
? {
|
||||
name: 'Cancel',
|
||||
cb: () => api.interrupt(item.id)
|
||||
}
|
||||
: {
|
||||
name: 'Delete',
|
||||
cb: () => api.deleteItem(this._type, item.id)
|
||||
}
|
||||
return $el('div', { textContent: item.priority + ': ' }, [
|
||||
$el('button', {
|
||||
textContent: 'Load',
|
||||
onclick: async () => {
|
||||
await app.loadGraphData(
|
||||
item.prompt[3].extra_pnginfo.workflow as Parameters<
|
||||
typeof app.loadGraphData
|
||||
>[0],
|
||||
true,
|
||||
false
|
||||
)
|
||||
if ('outputs' in item && item.outputs) {
|
||||
const job = await api.getJobDetail(item.id)
|
||||
if (!job) return
|
||||
const workflow = await extractWorkflow(job)
|
||||
await app.loadGraphData(workflow, true, false)
|
||||
if ('outputs' in job && job.outputs) {
|
||||
app.nodeOutputs = {}
|
||||
for (const [key, value] of Object.entries(item.outputs)) {
|
||||
const realKey = item['meta']?.[key]?.display_node ?? key
|
||||
// @ts-expect-error fixme ts strict error
|
||||
app.nodeOutputs[realKey] = value
|
||||
for (const [key, value] of Object.entries(job.outputs)) {
|
||||
app.nodeOutputs[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ export const useColorPaletteService = () => {
|
||||
* @param schema - The Zod schema object to analyze.
|
||||
* @returns Array of optional key names.
|
||||
*/
|
||||
const getOptionalKeys = (schema: z.ZodObject<any, any>) => {
|
||||
const getOptionalKeys = (schema: z.ZodObject<z.ZodRawShape>) => {
|
||||
const optionalKeys: string[] = []
|
||||
const shape = schema.shape
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export const useLitegraphService = () => {
|
||||
const state =
|
||||
useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state
|
||||
if (state === 'running') {
|
||||
return { color: '#0f0' }
|
||||
return { color: '#0f0', lineWidth: 3 }
|
||||
}
|
||||
}
|
||||
node.strokeStyles['dragOver'] = function (this: LGraphNode) {
|
||||
@@ -172,7 +172,7 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
node.strokeStyles['executionError'] = function (this: LGraphNode) {
|
||||
if (app.lastExecutionError?.node_id == this.id) {
|
||||
return { color: '#f0f', lineWidth: 2 }
|
||||
return { color: '#f0f', lineWidth: 3 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,7 +849,7 @@ export const useLitegraphService = () => {
|
||||
|
||||
function addNodeOnGraph(
|
||||
nodeDef: ComfyNodeDefV1 | ComfyNodeDefV2,
|
||||
options: Record<string, any> = {}
|
||||
options: Record<string, unknown> & { pos?: Point } = {}
|
||||
): LGraphNode {
|
||||
options.pos ??= getCanvasCenter()
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ type UseLoad3dViewerFn = (node?: LGraphNode) => {
|
||||
handleModelDrop: (file: File) => Promise<void>
|
||||
handleSeek: (progress: number) => void
|
||||
needApplyChanges: { value: boolean }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Type for SkeletonUtils module
|
||||
@@ -81,7 +80,7 @@ interface Load3DNode extends LGraphNode {
|
||||
syncLoad3dConfig?: () => void
|
||||
}
|
||||
|
||||
const viewerInstances = new Map<NodeId, any>()
|
||||
const viewerInstances = new Map<NodeId, ReturnType<UseLoad3dViewerFn>>()
|
||||
|
||||
export class Load3dService {
|
||||
private static instance: Load3dService
|
||||
@@ -165,12 +164,15 @@ export class Load3dService {
|
||||
* Only works after useLoad3dViewer has been loaded.
|
||||
* Returns null if module not yet loaded - use async version instead.
|
||||
*/
|
||||
getOrCreateViewerSync(node: LGraphNode, useLoad3dViewer: UseLoad3dViewerFn) {
|
||||
getOrCreateViewerSync<T extends UseLoad3dViewerFn>(
|
||||
node: LGraphNode,
|
||||
useLoad3dViewer: T
|
||||
): ReturnType<T> {
|
||||
if (!viewerInstances.has(node.id)) {
|
||||
viewerInstances.set(node.id, useLoad3dViewer(node))
|
||||
}
|
||||
|
||||
return viewerInstances.get(node.id)
|
||||
return viewerInstances.get(node.id) as ReturnType<T>
|
||||
}
|
||||
|
||||
removeViewer(node: LGraphNode) {
|
||||
@@ -288,6 +290,7 @@ export class Load3dService {
|
||||
|
||||
async handleViewerClose(node: LGraphNode) {
|
||||
const viewer = await useLoad3dService().getOrCreateViewer(node)
|
||||
if (!viewer) return
|
||||
|
||||
if (viewer.needApplyChanges.value) {
|
||||
await viewer.applyChanges()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import type { TgpuRoot } from 'typegpu'
|
||||
|
||||
import {
|
||||
BrushShape,
|
||||
@@ -71,7 +72,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
|
||||
|
||||
const canvasHistory = useCanvasHistory(20)
|
||||
|
||||
const tgpuRoot = ref<any>(null)
|
||||
const tgpuRoot = ref<TgpuRoot | null>(null)
|
||||
|
||||
const colorInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ComfyNodeDefImpl
|
||||
// V2 fields
|
||||
readonly inputs: Record<string, InputSpecV2>
|
||||
readonly outputs: OutputSpecV2[]
|
||||
readonly hidden?: Record<string, any>
|
||||
readonly hidden?: Record<string, boolean>
|
||||
|
||||
// ComfyNodeDefImpl fields
|
||||
readonly nodeSource: NodeSource
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface NodesIndexSuggestion {
|
||||
exact_nb_hits: number
|
||||
facets: {
|
||||
exact_matches: Record<string, number>
|
||||
analytics: Record<string, any>
|
||||
analytics: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
objectID: RegistryNodePack['id']
|
||||
|
||||