Compare commits
85 Commits
api-change
...
core/sub14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
622d97ff48 | ||
|
|
47729c1a08 | ||
|
|
55cf65ec41 | ||
|
|
683d81885a | ||
|
|
586f882423 | ||
|
|
4df20a3055 | ||
|
|
c462d356c1 | ||
|
|
6b3d89ee93 | ||
|
|
9bc4f66982 | ||
|
|
6926d449bc | ||
|
|
11b71bb820 | ||
|
|
586314e0da | ||
|
|
5acfe4ad98 | ||
|
|
aefc5eb078 | ||
|
|
cf9af94fac | ||
|
|
ee93e36ee2 | ||
|
|
9f0b22a5d8 | ||
|
|
76d6911ffa | ||
|
|
b8740c6ac3 | ||
|
|
27d33c24de | ||
|
|
9b488da973 | ||
|
|
c3065ff07b | ||
|
|
a5a1f8cf8a | ||
|
|
1aac2d52ac | ||
|
|
d2369c8c49 | ||
|
|
2afd295ad9 | ||
|
|
66fbdad4d2 | ||
|
|
b02408f517 | ||
|
|
2cdf547fdd | ||
|
|
d755210d03 | ||
|
|
6e89b196c2 | ||
|
|
97faee8879 | ||
|
|
7d568e13e5 | ||
|
|
c57c391659 | ||
|
|
cb91c3770c | ||
|
|
b0fc8efa6b | ||
|
|
842ec58511 | ||
|
|
060540ae80 | ||
|
|
962834e19c | ||
|
|
d018a69356 | ||
|
|
c84218d6bb | ||
|
|
6f9c481b38 | ||
|
|
bdc1ac1004 | ||
|
|
12e1508203 | ||
|
|
98f5216ddf | ||
|
|
b2550f6351 | ||
|
|
b3b0b95646 | ||
|
|
4ca5a92108 | ||
|
|
0a40c11b7d | ||
|
|
9d4537e803 | ||
|
|
4388cbe4a4 | ||
|
|
518faebf69 | ||
|
|
5685cb6748 | ||
|
|
fc191a1e03 | ||
|
|
f73be5d72a | ||
|
|
c76635ce7f | ||
|
|
20833e5090 | ||
|
|
359e9288ec | ||
|
|
4232e0503b | ||
|
|
a3615b3824 | ||
|
|
0fe0519531 | ||
|
|
2cd315a2bf | ||
|
|
7623711b40 | ||
|
|
dba8716fce | ||
|
|
15a2b37c93 | ||
|
|
bbd1ca234d | ||
|
|
b0fc736260 | ||
|
|
b65440e1c2 | ||
|
|
6918aa830b | ||
|
|
a5d0bc3198 | ||
|
|
f41ae1d408 | ||
|
|
1300a1351b | ||
|
|
5129cfa5a7 | ||
|
|
99c7ecfa82 | ||
|
|
77968fed6d | ||
|
|
09d17fec14 | ||
|
|
bff802eeeb | ||
|
|
71bbca613f | ||
|
|
6f8a91b0c1 | ||
|
|
f95f014fde | ||
|
|
d88a227e7c | ||
|
|
0dd308d885 | ||
|
|
31ab027da8 | ||
|
|
9620f833aa | ||
|
|
b3042d346a |
@@ -17,11 +17,11 @@ test.describe('Group Node', () => {
|
|||||||
await libraryTab.open()
|
await libraryTab.open()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Is added to node library sidebar', async ({ comfyPage }) => {
|
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||||
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
|
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can be added to canvas using node library sidebar', async ({
|
test.skip('Can be added to canvas using node library sidebar', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||||
@@ -34,7 +34,7 @@ test.describe('Group Node', () => {
|
|||||||
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
|
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
||||||
await libraryTab.getFolder(groupNodeCategory).click()
|
await libraryTab.getFolder(groupNodeCategory).click()
|
||||||
await libraryTab
|
await libraryTab
|
||||||
.getNode(groupNodeName)
|
.getNode(groupNodeName)
|
||||||
@@ -61,7 +61,7 @@ test.describe('Group Node', () => {
|
|||||||
).toHaveLength(0)
|
).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
||||||
await libraryTab.getFolder(groupNodeCategory).click()
|
await libraryTab.getFolder(groupNodeCategory).click()
|
||||||
await libraryTab
|
await libraryTab
|
||||||
.getNode(groupNodeName)
|
.getNode(groupNodeName)
|
||||||
@@ -95,7 +95,7 @@ test.describe('Group Node', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
test.skip('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||||
await comfyPage.page.mouse.move(47, 173)
|
await comfyPage.page.mouse.move(47, 173)
|
||||||
@@ -104,7 +104,7 @@ test.describe('Group Node', () => {
|
|||||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Manage group opens with the correct group selected', async ({
|
test.skip('Manage group opens with the correct group selected', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const makeGroup = async (name, type1, type2) => {
|
const makeGroup = async (name, type1, type2) => {
|
||||||
@@ -165,7 +165,7 @@ test.describe('Group Node', () => {
|
|||||||
expect(visibleInputCount).toBe(2)
|
expect(visibleInputCount).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
test.skip('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const expectSingleNode = async (type: string) => {
|
const expectSingleNode = async (type: string) => {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ test.describe('Canvas Right Click Menu', () => {
|
|||||||
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can convert to group node', async ({ comfyPage }) => {
|
test.skip('Can convert to group node', async ({ comfyPage }) => {
|
||||||
await comfyPage.select2Nodes()
|
await comfyPage.select2Nodes()
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||||
await comfyPage.rightClickCanvas()
|
await comfyPage.rightClickCanvas()
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"version": "1.23.2",
|
"version": "1.23.2-sub.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"version": "1.23.2",
|
"version": "1.23.2-sub.14",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||||
"@comfyorg/litegraph": "^0.15.15",
|
"@comfyorg/litegraph": "^0.16.0-sub.18",
|
||||||
"@primevue/forms": "^4.2.5",
|
"@primevue/forms": "^4.2.5",
|
||||||
"@primevue/themes": "^4.2.5",
|
"@primevue/themes": "^4.2.5",
|
||||||
"@sentry/vue": "^8.48.0",
|
"@sentry/vue": "^8.48.0",
|
||||||
@@ -948,9 +948,9 @@
|
|||||||
"license": "GPL-3.0-only"
|
"license": "GPL-3.0-only"
|
||||||
},
|
},
|
||||||
"node_modules/@comfyorg/litegraph": {
|
"node_modules/@comfyorg/litegraph": {
|
||||||
"version": "0.15.15",
|
"version": "0.16.0-sub.18",
|
||||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.15.tgz",
|
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.0-sub.18.tgz",
|
||||||
"integrity": "sha512-otOKgTxNPV6gEa6PW1fHGMMF8twjnZkP0vWQhGsRISK4vN8tPfX8O9sC9Hnq3nV8axaMv4/Ff49+7mMVcFEKeA==",
|
"integrity": "sha512-Au7I6zc9XbdsINBPNq9qIuccaBdlN7MMh6LUCUaJBjSgKDWCLfK1lpn1jF4B5cFTfEt/tzRkwLLUK6odV8A3gA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.23.2",
|
"version": "1.23.2-sub.14",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||||
"homepage": "https://comfy.org",
|
"homepage": "https://comfy.org",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||||
"@comfyorg/litegraph": "^0.15.15",
|
"@comfyorg/litegraph": "^0.16.0-sub.18",
|
||||||
"@primevue/forms": "^4.2.5",
|
"@primevue/forms": "^4.2.5",
|
||||||
"@primevue/themes": "^4.2.5",
|
"@primevue/themes": "^4.2.5",
|
||||||
"@sentry/vue": "^8.48.0",
|
"@sentry/vue": "^8.48.0",
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="workflowStore.isSubgraphActive" class="p-2 subgraph-breadcrumb">
|
||||||
v-if="workflowStore.isSubgraphActive"
|
|
||||||
class="fixed top-[var(--comfy-topbar-height)] left-[var(--sidebar-width)] p-2 subgraph-breadcrumb"
|
|
||||||
>
|
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
class="bg-transparent"
|
class="bg-transparent"
|
||||||
:home="home"
|
:home="home"
|
||||||
@@ -14,28 +11,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventListener, whenever } from '@vueuse/core'
|
import { useEventListener } from '@vueuse/core'
|
||||||
import Breadcrumb from 'primevue/breadcrumb'
|
import Breadcrumb from 'primevue/breadcrumb'
|
||||||
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
|
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { useWorkflowService } from '@/services/workflowService'
|
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
|
|
||||||
const workflowService = useWorkflowService()
|
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
|
const navigationStore = useSubgraphNavigationStore()
|
||||||
|
|
||||||
const workflowName = computed(() => workflowStore.activeWorkflow?.filename)
|
const workflowName = computed(() => workflowStore.activeWorkflow?.filename)
|
||||||
|
|
||||||
const items = computed(() => {
|
const items = computed(() => {
|
||||||
if (!workflowStore.subgraphNamePath.length) return []
|
if (!navigationStore.navigationStack.length) return []
|
||||||
|
|
||||||
return workflowStore.subgraphNamePath.map<MenuItem>((name) => ({
|
return navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
|
||||||
label: name,
|
label: subgraph.name,
|
||||||
command: async () => {
|
command: () => {
|
||||||
const workflow = workflowStore.getWorkflowByPath(name)
|
const canvas = useCanvasStore().getCanvas()
|
||||||
if (workflow) await workflowService.openWorkflow(workflow)
|
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||||
|
|
||||||
|
canvas.setGraph(subgraph)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -43,7 +42,7 @@ const items = computed(() => {
|
|||||||
const home = computed(() => ({
|
const home = computed(() => ({
|
||||||
label: workflowName.value,
|
label: workflowName.value,
|
||||||
icon: 'pi pi-home',
|
icon: 'pi pi-home',
|
||||||
command: async () => {
|
command: () => {
|
||||||
const canvas = useCanvasStore().getCanvas()
|
const canvas = useCanvasStore().getCanvas()
|
||||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||||
|
|
||||||
@@ -55,22 +54,32 @@ const handleItemClick = (event: MenuItemCommandEvent) => {
|
|||||||
event.item.command?.(event)
|
event.item.command?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
whenever(
|
// Escape exits from the current subgraph.
|
||||||
() => useCanvasStore().canvas,
|
useEventListener(document, 'keydown', (event) => {
|
||||||
(canvas) => {
|
if (event.key === 'Escape') {
|
||||||
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
|
const canvas = useCanvasStore().getCanvas()
|
||||||
useWorkflowStore().updateActiveGraph()
|
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||||
})
|
|
||||||
|
canvas.setGraph(
|
||||||
|
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.subgraph-breadcrumb {
|
.subgraph-breadcrumb {
|
||||||
.p-breadcrumb-item-link,
|
.p-breadcrumb-item-link,
|
||||||
.p-breadcrumb-item-icon {
|
.p-breadcrumb-item-icon {
|
||||||
|
@apply select-none;
|
||||||
|
|
||||||
color: #d26565;
|
color: #d26565;
|
||||||
user-select: none;
|
text-shadow:
|
||||||
|
1px 1px 0 #000,
|
||||||
|
-1px -1px 0 #000,
|
||||||
|
1px -1px 0 #000,
|
||||||
|
-1px 1px 0 #000,
|
||||||
|
0 0 0.375rem #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,16 +21,14 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
|||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
const domWidgetStore = useDomWidgetStore()
|
const domWidgetStore = useDomWidgetStore()
|
||||||
const widgetStates = computed(() =>
|
const widgetStates = computed(() => domWidgetStore.activeWidgetStates)
|
||||||
Array.from(domWidgetStore.widgetStates.values())
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateWidgets = () => {
|
const updateWidgets = () => {
|
||||||
const lgCanvas = canvasStore.canvas
|
const lgCanvas = canvasStore.canvas
|
||||||
if (!lgCanvas) return
|
if (!lgCanvas) return
|
||||||
|
|
||||||
const lowQuality = lgCanvas.low_quality
|
const lowQuality = lgCanvas.low_quality
|
||||||
for (const widgetState of domWidgetStore.widgetStates.values()) {
|
for (const widgetState of widgetStates.value) {
|
||||||
const widget = widgetState.widget
|
const widget = widgetState.widget
|
||||||
const node = widget.node as LGraphNode
|
const node = widget.node as LGraphNode
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
<BottomPanel />
|
<BottomPanel />
|
||||||
</template>
|
</template>
|
||||||
<template #graph-canvas-panel>
|
<template #graph-canvas-panel>
|
||||||
<SecondRowWorkflowTabs
|
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
|
||||||
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
<SecondRowWorkflowTabs
|
||||||
class="pointer-events-auto"
|
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
||||||
/>
|
/>
|
||||||
|
<SubgraphBreadcrumb />
|
||||||
|
</div>
|
||||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||||
</template>
|
</template>
|
||||||
</LiteGraphCanvasSplitterOverlay>
|
</LiteGraphCanvasSplitterOverlay>
|
||||||
@@ -39,12 +41,11 @@
|
|||||||
</SelectionOverlay>
|
</SelectionOverlay>
|
||||||
<DomWidgets />
|
<DomWidgets />
|
||||||
</template>
|
</template>
|
||||||
<SubgraphBreadcrumb />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener, whenever } from '@vueuse/core'
|
||||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||||
|
|
||||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||||
@@ -84,6 +85,7 @@ import { useCanvasStore } from '@/stores/graphStore'
|
|||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
|
||||||
@@ -192,10 +194,10 @@ watch(
|
|||||||
// Update the progress of the executing node
|
// Update the progress of the executing node
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() =>
|
||||||
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
|
[
|
||||||
NodeId | null,
|
executionStore.executingNodeId,
|
||||||
number | null
|
executionStore.executingNodeProgress
|
||||||
],
|
] satisfies [NodeId | null, number | null],
|
||||||
([executingNodeId, executingNodeProgress]) => {
|
([executingNodeId, executingNodeProgress]) => {
|
||||||
for (const node of comfyApp.graph.nodes) {
|
for (const node of comfyApp.graph.nodes) {
|
||||||
if (node.id == executingNodeId) {
|
if (node.id == executingNodeId) {
|
||||||
@@ -334,6 +336,16 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => useCanvasStore().canvas,
|
||||||
|
(canvas) => {
|
||||||
|
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
|
||||||
|
useWorkflowStore().updateActiveGraph()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
emit('ready')
|
emit('ready')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<BypassButton />
|
<BypassButton />
|
||||||
<PinButton />
|
<PinButton />
|
||||||
<MaskEditorButton />
|
<MaskEditorButton />
|
||||||
|
<ConvertToSubgraphButton />
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
<ExtensionCommandButton
|
<ExtensionCommandButton
|
||||||
@@ -28,6 +29,7 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||||
|
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
|
||||||
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
||||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||||
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
v-show="isVisible"
|
||||||
|
v-tooltip.top="{
|
||||||
|
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||||
|
showDelay: 1000
|
||||||
|
}"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
icon="pi pi-box"
|
||||||
|
@click="() => commandStore.execute('Comfy.Graph.ConvertToSubgraph')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const isVisible = computed(() => {
|
||||||
|
return (
|
||||||
|
canvasStore.groupSelected ||
|
||||||
|
canvasStore.rerouteSelected ||
|
||||||
|
canvasStore.nodeSelected
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
|
v-show="isDeletable"
|
||||||
v-tooltip.top="{
|
v-tooltip.top="{
|
||||||
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
|
||||||
showDelay: 1000
|
showDelay: 1000
|
||||||
@@ -13,10 +14,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const isDeletable = computed(() =>
|
||||||
|
canvasStore.selectedItems.some((x) => x.removable !== false)
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ const commandStore = useCommandStore()
|
|||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
const isSingleImageNode = computed(() => {
|
const isSingleImageNode = computed(() => {
|
||||||
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
|
const { selectedItems } = canvasStore
|
||||||
return nodes.length === 1 && nodes.some(isImageNode)
|
const item = selectedItems[0]
|
||||||
|
return selectedItems.length === 1 && isLGraphNode(item) && isImageNode(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
const openMaskEditor = () => {
|
const openMaskEditor = () => {
|
||||||
|
|||||||
@@ -95,12 +95,14 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectOnReset = false
|
|
||||||
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||||
pos: getNewNodeLocation()
|
pos: getNewNodeLocation()
|
||||||
})
|
})
|
||||||
|
|
||||||
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
|
if (disconnectOnReset) {
|
||||||
|
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
|
||||||
|
}
|
||||||
|
disconnectOnReset = false
|
||||||
|
|
||||||
// Notify changeTracker - new step should be added
|
// Notify changeTracker - new step should be added
|
||||||
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
|
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute top-0 left-0 w-auto max-w-full">
|
<div class="w-auto max-w-full">
|
||||||
<WorkflowTabs />
|
<WorkflowTabs />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useDialogService } from '@/services/dialogService'
|
|||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
import { useWorkflowService } from '@/services/workflowService'
|
import { useWorkflowService } from '@/services/workflowService'
|
||||||
import type { ComfyCommand } from '@/stores/commandStore'
|
import type { ComfyCommand } from '@/stores/commandStore'
|
||||||
import { useTitleEditorStore } from '@/stores/graphStore'
|
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
@@ -37,6 +37,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
const colorPaletteStore = useColorPaletteStore()
|
const colorPaletteStore = useColorPaletteStore()
|
||||||
const firebaseAuthActions = useFirebaseAuthActions()
|
const firebaseAuthActions = useFirebaseAuthActions()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||||
|
|
||||||
const getSelectedNodes = (): LGraphNode[] => {
|
const getSelectedNodes = (): LGraphNode[] => {
|
||||||
@@ -718,6 +719,30 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
label: 'Move Selected Nodes Right',
|
label: 'Move Selected Nodes Right',
|
||||||
versionAdded: moveSelectedNodesVersionAdded,
|
versionAdded: moveSelectedNodesVersionAdded,
|
||||||
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
|
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.Graph.ConvertToSubgraph',
|
||||||
|
icon: 'pi pi-sitemap',
|
||||||
|
label: 'Convert Selection to Subgraph',
|
||||||
|
versionAdded: '1.20.1',
|
||||||
|
function: () => {
|
||||||
|
const canvas = canvasStore.getCanvas()
|
||||||
|
const graph = canvas.subgraph ?? canvas.graph
|
||||||
|
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||||
|
|
||||||
|
const res = graph.convertToSubgraph(canvas.selectedItems)
|
||||||
|
if (!res) {
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('toastMessages.cannotCreateSubgraph'),
|
||||||
|
detail: t('toastMessages.failedToConvertToSubgraph'),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { node } = res
|
||||||
|
canvas.select(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export function useErrorHandling() {
|
|||||||
summary: t('g.error'),
|
summary: t('g.error'),
|
||||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||||
})
|
})
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapWithErrorHandling =
|
const wrapWithErrorHandling =
|
||||||
|
|||||||
@@ -173,5 +173,13 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
key: 'f'
|
key: 'f'
|
||||||
},
|
},
|
||||||
commandId: 'Workspace.ToggleFocusMode'
|
commandId: 'Workspace.ToggleFocusMode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
combo: {
|
||||||
|
key: 'e',
|
||||||
|
ctrl: true,
|
||||||
|
shift: true
|
||||||
|
},
|
||||||
|
commandId: 'Comfy.Graph.ConvertToSubgraph'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
import { LiteGraph } from '@comfyorg/litegraph'
|
||||||
import { LGraphNode, type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
import { LGraphNode, type NodeId } from '@comfyorg/litegraph/dist/LGraphNode'
|
||||||
|
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
@@ -1583,57 +1583,6 @@ export class GroupNodeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addConvertToGroupOptions() {
|
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
function addConvertOption(options, index) {
|
|
||||||
const selected = Object.values(app.canvas.selected_nodes ?? {})
|
|
||||||
const disabled =
|
|
||||||
selected.length < 2 ||
|
|
||||||
selected.find((n) => GroupNodeHandler.isGroupNode(n))
|
|
||||||
options.splice(index, null, {
|
|
||||||
content: `Convert to Group Node`,
|
|
||||||
disabled,
|
|
||||||
callback: convertSelectedNodesToGroupNode
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
function addManageOption(options, index) {
|
|
||||||
const groups = app.graph.extra?.groupNodes
|
|
||||||
const disabled = !groups || !Object.keys(groups).length
|
|
||||||
options.splice(index, null, {
|
|
||||||
content: `Manage Group Nodes`,
|
|
||||||
disabled,
|
|
||||||
callback: () => manageGroupNodes()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to canvas
|
|
||||||
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
|
|
||||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
const options = getCanvasMenuOptions.apply(this, arguments)
|
|
||||||
const index = options.findIndex((o) => o?.content === 'Add Group')
|
|
||||||
const insertAt = index === -1 ? options.length - 1 : index + 2
|
|
||||||
addConvertOption(options, insertAt)
|
|
||||||
addManageOption(options, insertAt + 1)
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to nodes
|
|
||||||
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions
|
|
||||||
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
|
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
const options = getNodeMenuOptions.apply(this, arguments)
|
|
||||||
if (!GroupNodeHandler.isGroupNode(node)) {
|
|
||||||
const index = options.findIndex((o) => o?.content === 'Properties')
|
|
||||||
const insertAt = index === -1 ? options.length - 1 : index
|
|
||||||
addConvertOption(options, insertAt)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
|
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
|
||||||
@@ -1723,9 +1672,6 @@ const ext: ComfyExtension = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
setup() {
|
|
||||||
addConvertToGroupOptions()
|
|
||||||
},
|
|
||||||
async beforeConfigureGraph(
|
async beforeConfigureGraph(
|
||||||
graphData: ComfyWorkflowJSON,
|
graphData: ComfyWorkflowJSON,
|
||||||
missingNodeTypes: string[]
|
missingNodeTypes: string[]
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "Give Feedback"
|
"label": "Give Feedback"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "Convert Selection to Subgraph"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "Fit Group To Contents"
|
"label": "Fit Group To Contents"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -817,6 +817,7 @@
|
|||||||
"Export": "Export",
|
"Export": "Export",
|
||||||
"Export (API)": "Export (API)",
|
"Export (API)": "Export (API)",
|
||||||
"Give Feedback": "Give Feedback",
|
"Give Feedback": "Give Feedback",
|
||||||
|
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||||
"Fit Group To Contents": "Fit Group To Contents",
|
"Fit Group To Contents": "Fit Group To Contents",
|
||||||
"Group Selected Nodes": "Group Selected Nodes",
|
"Group Selected Nodes": "Group Selected Nodes",
|
||||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||||
@@ -1302,7 +1303,9 @@
|
|||||||
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
|
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
|
||||||
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
|
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
|
||||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||||
"nothingSelected": "Nothing selected"
|
"nothingSelected": "Nothing selected",
|
||||||
|
"cannotCreateSubgraph": "Cannot create subgraph",
|
||||||
|
"failedToConvertToSubgraph": "Failed to convert items to subgraph"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"apiKey": {
|
"apiKey": {
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "Dar retroalimentación"
|
"label": "Dar retroalimentación"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "Convertir selección en subgrafo"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "Ajustar grupo al contenido"
|
"label": "Ajustar grupo al contenido"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "Foro de ComfyUI",
|
"ComfyUI Forum": "Foro de ComfyUI",
|
||||||
"ComfyUI Issues": "Problemas de ComfyUI",
|
"ComfyUI Issues": "Problemas de ComfyUI",
|
||||||
"Contact Support": "Contactar soporte",
|
"Contact Support": "Contactar soporte",
|
||||||
|
"Convert Selection to Subgraph": "Convertir selección en subgrafo",
|
||||||
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
|
||||||
"Delete Selected Items": "Eliminar elementos seleccionados",
|
"Delete Selected Items": "Eliminar elementos seleccionados",
|
||||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "Comienza con una Plantilla"
|
"title": "Comienza con una Plantilla"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "No se puede crear el subgrafo",
|
||||||
"couldNotDetermineFileType": "No se pudo determinar el tipo de archivo",
|
"couldNotDetermineFileType": "No se pudo determinar el tipo de archivo",
|
||||||
"dropFileError": "No se puede procesar el elemento soltado: {error}",
|
"dropFileError": "No se puede procesar el elemento soltado: {error}",
|
||||||
"emptyCanvas": "Lienzo vacío",
|
"emptyCanvas": "Lienzo vacío",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "Error al guardar la configuración {id}: {err}",
|
"errorSaveSetting": "Error al guardar la configuración {id}: {err}",
|
||||||
"failedToAccessBillingPortal": "No se pudo acceder al portal de facturación: {error}",
|
"failedToAccessBillingPortal": "No se pudo acceder al portal de facturación: {error}",
|
||||||
"failedToApplyTexture": "Error al aplicar textura",
|
"failedToApplyTexture": "Error al aplicar textura",
|
||||||
|
"failedToConvertToSubgraph": "No se pudo convertir los elementos en subgrafo",
|
||||||
"failedToCreateCustomer": "No se pudo crear el cliente: {error}",
|
"failedToCreateCustomer": "No se pudo crear el cliente: {error}",
|
||||||
"failedToDownloadFile": "Error al descargar el archivo",
|
"failedToDownloadFile": "Error al descargar el archivo",
|
||||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "Retour d'information"
|
"label": "Retour d'information"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "Convertir la sélection en sous-graphe"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "Ajuster le groupe au contenu"
|
"label": "Ajuster le groupe au contenu"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "Forum ComfyUI",
|
"ComfyUI Forum": "Forum ComfyUI",
|
||||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||||
"Contact Support": "Contacter le support",
|
"Contact Support": "Contacter le support",
|
||||||
|
"Convert Selection to Subgraph": "Convertir la sélection en sous-graphe",
|
||||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "Commencez avec un modèle"
|
"title": "Commencez avec un modèle"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "Impossible de créer le sous-graphe",
|
||||||
"couldNotDetermineFileType": "Impossible de déterminer le type de fichier",
|
"couldNotDetermineFileType": "Impossible de déterminer le type de fichier",
|
||||||
"dropFileError": "Impossible de traiter l'élément déposé : {error}",
|
"dropFileError": "Impossible de traiter l'élément déposé : {error}",
|
||||||
"emptyCanvas": "Toile vide",
|
"emptyCanvas": "Toile vide",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "Erreur lors de l'enregistrement du paramètre {id}: {err}",
|
"errorSaveSetting": "Erreur lors de l'enregistrement du paramètre {id}: {err}",
|
||||||
"failedToAccessBillingPortal": "Échec de l'accès au portail de facturation : {error}",
|
"failedToAccessBillingPortal": "Échec de l'accès au portail de facturation : {error}",
|
||||||
"failedToApplyTexture": "Échec de l'application de la texture",
|
"failedToApplyTexture": "Échec de l'application de la texture",
|
||||||
|
"failedToConvertToSubgraph": "Échec de la conversion des éléments en sous-graphe",
|
||||||
"failedToCreateCustomer": "Échec de la création du client : {error}",
|
"failedToCreateCustomer": "Échec de la création du client : {error}",
|
||||||
"failedToDownloadFile": "Échec du téléchargement du fichier",
|
"failedToDownloadFile": "Échec du téléchargement du fichier",
|
||||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "フィードバック"
|
"label": "フィードバック"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "選択範囲をサブグラフに変換"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "グループを内容に合わせて調整"
|
"label": "グループを内容に合わせて調整"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "ComfyUI フォーラム",
|
"ComfyUI Forum": "ComfyUI フォーラム",
|
||||||
"ComfyUI Issues": "ComfyUIの問題",
|
"ComfyUI Issues": "ComfyUIの問題",
|
||||||
"Contact Support": "サポートに連絡",
|
"Contact Support": "サポートに連絡",
|
||||||
|
"Convert Selection to Subgraph": "選択範囲をサブグラフに変換",
|
||||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||||
"Delete Selected Items": "選択したアイテムを削除",
|
"Delete Selected Items": "選択したアイテムを削除",
|
||||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "テンプレートを利用して開始"
|
"title": "テンプレートを利用して開始"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "サブグラフを作成できません",
|
||||||
"couldNotDetermineFileType": "ファイルタイプを判断できませんでした",
|
"couldNotDetermineFileType": "ファイルタイプを判断できませんでした",
|
||||||
"dropFileError": "ドロップされたアイテムを処理できません: {error}",
|
"dropFileError": "ドロップされたアイテムを処理できません: {error}",
|
||||||
"emptyCanvas": "キャンバスが空です",
|
"emptyCanvas": "キャンバスが空です",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "設定{id}の保存エラー: {err}",
|
"errorSaveSetting": "設定{id}の保存エラー: {err}",
|
||||||
"failedToAccessBillingPortal": "請求ポータルへのアクセスに失敗しました: {error}",
|
"failedToAccessBillingPortal": "請求ポータルへのアクセスに失敗しました: {error}",
|
||||||
"failedToApplyTexture": "テクスチャの適用に失敗しました",
|
"failedToApplyTexture": "テクスチャの適用に失敗しました",
|
||||||
|
"failedToConvertToSubgraph": "アイテムをサブグラフに変換できませんでした",
|
||||||
"failedToCreateCustomer": "顧客の作成に失敗しました: {error}",
|
"failedToCreateCustomer": "顧客の作成に失敗しました: {error}",
|
||||||
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
|
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
|
||||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "피드백"
|
"label": "피드백"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "선택 영역을 서브그래프로 변환"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "그룹을 내용에 맞게 맞추기"
|
"label": "그룹을 내용에 맞게 맞추기"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "ComfyUI 포럼",
|
"ComfyUI Forum": "ComfyUI 포럼",
|
||||||
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
||||||
"Contact Support": "고객 지원 문의",
|
"Contact Support": "고객 지원 문의",
|
||||||
|
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||||
"Delete Selected Items": "선택한 항목 삭제",
|
"Delete Selected Items": "선택한 항목 삭제",
|
||||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "템플릿으로 시작하기"
|
"title": "템플릿으로 시작하기"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "서브그래프를 생성할 수 없습니다",
|
||||||
"couldNotDetermineFileType": "파일 유형을 결정할 수 없습니다",
|
"couldNotDetermineFileType": "파일 유형을 결정할 수 없습니다",
|
||||||
"dropFileError": "드롭된 항목을 처리할 수 없습니다: {error}",
|
"dropFileError": "드롭된 항목을 처리할 수 없습니다: {error}",
|
||||||
"emptyCanvas": "빈 캔버스",
|
"emptyCanvas": "빈 캔버스",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "설정 {id} 저장 오류: {err}",
|
"errorSaveSetting": "설정 {id} 저장 오류: {err}",
|
||||||
"failedToAccessBillingPortal": "결제 포털에 접근하지 못했습니다: {error}",
|
"failedToAccessBillingPortal": "결제 포털에 접근하지 못했습니다: {error}",
|
||||||
"failedToApplyTexture": "텍스처 적용에 실패했습니다",
|
"failedToApplyTexture": "텍스처 적용에 실패했습니다",
|
||||||
|
"failedToConvertToSubgraph": "항목을 서브그래프로 변환하지 못했습니다",
|
||||||
"failedToCreateCustomer": "고객 생성에 실패했습니다: {error}",
|
"failedToCreateCustomer": "고객 생성에 실패했습니다: {error}",
|
||||||
"failedToDownloadFile": "파일 다운로드에 실패했습니다",
|
"failedToDownloadFile": "파일 다운로드에 실패했습니다",
|
||||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "Обратная связь"
|
"label": "Обратная связь"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "Преобразовать выделенное в подграф"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "Подогнать группу к содержимому"
|
"label": "Подогнать группу к содержимому"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "Форум ComfyUI",
|
"ComfyUI Forum": "Форум ComfyUI",
|
||||||
"ComfyUI Issues": "Проблемы ComfyUI",
|
"ComfyUI Issues": "Проблемы ComfyUI",
|
||||||
"Contact Support": "Связаться с поддержкой",
|
"Contact Support": "Связаться с поддержкой",
|
||||||
|
"Convert Selection to Subgraph": "Преобразовать выделенное в подграф",
|
||||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||||
"Delete Selected Items": "Удалить выбранные элементы",
|
"Delete Selected Items": "Удалить выбранные элементы",
|
||||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "Начните с шаблона"
|
"title": "Начните с шаблона"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "Невозможно создать подграф",
|
||||||
"couldNotDetermineFileType": "Не удалось определить тип файла",
|
"couldNotDetermineFileType": "Не удалось определить тип файла",
|
||||||
"dropFileError": "Не удалось обработать перетаскиваемый элемент: {error}",
|
"dropFileError": "Не удалось обработать перетаскиваемый элемент: {error}",
|
||||||
"emptyCanvas": "Пустой холст",
|
"emptyCanvas": "Пустой холст",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "Ошибка сохранения настройки {id}: {err}",
|
"errorSaveSetting": "Ошибка сохранения настройки {id}: {err}",
|
||||||
"failedToAccessBillingPortal": "Не удалось получить доступ к биллинговому порталу: {error}",
|
"failedToAccessBillingPortal": "Не удалось получить доступ к биллинговому порталу: {error}",
|
||||||
"failedToApplyTexture": "Не удалось применить текстуру",
|
"failedToApplyTexture": "Не удалось применить текстуру",
|
||||||
|
"failedToConvertToSubgraph": "Не удалось преобразовать элементы в подграф",
|
||||||
"failedToCreateCustomer": "Не удалось создать клиента: {error}",
|
"failedToCreateCustomer": "Не удалось создать клиента: {error}",
|
||||||
"failedToDownloadFile": "Не удалось скачать файл",
|
"failedToDownloadFile": "Не удалось скачать файл",
|
||||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
"Comfy_Feedback": {
|
"Comfy_Feedback": {
|
||||||
"label": "反馈"
|
"label": "反馈"
|
||||||
},
|
},
|
||||||
|
"Comfy_Graph_ConvertToSubgraph": {
|
||||||
|
"label": "将选区转换为子图"
|
||||||
|
},
|
||||||
"Comfy_Graph_FitGroupToContents": {
|
"Comfy_Graph_FitGroupToContents": {
|
||||||
"label": "适应节点框到内容"
|
"label": "适应节点框到内容"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -699,6 +699,7 @@
|
|||||||
"ComfyUI Forum": "ComfyUI 论坛",
|
"ComfyUI Forum": "ComfyUI 论坛",
|
||||||
"ComfyUI Issues": "ComfyUI 问题",
|
"ComfyUI Issues": "ComfyUI 问题",
|
||||||
"Contact Support": "联系支持",
|
"Contact Support": "联系支持",
|
||||||
|
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||||
"Delete Selected Items": "删除选定的项目",
|
"Delete Selected Items": "删除选定的项目",
|
||||||
"Desktop User Guide": "桌面端用户指南",
|
"Desktop User Guide": "桌面端用户指南",
|
||||||
@@ -1367,6 +1368,7 @@
|
|||||||
"title": "从模板开始"
|
"title": "从模板开始"
|
||||||
},
|
},
|
||||||
"toastMessages": {
|
"toastMessages": {
|
||||||
|
"cannotCreateSubgraph": "无法创建子图",
|
||||||
"couldNotDetermineFileType": "无法确定文件类型",
|
"couldNotDetermineFileType": "无法确定文件类型",
|
||||||
"dropFileError": "无法处理掉落的项目:{error}",
|
"dropFileError": "无法处理掉落的项目:{error}",
|
||||||
"emptyCanvas": "画布为空",
|
"emptyCanvas": "画布为空",
|
||||||
@@ -1375,6 +1377,7 @@
|
|||||||
"errorSaveSetting": "保存设置 {id} 出错:{err}",
|
"errorSaveSetting": "保存设置 {id} 出错:{err}",
|
||||||
"failedToAccessBillingPortal": "访问账单门户失败:{error}",
|
"failedToAccessBillingPortal": "访问账单门户失败:{error}",
|
||||||
"failedToApplyTexture": "应用纹理失败",
|
"failedToApplyTexture": "应用纹理失败",
|
||||||
|
"failedToConvertToSubgraph": "无法将项目转换为子图",
|
||||||
"failedToCreateCustomer": "创建客户失败:{error}",
|
"failedToCreateCustomer": "创建客户失败:{error}",
|
||||||
"failedToDownloadFile": "文件下载失败",
|
"failedToDownloadFile": "文件下载失败",
|
||||||
"failedToExportModel": "无法将模型导出为 {format}",
|
"failedToExportModel": "无法将模型导出为 {format}",
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ const zModelFile = z.object({
|
|||||||
|
|
||||||
const zGraphState = z
|
const zGraphState = z
|
||||||
.object({
|
.object({
|
||||||
lastGroupid: z.number().optional(),
|
lastGroupId: z.number(),
|
||||||
lastNodeId: z.number().optional(),
|
lastNodeId: z.number(),
|
||||||
lastLinkId: z.number().optional(),
|
lastLinkId: z.number(),
|
||||||
lastRerouteId: z.number().optional()
|
lastRerouteId: z.number()
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
@@ -214,6 +214,32 @@ const zComfyNode = z
|
|||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
|
export const zSubgraphIO = zNodeInput.extend({
|
||||||
|
/** Slot ID (internal; never changes once instantiated). */
|
||||||
|
id: z.string().uuid(),
|
||||||
|
/** The data type this slot uses. Unlike nodes, this does not support legacy numeric types. */
|
||||||
|
type: z.string(),
|
||||||
|
/** Links connected to this slot, or `undefined` if not connected. An ouptut slot should only ever have one link. */
|
||||||
|
linkIds: z.array(z.number()).optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
const zSubgraphInstance = z
|
||||||
|
.object({
|
||||||
|
id: zNodeId,
|
||||||
|
type: z.string().uuid(),
|
||||||
|
pos: zVector2,
|
||||||
|
size: zVector2,
|
||||||
|
flags: zFlags,
|
||||||
|
order: z.number(),
|
||||||
|
mode: z.number(),
|
||||||
|
inputs: z.array(zSubgraphIO).optional(),
|
||||||
|
outputs: z.array(zSubgraphIO).optional(),
|
||||||
|
widgets_values: zWidgetValues.optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
bgcolor: z.string().optional()
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
const zGroup = z
|
const zGroup = z
|
||||||
.object({
|
.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
@@ -248,9 +274,22 @@ const zExtra = z
|
|||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
|
export const zGraphDefinitions = z.object({
|
||||||
|
subgraphs: z.lazy(() => z.array(zSubgraphDefinition))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const zBaseExportableGraph = z.object({
|
||||||
|
/** Unique graph ID. Automatically generated if not provided. */
|
||||||
|
id: z.string().uuid().optional(),
|
||||||
|
revision: z.number().optional(),
|
||||||
|
config: zConfig.optional().nullable(),
|
||||||
|
/** Details of the appearance and location of subgraphs shown in this graph. Similar to */
|
||||||
|
subgraphs: z.array(zSubgraphInstance).optional()
|
||||||
|
})
|
||||||
|
|
||||||
/** Schema version 0.4 */
|
/** Schema version 0.4 */
|
||||||
export const zComfyWorkflow = z
|
export const zComfyWorkflow = zBaseExportableGraph
|
||||||
.object({
|
.extend({
|
||||||
id: z.string().uuid().optional(),
|
id: z.string().uuid().optional(),
|
||||||
revision: z.number().optional(),
|
revision: z.number().optional(),
|
||||||
last_node_id: zNodeId,
|
last_node_id: zNodeId,
|
||||||
@@ -262,13 +301,47 @@ export const zComfyWorkflow = z
|
|||||||
config: zConfig.optional().nullable(),
|
config: zConfig.optional().nullable(),
|
||||||
extra: zExtra.optional().nullable(),
|
extra: zExtra.optional().nullable(),
|
||||||
version: z.number(),
|
version: z.number(),
|
||||||
models: z.array(zModelFile).optional()
|
models: z.array(zModelFile).optional(),
|
||||||
|
definitions: zGraphDefinitions.optional()
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
|
/** Required for recursive definition of subgraphs. */
|
||||||
|
interface ComfyWorkflow1BaseType {
|
||||||
|
id?: string
|
||||||
|
revision?: number
|
||||||
|
version: 1
|
||||||
|
models?: z.infer<typeof zModelFile>[]
|
||||||
|
state: z.infer<typeof zGraphState>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Required for recursive definition of subgraphs w/ZodEffects. */
|
||||||
|
interface ComfyWorkflow1BaseInput extends ComfyWorkflow1BaseType {
|
||||||
|
groups?: z.input<typeof zGroup>[]
|
||||||
|
nodes: z.input<typeof zComfyNode>[]
|
||||||
|
links?: z.input<typeof zComfyLinkObject>[]
|
||||||
|
floatingLinks?: z.input<typeof zComfyLinkObject>[]
|
||||||
|
reroutes?: z.input<typeof zReroute>[]
|
||||||
|
definitions?: {
|
||||||
|
subgraphs: SubgraphDefinitionBase<ComfyWorkflow1BaseInput>[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Required for recursive definition of subgraphs w/ZodEffects. */
|
||||||
|
interface ComfyWorkflow1BaseOutput extends ComfyWorkflow1BaseType {
|
||||||
|
groups?: z.output<typeof zGroup>[]
|
||||||
|
nodes: z.output<typeof zComfyNode>[]
|
||||||
|
links?: z.output<typeof zComfyLinkObject>[]
|
||||||
|
floatingLinks?: z.output<typeof zComfyLinkObject>[]
|
||||||
|
reroutes?: z.output<typeof zReroute>[]
|
||||||
|
definitions?: {
|
||||||
|
subgraphs: SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Schema version 1 */
|
/** Schema version 1 */
|
||||||
export const zComfyWorkflow1 = z
|
export const zComfyWorkflow1 = zBaseExportableGraph
|
||||||
.object({
|
.extend({
|
||||||
id: z.string().uuid().optional(),
|
id: z.string().uuid().optional(),
|
||||||
revision: z.number().optional(),
|
revision: z.number().optional(),
|
||||||
version: z.literal(1),
|
version: z.literal(1),
|
||||||
@@ -280,7 +353,96 @@ export const zComfyWorkflow1 = z
|
|||||||
floatingLinks: z.array(zComfyLinkObject).optional(),
|
floatingLinks: z.array(zComfyLinkObject).optional(),
|
||||||
reroutes: z.array(zReroute).optional(),
|
reroutes: z.array(zReroute).optional(),
|
||||||
extra: zExtra.optional().nullable(),
|
extra: zExtra.optional().nullable(),
|
||||||
models: z.array(zModelFile).optional()
|
models: z.array(zModelFile).optional(),
|
||||||
|
definitions: z
|
||||||
|
.object({
|
||||||
|
subgraphs: z.lazy(
|
||||||
|
(): z.ZodArray<
|
||||||
|
z.ZodType<
|
||||||
|
SubgraphDefinitionBase<ComfyWorkflow1BaseOutput>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||||
|
>,
|
||||||
|
'many'
|
||||||
|
> => z.array(zSubgraphDefinition)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
|
export const zExportedSubgraphIONode = z.object({
|
||||||
|
id: zNodeId,
|
||||||
|
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
||||||
|
pinned: z.boolean().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const zExposedWidget = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
interface SubgraphDefinitionBase<
|
||||||
|
T extends ComfyWorkflow1BaseInput | ComfyWorkflow1BaseOutput
|
||||||
|
> {
|
||||||
|
/** Unique graph ID. Automatically generated if not provided. */
|
||||||
|
id: string
|
||||||
|
revision: number
|
||||||
|
name: string
|
||||||
|
|
||||||
|
inputNode: T extends ComfyWorkflow1BaseInput
|
||||||
|
? z.input<typeof zExportedSubgraphIONode>
|
||||||
|
: z.output<typeof zExportedSubgraphIONode>
|
||||||
|
outputNode: T extends ComfyWorkflow1BaseInput
|
||||||
|
? z.input<typeof zExportedSubgraphIONode>
|
||||||
|
: z.output<typeof zExportedSubgraphIONode>
|
||||||
|
/** Ordered list of inputs to the subgraph itself. Similar to a reroute, with the input side in the graph, and the output side in the subgraph. */
|
||||||
|
inputs?: T extends ComfyWorkflow1BaseInput
|
||||||
|
? z.input<typeof zSubgraphIO>[]
|
||||||
|
: z.output<typeof zSubgraphIO>[]
|
||||||
|
/** Ordered list of outputs from the subgraph itself. Similar to a reroute, with the input side in the subgraph, and the output side in the graph. */
|
||||||
|
outputs?: T extends ComfyWorkflow1BaseInput
|
||||||
|
? z.input<typeof zSubgraphIO>[]
|
||||||
|
: z.output<typeof zSubgraphIO>[]
|
||||||
|
/** A list of node widgets displayed in the parent graph, on the subgraph object. */
|
||||||
|
widgets?: T extends ComfyWorkflow1BaseInput
|
||||||
|
? z.input<typeof zExposedWidget>[]
|
||||||
|
: z.output<typeof zExposedWidget>[]
|
||||||
|
definitions?: {
|
||||||
|
subgraphs: SubgraphDefinitionBase<T>[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A subgraph definition `worfklow.definitions.subgraphs` */
|
||||||
|
export const zSubgraphDefinition = zComfyWorkflow1
|
||||||
|
.extend({
|
||||||
|
/** Unique graph ID. Automatically generated if not provided. */
|
||||||
|
id: z.string().uuid(),
|
||||||
|
revision: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
inputNode: zExportedSubgraphIONode,
|
||||||
|
outputNode: zExportedSubgraphIONode,
|
||||||
|
|
||||||
|
/** Ordered list of inputs to the subgraph itself. Similar to a reroute, with the input side in the graph, and the output side in the subgraph. */
|
||||||
|
inputs: z.array(zSubgraphIO).optional(),
|
||||||
|
/** Ordered list of outputs from the subgraph itself. Similar to a reroute, with the input side in the subgraph, and the output side in the graph. */
|
||||||
|
outputs: z.array(zSubgraphIO).optional(),
|
||||||
|
/** A list of node widgets displayed in the parent graph, on the subgraph object. */
|
||||||
|
widgets: z.array(zExposedWidget).optional(),
|
||||||
|
definitions: z
|
||||||
|
.object({
|
||||||
|
subgraphs: z.lazy(
|
||||||
|
(): z.ZodArray<
|
||||||
|
z.ZodType<
|
||||||
|
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
SubgraphDefinitionBase<ComfyWorkflow1BaseInput>
|
||||||
|
>,
|
||||||
|
'many'
|
||||||
|
> => zSubgraphDefinition.array()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ import { getSvgMetadata } from '@/scripts/metadata/svg'
|
|||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useExtensionService } from '@/services/extensionService'
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
|
import { useSubgraphService } from '@/services/subgraphService'
|
||||||
import { useWorkflowService } from '@/services/workflowService'
|
import { useWorkflowService } from '@/services/workflowService'
|
||||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useExtensionStore } from '@/stores/extensionStore'
|
import { useExtensionStore } from '@/stores/extensionStore'
|
||||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
@@ -72,7 +74,6 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
|
|||||||
|
|
||||||
import { type ComfyApi, PromptExecutionError, api } from './api'
|
import { type ComfyApi, PromptExecutionError, api } from './api'
|
||||||
import { defaultGraph } from './defaultGraph'
|
import { defaultGraph } from './defaultGraph'
|
||||||
import { pruneWidgets } from './domWidget'
|
|
||||||
import {
|
import {
|
||||||
getFlacMetadata,
|
getFlacMetadata,
|
||||||
getLatentMetadata,
|
getLatentMetadata,
|
||||||
@@ -715,25 +716,23 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#addAfterConfigureHandler() {
|
#addAfterConfigureHandler() {
|
||||||
const app = this
|
const { graph } = this
|
||||||
const onConfigure = app.graph.onConfigure
|
const { onConfigure } = graph
|
||||||
app.graph.onConfigure = function (this: LGraph, ...args) {
|
graph.onConfigure = function (...args) {
|
||||||
fixLinkInputSlots(this)
|
fixLinkInputSlots(this)
|
||||||
|
|
||||||
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
|
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
|
||||||
for (const node of app.graph.nodes) {
|
for (const node of graph.nodes) {
|
||||||
node.onGraphConfigured?.()
|
node.onGraphConfigured?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = onConfigure?.apply(this, args)
|
const r = onConfigure?.apply(this, args)
|
||||||
|
|
||||||
// Fire after onConfigure, used by primitives to generate widget using input nodes config
|
// Fire after onConfigure, used by primitives to generate widget using input nodes config
|
||||||
for (const node of app.graph.nodes) {
|
for (const node of graph.nodes) {
|
||||||
node.onAfterGraphConfigured?.()
|
node.onAfterGraphConfigured?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneWidgets(this.nodes)
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,6 +764,21 @@ export class ComfyApp {
|
|||||||
|
|
||||||
this.#graph = new LGraph()
|
this.#graph = new LGraph()
|
||||||
|
|
||||||
|
// Register the subgraph - adds type wrapper for Litegraph's `createNode` factory
|
||||||
|
this.graph.events.addEventListener('subgraph-created', (e) => {
|
||||||
|
try {
|
||||||
|
const { subgraph, data } = e.detail
|
||||||
|
useSubgraphService().registerNewSubgraph(subgraph, data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to register subgraph', err)
|
||||||
|
useToastStore().add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Failed to register subgraph',
|
||||||
|
detail: err instanceof Error ? err.message : String(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.#addAfterConfigureHandler()
|
this.#addAfterConfigureHandler()
|
||||||
|
|
||||||
this.canvas = new LGraphCanvas(canvasEl, this.graph)
|
this.canvas = new LGraphCanvas(canvasEl, this.graph)
|
||||||
@@ -777,6 +791,30 @@ export class ComfyApp {
|
|||||||
LiteGraph.alt_drag_do_clone_nodes = true
|
LiteGraph.alt_drag_do_clone_nodes = true
|
||||||
LiteGraph.macGesturesRequireMac = false
|
LiteGraph.macGesturesRequireMac = false
|
||||||
|
|
||||||
|
this.canvas.canvas.addEventListener<'litegraph:set-graph'>(
|
||||||
|
'litegraph:set-graph',
|
||||||
|
(e) => {
|
||||||
|
// Assertion: Not yet defined in litegraph.
|
||||||
|
const { newGraph } = e.detail
|
||||||
|
|
||||||
|
const nodeSet = new Set(newGraph.nodes)
|
||||||
|
const widgetStore = useDomWidgetStore()
|
||||||
|
|
||||||
|
// Assertions: UnwrapRef
|
||||||
|
for (const { widget } of widgetStore.activeWidgetStates) {
|
||||||
|
if (!nodeSet.has(widget.node)) {
|
||||||
|
widgetStore.deactivateWidget(widget.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { widget } of widgetStore.inactiveWidgetStates) {
|
||||||
|
if (nodeSet.has(widget.node)) {
|
||||||
|
widgetStore.activateWidget(widget.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
this.graph.start()
|
this.graph.start()
|
||||||
|
|
||||||
// Ensure the canvas fills the window
|
// Ensure the canvas fills the window
|
||||||
@@ -1013,6 +1051,7 @@ export class ComfyApp {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
useWorkflowService().beforeLoadNewGraph()
|
useWorkflowService().beforeLoadNewGraph()
|
||||||
|
useSubgraphService().loadSubgraphs(graphData)
|
||||||
|
|
||||||
const missingNodeTypes: MissingNodeType[] = []
|
const missingNodeTypes: MissingNodeType[] = []
|
||||||
const missingModels: ModelFile[] = []
|
const missingModels: ModelFile[] = []
|
||||||
@@ -1210,6 +1249,9 @@ export class ComfyApp {
|
|||||||
// Allow widgets to run callbacks before a prompt has been queued
|
// Allow widgets to run callbacks before a prompt has been queued
|
||||||
// e.g. random seed before every gen
|
// e.g. random seed before every gen
|
||||||
executeWidgetsCallback(this.graph.nodes, 'beforeQueued')
|
executeWidgetsCallback(this.graph.nodes, 'beforeQueued')
|
||||||
|
for (const subgraph of this.graph.subgraphs.values()) {
|
||||||
|
executeWidgetsCallback(subgraph.nodes, 'beforeQueued')
|
||||||
|
}
|
||||||
|
|
||||||
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
|
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
|
||||||
try {
|
try {
|
||||||
@@ -1252,9 +1294,13 @@ export class ComfyApp {
|
|||||||
executeWidgetsCallback(
|
executeWidgetsCallback(
|
||||||
p.workflow.nodes
|
p.workflow.nodes
|
||||||
.map((n) => this.graph.getNodeById(n.id))
|
.map((n) => this.graph.getNodeById(n.id))
|
||||||
.filter((n) => !!n) as LGraphNode[],
|
.filter((n) => !!n),
|
||||||
'afterQueued'
|
'afterQueued'
|
||||||
)
|
)
|
||||||
|
for (const subgraph of this.graph.subgraphs.values()) {
|
||||||
|
executeWidgetsCallback(subgraph.nodes, 'afterQueued')
|
||||||
|
}
|
||||||
|
|
||||||
this.canvas.draw(true, true)
|
this.canvas.draw(true, true)
|
||||||
await this.ui.queue.update()
|
await this.ui.queue.update()
|
||||||
}
|
}
|
||||||
@@ -1650,6 +1696,8 @@ export class ComfyApp {
|
|||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
executionStore.lastNodeErrors = null
|
executionStore.lastNodeErrors = null
|
||||||
executionStore.lastExecutionError = null
|
executionStore.lastExecutionError = null
|
||||||
|
|
||||||
|
useDomWidgetStore().clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
clientPosToCanvasPos(pos: Vector2): Vector2 {
|
clientPosToCanvasPos(pos: Vector2): Vector2 {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import log from 'loglevel'
|
|||||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||||
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||||
|
|
||||||
import { api } from './api'
|
import { api } from './api'
|
||||||
@@ -37,6 +38,10 @@ export class ChangeTracker {
|
|||||||
ds?: { scale: number; offset: [number, number] }
|
ds?: { scale: number; offset: [number, number] }
|
||||||
nodeOutputs?: Record<string, any>
|
nodeOutputs?: Record<string, any>
|
||||||
|
|
||||||
|
private subgraphState?: {
|
||||||
|
navigation: string[]
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
/**
|
/**
|
||||||
* The workflow that this change tracker is tracking
|
* The workflow that this change tracker is tracking
|
||||||
@@ -67,6 +72,8 @@ export class ChangeTracker {
|
|||||||
scale: app.canvas.ds.scale,
|
scale: app.canvas.ds.scale,
|
||||||
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
|
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
|
||||||
}
|
}
|
||||||
|
const navigation = useSubgraphNavigationStore().exportState()
|
||||||
|
this.subgraphState = navigation.length ? { navigation } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
restore() {
|
restore() {
|
||||||
@@ -77,6 +84,16 @@ export class ChangeTracker {
|
|||||||
if (this.nodeOutputs) {
|
if (this.nodeOutputs) {
|
||||||
app.nodeOutputs = this.nodeOutputs
|
app.nodeOutputs = this.nodeOutputs
|
||||||
}
|
}
|
||||||
|
if (this.subgraphState) {
|
||||||
|
const { navigation } = this.subgraphState
|
||||||
|
useSubgraphNavigationStore().restoreState(navigation)
|
||||||
|
|
||||||
|
const activeId = navigation.at(-1)
|
||||||
|
if (activeId) {
|
||||||
|
const subgraph = app.graph.subgraphs.get(activeId)
|
||||||
|
if (subgraph) app.canvas.setGraph(subgraph)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateModified() {
|
updateModified() {
|
||||||
@@ -376,7 +393,14 @@ export class ChangeTracker {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Compare other properties normally
|
// Compare other properties normally
|
||||||
for (const key of ['links', 'floatingLinks', 'reroutes', 'groups']) {
|
for (const key of [
|
||||||
|
'links',
|
||||||
|
'floatingLinks',
|
||||||
|
'reroutes',
|
||||||
|
'groups',
|
||||||
|
'definitions',
|
||||||
|
'subgraphs'
|
||||||
|
]) {
|
||||||
if (!_.isEqual(a[key], b[key])) {
|
if (!_.isEqual(a[key], b[key])) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -392,7 +416,12 @@ export class ChangeTracker {
|
|||||||
function sortGraphNodes(graph: ComfyWorkflowJSON) {
|
function sortGraphNodes(graph: ComfyWorkflowJSON) {
|
||||||
return {
|
return {
|
||||||
links: graph.links,
|
links: graph.links,
|
||||||
|
floatingLinks: graph.floatingLinks,
|
||||||
|
reroutes: graph.reroutes,
|
||||||
groups: graph.groups,
|
groups: graph.groups,
|
||||||
|
extra: graph.extra,
|
||||||
|
definitions: graph.definitions,
|
||||||
|
subgraphs: graph.subgraphs,
|
||||||
nodes: graph.nodes.sort((a, b) => {
|
nodes: graph.nodes.sort((a, b) => {
|
||||||
if (typeof a.id === 'number' && typeof b.id === 'number') {
|
if (typeof a.id === 'number' && typeof b.id === 'number') {
|
||||||
return a.id - b.id
|
return a.id - b.id
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
|||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { generateUUID } from '@/utils/formatUtil'
|
import { generateUUID } from '@/utils/formatUtil'
|
||||||
|
|
||||||
export interface BaseDOMWidget<V extends object | string>
|
export interface BaseDOMWidget<V extends object | string = object | string>
|
||||||
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
||||||
// ICustomWidget properties
|
// ICustomWidget properties
|
||||||
type: string
|
type: string
|
||||||
@@ -330,9 +330,8 @@ LGraphNode.prototype.addDOMWidget = function <
|
|||||||
export const pruneWidgets = (nodes: LGraphNode[]) => {
|
export const pruneWidgets = (nodes: LGraphNode[]) => {
|
||||||
const nodeSet = new Set(nodes)
|
const nodeSet = new Set(nodes)
|
||||||
const domWidgetStore = useDomWidgetStore()
|
const domWidgetStore = useDomWidgetStore()
|
||||||
for (const widgetState of domWidgetStore.widgetStates.values()) {
|
for (const { widget } of domWidgetStore.widgetStates.values()) {
|
||||||
const widget = widgetState.widget
|
if (!nodeSet.has(widget.node)) {
|
||||||
if (!nodeSet.has(widget.node as LGraphNode)) {
|
|
||||||
domWidgetStore.unregisterWidget(widget.id)
|
domWidgetStore.unregisterWidget(widget.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
type IContextMenuValue,
|
type IContextMenuValue,
|
||||||
|
LGraphBadge,
|
||||||
LGraphCanvas,
|
LGraphCanvas,
|
||||||
LGraphEventMode,
|
LGraphEventMode,
|
||||||
LGraphNode,
|
LGraphNode,
|
||||||
LiteGraph,
|
LiteGraph,
|
||||||
RenderShape,
|
RenderShape,
|
||||||
|
type Subgraph,
|
||||||
|
SubgraphNode,
|
||||||
type Vector2,
|
type Vector2,
|
||||||
createBounds
|
createBounds
|
||||||
} from '@comfyorg/litegraph'
|
} from '@comfyorg/litegraph'
|
||||||
import type {
|
import type {
|
||||||
|
ExportedSubgraphInstance,
|
||||||
ISerialisableNodeInput,
|
ISerialisableNodeInput,
|
||||||
ISerialisableNodeOutput,
|
ISerialisableNodeOutput,
|
||||||
ISerialisedNode
|
ISerialisedNode
|
||||||
@@ -35,6 +39,7 @@ import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
|||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
import { useWidgetStore } from '@/stores/widgetStore'
|
import { useWidgetStore } from '@/stores/widgetStore'
|
||||||
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import {
|
import {
|
||||||
isImageNode,
|
isImageNode,
|
||||||
@@ -56,6 +61,267 @@ export const useLitegraphService = () => {
|
|||||||
const widgetStore = useWidgetStore()
|
const widgetStore = useWidgetStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
// TODO: Dedupe `registerNodeDef`; this should remain synchronous.
|
||||||
|
function registerSubgraphNodeDef(
|
||||||
|
nodeDefV1: ComfyNodeDefV1,
|
||||||
|
subgraph: Subgraph,
|
||||||
|
instanceData: ExportedSubgraphInstance
|
||||||
|
) {
|
||||||
|
const node = class ComfyNode extends SubgraphNode {
|
||||||
|
static comfyClass: string
|
||||||
|
static override title: string
|
||||||
|
static override category: string
|
||||||
|
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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() {
|
||||||
|
super(app.graph, subgraph, instanceData)
|
||||||
|
|
||||||
|
this.#setupStrokeStyles()
|
||||||
|
this.#addInputs(ComfyNode.nodeData.inputs)
|
||||||
|
this.#addOutputs(ComfyNode.nodeData.outputs)
|
||||||
|
this.#setInitialSize()
|
||||||
|
this.serialize_widgets = true
|
||||||
|
void extensionService.invokeExtensionsAsync('nodeCreated', this)
|
||||||
|
this.badges.push(
|
||||||
|
new LGraphBadge({
|
||||||
|
text: '⇌',
|
||||||
|
fgColor: '#dad0de',
|
||||||
|
bgColor: '#b3b'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 input sockets to the node. (No widget)
|
||||||
|
*/
|
||||||
|
#addInputSocket(inputSpec: InputSpec) {
|
||||||
|
const inputName = inputSpec.name
|
||||||
|
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||||
|
const widgetConstructor = widgetStore.widgets.get(
|
||||||
|
inputSpec.widgetType ?? inputSpec.type
|
||||||
|
)
|
||||||
|
if (widgetConstructor && !inputSpec.forceInput) return
|
||||||
|
|
||||||
|
this.addInput(inputName, inputSpec.type, {
|
||||||
|
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
|
||||||
|
localized_name: st(nameKey, inputName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Add a widget to the node. For both primitive types and custom widgets
|
||||||
|
* (unless `socketless`), an input socket is also added.
|
||||||
|
*/
|
||||||
|
#addInputWidget(inputSpec: InputSpec) {
|
||||||
|
const widgetInputSpec = { ...inputSpec }
|
||||||
|
if (inputSpec.widgetType) {
|
||||||
|
widgetInputSpec.type = inputSpec.widgetType
|
||||||
|
}
|
||||||
|
const inputName = inputSpec.name
|
||||||
|
const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name`
|
||||||
|
const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type)
|
||||||
|
if (!widgetConstructor || inputSpec.forceInput) return
|
||||||
|
|
||||||
|
const {
|
||||||
|
widget,
|
||||||
|
minWidth = 1,
|
||||||
|
minHeight = 1
|
||||||
|
} = widgetConstructor(
|
||||||
|
this,
|
||||||
|
inputName,
|
||||||
|
transformInputSpecV2ToV1(widgetInputSpec),
|
||||||
|
app
|
||||||
|
) ?? {}
|
||||||
|
|
||||||
|
if (widget) {
|
||||||
|
widget.label = st(nameKey, widget.label ?? inputName)
|
||||||
|
widget.options ??= {}
|
||||||
|
Object.assign(widget.options, {
|
||||||
|
advanced: inputSpec.advanced,
|
||||||
|
hidden: inputSpec.hidden
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget?.options?.socketless) {
|
||||||
|
const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec)
|
||||||
|
this.addInput(inputName, inputSpec.type, {
|
||||||
|
shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined,
|
||||||
|
localized_name: st(nameKey, inputName),
|
||||||
|
widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#initialMinSize.width = Math.max(
|
||||||
|
this.#initialMinSize.width,
|
||||||
|
minWidth
|
||||||
|
)
|
||||||
|
this.#initialMinSize.height = Math.max(
|
||||||
|
this.#initialMinSize.height,
|
||||||
|
minHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Add inputs to the node.
|
||||||
|
*/
|
||||||
|
#addInputs(inputs: Record<string, InputSpec>) {
|
||||||
|
for (const inputSpec of Object.values(inputs))
|
||||||
|
this.#addInputSocket(inputSpec)
|
||||||
|
for (const inputSpec of Object.values(inputs))
|
||||||
|
this.#addInputWidget(inputSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
// e.g.
|
||||||
|
// - type ("INT"); name ("Positive") => translate name
|
||||||
|
// - type ("FLOAT"); name ("FLOAT") => translate type
|
||||||
|
localized_name:
|
||||||
|
type !== name ? st(nameKey, name) : st(typeKey, name)
|
||||||
|
}
|
||||||
|
this.addOutput(name, type, outputOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal Set the initial size of the node.
|
||||||
|
*/
|
||||||
|
#setInitialSize() {
|
||||||
|
const s = this.computeSize()
|
||||||
|
// Expand the width a little to fit widget values on screen.
|
||||||
|
const pad =
|
||||||
|
this.widgets?.length &&
|
||||||
|
!useSettingStore().get('LiteGraph.Node.DefaultPadding')
|
||||||
|
s[0] = Math.max(this.#initialMinSize.width, s[0] + (pad ? 60 : 0))
|
||||||
|
s[1] = Math.max(this.#initialMinSize.height, s[1])
|
||||||
|
this.setSize(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the node from a serialised node. Keep 'name', 'type', 'shape',
|
||||||
|
* and 'localized_name' information from the original node definition.
|
||||||
|
*/
|
||||||
|
override configure(data: ISerialisedNode): void {
|
||||||
|
const RESERVED_KEYS = ['name', 'type', 'shape', 'localized_name']
|
||||||
|
|
||||||
|
// Note: input name is unique in a node definition, so we can lookup
|
||||||
|
// input by name.
|
||||||
|
const inputByName = new Map<string, ISerialisableNodeInput>(
|
||||||
|
data.inputs?.map((input) => [input.name, input]) ?? []
|
||||||
|
)
|
||||||
|
// Inputs defined by the node definition.
|
||||||
|
const definedInputNames = new Set(
|
||||||
|
this.inputs.map((input) => input.name)
|
||||||
|
)
|
||||||
|
const definedInputs = this.inputs.map((input) => {
|
||||||
|
const inputData = inputByName.get(input.name)
|
||||||
|
return inputData
|
||||||
|
? {
|
||||||
|
...inputData,
|
||||||
|
// Whether the input has associated widget follows the
|
||||||
|
// original node definition.
|
||||||
|
..._.pick(input, RESERVED_KEYS.concat('widget'))
|
||||||
|
}
|
||||||
|
: input
|
||||||
|
})
|
||||||
|
// Extra inputs that potentially dynamically added by custom js logic.
|
||||||
|
const extraInputs = data.inputs?.filter(
|
||||||
|
(input) => !definedInputNames.has(input.name)
|
||||||
|
)
|
||||||
|
data.inputs = [...definedInputs, ...(extraInputs ?? [])]
|
||||||
|
|
||||||
|
// Note: output name is not unique, so we cannot lookup output by name.
|
||||||
|
// Use index instead.
|
||||||
|
data.outputs = _.zip(this.outputs, data.outputs).map(
|
||||||
|
([output, outputData]) => {
|
||||||
|
// If there are extra outputs in the serialised node, use them directly.
|
||||||
|
// There are currently custom nodes that dynamically add outputs via
|
||||||
|
// js logic.
|
||||||
|
if (!output) return outputData as ISerialisableNodeOutput
|
||||||
|
|
||||||
|
return outputData
|
||||||
|
? {
|
||||||
|
...outputData,
|
||||||
|
..._.pick(output, RESERVED_KEYS)
|
||||||
|
}
|
||||||
|
: output
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data.widgets_values = migrateWidgetsValues(
|
||||||
|
ComfyNode.nodeData.inputs,
|
||||||
|
this.widgets ?? [],
|
||||||
|
data.widgets_values ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
super.configure(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addNodeContextMenuHandler(node)
|
||||||
|
addDrawBackgroundHandler(node)
|
||||||
|
addNodeKeyHandler(node)
|
||||||
|
// Note: Some extensions expects node.comfyClass to be set in
|
||||||
|
// `beforeRegisterNodeDef`.
|
||||||
|
node.prototype.comfyClass = nodeDefV1.name
|
||||||
|
node.comfyClass = nodeDefV1.name
|
||||||
|
|
||||||
|
const nodeDef = new ComfyNodeDefImpl(nodeDefV1)
|
||||||
|
node.nodeData = nodeDef
|
||||||
|
LiteGraph.registerNodeType(subgraph.id, node)
|
||||||
|
// Note: Do not following assignments before `LiteGraph.registerNodeType`
|
||||||
|
// because `registerNodeType` will overwrite the assignments.
|
||||||
|
node.category = nodeDef.category
|
||||||
|
node.title = nodeDef.display_name || nodeDef.name
|
||||||
|
}
|
||||||
|
|
||||||
async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) {
|
async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) {
|
||||||
const node = class ComfyNode extends LGraphNode {
|
const node = class ComfyNode extends LGraphNode {
|
||||||
static comfyClass: string
|
static comfyClass: string
|
||||||
@@ -622,8 +888,10 @@ export const useLitegraphService = () => {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const graph = useWorkflowStore().activeSubgraph ?? app.graph
|
||||||
|
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
app.graph.add(node)
|
graph.add(node)
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@@ -665,6 +933,7 @@ export const useLitegraphService = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
registerNodeDef,
|
registerNodeDef,
|
||||||
|
registerSubgraphNodeDef,
|
||||||
addNodeOnGraph,
|
addNodeOnGraph,
|
||||||
getCanvasCenter,
|
getCanvasCenter,
|
||||||
goToNode,
|
goToNode,
|
||||||
|
|||||||
91
src/services/subgraphService.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
type ExportedSubgraph,
|
||||||
|
type ExportedSubgraphInstance,
|
||||||
|
type Subgraph
|
||||||
|
} from '@comfyorg/litegraph'
|
||||||
|
|
||||||
|
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||||
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
|
|
||||||
|
import { useLitegraphService } from './litegraphService'
|
||||||
|
|
||||||
|
export const useSubgraphService = () => {
|
||||||
|
const nodeDefStore = useNodeDefStore()
|
||||||
|
|
||||||
|
/** Loads a single subgraph definition and registers it with the node def store */
|
||||||
|
function registerLitegraphNode(
|
||||||
|
nodeDef: ComfyNodeDefV1,
|
||||||
|
subgraph: Subgraph,
|
||||||
|
exportedSubgraph: ExportedSubgraph
|
||||||
|
) {
|
||||||
|
const instanceData: ExportedSubgraphInstance = {
|
||||||
|
id: -1,
|
||||||
|
type: exportedSubgraph.id,
|
||||||
|
pos: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
flags: {},
|
||||||
|
order: 0,
|
||||||
|
mode: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
useLitegraphService().registerSubgraphNodeDef(
|
||||||
|
nodeDef,
|
||||||
|
subgraph,
|
||||||
|
instanceData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNodeDef(exportedSubgraph: ExportedSubgraph) {
|
||||||
|
const { id, name } = exportedSubgraph
|
||||||
|
|
||||||
|
const nodeDef: ComfyNodeDefV1 = {
|
||||||
|
input: { required: {} },
|
||||||
|
output: [],
|
||||||
|
output_is_list: [],
|
||||||
|
output_name: [],
|
||||||
|
output_tooltips: [],
|
||||||
|
name: id,
|
||||||
|
display_name: name,
|
||||||
|
description: `Subgraph node for ${name}`,
|
||||||
|
category: 'subgraph',
|
||||||
|
output_node: false,
|
||||||
|
python_module: 'nodes'
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeDefStore.addNodeDef(nodeDef)
|
||||||
|
return nodeDef
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loads all exported subgraph definitions from workflow */
|
||||||
|
function loadSubgraphs(graphData: ComfyWorkflowJSON) {
|
||||||
|
const subgraphs = graphData.definitions?.subgraphs
|
||||||
|
if (!subgraphs) return
|
||||||
|
|
||||||
|
// Assertion: overriding Zod schema
|
||||||
|
for (const subgraphData of subgraphs as ExportedSubgraph[]) {
|
||||||
|
const subgraph =
|
||||||
|
comfyApp.graph.subgraphs.get(subgraphData.id) ??
|
||||||
|
comfyApp.graph.createSubgraph(subgraphData)
|
||||||
|
|
||||||
|
registerNewSubgraph(subgraph, subgraphData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers a new subgraph (e.g. user converted from nodes) */
|
||||||
|
function registerNewSubgraph(
|
||||||
|
subgraph: Subgraph,
|
||||||
|
exportedSubgraph: ExportedSubgraph
|
||||||
|
) {
|
||||||
|
const nodeDef = createNodeDef(exportedSubgraph)
|
||||||
|
registerLitegraphNode(nodeDef, subgraph, exportedSubgraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadSubgraphs,
|
||||||
|
registerNewSubgraph
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||||
import { downloadBlob } from '@/scripts/utils'
|
import { downloadBlob } from '@/scripts/utils'
|
||||||
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||||
@@ -20,6 +21,7 @@ export const useWorkflowService = () => {
|
|||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
|
const domWidgetStore = useDomWidgetStore()
|
||||||
|
|
||||||
async function getFilename(defaultName: string): Promise<string | null> {
|
async function getFilename(defaultName: string): Promise<string | null> {
|
||||||
if (settingStore.get('Comfy.PromptFilename')) {
|
if (settingStore.get('Comfy.PromptFilename')) {
|
||||||
@@ -285,11 +287,8 @@ export const useWorkflowService = () => {
|
|||||||
*/
|
*/
|
||||||
const beforeLoadNewGraph = () => {
|
const beforeLoadNewGraph = () => {
|
||||||
// Use workspaceStore here as it is patched in unit tests.
|
// Use workspaceStore here as it is patched in unit tests.
|
||||||
const workflowStore = useWorkspaceStore().workflow
|
useWorkspaceStore().workflow.activeWorkflow?.changeTracker?.store()
|
||||||
const activeWorkflow = workflowStore.activeWorkflow
|
domWidgetStore.clear()
|
||||||
if (activeWorkflow) {
|
|
||||||
activeWorkflow.changeTracker.store()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -345,8 +344,7 @@ export const useWorkflowService = () => {
|
|||||||
options: { position?: Vector2 } = {}
|
options: { position?: Vector2 } = {}
|
||||||
) => {
|
) => {
|
||||||
const loadedWorkflow = await workflow.load()
|
const loadedWorkflow = await workflow.load()
|
||||||
const data = loadedWorkflow.initialState
|
const workflowJSON = toRaw(loadedWorkflow.initialState)
|
||||||
const workflowJSON = data
|
|
||||||
const old = localStorage.getItem('litegrapheditor_clipboard')
|
const old = localStorage.getItem('litegrapheditor_clipboard')
|
||||||
// unknown conversion: ComfyWorkflowJSON is stricter than LiteGraph's
|
// unknown conversion: ComfyWorkflowJSON is stricter than LiteGraph's
|
||||||
// serialisation schema.
|
// serialisation schema.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Stores all DOM widgets that are used in the canvas.
|
* Stores all DOM widgets that are used in the canvas.
|
||||||
*/
|
*/
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { type Raw, markRaw, ref } from 'vue'
|
import { type Raw, computed, markRaw, ref } from 'vue'
|
||||||
|
|
||||||
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
||||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||||
@@ -13,11 +13,20 @@ export interface DomWidgetState extends PositionConfig {
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
zIndex: number
|
zIndex: number
|
||||||
|
/** If the widget belongs to the current graph/subgraph. */
|
||||||
|
active: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDomWidgetStore = defineStore('domWidget', () => {
|
export const useDomWidgetStore = defineStore('domWidget', () => {
|
||||||
const widgetStates = ref<Map<string, DomWidgetState>>(new Map())
|
const widgetStates = ref<Map<string, DomWidgetState>>(new Map())
|
||||||
|
|
||||||
|
const activeWidgetStates = computed(() =>
|
||||||
|
[...widgetStates.value.values()].filter((state) => state.active)
|
||||||
|
)
|
||||||
|
const inactiveWidgetStates = computed(() =>
|
||||||
|
[...widgetStates.value.values()].filter((state) => !state.active)
|
||||||
|
)
|
||||||
|
|
||||||
// Register a widget with the store
|
// Register a widget with the store
|
||||||
const registerWidget = <V extends object | string>(
|
const registerWidget = <V extends object | string>(
|
||||||
widget: BaseDOMWidget<V>
|
widget: BaseDOMWidget<V>
|
||||||
@@ -28,7 +37,8 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
pos: [0, 0],
|
pos: [0, 0],
|
||||||
size: [0, 0]
|
size: [0, 0],
|
||||||
|
active: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +47,28 @@ export const useDomWidgetStore = defineStore('domWidget', () => {
|
|||||||
widgetStates.value.delete(widgetId)
|
widgetStates.value.delete(widgetId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activateWidget = (widgetId: string) => {
|
||||||
|
const state = widgetStates.value.get(widgetId)
|
||||||
|
if (state) state.active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deactivateWidget = (widgetId: string) => {
|
||||||
|
const state = widgetStates.value.get(widgetId)
|
||||||
|
if (state) state.active = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
widgetStates.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
widgetStates,
|
widgetStates,
|
||||||
|
activeWidgetStates,
|
||||||
|
inactiveWidgetStates,
|
||||||
registerWidget,
|
registerWidget,
|
||||||
unregisterWidget
|
unregisterWidget,
|
||||||
|
activateWidget,
|
||||||
|
deactivateWidget,
|
||||||
|
clear
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { LGraph, Subgraph } from '@comfyorg/litegraph'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
@@ -20,9 +21,9 @@ import type {
|
|||||||
NodeId
|
NodeId
|
||||||
} from '@/schemas/comfyWorkflowSchema'
|
} from '@/schemas/comfyWorkflowSchema'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
|
|
||||||
import { ComfyWorkflow } from './workflowStore'
|
import { useCanvasStore } from './graphStore'
|
||||||
|
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
|
||||||
|
|
||||||
export interface QueuedPrompt {
|
export interface QueuedPrompt {
|
||||||
/**
|
/**
|
||||||
@@ -37,6 +38,9 @@ export interface QueuedPrompt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useExecutionStore = defineStore('execution', () => {
|
export const useExecutionStore = defineStore('execution', () => {
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
const clientId = ref<string | null>(null)
|
const clientId = ref<string | null>(null)
|
||||||
const activePromptId = ref<string | null>(null)
|
const activePromptId = ref<string | null>(null)
|
||||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||||
@@ -54,12 +58,64 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
if (!canvasState) return null
|
if (!canvasState) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
canvasState.nodes.find(
|
canvasState.nodes.find((n) => String(n.id) === executingNodeId.value) ??
|
||||||
(n: ComfyNode) => String(n.id) === executingNodeId.value
|
null
|
||||||
) ?? null
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => {
|
||||||
|
const node = graph.getNodeById(id)
|
||||||
|
if (node?.isSubgraphNode()) return node.subgraph
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively get the subgraph objects for the given subgraph instance IDs
|
||||||
|
* @param currentGraph The current graph
|
||||||
|
* @param subgraphNodeIds The instance IDs
|
||||||
|
* @param subgraphs The subgraphs
|
||||||
|
* @returns The subgraphs that correspond to each of the instance IDs.
|
||||||
|
*/
|
||||||
|
const getSubgraphsFromInstanceIds = (
|
||||||
|
currentGraph: LGraph | Subgraph,
|
||||||
|
subgraphNodeIds: string[],
|
||||||
|
subgraphs: Subgraph[] = []
|
||||||
|
): Subgraph[] => {
|
||||||
|
// Last segment is the node portion; nothing to do.
|
||||||
|
if (subgraphNodeIds.length === 1) return subgraphs
|
||||||
|
|
||||||
|
const currentPart = subgraphNodeIds.shift()
|
||||||
|
if (currentPart === undefined) return subgraphs
|
||||||
|
|
||||||
|
const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph)
|
||||||
|
if (!subgraph) throw new Error(`Subgraph not found: ${currentPart}`)
|
||||||
|
|
||||||
|
subgraphs.push(subgraph)
|
||||||
|
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionIdToCurrentId = (id: string) => {
|
||||||
|
const subgraph = workflowStore.activeSubgraph
|
||||||
|
|
||||||
|
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||||
|
if (!id.includes(':')) {
|
||||||
|
return !subgraph ? id : undefined
|
||||||
|
} else if (!subgraph) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||||
|
const subgraphNodeIds = id.split(':')
|
||||||
|
|
||||||
|
// If the last subgraph is the active subgraph, return the node ID
|
||||||
|
const subgraphs = getSubgraphsFromInstanceIds(
|
||||||
|
subgraph.rootGraph,
|
||||||
|
subgraphNodeIds
|
||||||
|
)
|
||||||
|
if (subgraphs.at(-1) === subgraph) {
|
||||||
|
return subgraphNodeIds.at(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is the progress of the currently executing node, if any
|
// This is the progress of the currently executing node, if any
|
||||||
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
|
||||||
const executingNodeProgress = computed(() =>
|
const executingNodeProgress = computed(() =>
|
||||||
@@ -132,7 +188,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
activePrompt.value.nodes[e.detail.node] = true
|
activePrompt.value.nodes[e.detail.node] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuting(e: CustomEvent<NodeId | null>) {
|
function handleExecuting(e: CustomEvent<NodeId | null>): void {
|
||||||
// Clear the current node progress when a new node starts executing
|
// Clear the current node progress when a new node starts executing
|
||||||
_executingNodeProgress.value = null
|
_executingNodeProgress.value = null
|
||||||
|
|
||||||
@@ -142,12 +198,16 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
// Seems sometimes nodes that are cached fire executing but not executed
|
// Seems sometimes nodes that are cached fire executing but not executed
|
||||||
activePrompt.value.nodes[executingNodeId.value] = true
|
activePrompt.value.nodes[executingNodeId.value] = true
|
||||||
}
|
}
|
||||||
executingNodeId.value = e.detail
|
if (typeof e.detail === 'string') {
|
||||||
if (executingNodeId.value === null) {
|
executingNodeId.value = executionIdToCurrentId(e.detail) ?? null
|
||||||
if (activePromptId.value) {
|
} else {
|
||||||
delete queuedPrompts.value[activePromptId.value]
|
executingNodeId.value = e.detail
|
||||||
|
if (executingNodeId.value === null) {
|
||||||
|
if (activePromptId.value) {
|
||||||
|
delete queuedPrompts.value[activePromptId.value]
|
||||||
|
}
|
||||||
|
activePromptId.value = null
|
||||||
}
|
}
|
||||||
activePromptId.value = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,19 +228,31 @@ export const useExecutionStore = defineStore('execution', () => {
|
|||||||
lastExecutionError.value = e.detail
|
lastExecutionError.value = e.detail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeIdIfExecuting(nodeId: string | number) {
|
||||||
|
const nodeIdStr = String(nodeId)
|
||||||
|
return nodeIdStr.includes(':')
|
||||||
|
? workflowStore.executionIdToCurrentId(nodeIdStr)
|
||||||
|
: nodeIdStr
|
||||||
|
}
|
||||||
|
|
||||||
function handleProgressText(e: CustomEvent<ProgressTextWsMessage>) {
|
function handleProgressText(e: CustomEvent<ProgressTextWsMessage>) {
|
||||||
const { nodeId, text } = e.detail
|
const { nodeId, text } = e.detail
|
||||||
if (!text || !nodeId) return
|
if (!text || !nodeId) return
|
||||||
|
|
||||||
const node = app.graph.getNodeById(nodeId)
|
// Handle hierarchical node IDs for subgraphs
|
||||||
|
const currentId = getNodeIdIfExecuting(nodeId)
|
||||||
|
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
useNodeProgressText().showTextPreview(node, text)
|
useNodeProgressText().showTextPreview(node, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
|
function handleDisplayComponent(e: CustomEvent<DisplayComponentWsMessage>) {
|
||||||
const { node_id, component, props = {} } = e.detail
|
const { node_id: nodeId, component, props = {} } = e.detail
|
||||||
const node = app.graph.getNodeById(node_id)
|
|
||||||
|
// Handle hierarchical node IDs for subgraphs
|
||||||
|
const currentId = getNodeIdIfExecuting(nodeId)
|
||||||
|
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
if (component === 'ChatHistoryWidget') {
|
if (component === 'ChatHistoryWidget') {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Positionable } from '@comfyorg/litegraph/dist/interfaces'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
|
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
|
||||||
|
|
||||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'
|
||||||
|
|
||||||
export const useTitleEditorStore = defineStore('titleEditor', () => {
|
export const useTitleEditorStore = defineStore('titleEditor', () => {
|
||||||
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
|
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
|
||||||
@@ -31,6 +31,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||||||
|
|
||||||
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
|
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
|
||||||
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
|
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
|
||||||
|
const rerouteSelected = computed(() => selectedItems.value.some(isReroute))
|
||||||
|
|
||||||
const getCanvas = () => {
|
const getCanvas = () => {
|
||||||
if (!canvas.value) throw new Error('getCanvas: canvas is null')
|
if (!canvas.value) throw new Error('getCanvas: canvas is null')
|
||||||
@@ -42,6 +43,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||||||
selectedItems,
|
selectedItems,
|
||||||
nodeSelected,
|
nodeSelected,
|
||||||
groupSelected,
|
groupSelected,
|
||||||
|
rerouteSelected,
|
||||||
updateSelectedItems,
|
updateSelectedItems,
|
||||||
getCanvas
|
getCanvas
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,8 +306,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
}
|
}
|
||||||
function fromLGraphNode(node: LGraphNode): ComfyNodeDefImpl | null {
|
function fromLGraphNode(node: LGraphNode): ComfyNodeDefImpl | null {
|
||||||
// Frontend-only nodes don't have nodeDef
|
// Frontend-only nodes don't have nodeDef
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// @ts-expect-error Optional chaining used in index
|
||||||
// @ts-ignore Optional chaining used in index
|
|
||||||
return nodeDefsByName.value[node.constructor?.nodeData?.name] ?? null
|
return nodeDefsByName.value[node.constructor?.nodeData?.name] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
src/stores/subgraphNavigationStore.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { Subgraph } from '@comfyorg/litegraph'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, shallowReactive, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { isNonNullish } from '@/utils/typeGuardUtil'
|
||||||
|
|
||||||
|
import { useWorkflowStore } from './workflowStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current subgraph navigation state; a stack representing subgraph
|
||||||
|
* navigation history from the root graph to the subgraph that is currently
|
||||||
|
* open.
|
||||||
|
*/
|
||||||
|
export const useSubgraphNavigationStore = defineStore(
|
||||||
|
'subgraphNavigation',
|
||||||
|
() => {
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
|
||||||
|
/** The currently opened subgraph. */
|
||||||
|
const activeSubgraph = shallowRef<Subgraph>()
|
||||||
|
|
||||||
|
/** The stack of subgraph IDs from the root graph to the currently opened subgraph. */
|
||||||
|
const idStack = shallowReactive<string[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stack representing subgraph navigation history from the root graph to
|
||||||
|
* the current opened subgraph.
|
||||||
|
*/
|
||||||
|
const navigationStack = computed(() =>
|
||||||
|
idStack.map((id) => app.graph.subgraphs.get(id)).filter(isNonNullish)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the navigation stack from a list of subgraph IDs.
|
||||||
|
* @param subgraphIds The list of subgraph IDs to restore the navigation stack from.
|
||||||
|
* @see exportState
|
||||||
|
*/
|
||||||
|
const restoreState = (subgraphIds: string[]) => {
|
||||||
|
idStack.length = 0
|
||||||
|
for (const id of subgraphIds) idStack.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the navigation stack as a list of subgraph IDs.
|
||||||
|
* @returns The list of subgraph IDs, ending with the currently active subgraph.
|
||||||
|
* @see restoreState
|
||||||
|
*/
|
||||||
|
const exportState = () => [...idStack]
|
||||||
|
|
||||||
|
// Reset on workflow change
|
||||||
|
watch(
|
||||||
|
() => workflowStore.activeWorkflow,
|
||||||
|
() => (idStack.length = 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update navigation stack when opened subgraph changes
|
||||||
|
watch(
|
||||||
|
() => workflowStore.activeSubgraph,
|
||||||
|
(subgraph) => {
|
||||||
|
// Navigated back to the root graph
|
||||||
|
if (!subgraph) {
|
||||||
|
idStack.length = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = idStack.lastIndexOf(subgraph.id)
|
||||||
|
const lastIndex = idStack.length - 1
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
// Opened a new subgraph
|
||||||
|
idStack.push(subgraph.id)
|
||||||
|
} else if (index !== lastIndex) {
|
||||||
|
// Navigated to a different subgraph
|
||||||
|
idStack.splice(index + 1, lastIndex - index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeSubgraph,
|
||||||
|
navigationStack,
|
||||||
|
restoreState,
|
||||||
|
exportState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import type { LGraph, Subgraph } from '@comfyorg/litegraph'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, markRaw, ref, watch } from 'vue'
|
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
@@ -156,12 +157,12 @@ export interface WorkflowStore {
|
|||||||
syncWorkflows: (dir?: string) => Promise<void>
|
syncWorkflows: (dir?: string) => Promise<void>
|
||||||
reorderWorkflows: (from: number, to: number) => void
|
reorderWorkflows: (from: number, to: number) => void
|
||||||
|
|
||||||
/** An ordered list of all parent subgraphs, ending with the current subgraph. */
|
|
||||||
subgraphNamePath: string[]
|
|
||||||
/** `true` if any subgraph is currently being viewed. */
|
/** `true` if any subgraph is currently being viewed. */
|
||||||
isSubgraphActive: boolean
|
isSubgraphActive: boolean
|
||||||
|
activeSubgraph: Subgraph | undefined
|
||||||
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
|
||||||
updateActiveGraph: () => void
|
updateActiveGraph: () => void
|
||||||
|
executionIdToCurrentId: (id: string) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWorkflowStore = defineStore('workflow', () => {
|
export const useWorkflowStore = defineStore('workflow', () => {
|
||||||
@@ -427,24 +428,61 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @see WorkflowStore.subgraphNamePath */
|
|
||||||
const subgraphNamePath = ref<string[]>([])
|
|
||||||
/** @see WorkflowStore.isSubgraphActive */
|
/** @see WorkflowStore.isSubgraphActive */
|
||||||
const isSubgraphActive = ref(false)
|
const isSubgraphActive = ref(false)
|
||||||
|
|
||||||
|
/** @see WorkflowStore.activeSubgraph */
|
||||||
|
const activeSubgraph = shallowRef<Raw<Subgraph>>()
|
||||||
|
|
||||||
/** @see WorkflowStore.updateActiveGraph */
|
/** @see WorkflowStore.updateActiveGraph */
|
||||||
const updateActiveGraph = () => {
|
const updateActiveGraph = () => {
|
||||||
|
const subgraph = comfyApp.canvas?.subgraph
|
||||||
|
activeSubgraph.value = subgraph ? markRaw(subgraph) : undefined
|
||||||
if (!comfyApp.canvas) return
|
if (!comfyApp.canvas) return
|
||||||
|
|
||||||
const { subgraph } = comfyApp.canvas
|
|
||||||
isSubgraphActive.value = isSubgraph(subgraph)
|
isSubgraphActive.value = isSubgraph(subgraph)
|
||||||
|
}
|
||||||
|
|
||||||
if (subgraph) {
|
const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => {
|
||||||
const [, ...pathFromRoot] = subgraph.pathToRootGraph
|
const node = graph.getNodeById(id)
|
||||||
|
if (node?.isSubgraphNode()) return node.subgraph
|
||||||
|
}
|
||||||
|
|
||||||
subgraphNamePath.value = pathFromRoot.map((graph) => graph.name)
|
const getSubgraphsFromInstanceIds = (
|
||||||
} else {
|
currentGraph: LGraph | Subgraph,
|
||||||
subgraphNamePath.value = []
|
subgraphNodeIds: string[],
|
||||||
|
subgraphs: Subgraph[] = []
|
||||||
|
): Subgraph[] => {
|
||||||
|
const currentPart = subgraphNodeIds.shift()
|
||||||
|
if (currentPart === undefined) return subgraphs
|
||||||
|
|
||||||
|
const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph)
|
||||||
|
if (subgraph === undefined) throw new Error('Subgraph not found')
|
||||||
|
|
||||||
|
subgraphs.push(subgraph)
|
||||||
|
return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionIdToCurrentId = (id: string) => {
|
||||||
|
const subgraph = activeSubgraph.value
|
||||||
|
|
||||||
|
// Short-circuit: ID belongs to the parent workflow / no active subgraph
|
||||||
|
if (!id.includes(':')) {
|
||||||
|
return !subgraph ? id : undefined
|
||||||
|
} else if (!subgraph) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the hierarchical ID (e.g., "123:456:789")
|
||||||
|
const subgraphNodeIds = id.split(':')
|
||||||
|
|
||||||
|
// Start from the root graph
|
||||||
|
const { graph } = comfyApp
|
||||||
|
|
||||||
|
// If the last subgraph is the active subgraph, return the node ID
|
||||||
|
const subgraphs = getSubgraphsFromInstanceIds(graph, subgraphNodeIds)
|
||||||
|
if (subgraphs.at(-1) === subgraph) {
|
||||||
|
return subgraphNodeIds.at(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,9 +511,10 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
|||||||
getWorkflowByPath,
|
getWorkflowByPath,
|
||||||
syncWorkflows,
|
syncWorkflows,
|
||||||
|
|
||||||
subgraphNamePath,
|
|
||||||
isSubgraphActive,
|
isSubgraphActive,
|
||||||
updateActiveGraph
|
activeSubgraph,
|
||||||
|
updateActiveGraph,
|
||||||
|
executionIdToCurrentId
|
||||||
}
|
}
|
||||||
}) satisfies () => WorkflowStore
|
}) satisfies () => WorkflowStore
|
||||||
|
|
||||||
|
|||||||
6
src/types/litegraph-augmentation.d.ts
vendored
@@ -60,6 +60,7 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
|||||||
* ComfyUI extensions of litegraph
|
* ComfyUI extensions of litegraph
|
||||||
*/
|
*/
|
||||||
declare module '@comfyorg/litegraph' {
|
declare module '@comfyorg/litegraph' {
|
||||||
|
import type { ExecutableLGraphNode } from '@comfyorg/litegraph'
|
||||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||||
|
|
||||||
interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||||
@@ -88,7 +89,10 @@ declare module '@comfyorg/litegraph' {
|
|||||||
/** @deprecated groupNode */
|
/** @deprecated groupNode */
|
||||||
setInnerNodes?(nodes: LGraphNode[]): void
|
setInnerNodes?(nodes: LGraphNode[]): void
|
||||||
/** Originally a group node API. */
|
/** Originally a group node API. */
|
||||||
getInnerNodes?(): LGraphNode[]
|
getInnerNodes?(
|
||||||
|
nodes?: ExecutableLGraphNode[],
|
||||||
|
subgraphs?: WeakSet<LGraphNode>
|
||||||
|
): ExecutableLGraphNode[]
|
||||||
/** @deprecated groupNode */
|
/** @deprecated groupNode */
|
||||||
convertToNodes?(): LGraphNode[]
|
convertToNodes?(): LGraphNode[]
|
||||||
recreate?(): Promise<LGraphNode>
|
recreate?(): Promise<LGraphNode>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { LGraph, NodeId } from '@comfyorg/litegraph'
|
import type { LGraph, NodeId } from '@comfyorg/litegraph'
|
||||||
import { LGraphEventMode } from '@comfyorg/litegraph'
|
import {
|
||||||
|
ExecutableNodeDTO,
|
||||||
|
LGraphEventMode,
|
||||||
|
SubgraphNode
|
||||||
|
} from '@comfyorg/litegraph'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ComfyApiWorkflow,
|
ComfyApiWorkflow,
|
||||||
@@ -74,20 +78,31 @@ export const graphToPrompt = async (
|
|||||||
workflow.extra ??= {}
|
workflow.extra ??= {}
|
||||||
workflow.extra.frontendVersion = __COMFYUI_FRONTEND_VERSION__
|
workflow.extra.frontendVersion = __COMFYUI_FRONTEND_VERSION__
|
||||||
|
|
||||||
|
const computedNodeDtos = graph
|
||||||
|
.computeExecutionOrder(false)
|
||||||
|
.map(
|
||||||
|
(node) =>
|
||||||
|
new ExecutableNodeDTO(
|
||||||
|
node,
|
||||||
|
[],
|
||||||
|
node instanceof SubgraphNode ? node : undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
let output: ComfyApiWorkflow = {}
|
let output: ComfyApiWorkflow = {}
|
||||||
// Process nodes in order of execution
|
// Process nodes in order of execution
|
||||||
for (const outerNode of graph.computeExecutionOrder(false)) {
|
for (const outerNode of computedNodeDtos) {
|
||||||
const skipNode =
|
// Don't serialize muted nodes
|
||||||
|
if (
|
||||||
outerNode.mode === LGraphEventMode.NEVER ||
|
outerNode.mode === LGraphEventMode.NEVER ||
|
||||||
outerNode.mode === LGraphEventMode.BYPASS
|
outerNode.mode === LGraphEventMode.BYPASS
|
||||||
const innerNodes =
|
) {
|
||||||
!skipNode && outerNode.getInnerNodes
|
continue
|
||||||
? outerNode.getInnerNodes()
|
}
|
||||||
: [outerNode]
|
|
||||||
for (const node of innerNodes) {
|
for (const node of outerNode.getInnerNodes()) {
|
||||||
if (
|
if (
|
||||||
node.isVirtualNode ||
|
node.isVirtualNode ||
|
||||||
// Don't serialize muted nodes
|
|
||||||
node.mode === LGraphEventMode.NEVER ||
|
node.mode === LGraphEventMode.NEVER ||
|
||||||
node.mode === LGraphEventMode.BYPASS
|
node.mode === LGraphEventMode.BYPASS
|
||||||
) {
|
) {
|
||||||
@@ -120,55 +135,14 @@ export const graphToPrompt = async (
|
|||||||
|
|
||||||
// Store all node links
|
// Store all node links
|
||||||
for (const [i, input] of node.inputs.entries()) {
|
for (const [i, input] of node.inputs.entries()) {
|
||||||
let parent = node.getInputNode(i)
|
const resolvedInput = node.resolveInput(i)
|
||||||
if (!parent) continue
|
if (!resolvedInput) continue
|
||||||
|
|
||||||
let link = node.getInputLink(i)
|
inputs[input.name] = [
|
||||||
while (
|
String(resolvedInput.origin_id),
|
||||||
parent?.mode === LGraphEventMode.BYPASS ||
|
// @ts-expect-error link.origin_slot is already number.
|
||||||
parent?.isVirtualNode
|
parseInt(resolvedInput.origin_slot)
|
||||||
) {
|
]
|
||||||
if (!link) break
|
|
||||||
|
|
||||||
if (parent.isVirtualNode) {
|
|
||||||
link = parent.getInputLink(link.origin_slot)
|
|
||||||
if (!link) break
|
|
||||||
|
|
||||||
parent = parent.getInputNode(link.target_slot)
|
|
||||||
if (!parent) break
|
|
||||||
} else if (!parent.inputs) {
|
|
||||||
// Maintains existing behaviour if parent.getInputLink is overriden
|
|
||||||
break
|
|
||||||
} else if (parent.mode === LGraphEventMode.BYPASS) {
|
|
||||||
// Bypass nodes by finding first input with matching type
|
|
||||||
const parentInputIndexes = Object.keys(parent.inputs).map(Number)
|
|
||||||
// Prioritise exact slot index
|
|
||||||
const indexes = [link.origin_slot].concat(parentInputIndexes)
|
|
||||||
|
|
||||||
const matchingIndex = indexes.find(
|
|
||||||
(index) => parent?.inputs[index]?.type === input.type
|
|
||||||
)
|
|
||||||
// No input types match
|
|
||||||
if (matchingIndex === undefined) break
|
|
||||||
|
|
||||||
link = parent.getInputLink(matchingIndex)
|
|
||||||
if (link) parent = parent.getInputNode(matchingIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
if (parent?.updateLink) {
|
|
||||||
// Subgraph node / groupNode callback; deprecated, should be replaced
|
|
||||||
link = parent.updateLink(link)
|
|
||||||
}
|
|
||||||
if (link) {
|
|
||||||
inputs[input.name] = [
|
|
||||||
String(link.origin_id),
|
|
||||||
// @ts-expect-error link.origin_slot is already number.
|
|
||||||
parseInt(link.origin_slot)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output[String(node.id)] = {
|
output[String(node.id)] = {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { ColorOption, LGraph } from '@comfyorg/litegraph'
|
import { ColorOption, LGraph, Reroute } from '@comfyorg/litegraph'
|
||||||
import { LGraphGroup, LGraphNode, isColorable } from '@comfyorg/litegraph'
|
import { LGraphGroup, LGraphNode, isColorable } from '@comfyorg/litegraph'
|
||||||
import type { ISerialisedGraph } from '@comfyorg/litegraph/dist/types/serialisation'
|
import type {
|
||||||
|
ExportedSubgraph,
|
||||||
|
ISerialisableNodeInput,
|
||||||
|
ISerialisedGraph
|
||||||
|
} from '@comfyorg/litegraph/dist/types/serialisation'
|
||||||
import type {
|
import type {
|
||||||
IBaseWidget,
|
IBaseWidget,
|
||||||
IComboWidget
|
IComboWidget
|
||||||
@@ -50,6 +54,10 @@ export const isLGraphGroup = (item: unknown): item is LGraphGroup => {
|
|||||||
return item instanceof LGraphGroup
|
return item instanceof LGraphGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isReroute = (item: unknown): item is Reroute => {
|
||||||
|
return item instanceof Reroute
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the color option of all canvas items if they are all the same.
|
* Get the color option of all canvas items if they are all the same.
|
||||||
* @param items - The items to get the color option of.
|
* @param items - The items to get the color option of.
|
||||||
@@ -163,12 +171,11 @@ export function fixLinkInputSlots(graph: LGraph) {
|
|||||||
* This should match the serialization format of legacy widget conversion.
|
* This should match the serialization format of legacy widget conversion.
|
||||||
*
|
*
|
||||||
* @param graph - The graph to compress widget input slots for.
|
* @param graph - The graph to compress widget input slots for.
|
||||||
|
* @throws If an infinite loop is detected.
|
||||||
*/
|
*/
|
||||||
export function compressWidgetInputSlots(graph: ISerialisedGraph) {
|
export function compressWidgetInputSlots(graph: ISerialisedGraph) {
|
||||||
for (const node of graph.nodes) {
|
for (const node of graph.nodes) {
|
||||||
node.inputs = node.inputs?.filter(
|
node.inputs = node.inputs?.filter(matchesLegacyApi)
|
||||||
(input) => !(input.widget && input.link === null)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [inputIndex, input] of node.inputs?.entries() ?? []) {
|
for (const [inputIndex, input] of node.inputs?.entries() ?? []) {
|
||||||
if (input.link) {
|
if (input.link) {
|
||||||
@@ -179,4 +186,44 @@ export function compressWidgetInputSlots(graph: ISerialisedGraph) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compressSubgraphWidgetInputSlots(graph.definitions?.subgraphs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesLegacyApi(input: ISerialisableNodeInput) {
|
||||||
|
return !(input.widget && input.link === null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplication to handle the legacy link arrays in the root workflow.
|
||||||
|
* @see compressWidgetInputSlots
|
||||||
|
* @param subgraph The subgraph to compress widget input slots for.
|
||||||
|
*/
|
||||||
|
function compressSubgraphWidgetInputSlots(
|
||||||
|
subgraphs: ExportedSubgraph[] | undefined,
|
||||||
|
visited = new WeakSet<ExportedSubgraph>()
|
||||||
|
) {
|
||||||
|
if (!subgraphs) return
|
||||||
|
|
||||||
|
for (const subgraph of subgraphs) {
|
||||||
|
if (visited.has(subgraph)) throw new Error('Infinite loop detected')
|
||||||
|
visited.add(subgraph)
|
||||||
|
|
||||||
|
if (subgraph.nodes) {
|
||||||
|
for (const node of subgraph.nodes) {
|
||||||
|
node.inputs = node.inputs?.filter(matchesLegacyApi)
|
||||||
|
|
||||||
|
if (!subgraph.links) continue
|
||||||
|
|
||||||
|
for (const [inputIndex, input] of node.inputs?.entries() ?? []) {
|
||||||
|
if (input.link) {
|
||||||
|
const link = subgraph.links.find((link) => link.id === input.link)
|
||||||
|
if (link) link.target_slot = inputIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compressSubgraphWidgetInputSlots(subgraph.definitions?.subgraphs, visited)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,9 @@ export const isAbortError = (
|
|||||||
export const isSubgraph = (
|
export const isSubgraph = (
|
||||||
item: LGraph | Subgraph | undefined | null
|
item: LGraph | Subgraph | undefined | null
|
||||||
): item is Subgraph => item?.isRootGraph === false
|
): item is Subgraph => item?.isRootGraph === false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an item is non-nullish.
|
||||||
|
*/
|
||||||
|
export const isNonNullish = <T>(item: T | undefined | null): item is T =>
|
||||||
|
item != null
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ describe('useWorkflowStore', () => {
|
|||||||
// Assert
|
// Assert
|
||||||
console.debug(store.isSubgraphActive)
|
console.debug(store.isSubgraphActive)
|
||||||
expect(store.isSubgraphActive).toBe(false) // Should default to false
|
expect(store.isSubgraphActive).toBe(false) // Should default to false
|
||||||
expect(store.subgraphNamePath).toEqual([]) // Should default to empty
|
expect(store.activeSubgraph).toBeUndefined() // Should default to empty
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly update state when the root graph is active', async () => {
|
it('should correctly update state when the root graph is active', async () => {
|
||||||
@@ -505,7 +505,7 @@ describe('useWorkflowStore', () => {
|
|||||||
|
|
||||||
// Assert: Check store state
|
// Assert: Check store state
|
||||||
expect(store.isSubgraphActive).toBe(false)
|
expect(store.isSubgraphActive).toBe(false)
|
||||||
expect(store.subgraphNamePath).toEqual([]) // Path is empty for root graph
|
expect(store.activeSubgraph).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly update state when a subgraph is active', async () => {
|
it('should correctly update state when a subgraph is active', async () => {
|
||||||
@@ -527,10 +527,7 @@ describe('useWorkflowStore', () => {
|
|||||||
|
|
||||||
// Assert: Check store state
|
// Assert: Check store state
|
||||||
expect(store.isSubgraphActive).toBe(true)
|
expect(store.isSubgraphActive).toBe(true)
|
||||||
expect(store.subgraphNamePath).toEqual([
|
expect(store.activeSubgraph).toEqual(mockSubgraph)
|
||||||
'Level 1 Subgraph',
|
|
||||||
'Level 2 Subgraph'
|
|
||||||
]) // Path excludes the root
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update automatically when activeWorkflow changes', async () => {
|
it('should update automatically when activeWorkflow changes', async () => {
|
||||||
@@ -548,7 +545,7 @@ describe('useWorkflowStore', () => {
|
|||||||
|
|
||||||
// Verify initial state
|
// Verify initial state
|
||||||
expect(store.isSubgraphActive).toBe(true)
|
expect(store.isSubgraphActive).toBe(true)
|
||||||
expect(store.subgraphNamePath).toEqual(['Initial Subgraph'])
|
expect(store.activeSubgraph).toEqual(initialSubgraph)
|
||||||
|
|
||||||
// Act: Change the active workflow
|
// Act: Change the active workflow
|
||||||
const workflow2 = store.createTemporary('workflow2.json')
|
const workflow2 = store.createTemporary('workflow2.json')
|
||||||
@@ -569,7 +566,7 @@ describe('useWorkflowStore', () => {
|
|||||||
|
|
||||||
// Assert: Check that the state was updated by the watcher based on the *new* canvas state
|
// Assert: Check that the state was updated by the watcher based on the *new* canvas state
|
||||||
expect(store.isSubgraphActive).toBe(false) // Should reflect the change to undefined subgraph
|
expect(store.isSubgraphActive).toBe(false) // Should reflect the change to undefined subgraph
|
||||||
expect(store.subgraphNamePath).toEqual([]) // Path should be empty for root
|
expect(store.activeSubgraph).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||