mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 13:59:28 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c611c15d40 | ||
|
|
269686eebb | ||
|
|
0e3590d017 | ||
|
|
7d2d6df57b | ||
|
|
4462dabc63 | ||
|
|
53bfc0c95a | ||
|
|
b78682689e | ||
|
|
6d1dce8255 | ||
|
|
73f4e5143d | ||
|
|
7d75cc99ba | ||
|
|
0aa7d0b99a | ||
|
|
66b690e5c8 | ||
|
|
6e27b884fc | ||
|
|
561162fb3e | ||
|
|
7c6bd7ed71 | ||
|
|
fc5bdf80b3 | ||
|
|
033f242e43 | ||
|
|
304429b967 | ||
|
|
6dbdb9baa6 | ||
|
|
3e3e909e36 |
2
.github/workflows/test-browser-exp.yaml
vendored
2
.github/workflows/test-browser-exp.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Checkout ComfyUI_frontend
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: "huchenlei/ComfyUI_frontend"
|
||||
repository: "Comfy-Org/ComfyUI_frontend"
|
||||
path: "ComfyUI_frontend"
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
91
README.md
91
README.md
@@ -14,6 +14,90 @@ For Windows stand-alone build users, please edit the `run_cpu.bat` / `run_nvidia
|
||||
pause
|
||||
```
|
||||
|
||||
## Release Summary
|
||||
|
||||
### Major features
|
||||
|
||||
<details>
|
||||
<summary>v1.2.4: Node library sidebar tab</summary>
|
||||
|
||||
#### Drag & Drop
|
||||
https://github.com/user-attachments/assets/853e20b7-bc0e-49c9-bbce-a2ba7566f92f
|
||||
|
||||
#### Filter
|
||||
https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.0: Queue/History sidebar tab</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/86e264fe-4d26-4f07-aa9a-83bdd2d02b8f
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.1.0: Node search box</summary>
|
||||
|
||||
#### Fuzzy search & Node preview
|
||||

