mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-01 12:07:17 +00:00
Compare commits
52 Commits
manager/co
...
bugfix/uns
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d19bd2f32 | ||
|
|
323db0e049 | ||
|
|
3978613f14 | ||
|
|
0a40e07f7e | ||
|
|
577af51ff8 | ||
|
|
df7c7383e2 | ||
|
|
1279f30f5a | ||
|
|
9ab4b549c0 | ||
|
|
10de4e5445 | ||
|
|
30420f2c0a | ||
|
|
39c3a57c11 | ||
|
|
6d09b7165f | ||
|
|
8fc6840434 | ||
|
|
db575425fe | ||
|
|
ccb71bf1a3 | ||
|
|
733d71aaac | ||
|
|
e059b9b82f | ||
|
|
cfaf769a65 | ||
|
|
b80e0e1a3c | ||
|
|
7b7d9905a7 | ||
|
|
594fc5945c | ||
|
|
e5abf765bd | ||
|
|
712c127bb5 | ||
|
|
854501ef27 | ||
|
|
aea4493b4d | ||
|
|
df47226fd4 | ||
|
|
f26f5f25bb | ||
|
|
284902cabe | ||
|
|
58dec5ea42 | ||
|
|
7e76665a22 | ||
|
|
cb06d96930 | ||
|
|
b01ddb6aff | ||
|
|
10bed33383 | ||
|
|
a57e60d60a | ||
|
|
8c789bd05d | ||
|
|
28def833f9 | ||
|
|
fcc22f06ac | ||
|
|
3922a5882b | ||
|
|
4a40e83b98 | ||
|
|
21e0caa1b1 | ||
|
|
04af8cda4d | ||
|
|
504b717575 | ||
|
|
62fdcd4949 | ||
|
|
cb7adaef9b | ||
|
|
6aad5222ab | ||
|
|
690326c374 | ||
|
|
25ce267b2e | ||
|
|
78e3a20773 | ||
|
|
56dbcbbd22 | ||
|
|
4bfc8e9e33 | ||
|
|
6e72207927 | ||
|
|
71968ae133 |
@@ -51,7 +51,10 @@
|
||||
0.85,
|
||||
false,
|
||||
false,
|
||||
""
|
||||
"",
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('Keybindings', () => {
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('Should handle async command errors', async ({ comfyPage }) => {
|
||||
@@ -45,6 +45,6 @@ test.describe('Keybindings', () => {
|
||||
})
|
||||
|
||||
await comfyPage.executeCommand('TestCommand')
|
||||
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
|
||||
expect(await comfyPage.getToastErrorCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -323,7 +323,21 @@ test.describe('Error dialog', () => {
|
||||
|
||||
await comfyPage.loadWorkflow('default')
|
||||
|
||||
const errorDialog = comfyPage.page.locator('.error-dialog-content')
|
||||
const errorDialog = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(errorDialog).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should display an error dialog when prompt execution fails', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
const app = window['app']
|
||||
app.api.queuePrompt = () => {
|
||||
throw new Error('Error on queuePrompt!')
|
||||
}
|
||||
await app.queuePrompt(0)
|
||||
})
|
||||
const errorDialog = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(errorDialog).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
20
browser_tests/tests/execution.spec.ts
Normal file
20
browser_tests/tests/execution.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Execution', () => {
|
||||
test('Report error on unconnected slot', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
await comfyPage.clickEmptySpace()
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueuePrompt')
|
||||
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
|
||||
await comfyPage.page.locator('.p-dialog-close-button').click()
|
||||
await comfyPage.page.locator('.comfy-error-report').waitFor({
|
||||
state: 'hidden'
|
||||
})
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'execution-error-unconnected-slot.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
@@ -39,6 +39,10 @@ test.describe('Reroute Node', () => {
|
||||
})
|
||||
|
||||
test.describe('LiteGraph Native Reroute Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
|
||||
})
|
||||
|
||||
test('loads from workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('reroute/native_reroute')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
@@ -246,5 +246,24 @@ test.describe('Selection Toolbox', () => {
|
||||
)
|
||||
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
|
||||
})
|
||||
|
||||
test('colorization via color picker can be undone', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Select a node and color it
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container i[data-testid="blue"]')
|
||||
.click()
|
||||
|
||||
// Undo the colorization
|
||||
await comfyPage.page.keyboard.press('Control+Z')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node should be uncolored again
|
||||
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
expect(await selectedNode.getProperty('color')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -192,3 +192,19 @@ test.describe('Load audio widget', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Unserialized widgets', () => {
|
||||
test('Unserialized widgets values do not mark graph as modified', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
|
||||
await comfyPage.loadWorkflow('widgets/load_image_widget')
|
||||
|
||||
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
|
||||
await comfyPage.page.mouse.move(10, 10)
|
||||
await comfyPage.page.mouse.click(10, 10)
|
||||
|
||||
// Expect the graph to not be modified
|
||||
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.15.3",
|
||||
"version": "1.15.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.15.3",
|
||||
"version": "1.15.11",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.11.3",
|
||||
"@comfyorg/litegraph": "^0.11.10",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -77,7 +77,7 @@
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.15",
|
||||
"vite": "^5.4.16",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vue-tsc": "^2.1.10",
|
||||
@@ -478,9 +478,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.11.3.tgz",
|
||||
"integrity": "sha512-OEZJMFbC4WwsIW+w42MA8cwMY/Uu+OL5tQfvdhrlIj51G/TZv1yvU7tNPeu0gZM24Vm5rzF9IxXF8aajD9BIlw==",
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.11.10.tgz",
|
||||
"integrity": "sha512-3kMndikjaJSJ0NDQyueOKJKRI7GwThMIbdLSdPREnHiyCPpNusNev7bmzP4XvlzuLppmhH7dEwzBTWTQtQVRyA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -12559,9 +12559,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.15",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
|
||||
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
|
||||
"version": "5.4.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz",
|
||||
"integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.15.3",
|
||||
"version": "1.15.11",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -60,7 +60,7 @@
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.15",
|
||||
"vite": "^5.4.16",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vue-tsc": "^2.1.10",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.11.3",
|
||||
"@comfyorg/litegraph": "^0.11.10",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -17,7 +17,9 @@ const TITLE_SUFFIX = ' - ComfyUI'
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
||||
executionStore.isIdle
|
||||
? ''
|
||||
: `[${Math.round(executionStore.executionProgress * 100)}%]`
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
@@ -41,7 +43,7 @@ const workflowNameText = computed(() => {
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${executionStore.executingNodeProgress}%] ${executionStore.executingNode.type}`
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
|
||||
|
||||
@@ -1,80 +1,159 @@
|
||||
<template>
|
||||
<div class="error-dialog-content flex flex-col gap-4">
|
||||
<div class="comfy-error-report flex flex-col gap-4">
|
||||
<NoResultsPlaceholder
|
||||
class="pb-0"
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="title"
|
||||
:message="errorMessage"
|
||||
:message="error.exceptionMessage"
|
||||
/>
|
||||
<pre
|
||||
class="stack-trace p-5 text-neutral-400 text-xs max-h-[50vh] overflow-auto bg-black/20"
|
||||
>
|
||||
{{ stackTrace }}
|
||||
</pre>
|
||||
|
||||
<template v-if="extensionFile">
|
||||
<template v-if="error.extensionFile">
|
||||
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
|
||||
<br />
|
||||
<span class="font-bold">{{ extensionFile }}</span>
|
||||
<span class="font-bold">{{ error.extensionFile }}</span>
|
||||
</template>
|
||||
|
||||
<Button
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
fluid
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2 justify-center">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
/>
|
||||
<Button
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
<ScrollPanel class="w-full h-[400px] max-w-[80vw]">
|
||||
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
:error-type="errorType"
|
||||
:extra-fields="[
|
||||
{
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
getData: () => stackTrace
|
||||
}
|
||||
]"
|
||||
:title="$t('issueReport.submitErrorReport')"
|
||||
:error-type="error.reportType ?? 'unknownError'"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{
|
||||
exceptionMessage: errorMessage,
|
||||
extensionFile: extensionFile ?? 'UNKNOWN'
|
||||
exceptionMessage: error.exceptionMessage,
|
||||
nodeType: error.nodeType ?? 'UNKNOWN'
|
||||
}"
|
||||
:title="t('issueReport.submitErrorReport')"
|
||||
/>
|
||||
<div class="flex gap-4 justify-end">
|
||||
<FindIssueButton
|
||||
:errorMessage="error.exceptionMessage"
|
||||
:repoOwner="repoOwner"
|
||||
:repoName="repoName"
|
||||
/>
|
||||
<Button
|
||||
v-if="reportOpen"
|
||||
:label="$t('g.copyToClipboard')"
|
||||
icon="pi pi-copy"
|
||||
@click="copyReportToClipboard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
import {
|
||||
type ErrorReportData,
|
||||
generateErrorReport
|
||||
} from '@/utils/errorReportUtil'
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
title: _title,
|
||||
errorMessage,
|
||||
stackTrace: _stackTrace,
|
||||
extensionFile,
|
||||
errorType = 'frontendError'
|
||||
} = defineProps<{
|
||||
title?: string
|
||||
errorMessage: string
|
||||
stackTrace?: string
|
||||
extensionFile?: string
|
||||
errorType?: string
|
||||
const { error } = defineProps<{
|
||||
error: Omit<ErrorReportData, 'workflow' | 'systemStats' | 'serverLogs'> & {
|
||||
/**
|
||||
* The type of error report to submit.
|
||||
* @default 'unknownError'
|
||||
*/
|
||||
reportType?: string
|
||||
/**
|
||||
* The file name of the extension that caused the error.
|
||||
*/
|
||||
extensionFile?: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const title = computed(() => _title ?? t('errorDialog.defaultTitle'))
|
||||
const stackTrace = computed(() => _stackTrace ?? t('errorDialog.noStackTrace'))
|
||||
|
||||
const repoOwner = 'comfyanonymous'
|
||||
const repoName = 'ComfyUI'
|
||||
const reportContent = ref('')
|
||||
const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const sendReportOpen = ref(false)
|
||||
function showSendReport() {
|
||||
const showSendReport = () => {
|
||||
sendReportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const title = computed<string>(
|
||||
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
|
||||
)
|
||||
|
||||
const stackTraceField = computed<ReportField>(() => {
|
||||
return {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
getData: () => error.traceback
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
|
||||
try {
|
||||
const [logs] = await Promise.all([api.getLogs()])
|
||||
|
||||
reportContent.value = generateErrorReport({
|
||||
systemStats: systemStatsStore.systemStats!,
|
||||
serverLogs: logs,
|
||||
workflow: app.graph.serialize(),
|
||||
exceptionType: error.exceptionType,
|
||||
exceptionMessage: error.exceptionMessage,
|
||||
traceback: error.traceback,
|
||||
nodeId: error.nodeId,
|
||||
nodeType: error.nodeType
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToFetchLogs'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const copyReportToClipboard = async () => {
|
||||
await copyToClipboard(reportContent.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="props.error.node_type"
|
||||
:message="props.error.exception_message"
|
||||
/>
|
||||
<div class="comfy-error-report">
|
||||
<div class="flex gap-2 justify-center">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
/>
|
||||
<Button
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
|
||||
<pre class="wrapper-pre">{{ reportContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
:title="$t('issueReport.submitErrorReport')"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{
|
||||
exceptionMessage: props.error.exception_message,
|
||||
nodeType: props.error.node_type
|
||||
}"
|
||||
/>
|
||||
<div class="action-container">
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
:repoName="repoName"
|
||||
/>
|
||||
<Button
|
||||
v-if="reportOpen"
|
||||
:label="$t('g.copyToClipboard')"
|
||||
icon="pi pi-copy"
|
||||
@click="copyReportToClipboard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
}>()
|
||||
|
||||
const repoOwner = 'comfyanonymous'
|
||||
const repoName = 'ComfyUI'
|
||||
const reportContent = ref('')
|
||||
const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const sendReportOpen = ref(false)
|
||||
const showSendReport = () => {
|
||||
sendReportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const stackTraceField = computed<ReportField>(() => {
|
||||
return {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
getData: () => props.error.traceback?.join('\n')
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [systemStats, logs] = await Promise.all([
|
||||
api.getSystemStats(),
|
||||
api.getLogs()
|
||||
])
|
||||
generateReport(systemStats, logs)
|
||||
} catch (error) {
|
||||
console.error('Error fetching system stats or logs:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to fetch system information',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const generateReport = (systemStats: SystemStats, logs: string) => {
|
||||
// The default JSON workflow has about 3000 characters.
|
||||
const MAX_JSON_LENGTH = 20000
|
||||
const workflowJSONString = JSON.stringify(app.graph.serialize())
|
||||
const workflowText =
|
||||
workflowJSONString.length > MAX_JSON_LENGTH
|
||||
? 'Workflow too large. Please manually upload the workflow from local file system.'
|
||||
: workflowJSONString
|
||||
|
||||
reportContent.value = `
|
||||
# ComfyUI Error Report
|
||||
## Error Details
|
||||
- **Node ID:** ${props.error.node_id}
|
||||
- **Node Type:** ${props.error.node_type}
|
||||
- **Exception Type:** ${props.error.exception_type}
|
||||
- **Exception Message:** ${props.error.exception_message}
|
||||
## Stack Trace
|
||||
\`\`\`
|
||||
${props.error.traceback.join('\n')}
|
||||
\`\`\`
|
||||
## System Information
|
||||
- **ComfyUI Version:** ${systemStats.system.comfyui_version}
|
||||
- **Arguments:** ${systemStats.system.argv.join(' ')}
|
||||
- **OS:** ${systemStats.system.os}
|
||||
- **Python Version:** ${systemStats.system.python_version}
|
||||
- **Embedded Python:** ${systemStats.system.embedded_python}
|
||||
- **PyTorch Version:** ${systemStats.system.pytorch_version}
|
||||
## Devices
|
||||
${systemStats.devices
|
||||
.map(
|
||||
(device) => `
|
||||
- **Name:** ${device.name}
|
||||
- **Type:** ${device.type}
|
||||
- **VRAM Total:** ${device.vram_total}
|
||||
- **VRAM Free:** ${device.vram_free}
|
||||
- **Torch VRAM Total:** ${device.torch_vram_total}
|
||||
- **Torch VRAM Free:** ${device.torch_vram_free}
|
||||
`
|
||||
)
|
||||
.join('\n')}
|
||||
## Logs
|
||||
\`\`\`
|
||||
${logs}
|
||||
\`\`\`
|
||||
## Attached Workflow
|
||||
Please make sure that workflow does not contain any sensitive information such as API keys or passwords.
|
||||
\`\`\`
|
||||
${workflowText}
|
||||
\`\`\`
|
||||
|
||||
## Additional Context
|
||||
(Please add any additional context or steps to reproduce the error here)
|
||||
`
|
||||
}
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const copyReportToClipboard = async () => {
|
||||
await copyToClipboard(reportContent.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-error-report {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.wrapper-pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -188,59 +188,52 @@ const {
|
||||
isLoading: isLoadingWorkflow
|
||||
} = useWorkflowPacks()
|
||||
|
||||
const getInstalledResults = () => {
|
||||
if (isEmptySearch.value) {
|
||||
startFetchInstalled()
|
||||
return installedPacks.value
|
||||
} else {
|
||||
return filterInstalledPack(searchResults.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getInWorkflowResults = () => {
|
||||
if (isEmptySearch.value) {
|
||||
startFetchWorkflowPacks()
|
||||
return workflowPacks.value
|
||||
} else {
|
||||
return filterWorkflowPack(searchResults.value)
|
||||
}
|
||||
}
|
||||
|
||||
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
|
||||
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
|
||||
|
||||
const setMissingPacks = () => {
|
||||
displayPacks.value = filterMissingPacks(workflowPacks.value)
|
||||
}
|
||||
const isInstalledTab = computed(
|
||||
() => selectedTab.value?.id === ManagerTab.Installed
|
||||
)
|
||||
const isMissingTab = computed(
|
||||
() => selectedTab.value?.id === ManagerTab.Missing
|
||||
)
|
||||
const isWorkflowTab = computed(
|
||||
() => selectedTab.value?.id === ManagerTab.Workflow
|
||||
)
|
||||
const isAllTab = computed(() => selectedTab.value?.id === ManagerTab.All)
|
||||
|
||||
const getMissingPacks = () => {
|
||||
if (isEmptySearch.value) {
|
||||
startFetchWorkflowPacks()
|
||||
whenever(() => workflowPacks.value.length, setMissingPacks, {
|
||||
immediate: true,
|
||||
once: true
|
||||
})
|
||||
return filterMissingPacks(workflowPacks.value)
|
||||
watch([isInstalledTab, installedPacks], () => {
|
||||
if (!isInstalledTab.value) return
|
||||
|
||||
if (!isEmptySearch.value) {
|
||||
displayPacks.value = filterInstalledPack(searchResults.value)
|
||||
} else if (!installedPacks.value.length) {
|
||||
startFetchInstalled()
|
||||
} else {
|
||||
return filterMissingPacks(filterWorkflowPack(searchResults.value))
|
||||
displayPacks.value = installedPacks.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const onTabChange = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.Installed:
|
||||
displayPacks.value = getInstalledResults()
|
||||
break
|
||||
case ManagerTab.Workflow:
|
||||
displayPacks.value = getInWorkflowResults()
|
||||
break
|
||||
case ManagerTab.Missing:
|
||||
displayPacks.value = getMissingPacks()
|
||||
break
|
||||
default:
|
||||
displayPacks.value = searchResults.value
|
||||
watch([isMissingTab, isWorkflowTab, workflowPacks], () => {
|
||||
if (!isWorkflowTab.value && !isMissingTab.value) return
|
||||
|
||||
if (!isEmptySearch.value) {
|
||||
displayPacks.value = isMissingTab.value
|
||||
? filterMissingPacks(filterWorkflowPack(searchResults.value))
|
||||
: filterWorkflowPack(searchResults.value)
|
||||
} else if (!workflowPacks.value.length) {
|
||||
startFetchWorkflowPacks()
|
||||
} else {
|
||||
displayPacks.value = isMissingTab.value
|
||||
? filterMissingPacks(workflowPacks.value)
|
||||
: workflowPacks.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch([isAllTab, searchResults], () => {
|
||||
if (!isAllTab.value) return
|
||||
displayPacks.value = searchResults.value
|
||||
})
|
||||
|
||||
const onResultsChange = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
@@ -260,7 +253,6 @@ const onResultsChange = () => {
|
||||
}
|
||||
}
|
||||
|
||||
whenever(selectedTab, onTabChange)
|
||||
watch(searchResults, onResultsChange, { flush: 'pre' })
|
||||
watch(() => comfyManagerStore.installedPacksIds, onResultsChange)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
:pt="{
|
||||
header: 'px-0'
|
||||
}"
|
||||
@rowDblclick="editKeybinding($event.data)"
|
||||
>
|
||||
<Column field="actions" header="">
|
||||
<template #body="slotProps">
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const tabs = [{ label: 'Installation Queue' }, { label: 'Failed (0)' }]
|
||||
const { t } = useI18n()
|
||||
const tabs = [
|
||||
{ label: t('manager.installationQueue') },
|
||||
{ label: t('manager.failed', { count: 0 }) }
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
@@ -61,13 +62,15 @@ import { usePaste } from '@/composables/usePaste'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -80,6 +83,7 @@ const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
@@ -158,6 +162,55 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of the executing node
|
||||
watch(
|
||||
() =>
|
||||
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
|
||||
NodeId | null,
|
||||
number | null
|
||||
],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
} else {
|
||||
node.progress = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
watch(
|
||||
() => executionStore.lastNodeErrors,
|
||||
(lastNodeErrors) => {
|
||||
const removeSlotError = (node: LGraphNode) => {
|
||||
for (const slot of node.inputs) {
|
||||
delete slot.hasErrors
|
||||
}
|
||||
for (const slot of node.outputs) {
|
||||
delete slot.hasErrors
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
removeSlotError(node)
|
||||
const nodeErrors = lastNodeErrors?.[node.id]
|
||||
if (!nodeErrors) continue
|
||||
for (const error of nodeErrors.errors) {
|
||||
if (error.extra_info && error.extra_info.input_name) {
|
||||
const inputIndex = node.findInputSlot(error.extra_info.input_name)
|
||||
if (inputIndex !== -1) {
|
||||
node.inputs[inputIndex].hasErrors = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comfyApp.canvas.draw(true, true)
|
||||
}
|
||||
)
|
||||
|
||||
const loadCustomNodesI18n = async () => {
|
||||
try {
|
||||
const i18nData = await api.getCustomNodesI18n()
|
||||
@@ -188,7 +241,20 @@ onMounted(async () => {
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init(comfyApp)
|
||||
await loadCustomNodesI18n()
|
||||
await settingStore.loadSettingValues()
|
||||
try {
|
||||
await settingStore.loadSettingValues()
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
console.log(
|
||||
'Failed loading user settings, user unauthorized, cleaning local Comfy.userId'
|
||||
)
|
||||
localStorage.removeItem('Comfy.userId')
|
||||
localStorage.removeItem('Comfy.userName')
|
||||
window.location.reload()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { getItemsColorOption } from '@/utils/litegraphUtil'
|
||||
@@ -54,6 +55,7 @@ import { getItemsColorOption } from '@/utils/litegraphUtil'
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
@@ -108,6 +110,7 @@ const applyColor = (colorOption: ColorOption | null) => {
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
currentColorOption.value = canvasColorOption
|
||||
showColorPicker.value = false
|
||||
workflowStore.activeWorkflow?.changeTracker.checkState()
|
||||
}
|
||||
|
||||
const currentColorOption = ref<CanvasColorOption | null>(null)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -63,9 +62,9 @@ const updateDomClipping = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas || !widgetElement.value) return
|
||||
|
||||
const selectedNode = Object.values(
|
||||
lgCanvas.selected_nodes ?? {}
|
||||
)[0] as LGraphNode
|
||||
const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]
|
||||
if (!selectedNode) return
|
||||
|
||||
const node = widget.node
|
||||
const isSelected = selectedNode === node
|
||||
const renderArea = selectedNode?.renderArea
|
||||
|
||||
@@ -60,12 +60,17 @@
|
||||
<!-- FilterAndValue -->
|
||||
<template v-slot:chip="{ value }">
|
||||
<SearchFilterChip
|
||||
v-if="Array.isArray(value) && value.length === 2"
|
||||
:key="`${value[0].id}-${value[1]}`"
|
||||
@remove="onRemoveFilter($event, value as FilterAndValue)"
|
||||
:text="value[1]"
|
||||
:badge="value[0].invokeSequence.toUpperCase()"
|
||||
:badge-class="value[0].invokeSequence + '-badge'"
|
||||
v-if="value.filterDef && value.value"
|
||||
:key="`${value.filterDef.id}-${value.value}`"
|
||||
@remove="
|
||||
onRemoveFilter(
|
||||
$event,
|
||||
value as FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
)
|
||||
"
|
||||
:text="value.value"
|
||||
:badge="value.filterDef.invokeSequence.toUpperCase()"
|
||||
:badge-class="value.filterDef.invokeSequence + '-badge'"
|
||||
/>
|
||||
</template>
|
||||
</AutoCompletePlus>
|
||||
@@ -82,13 +87,13 @@ import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import { type FilterAndValue } from '@/services/nodeSearchService'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore,
|
||||
useNodeFrequencyStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
|
||||
@@ -100,7 +105,7 @@ const enableNodePreview = computed(() =>
|
||||
)
|
||||
|
||||
const { filters, searchLimit = 64 } = defineProps<{
|
||||
filters: FilterAndValue[]
|
||||
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[]
|
||||
searchLimit?: number
|
||||
}>()
|
||||
|
||||
@@ -139,11 +144,16 @@ const reFocusInput = () => {
|
||||
}
|
||||
|
||||
onMounted(reFocusInput)
|
||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||
const onAddFilter = (
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
nodeSearchFilterVisible.value = false
|
||||
emit('addFilter', filterAndValue)
|
||||
}
|
||||
const onRemoveFilter = (event: Event, filterAndValue: FilterAndValue) => {
|
||||
const onRemoveFilter = (
|
||||
event: Event,
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
emit('removeFilter', filterAndValue)
|
||||
|
||||
@@ -46,13 +46,13 @@ import Dialog from 'primevue/dialog'
|
||||
import { computed, ref, toRaw, watchEffect } from 'vue'
|
||||
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
|
||||
@@ -71,11 +71,13 @@ const getNewNodeLocation = (): Vector2 => {
|
||||
.originalEvent
|
||||
return [originalEvent.canvasX, originalEvent.canvasY]
|
||||
}
|
||||
const nodeFilters = ref<FilterAndValue[]>([])
|
||||
const addFilter = (filter: FilterAndValue) => {
|
||||
const nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])
|
||||
const addFilter = (filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) => {
|
||||
nodeFilters.value.push(filter)
|
||||
}
|
||||
const removeFilter = (filter: FilterAndValue) => {
|
||||
const removeFilter = (
|
||||
filter: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
nodeFilters.value = nodeFilters.value.filter(
|
||||
(f) => toRaw(f) !== toRaw(filter)
|
||||
)
|
||||
@@ -136,13 +138,16 @@ const showNewSearchBox = (e: LiteGraphCanvasEvent) => {
|
||||
return
|
||||
}
|
||||
const firstLink = ConnectingLinkImpl.createFromPlainObject(links[0])
|
||||
const filter = nodeDefStore.nodeSearchService.getFilterById(
|
||||
firstLink.releaseSlotType
|
||||
)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const dataType = firstLink.type.toString()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
addFilter([filter, dataType])
|
||||
const filter =
|
||||
firstLink.releaseSlotType === 'input'
|
||||
? nodeDefStore.nodeSearchService.inputTypeFilter
|
||||
: nodeDefStore.nodeSearchService.outputTypeFilter
|
||||
|
||||
const dataType = firstLink.type?.toString() ?? ''
|
||||
addFilter({
|
||||
filterDef: filter,
|
||||
value: dataType
|
||||
})
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
|
||||
@@ -27,11 +27,11 @@ import Select from 'primevue/select'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { type FilterAndValue, NodeFilter } from '@/services/nodeSearchService'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { FuseFilter, FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
|
||||
const filters = computed(() => nodeDefStore.nodeSearchService.nodeFilters)
|
||||
const selectedFilter = ref<NodeFilter>()
|
||||
const selectedFilter = ref<FuseFilter<ComfyNodeDefImpl, string>>()
|
||||
const filterValues = computed(() => selectedFilter.value?.fuseSearch.data ?? [])
|
||||
const selectedFilterValue = ref<string>('')
|
||||
|
||||
@@ -43,7 +43,10 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'addFilter', filterAndValue: FilterAndValue): void
|
||||
(
|
||||
event: 'addFilter',
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
): void
|
||||
}>()
|
||||
|
||||
const updateSelectedFilterValue = () => {
|
||||
@@ -54,10 +57,13 @@ const updateSelectedFilterValue = () => {
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
emit('addFilter', [
|
||||
selectedFilter.value,
|
||||
selectedFilterValue.value
|
||||
] as FilterAndValue)
|
||||
if (!selectedFilter.value) {
|
||||
return
|
||||
}
|
||||
emit('addFilter', {
|
||||
filterDef: selectedFilter.value,
|
||||
value: selectedFilterValue.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue
|
||||
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
|
||||
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import {
|
||||
ComfyNodeDefImpl,
|
||||
@@ -85,6 +84,7 @@ import {
|
||||
} from '@/stores/nodeDefStore'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { sortedTree } from '@/utils/treeUtil'
|
||||
|
||||
import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue'
|
||||
@@ -150,8 +150,9 @@ const filteredRoot = computed<TreeNode | null>(() => {
|
||||
}
|
||||
return buildNodeDefTree(filteredNodeDefs.value)
|
||||
})
|
||||
const filters: Ref<Array<SearchFilter & { filter: FilterAndValue<string> }>> =
|
||||
ref([])
|
||||
const filters: Ref<
|
||||
(SearchFilter & { filter: FuseFilterWithValue<ComfyNodeDefImpl, string> })[]
|
||||
> = ref([])
|
||||
const handleSearch = (query: string) => {
|
||||
// Don't apply a min length filter because it does not make sense in
|
||||
// multi-byte languages like Chinese, Japanese, Korean, etc.
|
||||
@@ -161,7 +162,7 @@ const handleSearch = (query: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
const f = filters.value.map((f) => f.filter as FilterAndValue<string>)
|
||||
const f = filters.value.map((f) => f.filter)
|
||||
filteredNodeDefs.value = nodeDefStore.nodeSearchService.searchNode(
|
||||
query,
|
||||
f,
|
||||
@@ -179,12 +180,14 @@ const handleSearch = (query: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onAddFilter = (filterAndValue: FilterAndValue) => {
|
||||
const onAddFilter = (
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
filters.value.push({
|
||||
filter: filterAndValue,
|
||||
badge: filterAndValue[0].invokeSequence.toUpperCase(),
|
||||
badgeClass: filterAndValue[0].invokeSequence + '-badge',
|
||||
text: filterAndValue[1],
|
||||
badge: filterAndValue.filterDef.invokeSequence.toUpperCase(),
|
||||
badgeClass: filterAndValue.filterDef.invokeSequence + '-badge',
|
||||
text: filterAndValue.value,
|
||||
id: +new Date()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="result-container" ref="resultContainer">
|
||||
<div
|
||||
class="result-container"
|
||||
ref="resultContainer"
|
||||
@click="handlePreviewClick"
|
||||
>
|
||||
<ComfyImage
|
||||
v-if="result.isImage"
|
||||
:src="result.url"
|
||||
@@ -12,20 +16,10 @@
|
||||
<i class="pi pi-file"></i>
|
||||
<span>{{ result.mediaType }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result.supportsPreview" class="preview-mask">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
severity="secondary"
|
||||
@click="emit('preview', result)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
@@ -48,6 +42,12 @@ const imageFit = computed<string>(() =>
|
||||
settingStore.get('Comfy.Queue.ImageFit')
|
||||
)
|
||||
|
||||
const handlePreviewClick = () => {
|
||||
if (props.result.supportsPreview) {
|
||||
emit('preview', props.result)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.result.mediaType === 'images') {
|
||||
resultContainer.value?.querySelectorAll('img').forEach((img) => {
|
||||
@@ -67,22 +67,11 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.result-container:hover .preview-mask {
|
||||
opacity: 1;
|
||||
.result-container:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -231,6 +231,13 @@ const cancelledWithoutResults = computed(() => {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none; /* Allow clicks to pass through this div */
|
||||
}
|
||||
|
||||
/* Make individual controls clickable again by restoring pointer events */
|
||||
.task-item-details .tag-wrapper,
|
||||
.task-item-details button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.task-node-link {
|
||||
|
||||
46
src/components/toast/RerouteMigrationToast.vue
Normal file
46
src/components/toast/RerouteMigrationToast.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<Toast group="reroute-migration">
|
||||
<template #message>
|
||||
<div class="flex flex-col items-start flex-auto">
|
||||
<div class="font-medium text-lg my-4">
|
||||
{{ t('toastMessages.migrateToLitegraphReroute') }}
|
||||
</div>
|
||||
<Button
|
||||
class="self-end"
|
||||
size="small"
|
||||
:label="t('g.migrate')"
|
||||
severity="warn"
|
||||
@click="migrateToLitegraphReroute"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Toast>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { WorkflowJSON04 } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const migrateToLitegraphReroute = () => {
|
||||
const workflowJSON = app.serializeGraph() as unknown as WorkflowJSON04
|
||||
const migratedWorkflowJSON = migrateLegacyRerouteNodes(workflowJSON)
|
||||
app.loadGraphData(
|
||||
migratedWorkflowJSON,
|
||||
false,
|
||||
false,
|
||||
workflowStore.activeWorkflow
|
||||
)
|
||||
toast.removeGroup('reroute-migration')
|
||||
}
|
||||
</script>
|
||||
36
src/composables/element/useCanvasPositionConversion.ts
Normal file
36
src/composables/element/useCanvasPositionConversion.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { LGraphCanvas, Vector2 } from '@comfyorg/litegraph'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* Convert between canvas and client positions
|
||||
* @param canvasElement - The canvas element
|
||||
* @param lgCanvas - The litegraph canvas
|
||||
* @returns The canvas position conversion functions
|
||||
*/
|
||||
export const useCanvasPositionConversion = (
|
||||
canvasElement: Parameters<typeof useElementBounding>[0],
|
||||
lgCanvas: LGraphCanvas
|
||||
) => {
|
||||
const { left, top } = useElementBounding(canvasElement)
|
||||
|
||||
const clientPosToCanvasPos = (pos: Vector2): Vector2 => {
|
||||
const { offset, scale } = lgCanvas.ds
|
||||
return [
|
||||
(pos[0] - left.value) / scale + offset[0],
|
||||
(pos[1] - top.value) / scale + offset[1]
|
||||
]
|
||||
}
|
||||
|
||||
const canvasPosToClientPos = (pos: Vector2): Vector2 => {
|
||||
const { offset, scale } = lgCanvas.ds
|
||||
return [
|
||||
(pos[0] + offset[0]) * scale + left.value,
|
||||
(pos[1] + offset[1]) * scale + top.value
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
clientPosToCanvasPos,
|
||||
canvasPosToClientPos
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@
|
||||
* @param callbacks - The callbacks to chain.
|
||||
* @returns A new callback that chains the original callback with the callbacks.
|
||||
*/
|
||||
export const useChainCallback = <T extends (...args: any[]) => void>(
|
||||
export const useChainCallback = <
|
||||
O,
|
||||
T extends (this: O, ...args: any[]) => void
|
||||
>(
|
||||
originalCallback: T | undefined,
|
||||
...callbacks: ((...args: Parameters<T>) => void)[]
|
||||
...callbacks: ((this: O, ...args: Parameters<T>) => void)[]
|
||||
) => {
|
||||
return (...args: Parameters<T>) => {
|
||||
originalCallback?.(...args)
|
||||
callbacks.forEach((callback) => callback(...args))
|
||||
return function (this: O, ...args: Parameters<T>) {
|
||||
originalCallback?.call(this, ...args)
|
||||
callbacks.forEach((callback) => callback.call(this, ...args))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { chunk } from 'lodash'
|
||||
import { Ref, computed, isRef, ref } from 'vue'
|
||||
import { get, useAsyncState } from '@vueuse/core'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import { UseNodePacksOptions } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_MAX_CONCURRENT = 6
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
/**
|
||||
* Handles fetching node packs from the registry given a list of node pack IDs
|
||||
@@ -17,54 +11,25 @@ export const useNodePacks = (
|
||||
packsIds: string[] | Ref<string[]>,
|
||||
options: UseNodePacksOptions = {}
|
||||
) => {
|
||||
const { immediate = false, maxConcurrent = DEFAULT_MAX_CONCURRENT } = options
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
const { immediate = false } = options
|
||||
const { getPacksByIds } = useComfyRegistryStore()
|
||||
|
||||
const nodePacks = ref<NodePack[]>([])
|
||||
const processedIds = ref<Set<string>>(new Set())
|
||||
const fetchPacks = () => getPacksByIds.call(get(packsIds).filter(Boolean))
|
||||
|
||||
const queuedPackIds = isRef(packsIds) ? packsIds : ref<string[]>(packsIds)
|
||||
const remainingIds = computed(() =>
|
||||
queuedPackIds.value?.filter((id) => !processedIds.value.has(id))
|
||||
)
|
||||
const chunks = computed(() =>
|
||||
remainingIds.value?.length ? chunk(remainingIds.value, maxConcurrent) : []
|
||||
)
|
||||
|
||||
const fetchPack = (id: Parameters<typeof getPackById.call>[0]) =>
|
||||
id ? getPackById.call(id) : null
|
||||
|
||||
const toRequestBatch = async (ids: string[]) =>
|
||||
Promise.all(ids.map(fetchPack))
|
||||
|
||||
const isValidResponse = (response: NodePack | null) => response !== null
|
||||
|
||||
const fetchPacks = async () => {
|
||||
for (const chunk of chunks.value) {
|
||||
const resolvedChunk = await toRequestBatch(chunk)
|
||||
chunk.forEach((id) => processedIds.value.add(id))
|
||||
if (!resolvedChunk) continue
|
||||
nodePacks.value.push(...resolvedChunk.filter(isValidResponse))
|
||||
}
|
||||
}
|
||||
|
||||
const { isReady, isLoading, error, execute } = useAsyncState(
|
||||
fetchPacks,
|
||||
null,
|
||||
{
|
||||
immediate
|
||||
}
|
||||
)
|
||||
|
||||
const clear = () => {
|
||||
queuedPackIds.value = []
|
||||
isReady.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
const {
|
||||
isReady,
|
||||
isLoading,
|
||||
error,
|
||||
execute,
|
||||
state: nodePacks
|
||||
} = useAsyncState(fetchPacks, [], {
|
||||
immediate
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
getPackById.cancel()
|
||||
clear()
|
||||
getPacksByIds.cancel()
|
||||
isReady.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -116,4 +116,10 @@ export const useLitegraphSettings = () => {
|
||||
'LiteGraph.ContextMenu.Scaling'
|
||||
)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
LiteGraph.Reroute.maxSplineOffset = settingStore.get(
|
||||
'LiteGraph.Reroute.SplineOffset'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, watch, watchEffect } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
@@ -70,15 +70,16 @@ export function useWorkflowPersistence() {
|
||||
}
|
||||
|
||||
// Setup watchers
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow?.key,
|
||||
(activeWorkflowKey) => {
|
||||
if (!activeWorkflowKey) return
|
||||
setStorageValue('Comfy.PreviousWorkflow', activeWorkflowKey)
|
||||
// When the activeWorkflow changes, the graph has already been loaded.
|
||||
// Saving the current state of the graph to the localStorage.
|
||||
persistCurrentWorkflow()
|
||||
}
|
||||
})
|
||||
)
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Restore workflow tabs states
|
||||
|
||||
@@ -752,5 +752,17 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.10.5'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Reroute.SplineOffset',
|
||||
name: 'Reroute spline offset',
|
||||
tooltip: 'The bezier control point offset from the reroute centre point',
|
||||
type: 'slider',
|
||||
defaultValue: 20,
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 400
|
||||
},
|
||||
versionAdded: '1.15.7'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -357,6 +357,78 @@ export class GroupNodeConfig {
|
||||
output_is_list: []
|
||||
})
|
||||
return def
|
||||
} else if (node.type === 'Reroute') {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const linksTo = this.linksTo[node.index]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) {
|
||||
// Being used internally
|
||||
return null
|
||||
}
|
||||
|
||||
let config = {}
|
||||
let rerouteType = '*'
|
||||
if (linksFrom) {
|
||||
for (const [, , id, slot] of linksFrom['0']) {
|
||||
const node = this.nodeData.nodes[id]
|
||||
const input = node.inputs[slot]
|
||||
if (rerouteType === '*') {
|
||||
rerouteType = input.type
|
||||
}
|
||||
if (input.widget) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const targetDef = globalDefs[node.type]
|
||||
const targetWidget =
|
||||
targetDef.input.required[input.widget.name] ??
|
||||
targetDef.input.optional[input.widget.name]
|
||||
|
||||
const widget = [targetWidget[0], config]
|
||||
const res = mergeIfValid(
|
||||
{
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget
|
||||
},
|
||||
targetWidget,
|
||||
false,
|
||||
null,
|
||||
widget
|
||||
)
|
||||
config = res?.customConfig ?? config
|
||||
}
|
||||
}
|
||||
} else if (linksTo) {
|
||||
const [id, slot] = linksTo['0']
|
||||
rerouteType = this.nodeData.nodes[id].outputs[slot].type
|
||||
} else {
|
||||
// Reroute used as a pipe
|
||||
for (const l of this.nodeData.links) {
|
||||
if (l[2] === node.index) {
|
||||
rerouteType = l[5]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (rerouteType === '*') {
|
||||
// Check for an external link
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const t = this.externalFrom[node.index]?.[0]
|
||||
if (t) {
|
||||
rerouteType = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
config.forceInput = true
|
||||
return {
|
||||
input: {
|
||||
required: {
|
||||
[rerouteType]: [rerouteType, config]
|
||||
}
|
||||
},
|
||||
output: [rerouteType],
|
||||
output_name: [],
|
||||
output_is_list: []
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
@@ -808,13 +880,22 @@ export class GroupNodeHandler {
|
||||
link = { ...link }
|
||||
const output = this.groupData.newToOldOutputMap[link.origin_slot]
|
||||
let innerNode = this.innerNodes[output.node.index]
|
||||
let l
|
||||
while (innerNode?.type === 'Reroute') {
|
||||
l = innerNode.getInputLink(0)
|
||||
innerNode = innerNode.getInputNode(0)
|
||||
}
|
||||
|
||||
if (!innerNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (l && GroupNodeHandler.isGroupNode(innerNode)) {
|
||||
return innerNode.updateLink(l)
|
||||
}
|
||||
|
||||
link.origin_id = innerNode.id
|
||||
link.origin_slot = output.slot
|
||||
link.origin_slot = l?.origin_slot ?? output.slot
|
||||
return link
|
||||
}
|
||||
|
||||
@@ -1271,6 +1352,23 @@ export class GroupNodeHandler {
|
||||
}
|
||||
}
|
||||
continue
|
||||
} else if (innerNode.type === 'Reroute') {
|
||||
const rerouteLinks = this.groupData.linksFrom[old.node.index]
|
||||
if (rerouteLinks) {
|
||||
for (const [_, , targetNodeId, targetSlot] of rerouteLinks['0']) {
|
||||
const node = this.innerNodes[targetNodeId]
|
||||
const input = node.inputs[targetSlot]
|
||||
if (input.widget) {
|
||||
const widget = node.widgets?.find(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(w) => w.name === input.widget.name
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1313,6 +1411,30 @@ export class GroupNodeHandler {
|
||||
return true
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
populateReroute(node, nodeId, map) {
|
||||
if (node.type !== 'Reroute') return
|
||||
|
||||
const link = this.groupData.linksFrom[nodeId]?.[0]?.[0]
|
||||
if (!link) return
|
||||
const [, , targetNodeId, targetNodeSlot] = link
|
||||
const targetNode = this.groupData.nodeData.nodes[targetNodeId]
|
||||
const inputs = targetNode.inputs
|
||||
const targetWidget = inputs?.[targetNodeSlot]?.widget
|
||||
if (!targetWidget) return
|
||||
|
||||
const offset = inputs.length - (targetNode.widgets_values?.length ?? 0)
|
||||
const v = targetNode.widgets_values?.[targetNodeSlot - offset]
|
||||
if (v == null) return
|
||||
|
||||
const widgetName = Object.values(map)[0]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const widget = this.node.widgets.find((w) => w.name === widgetName)
|
||||
if (widget) {
|
||||
widget.value = v
|
||||
}
|
||||
}
|
||||
|
||||
populateWidgets() {
|
||||
if (!this.node.widgets) return
|
||||
|
||||
@@ -1326,6 +1448,9 @@ export class GroupNodeHandler {
|
||||
const widgets = Object.keys(map)
|
||||
|
||||
if (!node.widgets_values?.length) {
|
||||
// special handling for populating values into reroutes
|
||||
// this allows primitives connect to them to pick up the correct value
|
||||
this.populateReroute(node, nodeId, map)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import './load3d'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
import './noteNode'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
import './saveMesh'
|
||||
import './simpleTouchSupport'
|
||||
|
||||
@@ -2382,7 +2382,6 @@ class BrushTool {
|
||||
const isErasing = maskCtx.globalCompositeOperation === 'destination-out'
|
||||
|
||||
if (hardness === 1) {
|
||||
console.log(sliderOpacity, opacity)
|
||||
gradient.addColorStop(
|
||||
0,
|
||||
isErasing
|
||||
@@ -4210,6 +4209,7 @@ class PanAndZoomManager {
|
||||
imageRootHeight: number = 0
|
||||
|
||||
cursorPoint: Point = { x: 0, y: 0 }
|
||||
penPointerIdList: number[] = []
|
||||
|
||||
constructor(maskEditor: MaskEditorDialog) {
|
||||
this.maskEditor = maskEditor
|
||||
@@ -4243,6 +4243,18 @@ class PanAndZoomManager {
|
||||
this.updateCursorPosition(point)
|
||||
})
|
||||
|
||||
this.messageBroker.subscribe('pointerDown', async (event: PointerEvent) => {
|
||||
if (event.pointerType === 'pen')
|
||||
this.penPointerIdList.push(event.pointerId)
|
||||
})
|
||||
|
||||
this.messageBroker.subscribe('pointerUp', async (event: PointerEvent) => {
|
||||
if (event.pointerType === 'pen') {
|
||||
const index = this.penPointerIdList.indexOf(event.pointerId)
|
||||
if (index > -1) this.penPointerIdList.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
this.messageBroker.subscribe(
|
||||
'handleTouchStart',
|
||||
async (event: TouchEvent) => {
|
||||
@@ -4281,7 +4293,10 @@ class PanAndZoomManager {
|
||||
|
||||
handleTouchStart(event: TouchEvent) {
|
||||
event.preventDefault()
|
||||
if ((event.touches[0] as any).touchType === 'stylus') return
|
||||
|
||||
// for pen device, if drawing with pen, do not move the canvas
|
||||
if (this.penPointerIdList.length > 0) return
|
||||
|
||||
this.messageBroker.publish('setBrushVisibility', false)
|
||||
if (event.touches.length === 2) {
|
||||
const currentTime = new Date().getTime()
|
||||
@@ -4310,7 +4325,9 @@ class PanAndZoomManager {
|
||||
|
||||
async handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault()
|
||||
if ((event.touches[0] as any).touchType === 'stylus') return
|
||||
|
||||
// for pen device, if drawing with pen, do not move the canvas
|
||||
if (this.penPointerIdList.length > 0) return
|
||||
|
||||
this.lastTwoFingerTap = 0
|
||||
if (this.isTouchZooming && event.touches.length === 2) {
|
||||
@@ -4361,23 +4378,17 @@ class PanAndZoomManager {
|
||||
|
||||
handleTouchEnd(event: TouchEvent) {
|
||||
event.preventDefault()
|
||||
if (
|
||||
event.touches.length === 0 &&
|
||||
(event.touches[0] as any).touchType === 'stylus'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isTouchZooming = false
|
||||
this.lastTouchMidPoint = { x: 0, y: 0 }
|
||||
|
||||
if (event.touches.length === 0) {
|
||||
this.lastTouchPoint = { x: 0, y: 0 }
|
||||
} else if (event.touches.length === 1) {
|
||||
const lastTouch = event.touches[0]
|
||||
// if all touches are removed, lastTouch will be null
|
||||
if (lastTouch) {
|
||||
this.lastTouchPoint = {
|
||||
x: event.touches[0].clientX,
|
||||
y: event.touches[0].clientY
|
||||
x: lastTouch.clientX,
|
||||
y: lastTouch.clientY
|
||||
}
|
||||
} else {
|
||||
this.isTouchZooming = false
|
||||
this.lastTouchMidPoint = { x: 0, y: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4586,6 +4597,8 @@ class PanAndZoomManager {
|
||||
this.zoom_ratio = Math.min(zoomRatioWidth, zoomRatioHeight)
|
||||
this.pan_offset = pan_offset
|
||||
|
||||
this.penPointerIdList = []
|
||||
|
||||
await this.invalidatePanZoom()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ app.registerExtension({
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
isVirtualNode: boolean
|
||||
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = { text: '' }
|
||||
@@ -58,8 +57,7 @@ app.registerExtension({
|
||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = { text: '' }
|
||||
|
||||
319
src/extensions/core/rerouteNode.ts
Normal file
319
src/extensions/core/rerouteNode.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import type { IContextMenuValue } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
|
||||
// Node that allows you to redirect connections for cleaner graphs
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.RerouteNode',
|
||||
registerCustomNodes(app) {
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string
|
||||
}
|
||||
|
||||
class RerouteNode extends LGraphNode {
|
||||
static category: string | undefined
|
||||
static defaultVisibility = false
|
||||
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = {}
|
||||
}
|
||||
this.properties.showOutputText = RerouteNode.defaultVisibility
|
||||
this.properties.horizontal = false
|
||||
|
||||
this.addInput('', '*')
|
||||
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
|
||||
this.onAfterGraphConfigured = function () {
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.onConnectionsChange(LiteGraph.INPUT, null, true, null)
|
||||
})
|
||||
}
|
||||
|
||||
this.onConnectionsChange = (type, _index, connected) => {
|
||||
if (app.configuringGraph) return
|
||||
|
||||
// Prevent multiple connections to different types when we have no input
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
// Ignore wildcard nodes as these will be updated to real types
|
||||
const types = new Set(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.outputs[0].links
|
||||
.map((l) => app.graph.links[l].type)
|
||||
.filter((t) => t !== '*')
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = []
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const linkId = this.outputs[0].links[i]
|
||||
const link = app.graph.links[linkId]
|
||||
linksToDisconnect.push(link)
|
||||
}
|
||||
for (const link of linksToDisconnect) {
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find root input
|
||||
let currentNode: LGraphNode | null = this
|
||||
let updateNodes = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = app.graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = app.graph.getNodeById(link.origin_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = node.constructor.type
|
||||
if (type === 'Reroute') {
|
||||
if (node === this) {
|
||||
// We've found a circle
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
// Move the previous node
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
// We've found the end
|
||||
inputNode = currentNode
|
||||
// @ts-expect-error fixme ts strict error
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// This path has no input node
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all outputs
|
||||
const nodes: LGraphNode[] = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
currentNode = nodes.pop()
|
||||
const outputs =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(currentNode.outputs ? currentNode.outputs[0].links : []) || []
|
||||
if (outputs.length) {
|
||||
for (const linkId of outputs) {
|
||||
const link = app.graph.links[linkId]
|
||||
|
||||
// When disconnecting sometimes the link is still registered
|
||||
if (!link) continue
|
||||
|
||||
const node = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = node.constructor.type
|
||||
|
||||
if (type === 'Reroute') {
|
||||
// Follow reroute nodes
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
// We've found an output
|
||||
const nodeOutType =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[link?.target_slot] &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[link.target_slot].type
|
||||
? // @ts-expect-error fixme ts strict error
|
||||
node.inputs[link.target_slot].type
|
||||
: null
|
||||
if (
|
||||
inputType &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
!LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
) {
|
||||
// The output doesnt match our input so disconnect it
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.disconnectInput(link.target_slot)
|
||||
} else {
|
||||
outputType = nodeOutType
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No more outputs for this path
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig
|
||||
let widgetType
|
||||
// Update the types of each node
|
||||
for (const node of updateNodes) {
|
||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||
// This lets you change the output link to a different type and all nodes will update
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.outputs[0].type = inputType || '*'
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.__outputType = displayType
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.outputs[0].name = node.properties.showOutputText
|
||||
? displayType
|
||||
: ''
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.setSize(node.computeSize())
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = app.graph.links[l]
|
||||
if (link) {
|
||||
link.color = color
|
||||
|
||||
if (app.configuringGraph) continue
|
||||
const targetNode = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
|
||||
const merged = mergeIfValid(targetInput, [
|
||||
config[0],
|
||||
widgetConfig
|
||||
])
|
||||
if (merged.customConfig) {
|
||||
widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
// @ts-expect-error fixme ts strict error
|
||||
setWidgetConfig(node.inputs[0], [
|
||||
widgetType ?? displayType,
|
||||
widgetConfig
|
||||
])
|
||||
} else {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
setWidgetConfig(node.inputs[0], null)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const link = app.graph.links[inputNode.inputs[0].link]
|
||||
if (link) {
|
||||
link.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clone = function () {
|
||||
const cloned = RerouteNode.prototype.clone.apply(this)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.removeOutput(0)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
cloned.setSize(cloned.computeSize())
|
||||
return cloned
|
||||
}
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getExtraMenuOptions(_, options): IContextMenuValue[] {
|
||||
options.unshift(
|
||||
{
|
||||
content:
|
||||
(this.properties.showOutputText ? 'Hide' : 'Show') + ' Type',
|
||||
callback: () => {
|
||||
this.properties.showOutputText = !this.properties.showOutputText
|
||||
if (this.properties.showOutputText) {
|
||||
this.outputs[0].name =
|
||||
this.__outputType || (this.outputs[0].type as string)
|
||||
} else {
|
||||
this.outputs[0].name = ''
|
||||
}
|
||||
this.setSize(this.computeSize())
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
content:
|
||||
(RerouteNode.defaultVisibility ? 'Hide' : 'Show') +
|
||||
' Type By Default',
|
||||
callback: () => {
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!RerouteNode.defaultVisibility
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
return []
|
||||
}
|
||||
computeSize(): [number, number] {
|
||||
return [
|
||||
this.properties.showOutputText && this.outputs && this.outputs.length
|
||||
? Math.max(
|
||||
75,
|
||||
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 +
|
||||
40
|
||||
)
|
||||
: 75,
|
||||
26
|
||||
]
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
static setDefaultTextVisibility(visible) {
|
||||
RerouteNode.defaultVisibility = visible
|
||||
if (visible) {
|
||||
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
||||
} else {
|
||||
delete localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load default visibility
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!!localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
)
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
'Reroute',
|
||||
Object.assign(RerouteNode, {
|
||||
title_mode: LiteGraph.NO_TITLE,
|
||||
title: 'Reroute',
|
||||
collapsable: false
|
||||
})
|
||||
)
|
||||
|
||||
RerouteNode.category = 'utils'
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphNode, LiteGraph, RenderShape } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IFoundSlot,
|
||||
INodeInputSlot,
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { CanvasMouseEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyWidgets, addValueControlWidgets } from '@/scripts/widgets'
|
||||
@@ -34,13 +35,10 @@ const GET_CONFIG = Symbol()
|
||||
|
||||
const replacePropertyName = 'Run widget replace on values'
|
||||
export class PrimitiveNode extends LGraphNode {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
controlValues: any[]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
lastType: string
|
||||
controlValues?: any[]
|
||||
lastType?: string
|
||||
static category: string
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
this.addOutput('connect to widget input', '*')
|
||||
this.serialize_widgets = true
|
||||
@@ -95,6 +93,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
refreshComboInNode() {
|
||||
const widget = this.widgets?.[0]
|
||||
if (widget?.type === 'combo') {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -150,7 +149,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onConnectOutput(
|
||||
slot: number,
|
||||
_type: string,
|
||||
@@ -160,8 +158,8 @@ export class PrimitiveNode extends LGraphNode {
|
||||
) {
|
||||
// Fires before the link is made allowing us to reject it if it isn't valid
|
||||
// No widget, we cant connect
|
||||
if (!input.widget) {
|
||||
if (!(input.type in ComfyWidgets)) return false
|
||||
if (!input.widget && !(input.type in ComfyWidgets)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.outputs[slot].links?.length) {
|
||||
@@ -172,6 +170,8 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
#onFirstConnection(recreating?: boolean) {
|
||||
@@ -211,7 +211,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
this.outputs[0].widget = widget
|
||||
|
||||
this.#createWidget(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget[CONFIG] ?? config,
|
||||
theirNode,
|
||||
widget.name,
|
||||
@@ -236,7 +235,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const [oldWidth, oldHeight] = this.size
|
||||
let widget: IWidget | undefined
|
||||
if (type in ComfyWidgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
|
||||
} else {
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
@@ -259,7 +257,6 @@ export class PrimitiveNode extends LGraphNode {
|
||||
control_value = 'fixed'
|
||||
}
|
||||
addValueControlWidgets(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this,
|
||||
widget,
|
||||
control_value as string,
|
||||
@@ -267,9 +264,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
inputData
|
||||
)
|
||||
let filter = this.widgets_values?.[2]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (filter && this.widgets.length === 3) {
|
||||
// @ts-expect-error change widget type from string to unknown
|
||||
if (filter && this.widgets && this.widgets.length === 3) {
|
||||
this.widgets[2].value = filter
|
||||
}
|
||||
}
|
||||
@@ -277,27 +272,20 @@ export class PrimitiveNode extends LGraphNode {
|
||||
// Restore any saved control values
|
||||
const controlValues = this.controlValues
|
||||
if (
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.lastType === this.widgets[0].type &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.widgets &&
|
||||
this.lastType === this.widgets[0]?.type &&
|
||||
controlValues?.length === this.widgets.length - 1
|
||||
) {
|
||||
for (let i = 0; i < controlValues.length; i++) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.widgets[i + 1].value = controlValues[i]
|
||||
}
|
||||
}
|
||||
|
||||
// When our value changes, update other widgets to reflect our changes
|
||||
// e.g. so LoadImage shows correct image
|
||||
const callback = widget.callback
|
||||
const self = this
|
||||
widget.callback = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const r = callback ? callback.apply(this, arguments) : undefined
|
||||
self.applyToGraph()
|
||||
return r
|
||||
}
|
||||
widget.callback = useChainCallback(widget.callback, () => {
|
||||
this.applyToGraph()
|
||||
})
|
||||
|
||||
// Use the biggest dimensions in case the widgets caused the node to grow
|
||||
this.setSize([
|
||||
@@ -316,10 +304,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.onResize) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.onResize(this.size)
|
||||
}
|
||||
this.onResize?.(this.size)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -328,10 +313,8 @@ export class PrimitiveNode extends LGraphNode {
|
||||
const values = this.widgets?.map((w) => w.value)
|
||||
this.#removeWidgets()
|
||||
this.#onFirstConnection(true)
|
||||
if (values?.length) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (let i = 0; i < this.widgets?.length; i++)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (values?.length && this.widgets) {
|
||||
for (let i = 0; i < this.widgets.length; i++)
|
||||
this.widgets[i].value = values[i]
|
||||
}
|
||||
return this.widgets?.[0]
|
||||
@@ -340,35 +323,32 @@ export class PrimitiveNode extends LGraphNode {
|
||||
#mergeWidgetConfig() {
|
||||
// Merge widget configs if the node has multiple outputs
|
||||
const output = this.outputs[0]
|
||||
const links = output.links
|
||||
const links = output.links ?? []
|
||||
|
||||
const hasConfig = !!output.widget[CONFIG]
|
||||
const hasConfig = !!output.widget?.[CONFIG]
|
||||
if (hasConfig) {
|
||||
delete output.widget[CONFIG]
|
||||
delete output.widget?.[CONFIG]
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (links?.length < 2 && hasConfig) {
|
||||
// Copy the widget options from the source
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (links.length) {
|
||||
this.recreateWidget()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const config1 = output.widget[GET_CONFIG]()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const config1 = output.widget?.[GET_CONFIG]?.()
|
||||
const isNumber = config1[0] === 'INT' || config1[0] === 'FLOAT'
|
||||
if (!isNumber) return
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (const linkId of links) {
|
||||
const link = app.graph.links[linkId]
|
||||
if (!link) continue // Can be null when removing a node
|
||||
|
||||
const theirNode = app.graph.getNodeById(link.target_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (!theirNode) continue
|
||||
const theirInput = theirNode.inputs[link.target_slot]
|
||||
|
||||
// Call is valid connection so it can merge the configs when validating
|
||||
@@ -388,6 +368,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
if (!isConvertibleWidget(targetWidget, config2)) return false
|
||||
|
||||
const output = this.outputs[originSlot]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (!(output.widget?.[CONFIG] ?? output.widget?.[GET_CONFIG]())) {
|
||||
// No widget defined for this primitive yet so allow it
|
||||
return true
|
||||
@@ -427,9 +408,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
this.controlValues.push(this.widgets[i].value)
|
||||
}
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.lastType
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete this.controlValues
|
||||
}, 15)
|
||||
this.widgets.length = 0
|
||||
@@ -448,11 +427,11 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
export function getWidgetConfig(slot: INodeInputSlot | INodeOutputSlot) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG]?.() ?? ['*', {}]
|
||||
}
|
||||
|
||||
function getConfig(widgetName: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function getConfig(this: LGraphNode, widgetName: string) {
|
||||
const { nodeData } = this.constructor
|
||||
return (
|
||||
nodeData?.input?.required?.[widgetName] ??
|
||||
@@ -541,15 +520,12 @@ export function convertToInput(
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
const inputIsOptional = !!widget.options?.inputIsOptional
|
||||
const input = node.addInput(widget.name, type, {
|
||||
// @ts-expect-error [GET_CONFIG] is not a valid property of IWidget
|
||||
widget: { name: widget.name, [GET_CONFIG]: () => config },
|
||||
...(inputIsOptional ? { shape: LiteGraph.SlotShape.HollowCircle } : {})
|
||||
...(inputIsOptional ? { shape: RenderShape.HollowCircle } : {})
|
||||
})
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (const widget of node.widgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.last_y = (widget.last_y ?? 0) + LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
@@ -565,10 +541,8 @@ function convertToWidget(node: LGraphNode, widget: IWidget) {
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name))
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
for (const widget of node.widgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.last_y = (widget.last_y ?? 0) - LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
// Restore original size but grow if needed
|
||||
@@ -599,12 +573,10 @@ export function setWidgetConfig(
|
||||
}
|
||||
|
||||
if ('link' in slot) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const link = app.graph.links[slot.link]
|
||||
const link = app.graph.links[slot.link ?? -1]
|
||||
if (link) {
|
||||
const originNode = app.graph.getNodeById(link.origin_id)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (isPrimitiveNode(originNode)) {
|
||||
if (originNode && isPrimitiveNode(originNode)) {
|
||||
if (config) {
|
||||
originNode.recreateWidget()
|
||||
} else if (!app.configuringGraph) {
|
||||
@@ -632,6 +604,7 @@ export function mergeIfValid(
|
||||
|
||||
if (customSpec || forceUpdate) {
|
||||
if (customSpec) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
output.widget[CONFIG] = customSpec
|
||||
}
|
||||
|
||||
@@ -718,8 +691,9 @@ app.registerExtension({
|
||||
) {
|
||||
if (!slot.input || !slot.input.widget) return []
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const widget = this.widgets.find((w) => w.name === slot.input.widget.name)
|
||||
const widget = this.widgets?.find(
|
||||
(w) => w.name === slot.input?.widget?.name
|
||||
)
|
||||
if (!widget) return []
|
||||
return [
|
||||
{
|
||||
@@ -746,8 +720,7 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const { canvasX, canvasY } = getPointerCanvasPos()
|
||||
const { canvasX = 0, canvasY = 0 } = getPointerCanvasPos() ?? {}
|
||||
const widget = this.getWidgetOnPos(canvasX, canvasY)
|
||||
// @ts-expect-error custom widget type
|
||||
if (widget && widget.type !== CONVERTED_TYPE) {
|
||||
@@ -821,95 +794,66 @@ app.registerExtension({
|
||||
return r
|
||||
}
|
||||
|
||||
nodeType.prototype.onGraphConfigured = function (this: LGraphNode) {
|
||||
if (!this.inputs) return
|
||||
this.widgets ??= []
|
||||
nodeType.prototype.onGraphConfigured = useChainCallback(
|
||||
nodeType.prototype.onGraphConfigured,
|
||||
function (this: LGraphNode) {
|
||||
if (!this.inputs) return
|
||||
this.widgets ??= []
|
||||
|
||||
for (const input of this.inputs) {
|
||||
if (input.widget) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (!input.widget[GET_CONFIG]) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
input.widget[GET_CONFIG] = () =>
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getConfig.call(this, input.widget.name)
|
||||
}
|
||||
|
||||
// Cleanup old widget config
|
||||
// @ts-expect-error WidgetRef
|
||||
if (input.widget.config) {
|
||||
// @ts-expect-error WidgetRef
|
||||
if (input.widget.config[0] instanceof Array) {
|
||||
// If we are an old converted combo then replace the input type and the stored link data
|
||||
input.type = 'COMBO'
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const link = app.graph.links[input.link]
|
||||
if (link) {
|
||||
link.type = input.type
|
||||
}
|
||||
}
|
||||
// @ts-expect-error WidgetRef
|
||||
delete input.widget.config
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const w = this.widgets.find((w) => w.name === input.widget.name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
} else {
|
||||
this.removeInput(this.inputs.findIndex((i) => i === input))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function (this: LGraphNode) {
|
||||
const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined
|
||||
|
||||
// When node is created, convert any force/default inputs
|
||||
if (!app.configuringGraph && this.widgets) {
|
||||
for (const w of this.widgets) {
|
||||
if (w?.options?.forceInput || w?.options?.defaultInput) {
|
||||
const config = getConfig.call(this, w.name) ?? [
|
||||
w.type,
|
||||
w.options || {}
|
||||
]
|
||||
convertToInput(this, w, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
const origOnConfigure = nodeType.prototype.onConfigure
|
||||
nodeType.prototype.onConfigure = function (this: LGraphNode) {
|
||||
const r = origOnConfigure
|
||||
? // @ts-expect-error fixme ts strict error
|
||||
origOnConfigure.apply(this, arguments)
|
||||
: undefined
|
||||
if (!app.configuringGraph && this.inputs) {
|
||||
// On copy + paste of nodes, ensure that widget configs are set up
|
||||
for (const input of this.inputs) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (input.widget && !input.widget[GET_CONFIG]) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
input.widget[GET_CONFIG] = () =>
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getConfig.call(this, input.widget.name)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const w = this.widgets.find((w) => w.name === input.widget.name)
|
||||
if (input.widget) {
|
||||
const name = input.widget.name
|
||||
if (!input.widget[GET_CONFIG]) {
|
||||
input.widget[GET_CONFIG] = () => getConfig.call(this, name)
|
||||
}
|
||||
|
||||
const w = this.widgets?.find((w) => w.name === name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
} else {
|
||||
this.removeInput(this.inputs.findIndex((i) => i === input))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return r
|
||||
}
|
||||
nodeType.prototype.onNodeCreated = useChainCallback(
|
||||
nodeType.prototype.onNodeCreated,
|
||||
function (this: LGraphNode) {
|
||||
// When node is created, convert any force/default inputs
|
||||
if (!app.configuringGraph && this.widgets) {
|
||||
for (const w of this.widgets) {
|
||||
if (w?.options?.forceInput || w?.options?.defaultInput) {
|
||||
const config = getConfig.call(this, w.name) ?? [
|
||||
w.type,
|
||||
w.options || {}
|
||||
]
|
||||
convertToInput(this, w, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
nodeType.prototype.onConfigure = useChainCallback(
|
||||
nodeType.prototype.onConfigure,
|
||||
function (this: LGraphNode) {
|
||||
if (!app.configuringGraph && this.inputs) {
|
||||
// On copy + paste of nodes, ensure that widget configs are set up
|
||||
for (const input of this.inputs) {
|
||||
if (input.widget && !input.widget[GET_CONFIG]) {
|
||||
const name = input.widget.name
|
||||
input.widget[GET_CONFIG] = () => getConfig.call(this, name)
|
||||
const w = this.widgets?.find((w) => w.name === name)
|
||||
if (w) {
|
||||
hideWidget(this, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function isNodeAtPos(pos: Vector2) {
|
||||
for (const n of app.graph.nodes) {
|
||||
@@ -945,12 +889,12 @@ app.registerExtension({
|
||||
|
||||
// Create a primitive node
|
||||
const node = LiteGraph.createNode('PrimitiveNode')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (!node) return r
|
||||
|
||||
app.graph.add(node)
|
||||
|
||||
// Calculate a position that wont directly overlap another node
|
||||
const pos: [number, number] = [
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.pos[0] - node.size[0] - 30,
|
||||
this.pos[1]
|
||||
]
|
||||
@@ -958,11 +902,8 @@ app.registerExtension({
|
||||
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.pos = pos
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.connect(0, this, slot)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node.title = input.name
|
||||
|
||||
return r
|
||||
@@ -971,7 +912,6 @@ app.registerExtension({
|
||||
registerCustomNodes() {
|
||||
LiteGraph.registerNodeType(
|
||||
'PrimitiveNode',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
Object.assign(PrimitiveNode, {
|
||||
title: 'Primitive'
|
||||
})
|
||||
|
||||
@@ -101,10 +101,17 @@
|
||||
"missing": "Missing",
|
||||
"inProgress": "In progress",
|
||||
"completed": "Completed",
|
||||
"interrupted": "Interrupted"
|
||||
"interrupted": "Interrupted",
|
||||
"enabling": "Enabling",
|
||||
"disabling": "Disabling",
|
||||
"updating": "Updating",
|
||||
"migrate": "Migrate"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"failed": "Failed ({count})",
|
||||
"installationQueue": "Installation Queue",
|
||||
"changingVersion": "Changing version from {from} to {to}",
|
||||
"dependencies": "Dependencies",
|
||||
"inWorkflow": "In Workflow",
|
||||
"infoPanelEmpty": "Click an item to see the info",
|
||||
@@ -117,6 +124,7 @@
|
||||
"uninstalling": "Uninstalling",
|
||||
"update": "Update",
|
||||
"uninstallSelected": "Uninstall Selected",
|
||||
"updatingAllPacks": "Updating all packages",
|
||||
"license": "License",
|
||||
"nightlyVersion": "Nightly",
|
||||
"latestVersion": "Latest",
|
||||
@@ -705,7 +713,8 @@
|
||||
"EditTokenWeight": "Edit Token Weight",
|
||||
"CustomColorPalettes": "Custom Color Palettes",
|
||||
"UV": "UV",
|
||||
"ContextMenu": "Context Menu"
|
||||
"ContextMenu": "Context Menu",
|
||||
"Reroute": "Reroute"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -958,7 +967,8 @@
|
||||
"defaultTitle": "An error occurred",
|
||||
"loadWorkflowTitle": "Loading aborted due to error reloading workflow data",
|
||||
"noStackTrace": "No stacktrace available",
|
||||
"extensionFileHint": "This may be due to the following script"
|
||||
"extensionFileHint": "This may be due to the following script",
|
||||
"promptExecutionError": "Prompt execution failed"
|
||||
},
|
||||
"desktopUpdate": {
|
||||
"title": "Updating ComfyUI Desktop",
|
||||
@@ -1015,6 +1025,8 @@
|
||||
"nodeDefinitionsUpdated": "Node definitions updated",
|
||||
"errorSaveSetting": "Error saving setting {id}: {err}",
|
||||
"errorCopyImage": "Error copying image: {error}",
|
||||
"noTemplatesToExport": "No templates to export"
|
||||
"noTemplatesToExport": "No templates to export",
|
||||
"failedToFetchLogs": "Failed to fetch server logs",
|
||||
"migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute."
|
||||
}
|
||||
}
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Tooltip Delay"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Reroute spline offset",
|
||||
"tooltip": "The bezier control point offset from the reroute centre point"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Always snap to grid"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "Ocurrió un error",
|
||||
"extensionFileHint": "Esto puede deberse al siguiente script",
|
||||
"loadWorkflowTitle": "La carga se interrumpió debido a un error al recargar los datos del flujo de trabajo",
|
||||
"noStackTrace": "No hay seguimiento de pila disponible"
|
||||
"noStackTrace": "No hay seguimiento de pila disponible",
|
||||
"promptExecutionError": "La ejecución del prompt falló"
|
||||
},
|
||||
"g": {
|
||||
"about": "Acerca de",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "Descripción",
|
||||
"devices": "Dispositivos",
|
||||
"disableAll": "Deshabilitar todo",
|
||||
"disabling": "Deshabilitando",
|
||||
"download": "Descargar",
|
||||
"empty": "Vacío",
|
||||
"enableAll": "Habilitar todo",
|
||||
"enabled": "Habilitado",
|
||||
"enabling": "Habilitando",
|
||||
"error": "Error",
|
||||
"experimental": "BETA",
|
||||
"export": "Exportar",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "Cargar flujo de trabajo",
|
||||
"loading": "Cargando",
|
||||
"logs": "Registros",
|
||||
"migrate": "Migrar",
|
||||
"missing": "Faltante",
|
||||
"name": "Nombre",
|
||||
"newFolder": "Nueva carpeta",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "Terminal",
|
||||
"update": "Actualizar",
|
||||
"updated": "Actualizado",
|
||||
"updating": "Actualizando",
|
||||
"upload": "Subir",
|
||||
"videoFailedToLoad": "Falló la carga del video",
|
||||
"workflow": "Flujo de trabajo"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "Mantenimiento"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "Cambiando versión de {from} a {to}",
|
||||
"createdBy": "Creado Por",
|
||||
"dependencies": "Dependencias",
|
||||
"discoverCommunityContent": "Descubre paquetes de nodos, extensiones y más creados por la comunidad...",
|
||||
"downloads": "Descargas",
|
||||
"errorConnecting": "Error al conectar con el Registro de Nodos Comfy.",
|
||||
"failed": "Falló ({count})",
|
||||
"filter": {
|
||||
"disabled": "Deshabilitado",
|
||||
"enabled": "Habilitado",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "En Flujo de Trabajo",
|
||||
"infoPanelEmpty": "Haz clic en un elemento para ver la información",
|
||||
"installSelected": "Instalar Seleccionado",
|
||||
"installationQueue": "Cola de Instalación",
|
||||
"lastUpdated": "Última Actualización",
|
||||
"latestVersion": "Última",
|
||||
"license": "Licencia",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "Desinstalar Seleccionado",
|
||||
"uninstalling": "Desinstalando",
|
||||
"update": "Actualizar",
|
||||
"updatingAllPacks": "Actualizando todos los paquetes",
|
||||
"version": "Versión"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "Puntero",
|
||||
"Queue": "Cola",
|
||||
"QueueButton": "Botón de Cola",
|
||||
"Reroute": "Reenrutar",
|
||||
"RerouteBeta": "Reroute Beta",
|
||||
"Server": "Servidor",
|
||||
"Server-Config": "Configuración del Servidor",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "Error al aplicar textura",
|
||||
"failedToDownloadFile": "Error al descargar el archivo",
|
||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||
"failedToFetchLogs": "Error al obtener los registros del servidor",
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
"fileUploadFailed": "Error al subir el archivo",
|
||||
"interrupted": "La ejecución ha sido interrumpida",
|
||||
"migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.",
|
||||
"no3dScene": "No hay escena 3D para aplicar textura",
|
||||
"no3dSceneToExport": "No hay escena 3D para exportar",
|
||||
"noTemplatesToExport": "No hay plantillas para exportar",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Retraso de la información sobre herramientas"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Desvío de la compensación de la spline",
|
||||
"tooltip": "El punto de control bezier desplazado desde el punto central de reenrutamiento"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Siempre ajustar a la cuadrícula"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "Une erreur est survenue",
|
||||
"extensionFileHint": "Cela peut être dû au script suivant",
|
||||
"loadWorkflowTitle": "Chargement interrompu en raison d'une erreur de rechargement des données de workflow",
|
||||
"noStackTrace": "Aucune trace de pile disponible"
|
||||
"noStackTrace": "Aucune trace de pile disponible",
|
||||
"promptExecutionError": "L'exécution de l'invite a échoué"
|
||||
},
|
||||
"g": {
|
||||
"about": "À propos",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "Description",
|
||||
"devices": "Appareils",
|
||||
"disableAll": "Désactiver tout",
|
||||
"disabling": "Désactivation",
|
||||
"download": "Télécharger",
|
||||
"empty": "Vide",
|
||||
"enableAll": "Activer tout",
|
||||
"enabled": "Activé",
|
||||
"enabling": "Activation",
|
||||
"error": "Erreur",
|
||||
"experimental": "BETA",
|
||||
"export": "Exportation",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "Charger le flux de travail",
|
||||
"loading": "Chargement",
|
||||
"logs": "Journaux",
|
||||
"migrate": "Migrer",
|
||||
"missing": "Manquant",
|
||||
"name": "Nom",
|
||||
"newFolder": "Nouveau dossier",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "Terminal",
|
||||
"update": "Mettre à jour",
|
||||
"updated": "Mis à jour",
|
||||
"updating": "Mise à jour",
|
||||
"upload": "Téléverser",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "Maintenance"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "Changement de version de {from} à {to}",
|
||||
"createdBy": "Créé par",
|
||||
"dependencies": "Dépendances",
|
||||
"discoverCommunityContent": "Découvrez les packs de nœuds, extensions et plus encore créés par la communauté...",
|
||||
"downloads": "Téléchargements",
|
||||
"errorConnecting": "Erreur de connexion au registre de nœuds Comfy.",
|
||||
"failed": "Échoué ({count})",
|
||||
"filter": {
|
||||
"disabled": "Désactivé",
|
||||
"enabled": "Activé",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "Dans le flux de travail",
|
||||
"infoPanelEmpty": "Cliquez sur un élément pour voir les informations",
|
||||
"installSelected": "Installer sélectionné",
|
||||
"installationQueue": "File d'attente d'installation",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"latestVersion": "Dernière",
|
||||
"license": "Licence",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "Désinstaller sélectionné",
|
||||
"uninstalling": "Désinstallation",
|
||||
"update": "Mettre à jour",
|
||||
"updatingAllPacks": "Mise à jour de tous les paquets",
|
||||
"version": "Version"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "Pointeur",
|
||||
"Queue": "File d'Attente",
|
||||
"QueueButton": "Bouton de File d'Attente",
|
||||
"Reroute": "Réacheminement",
|
||||
"RerouteBeta": "Reroute Beta",
|
||||
"Server": "Serveur",
|
||||
"Server-Config": "Config-Serveur",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "Échec de l'application de la texture",
|
||||
"failedToDownloadFile": "Échec du téléchargement du fichier",
|
||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
"fileUploadFailed": "Échec du téléchargement du fichier",
|
||||
"interrupted": "L'exécution a été interrompue",
|
||||
"migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.",
|
||||
"no3dScene": "Aucune scène 3D pour appliquer la texture",
|
||||
"no3dSceneToExport": "Aucune scène 3D à exporter",
|
||||
"noTemplatesToExport": "Aucun modèle à exporter",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Délai d'infobulle"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Réacheminement décalage de spline",
|
||||
"tooltip": "Le point de contrôle de Bézier est décalé par rapport au point central de réacheminement"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Toujours aligner sur la grille"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "エラーが発生しました",
|
||||
"extensionFileHint": "これは次のスクリプトが原因かもしれません",
|
||||
"loadWorkflowTitle": "ワークフローデータの再読み込みエラーにより、読み込みが中止されました",
|
||||
"noStackTrace": "スタックトレースは利用できません"
|
||||
"noStackTrace": "スタックトレースは利用できません",
|
||||
"promptExecutionError": "プロンプトの実行に失敗しました"
|
||||
},
|
||||
"g": {
|
||||
"about": "情報",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "説明",
|
||||
"devices": "デバイス",
|
||||
"disableAll": "すべて無効にする",
|
||||
"disabling": "無効化",
|
||||
"download": "ダウンロード",
|
||||
"empty": "空",
|
||||
"enableAll": "すべて有効にする",
|
||||
"enabled": "有効",
|
||||
"enabling": "有効化",
|
||||
"error": "エラー",
|
||||
"experimental": "ベータ",
|
||||
"export": "エクスポート",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "ワークフローを読み込む",
|
||||
"loading": "読み込み中",
|
||||
"logs": "ログ",
|
||||
"migrate": "移行する",
|
||||
"missing": "不足している",
|
||||
"name": "名前",
|
||||
"newFolder": "新しいフォルダー",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "ターミナル",
|
||||
"update": "更新",
|
||||
"updated": "更新済み",
|
||||
"updating": "更新中",
|
||||
"upload": "アップロード",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "メンテナンス"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "バージョンを {from} から {to} に変更",
|
||||
"createdBy": "作成者",
|
||||
"dependencies": "依存関係",
|
||||
"discoverCommunityContent": "コミュニティが作成したノードパック、拡張機能などを探す...",
|
||||
"downloads": "ダウンロード",
|
||||
"errorConnecting": "Comfy Node Registryへの接続エラー。",
|
||||
"failed": "失敗しました ({count})",
|
||||
"filter": {
|
||||
"disabled": "無効",
|
||||
"enabled": "有効",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "ワークフロー内",
|
||||
"infoPanelEmpty": "アイテムをクリックして情報を表示します",
|
||||
"installSelected": "選択したものをインストール",
|
||||
"installationQueue": "インストールキュー",
|
||||
"lastUpdated": "最終更新日",
|
||||
"latestVersion": "最新",
|
||||
"license": "ライセンス",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "選択したものをアンインストール",
|
||||
"uninstalling": "アンインストール中",
|
||||
"update": "更新",
|
||||
"updatingAllPacks": "すべてのパッケージを更新中",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "ポインタ",
|
||||
"Queue": "キュー",
|
||||
"QueueButton": "キューボタン",
|
||||
"Reroute": "リルート",
|
||||
"RerouteBeta": "ルート変更ベータ",
|
||||
"Server": "サーバー",
|
||||
"Server-Config": "サーバー設定",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "テクスチャの適用に失敗しました",
|
||||
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
|
||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||
"failedToFetchLogs": "サーバーログの取得に失敗しました",
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
"fileUploadFailed": "ファイルのアップロードに失敗しました",
|
||||
"interrupted": "実行が中断されました",
|
||||
"migrateToLitegraphReroute": "将来のバージョンではRerouteノードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
|
||||
"no3dScene": "テクスチャを適用する3Dシーンがありません",
|
||||
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
|
||||
"noTemplatesToExport": "エクスポートするテンプレートがありません",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "ツールチップ遅延"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "リルートスプラインオフセット",
|
||||
"tooltip": "リルート中心点からのベジエ制御点のオフセット"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "常にグリッドにスナップ"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "오류가 발생했습니다",
|
||||
"extensionFileHint": "다음 스크립트 때문일 수 있습니다",
|
||||
"loadWorkflowTitle": "워크플로우 데이터를 다시 로드하는 중 오류로 인해 로드가 중단되었습니다",
|
||||
"noStackTrace": "스택 추적이 사용할 수 없습니다"
|
||||
"noStackTrace": "스택 추적이 사용할 수 없습니다",
|
||||
"promptExecutionError": "프롬프트 실행 실패"
|
||||
},
|
||||
"g": {
|
||||
"about": "정보",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "설명",
|
||||
"devices": "장치",
|
||||
"disableAll": "모두 비활성화",
|
||||
"disabling": "비활성화 중",
|
||||
"download": "다운로드",
|
||||
"empty": "비어 있음",
|
||||
"enableAll": "모두 활성화",
|
||||
"enabled": "활성화됨",
|
||||
"enabling": "활성화 중",
|
||||
"error": "오류",
|
||||
"experimental": "베타",
|
||||
"export": "내보내기",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "워크플로 로드",
|
||||
"loading": "로딩 중",
|
||||
"logs": "로그",
|
||||
"migrate": "마이그레이트",
|
||||
"missing": "누락됨",
|
||||
"name": "이름",
|
||||
"newFolder": "새 폴더",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "터미널",
|
||||
"update": "업데이트",
|
||||
"updated": "업데이트 됨",
|
||||
"updating": "업데이트 중",
|
||||
"upload": "업로드",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "유지 보수"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "{from}에서 {to}로 버전 변경 중",
|
||||
"createdBy": "작성자",
|
||||
"dependencies": "의존성",
|
||||
"discoverCommunityContent": "커뮤니티에서 만든 노드 팩 및 확장 프로그램을 찾아보세요...",
|
||||
"downloads": "다운로드",
|
||||
"errorConnecting": "Comfy Node Registry에 연결하는 중 오류가 발생했습니다.",
|
||||
"failed": "실패 ({count})",
|
||||
"filter": {
|
||||
"disabled": "비활성화",
|
||||
"enabled": "활성화",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "워크플로우 내",
|
||||
"infoPanelEmpty": "정보를 보려면 항목을 클릭하세요",
|
||||
"installSelected": "선택한 항목 설치",
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"license": "라이선스",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "선택 항목 제거",
|
||||
"uninstalling": "제거 중",
|
||||
"update": "업데이트",
|
||||
"updatingAllPacks": "모든 패키지 업데이트 중",
|
||||
"version": "버전"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "포인터",
|
||||
"Queue": "실행 큐",
|
||||
"QueueButton": "실행 큐 버튼",
|
||||
"Reroute": "재경로",
|
||||
"RerouteBeta": "경로재설정 베타",
|
||||
"Server": "서버",
|
||||
"Server-Config": "서버 구성",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "텍스처 적용에 실패했습니다",
|
||||
"failedToDownloadFile": "파일 다운로드에 실패했습니다",
|
||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
|
||||
"fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다",
|
||||
"fileUploadFailed": "파일 업로드에 실패했습니다",
|
||||
"interrupted": "실행이 중단되었습니다",
|
||||
"migrateToLitegraphReroute": "미래 버전에서는 Reroute 노드가 제거됩니다. litegraph-native reroute로 마이그레이트하려면 클릭하세요.",
|
||||
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
|
||||
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
|
||||
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "툴팁 지연"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Reroute 스플라인 오프셋",
|
||||
"tooltip": "재경로 중심점에서 베지어 제어점까지의 오프셋"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "항상 그리드에 스냅"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "Произошла ошибка",
|
||||
"extensionFileHint": "Это может быть связано со следующим скриптом",
|
||||
"loadWorkflowTitle": "Загрузка прервана из-за ошибки при перезагрузке данных рабочего процесса",
|
||||
"noStackTrace": "Стек вызовов недоступен"
|
||||
"noStackTrace": "Стек вызовов недоступен",
|
||||
"promptExecutionError": "Ошибка выполнения запроса"
|
||||
},
|
||||
"g": {
|
||||
"about": "О программе",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "Описание",
|
||||
"devices": "Устройства",
|
||||
"disableAll": "Отключить все",
|
||||
"disabling": "Отключение",
|
||||
"download": "Скачать",
|
||||
"empty": "Пусто",
|
||||
"enableAll": "Включить все",
|
||||
"enabled": "Включено",
|
||||
"enabling": "Включение",
|
||||
"error": "Ошибка",
|
||||
"experimental": "БЕТА",
|
||||
"export": "Экспорт",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
"loading": "Загрузка",
|
||||
"logs": "Логи",
|
||||
"migrate": "Мигрировать",
|
||||
"missing": "Отсутствует",
|
||||
"name": "Имя",
|
||||
"newFolder": "Новая папка",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "Терминал",
|
||||
"update": "Обновить",
|
||||
"updated": "Обновлено",
|
||||
"updating": "Обновление",
|
||||
"upload": "Загрузить",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "Обслуживание"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "Изменение версии с {from} на {to}",
|
||||
"createdBy": "Создано",
|
||||
"dependencies": "Зависимости",
|
||||
"discoverCommunityContent": "Откройте для себя пакеты узлов, расширения и многое другое, созданные сообществом...",
|
||||
"downloads": "Загрузки",
|
||||
"errorConnecting": "Ошибка подключения к реестру Comfy Node.",
|
||||
"failed": "Не удалось ({count})",
|
||||
"filter": {
|
||||
"disabled": "Отключено",
|
||||
"enabled": "Включено",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "В рабочем процессе",
|
||||
"infoPanelEmpty": "Нажмите на элемент, чтобы увидеть информацию",
|
||||
"installSelected": "Установить выбранное",
|
||||
"installationQueue": "Очередь установки",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"latestVersion": "Последняя",
|
||||
"license": "Лицензия",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "Удалить выбранное",
|
||||
"uninstalling": "Удаление",
|
||||
"update": "Обновить",
|
||||
"updatingAllPacks": "Обновление всех пакетов",
|
||||
"version": "Версия"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "Указатель",
|
||||
"Queue": "Очередь",
|
||||
"QueueButton": "Кнопка очереди",
|
||||
"Reroute": "Перенаправление",
|
||||
"RerouteBeta": "Бета-версия перенаправления",
|
||||
"Server": "Сервер",
|
||||
"Server-Config": "Настройки сервера",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "Не удалось применить текстуру",
|
||||
"failedToDownloadFile": "Не удалось скачать файл",
|
||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||
"failedToFetchLogs": "Не удалось получить серверные логи",
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
"fileUploadFailed": "Не удалось загрузить файл",
|
||||
"interrupted": "Выполнение было прервано",
|
||||
"migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.",
|
||||
"no3dScene": "Нет 3D сцены для применения текстуры",
|
||||
"no3dSceneToExport": "Нет 3D сцены для экспорта",
|
||||
"noTemplatesToExport": "Нет шаблонов для экспорта",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Задержка всплывающей подсказки"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "Перераспределение смещения сплайна",
|
||||
"tooltip": "Смещение контрольной точки Безье от центральной точки перераспределения"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Всегда привязываться к сетке"
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"defaultTitle": "发生错误",
|
||||
"extensionFileHint": "这可能是由于以下脚本",
|
||||
"loadWorkflowTitle": "由于重新加载工作流数据出错,加载被中止",
|
||||
"noStackTrace": "无可用堆栈跟踪"
|
||||
"noStackTrace": "无可用堆栈跟踪",
|
||||
"promptExecutionError": "提示执行失败"
|
||||
},
|
||||
"g": {
|
||||
"about": "关于",
|
||||
@@ -151,10 +152,12 @@
|
||||
"description": "描述",
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"disabling": "禁用中",
|
||||
"download": "下载",
|
||||
"empty": "空",
|
||||
"enableAll": "启用全部",
|
||||
"enabled": "已启用",
|
||||
"enabling": "启用中",
|
||||
"error": "错误",
|
||||
"experimental": "测试版",
|
||||
"export": "导出",
|
||||
@@ -178,6 +181,7 @@
|
||||
"loadWorkflow": "加载工作流",
|
||||
"loading": "加载中",
|
||||
"logs": "日志",
|
||||
"migrate": "迁移",
|
||||
"missing": "缺失",
|
||||
"name": "名称",
|
||||
"newFolder": "新文件夹",
|
||||
@@ -221,6 +225,7 @@
|
||||
"terminal": "终端",
|
||||
"update": "更新",
|
||||
"updated": "已更新",
|
||||
"updating": "更新中",
|
||||
"upload": "上传",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
@@ -399,11 +404,13 @@
|
||||
"title": "维护"
|
||||
},
|
||||
"manager": {
|
||||
"changingVersion": "将版本从 {from} 更改为 {to}",
|
||||
"createdBy": "创建者",
|
||||
"dependencies": "依赖关系",
|
||||
"discoverCommunityContent": "发现社区制作的节点包,扩展等等...",
|
||||
"downloads": "下载",
|
||||
"errorConnecting": "连接到Comfy节点注册表时出错。",
|
||||
"failed": "失败 ({count})",
|
||||
"filter": {
|
||||
"disabled": "已禁用",
|
||||
"enabled": "已启用",
|
||||
@@ -412,6 +419,7 @@
|
||||
"inWorkflow": "在工作流中",
|
||||
"infoPanelEmpty": "点击一个项目查看信息",
|
||||
"installSelected": "安装选定",
|
||||
"installationQueue": "安装队列",
|
||||
"lastUpdated": "最后更新",
|
||||
"latestVersion": "最新",
|
||||
"license": "许可证",
|
||||
@@ -445,6 +453,7 @@
|
||||
"uninstallSelected": "卸载所选",
|
||||
"uninstalling": "正在卸载",
|
||||
"update": "更新",
|
||||
"updatingAllPacks": "更新所有包",
|
||||
"version": "版本"
|
||||
},
|
||||
"maskEditor": {
|
||||
@@ -826,6 +835,7 @@
|
||||
"Pointer": "指针",
|
||||
"Queue": "队列",
|
||||
"QueueButton": "执行按钮",
|
||||
"Reroute": "重新路由",
|
||||
"RerouteBeta": "转接点 Beta",
|
||||
"Server": "服务器",
|
||||
"Server-Config": "服务器配置",
|
||||
@@ -988,9 +998,11 @@
|
||||
"failedToApplyTexture": "应用纹理失败",
|
||||
"failedToDownloadFile": "文件下载失败",
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
"fileUploadFailed": "文件上传失败",
|
||||
"interrupted": "执行已被中断",
|
||||
"migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。",
|
||||
"no3dScene": "没有3D场景可以应用纹理",
|
||||
"no3dSceneToExport": "没有3D场景可以导出",
|
||||
"noTemplatesToExport": "没有模板可以导出",
|
||||
|
||||
@@ -333,6 +333,10 @@
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "工具提示延迟"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "重新路由样条偏移",
|
||||
"tooltip": "贝塞尔控制点从重新路由中心点的偏移"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "始终吸附到网格"
|
||||
}
|
||||
|
||||
@@ -255,14 +255,31 @@ export function validateTaskItem(taskItem: unknown) {
|
||||
|
||||
const zEmbeddingsResponse = z.array(z.string())
|
||||
const zExtensionsResponse = z.array(z.string())
|
||||
const zError = z.object({
|
||||
type: z.string(),
|
||||
message: z.string(),
|
||||
details: z.string(),
|
||||
extra_info: z
|
||||
.object({
|
||||
input_name: z.string().optional()
|
||||
})
|
||||
.passthrough()
|
||||
.optional()
|
||||
})
|
||||
const zNodeError = z.object({
|
||||
errors: z.array(zError),
|
||||
class_type: z.string(),
|
||||
dependent_outputs: z.array(z.any())
|
||||
})
|
||||
const zPromptResponse = z.object({
|
||||
node_errors: z.array(z.string()).optional(),
|
||||
node_errors: z.record(zNodeId, zNodeError).optional(),
|
||||
prompt_id: z.string().optional(),
|
||||
exec_info: z
|
||||
.object({
|
||||
queue_remaining: z.number().optional()
|
||||
})
|
||||
.optional()
|
||||
.optional(),
|
||||
error: z.union([z.string(), zError])
|
||||
})
|
||||
|
||||
const zDeviceStats = z.object({
|
||||
@@ -414,6 +431,7 @@ const zSettings = z.record(z.any()).and(
|
||||
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>
|
||||
export type ExtensionsResponse = z.infer<typeof zExtensionsResponse>
|
||||
export type PromptResponse = z.infer<typeof zPromptResponse>
|
||||
export type NodeError = z.infer<typeof zNodeError>
|
||||
export type Settings = z.infer<typeof zSettings>
|
||||
export type DeviceStats = z.infer<typeof zDeviceStats>
|
||||
export type SystemStats = z.infer<typeof zSystemStats>
|
||||
|
||||
@@ -82,7 +82,12 @@ const zReroute = z
|
||||
id: z.number(),
|
||||
parentId: z.number().optional(),
|
||||
pos: zVector2,
|
||||
linkIds: z.array(z.number()).nullish()
|
||||
linkIds: z.array(z.number()).nullish(),
|
||||
floating: z
|
||||
.object({
|
||||
slotType: z.enum(['input', 'output'])
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
@@ -277,6 +282,7 @@ export type ModelFile = z.infer<typeof zModelFile>
|
||||
export type NodeInput = z.infer<typeof zNodeInput>
|
||||
export type NodeOutput = z.infer<typeof zNodeOutput>
|
||||
export type ComfyLink = z.infer<typeof zComfyLink>
|
||||
export type ComfyLinkObject = z.infer<typeof zComfyLinkObject>
|
||||
export type ComfyNode = z.infer<typeof zComfyNode>
|
||||
export type Reroute = z.infer<typeof zReroute>
|
||||
export type WorkflowJSON04 = z.infer<typeof zComfyWorkflow>
|
||||
|
||||
@@ -79,6 +79,8 @@ interface ApiMessage<T extends keyof ApiCalls> {
|
||||
data: ApiCalls[T]
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {}
|
||||
|
||||
/** Ensures workers get a fair shake. */
|
||||
type Unionize<T> = T[keyof T]
|
||||
|
||||
@@ -145,6 +147,36 @@ export interface ComfyApi extends EventTarget {
|
||||
): void
|
||||
}
|
||||
|
||||
export class PromptExecutionError extends Error {
|
||||
response: PromptResponse
|
||||
|
||||
constructor(response: PromptResponse) {
|
||||
super('Prompt execution failed')
|
||||
this.response = response
|
||||
}
|
||||
|
||||
override toString() {
|
||||
let message = ''
|
||||
if (typeof this.response.error === 'string') {
|
||||
message += this.response.error
|
||||
} else if (this.response.error) {
|
||||
message +=
|
||||
this.response.error.message + ': ' + this.response.error.details
|
||||
}
|
||||
|
||||
for (const [_, nodeError] of Object.entries(
|
||||
this.response.node_errors ?? []
|
||||
)) {
|
||||
message += '\n' + nodeError.class_type + ':'
|
||||
for (const errorReason of nodeError.errors) {
|
||||
message += '\n - ' + errorReason.message + ': ' + errorReason.details
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyApi extends EventTarget {
|
||||
#registered = new Set()
|
||||
api_host: string
|
||||
@@ -464,9 +496,10 @@ export class ComfyApi extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Queues a prompt to be executed
|
||||
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
||||
* @param {object} prompt The prompt data to queue
|
||||
* @throws {PromptExecutionError} If the prompt fails to execute
|
||||
*/
|
||||
async queuePrompt(
|
||||
number: number,
|
||||
@@ -496,9 +529,7 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw {
|
||||
response: await res.json()
|
||||
}
|
||||
throw new PromptExecutionError(await res.json())
|
||||
}
|
||||
|
||||
return await res.json()
|
||||
@@ -704,7 +735,12 @@ export class ComfyApi extends EventTarget {
|
||||
* @returns { Promise<string, unknown> } A dictionary of id -> value
|
||||
*/
|
||||
async getSettings(): Promise<Settings> {
|
||||
return (await this.fetchApi('/settings')).json()
|
||||
const resp = await this.fetchApi('/settings')
|
||||
|
||||
if (resp.status == 401) {
|
||||
throw new UnauthorizedError(resp.statusText)
|
||||
}
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,16 +3,20 @@ import {
|
||||
LGraphCanvas,
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
strokeShape
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { IWidget, Rect, Vector2 } from '@comfyorg/litegraph'
|
||||
import type { IWidget, Vector2 } from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { st, t } from '@/i18n'
|
||||
import type { ResultItem } from '@/schemas/apiSchema'
|
||||
import type {
|
||||
ExecutionErrorWsMessage,
|
||||
NodeError,
|
||||
ResultItem
|
||||
} from '@/schemas/apiSchema'
|
||||
import {
|
||||
ComfyApiWorkflow,
|
||||
type ComfyWorkflowJSON,
|
||||
@@ -44,10 +48,13 @@ import { ExtensionManager } from '@/types/extensionTypes'
|
||||
import { ColorAdjustOptions, adjustColor } from '@/utils/colorUtil'
|
||||
import { graphToPrompt } from '@/utils/executionUtil'
|
||||
import { executeWidgetsCallback, isImageNode } from '@/utils/litegraphUtil'
|
||||
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
|
||||
import {
|
||||
findLegacyRerouteNodes,
|
||||
noNativeReroutes
|
||||
} from '@/utils/migration/migrateReroute'
|
||||
import { deserialiseAndCreate } from '@/utils/vintageClipboard'
|
||||
|
||||
import { type ComfyApi, api } from './api'
|
||||
import { type ComfyApi, PromptExecutionError, api } from './api'
|
||||
import { defaultGraph } from './defaultGraph'
|
||||
import {
|
||||
getFlacMetadata,
|
||||
@@ -121,9 +128,6 @@ export class ComfyApp {
|
||||
dragOverNode: LGraphNode | null = null
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvasEl: HTMLCanvasElement
|
||||
lastNodeErrors: any[] | null = null
|
||||
/** @type {ExecutionErrorWsMessage} */
|
||||
lastExecutionError: { node_id?: NodeId } | null = null
|
||||
configuringGraph: boolean = false
|
||||
// @ts-expect-error fixme ts strict error
|
||||
ctx: CanvasRenderingContext2D
|
||||
@@ -137,10 +141,31 @@ export class ComfyApp {
|
||||
// Set by Comfy.Clipspace extension
|
||||
openClipspace: () => void = () => {}
|
||||
|
||||
#positionConversion?: {
|
||||
clientPosToCanvasPos: (pos: Vector2) => Vector2
|
||||
canvasPosToClientPos: (pos: Vector2) => Vector2
|
||||
}
|
||||
|
||||
/**
|
||||
* The node errors from the previous execution.
|
||||
* @deprecated Use useExecutionStore().lastNodeErrors instead
|
||||
*/
|
||||
get lastNodeErrors(): Record<NodeId, NodeError> | null {
|
||||
return useExecutionStore().lastNodeErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* The error from the previous execution.
|
||||
* @deprecated Use useExecutionStore().lastExecutionError instead
|
||||
*/
|
||||
get lastExecutionError(): ExecutionErrorWsMessage | null {
|
||||
return useExecutionStore().lastExecutionError
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use useExecutionStore().executingNodeId instead
|
||||
*/
|
||||
get runningNodeId(): string | null {
|
||||
get runningNodeId(): NodeId | null {
|
||||
return useExecutionStore().executingNodeId
|
||||
}
|
||||
|
||||
@@ -244,7 +269,7 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
getPreviewFormatParam() {
|
||||
let preview_format = this.ui.settings.getSettingValue('Comfy.PreviewFormat')
|
||||
let preview_format = useSettingStore().get('Comfy.PreviewFormat')
|
||||
if (preview_format) return `&preview=${preview_format}`
|
||||
else return ''
|
||||
}
|
||||
@@ -554,99 +579,7 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws node highlights (executing, drag drop) and progress bar
|
||||
*/
|
||||
#addDrawNodeHandler() {
|
||||
const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape
|
||||
const self = this
|
||||
LGraphCanvas.prototype.drawNodeShape = function (
|
||||
node,
|
||||
ctx,
|
||||
size,
|
||||
_fgcolor,
|
||||
bgcolor
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const res = origDrawNodeShape.apply(this, arguments)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const nodeErrors = self.lastNodeErrors?.[node.id]
|
||||
|
||||
let color = null
|
||||
let lineWidth = 1
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (node.id === +self.runningNodeId) {
|
||||
color = '#0f0'
|
||||
} else if (self.dragOverNode && node.id === self.dragOverNode.id) {
|
||||
color = 'dodgerblue'
|
||||
} else if (nodeErrors?.errors) {
|
||||
color = 'red'
|
||||
lineWidth = 2
|
||||
} else if (
|
||||
self.lastExecutionError &&
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+self.lastExecutionError.node_id === node.id
|
||||
) {
|
||||
color = '#f0f'
|
||||
lineWidth = 2
|
||||
}
|
||||
|
||||
if (color) {
|
||||
const area: Rect = [
|
||||
0,
|
||||
-LiteGraph.NODE_TITLE_HEIGHT,
|
||||
size[0],
|
||||
size[1] + LiteGraph.NODE_TITLE_HEIGHT
|
||||
]
|
||||
strokeShape(ctx, area, {
|
||||
shape: node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE,
|
||||
thickness: lineWidth,
|
||||
colour: color,
|
||||
title_height: LiteGraph.NODE_TITLE_HEIGHT,
|
||||
collapsed: node.collapsed
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (self.progress && node.id === +self.runningNodeId) {
|
||||
ctx.fillStyle = 'green'
|
||||
ctx.fillRect(
|
||||
0,
|
||||
0,
|
||||
size[0] * (self.progress.value / self.progress.max),
|
||||
6
|
||||
)
|
||||
ctx.fillStyle = bgcolor
|
||||
}
|
||||
|
||||
// Highlight inputs that failed validation
|
||||
if (nodeErrors) {
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = 'red'
|
||||
for (const error of nodeErrors.errors) {
|
||||
if (error.extra_info && error.extra_info.input_name) {
|
||||
const inputIndex = node.findInputSlot(error.extra_info.input_name)
|
||||
if (inputIndex !== -1) {
|
||||
let pos = node.getConnectionPos(true, inputIndex)
|
||||
ctx.beginPath()
|
||||
ctx.arc(
|
||||
pos[0] - node.pos[0],
|
||||
pos[1] - node.pos[1],
|
||||
12,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
false
|
||||
)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const origDrawNode = LGraphCanvas.prototype.drawNode
|
||||
LGraphCanvas.prototype.drawNode = function (node) {
|
||||
const editor_alpha = this.editor_alpha
|
||||
@@ -733,15 +666,13 @@ export class ComfyApp {
|
||||
})
|
||||
|
||||
api.addEventListener('execution_start', () => {
|
||||
this.lastExecutionError = null
|
||||
this.graph.nodes.forEach((node) => {
|
||||
if (node.onExecutionStart) node.onExecutionStart()
|
||||
})
|
||||
})
|
||||
|
||||
api.addEventListener('execution_error', ({ detail }) => {
|
||||
this.lastExecutionError = detail
|
||||
useDialogService().showExecutionErrorDialog({ error: detail })
|
||||
useDialogService().showExecutionErrorDialog(detail)
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
@@ -852,6 +783,11 @@ export class ComfyApp {
|
||||
this.#addDropHandler()
|
||||
|
||||
await useExtensionService().invokeExtensionsAsync('setup')
|
||||
|
||||
this.#positionConversion = useCanvasPositionConversion(
|
||||
this.canvasContainer,
|
||||
this.canvas
|
||||
)
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
@@ -1023,7 +959,11 @@ export class ComfyApp {
|
||||
clean: boolean = true,
|
||||
restore_view: boolean = true,
|
||||
workflow: string | null | ComfyWorkflow = null,
|
||||
{ showMissingNodesDialog = true, showMissingModelsDialog = true } = {}
|
||||
{
|
||||
showMissingNodesDialog = true,
|
||||
showMissingModelsDialog = true,
|
||||
checkForRerouteMigration = true
|
||||
} = {}
|
||||
) {
|
||||
if (clean !== false) {
|
||||
this.clean()
|
||||
@@ -1049,12 +989,21 @@ export class ComfyApp {
|
||||
// Ideally we should not block users from loading the workflow.
|
||||
graphData = validatedGraphData ?? graphData
|
||||
}
|
||||
|
||||
// Migrate legacy reroute nodes to the new format
|
||||
if (graphData.version === 0.4) {
|
||||
graphData = migrateLegacyRerouteNodes(graphData)
|
||||
// Only show the reroute migration warning if the workflow does not have native
|
||||
// reroutes. Merging reroute network has great complexity, and it is not supported
|
||||
// for now.
|
||||
// See: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3317
|
||||
if (
|
||||
checkForRerouteMigration &&
|
||||
graphData.version === 0.4 &&
|
||||
findLegacyRerouteNodes(graphData).length &&
|
||||
noNativeReroutes(graphData)
|
||||
) {
|
||||
useToastStore().add({
|
||||
group: 'reroute-migration',
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
|
||||
useWorkflowService().beforeLoadNewGraph()
|
||||
|
||||
const missingNodeTypes: MissingNodeType[] = []
|
||||
@@ -1129,7 +1078,7 @@ export class ComfyApp {
|
||||
} catch (error) {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
title: t('errorDialog.loadWorkflowTitle'),
|
||||
errorType: 'loadWorkflowError'
|
||||
reportType: 'loadWorkflowError'
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1223,32 +1172,6 @@ export class ComfyApp {
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
#formatPromptError(error) {
|
||||
if (error == null) {
|
||||
return '(unknown error)'
|
||||
} else if (typeof error === 'string') {
|
||||
return error
|
||||
} else if (error.stack && error.message) {
|
||||
return error.toString()
|
||||
} else if (error.response) {
|
||||
let message = error.response.error.message
|
||||
if (error.response.error.details)
|
||||
message += ': ' + error.response.error.details
|
||||
for (const [_, nodeError] of Object.entries(error.response.node_errors)) {
|
||||
// @ts-expect-error
|
||||
message += '\n' + nodeError.class_type + ':'
|
||||
// @ts-expect-error
|
||||
for (const errorReason of nodeError.errors) {
|
||||
message +=
|
||||
'\n - ' + errorReason.message + ': ' + errorReason.details
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
return '(unknown error)'
|
||||
}
|
||||
|
||||
async queuePrompt(number: number, batchCount: number = 1): Promise<boolean> {
|
||||
this.#queueItems.push({ number, batchCount })
|
||||
|
||||
@@ -1258,7 +1181,8 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
this.#processingQueue = true
|
||||
this.lastNodeErrors = null
|
||||
const executionStore = useExecutionStore()
|
||||
executionStore.lastNodeErrors = null
|
||||
|
||||
try {
|
||||
while (this.#queueItems.length) {
|
||||
@@ -1272,13 +1196,13 @@ export class ComfyApp {
|
||||
const p = await this.graphToPrompt()
|
||||
try {
|
||||
const res = await api.queuePrompt(number, p)
|
||||
this.lastNodeErrors = res.node_errors ?? null
|
||||
if (this.lastNodeErrors?.length) {
|
||||
executionStore.lastNodeErrors = res.node_errors ?? null
|
||||
if (executionStore.lastNodeErrors?.length) {
|
||||
this.canvas.draw(true, true)
|
||||
} else {
|
||||
try {
|
||||
if (res.prompt_id) {
|
||||
useExecutionStore().storePrompt({
|
||||
executionStore.storePrompt({
|
||||
id: res.prompt_id,
|
||||
nodes: Object.keys(p.output),
|
||||
workflow: useWorkspaceStore().workflow
|
||||
@@ -1287,13 +1211,14 @@ export class ComfyApp {
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
} catch (error) {
|
||||
const formattedError = this.#formatPromptError(error)
|
||||
this.ui.dialog.show(formattedError)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (error.response) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.lastNodeErrors = error.response.node_errors
|
||||
} catch (error: unknown) {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
title: t('errorDialog.promptExecutionError'),
|
||||
reportType: 'promptExecutionError'
|
||||
})
|
||||
|
||||
if (error instanceof PromptExecutionError) {
|
||||
executionStore.lastNodeErrors = error.response.node_errors ?? null
|
||||
this.canvas.draw(true, true)
|
||||
}
|
||||
break
|
||||
@@ -1315,7 +1240,7 @@ export class ComfyApp {
|
||||
this.#processingQueue = false
|
||||
}
|
||||
api.dispatchCustomEvent('promptQueued', { number, batchCount })
|
||||
return !this.lastNodeErrors
|
||||
return !executionStore.lastNodeErrors
|
||||
}
|
||||
|
||||
showErrorOnFileLoad(file: File) {
|
||||
@@ -1643,26 +1568,23 @@ export class ComfyApp {
|
||||
this.revokePreviews(id)
|
||||
}
|
||||
this.nodePreviewImages = {}
|
||||
this.lastNodeErrors = null
|
||||
this.lastExecutionError = null
|
||||
const executionStore = useExecutionStore()
|
||||
executionStore.lastNodeErrors = null
|
||||
executionStore.lastExecutionError = null
|
||||
}
|
||||
|
||||
clientPosToCanvasPos(pos: Vector2): Vector2 {
|
||||
const rect = this.canvasContainer.getBoundingClientRect()
|
||||
const containerOffsets = [rect.left, rect.top]
|
||||
return _.zip(pos, this.canvas.ds.offset, containerOffsets).map(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
([p, o1, o2]) => (p - o2) / this.canvas.ds.scale - o1
|
||||
) as Vector2
|
||||
if (!this.#positionConversion) {
|
||||
throw new Error('clientPosToCanvasPos called before setup')
|
||||
}
|
||||
return this.#positionConversion.clientPosToCanvasPos(pos)
|
||||
}
|
||||
|
||||
canvasPosToClientPos(pos: Vector2): Vector2 {
|
||||
const rect = this.canvasContainer.getBoundingClientRect()
|
||||
const containerOffsets = [rect.left, rect.top]
|
||||
return _.zip(pos, this.canvas.ds.offset, containerOffsets).map(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
([p, o1, o2]) => (p + o1) * this.canvas.ds.scale + o2
|
||||
) as Vector2
|
||||
if (!this.#positionConversion) {
|
||||
throw new Error('canvasPosToClientPos called before setup')
|
||||
}
|
||||
return this.#positionConversion.canvasPosToClientPos(pos)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { filterSerializedWidgetValues } from '@/utils/litegraphUtil'
|
||||
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
@@ -135,7 +136,8 @@ export class ChangeTracker {
|
||||
try {
|
||||
await this.app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
showMissingModelsDialog: false,
|
||||
showMissingNodesDialog: false
|
||||
showMissingNodesDialog: false,
|
||||
checkForRerouteMigration: false
|
||||
})
|
||||
this.activeState = prevState
|
||||
this.updateModified()
|
||||
@@ -384,7 +386,10 @@ export class ChangeTracker {
|
||||
if (
|
||||
!_.isEqualWith(a.nodes, b.nodes, (arrA, arrB) => {
|
||||
if (Array.isArray(arrA) && Array.isArray(arrB)) {
|
||||
return _.isEqual(new Set(arrA), new Set(arrB))
|
||||
// Filter non-serializable widget values before comparison
|
||||
const filteredArrA = filterSerializedWidgetValues(arrA, app.graph)
|
||||
const filteredArrB = filterSerializedWidgetValues(arrB, app.graph)
|
||||
return _.isEqual(new Set(filteredArrA), new Set(filteredArrB))
|
||||
}
|
||||
})
|
||||
) {
|
||||
|
||||
@@ -56,6 +56,9 @@ export interface ComponentWidget<V extends object | string>
|
||||
|
||||
export interface DOMWidgetOptions<V extends object | string>
|
||||
extends IWidgetOptions {
|
||||
/**
|
||||
* Whether to render a placeholder rectangle when zoomed out.
|
||||
*/
|
||||
hideOnZoom?: boolean
|
||||
selectOn?: string[]
|
||||
onHide?: (widget: BaseDOMWidget<V>) => void
|
||||
@@ -145,7 +148,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
widget_height: number,
|
||||
lowQuality?: boolean
|
||||
): void {
|
||||
if (this.options.hideOnZoom && lowQuality) {
|
||||
if (this.options.hideOnZoom && lowQuality && this.isVisible()) {
|
||||
// Draw a placeholder rectangle
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
ctx.beginPath()
|
||||
|
||||
65
src/scripts/errorNodeWidgets.ts
Normal file
65
src/scripts/errorNodeWidgets.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
|
||||
import { useFloatWidget } from '@/composables/widgets/useFloatWidget'
|
||||
import { useStringWidget } from '@/composables/widgets/useStringWidget'
|
||||
|
||||
const StringWidget = useStringWidget()
|
||||
const FloatWidget = useFloatWidget()
|
||||
const BooleanWidget = useBooleanWidget()
|
||||
|
||||
function addWidgetFromValue(node: LGraphNode, value: unknown) {
|
||||
let widget: IWidget
|
||||
|
||||
if (typeof value === 'string') {
|
||||
widget = StringWidget(node, {
|
||||
type: 'STRING',
|
||||
name: 'UNKNOWN',
|
||||
multiline: value.length > 20
|
||||
})
|
||||
} else if (typeof value === 'number') {
|
||||
widget = FloatWidget(node, {
|
||||
type: 'FLOAT',
|
||||
name: 'UNKNOWN'
|
||||
})
|
||||
} else if (typeof value === 'boolean') {
|
||||
widget = BooleanWidget(node, {
|
||||
type: 'BOOLEAN',
|
||||
name: 'UNKNOWN'
|
||||
})
|
||||
} else {
|
||||
widget = StringWidget(node, {
|
||||
type: 'STRING',
|
||||
name: 'UNKNOWN',
|
||||
multiline: true
|
||||
})
|
||||
widget.value = JSON.stringify(value)
|
||||
return
|
||||
}
|
||||
|
||||
widget.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Try add widgets to node with missing definition.
|
||||
*/
|
||||
LGraphNode.prototype.onConfigure = useChainCallback(
|
||||
LGraphNode.prototype.onConfigure,
|
||||
function (this: LGraphNode, info) {
|
||||
if (!this.has_errors || !info.widgets_values) return
|
||||
|
||||
/**
|
||||
* Note: Some custom nodes overrides the `widgets_values` property to an
|
||||
* object that has `length` property and index access. It is not safe to call
|
||||
* any array methods on it.
|
||||
* See example in https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/blob/8629188458dc6cb832f871ece3bd273507e8a766/web/js/VHS.core.js#L59-L84
|
||||
*/
|
||||
for (let i = 0; i < info.widgets_values.length; i++) {
|
||||
const widgetValue = info.widgets_values[i]
|
||||
addWidgetFromValue(this, widgetValue)
|
||||
}
|
||||
|
||||
this.serialize_widgets = true
|
||||
}
|
||||
)
|
||||
@@ -20,6 +20,7 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import type { ComfyApp } from './app'
|
||||
import './domWidget'
|
||||
import './errorNodeWidgets'
|
||||
|
||||
export type ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
|
||||
@@ -181,6 +181,7 @@ export const useColorPaletteService = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
getActiveColorPalette: () => colorPaletteStore.completedActivePalette,
|
||||
addCustomColorPalette: wrapWithErrorHandling(addCustomColorPalette),
|
||||
deleteCustomColorPalette: wrapWithErrorHandling(deleteCustomColorPalette),
|
||||
loadColorPalette: wrapWithErrorHandlingAsync(loadColorPalette),
|
||||
|
||||
@@ -11,6 +11,10 @@ const registryApiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
paramsSerializer: {
|
||||
// Disables PHP-style notation (e.g. param[]=value) in favor of repeated params (e.g. param=value1¶m=value2)
|
||||
indexes: null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
|
||||
@@ -15,6 +14,7 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
|
||||
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
@@ -70,12 +70,21 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showExecutionErrorDialog(
|
||||
props: InstanceType<typeof ExecutionErrorDialogContent>['$props']
|
||||
) {
|
||||
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
|
||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
||||
error: {
|
||||
exceptionType: executionError.exception_type,
|
||||
exceptionMessage: executionError.exception_message,
|
||||
nodeId: executionError.node_id,
|
||||
nodeType: executionError.node_type,
|
||||
traceback: executionError.traceback.join('\n'),
|
||||
reportType: 'graphExecutionError'
|
||||
}
|
||||
}
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: 'global-execution-error',
|
||||
component: ExecutionErrorDialogContent,
|
||||
component: ErrorDialogContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
@@ -174,23 +183,33 @@ export const useDialogService = () => {
|
||||
error: unknown,
|
||||
options: {
|
||||
title?: string
|
||||
errorType?: string
|
||||
reportType?: string
|
||||
} = {}
|
||||
) {
|
||||
const props =
|
||||
const errorProps: {
|
||||
errorMessage: string
|
||||
stackTrace?: string
|
||||
extensionFile?: string
|
||||
} =
|
||||
error instanceof Error
|
||||
? parseError(error)
|
||||
: {
|
||||
errorMessage: String(error)
|
||||
}
|
||||
|
||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
||||
error: {
|
||||
exceptionType: options.title ?? 'Unknown Error',
|
||||
exceptionMessage: errorProps.errorMessage,
|
||||
traceback: errorProps.stackTrace ?? t('errorDialog.noStackTrace'),
|
||||
reportType: options.reportType
|
||||
}
|
||||
}
|
||||
|
||||
dialogStore.showDialog({
|
||||
key: 'global-error',
|
||||
component: ErrorDialogContent,
|
||||
props: {
|
||||
...props,
|
||||
...options
|
||||
}
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { st, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type {
|
||||
ComfyNodeDef as ComfyNodeDefV2,
|
||||
InputSpec,
|
||||
OutputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
@@ -39,20 +43,65 @@ export const useLitegraphService = () => {
|
||||
|
||||
async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) {
|
||||
const node = class ComfyNode extends LGraphNode {
|
||||
static comfyClass?: string
|
||||
static title?: string
|
||||
static nodeData?: ComfyNodeDefV1 & ComfyNodeDefV2
|
||||
static category?: string
|
||||
static comfyClass: string
|
||||
static title: string
|
||||
static category: string
|
||||
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
|
||||
|
||||
constructor(title?: string) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
/**
|
||||
* @internal The initial minimum size of the node.
|
||||
*/
|
||||
#initialMinSize = { width: 1, height: 1 }
|
||||
/**
|
||||
* @internal The key for the node definition in the i18n file.
|
||||
*/
|
||||
get #nodeKey(): string {
|
||||
return `nodeDefs.${normalizeI18nKey(ComfyNode.nodeData.name)}`
|
||||
}
|
||||
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
this.#setupStrokeStyles()
|
||||
this.#addInputs(ComfyNode.nodeData.inputs)
|
||||
this.#addOutputs(ComfyNode.nodeData.outputs)
|
||||
this.#setInitialSize()
|
||||
this.serialize_widgets = true
|
||||
extensionService.invokeExtensionsAsync('nodeCreated', this)
|
||||
}
|
||||
|
||||
const nodeMinSize = { width: 1, height: 1 }
|
||||
// Process inputs using V2 schema
|
||||
for (const [inputName, inputSpec] of Object.entries(nodeDef.inputs)) {
|
||||
/**
|
||||
* @internal Setup stroke styles for the node under various conditions.
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
this.strokeStyles['running'] = function (this: LGraphNode) {
|
||||
if (this.id == app.runningNodeId) {
|
||||
return { color: '#0f0' }
|
||||
}
|
||||
}
|
||||
this.strokeStyles['nodeError'] = function (this: LGraphNode) {
|
||||
if (app.lastNodeErrors?.[this.id]?.errors) {
|
||||
return { color: 'red' }
|
||||
}
|
||||
}
|
||||
this.strokeStyles['dragOver'] = function (this: LGraphNode) {
|
||||
if (app.dragOverNode?.id == this.id) {
|
||||
return { color: 'dodgerblue' }
|
||||
}
|
||||
}
|
||||
this.strokeStyles['executionError'] = function (this: LGraphNode) {
|
||||
if (app.lastExecutionError?.node_id == this.id) {
|
||||
return { color: '#f0f', lineWidth: 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Add inputs to the node.
|
||||
*/
|
||||
#addInputs(inputs: Record<string, InputSpec>) {
|
||||
for (const [inputName, inputSpec] of Object.entries(inputs)) {
|
||||
const inputType = inputSpec.type
|
||||
const nameKey = `nodeDefs.${normalizeI18nKey(nodeDef.name)}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||
|
||||
const widgetConstructor = widgetStore.widgets[inputType]
|
||||
if (widgetConstructor) {
|
||||
@@ -68,29 +117,25 @@ export const useLitegraphService = () => {
|
||||
) ?? {}
|
||||
|
||||
if (widget) {
|
||||
const fallback = widget.label ?? inputName
|
||||
widget.label = st(nameKey, fallback)
|
||||
|
||||
widget.label = st(nameKey, widget.label ?? inputName)
|
||||
widget.options ??= {}
|
||||
if (inputSpec.isOptional) {
|
||||
widget.options.inputIsOptional = true
|
||||
}
|
||||
if (inputSpec.forceInput) {
|
||||
widget.options.forceInput = true
|
||||
}
|
||||
if (inputSpec.defaultInput) {
|
||||
widget.options.defaultInput = true
|
||||
}
|
||||
if (inputSpec.advanced) {
|
||||
widget.advanced = true
|
||||
}
|
||||
if (inputSpec.hidden) {
|
||||
widget.hidden = true
|
||||
}
|
||||
Object.assign(widget.options, {
|
||||
inputIsOptional: inputSpec.isOptional,
|
||||
forceInput: inputSpec.forceInput,
|
||||
defaultInput: inputSpec.defaultInput,
|
||||
advanced: inputSpec.advanced,
|
||||
hidden: inputSpec.hidden
|
||||
})
|
||||
}
|
||||
|
||||
nodeMinSize.width = Math.max(nodeMinSize.width, minWidth)
|
||||
nodeMinSize.height = Math.max(nodeMinSize.height, minHeight)
|
||||
this.#initialMinSize.width = Math.max(
|
||||
this.#initialMinSize.width,
|
||||
minWidth
|
||||
)
|
||||
this.#initialMinSize.height = Math.max(
|
||||
this.#initialMinSize.height,
|
||||
minHeight
|
||||
)
|
||||
} else {
|
||||
// Node connection inputs
|
||||
const shapeOptions = inputSpec.isOptional
|
||||
@@ -103,17 +148,17 @@ export const useLitegraphService = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process outputs using V2 schema
|
||||
for (const output of nodeDef.outputs) {
|
||||
const outputName = output.name
|
||||
const outputType = output.type
|
||||
const outputIsList = output.is_list
|
||||
const shapeOptions = outputIsList
|
||||
? { shape: LiteGraph.GRID_SHAPE }
|
||||
: {}
|
||||
const nameKey = `nodeDefs.${normalizeI18nKey(nodeDef.name)}.outputs.${output.index}.name`
|
||||
const typeKey = `dataTypes.${normalizeI18nKey(outputType)}`
|
||||
/**
|
||||
* @internal Add outputs to the node.
|
||||
*/
|
||||
#addOutputs(outputs: OutputSpec[]) {
|
||||
for (const output of outputs) {
|
||||
const { name, type, is_list } = output
|
||||
const shapeOptions = is_list ? { shape: LiteGraph.GRID_SHAPE } : {}
|
||||
const nameKey = `${this.#nodeKey}.outputs.${output.index}.name`
|
||||
const typeKey = `dataTypes.${normalizeI18nKey(type)}`
|
||||
const outputOptions = {
|
||||
...shapeOptions,
|
||||
// If the output name is different from the output type, use the output name.
|
||||
@@ -121,20 +166,20 @@ export const useLitegraphService = () => {
|
||||
// - type ("INT"); name ("Positive") => translate name
|
||||
// - type ("FLOAT"); name ("FLOAT") => translate type
|
||||
localized_name:
|
||||
outputType !== outputName
|
||||
? st(nameKey, outputName)
|
||||
: st(typeKey, outputName)
|
||||
type !== name ? st(nameKey, name) : st(typeKey, name)
|
||||
}
|
||||
this.addOutput(outputName, outputType, outputOptions)
|
||||
this.addOutput(name, type, outputOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Set the initial size of the node.
|
||||
*/
|
||||
#setInitialSize() {
|
||||
const s = this.computeSize()
|
||||
s[0] = Math.max(nodeMinSize.width, s[0] * 1.5)
|
||||
s[1] = Math.max(nodeMinSize.height, s[1])
|
||||
s[0] = Math.max(this.#initialMinSize.width, s[0] * 1.5)
|
||||
s[1] = Math.max(this.#initialMinSize.height, s[1])
|
||||
this.setSize(s)
|
||||
this.serialize_widgets = true
|
||||
|
||||
extensionService.invokeExtensionsAsync('nodeCreated', this)
|
||||
}
|
||||
|
||||
configure(data: any) {
|
||||
|
||||
@@ -1,287 +1,96 @@
|
||||
import Fuse, { FuseSearchOptions, IFuseOptions } from 'fuse.js'
|
||||
import _ from 'lodash'
|
||||
import { FuseSearchOptions } from 'fuse.js'
|
||||
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
|
||||
export type SearchAuxScore = number[]
|
||||
|
||||
interface ExtraSearchOptions {
|
||||
matchWildcards?: boolean
|
||||
}
|
||||
|
||||
export class FuseSearch<T> {
|
||||
public readonly fuse: Fuse<T>
|
||||
public readonly keys: string[]
|
||||
public readonly data: T[]
|
||||
public readonly advancedScoring: boolean
|
||||
|
||||
constructor(
|
||||
data: T[],
|
||||
options?: IFuseOptions<T>,
|
||||
createIndex: boolean = true,
|
||||
advancedScoring: boolean = false
|
||||
) {
|
||||
this.data = data
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.keys = (options.keys ?? []) as string[]
|
||||
this.advancedScoring = advancedScoring
|
||||
const index =
|
||||
createIndex && options?.keys
|
||||
? Fuse.createIndex(options.keys, data)
|
||||
: undefined
|
||||
this.fuse = new Fuse(data, options, index)
|
||||
}
|
||||
|
||||
public search(query: string, options?: FuseSearchOptions): T[] {
|
||||
const fuseResult = !query
|
||||
? this.data.map((x) => ({ item: x, score: 0 }))
|
||||
: this.fuse.search(query, options)
|
||||
|
||||
if (!this.advancedScoring) {
|
||||
return fuseResult.map((x) => x.item)
|
||||
}
|
||||
|
||||
const aux = fuseResult
|
||||
.map((x) => ({
|
||||
item: x.item,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
scores: this.calcAuxScores(query.toLocaleLowerCase(), x.item, x.score)
|
||||
}))
|
||||
.sort((a, b) => this.compareAux(a.scores, b.scores))
|
||||
|
||||
return aux.map((x) => x.item)
|
||||
}
|
||||
|
||||
public calcAuxScores(query: string, entry: T, score: number): SearchAuxScore {
|
||||
let values: string[] = []
|
||||
if (!this.keys.length) values = [entry as string]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
else values = this.keys.map((x) => entry[x])
|
||||
const scores = values.map((x) => this.calcAuxSingle(query, x, score))
|
||||
let result = scores.sort(this.compareAux)[0]
|
||||
|
||||
const deprecated = values.some((x) =>
|
||||
x.toLocaleLowerCase().includes('deprecated')
|
||||
)
|
||||
result[0] += deprecated && result[0] != 0 ? 5 : 0
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (entry['postProcessSearchScores']) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
result = entry['postProcessSearchScores'](result) as SearchAuxScore
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public calcAuxSingle(
|
||||
query: string,
|
||||
item: string,
|
||||
score: number
|
||||
): SearchAuxScore {
|
||||
const itemWords = item
|
||||
.split(/ |\b|(?<=[a-z])(?=[A-Z])|(?=[A-Z][a-z])/)
|
||||
.map((x) => x.toLocaleLowerCase())
|
||||
const queryParts = query.split(' ')
|
||||
item = item.toLocaleLowerCase()
|
||||
|
||||
let main = 9
|
||||
let aux1 = 0
|
||||
let aux2 = 0
|
||||
|
||||
if (item == query) {
|
||||
main = 0
|
||||
} else if (item.startsWith(query)) {
|
||||
main = 1
|
||||
aux2 = item.length
|
||||
} else if (itemWords.includes(query)) {
|
||||
main = 2
|
||||
aux1 = item.indexOf(query) + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (item.includes(query)) {
|
||||
main = 3
|
||||
aux1 = item.indexOf(query) + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (queryParts.every((x) => itemWords.includes(x))) {
|
||||
const indexes = queryParts.map((x) => itemWords.indexOf(x))
|
||||
const min = Math.min(...indexes)
|
||||
const max = Math.max(...indexes)
|
||||
main = 4
|
||||
aux1 = max - min + max * 0.5 + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (queryParts.every((x) => item.includes(x))) {
|
||||
const min = Math.min(...queryParts.map((x) => item.indexOf(x)))
|
||||
const max = Math.max(...queryParts.map((x) => item.indexOf(x) + x.length))
|
||||
main = 5
|
||||
aux1 = max - min + max * 0.5 + item.length * 0.5
|
||||
aux2 = item.length
|
||||
}
|
||||
|
||||
const lengthPenalty =
|
||||
0.2 *
|
||||
(1 -
|
||||
Math.min(item.length, query.length) /
|
||||
Math.max(item.length, query.length))
|
||||
return [main, aux1, aux2, score + lengthPenalty]
|
||||
}
|
||||
|
||||
public compareAux(a: SearchAuxScore, b: SearchAuxScore) {
|
||||
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return a[i] - b[i]
|
||||
}
|
||||
}
|
||||
return a.length - b.length
|
||||
}
|
||||
}
|
||||
|
||||
export type FilterAndValue<T = string> = [NodeFilter<T>, T]
|
||||
|
||||
export class NodeFilter<FilterOptionT = string> {
|
||||
public readonly fuseSearch: FuseSearch<FilterOptionT>
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly name: string,
|
||||
public readonly invokeSequence: string,
|
||||
public readonly longInvokeSequence: string,
|
||||
public readonly nodeOptions:
|
||||
| FilterOptionT[]
|
||||
| ((node: ComfyNodeDefImpl) => FilterOptionT[]),
|
||||
nodeDefs: ComfyNodeDefImpl[],
|
||||
options?: IFuseOptions<FilterOptionT>
|
||||
) {
|
||||
this.fuseSearch = new FuseSearch(this.getAllNodeOptions(nodeDefs), options)
|
||||
}
|
||||
|
||||
public getNodeOptions(node: ComfyNodeDefImpl): FilterOptionT[] {
|
||||
return this.nodeOptions instanceof Function
|
||||
? this.nodeOptions(node)
|
||||
: this.nodeOptions
|
||||
}
|
||||
|
||||
public getAllNodeOptions(nodeDefs: ComfyNodeDefImpl[]): FilterOptionT[] {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return [
|
||||
...new Set(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeDefs.reduce((acc, nodeDef) => {
|
||||
return [...acc, ...this.getNodeOptions(nodeDef)]
|
||||
}, [])
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
public matches(
|
||||
node: ComfyNodeDefImpl,
|
||||
value: FilterOptionT,
|
||||
extraOptions?: ExtraSearchOptions
|
||||
): boolean {
|
||||
const matchWildcards = extraOptions?.matchWildcards !== false
|
||||
if (matchWildcards && value === '*') {
|
||||
return true
|
||||
}
|
||||
const options = this.getNodeOptions(node)
|
||||
return (
|
||||
options.includes(value) ||
|
||||
(matchWildcards && _.some(options, (option) => option === '*'))
|
||||
)
|
||||
}
|
||||
}
|
||||
import { FuseFilter, FuseFilterWithValue, FuseSearch } from '@/utils/fuseUtil'
|
||||
|
||||
export class NodeSearchService {
|
||||
public readonly nodeFuseSearch: FuseSearch<ComfyNodeDefImpl>
|
||||
public readonly nodeFilters: NodeFilter<string>[]
|
||||
public readonly inputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly outputTypeFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly nodeCategoryFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
public readonly nodeSourceFilter: FuseFilter<ComfyNodeDefImpl, string>
|
||||
|
||||
constructor(data: ComfyNodeDefImpl[]) {
|
||||
this.nodeFuseSearch = new FuseSearch(
|
||||
data,
|
||||
{
|
||||
this.nodeFuseSearch = new FuseSearch(data, {
|
||||
fuseOptions: {
|
||||
keys: ['name', 'display_name'],
|
||||
includeScore: true,
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
useExtendedSearch: true
|
||||
},
|
||||
true,
|
||||
true
|
||||
)
|
||||
createIndex: true,
|
||||
advancedScoring: true
|
||||
})
|
||||
|
||||
const filterSearchOptions = {
|
||||
const fuseOptions = {
|
||||
includeScore: true,
|
||||
threshold: 0.3,
|
||||
shouldSort: true
|
||||
}
|
||||
|
||||
const inputTypeFilter = new NodeFilter<string>(
|
||||
/* id */ 'input',
|
||||
/* name */ 'Input Type',
|
||||
/* invokeSequence */ 'i',
|
||||
/* longInvokeSequence */ 'input',
|
||||
(node) => Object.values(node.inputs).map((input) => input.type),
|
||||
data,
|
||||
filterSearchOptions
|
||||
)
|
||||
this.inputTypeFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
|
||||
id: 'input',
|
||||
name: 'Input Type',
|
||||
invokeSequence: 'i',
|
||||
getItemOptions: (node) =>
|
||||
Object.values(node.inputs).map((input) => input.type),
|
||||
fuseOptions
|
||||
})
|
||||
|
||||
const outputTypeFilter = new NodeFilter<string>(
|
||||
/* id */ 'output',
|
||||
/* name */ 'Output Type',
|
||||
/* invokeSequence */ 'o',
|
||||
/* longInvokeSequence */ 'output',
|
||||
(node) => node.outputs.map((output) => output.type),
|
||||
data,
|
||||
filterSearchOptions
|
||||
)
|
||||
this.outputTypeFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
|
||||
id: 'output',
|
||||
name: 'Output Type',
|
||||
invokeSequence: 'o',
|
||||
getItemOptions: (node) => node.outputs.map((output) => output.type),
|
||||
fuseOptions
|
||||
})
|
||||
|
||||
const nodeCategoryFilter = new NodeFilter<string>(
|
||||
/* id */ 'category',
|
||||
/* name */ 'Category',
|
||||
/* invokeSequence */ 'c',
|
||||
/* longInvokeSequence */ 'category',
|
||||
(node) => [node.category],
|
||||
data,
|
||||
filterSearchOptions
|
||||
)
|
||||
this.nodeCategoryFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
|
||||
id: 'category',
|
||||
name: 'Category',
|
||||
invokeSequence: 'c',
|
||||
getItemOptions: (node) => [node.category],
|
||||
fuseOptions
|
||||
})
|
||||
|
||||
const nodeSourceFilter = new NodeFilter<string>(
|
||||
/* id */ 'source',
|
||||
/* name */ 'Source',
|
||||
/* invokeSequence */ 's',
|
||||
/* longInvokeSequence */ 'source',
|
||||
(node) => [node.nodeSource.displayText],
|
||||
data,
|
||||
filterSearchOptions
|
||||
)
|
||||
|
||||
this.nodeFilters = [
|
||||
inputTypeFilter,
|
||||
outputTypeFilter,
|
||||
nodeCategoryFilter,
|
||||
nodeSourceFilter
|
||||
]
|
||||
}
|
||||
|
||||
public endsWithFilterStartSequence(query: string): boolean {
|
||||
return query.endsWith(':')
|
||||
this.nodeSourceFilter = new FuseFilter<ComfyNodeDefImpl, string>(data, {
|
||||
id: 'source',
|
||||
name: 'Source',
|
||||
invokeSequence: 's',
|
||||
getItemOptions: (node) => [node.nodeSource.displayText],
|
||||
fuseOptions
|
||||
})
|
||||
}
|
||||
|
||||
public searchNode(
|
||||
query: string,
|
||||
filters: FilterAndValue<string>[] = [],
|
||||
filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[] = [],
|
||||
options?: FuseSearchOptions,
|
||||
extraOptions?: ExtraSearchOptions
|
||||
extraOptions: {
|
||||
matchWildcards?: boolean
|
||||
} = {}
|
||||
): ComfyNodeDefImpl[] {
|
||||
const { matchWildcards = true } = extraOptions
|
||||
const wildcard = matchWildcards ? '*' : undefined
|
||||
const matchedNodes = this.nodeFuseSearch.search(query)
|
||||
|
||||
const results = matchedNodes.filter((node) => {
|
||||
return _.every(filters, (filterAndValue) => {
|
||||
const [filter, value] = filterAndValue
|
||||
return filter.matches(node, value, extraOptions)
|
||||
return filters.every((filterAndValue) => {
|
||||
const { filterDef, value } = filterAndValue
|
||||
return filterDef.matches(node, value, { wildcard })
|
||||
})
|
||||
})
|
||||
|
||||
return options?.limit ? results.slice(0, options.limit) : results
|
||||
}
|
||||
|
||||
public getFilterById(id: string): NodeFilter<string> | undefined {
|
||||
return this.nodeFilters.find((filter) => filter.id === id)
|
||||
get nodeFilters(): FuseFilter<ComfyNodeDefImpl, string>[] {
|
||||
return [
|
||||
this.inputTypeFilter,
|
||||
this.outputTypeFilter,
|
||||
this.nodeCategoryFilter,
|
||||
this.nodeSourceFilter
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { SerialisableGraph, Vector2 } from '@comfyorg/litegraph'
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
ComfyWorkflowJSON,
|
||||
WorkflowJSON04
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { downloadBlob } from '@/scripts/utils'
|
||||
@@ -15,7 +12,6 @@ import { useToastStore } from '@/stores/toastStore'
|
||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
|
||||
|
||||
import { useDialogService } from './dialogService'
|
||||
|
||||
@@ -167,7 +163,8 @@ export const useWorkflowService = () => {
|
||||
workflow,
|
||||
{
|
||||
showMissingModelsDialog: loadFromRemote,
|
||||
showMissingNodesDialog: loadFromRemote
|
||||
showMissingNodesDialog: loadFromRemote,
|
||||
checkForRerouteMigration: loadFromRemote
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -328,10 +325,7 @@ export const useWorkflowService = () => {
|
||||
) => {
|
||||
const loadedWorkflow = await workflow.load()
|
||||
const data = loadedWorkflow.initialState
|
||||
const workflowJSON =
|
||||
data.version === 0.4
|
||||
? migrateLegacyRerouteNodes(data as WorkflowJSON04)
|
||||
: data
|
||||
const workflowJSON = data
|
||||
const old = localStorage.getItem('litegrapheditor_clipboard')
|
||||
// unknown conversion: ComfyWorkflowJSON is stricter than LiteGraph's
|
||||
// serialisation schema.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
* Store for state of installed node packs
|
||||
*/
|
||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const { t } = useI18n()
|
||||
const managerService = useComfyManagerService()
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
@@ -136,14 +138,17 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
async (params: InstallPackParams, signal?: AbortSignal) => {
|
||||
if (!params.id) return
|
||||
|
||||
let actionDescription = 'Installing'
|
||||
let actionDescription = t('g.installing')
|
||||
if (installedPacksIds.value.has(params.id)) {
|
||||
const installedPack = installedPacks.value[params.id]
|
||||
|
||||
if (installedPack && installedPack.ver !== params.selected_version) {
|
||||
actionDescription = `Changing version from ${installedPack.ver} to ${params.selected_version}:`
|
||||
actionDescription = t('manager.changingVersion', {
|
||||
from: installedPack.ver,
|
||||
to: params.selected_version
|
||||
})
|
||||
} else {
|
||||
actionDescription = 'Enabling'
|
||||
actionDescription = t('g.enabling')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,14 +162,14 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installPack.clear()
|
||||
installPack.cancel()
|
||||
const task = () => managerService.uninstallPack(params, signal)
|
||||
enqueueTask(withLogs(task, `Uninstalling ${params.id}`))
|
||||
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
|
||||
}
|
||||
|
||||
const updatePack = useCachedRequest<ManagerPackInfo, void>(
|
||||
async (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
updateAllPacks.cancel()
|
||||
const task = () => managerService.updatePack(params, signal)
|
||||
enqueueTask(withLogs(task, `Updating ${params.id}`))
|
||||
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
@@ -172,14 +177,14 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
|
||||
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
|
||||
const task = () => managerService.updateAllPacks(params, signal)
|
||||
enqueueTask(withLogs(task, 'Updating all packs'))
|
||||
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
|
||||
},
|
||||
{ maxSize: 1 }
|
||||
)
|
||||
|
||||
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
|
||||
const task = () => managerService.disablePack(params, signal)
|
||||
enqueueTask(withLogs(task, `Disabling ${params.id}`))
|
||||
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
import { partition } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
@@ -5,7 +7,7 @@ import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const PACK_LIST_CACHE_SIZE = 20
|
||||
const PACK_BY_ID_CACHE_SIZE = 50
|
||||
const PACK_BY_ID_CACHE_SIZE = 64
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
type ListPacksParams = operations['listAllNodes']['parameters']['query']
|
||||
@@ -14,12 +16,21 @@ type ListPacksResult =
|
||||
type ComfyNode = components['schemas']['ComfyNode']
|
||||
type GetPackByIdPath = operations['getNode']['parameters']['path']['nodeId']
|
||||
|
||||
const isNodePack = (pack: NodePack | undefined): pack is NodePack => {
|
||||
return pack !== undefined && 'id' in pack
|
||||
}
|
||||
|
||||
/**
|
||||
* Store for managing remote custom nodes
|
||||
*/
|
||||
export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
const registryService = useComfyRegistryService()
|
||||
|
||||
let getPacksByIdController: AbortController | null = null
|
||||
const getPacksByIdCache = new QuickLRU<NodePack['id'], NodePack>({
|
||||
maxSize: PACK_BY_ID_CACHE_SIZE
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a list of all node packs from the registry
|
||||
*/
|
||||
@@ -39,6 +50,41 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
{ maxSize: PACK_BY_ID_CACHE_SIZE }
|
||||
)
|
||||
|
||||
/**
|
||||
* Get a list of packs by their IDs from the registry
|
||||
*/
|
||||
const getPacksByIds = async (ids: NodePack['id'][]): Promise<NodePack[]> => {
|
||||
const [cachedPacksIds, uncachedPacksIds] = partition(ids, (id) =>
|
||||
getPacksByIdCache.has(id)
|
||||
)
|
||||
|
||||
const resolvedPacks = cachedPacksIds
|
||||
.map((id) => getPacksByIdCache.get(id))
|
||||
.filter(isNodePack)
|
||||
|
||||
if (uncachedPacksIds.length) {
|
||||
getPacksByIdController = new AbortController()
|
||||
const uncachedPacks = await registryService.listAllPacks(
|
||||
{
|
||||
node_id: uncachedPacksIds.filter(
|
||||
(id): id is string => id !== undefined
|
||||
)
|
||||
},
|
||||
getPacksByIdController.signal
|
||||
)
|
||||
|
||||
const { nodes = [] } = uncachedPacks ?? {}
|
||||
nodes.forEach((pack) => {
|
||||
if (pack?.id) {
|
||||
getPacksByIdCache.set(pack.id, pack)
|
||||
resolvedPacks.push(pack)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return resolvedPacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node definitions for a pack
|
||||
*/
|
||||
@@ -63,11 +109,16 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
getNodeDefs.cancel()
|
||||
listAllPacks.cancel()
|
||||
getPackById.cancel()
|
||||
getPacksByIdController?.abort()
|
||||
}
|
||||
|
||||
return {
|
||||
listAllPacks,
|
||||
getPackById,
|
||||
getPacksByIds: {
|
||||
call: getPacksByIds,
|
||||
cancel: () => getPacksByIdController?.abort()
|
||||
},
|
||||
getNodeDefs,
|
||||
|
||||
clearCache,
|
||||
|
||||
@@ -3,29 +3,40 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
ExecutedWsMessage,
|
||||
ExecutingWsMessage,
|
||||
ExecutionCachedWsMessage,
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { ComfyWorkflow } from './workflowStore'
|
||||
|
||||
export interface QueuedPrompt {
|
||||
nodes: Record<string, boolean>
|
||||
/**
|
||||
* The nodes that are queued to be executed. The key is the node id and the
|
||||
* value is a boolean indicating if the node has been executed.
|
||||
*/
|
||||
nodes: Record<NodeId, boolean>
|
||||
/**
|
||||
* The workflow that is queued to be executed
|
||||
*/
|
||||
workflow?: ComfyWorkflow
|
||||
}
|
||||
|
||||
export const useExecutionStore = defineStore('execution', () => {
|
||||
const clientId = ref<string | null>(null)
|
||||
const activePromptId = ref<string | null>(null)
|
||||
const queuedPrompts = ref<Record<string, QueuedPrompt>>({})
|
||||
const executingNodeId = ref<string | null>(null)
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
const executingNodeId = ref<NodeId | null>(null)
|
||||
const executingNode = computed<ComfyNode | null>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -47,11 +58,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||
const executingNodeProgress = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
? Math.round(
|
||||
(_executingNodeProgress.value.value /
|
||||
_executingNodeProgress.value.max) *
|
||||
100
|
||||
)
|
||||
? _executingNodeProgress.value.value / _executingNodeProgress.value.max
|
||||
: null
|
||||
)
|
||||
|
||||
@@ -75,7 +82,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
if (!activePrompt.value) return 0
|
||||
const total = totalNodesToExecute.value
|
||||
const done = nodesExecuted.value
|
||||
return Math.round((done / total) * 100)
|
||||
return done / total
|
||||
})
|
||||
|
||||
function bindExecutionEvents() {
|
||||
@@ -91,6 +98,10 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.addEventListener('executing', handleExecuting as EventListener)
|
||||
api.addEventListener('progress', handleProgress as EventListener)
|
||||
api.addEventListener('status', handleStatus as EventListener)
|
||||
api.addEventListener(
|
||||
'execution_error',
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
function unbindExecutionEvents() {
|
||||
@@ -106,9 +117,14 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.removeEventListener('executing', handleExecuting as EventListener)
|
||||
api.removeEventListener('progress', handleProgress as EventListener)
|
||||
api.removeEventListener('status', handleStatus as EventListener)
|
||||
api.removeEventListener(
|
||||
'execution_error',
|
||||
handleExecutionError as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
|
||||
lastExecutionError.value = null
|
||||
activePromptId.value = e.detail.prompt_id
|
||||
queuedPrompts.value[activePromptId.value] ??= { nodes: {} }
|
||||
}
|
||||
@@ -125,7 +141,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
activePrompt.value.nodes[e.detail.node] = true
|
||||
}
|
||||
|
||||
function handleExecuting(e: CustomEvent<ExecutingWsMessage>) {
|
||||
function handleExecuting(e: CustomEvent<NodeId | null>) {
|
||||
// Clear the current node progress when a new node starts executing
|
||||
_executingNodeProgress.value = null
|
||||
|
||||
@@ -135,8 +151,8 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
}
|
||||
executingNodeId.value = e.detail ? String(e.detail) : null
|
||||
if (!executingNodeId.value) {
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
@@ -157,6 +173,10 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function handleExecutionError(e: CustomEvent<ExecutionErrorWsMessage>) {
|
||||
lastExecutionError.value = e.detail
|
||||
}
|
||||
|
||||
function storePrompt({
|
||||
nodes,
|
||||
id,
|
||||
@@ -185,14 +205,49 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
return {
|
||||
isIdle,
|
||||
clientId,
|
||||
/**
|
||||
* The id of the prompt that is currently being executed
|
||||
*/
|
||||
activePromptId,
|
||||
/**
|
||||
* The queued prompts
|
||||
*/
|
||||
queuedPrompts,
|
||||
/**
|
||||
* The node errors from the previous execution.
|
||||
*/
|
||||
lastNodeErrors,
|
||||
/**
|
||||
* The error from the previous execution.
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
activePrompt,
|
||||
/**
|
||||
* The total number of nodes to execute
|
||||
*/
|
||||
totalNodesToExecute,
|
||||
/**
|
||||
* The number of nodes that have been executed
|
||||
*/
|
||||
nodesExecuted,
|
||||
/**
|
||||
* The progress of the execution
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
|
||||
@@ -14,19 +14,19 @@ import type {
|
||||
ComfyNodeDef as ComfyNodeDefV1,
|
||||
ComfyOutputTypesSpec as ComfyOutputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import {
|
||||
NodeSearchService,
|
||||
type SearchAuxScore
|
||||
} from '@/services/nodeSearchService'
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import {
|
||||
type NodeSource,
|
||||
NodeSourceType,
|
||||
getNodeSource
|
||||
} from '@/types/nodeSource'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import type { FuseSearchable, SearchAuxScore } from '@/utils/fuseUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
export class ComfyNodeDefImpl implements ComfyNodeDefV1, ComfyNodeDefV2 {
|
||||
export class ComfyNodeDefImpl
|
||||
implements ComfyNodeDefV1, ComfyNodeDefV2, FuseSearchable
|
||||
{
|
||||
// ComfyNodeDef fields (V1)
|
||||
readonly name: string
|
||||
readonly display_name: string
|
||||
@@ -140,6 +140,18 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDefV1> = {
|
||||
python_module: 'nodes',
|
||||
description: 'Primitive values like numbers, strings, and booleans.'
|
||||
},
|
||||
Reroute: {
|
||||
name: 'Reroute',
|
||||
display_name: 'Reroute',
|
||||
category: 'utils',
|
||||
input: { required: { '': ['*', {}] }, optional: {} },
|
||||
output: ['*'],
|
||||
output_name: [''],
|
||||
output_is_list: [false],
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
description: 'Reroute the connection to another node.'
|
||||
},
|
||||
Note: {
|
||||
name: 'Note',
|
||||
display_name: 'Note',
|
||||
|
||||
2
src/types/litegraph-augmentation.d.ts
vendored
2
src/types/litegraph-augmentation.d.ts
vendored
@@ -186,7 +186,7 @@ declare module '@comfyorg/litegraph' {
|
||||
* We should remove this hacky solution once we have a proper solution.
|
||||
*/
|
||||
interface INodeOutputSlot {
|
||||
widget?: IWidget
|
||||
widget?: { name: string; [key: symbol]: unknown }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
81
src/utils/errorReportUtil.ts
Normal file
81
src/utils/errorReportUtil.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { ISerialisedGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
export interface ErrorReportData {
|
||||
exceptionType: string
|
||||
exceptionMessage: string
|
||||
systemStats: SystemStats
|
||||
serverLogs: string
|
||||
workflow: ISerialisedGraph
|
||||
|
||||
traceback?: string
|
||||
nodeId?: NodeId
|
||||
nodeType?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a report for an error.
|
||||
* @param error - The error to report.
|
||||
* @returns The report.
|
||||
*/
|
||||
export function generateErrorReport(error: ErrorReportData): string {
|
||||
// The default JSON workflow has about 3000 characters.
|
||||
const MAX_JSON_LENGTH = 20000
|
||||
const workflowJSONString = JSON.stringify(error.workflow)
|
||||
const workflowText =
|
||||
workflowJSONString.length > MAX_JSON_LENGTH
|
||||
? 'Workflow too large. Please manually upload the workflow from local file system.'
|
||||
: workflowJSONString
|
||||
const systemStats = error.systemStats
|
||||
|
||||
return `
|
||||
# ComfyUI Error Report
|
||||
${
|
||||
error
|
||||
? `## Error Details
|
||||
- **Node ID:** ${error.nodeId || 'N/A'}
|
||||
- **Node Type:** ${error.nodeType || 'N/A'}
|
||||
- **Exception Type:** ${error.exceptionType || 'N/A'}
|
||||
- **Exception Message:** ${error.exceptionMessage || 'N/A'}
|
||||
## Stack Trace
|
||||
\`\`\`
|
||||
${error.traceback || 'No stack trace available'}
|
||||
\`\`\``
|
||||
: ''
|
||||
}
|
||||
## System Information
|
||||
- **ComfyUI Version:** ${systemStats.system.comfyui_version}
|
||||
- **Arguments:** ${systemStats.system.argv.join(' ')}
|
||||
- **OS:** ${systemStats.system.os}
|
||||
- **Python Version:** ${systemStats.system.python_version}
|
||||
- **Embedded Python:** ${systemStats.system.embedded_python}
|
||||
- **PyTorch Version:** ${systemStats.system.pytorch_version}
|
||||
## Devices
|
||||
${systemStats.devices
|
||||
.map(
|
||||
(device) => `
|
||||
- **Name:** ${device.name}
|
||||
- **Type:** ${device.type}
|
||||
- **VRAM Total:** ${device.vram_total}
|
||||
- **VRAM Free:** ${device.vram_free}
|
||||
- **Torch VRAM Total:** ${device.torch_vram_total}
|
||||
- **Torch VRAM Free:** ${device.torch_vram_free}
|
||||
`
|
||||
)
|
||||
.join('\n')}
|
||||
## Logs
|
||||
\`\`\`
|
||||
${error.serverLogs}
|
||||
\`\`\`
|
||||
## Attached Workflow
|
||||
Please make sure that workflow does not contain any sensitive information such as API keys or passwords.
|
||||
\`\`\`
|
||||
${workflowText}
|
||||
\`\`\`
|
||||
|
||||
## Additional Context
|
||||
(Please add any additional context or steps to reproduce the error here)
|
||||
`
|
||||
}
|
||||
212
src/utils/fuseUtil.ts
Normal file
212
src/utils/fuseUtil.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import Fuse, { FuseOptionKey, FuseSearchOptions, IFuseOptions } from 'fuse.js'
|
||||
|
||||
export type SearchAuxScore = number[]
|
||||
|
||||
export interface FuseFilterWithValue<T, O = string> {
|
||||
filterDef: FuseFilter<T, O>
|
||||
value: O
|
||||
}
|
||||
|
||||
export class FuseFilter<T, O = string> {
|
||||
public readonly fuseSearch: FuseSearch<O>
|
||||
/** The unique identifier for the filter. */
|
||||
public readonly id: string
|
||||
/** The name of the filter for display purposes. */
|
||||
public readonly name: string
|
||||
/** The sequence of characters to invoke the filter. */
|
||||
public readonly invokeSequence: string
|
||||
/** A function that returns the options for the filter. */
|
||||
public readonly getItemOptions: (item: T) => O[]
|
||||
|
||||
constructor(
|
||||
data: T[],
|
||||
options: {
|
||||
id: string
|
||||
name: string
|
||||
invokeSequence: string
|
||||
getItemOptions: (item: T) => O[]
|
||||
fuseOptions?: IFuseOptions<O>
|
||||
}
|
||||
) {
|
||||
this.id = options.id
|
||||
this.name = options.name
|
||||
this.invokeSequence = options.invokeSequence
|
||||
this.getItemOptions = options.getItemOptions
|
||||
|
||||
this.fuseSearch = new FuseSearch(this.getAllNodeOptions(data), {
|
||||
fuseOptions: options.fuseOptions
|
||||
})
|
||||
}
|
||||
|
||||
public getAllNodeOptions(data: T[]): O[] {
|
||||
const options = new Set<O>()
|
||||
for (const item of data) {
|
||||
for (const option of this.getItemOptions(item)) {
|
||||
options.add(option)
|
||||
}
|
||||
}
|
||||
return Array.from(options)
|
||||
}
|
||||
|
||||
public matches(
|
||||
item: T,
|
||||
value: O,
|
||||
extraOptions: {
|
||||
wildcard?: O
|
||||
} = {}
|
||||
): boolean {
|
||||
const { wildcard } = extraOptions
|
||||
|
||||
if (wildcard && value === wildcard) {
|
||||
return true
|
||||
}
|
||||
const options = this.getItemOptions(item)
|
||||
return (
|
||||
options.includes(value) ||
|
||||
(!!wildcard && options.some((option) => option === wildcard))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export interface FuseSearchable {
|
||||
postProcessSearchScores: (scores: SearchAuxScore) => SearchAuxScore
|
||||
}
|
||||
|
||||
function isFuseSearchable(item: any): item is FuseSearchable {
|
||||
return 'postProcessSearchScores' in item
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around Fuse.js that provides a more type-safe API.
|
||||
*/
|
||||
export class FuseSearch<T> {
|
||||
public readonly fuse: Fuse<T>
|
||||
public readonly keys: FuseOptionKey<T>[]
|
||||
public readonly data: T[]
|
||||
public readonly advancedScoring: boolean
|
||||
|
||||
constructor(
|
||||
data: T[],
|
||||
options: {
|
||||
fuseOptions?: IFuseOptions<T>
|
||||
createIndex?: boolean
|
||||
advancedScoring?: boolean
|
||||
}
|
||||
) {
|
||||
const { fuseOptions, createIndex = true, advancedScoring = false } = options
|
||||
|
||||
this.data = data
|
||||
this.keys = fuseOptions?.keys ?? []
|
||||
this.advancedScoring = advancedScoring
|
||||
const index =
|
||||
createIndex && this.keys.length
|
||||
? Fuse.createIndex(this.keys, data)
|
||||
: undefined
|
||||
this.fuse = new Fuse(data, fuseOptions, index)
|
||||
}
|
||||
|
||||
public search(query: string, options?: FuseSearchOptions): T[] {
|
||||
const fuseResult = !query
|
||||
? this.data.map((x) => ({ item: x, score: 0 }))
|
||||
: this.fuse.search(query, options)
|
||||
|
||||
if (!this.advancedScoring) {
|
||||
return fuseResult.map((x) => x.item)
|
||||
}
|
||||
|
||||
const aux = fuseResult
|
||||
.map((x) => ({
|
||||
item: x.item,
|
||||
scores: this.calcAuxScores(
|
||||
query.toLocaleLowerCase(),
|
||||
x.item,
|
||||
x.score ?? 0
|
||||
)
|
||||
}))
|
||||
.sort((a, b) => this.compareAux(a.scores, b.scores))
|
||||
|
||||
return aux.map((x) => x.item)
|
||||
}
|
||||
|
||||
public calcAuxScores(query: string, entry: T, score: number): SearchAuxScore {
|
||||
let values: string[] = []
|
||||
if (typeof entry === 'string') {
|
||||
values = [entry]
|
||||
} else if (typeof entry === 'object' && entry !== null) {
|
||||
values = this.keys
|
||||
.map((x) => entry[x as keyof T])
|
||||
.filter((x) => typeof x === 'string') as string[]
|
||||
}
|
||||
const scores = values.map((x) => this.calcAuxSingle(query, x, score))
|
||||
let result = scores.sort(this.compareAux)[0]
|
||||
|
||||
const deprecated = values.some((x) =>
|
||||
x.toLocaleLowerCase().includes('deprecated')
|
||||
)
|
||||
result[0] += deprecated && result[0] !== 0 ? 5 : 0
|
||||
if (isFuseSearchable(entry)) {
|
||||
result = entry.postProcessSearchScores(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public calcAuxSingle(
|
||||
query: string,
|
||||
item: string,
|
||||
score: number
|
||||
): SearchAuxScore {
|
||||
const itemWords = item
|
||||
.split(/ |\b|(?<=[a-z])(?=[A-Z])|(?=[A-Z][a-z])/)
|
||||
.map((x) => x.toLocaleLowerCase())
|
||||
const queryParts = query.split(' ')
|
||||
item = item.toLocaleLowerCase()
|
||||
|
||||
let main = 9
|
||||
let aux1 = 0
|
||||
let aux2 = 0
|
||||
|
||||
if (item == query) {
|
||||
main = 0
|
||||
} else if (item.startsWith(query)) {
|
||||
main = 1
|
||||
aux2 = item.length
|
||||
} else if (itemWords.includes(query)) {
|
||||
main = 2
|
||||
aux1 = item.indexOf(query) + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (item.includes(query)) {
|
||||
main = 3
|
||||
aux1 = item.indexOf(query) + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (queryParts.every((x) => itemWords.includes(x))) {
|
||||
const indexes = queryParts.map((x) => itemWords.indexOf(x))
|
||||
const min = Math.min(...indexes)
|
||||
const max = Math.max(...indexes)
|
||||
main = 4
|
||||
aux1 = max - min + max * 0.5 + item.length * 0.5
|
||||
aux2 = item.length
|
||||
} else if (queryParts.every((x) => item.includes(x))) {
|
||||
const min = Math.min(...queryParts.map((x) => item.indexOf(x)))
|
||||
const max = Math.max(...queryParts.map((x) => item.indexOf(x) + x.length))
|
||||
main = 5
|
||||
aux1 = max - min + max * 0.5 + item.length * 0.5
|
||||
aux2 = item.length
|
||||
}
|
||||
|
||||
const lengthPenalty =
|
||||
0.2 *
|
||||
(1 -
|
||||
Math.min(item.length, query.length) /
|
||||
Math.max(item.length, query.length))
|
||||
return [main, aux1, aux2, score + lengthPenalty]
|
||||
}
|
||||
|
||||
public compareAux(a: SearchAuxScore, b: SearchAuxScore) {
|
||||
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return a[i] - b[i]
|
||||
}
|
||||
}
|
||||
return a.length - b.length
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { ColorOption } from '@comfyorg/litegraph'
|
||||
import type { ColorOption, LGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphGroup, LGraphNode, isColorable } from '@comfyorg/litegraph'
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
|
||||
import type { ComfyNode } from '@/schemas/comfyWorkflowSchema'
|
||||
|
||||
type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined }
|
||||
type VideoNode = LGraphNode & {
|
||||
videoContainer: HTMLElement | undefined
|
||||
@@ -70,3 +72,40 @@ export function executeWidgetsCallback(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the nodes with non-serialized widget values removed
|
||||
*/
|
||||
export function filterSerializedWidgetValues(
|
||||
nodes: ComfyNode[],
|
||||
graph: LGraph
|
||||
): ComfyNode[] {
|
||||
if (!graph) return nodes
|
||||
|
||||
return nodes.map((node) => {
|
||||
if (!node.widgets_values) return node
|
||||
|
||||
const graphNode = graph.getNodeById(node.id)
|
||||
if (!graphNode?.widgets) return node
|
||||
|
||||
const filteredNode = { ...node }
|
||||
const serializedValues = []
|
||||
|
||||
for (let i = 0; i < graphNode.widgets.length; i++) {
|
||||
const widget = graphNode.widgets[i]
|
||||
|
||||
// Skip if widget is not serialized
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
const value = Array.isArray(node.widgets_values)
|
||||
? node.widgets_values[i]
|
||||
: node.widgets_values[widget.name || i.toString()]
|
||||
serializedValues.push(
|
||||
typeof value === 'object' && value !== null ? value.value : value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
filteredNode.widgets_values = serializedValues
|
||||
return filteredNode
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import type {
|
||||
ComfyLink,
|
||||
ComfyLinkObject,
|
||||
ComfyNode,
|
||||
NodeId,
|
||||
Reroute,
|
||||
@@ -17,20 +17,26 @@ type LinkExtension = {
|
||||
parentId: number
|
||||
}
|
||||
|
||||
type RerouteEntry = {
|
||||
reroute: Reroute
|
||||
rerouteNode: RerouteNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies all legacy Reroute nodes in a workflow
|
||||
*/
|
||||
function findLegacyRerouteNodes(workflow: WorkflowJSON04): RerouteNode[] {
|
||||
export function findLegacyRerouteNodes(
|
||||
workflow: WorkflowJSON04
|
||||
): RerouteNode[] {
|
||||
return workflow.nodes.filter(
|
||||
(node) => node.type === 'Reroute'
|
||||
) as RerouteNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the workflow has no native reroutes
|
||||
*/
|
||||
export function noNativeReroutes(workflow: WorkflowJSON04): boolean {
|
||||
return (
|
||||
!workflow.extra?.reroutes?.length && !workflow.extra?.linkExtensions?.length
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the center position of a node
|
||||
*/
|
||||
@@ -38,156 +44,269 @@ function getNodeCenter(node: ComfyNode): [number, number] {
|
||||
return [node.pos[0] + node.size[0] / 2, node.pos[1] + node.size[1] / 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates native reroute points from legacy Reroute nodes
|
||||
*/
|
||||
export function createReroutePoints(
|
||||
rerouteNodes: RerouteNode[]
|
||||
): Map<NodeId, RerouteEntry> {
|
||||
const rerouteMap = new Map<NodeId, RerouteEntry>()
|
||||
|
||||
let rerouteIdCounter = 1
|
||||
rerouteNodes.forEach((node) => {
|
||||
const rerouteId = rerouteIdCounter++
|
||||
rerouteMap.set(node.id, {
|
||||
reroute: {
|
||||
id: rerouteId,
|
||||
pos: getNodeCenter(node),
|
||||
linkIds: []
|
||||
},
|
||||
rerouteNode: node
|
||||
})
|
||||
})
|
||||
|
||||
return rerouteMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new links and link extensions for the migrated workflow
|
||||
*/
|
||||
export function createNewLinks(
|
||||
workflow: WorkflowJSON04,
|
||||
rerouteMap: Map<NodeId, RerouteEntry>
|
||||
): {
|
||||
nodes: ComfyNode[]
|
||||
reroutes: Reroute[]
|
||||
links: ComfyLink[]
|
||||
class ConversionContext {
|
||||
nodeById: Record<NodeId, ComfyNode>
|
||||
linkById: Record<number, ComfyLinkObject>
|
||||
rerouteById: Record<number, Reroute>
|
||||
rerouteByNodeId: Record<NodeId, Reroute>
|
||||
linkExtensions: LinkExtension[]
|
||||
} {
|
||||
const nodeById = _.keyBy(
|
||||
workflow.nodes.filter((node) => node.type !== 'Reroute').map(_.cloneDeep),
|
||||
'id'
|
||||
)
|
||||
const links: ComfyLink[] = []
|
||||
const linkExtensions: LinkExtension[] = []
|
||||
|
||||
const rerouteMapByRerouteId = new Map<number, RerouteEntry>(
|
||||
Array.from(rerouteMap.values()).map((entry) => [entry.reroute.id, entry])
|
||||
)
|
||||
const linksMap = new Map<number, ComfyLink>(
|
||||
Array.from(workflow.links).map((link) => [link[0], link])
|
||||
)
|
||||
/** Reroutes that has at least a valid link pass through it */
|
||||
validReroutes: Set<Reroute>
|
||||
|
||||
// Process each link in the workflow
|
||||
for (const link of workflow.links) {
|
||||
const [
|
||||
linkId,
|
||||
sourceNodeId,
|
||||
_sourceSlot,
|
||||
targetNodeId,
|
||||
_targetSlot,
|
||||
_dataType
|
||||
] = link
|
||||
#rerouteIdCounter = 0
|
||||
|
||||
// Check if this link connects to or from a reroute node
|
||||
const sourceEntry = rerouteMap.get(sourceNodeId)
|
||||
const targetEntry = rerouteMap.get(targetNodeId)
|
||||
const sourceIsReroute = !!sourceEntry
|
||||
const targetIsReroute = !!targetEntry
|
||||
constructor(public workflow: WorkflowJSON04) {
|
||||
this.nodeById = _.keyBy(workflow.nodes.map(_.cloneDeep), 'id')
|
||||
this.linkById = _.keyBy(
|
||||
workflow.links.map((l) => ({
|
||||
id: l[0],
|
||||
origin_id: l[1],
|
||||
origin_slot: l[2],
|
||||
target_id: l[3],
|
||||
target_slot: l[4],
|
||||
type: l[5]
|
||||
})),
|
||||
'id'
|
||||
)
|
||||
|
||||
if (!sourceIsReroute && !targetIsReroute) {
|
||||
// If neither end is a reroute, keep the link as is
|
||||
links.push(link)
|
||||
} else if (sourceIsReroute && !targetIsReroute) {
|
||||
// This is a link from a reroute node to a regular node
|
||||
linkExtensions.push({
|
||||
id: linkId,
|
||||
parentId: sourceEntry.reroute.id
|
||||
})
|
||||
} else if (sourceIsReroute && targetIsReroute) {
|
||||
targetEntry.reroute.parentId = sourceEntry.reroute.id
|
||||
const reroutes = findLegacyRerouteNodes(workflow).map((node, index) => ({
|
||||
nodeId: node.id,
|
||||
id: index + 1,
|
||||
pos: getNodeCenter(node),
|
||||
linkIds: []
|
||||
}))
|
||||
this.#rerouteIdCounter = reroutes.length + 1
|
||||
|
||||
this.rerouteByNodeId = _.keyBy(reroutes, 'nodeId')
|
||||
this.rerouteById = _.keyBy(reroutes, 'id')
|
||||
|
||||
this.linkExtensions = []
|
||||
this.validReroutes = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the chain of reroute nodes leading to the given node
|
||||
*/
|
||||
#getRerouteChain(node: RerouteNode): RerouteNode[] {
|
||||
const nodes: RerouteNode[] = []
|
||||
let currentNode: RerouteNode = node
|
||||
while (currentNode?.type === 'Reroute') {
|
||||
nodes.push(currentNode)
|
||||
const inputLink: ComfyLinkObject | undefined =
|
||||
this.linkById[currentNode.inputs?.[0]?.link ?? 0]
|
||||
|
||||
if (!inputLink) {
|
||||
break
|
||||
}
|
||||
|
||||
currentNode = this.nodeById[inputLink.origin_id] as RerouteNode
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
#connectRerouteChain(rerouteNodes: RerouteNode[]): Reroute[] {
|
||||
const reroutes = rerouteNodes.map((node) => this.rerouteByNodeId[node.id])
|
||||
for (const reroute of reroutes) {
|
||||
this.validReroutes.add(reroute)
|
||||
}
|
||||
|
||||
for (let i = 0; i < rerouteNodes.length - 1; i++) {
|
||||
const to = reroutes[i]
|
||||
const from = reroutes[i + 1]
|
||||
to.parentId = from.id
|
||||
}
|
||||
|
||||
return reroutes
|
||||
}
|
||||
|
||||
#createNewLink(
|
||||
startingLink: ComfyLinkObject,
|
||||
endingLink: ComfyLinkObject,
|
||||
rerouteNodes: RerouteNode[]
|
||||
): ComfyLinkObject {
|
||||
if (rerouteNodes.length === 0) {
|
||||
throw new Error('No reroute nodes found')
|
||||
}
|
||||
|
||||
const reroute = this.rerouteByNodeId[rerouteNodes[0].id]
|
||||
this.linkExtensions.push({
|
||||
id: endingLink.id,
|
||||
parentId: reroute.id
|
||||
})
|
||||
|
||||
const reroutes = this.#connectRerouteChain(rerouteNodes)
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds ??= []
|
||||
reroute.linkIds.push(endingLink.id)
|
||||
delete reroute.floating
|
||||
}
|
||||
|
||||
return {
|
||||
id: endingLink.id,
|
||||
origin_id: startingLink.origin_id,
|
||||
origin_slot: startingLink.origin_slot,
|
||||
target_id: endingLink.target_id,
|
||||
target_slot: endingLink.target_slot,
|
||||
type: endingLink.type
|
||||
}
|
||||
}
|
||||
|
||||
// Populate linkIds on reroute nodes
|
||||
// Remove all partially connected reroutes
|
||||
const validLinkExtensions: LinkExtension[] = []
|
||||
const validReroutes: Set<Reroute> = new Set()
|
||||
|
||||
for (const linkExtension of linkExtensions) {
|
||||
let entry = rerouteMapByRerouteId.get(linkExtension.parentId)
|
||||
const chainedReroutes: Reroute[] = []
|
||||
|
||||
while (entry) {
|
||||
const reroute = entry.reroute
|
||||
reroute.linkIds ??= []
|
||||
reroute.linkIds.push(linkExtension.id)
|
||||
chainedReroutes.push(reroute)
|
||||
|
||||
if (reroute.parentId) {
|
||||
entry = rerouteMapByRerouteId.get(reroute.parentId)
|
||||
} else {
|
||||
// Last reroute in the chain
|
||||
const rerouteNode = entry.rerouteNode
|
||||
const rerouteInputLink = linksMap.get(
|
||||
rerouteNode?.inputs?.[0]?.link ?? -1
|
||||
)
|
||||
const rerouteOutputLink = linksMap.get(linkExtension.id)
|
||||
|
||||
if (rerouteInputLink && rerouteOutputLink) {
|
||||
const [_, sourceNodeId, sourceSlot] = rerouteInputLink
|
||||
const [linkId, __, ___, targetNodeId, targetSlot, dataType] =
|
||||
rerouteOutputLink
|
||||
|
||||
links.push([
|
||||
linkId,
|
||||
sourceNodeId,
|
||||
sourceSlot,
|
||||
targetNodeId,
|
||||
targetSlot,
|
||||
dataType
|
||||
])
|
||||
|
||||
validLinkExtensions.push(linkExtension)
|
||||
chainedReroutes.forEach((reroute) => validReroutes.add(reroute))
|
||||
|
||||
// Update source node's output slot's link ids to point to the new link.
|
||||
const sourceNode = nodeById[sourceNodeId]
|
||||
if (!sourceNode) {
|
||||
throw new Error(
|
||||
`Corrupted workflow: Source node ${sourceNodeId} not found`
|
||||
)
|
||||
}
|
||||
const outputSlot = sourceNode.outputs?.[sourceSlot]
|
||||
if (!outputSlot) {
|
||||
throw new Error(
|
||||
`Corrupted workflow: Output slot ${sourceSlot} not found`
|
||||
)
|
||||
}
|
||||
outputSlot.links = outputSlot.links?.map((l) =>
|
||||
l === rerouteInputLink[0] ? linkId : l
|
||||
)
|
||||
#createNewInputFloatingLink(
|
||||
endingLink: ComfyLinkObject,
|
||||
rerouteNodes: RerouteNode[]
|
||||
): ComfyLinkObject {
|
||||
const reroutes = this.#connectRerouteChain(rerouteNodes)
|
||||
for (const reroute of reroutes) {
|
||||
if (!reroute.linkIds?.length) {
|
||||
reroute.floating = {
|
||||
slotType: 'input'
|
||||
}
|
||||
entry = undefined
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: this.#rerouteIdCounter++,
|
||||
origin_id: -1,
|
||||
origin_slot: -1,
|
||||
target_id: endingLink.target_id,
|
||||
target_slot: endingLink.target_slot,
|
||||
type: endingLink.type,
|
||||
parentId: reroutes[0].id
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: Object.values(nodeById),
|
||||
links,
|
||||
linkExtensions: validLinkExtensions,
|
||||
reroutes: Array.from(validReroutes)
|
||||
#createNewOutputFloatingLink(
|
||||
startingLink: ComfyLinkObject,
|
||||
rerouteNodes: RerouteNode[]
|
||||
): ComfyLinkObject {
|
||||
const reroutes = this.#connectRerouteChain(rerouteNodes)
|
||||
for (const reroute of reroutes) {
|
||||
if (!reroute.linkIds?.length) {
|
||||
reroute.floating = {
|
||||
slotType: 'output'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.#rerouteIdCounter++,
|
||||
origin_id: startingLink.origin_id,
|
||||
origin_slot: startingLink.origin_slot,
|
||||
target_id: -1,
|
||||
target_slot: -1,
|
||||
type: startingLink.type,
|
||||
parentId: reroutes[0].id
|
||||
}
|
||||
}
|
||||
|
||||
#reconnectLinks(nodes: ComfyNode[], links: ComfyLinkObject[]): void {
|
||||
// Remove all existing links on sockets
|
||||
for (const node of nodes) {
|
||||
for (const input of node.inputs ?? []) {
|
||||
input.link = null
|
||||
}
|
||||
for (const output of node.outputs ?? []) {
|
||||
output.links = []
|
||||
}
|
||||
}
|
||||
|
||||
const nodesById = _.keyBy(nodes, 'id')
|
||||
|
||||
// Reconnect the links
|
||||
for (const link of links) {
|
||||
const sourceNode = nodesById[link.origin_id]
|
||||
sourceNode.outputs![link.origin_slot]!.links!.push(link.id)
|
||||
|
||||
const targetNode = nodesById[link.target_id]
|
||||
targetNode.inputs![link.target_slot]!.link = link.id
|
||||
}
|
||||
}
|
||||
|
||||
migrateReroutes(): WorkflowJSON04 {
|
||||
const links: ComfyLinkObject[] = []
|
||||
const floatingLinks: ComfyLinkObject[] = []
|
||||
|
||||
const endingLinks: ComfyLinkObject[] = []
|
||||
|
||||
for (const link of Object.values(this.linkById)) {
|
||||
const sourceIsReroute = !!this.rerouteByNodeId[link.origin_id]
|
||||
const targetIsReroute = !!this.rerouteByNodeId[link.target_id]
|
||||
|
||||
// Process links that are not connected to reroute nodes
|
||||
if (!sourceIsReroute && !targetIsReroute) {
|
||||
links.push(link)
|
||||
} else if (sourceIsReroute && !targetIsReroute) {
|
||||
endingLinks.push(link)
|
||||
}
|
||||
}
|
||||
|
||||
for (const endingLink of endingLinks) {
|
||||
const endingRerouteNode = this.nodeById[
|
||||
endingLink.origin_id
|
||||
] as RerouteNode
|
||||
const rerouteNodes = this.#getRerouteChain(endingRerouteNode)
|
||||
const startingLink =
|
||||
this.linkById[
|
||||
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
|
||||
]
|
||||
if (startingLink) {
|
||||
// Valid link found, create a new link
|
||||
links.push(this.#createNewLink(startingLink, endingLink, rerouteNodes))
|
||||
} else {
|
||||
// Floating link found, create a new floating link
|
||||
floatingLinks.push(
|
||||
this.#createNewInputFloatingLink(endingLink, rerouteNodes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const floatingEndingRerouteNodes = Object.keys(this.rerouteByNodeId)
|
||||
.map((nodeId) => this.nodeById[nodeId] as RerouteNode)
|
||||
.filter((rerouteNode) => {
|
||||
const output = rerouteNode.outputs?.[0]
|
||||
if (!output) return false
|
||||
return !output.links?.length
|
||||
})
|
||||
|
||||
for (const rerouteNode of floatingEndingRerouteNodes) {
|
||||
const rerouteNodes = this.#getRerouteChain(rerouteNode)
|
||||
const startingLink =
|
||||
this.linkById[
|
||||
rerouteNodes[rerouteNodes.length - 1]?.inputs?.[0]?.link ?? -1
|
||||
]
|
||||
if (startingLink) {
|
||||
floatingLinks.push(
|
||||
this.#createNewOutputFloatingLink(startingLink, rerouteNodes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = Object.values(this.nodeById).filter(
|
||||
(node) => node.type !== 'Reroute'
|
||||
)
|
||||
this.#reconnectLinks(nodes, links)
|
||||
|
||||
return {
|
||||
...this.workflow,
|
||||
nodes,
|
||||
links: links.map((link) => [
|
||||
link.id,
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot,
|
||||
link.type
|
||||
]),
|
||||
floatingLinks: floatingLinks.length > 0 ? floatingLinks : undefined,
|
||||
extra: {
|
||||
...this.workflow.extra,
|
||||
reroutes: Array.from(this.validReroutes).map(
|
||||
(reroute) => _.omit(reroute, 'nodeId') as Reroute
|
||||
),
|
||||
linkExtensions: this.linkExtensions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,20 +332,6 @@ export const migrateLegacyRerouteNodes = (
|
||||
newWorkflow.extra = {}
|
||||
}
|
||||
|
||||
// Create native reroute points
|
||||
const rerouteMap = createReroutePoints(legacyRerouteNodes)
|
||||
|
||||
// Create new links and link extensions
|
||||
const { nodes, links, linkExtensions, reroutes } = createNewLinks(
|
||||
workflow,
|
||||
rerouteMap
|
||||
)
|
||||
|
||||
// Update the workflow
|
||||
newWorkflow.links = links
|
||||
newWorkflow.nodes = nodes
|
||||
newWorkflow.extra.reroutes = reroutes
|
||||
newWorkflow.extra.linkExtensions = linkExtensions
|
||||
|
||||
return newWorkflow
|
||||
const context = new ConversionContext(newWorkflow)
|
||||
return context.migrateReroutes()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
</div>
|
||||
|
||||
<GlobalToast />
|
||||
<RerouteMigrationToast />
|
||||
<UnloadWindowConfirmDialog v-if="!isElectron()" />
|
||||
<BrowserTabTitle />
|
||||
<MenuHamburger />
|
||||
@@ -31,6 +32,7 @@ import MenuHamburger from '@/components/MenuHamburger.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'
|
||||
import TopMenubar from '@/components/topbar/TopMenubar.vue'
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
@@ -232,7 +234,7 @@ const onGraphReady = () => {
|
||||
// Node defs now available after comfyApp.setup.
|
||||
// Explicitly initialize nodeSearchService to avoid indexing delay when
|
||||
// node search is triggered
|
||||
useNodeDefStore().nodeSearchService.endsWithFilterStartSequence('')
|
||||
useNodeDefStore().nodeSearchService.searchNode('')
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
|
||||
describe('useChainCallback', () => {
|
||||
it('preserves "this" context in original callback and chained callbacks', () => {
|
||||
class TestClass {
|
||||
value = 'test'
|
||||
|
||||
constructor() {
|
||||
this.method = useChainCallback(this.method, function (this: TestClass) {
|
||||
expect(this.value).toBe('test')
|
||||
})
|
||||
}
|
||||
|
||||
method() {
|
||||
expect(this.value).toBe('test')
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass()
|
||||
instance.method()
|
||||
})
|
||||
|
||||
it('handles undefined original callback', () => {
|
||||
const context = { value: 'test' }
|
||||
const chainedFn = useChainCallback(
|
||||
undefined,
|
||||
function (this: typeof context) {
|
||||
expect(this.value).toBe('test')
|
||||
}
|
||||
)
|
||||
|
||||
chainedFn.call(context)
|
||||
})
|
||||
|
||||
it('passes arguments to all callbacks', () => {
|
||||
const originalCalls: number[] = []
|
||||
const chainedCalls: number[] = []
|
||||
|
||||
const original = function (this: unknown, num: number) {
|
||||
originalCalls.push(num)
|
||||
}
|
||||
|
||||
const chained = function (this: unknown, num: number) {
|
||||
chainedCalls.push(num)
|
||||
}
|
||||
|
||||
const chainedFn = useChainCallback(original, chained)
|
||||
chainedFn(42)
|
||||
|
||||
expect(originalCalls).toEqual([42])
|
||||
expect(chainedCalls).toEqual([42])
|
||||
})
|
||||
})
|
||||
@@ -64,12 +64,14 @@ const EXAMPLE_NODE_DEFS: ComfyNodeDefImpl[] = (
|
||||
describe('nodeSearchService', () => {
|
||||
it('searches with input filter', () => {
|
||||
const service = new NodeSearchService(EXAMPLE_NODE_DEFS)
|
||||
const inputFilter = service.getFilterById('input')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(service.searchNode('L', [[inputFilter, 'LATENT']])).toHaveLength(1)
|
||||
const inputFilter = service.inputTypeFilter
|
||||
expect(
|
||||
service.searchNode('L', [{ filterDef: inputFilter, value: 'LATENT' }])
|
||||
).toHaveLength(1)
|
||||
// Wildcard should match all.
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(service.searchNode('L', [[inputFilter, '*']])).toHaveLength(2)
|
||||
expect(
|
||||
service.searchNode('L', [{ filterDef: inputFilter, value: '*' }])
|
||||
).toHaveLength(2)
|
||||
expect(service.searchNode('L')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,17 @@ vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: vi.fn((key) => key)
|
||||
}),
|
||||
createI18n: vi.fn(() => ({
|
||||
global: {
|
||||
t: vi.fn((key) => key)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
interface EnabledDisabledTestCase {
|
||||
desc: string
|
||||
installed: Record<string, ManagerPackInstalled>
|
||||
|
||||
@@ -26,6 +26,38 @@ const mockNodePack: components['schemas']['Node'] = {
|
||||
}
|
||||
}
|
||||
|
||||
const mockNodePack2: components['schemas']['Node'] = {
|
||||
id: 'test-pack-id-2',
|
||||
name: 'Test Pack 2',
|
||||
description: 'A second test node pack',
|
||||
downloads: 1000,
|
||||
publisher: {
|
||||
id: 'test-publisher',
|
||||
name: 'Test Publisher'
|
||||
},
|
||||
latest_version: {
|
||||
id: 'test-version',
|
||||
version: '1.0.0',
|
||||
createdAt: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
const mockNodePack3: components['schemas']['Node'] = {
|
||||
id: 'test-pack-id-3',
|
||||
name: 'Test Pack 3',
|
||||
description: 'A third test node pack',
|
||||
downloads: 1000,
|
||||
publisher: {
|
||||
id: 'test-publisher',
|
||||
name: 'Test Publisher'
|
||||
},
|
||||
latest_version: {
|
||||
id: 'test-version',
|
||||
version: '1.0.0',
|
||||
createdAt: '2023-01-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
const mockListResult: operations['listAllNodes']['responses'][200]['content']['application/json'] =
|
||||
{
|
||||
nodes: [mockNodePack],
|
||||
@@ -48,7 +80,32 @@ describe('useComfyRegistryStore', () => {
|
||||
mockRegistryService = {
|
||||
isLoading: ref(false),
|
||||
error: ref(null),
|
||||
listAllPacks: vi.fn().mockResolvedValue(mockListResult),
|
||||
listAllPacks: vi.fn().mockImplementation((params) => {
|
||||
// If node_id is provided, return specific nodes
|
||||
if (params.node_id) {
|
||||
return Promise.resolve({
|
||||
nodes: params.node_id
|
||||
.map((id: string) => {
|
||||
switch (id) {
|
||||
case 'test-pack-id':
|
||||
return mockNodePack
|
||||
case 'test-pack-id-2':
|
||||
return mockNodePack2
|
||||
case 'test-pack-id-3':
|
||||
return mockNodePack3
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
total: params.node_id.length,
|
||||
page: 1,
|
||||
limit: 10
|
||||
})
|
||||
}
|
||||
// Otherwise return paginated results
|
||||
return Promise.resolve(mockListResult)
|
||||
}),
|
||||
getPackById: vi.fn().mockResolvedValue(mockNodePack)
|
||||
}
|
||||
|
||||
@@ -117,4 +174,16 @@ describe('useComfyRegistryStore', () => {
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should fetch packs by IDs', async () => {
|
||||
const store = useComfyRegistryStore()
|
||||
const packIds = ['test-pack-id', 'test-pack-id-2', 'test-pack-id-3']
|
||||
const result = await store.getPacksByIds.call(packIds)
|
||||
|
||||
expect(result).toEqual([mockNodePack, mockNodePack2, mockNodePack3])
|
||||
expect(mockRegistryService.listAllPacks).toHaveBeenCalledWith(
|
||||
{ node_id: packIds },
|
||||
expect.any(Object) // abort signal
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,22 +14,24 @@ describe('migrateReroute', () => {
|
||||
return JSON.parse(fileContent) as WorkflowJSON04
|
||||
}
|
||||
|
||||
it.each(['branching.json', 'single_connected.json', 'floating.json'])(
|
||||
'should correctly migrate %s',
|
||||
(fileName) => {
|
||||
// Load the legacy workflow
|
||||
const legacyWorkflow = loadWorkflow(
|
||||
`workflows/reroute/legacy/${fileName}`
|
||||
)
|
||||
it.each([
|
||||
'branching.json',
|
||||
'single_connected.json',
|
||||
'floating.json',
|
||||
'floating_branch.json'
|
||||
])('should correctly migrate %s', (fileName) => {
|
||||
// Load the legacy workflow
|
||||
const legacyWorkflow = loadWorkflow(
|
||||
`workflows/reroute/legacy/${fileName}`
|
||||
)
|
||||
|
||||
// Migrate the workflow
|
||||
const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow)
|
||||
// Migrate the workflow
|
||||
const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow)
|
||||
|
||||
// Compare with snapshot
|
||||
expect(JSON.stringify(migratedWorkflow, null, 2)).toMatchFileSnapshot(
|
||||
`workflows/reroute/native/${fileName}`
|
||||
)
|
||||
}
|
||||
)
|
||||
// Compare with snapshot
|
||||
expect(JSON.stringify(migratedWorkflow, null, 2)).toMatchFileSnapshot(
|
||||
`workflows/reroute/native/${fileName}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"last_node_id": 36,
|
||||
"last_link_id": 44,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 33,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
492.768310546875,
|
||||
274.761962890625
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 41
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
40
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
362.8304138183594,
|
||||
275.12872314453125
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 39
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
41,
|
||||
42
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-0.6348835229873657,
|
||||
238.0631866455078
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
39
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
611.6028442382812,
|
||||
254.6018524169922
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 40
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
490.8152770996094,
|
||||
364.4836730957031
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 42
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
39,
|
||||
4,
|
||||
2,
|
||||
32,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
40,
|
||||
33,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
41,
|
||||
32,
|
||||
0,
|
||||
33,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
42,
|
||||
32,
|
||||
0,
|
||||
34,
|
||||
0,
|
||||
"*"
|
||||
]
|
||||
],
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.6672297511789418,
|
||||
"offset": [
|
||||
262.0504372113823,
|
||||
124.35120995663942
|
||||
]
|
||||
},
|
||||
"linkExtensions": []
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -35,8 +35,8 @@
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
13,
|
||||
21
|
||||
21,
|
||||
34
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -77,7 +77,7 @@
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -115,7 +115,7 @@
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
|
||||
@@ -34,10 +34,7 @@
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
13,
|
||||
31
|
||||
]
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -72,14 +69,14 @@
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -101,8 +98,51 @@
|
||||
200.06933385457722
|
||||
]
|
||||
},
|
||||
"reroutes": [],
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
547.5,
|
||||
293
|
||||
],
|
||||
"linkIds": [],
|
||||
"floating": {
|
||||
"slotType": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pos": [
|
||||
438.99267578125,
|
||||
291.96600341796875
|
||||
],
|
||||
"linkIds": [],
|
||||
"floating": {
|
||||
"slotType": "output"
|
||||
}
|
||||
}
|
||||
],
|
||||
"linkExtensions": []
|
||||
},
|
||||
"version": 0.4
|
||||
"version": 0.4,
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": -1,
|
||||
"origin_slot": -1,
|
||||
"target_id": 12,
|
||||
"target_slot": 1,
|
||||
"type": "VAE",
|
||||
"parentId": 1
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*",
|
||||
"parentId": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"last_node_id": 36,
|
||||
"last_link_id": 44,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-0.6348835229873657,
|
||||
238.0631866455078
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
40
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
611.6028442382812,
|
||||
254.6018524169922
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 40
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
40,
|
||||
4,
|
||||
2,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*",
|
||||
"parentId": 3
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.6672297511789418,
|
||||
"offset": [
|
||||
262.0504372113823,
|
||||
124.35120995663942
|
||||
]
|
||||
},
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 40,
|
||||
"parentId": 1
|
||||
}
|
||||
],
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
530.268310546875,
|
||||
287.761962890625
|
||||
],
|
||||
"linkIds": [
|
||||
40
|
||||
],
|
||||
"parentId": 2
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pos": [
|
||||
400.3304138183594,
|
||||
288.12872314453125
|
||||
],
|
||||
"linkIds": [
|
||||
40
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"pos": [
|
||||
528.3152770996094,
|
||||
377.4836730957031
|
||||
],
|
||||
"linkIds": [],
|
||||
"parentId": 2,
|
||||
"floating": {
|
||||
"slotType": "output"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user