|
||||
|
||||
#### Release link with shift
|
||||
https://github.com/user-attachments/assets/a1b2b5c3-10d1-4256-b620-345de6858f25
|
||||
</details>
|
||||
|
||||
### QoL changes
|
||||
|
||||
<details>
|
||||
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/68826715-bb55-4b2a-be6e-675cfc424afe
|
||||
|
||||
https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.2.2: **Litegraph** auto connects to correct slot</summary>
|
||||
|
||||
#### Before
|
||||
https://github.com/user-attachments/assets/c253f778-82d5-4e6f-aec0-ea2ccf421651
|
||||
|
||||
#### After
|
||||
https://github.com/user-attachments/assets/b6360ac0-f0d2-447c-9daa-8a2e20c0dc1d
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v1.1.8: **Litegraph** hides text overflow on widget value</summary>
|
||||
|
||||
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
|
||||
</details>
|
||||
|
||||
### Node developers API
|
||||
<details>
|
||||
<summary>v1.2.4: Extension API to register custom side bar tab</summary>
|
||||
|
||||
Extensions now can call following API to register a sidebar tab.
|
||||
|
||||
```js
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-search",
|
||||
title: "search",
|
||||
tooltip: "search",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = "<div>Custom search tab</div>";
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The list of supported icons can be find here: https://primevue.org/icons/#list
|
||||
|
||||
We will support custom icon later.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
|
||||
## Road Map
|
||||
|
||||
### What has been done
|
||||
@@ -25,10 +109,8 @@ pause
|
||||
- Zod schema for input validation on ComfyUI workflow.
|
||||
- Make litegraph a npm dependency. <https://github.com/Comfy-Org/ComfyUI_frontend/pull/89>
|
||||
- Introduce Vue to start managing part of the UI.
|
||||
|
||||
- Starting with node search box revamp 
|
||||
|
||||
- Easy install and version management (<https://github.com/comfyanonymous/ComfyUI/pull/3897>).
|
||||
- Better node management. Sherlock <https://github.com/Nuked88/ComfyUI-N-Sidebar>.
|
||||
|
||||
|
||||
### What to be done
|
||||
@@ -36,10 +118,9 @@ pause
|
||||
- Replace the existing ComfyUI front-end impl
|
||||
- Remove `@ts-ignore`s.
|
||||
- Turn on `strict` on `tsconfig.json`.
|
||||
- Introduce a UI library to add more widget types for node developers.
|
||||
- Add more widget types for node developers.
|
||||
- LLM streaming node.
|
||||
- Linear mode (Similar to InvokeAI's linear mode).
|
||||
- Better node management. Sherlock https://github.com/Nuked88/ComfyUI-N-Sidebar.
|
||||
- Keybinding settings management. Register keybindings API for custom nodes.
|
||||
- New extensions API for adding UI-related features.
|
||||
|
||||
|
||||
193
browser_tests/assets/batch_move_links.json
Normal file
193
browser_tests/assets/batch_move_links.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
0,
|
||||
92
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [
|
||||
3,
|
||||
5
|
||||
],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"3Guofeng3_v32Light.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
460,
|
||||
92
|
||||
],
|
||||
"size": {
|
||||
"0": 422.84503173828125,
|
||||
"1": 164.31304931640625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [
|
||||
460,
|
||||
368
|
||||
],
|
||||
"size": {
|
||||
"0": 425.27801513671875,
|
||||
"1": 180.6060791015625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
0,
|
||||
276
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"3Guofeng3_v32Light.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
1,
|
||||
7,
|
||||
0,
|
||||
"CLIP"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -53,4 +53,15 @@ test.describe('Copy Paste', () => {
|
||||
await comfyPage.ctrlV()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
|
||||
})
|
||||
|
||||
test('Copy node by dragging + alt', async ({ comfyPage }) => {
|
||||
// TextEncodeNode1
|
||||
await comfyPage.page.mouse.move(618, 191)
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
await comfyPage.page.mouse.down()
|
||||
await comfyPage.page.mouse.move(100, 100)
|
||||
await comfyPage.page.mouse.up()
|
||||
await comfyPage.page.keyboard.up('Alt')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
@@ -58,6 +58,28 @@ test.describe('Node Interaction', () => {
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
|
||||
})
|
||||
|
||||
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('batch_move_links')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
}
|
||||
const outputSlot2Pos = {
|
||||
x: 307,
|
||||
y: 310
|
||||
}
|
||||
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'batch_move_links_moved.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Canvas Interaction', () => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -32,4 +32,26 @@ test.describe('Node search box', () => {
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
|
||||
})
|
||||
|
||||
test('Can auto link batch moved node', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('batch_move_links')
|
||||
|
||||
const outputSlot1Pos = {
|
||||
x: 304,
|
||||
y: 127
|
||||
}
|
||||
const emptySpacePos = {
|
||||
x: 5,
|
||||
y: 5
|
||||
}
|
||||
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
|
||||
await comfyPage.page.keyboard.up('Shift')
|
||||
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'auto-linked-node-batch.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
35
package-lock.json
generated
35
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-frontend",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.7",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.29",
|
||||
"@comfyorg/litegraph": "^0.7.34",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -54,6 +54,28 @@
|
||||
"zip-dir": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"../litegraph.js": {
|
||||
"name": "@comfyorg/litegraph",
|
||||
"version": "0.7.32",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^28.1.3",
|
||||
"eslint": "^8.37.0 ",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"express": "^4.17.1",
|
||||
"google-closure-compiler": "^20230411.0.0",
|
||||
"grunt": "^1.1.0",
|
||||
"grunt-cli": "^1.2.0",
|
||||
"grunt-closure-tools": "^1.0.0",
|
||||
"grunt-contrib-concat": "^2.1.0",
|
||||
"jest": "^28.1.3",
|
||||
"jest-cli": "^28.1.3",
|
||||
"nodemon": "^2.0.22",
|
||||
"rimraf": "^5.0.0",
|
||||
"yuidocjs": "^0.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -1830,9 +1852,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.29.tgz",
|
||||
"integrity": "sha512-lXgqcJseywRJQ/B9ClW+5u6VIbDJWy8SMJJ1nxXDgTsE30UUmOnBhZkLZZ3ffMv3QFUcYoNLq5EJn3EFx3g+zA=="
|
||||
"version": "0.7.34",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.7.34.tgz",
|
||||
"integrity": "sha512-PLQ9QDTbsVzQE6qr64HWs3z+U8wGHC4GgFeLvEbXL70LLZ1yiWVfNVZI3TsKuVD2jKdsBpDJ8vAdVUW5BwqGyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@comfyorg/litegraph": "^0.7.29",
|
||||
"@comfyorg/litegraph": "^0.7.34",
|
||||
"@primevue/themes": "^4.0.0-rc.2",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
|
||||
54
src/App.vue
54
src/App.vue
@@ -1,36 +1,22 @@
|
||||
<template>
|
||||
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
||||
<div v-else>
|
||||
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
||||
<teleport to=".graph-canvas-container">
|
||||
<LiteGraphCanvasSplitterOverlay v-if="betaMenuEnabled">
|
||||
<template #side-bar-panel>
|
||||
<SideToolBar />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
</teleport>
|
||||
</div>
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<GraphCanvas />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, markRaw, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
|
||||
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
|
||||
import { computed, markRaw, onMounted, watch } from 'vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
|
||||
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
|
||||
import { app } from './scripts/app'
|
||||
import { useSettingStore } from './stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from './stores/workspaceStateStore'
|
||||
import NodeLibrarySideBarTab from './components/sidebar/tabs/NodeLibrarySideBarTab.vue'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import { useNodeDefStore } from './stores/nodeDefStore'
|
||||
|
||||
const isLoading = ref(true)
|
||||
const nodeSearchEnabled = computed<boolean>(
|
||||
() => useSettingStore().get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const theme = computed<string>(() =>
|
||||
useSettingStore().get('Comfy.ColorPalette')
|
||||
)
|
||||
@@ -47,15 +33,10 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
const betaMenuEnabled = computed(
|
||||
() => useSettingStore().get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
let dropTargetCleanup = () => {}
|
||||
const init = () => {
|
||||
useSettingStore().addSettings(app.ui.settings)
|
||||
app.vueAppReady = true
|
||||
app.extensionManager = useWorkspaceStore()
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: 'queue',
|
||||
@@ -73,21 +54,6 @@ const init = () => {
|
||||
component: markRaw(NodeLibrarySideBarTab),
|
||||
type: 'vue'
|
||||
})
|
||||
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: document.querySelector('.graph-canvas-container'),
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = app.clientPosToCanvasPos([loc.clientX - 20, loc.clientY])
|
||||
const comfyNodeName = event.source.element.getAttribute(
|
||||
'data-comfy-node-name'
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName]
|
||||
app.addNodeOnGraph(nodeDef, { pos })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -95,14 +61,8 @@ onMounted(() => {
|
||||
init()
|
||||
} catch (e) {
|
||||
console.error('Failed to init Vue app', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -39,7 +39,9 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
<div class="_sb_col _sb_arrow">◀</div>
|
||||
<div class="_sb_col">{{ widgetInput.name }}</div>
|
||||
<div class="_sb_col middle-column"></div>
|
||||
<div class="_sb_col _sb_inherit">{{ widgetInput.default }}</div>
|
||||
<div class="_sb_col _sb_inherit">
|
||||
{{ truncateDefaultValue(widgetInput.default) }}
|
||||
</div>
|
||||
<div class="_sb_col _sb_arrow">▶</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,6 +73,12 @@ const slotInputDefs = allInputDefs.filter(
|
||||
const widgetInputDefs = allInputDefs.filter((input) =>
|
||||
nodeDefStore.inputIsWidget(input)
|
||||
)
|
||||
const truncateDefaultValue = (value: any): string => {
|
||||
if (value instanceof Object) {
|
||||
return _.truncate(JSON.stringify(value), { length: 20 })
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -179,7 +187,6 @@ const widgetInputDefs = allInputDefs.filter((input) =>
|
||||
align-items: center;
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
._sb_row_string {
|
||||
|
||||
@@ -25,14 +25,10 @@ import { app } from '@/scripts/app'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import NodeSearchBox from './NodeSearchBox.vue'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import {
|
||||
INodeSlot,
|
||||
LiteGraphCanvasEvent,
|
||||
LGraphNode,
|
||||
LinkReleaseContext
|
||||
} from '@comfyorg/litegraph'
|
||||
import { LiteGraphCanvasEvent, ConnectingLink } from '@comfyorg/litegraph'
|
||||
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ConnectingLinkImpl } from '@/types/litegraphTypes'
|
||||
|
||||
interface LiteGraphPointerEvent extends Event {
|
||||
canvasX: number
|
||||
@@ -67,36 +63,7 @@ const clearFilters = () => {
|
||||
const closeDialog = () => {
|
||||
visible.value = false
|
||||
}
|
||||
const connectNodeOnLinkRelease = (
|
||||
node: LGraphNode,
|
||||
context: LinkReleaseContext
|
||||
) => {
|
||||
const destIsInput = context.node_from !== undefined
|
||||
const srcNode = (
|
||||
destIsInput ? context.node_from : context.node_to
|
||||
) as LGraphNode
|
||||
const srcSlotIndex: number = context.slot_from.slot_index
|
||||
const linkDataType = destIsInput
|
||||
? context.type_filter_in
|
||||
: context.type_filter_out
|
||||
const destSlots = destIsInput ? node.inputs : node.outputs
|
||||
const destSlotIndex = destSlots.findIndex(
|
||||
(slot: INodeSlot) => slot.type === linkDataType
|
||||
)
|
||||
|
||||
if (destSlotIndex === -1) {
|
||||
console.warn(
|
||||
`Could not find slot with type ${linkDataType} on node ${node.title}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (destIsInput) {
|
||||
srcNode.connect(srcSlotIndex, node, destSlotIndex)
|
||||
} else {
|
||||
node.connect(destSlotIndex, srcNode, srcSlotIndex)
|
||||
}
|
||||
}
|
||||
const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
closeDialog()
|
||||
|
||||
@@ -104,7 +71,9 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
|
||||
const eventDetail = triggerEvent.value.detail
|
||||
if (eventDetail.subType === 'empty-release') {
|
||||
connectNodeOnLinkRelease(node, eventDetail.linkReleaseContext)
|
||||
eventDetail.linkReleaseContext.links.forEach((link: ConnectingLink) => {
|
||||
ConnectingLinkImpl.createFromPlainObject(link).connectTo(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +86,17 @@ const canvasEventHandler = (e: LiteGraphCanvasEvent) => {
|
||||
}
|
||||
|
||||
if (e.detail.subType === 'empty-release') {
|
||||
const destIsInput = e.detail.linkReleaseContext.node_from !== undefined
|
||||
const context = e.detail.linkReleaseContext
|
||||
if (context.links.length === 0) {
|
||||
console.warn('Empty release with no links! This should never happen')
|
||||
return
|
||||
}
|
||||
const firstLink = ConnectingLinkImpl.createFromPlainObject(context.links[0])
|
||||
const filter = useNodeDefStore().nodeSearchService.getFilterById(
|
||||
destIsInput ? 'input' : 'output'
|
||||
firstLink.releaseSlotType
|
||||
)
|
||||
|
||||
const value = destIsInput
|
||||
? e.detail.linkReleaseContext.type_filter_in
|
||||
: e.detail.linkReleaseContext.type_filter_out
|
||||
|
||||
addFilter([filter, value])
|
||||
const dataType = firstLink.type
|
||||
addFilter([filter, dataType])
|
||||
}
|
||||
triggerEvent.value = e
|
||||
visible.value = true
|
||||
|
||||
68
src/components/graph/GraphCanvas.vue
Normal file
68
src/components/graph/GraphCanvas.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<teleport to=".graph-canvas-container">
|
||||
<LiteGraphCanvasSplitterOverlay v-if="betaMenuEnabled">
|
||||
<template #side-bar-panel>
|
||||
<SideToolBar />
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<canvas ref="canvasRef" id="graph-canvas" tabindex="1" />
|
||||
</teleport>
|
||||
<NodeSearchboxPopover v-if="nodeSearchEnabled" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
|
||||
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const nodeSearchEnabled = computed<boolean>(
|
||||
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
|
||||
)
|
||||
|
||||
let dropTargetCleanup = () => {}
|
||||
|
||||
onMounted(async () => {
|
||||
comfyApp.vueAppReady = true
|
||||
|
||||
workspaceStore.spinner = true
|
||||
await comfyApp.setup(canvasRef.value)
|
||||
workspaceStore.spinner = false
|
||||
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: canvasRef.value,
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX - 20, loc.clientY])
|
||||
const comfyNodeName = event.source.element.getAttribute(
|
||||
'data-comfy-node-name'
|
||||
)
|
||||
const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName]
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
}
|
||||
})
|
||||
emit('ready')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
})
|
||||
</script>
|
||||
@@ -1,63 +1,83 @@
|
||||
<template>
|
||||
<TreePlus
|
||||
class="node-lib-tree"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
selectionMode="single"
|
||||
:value="renderedRoot.children"
|
||||
:filter="true"
|
||||
filterMode="lenient"
|
||||
dragSelector=".p-tree-node-leaf"
|
||||
:pt="{
|
||||
nodeLabel: 'node-lib-tree-node-label',
|
||||
nodeChildren: ({ props }) => ({
|
||||
'data-comfy-node-name': props.node?.data?.name,
|
||||
onMouseenter: (event: MouseEvent) => {
|
||||
hoveredComfyNodeName = props.node?.data?.name
|
||||
<SideBarTabTemplate :title="$t('sideToolBar.nodeLibrary')">
|
||||
<template #tool-buttons>
|
||||
<ToggleButton
|
||||
v-model:model-value="alphabeticalSort"
|
||||
on-icon="pi pi-sort-alpha-down"
|
||||
off-icon="pi pi-sort-alt"
|
||||
aria-label="Sort"
|
||||
:pt="{
|
||||
label: { style: { display: 'none' } }
|
||||
}"
|
||||
v-tooltip="$t('sideToolBar.nodeLibraryTab.sortOrder')"
|
||||
>
|
||||
</ToggleButton>
|
||||
</template>
|
||||
<template #body>
|
||||
<TreePlus
|
||||
class="node-lib-tree"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
selectionMode="single"
|
||||
:value="renderedRoot.children"
|
||||
:filter="true"
|
||||
filterMode="lenient"
|
||||
dragSelector=".p-tree-node-leaf"
|
||||
:pt="{
|
||||
nodeLabel: 'node-lib-tree-node-label',
|
||||
nodeChildren: ({ props }) => ({
|
||||
'data-comfy-node-name': props.node?.data?.name,
|
||||
onMouseenter: (event: MouseEvent) => {
|
||||
hoveredComfyNodeName = props.node?.data?.name
|
||||
|
||||
const hoverTarget = event.target as HTMLElement
|
||||
const targetRect = hoverTarget.getBoundingClientRect()
|
||||
nodePreviewStyle.top = `${targetRect.top - 40}px`
|
||||
nodePreviewStyle.left = `${targetRect.right}px`
|
||||
},
|
||||
onMouseleave: () => {
|
||||
hoveredComfyNodeName = null
|
||||
}
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<span class="folder-label">{{ node.label }}</span>
|
||||
<Badge
|
||||
:value="node.totalNodes"
|
||||
severity="secondary"
|
||||
:style="{ marginLeft: '0.5rem' }"
|
||||
></Badge>
|
||||
const hoverTarget = event.target as HTMLElement
|
||||
const targetRect = hoverTarget.getBoundingClientRect()
|
||||
nodePreviewStyle.top = `${targetRect.top - 40}px`
|
||||
nodePreviewStyle.left = `${targetRect.right}px`
|
||||
},
|
||||
onMouseleave: () => {
|
||||
hoveredComfyNodeName = null
|
||||
}
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<span class="folder-label">{{ node.label }}</span>
|
||||
<Badge
|
||||
:value="node.totalNodes"
|
||||
severity="secondary"
|
||||
:style="{ marginLeft: '0.5rem' }"
|
||||
></Badge>
|
||||
</template>
|
||||
<template #node="{ node }">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</template>
|
||||
</TreePlus>
|
||||
<div
|
||||
v-if="hoveredComfyNode"
|
||||
class="node-lib-node-preview"
|
||||
:style="nodePreviewStyle"
|
||||
>
|
||||
<NodePreview
|
||||
:key="hoveredComfyNode.name"
|
||||
:nodeDef="hoveredComfyNode"
|
||||
></NodePreview>
|
||||
</div>
|
||||
</template>
|
||||
<template #node="{ node }">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</template>
|
||||
</TreePlus>
|
||||
<div
|
||||
v-if="hoveredComfyNode"
|
||||
class="node-lib-node-preview"
|
||||
:style="nodePreviewStyle"
|
||||
>
|
||||
<NodePreview
|
||||
:key="hoveredComfyNode.name"
|
||||
:nodeDef="hoveredComfyNode"
|
||||
></NodePreview>
|
||||
</div>
|
||||
</SideBarTabTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Badge from 'primevue/badge'
|
||||
import ToggleButton from 'primevue/togglebutton'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { computed, ref } from 'vue'
|
||||
import { TreeNode } from 'primevue/treenode'
|
||||
import TreePlus from '@/components/primevueOverride/TreePlus.vue'
|
||||
import NodePreview from '@/components/NodePreview.vue'
|
||||
import SideBarTabTemplate from '@/components/sidebar/tabs/SideBarTabTemplate.vue'
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const alphabeticalSort = ref(false)
|
||||
const expandedKeys = ref({})
|
||||
const hoveredComfyNodeName = ref<string | null>(null)
|
||||
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
|
||||
@@ -72,14 +92,15 @@ const nodePreviewStyle = ref<Record<string, string>>({
|
||||
left: '0px'
|
||||
})
|
||||
|
||||
const root = computed(() => nodeDefStore.nodeTree)
|
||||
const root = computed(() =>
|
||||
alphabeticalSort.value ? nodeDefStore.sortedNodeTree : nodeDefStore.nodeTree
|
||||
)
|
||||
const renderedRoot = computed(() => {
|
||||
return fillNodeInfo(root.value)
|
||||
})
|
||||
const fillNodeInfo = (node: TreeNode): TreeNode => {
|
||||
const isLeaf = node.children === undefined || node.children.length === 0
|
||||
const isExpanded = expandedKeys.value[node.key]
|
||||
const icon = isLeaf
|
||||
const icon = node.leaf
|
||||
? 'pi pi-circle-fill'
|
||||
: isExpanded
|
||||
? 'pi pi-folder-open'
|
||||
@@ -90,8 +111,8 @@ const fillNodeInfo = (node: TreeNode): TreeNode => {
|
||||
...node,
|
||||
icon,
|
||||
children,
|
||||
type: isLeaf ? 'node' : 'folder',
|
||||
totalNodes: isLeaf
|
||||
type: node.leaf ? 'node' : 'folder',
|
||||
totalNodes: node.leaf
|
||||
? 1
|
||||
: children.reduce((acc, child) => acc + child.totalNodes, 0)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
<div>
|
||||
<div v-else>
|
||||
<Message icon="pi pi-info" severity="error">
|
||||
<span class="ml-2">No tasks</span>
|
||||
</Message>
|
||||
|
||||
65
src/components/sidebar/tabs/SideBarTabTemplate.vue
Normal file
65
src/components/sidebar/tabs/SideBarTabTemplate.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="comfy-vue-side-bar-container">
|
||||
<Toolbar class="comfy-vue-side-bar-header">
|
||||
<template #start>
|
||||
<span class="comfy-vue-side-bar-header-span">{{
|
||||
props.title.toUpperCase()
|
||||
}}</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<div class="comfy-vue-side-bar-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Toolbar from 'primevue/toolbar'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-vue-side-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header {
|
||||
flex-shrink: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-header-span {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.comfy-vue-side-bar-body::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app } from '../../scripts/app'
|
||||
import { api } from '../../scripts/api'
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.Keybinds',
|
||||
@@ -6,8 +7,14 @@ app.registerExtension({
|
||||
const keybindListener = function (event) {
|
||||
const modifierPressed = event.ctrlKey || event.metaKey
|
||||
|
||||
// Queue prompt using ctrl or command + enter
|
||||
// Queue prompt using (ctrl or command) + enter
|
||||
if (modifierPressed && event.key === 'Enter') {
|
||||
// Cancel current prompt using (ctrl or command) + alt + enter
|
||||
if(event.altKey) {
|
||||
api.interrupt()
|
||||
return
|
||||
}
|
||||
// Queue prompt as first for generation using (ctrl or command) + shift + enter
|
||||
app.queuePrompt(event.shiftKey ? -1 : 0).then()
|
||||
return
|
||||
}
|
||||
|
||||
10
src/i18n.ts
10
src/i18n.ts
@@ -6,7 +6,10 @@ const messages = {
|
||||
settings: 'Settings',
|
||||
themeToggle: 'Toggle Theme',
|
||||
queue: 'Queue',
|
||||
nodeLibrary: 'Node Library'
|
||||
nodeLibrary: 'Node Library',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: 'Sort Order'
|
||||
}
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
@@ -14,7 +17,10 @@ const messages = {
|
||||
settings: '设置',
|
||||
themeToggle: '主题切换',
|
||||
queue: '队列',
|
||||
nodeLibrary: '节点库'
|
||||
nodeLibrary: '节点库',
|
||||
nodeLibraryTab: {
|
||||
sortOrder: '排序顺序'
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Add more languages
|
||||
|
||||
@@ -8,7 +8,6 @@ import Tooltip from 'primevue/tooltip'
|
||||
import 'primeicons/primeicons.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { createPinia } from 'pinia'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
@@ -40,8 +39,3 @@ app
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.mount('#vue-app')
|
||||
|
||||
comfyApp.setup().then(() => {
|
||||
window['app'] = comfyApp
|
||||
window['graph'] = comfyApp.graph
|
||||
})
|
||||
|
||||
@@ -1853,17 +1853,13 @@ export class ComfyApp {
|
||||
/**
|
||||
* Set up the app on the page
|
||||
*/
|
||||
async setup() {
|
||||
async setup(canvasEl: HTMLCanvasElement) {
|
||||
this.canvasEl = canvasEl
|
||||
await this.#setUser()
|
||||
|
||||
// Create and mount the LiteGraph in the DOM
|
||||
const mainCanvas = document.createElement('canvas')
|
||||
mainCanvas.style.touchAction = 'none'
|
||||
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, {
|
||||
id: 'graph-canvas'
|
||||
}))
|
||||
canvasEl.tabIndex = 1
|
||||
this.canvasContainer.prepend(canvasEl)
|
||||
|
||||
this.resizeCanvas()
|
||||
|
||||
@@ -2957,7 +2953,7 @@ export class ComfyApp {
|
||||
const rect = this.canvasContainer.getBoundingClientRect()
|
||||
const containerOffsets = [rect.left, rect.top]
|
||||
return _.zip(pos, this.canvas.ds.offset, containerOffsets).map(
|
||||
([p, o1, o2]) => p / this.canvas.ds.scale - o1 - o2
|
||||
([p, o1, o2]) => (p - o2) / this.canvas.ds.scale - o1
|
||||
) as Vector2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ export class ChangeTracker {
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
(e) => {
|
||||
const activeEl = document.activeElement;
|
||||
requestAnimationFrame(async () => {
|
||||
let activeEl
|
||||
let bindInputEl
|
||||
// If we are auto queue in change mode then we do want to trigger on inputs
|
||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === 'instant') {
|
||||
activeEl = document.activeElement
|
||||
if (
|
||||
activeEl?.tagName === 'INPUT' ||
|
||||
activeEl?.['type'] === 'textarea'
|
||||
@@ -122,6 +122,7 @@ export class ChangeTracker {
|
||||
// Ignore events on inputs, they have their native history
|
||||
return
|
||||
}
|
||||
bindInputEl = activeEl
|
||||
}
|
||||
|
||||
keyIgnored =
|
||||
@@ -135,7 +136,7 @@ export class ChangeTracker {
|
||||
if (await changeTracker().undoRedo(e)) return
|
||||
|
||||
// If our active element is some type of input then handle changes after they're done
|
||||
if (ChangeTracker.bindInput(app, activeEl)) return
|
||||
if (ChangeTracker.bindInput(app, bindInputEl)) return
|
||||
changeTracker().checkState()
|
||||
})
|
||||
},
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
z-index: 400;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.comfyui-workflows-panel {
|
||||
|
||||
@@ -313,6 +313,7 @@ function addMultilineWidget(node, name, opts, app) {
|
||||
inputEl.className = 'comfy-multiline-input'
|
||||
inputEl.value = opts.defaultVal
|
||||
inputEl.placeholder = opts.placeholder || name
|
||||
inputEl.spellcheck = opts.spellcheck || false
|
||||
|
||||
const widget = node.addDOMWidget(name, 'customtext', inputEl, {
|
||||
getValue() {
|
||||
|
||||
@@ -218,6 +218,27 @@ export const SYSTEM_NODE_DEFS: ComfyNodeDef[] = [
|
||||
}
|
||||
]
|
||||
|
||||
function sortedTree(node: TreeNode): TreeNode {
|
||||
// Create a new node with the same label and data
|
||||
const newNode: TreeNode = {
|
||||
...node
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
// Sort the children of the current node
|
||||
const sortedChildren = [...node.children].sort((a, b) =>
|
||||
a.label.localeCompare(b.label)
|
||||
)
|
||||
// Recursively sort the children and add them to the new node
|
||||
newNode.children = []
|
||||
for (const child of sortedChildren) {
|
||||
newNode.children.push(sortedTree(child))
|
||||
}
|
||||
}
|
||||
|
||||
return newNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
nodeDefsByName: Record<string, ComfyNodeDefImpl>
|
||||
widgets: Record<string, ComfyWidgetConstructor>
|
||||
@@ -239,6 +260,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
const root: TreeNode = {
|
||||
key: 'root',
|
||||
label: 'Nodes',
|
||||
leaf: false,
|
||||
children: []
|
||||
}
|
||||
for (const nodeDef of Object.values(state.nodeDefsByName)) {
|
||||
@@ -249,7 +271,7 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
key += `/${part}`
|
||||
let next = current.children.find((child) => child.label === part)
|
||||
if (!next) {
|
||||
next = { key, label: part, children: [] }
|
||||
next = { key, label: part, children: [], leaf: false }
|
||||
current.children.push(next)
|
||||
}
|
||||
current = next
|
||||
@@ -257,10 +279,14 @@ export const useNodeDefStore = defineStore('nodeDef', {
|
||||
current.children.push({
|
||||
label: nodeDef.display_name,
|
||||
data: nodeDef,
|
||||
key: `${key}/${nodeDef.name}`
|
||||
key: `${key}/${nodeDef.name}`,
|
||||
leaf: true
|
||||
})
|
||||
}
|
||||
return root
|
||||
},
|
||||
sortedNodeTree(): TreeNode {
|
||||
return sortedTree(this.nodeTree)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface WorkspaceState {
|
||||
spinner: boolean
|
||||
activeSidebarTab: string | null
|
||||
sidebarTabs: SidebarTabExtension[]
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', {
|
||||
state: (): WorkspaceState => ({
|
||||
spinner: false,
|
||||
activeSidebarTab: null,
|
||||
sidebarTabs: []
|
||||
}),
|
||||
|
||||
74
src/types/litegraphTypes.ts
Normal file
74
src/types/litegraphTypes.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ConnectingLink,
|
||||
LGraphNode,
|
||||
Vector2,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
INodeSlot
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
export class ConnectingLinkImpl implements ConnectingLink {
|
||||
node: LGraphNode
|
||||
slot: number
|
||||
input: INodeInputSlot | null
|
||||
output: INodeOutputSlot | null
|
||||
pos: Vector2
|
||||
|
||||
constructor(
|
||||
node: LGraphNode,
|
||||
slot: number,
|
||||
input: INodeInputSlot | null,
|
||||
output: INodeOutputSlot | null,
|
||||
pos: Vector2
|
||||
) {
|
||||
this.node = node
|
||||
this.slot = slot
|
||||
this.input = input
|
||||
this.output = output
|
||||
this.pos = pos
|
||||
}
|
||||
|
||||
static createFromPlainObject(obj: ConnectingLink) {
|
||||
return new ConnectingLinkImpl(
|
||||
obj.node,
|
||||
obj.slot,
|
||||
obj.input,
|
||||
obj.output,
|
||||
obj.pos
|
||||
)
|
||||
}
|
||||
|
||||
get type(): string | null {
|
||||
const result = this.input ? this.input.type : this.output.type
|
||||
return result === -1 ? null : result
|
||||
}
|
||||
|
||||
/**
|
||||
* Which slot type is release and need to be reconnected.
|
||||
* - 'output' means we need a new node's outputs slot to connect with this link
|
||||
*/
|
||||
get releaseSlotType(): 'input' | 'output' {
|
||||
return this.output ? 'input' : 'output'
|
||||
}
|
||||
|
||||
connectTo(newNode: LGraphNode) {
|
||||
const newNodeSlots =
|
||||
this.releaseSlotType === 'output' ? newNode.outputs : newNode.inputs
|
||||
const newNodeSlot = newNodeSlots.findIndex(
|
||||
(slot: INodeSlot) => slot.type === this.type
|
||||
)
|
||||
|
||||
if (newNodeSlot === -1) {
|
||||
console.warn(
|
||||
`Could not find slot with type ${this.type} on node ${newNode.title}. This should never happen`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.releaseSlotType === 'input') {
|
||||
this.node.connect(this.slot, newNode, newNodeSlot)
|
||||
} else {
|
||||
newNode.connect(newNodeSlot, this.node, this.slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,12 @@ export async function start(config: StartConfig = {}): Promise<StartResult> {
|
||||
const { app } = await import('../../src/scripts/app')
|
||||
const { LiteGraph, LGraphCanvas } = await import('@comfyorg/litegraph')
|
||||
config.preSetup?.(app)
|
||||
await app.setup()
|
||||
const canvasEl = document.createElement('canvas')
|
||||
canvasEl.style.touchAction = 'none'
|
||||
canvasEl.id = 'graph-canvas'
|
||||
canvasEl.tabIndex = 1
|
||||
app.canvasContainer.prepend(canvasEl)
|
||||
await app.setup(canvasEl)
|
||||
|
||||
// @ts-ignore
|
||||
return { ...Ez.graph(app, LiteGraph, LGraphCanvas), app }
|
||||
|
||||
Reference in New Issue
Block a user