Compare commits

..

74 Commits

Author SHA1 Message Date
Chenlei Hu
8dcf7eca74 1.11.0 (#2685) 2025-02-22 19:22:19 -05:00
bymyself
f94831d054 Add node video previews (#2635) 2025-02-22 18:37:42 -05:00
Terry Jia
c502b86c31 [3d] refactor load3d nodes (#2683)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-22 18:36:47 -05:00
Chenlei Hu
86b65d481a 1.10.10 (#2679) 2025-02-21 23:30:06 -05:00
Chenlei Hu
064e982f01 Revert "[Refactor] Extract RerouteNode as a separate file" (#2678) 2025-02-21 22:57:19 -05:00
Chenlei Hu
f43eac7c71 Revert "Restrict applyToGraph to PrimitiveNode" (#2677) 2025-02-21 22:56:51 -05:00
bymyself
d7c9a43aba [Docs] Add id to details tag in README.md (#2676) 2025-02-21 22:36:55 -05:00
Chenlei Hu
dee3f5824a 1.10.9 (#2673) 2025-02-21 20:19:29 -05:00
Chenlei Hu
9b88909caa [Extension] Selection toolbox API (#2672) 2025-02-21 19:25:30 -05:00
Chenlei Hu
3fa512957c [CI] Enable release on LTS branches (#2671) 2025-02-21 16:36:17 -05:00
Chenlei Hu
b012f243b3 Only show delete in selection toolbox for reroute (#2670) 2025-02-21 16:03:36 -05:00
Chenlei Hu
6cb33d9431 Restrict applyToGraph to PrimitiveNode (#2669) 2025-02-21 15:53:33 -05:00
Chenlei Hu
85d04f6814 [Refactor] Extract RerouteNode as a separate file (#2668) 2025-02-21 15:04:47 -05:00
Chenlei Hu
40da43861e [Refactor] Move Widget.beforeQueued invocation from graphToPrompt to queuePrompt (#2667) 2025-02-21 14:18:11 -05:00
Chenlei Hu
40deb19634 [Cleanup] Remove upstreamed litegraph types (#2666) 2025-02-21 13:39:13 -05:00
Chenlei Hu
abfc7481d3 Remove 'clean' param from graphToPrompt (#2665) 2025-02-21 12:01:26 -05:00
Chenlei Hu
ec94811637 [Refactor] Move app.graphToPrompt to executionUtil (#2664) 2025-02-21 11:41:25 -05:00
Chenlei Hu
cea5a4a3dd Update litegraph 0.8.92 (#2662) 2025-02-21 09:28:43 -05:00
Chenlei Hu
0937c1f2cd 1.10.8 (#2659) 2025-02-20 20:14:32 -05:00
Chenlei Hu
8074d797b0 [BugFix] Fix incorrect selection overlay after drag (#2658) 2025-02-20 20:12:42 -05:00
Terry Jia
c3c6ec627b [3d] support using image as background (#2657)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-20 20:05:54 -05:00
Chenlei Hu
02d77002c9 Update litegraph to 0.8.91 (#2654)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-20 14:43:51 -05:00
Chenlei Hu
3e31045fbb [BugFix] Properly trigger onClose hook in dialogService (#2655) 2025-02-20 14:42:52 -05:00
Chenlei Hu
78146c86f4 [BugFix] Copy LGraphCanvas.ds on serialization (#2653) 2025-02-20 10:25:17 -05:00
bymyself
365fd1e047 Fix error translating legacy setting options (#2648) 2025-02-20 10:12:46 -05:00
filtered
fbb6c2f825 Update issue report image (#2647) 2025-02-20 16:29:21 +11:00
bymyself
9c7d86ee49 Update issue report template (#2645) 2025-02-19 22:46:22 -05:00
filtered
bc43cf0290 [CI] Always test LTS branches (#2641)
- Adds core/* and desktop/* to CI testing worfklows
2025-02-20 13:51:17 +11:00
Chenlei Hu
45c59f9e84 1.10.7 (#2639) 2025-02-19 17:45:05 -05:00
Terry Jia
014a65411e [3d] flash preview screen border if reach out limitation (#2638) 2025-02-19 15:49:36 -05:00
Chenlei Hu
6c6d86a30b Selection toolbox color picker button (#2637)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-19 15:25:46 -05:00
filtered
08a6867c00 [Desktop] Offer Troubleshoot page instead of Reinstall on start error (#2623)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-19 10:30:23 -05:00
filtered
dbbe67dfcd [Desktop] Fix missing git logo in troubleshooting (#2633) 2025-02-19 10:29:48 -05:00
bymyself
40fa1d37bc Fix pasting image that was copied from browser (#2630) 2025-02-19 10:27:58 -05:00
filtered
0d6bc669f5 [Desktop] Fix invalid type assertion in API (#2631) 2025-02-19 21:59:17 +11:00
Chenlei Hu
e4444d4074 1.10.6 (#2628) 2025-02-18 20:33:58 -05:00
Chenlei Hu
cbf5dff633 Update litegraph 0.8.87 (#2625)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-18 20:25:17 -05:00
Chenlei Hu
9de8450deb Update test expectations (#2627)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-18 20:25:06 -05:00
Chenlei Hu
3b0e3d635b [BugFix] Fix node color for custom light themes (#2621)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-18 19:08:34 -05:00
Chenlei Hu
d1a682bc01 [Refactor] Extract color selector as component (#2620) 2025-02-18 15:28:17 -05:00
Terry Jia
01ffc9e4eb [3d] allow using mouse wheel to adjust preview screen size (#2619) 2025-02-18 14:59:43 -05:00
Chenlei Hu
54e42178f7 1.10.5 (#2617) 2025-02-18 12:26:27 -05:00
Chenlei Hu
25e5ab3a36 Add bypass action to selection toolbox (#2616) 2025-02-18 12:25:49 -05:00
Chenlei Hu
28dd6a2702 Update litegraph 0.8.85 (#2615) 2025-02-18 11:51:36 -05:00
bymyself
3b3df250cd Add refresh button to selecton toolbox (#2612) 2025-02-18 11:39:43 -05:00
bymyself
6441a86619 [Style] Update toolbox style (#2614) 2025-02-18 11:34:06 -05:00
Chenlei Hu
79db202925 [New Feature] Selection Toolbox (#2608)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-17 19:07:49 -05:00
Chenlei Hu
f7556e0015 Add DeleteSelectedItems command (#2606) 2025-02-17 17:16:12 -05:00
bymyself
141e64354c Support batch image upload (#2597) 2025-02-17 13:56:21 -05:00
bymyself
79452ce267 Fix extraneous values in template workflows (#2605) 2025-02-17 13:55:29 -05:00
Chenlei Hu
4d8a5eacba 1.10.4 (#2604) 2025-02-17 10:13:43 -05:00
bymyself
8f5a9a50aa Remove duplicate outpaint template (#2602)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2025-02-17 10:12:09 -05:00
Margen67
7bc48c5074 Formatting/cleanup (#2594) 2025-02-17 10:10:00 -05:00
Dr.Lt.Data
e04ea07774 refine locales/ko (#2600) 2025-02-17 10:09:09 -05:00
bymyself
75af956279 Fix gallery navigator icons (#2601) 2025-02-17 10:08:46 -05:00
bymyself
434a2307a2 Remove lora dependency from flux canny template (#2603) 2025-02-17 10:08:16 -05:00
filtered
336b3caf9a [Desktop] Update uv cache clear task to show terminal (#2598) 2025-02-17 23:33:34 +11:00
filtered
c757fbaeb4 [Test] Fix unnecessary circular reference (#2596) 2025-02-17 20:18:26 +11:00
Chenlei Hu
fd27b3d580 Fix title editor font size (#2593) 2025-02-16 21:48:02 -05:00
Chenlei Hu
0658698a13 Selection Overlay (#2592) 2025-02-16 21:23:07 -05:00
Terry Jia
b2375a150c [3d] fully convert load 3d nodes into vue (#2590) 2025-02-16 20:15:49 -05:00
Chenlei Hu
9ebb5b2a0c 1.10.3 (#2591) 2025-02-16 20:14:50 -05:00
Chenlei Hu
d6a5deccd8 [Refactor] useAbsolutePosition composable (#2589) 2025-02-16 15:39:58 -05:00
Chenlei Hu
3f4d11c63a Inplace widget to input conversion (#2588)
Co-authored-by: github-actions <github-actions@github.com>
2025-02-16 13:41:32 -05:00
Margen67
44498739fc Update setup-node to v4 (#2587) 2025-02-16 13:14:01 -05:00
dependabot[bot]
764ec9f7d0 Bump vite from 5.4.6 to 5.4.14 (#2585)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-16 11:23:17 -05:00
bymyself
e3234aa0aa Normalize translation keys in template card component (#2574)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-02-16 10:17:49 -05:00
bymyself
df11c99393 Refactor node image upload and preview (#2580)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-02-16 10:09:02 -05:00
Chenlei Hu
317ea8b932 1.10.2 (#2583) 2025-02-16 09:49:25 -05:00
bymyself
108884a304 Replace "clip" with "text_encoders" in template workflows (#2572) 2025-02-16 09:39:10 -05:00
bymyself
9f1992ca59 Change title of pose ControlNet template workflow (#2573) 2025-02-16 09:38:18 -05:00
bymyself
39f245fd97 Remove interchangeable models from template workflows (#2575) 2025-02-16 09:36:34 -05:00
bymyself
2d2fa5bfe9 Fix incorrect link in template workflow (#2579) 2025-02-16 09:35:54 -05:00
Terry Jia
bfb1b80cd7 [3d] bug fix for unable click vue button (#2581) 2025-02-16 09:35:20 -05:00
173 changed files with 6183 additions and 3356 deletions

View File

@@ -16,7 +16,18 @@ body:
- type: textarea
attributes:
label: Frontend Version
description: 'What is the frontend version you are using? You can check this in the settings dialog'
description: |
What is the frontend version you are using? You can check this in the settings dialog.
<details>
<summary>Click to show where to find the version</summary>
Open the setting by clicking the cog icon in the bottom-left of the screen, then click `About`.
![Frontend version](https://github.com/user-attachments/assets/561fb7c3-3012-457c-a494-9bdc1ff035c0)
</details>
validations:
required: true
- type: textarea
@@ -72,7 +83,8 @@ body:
- Other
- type: textarea
attributes:
label: Other
description: 'Any other additional information you think might be helpful.'
label: Other Information
description: 'Any other context, details, or screenshots that might help solve the issue.'
placeholder: 'Add any other relevant information here...'
validations:
required: false

View File

@@ -2,10 +2,7 @@ name: ESLint
on:
pull_request:
branches:
- main
- master
- 'dev*'
branches: [ main, master, dev*, core/*, desktop/* ]
jobs:
eslint:
@@ -13,8 +10,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 'lts/*'
- run: npm ci
- run: npm run lint
- run: npm run lint

View File

@@ -2,7 +2,7 @@ name: Prettier Check
on:
pull_request:
branches: [ main, master, dev* ]
branches: [ main, master, dev*, core/*, desktop/* ]
jobs:
prettier:
@@ -12,12 +12,12 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 'lts/*'
- name: Install dependencies
run: npm ci
- name: Run Prettier check
run: npm run format:check
run: npm run format:check

View File

@@ -1,4 +1,5 @@
name: Update Locales for given custom node repository
on:
workflow_dispatch:
inputs:
@@ -23,27 +24,27 @@ jobs:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
repository: comfyanonymous/ComfyUI
path: ComfyUI
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
repository: Comfy-Org/ComfyUI_devtools
path: ComfyUI/custom_nodes/ComfyUI_devtools
- name: Checkout custom node repository
uses: actions/checkout@v4
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 'lts/*'
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -53,14 +54,12 @@ jobs:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
shell: bash
working-directory: ComfyUI
- name: Install custom node requirements
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
shell: bash
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Build & Install ComfyUI_frontend
run: |
@@ -68,14 +67,12 @@ jobs:
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
shell: bash
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu --multi-user &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
shell: bash
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -153,7 +150,7 @@ jobs:
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Create PR
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli

View File

@@ -1,4 +1,5 @@
name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]

View File

@@ -2,12 +2,10 @@ name: Create Release Draft
on:
pull_request:
types: [closed]
branches:
- main
- master
types: [ closed ]
branches: [ main, core/* ]
paths:
- "package.json"
- 'package.json'
jobs:
draft_release:
@@ -18,9 +16,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 'lts/*'
- name: Get current version
id: current_version
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
@@ -40,9 +38,10 @@ jobs:
files: |
dist.zip
tag_name: v${{ steps.current_version.outputs.version }}
draft: false
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' }}
draft: true
prerelease: false
make_latest: "true"
generate_release_notes: true
publish_types:
runs-on: ubuntu-latest
@@ -51,14 +50,14 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'Release')
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org"
node-version: 'lts/*'
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm run build:types
- name: Publish package
run: npm publish --access public
working-directory: ./dist
working-directory: dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,5 +1,4 @@
# Setting test expectation screenshots for Playwright
name: Update Playwright Expectations
on:

View File

@@ -2,14 +2,9 @@ name: Tests CI
on:
push:
branches:
- main
- master
branches: [ main, master, core/*, desktop/* ]
pull_request:
branches:
- main
- master
- 'dev*'
branches: [ main, master, dev*, core/*, desktop/* ]
jobs:
jest-tests:

View File

@@ -2,15 +2,9 @@ name: Vitest Tests
on:
push:
branches:
- main
- master
- 'dev*'
branches: [ main, master, dev*, core/*, desktop/* ]
pull_request:
branches:
- main
- master
- 'dev*'
branches: [ main, master, dev*, core/*, desktop/* ]
jobs:
test:
@@ -20,9 +14,9 @@ jobs:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 'lts/*'
- name: Install dependencies
run: npm ci

View File

@@ -468,6 +468,35 @@ We will support custom icons later.
![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0)
</details>
<details id='extension-api-selection-toolbox'>
<summary>v1.10.9: Selection Toolbox API</summary>
Extensions can register commands that appear in the selection toolbox when specific items are selected on the canvas.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'test.selection.command',
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
// Command logic here
}
}
],
// Return an array of command IDs to show in the selection toolbox
// when an item is selected
getSelectionToolboxCommands: (selectedItem) => ['test.selection.command']
})
```
The selection toolbox will display the command button when items are selected:
![Image](https://github.com/user-attachments/assets/28d91267-c0a9-4bd5-a7c4-36e8ec44c9bd)
</details>
## Development
### Tech Stack

View File

@@ -18,7 +18,7 @@
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "clip"
"directory": "text_encoders"
}
],
"version": 0.4

View File

@@ -0,0 +1,63 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "UNKNOWN NODE",
"pos": [
48,
86
],
"size": {
"0": 358.80780029296875,
"1": 314.7989501953125
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null,
"slot_index": 0
},
{
"name": "foo",
"type": "STRING",
"link": null,
"slot_index": 1,
"widget": {
"name": "foo"
}
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [],
"slot_index": 0,
"shape": 6
}
],
"properties": {
"Node name for S&R": "UNKNOWN NODE"
},
"widgets_values": [
"wd-v1-4-moat-tagger-v2",
0.35,
0.85,
false,
false,
""
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -152,9 +152,10 @@ test.describe('Color Palette', () => {
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.loadWorkflow('every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
'custom-color-palette-obsidian-dark-all-colors.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
@@ -232,7 +233,7 @@ test.describe('Node Color Adjustments', () => {
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
for (const node of JSON.parse(workflow).nodes) {
for (const node of JSON.parse(workflow ?? '{}').nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -85,8 +85,8 @@ test.describe('Missing models warning', () => {
status: 200,
body: JSON.stringify([
{
name: 'clip',
folders: ['ComfyUI/models/clip']
name: 'text_encoders',
folders: ['ComfyUI/models/text_encoders']
}
])
}
@@ -109,7 +109,7 @@ test.describe('Missing models warning', () => {
])
}
comfyPage.page.route(
'**/api/experiment/models/clip',
'**/api/experiment/models/text_encoders',
(route) => route.fulfill(clipModelsRes),
{ times: 1 }
)

View File

@@ -280,5 +280,63 @@ test.describe('Topbar commands', () => {
await comfyPage.confirmDialog.click('confirm')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
})
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['value'] = 'foo'
window['app'].extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
})
})
await comfyPage.confirmDialog.click('reject')
expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull()
})
})
test.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should allow adding commands to selection toolbox', async ({
comfyPage
}) => {
// Register an extension with a selection toolbox command
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'test.selection.command',
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
window['selectionCommandExecuted'] = true
}
}
],
getSelectionToolboxCommands: () => ['test.selection.command']
})
})
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Click the command button in the selection toolbox
const toolboxButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-star)'
)
await toolboxButton.click()
// Verify the command was executed
expect(
await comfyPage.page.evaluate(() => window['selectionCommandExecuted'])
).toBe(true)
})
})
})

View File

@@ -1,3 +1,4 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
@@ -278,8 +279,8 @@ export class ComfyPage {
await this.page.addStyleTag({
content: `
* {
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
})
await this.page.waitForFunction(() => document.fonts.ready)
await this.page.waitForFunction(
@@ -646,6 +647,18 @@ export class ComfyPage {
await this.nextFrame()
}
async selectNodes(nodeTitles: string[]) {
await this.page.keyboard.down('Control')
for (const nodeTitle of nodeTitles) {
const nodes = await this.getNodeRefsByTitle(nodeTitle)
for (const node of nodes) {
await node.click('title')
}
}
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async select2Nodes() {
// Select 2 CLIP nodes.
await this.page.keyboard.down('Control')
@@ -835,12 +848,24 @@ export class ComfyPage {
(
await this.page.evaluate((type) => {
return window['app'].graph.nodes
.filter((n) => n.type === type)
.map((n) => n.id)
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
}, type)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getNodeRefsByTitle(title: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((title) => {
return window['app'].graph.nodes
.filter((n: LGraphNode) => n.title === title)
.map((n: LGraphNode) => n.id)
}, title)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getFirstNodeRef(): Promise<NodeReference | null> {
const id = await this.page.evaluate(() => {
return window['app'].graph.nodes[0]?.id
@@ -885,10 +910,10 @@ export class ComfyPage {
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page, request }, use) => {
comfyPage: async ({ page, request }, use, testInfo) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = comfyPageFixture.info()
const { parallelIndex } = testInfo
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
@@ -896,9 +921,10 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
// Hide canvas menu/info by default.
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,

View File

@@ -53,4 +53,11 @@ test.describe('Optional input', () => {
await comfyPage.loadWorkflow('simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
})
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
await expect(comfyPage.canvas).toHaveScreenshot(
'missing_nodes_converted_widget.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -176,6 +176,23 @@ test.describe('Remote COMBO Widget', () => {
})
test.describe('Refresh Behavior', () => {
test('refresh button is visible in selection toolbar when node is selected', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
// Select remote widget node
await comfyPage.page.keyboard.press('Control+A')
await expect(
comfyPage.page.locator('.selection-toolbox .pi-refresh')
).toBeVisible()
})
test('refreshes options when TTL expires', async ({ comfyPage }) => {
// Fulfill each request with a unique timestamp
await comfyPage.page.route(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -0,0 +1,187 @@
import { expect } from '@playwright/test'
import { comfyPageFixture } from './fixtures/ComfyPage'
const test = comfyPageFixture
test.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
expect(
await comfyPage.page.locator('.selection-overlay-container').isVisible()
).toBe(false)
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection toolbox should be visible with multiple nodes selected
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
// Selection overlay should be visible but without border
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection overlay should show border with multiple selections
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
// Deselect to single node
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Border should be hidden again
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
})
test('displays refresh button in toolbox when all nodes are selected', async ({
comfyPage
}) => {
// Select all nodes
await comfyPage.page.focus('canvas')
await comfyPage.page.keyboard.press('Control+A')
await expect(
comfyPage.page.locator('.selection-toolbox .pi-refresh')
).toBeVisible()
})
test('displays bypass button in toolbox when nodes are selected', async ({
comfyPage
}) => {
// A group + a KSampler node
await comfyPage.loadWorkflow('single_group')
// Select group + node should show bypass button
await comfyPage.page.focus('canvas')
await comfyPage.page.keyboard.press('Control+A')
await expect(
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
)
).toBeVisible()
// Deselect node (Only group is selected) should hide bypass button
await comfyPage.selectNodes(['KSampler'])
await expect(
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
)
).not.toBeVisible()
})
test.describe('Color Picker', () => {
test('displays color picker button and allows color selection', async ({
comfyPage
}) => {
// Select a node
await comfyPage.selectNodes(['KSampler'])
// Color picker button should be visible
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
)
await expect(colorPickerButton).toBeVisible()
// Click color picker button
await colorPickerButton.click()
// Color picker dropdown should be visible
const colorPickerDropdown = comfyPage.page.locator(
'.color-picker-container'
)
await expect(colorPickerDropdown).toBeVisible()
// Select a color (e.g., blue)
const blueColorOption = colorPickerDropdown.locator(
'i[data-testid="blue"]'
)
await blueColorOption.click()
// Dropdown should close after selection
await expect(colorPickerDropdown).not.toBeVisible()
// Node should have the selected color class/style
// Note: Exact verification method depends on how color is applied to nodes
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
expect(selectedNode.getProperty('color')).not.toBeNull()
})
test('color picker shows current color of selected nodes', async ({
comfyPage
}) => {
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
)
// Initially should show default color
await expect(colorPickerButton).not.toHaveAttribute('color')
// Click color picker and select a color
await colorPickerButton.click()
const redColorOption = comfyPage.page.locator(
'.color-picker-container i[data-testid="red"]'
)
await redColorOption.click()
// Button should now show the selected color
await expect(colorPickerButton).toHaveCSS(
'color',
'rgb(85, 51, 51)' // Red color, adjust if different
)
})
test('color picker shows mixed state for differently colored selections', async ({
comfyPage
}) => {
// Select first node and color it
await comfyPage.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
await comfyPage.selectNodes(['KSampler'])
// Select second node and color it differently
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="red"]')
.click()
// Select both nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Color picker should show null/mixed state
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
)
await expect(colorPickerButton).not.toHaveAttribute('color')
})
})
})

View File

@@ -84,3 +84,19 @@ test.describe('Number widget', () => {
).toBeDefined()
})
})
test.describe('Dynamic widget manipulation', () => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.page.waitForTimeout(300)
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

29
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.10.1",
"version": "1.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.10.1",
"version": "1.11.0",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.16",
"@comfyorg/litegraph": "^0.8.81",
"@comfyorg/comfyui-electron-types": "^0.4.20",
"@comfyorg/litegraph": "^0.8.92",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -85,7 +85,7 @@
"typescript-strict-plugin": "^2.4.4",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.6",
"vite": "^5.4.14",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.1.9",
@@ -1938,15 +1938,15 @@
"dev": true
},
"node_modules/@comfyorg/comfyui-electron-types": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.16.tgz",
"integrity": "sha512-AKy4WLVAuDka/Xjv8zrKwfU/wfRSQpFVE5DgxoLfvroCI0sw+rV1JqdL6xFVrYIoeprzbfKhQiyqlAWU+QgHyg==",
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.20.tgz",
"integrity": "sha512-JFKGk9wSx7CcYh9MRNo7bqTLJwQzVc+1Xg8V2Ghn9BS3RzpmkfktaWHi+waU7/CRQMzvjF+mnDPP58xk1xbVhA==",
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.8.81",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.81.tgz",
"integrity": "sha512-YJDbOXGTDUKdLooNgNlfY3Zrl9GM4t1QPYNZS/qd5xvU5pPsqZ743Hz8gqH5tr4g4xcuC94q+pCek2yAfsIwpA==",
"version": "0.8.92",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.92.tgz",
"integrity": "sha512-vDOYEYqFVboVPg7lzUGKgtVJUsy2LObajw1ghKETM0DTYx5NP2Dw76RjjdD+lGUSAw8AjaBC6tbWH7HP0XXHaw==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -18509,10 +18509,11 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.10.1",
"version": "1.11.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -73,7 +73,7 @@
"typescript-strict-plugin": "^2.4.4",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.6",
"vite": "^5.4.14",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-static-copy": "^1.0.5",
"vitest": "^2.1.9",
@@ -83,8 +83,8 @@
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.16",
"@comfyorg/litegraph": "^0.8.81",
"@comfyorg/comfyui-electron-types": "^0.4.20",
"@comfyorg/litegraph": "^0.8.92",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

@@ -668,21 +668,6 @@
"name": "control_v11p_sd15_openpose_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors",
"directory": "controlnet"
},
{
"name": "Anything-V3.0.ckpt",
"url": "https://huggingface.co/xiaolxl/Stable-diffusion-models/resolve/main/Anything-V3.0.ckpt?download=true",
"directory": "checkpoints"
},
{
"name": "AOM3A3.safetensors",
"url": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/eb7490173381625e0403dd52b8051cb969093dc1/Models/AbyssOrangeMix3/AOM3A3.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "kl-f8-anime2.ckpt",
"url": "https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt?download=true",
"directory": "vae"
}
]
}

View File

@@ -957,16 +957,6 @@
},
"version": 0.4,
"models": [
{
"name": "Anything-V3.0.ckpt",
"url": "https://huggingface.co/xiaolxl/Stable-diffusion-models/resolve/main/Anything-V3.0.ckpt?download=true",
"directory": "checkpoints"
},
{
"name": "AbyssOrangeMix2_hard.safetensors",
"url": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "vae-ft-mse-840000-ema-pruned.safetensors",
"url": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors?download=true",

View File

@@ -958,16 +958,6 @@
},
"version": 0.4,
"models": [
{
"name": "Anything-V3.0.ckpt",
"url": "https://huggingface.co/xiaolxl/Stable-diffusion-models/resolve/main/Anything-V3.0.ckpt?download=true",
"directory": "checkpoints"
},
{
"name": "AbyssOrangeMix2_hard.safetensors",
"url": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "vae-ft-mse-840000-ema-pruned.safetensors",
"url": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors?download=true",

View File

@@ -611,11 +611,6 @@
},
"version": 0.4,
"models": [
{
"name": "Anything-V3.0.ckpt",
"url": "https://huggingface.co/xiaolxl/Stable-diffusion-models/resolve/main/Anything-V3.0.ckpt?download=true",
"directory": "checkpoints"
},
{
"name": "vae-ft-mse-840000-ema-pruned.safetensors",
"url": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors?download=true",

View File

@@ -371,11 +371,6 @@
},
"version": 0.4,
"models": [
{
"name": "Anything-V3.0.ckpt",
"url": "https://huggingface.co/xiaolxl/Stable-diffusion-models/resolve/main/Anything-V3.0.ckpt?download=true",
"directory": "checkpoints"
},
{
"name": "control_v11p_sd15_scribble_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors?download=true",

View File

@@ -324,7 +324,7 @@
"outputs": [],
"properties": {},
"widgets_values": [
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#pose-controlnet)"
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets)"
],
"color": "#432",
"bgcolor": "#653"

View File

@@ -453,17 +453,7 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
},
{
"name": "flux1-canny-dev-lora.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev-lora/resolve/main/flux1-canny-dev-lora.safetensors?download=true",
"directory": "loras"
},
{
"name": "flux1-dev-fp8.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
"directory": "checkpoints"
"directory": "text_encoders"
},
{
"name": "ae.safetensors",
@@ -478,7 +468,7 @@
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
}
]
}

View File

@@ -428,12 +428,7 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
},
{
"name": "flux1-dev-fp8.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
"directory": "checkpoints"
"directory": "text_encoders"
},
{
"name": "ae.safetensors",
@@ -448,7 +443,7 @@
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "flux1-depth-dev-lora.safetensors",

View File

@@ -463,7 +463,7 @@
"properties": {
"Node name for S&R": "UNETLoader"
},
"widgets_values": ["flux1-dev-fp8.safetensors", "default"],
"widgets_values": ["flux1-dev.safetensors", "default"],
"color": "#223",
"bgcolor": "#335"
},
@@ -542,7 +542,7 @@
"text": ""
},
"widgets_values": [
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev-fp8.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
],
"color": "#432",
"bgcolor": "#653"
@@ -750,12 +750,12 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "ae.safetensors",
@@ -763,8 +763,8 @@
"directory": "vae"
},
{
"name": "flux1-dev-fp8.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
"name": "flux1-dev.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors?download=true",
"directory": "diffusion_models"
}
]

View File

@@ -437,12 +437,12 @@
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "flux1-fill-dev.safetensors",

View File

@@ -470,12 +470,12 @@
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "flux1-fill-dev.safetensors",

View File

@@ -473,7 +473,7 @@
"text": ""
},
"widgets_values": [
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/unet/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/clip/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
"If you get an error in any of the nodes above make sure the files are in the correct directories.\n\nSee the top of the examples page for the links : https://comfyanonymous.github.io/ComfyUI_examples/flux/\n\nflux1-dev.safetensors goes in: ComfyUI/models/diffusion_models/\n\nt5xxl_fp16.safetensors and clip_l.safetensors go in: ComfyUI/models/text_encoders/\n\nae.safetensors goes in: ComfyUI/models/vae/\n\n\nTip: You can set the weight_dtype above to one of the fp8 types if you have memory issues."
],
"color": "#432",
"bgcolor": "#653"
@@ -920,10 +920,10 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "flux1-dev-fp8.safetensors",
"name": "flux1-dev.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors?download=true",
"directory": "diffusion_models"
},
@@ -932,11 +932,6 @@
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors?download=true",
"directory": "clip_vision"
},
{
"name": "flux1-dev-fp8.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-dev/resolve/main/flux1-dev-fp8.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "ae.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors?download=true",
@@ -944,13 +939,13 @@
},
{
"name": "flux1-redux-dev.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors?download=true",
"directory": "style_models"
},
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
}
]
}

View File

@@ -542,7 +542,7 @@
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "hunyuan_video_t2v_720p_bf16.safetensors",

View File

@@ -311,7 +311,7 @@
"outputs": [],
"properties": {},
"widgets_values": [
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/)"
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting)"
],
"color": "#432",
"bgcolor": "#653"

View File

@@ -524,17 +524,5 @@
"offset": [1200.17, 444.58]
}
},
"version": 0.4,
"models": [
{
"name": "wd-illusion-fp16.safetensors",
"url": "https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "cardosAnime_v10.safetensors",
"url": "https://huggingface.co/jomcs/NeverEnding_Dream-Feb19-2023/resolve/07c9bc67d4ac9a85b68321d9b62f20c00171d8d5/CarDos%20Anime/cardosAnime_v10.safetensors?download=true",
"directory": "checkpoints"
}
]
"version": 0.4
}

View File

@@ -268,7 +268,7 @@
"outputs": [],
"properties": {},
"widgets_values": [
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting)"
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/lora/)"
],
"color": "#432",
"bgcolor": "#653"

View File

@@ -471,7 +471,7 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "ltx-video-2b-v0.9.safetensors",

View File

@@ -408,7 +408,7 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "ltx-video-2b-v0.9.safetensors",

View File

@@ -483,20 +483,10 @@
"url": "https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors?download=true",
"directory": "controlnet"
},
{
"name": "AOM3A1.safetensors",
"url": "https://huggingface.co/Eata/Model_V1/resolve/main/AOM3A1.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "control_v11p_sd15_openpose_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors",
"directory": "controlnet"
},
{
"name": "kl-f8-anime2.ckpt",
"url": "https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt?download=true",
"directory": "vae"
}
]
}

View File

@@ -297,7 +297,7 @@
{
"name": "t5xxl_fp16.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors?download=true",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "mochi_preview_bf16.safetensors",

View File

@@ -481,11 +481,6 @@
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "sd_xl_refiner_1.0.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "clip_vision_g.safetensors",
"url": "https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors?download=true",

View File

@@ -485,11 +485,6 @@
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "sd_xl_refiner_1.0.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors?download=true",
"directory": "checkpoints"
},
{
"name": "clip_vision_g.safetensors",
"url": "https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors?download=true",

View File

@@ -291,7 +291,7 @@
{
"name": "t5_base.safetensors",
"url": "https://huggingface.co/google-t5/t5-base/resolve/main/model.safetensors",
"directory": "clip"
"directory": "text_encoders"
},
{
"name": "stable_audio_open_1.0.safetensors",

View File

@@ -1,425 +0,0 @@
{
"last_node_id": 17,
"last_link_id": 23,
"nodes": [
{
"id": 8,
"type": "VAEDecode",
"pos": [1235.7215576171875, 577.1878662109375],
"size": [210, 46],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "samples",
"localized_name": "samples",
"type": "LATENT",
"link": 7
},
{ "name": "vae", "localized_name": "vae", "type": "VAE", "link": 21 }
],
"outputs": [
{
"name": "IMAGE",
"localized_name": "IMAGE",
"type": "IMAGE",
"links": [9],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "VAEDecode" },
"widgets_values": []
},
{
"id": 10,
"type": "LatentUpscale",
"pos": [1238, 170],
"size": [315, 130],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "samples",
"localized_name": "samples",
"type": "LATENT",
"link": 10
}
],
"outputs": [
{
"name": "LATENT",
"localized_name": "LATENT",
"type": "LATENT",
"links": [14]
}
],
"properties": { "Node name for S&R": "LatentUpscale" },
"widgets_values": ["nearest-exact", 1152, 1152, "disabled"]
},
{
"id": 13,
"type": "VAEDecode",
"pos": [1961, 125],
"size": [210, 46],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"localized_name": "samples",
"type": "LATENT",
"link": 15
},
{ "name": "vae", "localized_name": "vae", "type": "VAE", "link": 22 }
],
"outputs": [
{
"name": "IMAGE",
"localized_name": "IMAGE",
"type": "IMAGE",
"links": [17],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "VAEDecode" },
"widgets_values": []
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [374, 171],
"size": [422.84503173828125, 164.31304931640625],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{ "name": "clip", "localized_name": "clip", "type": "CLIP", "link": 19 }
],
"outputs": [
{
"name": "CONDITIONING",
"localized_name": "CONDITIONING",
"type": "CONDITIONING",
"links": [4, 12],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "CLIPTextEncode" },
"widgets_values": [
"masterpiece HDR victorian portrait painting of woman, blonde hair, mountain nature, blue sky\n"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [377, 381],
"size": [425.27801513671875, 180.6060791015625],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{ "name": "clip", "localized_name": "clip", "type": "CLIP", "link": 20 }
],
"outputs": [
{
"name": "CONDITIONING",
"localized_name": "CONDITIONING",
"type": "CONDITIONING",
"links": [6, 13],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "CLIPTextEncode" },
"widgets_values": ["bad hands, text, watermark\n"]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [435, 600],
"size": [315, 106],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"localized_name": "LATENT",
"type": "LATENT",
"links": [2],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "EmptyLatentImage" },
"widgets_values": [768, 768, 1]
},
{
"id": 11,
"type": "KSampler",
"pos": [1585, 114],
"size": [315, 262],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"localized_name": "model",
"type": "MODEL",
"link": 23,
"slot_index": 0
},
{
"name": "positive",
"localized_name": "positive",
"type": "CONDITIONING",
"link": 12,
"slot_index": 1
},
{
"name": "negative",
"localized_name": "negative",
"type": "CONDITIONING",
"link": 13,
"slot_index": 2
},
{
"name": "latent_image",
"localized_name": "latent_image",
"type": "LATENT",
"link": 14,
"slot_index": 3
}
],
"outputs": [
{
"name": "LATENT",
"localized_name": "LATENT",
"type": "LATENT",
"links": [15],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "KSampler" },
"widgets_values": [
469771404043268,
"randomize",
14,
8,
"dpmpp_2m",
"simple",
0.5
]
},
{
"id": 12,
"type": "SaveImage",
"pos": [2203, 123],
"size": [407.53717041015625, 468.13226318359375],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "images",
"localized_name": "images",
"type": "IMAGE",
"link": 17
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI"]
},
{
"id": 3,
"type": "KSampler",
"pos": [845, 172],
"size": [315, 262],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "model",
"localized_name": "model",
"type": "MODEL",
"link": 18
},
{
"name": "positive",
"localized_name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"localized_name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"localized_name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"localized_name": "LATENT",
"type": "LATENT",
"links": [7, 10],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "KSampler" },
"widgets_values": [
89848141647836,
"randomize",
12,
8,
"dpmpp_sde",
"normal",
1
]
},
{
"id": 16,
"type": "CheckpointLoaderSimple",
"pos": [24, 315],
"size": [315, 98],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"localized_name": "MODEL",
"type": "MODEL",
"links": [18, 23],
"slot_index": 0
},
{
"name": "CLIP",
"localized_name": "CLIP",
"type": "CLIP",
"links": [19, 20],
"slot_index": 1
},
{
"name": "VAE",
"localized_name": "VAE",
"type": "VAE",
"links": [21, 22],
"slot_index": 2
}
],
"properties": { "Node name for S&R": "CheckpointLoaderSimple" },
"widgets_values": ["v2-1_768-ema-pruned.safetensors"]
},
{
"id": 9,
"type": "SaveImage",
"pos": [1495.7215576171875, 576.1878662109375],
"size": [232.94032287597656, 282.4336242675781],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"localized_name": "images",
"type": "IMAGE",
"link": 9
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI"]
},
{
"id": 17,
"type": "MarkdownNote",
"pos": [0, 795],
"size": [225, 60],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"🛈 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/)"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[2, 5, 0, 3, 3, "LATENT"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[9, 8, 0, 9, 0, "IMAGE"],
[10, 3, 0, 10, 0, "LATENT"],
[12, 6, 0, 11, 1, "CONDITIONING"],
[13, 7, 0, 11, 2, "CONDITIONING"],
[14, 10, 0, 11, 3, "LATENT"],
[15, 11, 0, 13, 0, "LATENT"],
[17, 13, 0, 12, 0, "IMAGE"],
[18, 16, 0, 3, 0, "MODEL"],
[19, 16, 1, 6, 0, "CLIP"],
[20, 16, 1, 7, 0, "CLIP"],
[21, 16, 2, 8, 1, "VAE"],
[22, 16, 2, 13, 1, "VAE"],
[23, 16, 0, 11, 0, "MODEL"]
],
"groups": [
{
"id": 1,
"title": "Txt2Img",
"bounding": [0, 30, 1211, 708],
"color": "#a1309b",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "Save Intermediate Image",
"bounding": [1230, 495, 516, 196],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Hires Fix",
"bounding": [1230, 30, 710, 464],
"color": "#b58b2a",
"font_size": 24,
"flags": {}
},
{
"id": 4,
"title": "Save Final Image",
"bounding": [1950, 30, 483, 199],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.8037574853834974,
"offset": [540.0834501660246, 269.28523360433144]
}
},
"version": 0.4
}

View File

@@ -1,360 +0,0 @@
{
"last_node_id": 31,
"last_link_id": 87,
"nodes": [
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [432, 158],
"size": [422.85, 164.31],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 81
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [4],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"outdoors in the yosemite national park mountains nature\n\n\n\n"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [434, 371],
"size": [425.28, 180.61],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 82
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [6],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": ["watermark, text\n"]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [1422, 387],
"size": [210, 46],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 42
},
{
"name": "vae",
"type": "VAE",
"link": 83
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [22],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 3,
"type": "KSampler",
"pos": [940, 180],
"size": [315, 262],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 80
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 72
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [42],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
152545289528694,
"randomize",
20,
8,
"uni_pc_bh2",
"normal",
1
]
},
{
"id": 29,
"type": "CheckpointLoaderSimple",
"pos": [17, 303],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [80],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [81, 82],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [83, 84],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["512-inpainting-ema.safetensors"]
},
{
"id": 20,
"type": "LoadImage",
"pos": [-107, 726],
"size": [344, 346],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [85],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["yosemite_outpaint_example.png", "image"]
},
{
"id": 30,
"type": "ImagePadForOutpaint",
"pos": [269, 727],
"size": [315, 174],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 85
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"shape": 3,
"links": [87],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"shape": 3,
"links": [86],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ImagePadForOutpaint"
},
"widgets_values": [0, 128, 0, 128, 40]
},
{
"id": 9,
"type": "SaveImage",
"pos": [1671, 384],
"size": [360.55, 441.53],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 22
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI"]
},
{
"id": 26,
"type": "VAEEncodeForInpaint",
"pos": [617, 720],
"size": [226.8, 98],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 87
},
{
"name": "vae",
"type": "VAE",
"link": 84
},
{
"name": "mask",
"type": "MASK",
"link": 86
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [72],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncodeForInpaint"
},
"widgets_values": [8]
},
{
"id": 31,
"type": "MarkdownNote",
"pos": [15, 465],
"size": [225, 60],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"\ud83d\udec8 [Learn more about this workflow](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting)"
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[4, 6, 0, 3, 1, "CONDITIONING"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[22, 8, 0, 9, 0, "IMAGE"],
[42, 3, 0, 8, 0, "LATENT"],
[72, 26, 0, 3, 3, "LATENT"],
[80, 29, 0, 3, 0, "MODEL"],
[81, 29, 1, 6, 0, "CLIP"],
[82, 29, 1, 7, 0, "CLIP"],
[83, 29, 2, 8, 1, "VAE"],
[84, 29, 2, 26, 1, "VAE"],
[85, 20, 0, 30, 0, "IMAGE"],
[86, 30, 1, 26, 2, "MASK"],
[87, 30, 0, 26, 0, "IMAGE"]
],
"groups": [
{
"id": 1,
"title": "Load image and pad for outpainting",
"bounding": [-120, 600, 1038, 509],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.93,
"offset": [359.29, 119.05]
}
},
"version": 0.4,
"models": [
{
"name": "512-inpainting-ema.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-2-inpainting/resolve/main/512-inpainting-ema.safetensors?download=true",
"directory": "checkpoints"
}
]
}

View File

@@ -69,7 +69,12 @@ test('collect-i18n-general', async ({ comfyPage }) => {
name: setting.name,
tooltip: setting.tooltip,
category: setting.category,
options: setting.options
options:
typeof setting.options === 'function'
? // @ts-expect-error: Audit and deprecate usage of legacy options type:
// (value) => [string | {text: string, value: string}]
setting.options(setting.defaultValue ?? '')
: setting.options
}))
})

View File

@@ -532,6 +532,12 @@ dialog::backdrop {
height: var(--comfy-img-preview-height);
}
.comfy-img-preview video {
object-fit: contain;
height: 100%;
width: 100%;
}
.comfy-missing-nodes li button {
font-size: 12px;
margin-left: 5px;

View File

@@ -13,7 +13,7 @@
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeMenu"
@contextmenu="showNativeSystemMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
</div>
@@ -26,7 +26,7 @@ import { CSSProperties, computed, watchEffect } from 'vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { showNativeMenu } from '@/utils/envUtil'
import { showNativeSystemMenu } from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()

View File

@@ -0,0 +1,88 @@
<template>
<div
class="color-customization-selector-container flex flex-row items-center gap-2"
>
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.name !== '_custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i v-else class="pi pi-palette text-lg"></i>
</template>
</SelectButton>
<ColorPicker
v-if="selectedColorOption.name === '_custom'"
v-model="customColorValue"
/>
</div>
</template>
<script setup lang="ts">
import ColorPicker from 'primevue/colorpicker'
import SelectButton from 'primevue/selectbutton'
import { computed, onMounted, ref, watch } from 'vue'
const {
modelValue,
colorOptions,
allowCustom = true
} = defineProps<{
modelValue: string | null
colorOptions: { name: Exclude<string, '_custom'>; value: string }[]
allowCustom?: boolean
}>()
const customColorOption = { name: '_custom', value: '' }
const colorOptionsWithCustom = computed(() => [
...colorOptions,
...(allowCustom ? [customColorOption] : [])
])
const emit = defineEmits<{
'update:modelValue': [value: string | null]
}>()
const selectedColorOption = ref(customColorOption)
const customColorValue = ref('')
// Initialize the component with the provided modelValue
onMounted(() => {
if (modelValue) {
const predefinedColor = colorOptions.find((opt) => opt.value === modelValue)
if (predefinedColor) {
selectedColorOption.value = predefinedColor
} else {
selectedColorOption.value = customColorOption
customColorValue.value = modelValue.replace('#', '')
}
}
})
// Watch for changes in selection and emit updates
watch(selectedColorOption, (newOption, oldOption) => {
if (newOption.name === '_custom') {
// Inherit the color from previous selection
customColorValue.value = oldOption.value.replace('#', '')
} else {
emit('update:modelValue', newOption.value)
}
})
watch(customColorValue, (newValue) => {
if (selectedColorOption.value.name === '_custom') {
emit('update:modelValue', newValue ? `#${newValue}` : null)
}
})
</script>

View File

@@ -20,37 +20,10 @@
<Divider />
<div class="field color-field">
<label for="color">{{ $t('g.color') }}</label>
<div class="color-picker-container">
<SelectButton
v-model="selectedColor"
:options="colorOptions"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.value !== 'custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i
v-else
class="pi pi-palette"
:style="{ fontSize: '1.2rem' }"
v-tooltip="$t('color.custom')"
></i>
</template>
</SelectButton>
<ColorPicker
v-if="selectedColor.value === 'custom'"
v-model="customColor"
/>
</div>
<ColorCustomizationSelector
v-model="finalColor"
:color-options="colorOptions"
/>
</div>
</div>
<template #footer>
@@ -72,13 +45,13 @@
<script setup lang="ts">
import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import Dialog from 'primevue/dialog'
import Divider from 'primevue/divider'
import SelectButton from 'primevue/selectbutton'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ColorCustomizationSelector from '@/components/common/ColorCustomizationSelector.vue'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()
@@ -118,29 +91,24 @@ const colorOptions = [
{ name: t('color.green'), value: '#28a745' },
{ name: t('color.red'), value: '#dc3545' },
{ name: t('color.pink'), value: '#e83e8c' },
{ name: t('color.yellow'), value: '#ffc107' },
{ name: t('color.custom'), value: 'custom' }
{ name: t('color.yellow'), value: '#ffc107' }
]
const defaultIcon = iconOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
)
const defaultColor = colorOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkColor
)
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const selectedColor = ref<{ name: string; value: string }>(defaultColor)
const finalColor = computed(() =>
selectedColor.value.value === 'custom'
? `#${customColor.value}`
: selectedColor.value.value
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const customColor = ref('000000')
const closeDialog = () => {
visible.value = false
const resetCustomization = () => {
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
}
const confirmCustomization = () => {
@@ -148,21 +116,8 @@ const confirmCustomization = () => {
closeDialog()
}
const resetCustomization = () => {
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
const colorOption = colorOptions.find(
(option) => option.value === props.initialColor
)
if (!props.initialColor) {
selectedColor.value = defaultColor
} else if (!colorOption) {
customColor.value = props.initialColor.replace('#', '')
selectedColor.value = { name: t('color.custom'), value: 'custom' }
} else {
selectedColor.value = colorOption
}
const closeDialog = () => {
visible.value = false
}
watch(
@@ -190,10 +145,4 @@ watch(
flex-direction: column;
gap: 0.5rem;
}
.color-picker-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>

View File

@@ -1,6 +1,6 @@
<!--
A refresh button that disables and shows a progress spinner whilst active.
Usage:
```vue
<RefreshButton

View File

@@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils'
import ColorPicker from 'primevue/colorpicker'
import PrimeVue from 'primevue/config'
import SelectButton from 'primevue/selectbutton'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import ColorCustomizationSelector from '../ColorCustomizationSelector.vue'
describe('ColorCustomizationSelector', () => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
})
}
it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)
expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})
it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// First select a predefined color
await selectButton.setValue(colorOptions[0])
// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })
const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})
it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
})
})

View File

@@ -51,7 +51,8 @@ const allowedSources = [
const allowedSuffixes = ['.safetensors', '.sft']
// Models that fail above conditions but are still allowed
const whiteListedUrls = new Set([
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt'
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true'
])
interface ModelInfo {

View File

@@ -280,7 +280,7 @@ watch(activeCategory, (_, oldValue) => {
<style>
.settings-tab-panels {
padding-top: 0px !important;
padding-top: 0 !important;
}
</style>

View File

@@ -32,6 +32,12 @@ const props = defineProps<{
const { t } = useI18n()
function translateOptions(options: (SettingOption | string)[]) {
if (typeof options === 'function') {
// @ts-expect-error: Audit and deprecate usage of legacy options type:
// (value) => [string | {text: string, value: string}]
return translateOptions(options(props.setting.value ?? ''))
}
return options.map((option) => {
const optionLabel = typeof option === 'string' ? option : option.text
const optionValue = typeof option === 'string' ? option : option.value

View File

@@ -0,0 +1,47 @@
// @ts-strict-ignore
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import SettingItem from '../SettingItem.vue'
const i18n = createI18n({
legacy: false,
locale: 'en'
})
vi.mock('@/utils/formatUtil', () => ({
normalizeI18nKey: vi.fn()
}))
describe('SettingItem', () => {
const mountComponent = (props: any, options = {}): any => {
return mount(SettingItem, {
global: {
plugins: [PrimeVue, i18n, createPinia()]
},
props,
...options
})
}
it('translates options that use legacy type', () => {
const wrapper = mountComponent({
setting: {
id: 'Comfy.NodeInputConversionSubmenus',
name: 'Node Input Conversion Submenus',
type: 'combo',
value: 'Top',
options: (value: string) => ['Correctly Translated']
}
})
// Get the options property of the FormItem
const options = wrapper.vm.formItem.options
expect(options).toEqual([
{ text: 'Correctly Translated', value: 'Correctly Translated' }
])
})
})

View File

@@ -28,6 +28,9 @@
class="w-full h-full touch-none"
/>
<NodeSearchboxPopover />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<NodeTooltip v-if="tooltipEnabled" />
<NodeBadge />
</template>
@@ -40,10 +43,13 @@ import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeBadge from '@/components/graph/NodeBadge.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasDrop } from '@/composables/useCanvasDrop'
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
import { useCopy } from '@/composables/useCopy'
@@ -82,6 +88,9 @@ const canvasMenuEnabled = computed(() =>
settingStore.get('Comfy.Graph.CanvasMenu')
)
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox')
)
watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
@@ -187,6 +196,11 @@ onMounted(async () => {
comfyAppReady.value = true
comfyApp.canvas.onSelectionChange = useChainCallback(
comfyApp.canvas.onSelectionChange,
() => canvasStore.updateSelectedItems()
)
// Load color palette
colorPaletteStore.customPalettes = settingStore.get(
'Comfy.CustomColorPalettes'

View File

@@ -0,0 +1,103 @@
<!-- This component is used to bound the selected items on the canvas. -->
<template>
<div
class="selection-overlay-container pointer-events-none z-40"
:class="{
'show-border': showBorder
}"
:style="style"
v-show="visible"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { createBounds } from '@comfyorg/litegraph'
import type { LGraphCanvas } from '@comfyorg/litegraph'
import { ref, watch } from 'vue'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasStore } from '@/stores/graphStore'
const canvasStore = useCanvasStore()
const { style, updatePosition } = useAbsolutePosition()
const visible = ref(false)
const showBorder = ref(false)
const positionSelectionOverlay = (canvas: LGraphCanvas) => {
const selectedItems = canvas.selectedItems
showBorder.value = selectedItems.size > 1
if (!selectedItems.size) {
visible.value = false
return
}
visible.value = true
const bounds = createBounds(selectedItems)
updatePosition({
pos: [bounds[0], bounds[1]],
size: [bounds[2], bounds[3]]
})
}
// Register listener on canvas creation.
watch(
() => canvasStore.canvas,
(canvas: LGraphCanvas | null) => {
if (!canvas) return
canvas.onSelectionChange = useChainCallback(canvas.onSelectionChange, () =>
positionSelectionOverlay(canvas)
)
},
{ immediate: true }
)
watch(
() => {
const canvas = canvasStore.canvas
if (!canvas) return null
return {
scale: canvas.ds.state.scale,
offset: [canvas.ds.state.offset[0], canvas.ds.state.offset[1]]
}
},
(state) => {
if (!state) return
positionSelectionOverlay(canvasStore.canvas as LGraphCanvas)
}
)
watch(
() => canvasStore.canvas?.state?.draggingItems,
(draggingItems) => {
// Litegraph draggingItems state can end early before the bounding boxes of
// the selected items are updated. Delay to make sure we put the overlay in
// the correct position.
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
if (draggingItems === false) {
setTimeout(() => {
visible.value = true
positionSelectionOverlay(canvasStore.canvas as LGraphCanvas)
}, 100)
} else {
visible.value = false
}
}
)
</script>
<style scoped>
.selection-overlay-container > * {
pointer-events: auto;
}
.show-border {
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<Panel
class="selection-toolbox absolute left-1/2 rounded-lg"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
>
<ColorPickerButton v-if="nodeSelected || groupSelected" />
<Button
v-if="nodeSelected"
severity="secondary"
text
@click="
() => commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')
"
data-testid="bypass-button"
>
<template #icon>
<i-game-icons:detour />
</template>
</Button>
<Button
v-if="nodeSelected || groupSelected"
severity="secondary"
text
icon="pi pi-thumbtack"
@click="() => commandStore.execute('Comfy.Canvas.ToggleSelected.Pin')"
/>
<Button
severity="danger"
text
icon="pi pi-trash"
@click="() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')"
/>
<Button
v-if="isRefreshable"
severity="info"
text
icon="pi pi-refresh"
@click="refreshSelected"
/>
<Button
v-for="command in extensionToolboxCommands"
:key="command.id"
severity="secondary"
text
:icon="typeof command.icon === 'function' ? command.icon() : command.icon"
@click="() => commandStore.execute(command.id)"
/>
</Panel>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Panel from 'primevue/panel'
import { computed } from 'vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
import { useExtensionService } from '@/services/extensionService'
import { ComfyCommand, useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const extensionService = useExtensionService()
const { isRefreshable, refreshSelected } = useRefreshableSelection()
const nodeSelected = computed(() =>
canvasStore.selectedItems.some(isLGraphNode)
)
const groupSelected = computed(() =>
canvasStore.selectedItems.some(isLGraphGroup)
)
const extensionToolboxCommands = computed<ComfyCommand[]>(() => {
const commandIds = new Set<string>(
canvasStore.selectedItems
.map(
(item) =>
extensionService
.invokeExtensions('getSelectionToolboxCommands', item)
.flat() as string[]
)
.flat()
)
return Array.from(commandIds)
.map((commandId) => commandStore.getCommand(commandId))
.filter((command) => command !== undefined)
})
</script>
<style scoped>
.selection-toolbox {
transform: translateX(-50%) translateY(-120%);
}
</style>

View File

@@ -16,9 +16,10 @@
import { LGraphGroup, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
import type { LiteGraphCanvasEvent } from '@comfyorg/litegraph'
import { useEventListener } from '@vueuse/core'
import { CSSProperties, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import EditableText from '@/components/common/EditableText.vue'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
import { app } from '@/scripts/app'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -27,14 +28,7 @@ const settingStore = useSettingStore()
const showInput = ref(false)
const editedTitle = ref('')
const inputStyle = ref<CSSProperties>({
position: 'fixed',
left: '0px',
top: '0px',
width: '200px',
height: '20px',
fontSize: '12px'
})
const { style: inputStyle, updatePosition } = useAbsolutePosition()
const titleEditorStore = useTitleEditorStore()
const canvasStore = useCanvasStore()
@@ -58,41 +52,30 @@ watch(
}
editedTitle.value = target.title
showInput.value = true
previousCanvasDraggable.value = canvasStore.canvas!.allow_dragcanvas
canvasStore.canvas!.allow_dragcanvas = false
const canvas = canvasStore.canvas!
previousCanvasDraggable.value = canvas.allow_dragcanvas
canvas.allow_dragcanvas = false
const scale = canvas.ds.scale
if (target instanceof LGraphGroup) {
const group = target
const [x, y] = group.pos
const [w, h] = group.size
const [left, top] = app.canvasPosToClientPos([x, y])
inputStyle.value.left = `${left}px`
inputStyle.value.top = `${top}px`
const width = w * app.canvas.ds.scale
const height = group.titleHeight * app.canvas.ds.scale
inputStyle.value.width = `${width}px`
inputStyle.value.height = `${height}px`
const fontSize = group.font_size * app.canvas.ds.scale
inputStyle.value.fontSize = `${fontSize}px`
updatePosition(
{
pos: group.pos,
size: [group.size[0], group.titleHeight]
},
{ fontSize: `${group.font_size * scale}px` }
)
} else if (target instanceof LGraphNode) {
const node = target
const [x, y] = node.getBounding()
const canvasWidth = node.width
const canvasHeight = LiteGraph.NODE_TITLE_HEIGHT
const [left, top] = app.canvasPosToClientPos([x, y])
inputStyle.value.left = `${left}px`
inputStyle.value.top = `${top}px`
const width = canvasWidth * app.canvas.ds.scale
const height = canvasHeight * app.canvas.ds.scale
inputStyle.value.width = `${width}px`
inputStyle.value.height = `${height}px`
const fontSize = 12 * app.canvas.ds.scale
inputStyle.value.fontSize = `${fontSize}px`
updatePosition(
{
pos: [x, y],
size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]
},
{ fontSize: `${12 * scale}px` }
)
}
}
)

View File

@@ -0,0 +1,140 @@
<template>
<div class="relative">
<Button
severity="secondary"
text
@click="() => (showColorPicker = !showColorPicker)"
>
<template #icon>
<div class="flex items-center gap-1">
<i class="pi pi-circle-fill" :style="{ color: currentColor }" />
<i class="pi pi-chevron-down" :style="{ fontSize: '0.5rem' }" />
</div>
</template>
</Button>
<div
v-if="showColorPicker"
class="color-picker-container absolute -top-10 left-1/2"
>
<SelectButton
:modelValue="selectedColorOption"
@update:modelValue="applyColor"
:options="colorOptions"
optionLabel="name"
dataKey="value"
>
<template #option="{ option }">
<i
class="pi pi-circle-fill"
:style="{
color: isLightTheme ? option.value.light : option.value.dark
}"
v-tooltip.top="option.localizedName"
:data-testid="option.name"
/>
</template>
</SelectButton>
</div>
</div>
</template>
<script setup lang="ts">
import type { ColorOption as CanvasColorOption } from '@comfyorg/litegraph'
import { LGraphCanvas, LiteGraph, isColorable } from '@comfyorg/litegraph'
import Button from 'primevue/button'
import SelectButton from 'primevue/selectbutton'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCanvasStore } from '@/stores/graphStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { getItemsColorOption } from '@/utils/litegraphUtil'
const { t } = useI18n()
const canvasStore = useCanvasStore()
const colorPaletteStore = useColorPaletteStore()
const isLightTheme = computed(
() => colorPaletteStore.completedActivePalette.light_theme
)
const toLightThemeColor = (color: string) =>
adjustColor(color, { lightness: 0.5 })
const showColorPicker = ref(false)
type ColorOption = {
name: string
localizedName: string
value: {
dark: string
light: string
}
}
const NO_COLOR_OPTION: ColorOption = {
name: 'noColor',
localizedName: t('color.noColor'),
value: {
dark: LiteGraph.NODE_DEFAULT_BGCOLOR,
light: toLightThemeColor(LiteGraph.NODE_DEFAULT_BGCOLOR)
}
}
const colorOptions: ColorOption[] = [
NO_COLOR_OPTION,
...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({
name,
localizedName: t(`color.${name}`),
value: {
dark: color.bgcolor,
light: toLightThemeColor(color.bgcolor)
}
}))
]
const selectedColorOption = ref<ColorOption | null>(null)
const applyColor = (colorOption: ColorOption | null) => {
const colorName = colorOption?.name ?? NO_COLOR_OPTION.name
const canvasColorOption =
colorName === NO_COLOR_OPTION.name
? null
: LGraphCanvas.node_colors[colorName]
for (const item of canvasStore.selectedItems) {
if (isColorable(item)) {
item.setColorOption(canvasColorOption)
}
}
canvasStore.canvas?.setDirty(true, true)
currentColorOption.value = canvasColorOption
showColorPicker.value = false
}
const currentColorOption = ref<CanvasColorOption | null>(null)
const currentColor = computed(() =>
currentColorOption.value
? isLightTheme.value
? toLightThemeColor(currentColorOption.value?.bgcolor)
: currentColorOption.value?.bgcolor
: null
)
watch(
() => canvasStore.selectedItems,
(newSelectedItems) => {
showColorPicker.value = false
selectedColorOption.value = null
currentColorOption.value = getItemsColorOption(newSelectedItems)
}
)
</script>
<style scoped>
.color-picker-container {
transform: translateX(-50%);
}
:deep(.p-togglebutton) {
@apply py-2 px-1;
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<div class="relative w-full h-full">
<Load3DScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
:backgroundImage="backgroundImage"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
@backgroundImageChange="listenBackgroundImageChange"
/>
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
:cameraType="cameraType"
:hasBackgroundImage="hasBackgroundImage"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"
@updateBackgroundColor="handleBackgroundColorChange"
@updateLightIntensity="handleUpdateLightIntensity"
@togglePreview="togglePreview"
@updateFOV="handleUpdateFOV"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
const props = defineProps<{
node: any
type: 'Load3D' | 'Preview3D'
}>()
const node = ref(props.node)
const backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
const hasBackgroundImage = ref(false)
const backgroundImage = ref('')
const showPreviewButton = computed(() => {
return !props.type.includes('Preview')
})
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.value.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.value.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.value.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.value.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.value.properties['Background Image'] = ''
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
node.value.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.value.properties['FOV'] = fov.value
}
const materialMode = ref<'original' | 'normal' | 'wireframe' | 'depth'>(
'original'
)
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.value.properties['Background Color'] = value
}
const listenMaterialModeChange = (
mode: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -0,0 +1,208 @@
<template>
<div class="relative w-full h-full">
<Load3DAnimationScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
:materialMode="materialMode"
:showFOVButton="showFOVButton"
:showLightIntensityButton="showLightIntensityButton"
:playing="playing"
:selectedSpeed="selectedSpeed"
:selectedAnimation="selectedAnimation"
:backgroundImage="backgroundImage"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
@backgroundImageChange="listenBackgroundImageChange"
@animationListChange="animationListChange"
/>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
:cameraType="cameraType"
:hasBackgroundImage="hasBackgroundImage"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"
@updateBackgroundColor="handleBackgroundColorChange"
@updateLightIntensity="handleUpdateLightIntensity"
@togglePreview="togglePreview"
@updateFOV="handleUpdateFOV"
/>
<Load3DAnimationControls
:animations="animations"
:playing="playing"
@togglePlay="togglePlay"
@speedChange="speedChange"
@animationChange="animationChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Load3DAnimationControls from '@/components/load3d/Load3DAnimationControls.vue'
import Load3DAnimationScene from '@/components/load3d/Load3DAnimationScene.vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { AnimationItem } from '@/extensions/core/load3d/interfaces'
const props = defineProps<{
node: any
type: 'Load3DAnimation' | 'Preview3DAnimation'
}>()
const node = ref(props.node)
const backgroundColor = ref('#000000')
const showGrid = ref(true)
const showPreview = ref(false)
const lightIntensity = ref(5)
const showLightIntensityButton = ref(true)
const fov = ref(75)
const showFOVButton = ref(true)
const cameraType = ref<'perspective' | 'orthographic'>('perspective')
const hasBackgroundImage = ref(false)
const animations = ref<AnimationItem[]>([])
const playing = ref(false)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundImage = ref('')
const showPreviewButton = computed(() => {
return !props.type.includes('Preview')
})
const switchCamera = () => {
cameraType.value =
cameraType.value === 'perspective' ? 'orthographic' : 'perspective'
showFOVButton.value = cameraType.value === 'perspective'
node.value.properties['Camera Type'] = cameraType.value
}
const togglePreview = (value: boolean) => {
showPreview.value = value
node.value.properties['Show Preview'] = showPreview.value
}
const toggleGrid = (value: boolean) => {
showGrid.value = value
node.value.properties['Show Grid'] = showGrid.value
}
const handleUpdateLightIntensity = (value: number) => {
lightIntensity.value = value
node.value.properties['Light Intensity'] = lightIntensity.value
}
const handleBackgroundImageUpdate = async (file: File | null) => {
if (!file) {
hasBackgroundImage.value = false
backgroundImage.value = ''
node.value.properties['Background Image'] = ''
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
node.value.properties['Background Image'] = backgroundImage.value
}
const handleUpdateFOV = (value: number) => {
fov.value = value
node.value.properties['FOV'] = fov.value
}
const materialMode = ref<'original' | 'normal' | 'wireframe' | 'depth'>(
'original'
)
const handleBackgroundColorChange = (value: string) => {
backgroundColor.value = value
node.value.properties['Background Color'] = value
}
const togglePlay = (value: boolean) => {
playing.value = value
}
const speedChange = (value: number) => {
selectedSpeed.value = value
}
const animationChange = (value: number) => {
selectedAnimation.value = value
}
const animationListChange = (value: any) => {
animations.value = value
}
const listenMaterialModeChange = (
mode: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const listenBackgroundImageChange = (value: string) => {
backgroundImage.value = value
if (backgroundImage.value && backgroundImage.value !== '') {
hasBackgroundImage.value = true
}
}
</script>

View File

@@ -1,55 +1,31 @@
<template>
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
<Load3DControls
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:showPreview="showPreview"
:lightIntensity="lightIntensity"
:showLightIntensityButton="showLightIntensityButton"
:fov="fov"
:showFOVButton="showFOVButton"
:showPreviewButton="showPreviewButton"
@toggleCamera="onToggleCamera"
@toggleGrid="onToggleGrid"
@togglePreview="onTogglePreview"
@updateBackgroundColor="onUpdateBackgroundColor"
@updateLightIntensity="onUpdateLightIntensity"
@updateFOV="onUpdateFOV"
ref="load3dControlsRef"
<div
v-if="animations && animations.length > 0"
class="absolute top-0 left-0 w-full flex justify-center pt-2 gap-2 items-center pointer-events-auto z-10"
>
<Button class="p-button-rounded p-button-text" @click="togglePlay">
<i
:class="['pi', playing ? 'pi-pause' : 'pi-play', 'text-white text-lg']"
></i>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
optionLabel="name"
optionValue="value"
@change="speedChange"
class="w-24"
/>
<div
v-if="animations && animations.length > 0"
class="absolute top-0 left-0 w-full flex justify-center pt-2 gap-2 items-center z-10"
>
<Button class="p-button-rounded p-button-text" @click="togglePlay">
<i
:class="[
'pi',
playing ? 'pi-pause' : 'pi-play',
'text-white text-lg'
]"
></i>
</Button>
<Select
v-model="selectedSpeed"
:options="speedOptions"
optionLabel="name"
optionValue="value"
@change="speedChange"
class="w-24"
/>
<Select
v-model="selectedAnimation"
:options="animations"
optionLabel="name"
optionValue="index"
@change="animationChange"
class="w-32"
/>
</div>
<Select
v-model="selectedAnimation"
:options="animations"
optionLabel="name"
optionValue="index"
@change="animationChange"
class="w-32"
/>
</div>
</template>
@@ -58,46 +34,21 @@ import Button from 'primevue/button'
import Select from 'primevue/select'
import { ref, watch } from 'vue'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
const props = defineProps<{
animations: Array<{ name: string; index: number }>
playing: boolean
backgroundColor: string
showGrid: boolean
showPreview: boolean
lightIntensity: number
showLightIntensityButton: boolean
fov: number
showFOVButton: boolean
showPreviewButton: boolean
}>()
const emit = defineEmits<{
(e: 'toggleCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'togglePreview', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'togglePlay', value: boolean): void
(e: 'speedChange', value: number): void
(e: 'animationChange', value: number): void
(e: 'updateLightIntensity', value: number): void
(e: 'updateFOV', value: number): void
}>()
const animations = ref(props.animations)
const playing = ref(props.playing)
const selectedSpeed = ref(1)
const selectedAnimation = ref(0)
const backgroundColor = ref(props.backgroundColor)
const showGrid = ref(props.showGrid)
const showPreview = ref(props.showPreview)
const lightIntensity = ref(props.lightIntensity)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const fov = ref(props.fov)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const load3dControlsRef = ref(null)
const speedOptions = [
{ name: '0.1x', value: 0.1 },
@@ -107,42 +58,16 @@ const speedOptions = [
{ name: '2x', value: 2 }
]
watch(backgroundColor, (newValue) => {
load3dControlsRef.value.backgroundColor = newValue
})
watch(showLightIntensityButton, (newValue) => {
load3dControlsRef.value.showLightIntensityButton = newValue
})
watch(showFOVButton, (newValue) => {
load3dControlsRef.value.showFOVButton = newValue
})
watch(showPreviewButton, (newValue) => {
load3dControlsRef.value.showPreviewButton = newValue
})
const onToggleCamera = () => {
emit('toggleCamera')
}
const onToggleGrid = (value: boolean) => emit('toggleGrid', value)
const onTogglePreview = (value: boolean) => {
emit('togglePreview', value)
}
const onUpdateBackgroundColor = (color: string) =>
emit('updateBackgroundColor', color)
const onUpdateLightIntensity = (lightIntensity: number) => {
emit('updateLightIntensity', lightIntensity)
}
const onUpdateFOV = (fov: number) => {
emit('updateFOV', fov)
}
watch(
() => props.animations,
(newVal) => {
animations.value = newVal
}
)
const togglePlay = () => {
playing.value = !playing.value
emit('togglePlay', playing.value)
}
@@ -153,16 +78,4 @@ const speedChange = () => {
const animationChange = () => {
emit('animationChange', selectedAnimation.value)
}
defineExpose({
animations,
selectedAnimation,
playing,
backgroundColor,
showGrid,
lightIntensity,
showLightIntensityButton,
fov,
showFOVButton
})
</script>

View File

@@ -0,0 +1,164 @@
<template>
<Load3DScene
:node="node"
:type="type"
:backgroundColor="backgroundColor"
:showGrid="showGrid"
:lightIntensity="lightIntensity"
:fov="fov"
:cameraType="cameraType"
:showPreview="showPreview"
:extraListeners="animationListeners"
:backgroundImage="backgroundImage"
@materialModeChange="listenMaterialModeChange"
@backgroundColorChange="listenBackgroundColorChange"
@lightIntensityChange="listenLightIntensityChange"
@fovChange="listenFOVChange"
@cameraTypeChange="listenCameraTypeChange"
@showGridChange="listenShowGridChange"
@showPreviewChange="listenShowPreviewChange"
ref="load3DSceneRef"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
const props = defineProps<{
node: any
type: 'Load3DAnimation' | 'Preview3DAnimation'
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: 'perspective' | 'orthographic'
showPreview: boolean
materialMode: 'original' | 'normal' | 'wireframe' | 'depth'
showFOVButton: boolean
showLightIntensityButton: boolean
playing: boolean
selectedSpeed: number
selectedAnimation: number
backgroundImage: string
}>()
const node = ref(props.node)
const backgroundColor = ref(props.backgroundColor)
const showPreview = ref(props.showPreview)
const fov = ref(props.fov)
const lightIntensity = ref(props.lightIntensity)
const cameraType = ref(props.cameraType)
const showGrid = ref(props.showGrid)
const materialMode = ref(props.materialMode)
const showFOVButton = ref(props.showFOVButton)
const showLightIntensityButton = ref(props.showLightIntensityButton)
const load3DSceneRef = ref(null)
watch(
() => props.cameraType,
(newValue) => {
cameraType.value = newValue
}
)
watch(
() => props.showGrid,
(newValue) => {
showGrid.value = newValue
}
)
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.playing,
(newValue) => {
load3DSceneRef.value.load3d.toggleAnimation(newValue)
}
)
watch(
() => props.selectedSpeed,
(newValue) => {
load3DSceneRef.value.load3d.setAnimationSpeed(newValue)
}
)
watch(
() => props.selectedAnimation,
(newValue) => {
load3DSceneRef.value.load3d.updateSelectedAnimation(newValue)
}
)
const emit = defineEmits<{
(e: 'animationListChange', animationList: string): void
}>()
const listenMaterialModeChange = (
mode: 'original' | 'normal' | 'wireframe' | 'depth'
) => {
materialMode.value = mode
showLightIntensityButton.value = mode === 'original'
}
const listenBackgroundColorChange = (value: string) => {
backgroundColor.value = value
}
const listenLightIntensityChange = (value: number) => {
lightIntensity.value = value
}
const listenFOVChange = (value: number) => {
fov.value = value
}
const listenCameraTypeChange = (value: 'perspective' | 'orthographic') => {
cameraType.value = value
showFOVButton.value = cameraType.value === 'perspective'
}
const listenShowGridChange = (value: boolean) => {
showGrid.value = value
}
const listenShowPreviewChange = (value: boolean) => {
showPreview.value = value
}
const animationListeners = {
animationListChange: (newValue: any) => {
emit('animationListChange', newValue)
}
}
</script>

View File

@@ -1,8 +1,10 @@
<template>
<div class="absolute top-2 left-2 flex flex-col gap-2 z-20">
<Button class="p-button-rounded p-button-text" @click="toggleCamera">
<div
class="absolute top-2 left-2 flex flex-col pointer-events-auto z-20 bg-gray-700 bg-opacity-30 rounded-lg"
>
<Button class="p-button-rounded p-button-text" @click="switchCamera">
<i
class="pi pi-camera text-white text-lg"
:class="['pi', getCameraIcon, 'text-white text-lg']"
v-tooltip.right="{ value: t('load3d.switchCamera'), showDelay: 300 }"
></i>
</Button>
@@ -11,28 +13,69 @@
class="p-button-rounded p-button-text"
:class="{ 'p-button-outlined': showGrid }"
@click="toggleGrid"
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
>
<i class="pi pi-table text-white text-lg"></i>
</Button>
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
<i
class="pi pi-palette text-white text-lg"
v-tooltip.right="{ value: t('load3d.backgroundColor'), showDelay: 300 }"
class="pi pi-table text-white text-lg"
v-tooltip.right="{ value: t('load3d.showGrid'), showDelay: 300 }"
></i>
<input
type="color"
ref="colorPickerRef"
:value="backgroundColor"
@input="
updateBackgroundColor(($event.target as HTMLInputElement).value)
"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
<div class="relative" v-if="showLightIntensityButton">
<div v-if="!hasBackgroundImage">
<Button class="p-button-rounded p-button-text" @click="openColorPicker">
<i
class="pi pi-palette text-white text-lg"
v-tooltip.right="{
value: t('load3d.backgroundColor'),
showDelay: 300
}"
></i>
<input
type="color"
ref="colorPickerRef"
:value="backgroundColor"
@input="
updateBackgroundColor(($event.target as HTMLInputElement).value)
"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
</div>
<div v-if="!hasBackgroundImage">
<Button class="p-button-rounded p-button-text" @click="openImagePicker">
<i
class="pi pi-image text-white text-lg"
v-tooltip.right="{
value: t('load3d.uploadBackgroundImage'),
showDelay: 300
}"
></i>
<input
type="file"
ref="imagePickerRef"
accept="image/*"
@change="uploadBackgroundImage"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
</div>
<div v-if="hasBackgroundImage">
<Button
class="p-button-rounded p-button-text"
@click="removeBackgroundImage"
>
<i
class="pi pi-times text-white text-lg"
v-tooltip.right="{
value: t('load3d.removeBackgroundImage'),
showDelay: 300
}"
></i>
</Button>
</div>
<div class="relative show-light-intensity" v-if="showLightIntensityButton">
<Button
class="p-button-rounded p-button-text"
@click="toggleLightIntensity"
@@ -61,7 +104,7 @@
</div>
</div>
<div class="relative" v-if="showFOVButton">
<div class="relative show-fov" v-if="showFOVButton">
<Button class="p-button-rounded p-button-text" @click="toggleFOV">
<i
class="pi pi-expand text-white text-lg"
@@ -100,12 +143,15 @@
</template>
<script setup lang="ts">
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
import Slider from 'primevue/slider'
import { onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { t } from '@/i18n'
const vTooltip = Tooltip
const props = defineProps<{
backgroundColor: string
showGrid: boolean
@@ -115,15 +161,18 @@ const props = defineProps<{
fov: number
showFOVButton: boolean
showPreviewButton: boolean
cameraType: 'perspective' | 'orthographic'
hasBackgroundImage?: boolean
}>()
const emit = defineEmits<{
(e: 'toggleCamera'): void
(e: 'switchCamera'): void
(e: 'toggleGrid', value: boolean): void
(e: 'updateBackgroundColor', color: string): void
(e: 'updateLightIntensity', value: number): void
(e: 'updateFOV', value: number): void
(e: 'togglePreview', value: boolean): void
(e: 'updateBackgroundImage', file: File | null): void
}>()
const backgroundColor = ref(props.backgroundColor)
@@ -137,9 +186,11 @@ const fov = ref(props.fov)
const showFOV = ref(false)
const showFOVButton = ref(props.showFOVButton)
const showPreviewButton = ref(props.showPreviewButton)
const hasBackgroundImage = ref(props.hasBackgroundImage)
const imagePickerRef = ref<HTMLInputElement | null>(null)
const toggleCamera = () => {
emit('toggleCamera')
const switchCamera = () => {
emit('switchCamera')
}
const toggleGrid = () => {
@@ -179,12 +230,91 @@ const updateFOV = () => {
const closeSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.relative')) {
showLightIntensity.value = false
if (!target.closest('.show-fov')) {
showFOV.value = false
}
if (!target.closest('.show-light-intensity')) {
showLightIntensity.value = false
}
}
const openImagePicker = () => {
imagePickerRef.value?.click()
}
const uploadBackgroundImage = (event: Event) => {
const input = event.target as HTMLInputElement
hasBackgroundImage.value = true
if (input.files && input.files[0]) {
emit('updateBackgroundImage', input.files[0])
}
}
const removeBackgroundImage = () => {
hasBackgroundImage.value = false
emit('updateBackgroundImage', null)
}
watch(
() => props.backgroundColor,
(newValue) => {
backgroundColor.value = newValue
}
)
watch(
() => props.fov,
(newValue) => {
fov.value = newValue
}
)
watch(
() => props.lightIntensity,
(newValue) => {
lightIntensity.value = newValue
}
)
watch(
() => props.showFOVButton,
(newValue) => {
showFOVButton.value = newValue
}
)
watch(
() => props.showLightIntensityButton,
(newValue) => {
showLightIntensityButton.value = newValue
}
)
watch(
() => props.showPreviewButton,
(newValue) => {
showPreviewButton.value = newValue
}
)
watch(
() => props.showPreview,
(newValue) => {
showPreview.value = newValue
}
)
watch(
() => props.hasBackgroundImage,
(newValue) => {
hasBackgroundImage.value = newValue
}
)
onMounted(() => {
document.addEventListener('click', closeSlider)
})
@@ -193,13 +323,7 @@ onUnmounted(() => {
document.removeEventListener('click', closeSlider)
})
defineExpose({
backgroundColor,
showGrid,
lightIntensity,
showLightIntensityButton,
fov,
showFOVButton,
showPreviewButton
const getCameraIcon = computed(() => {
return props.cameraType === 'perspective' ? 'pi-camera' : 'pi-th-large'
})
</script>

View File

@@ -0,0 +1,107 @@
<template>
<div ref="container" class="w-full h-full relative">
<LoadingOverlay ref="loadingOverlayRef" />
</div>
</template>
<script setup lang="ts">
import { LGraphNode } from '@comfyorg/litegraph'
import { onMounted, onUnmounted, ref, toRaw, watchEffect } from 'vue'
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import { useLoad3dService } from '@/services/load3dService'
const props = defineProps<{
node: any
type: 'Load3D' | 'Load3DAnimation' | 'Preview3D' | 'Preview3DAnimation'
backgroundColor: string
showGrid: boolean
lightIntensity: number
fov: number
cameraType: 'perspective' | 'orthographic'
showPreview: boolean
backgroundImage: string
extraListeners?: Record<string, (value: any) => void>
}>()
const container = ref<HTMLElement | null>(null)
const node = ref(props.node)
const load3d = ref<Load3d | Load3dAnimation | null>(null)
const loadingOverlayRef = ref<InstanceType<typeof LoadingOverlay> | null>(null)
const eventConfig = {
materialModeChange: (value: string) => emit('materialModeChange', value),
backgroundColorChange: (value: string) =>
emit('backgroundColorChange', value),
lightIntensityChange: (value: number) => emit('lightIntensityChange', value),
fovChange: (value: number) => emit('fovChange', value),
cameraTypeChange: (value: string) => emit('cameraTypeChange', value),
showGridChange: (value: boolean) => emit('showGridChange', value),
showPreviewChange: (value: boolean) => emit('showPreviewChange', value),
backgroundImageChange: (value: string) =>
emit('backgroundImageChange', value),
modelLoadingStart: () => loadingOverlayRef.value?.startLoading(),
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading()
} as const
watchEffect(() => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value)
rawLoad3d.setBackgroundColor(props.backgroundColor)
rawLoad3d.toggleGrid(props.showGrid)
rawLoad3d.setLightIntensity(props.lightIntensity)
rawLoad3d.setFOV(props.fov)
rawLoad3d.toggleCamera(props.cameraType)
rawLoad3d.togglePreview(props.showPreview)
rawLoad3d.setBackgroundImage(props.backgroundImage)
}
})
const emit = defineEmits<{
(e: 'materialModeChange', materialMode: string): void
(e: 'backgroundColorChange', color: string): void
(e: 'lightIntensityChange', lightIntensity: number): void
(e: 'fovChange', fov: number): void
(e: 'cameraTypeChange', cameraType: string): void
(e: 'showGridChange', showGrid: boolean): void
(e: 'showPreviewChange', showPreview: boolean): void
(e: 'backgroundImageChange', backgroundImage: string): void
}>()
const handleEvents = (action: 'add' | 'remove') => {
if (!load3d.value) return
Object.entries(eventConfig).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
if (props.extraListeners) {
Object.entries(props.extraListeners).forEach(([event, handler]) => {
const method = `${action}EventListener` as const
load3d.value?.[method](event, handler)
})
}
}
onMounted(() => {
load3d.value = useLoad3dService().registerLoad3d(
node.value as LGraphNode,
container.value,
props.type
)
handleEvents('add')
})
onUnmounted(() => {
handleEvents('remove')
useLoad3dService().removeLoad3d(node.value as LGraphNode)
})
defineExpose({
load3d
})
</script>

View File

@@ -0,0 +1,56 @@
<template>
<Transition name="fade">
<div
v-if="modelLoading"
class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<div class="flex flex-col items-center">
<div class="spinner"></div>
<div class="text-white mt-4 text-lg">
{{ t('load3d.loadingModel') }}
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { t } from '@/i18n'
const modelLoading = ref(false)
const startLoading = () => {
modelLoading.value = true
}
const endLoading = () => {
modelLoading.value = false
}
defineExpose({
startLoading,
endLoading
})
</script>
<style scoped>
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -76,16 +76,12 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
>
{{ nodeDef.description }}
</div>
<div>
<Select v-model="selectedAnimation" />
</div>
</div>
</template>
<script setup lang="ts">
import _ from 'lodash'
import Select from 'primevue/select'
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useWidgetStore } from '@/stores/widgetStore'
@@ -98,8 +94,6 @@ const props = defineProps({
}
})
const selectedAnimation = ref(0)
const colorPaletteStore = useColorPaletteStore()
const litegraphColors = computed(
() => colorPaletteStore.completedActivePalette.colors.litegraph_base
@@ -246,7 +240,7 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
}
._sb_col {
border: 0px solid #000;
border: 0 solid #000;
display: flex;
align-items: flex-end;
flex-direction: row-reverse;

View File

@@ -254,7 +254,7 @@ useEventListener(document, 'litegraph:canvas', canvasEventHandler)
}
@media all and (max-width: 768px) {
.invisible-dialog-root {
margin-left: 0px;
margin-left: 0;
}
}

View File

@@ -92,7 +92,7 @@ const props = defineProps<{
color: var(--p-primary-contrast-color);
font-weight: bold;
border-radius: 0.25rem;
padding: 0rem 0.125rem;
padding: 0 0.125rem;
margin: -0.125rem 0.125rem;
}
</style>

View File

@@ -17,7 +17,7 @@
v-if="['in_progress', 'paused', 'completed'].includes(download.status)"
>
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
-->
<ProgressBar
class="flex-1"

View File

@@ -133,7 +133,7 @@ onUnmounted(() => {
left: 0;
height: 1.5rem;
vertical-align: top;
width: 0px;
width: 0;
}
.model-lib-model-icon {
background-size: cover;

View File

@@ -16,6 +16,12 @@
onMousedown: onMaskMouseDown,
onMouseup: onMaskMouseUp,
'data-mask': true
},
prevButton: {
style: 'position: fixed !important'
},
nextButton: {
style: 'position: fixed !important'
}
}"
>

View File

@@ -55,6 +55,7 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { TemplateInfo } from '@/types/workflowTemplateTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { sourceModule, categoryTitle, loading, template } = defineProps<{
sourceModule: string
@@ -75,8 +76,7 @@ const thumbnailSrc = computed(() =>
const title = computed(() => {
return sourceModule === 'default'
? t(
`templateWorkflows.template.${categoryTitle}.${template.name}`,
template.name
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`
)
: template.name ?? `${sourceModule} Template`
})

View File

@@ -21,7 +21,7 @@
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
:aria-label="$t('menu.hideMenu')"
@click="workspaceState.focusMode = true"
@contextmenu="showNativeMenu"
@contextmenu="showNativeSystemMenu"
/>
<div
v-show="menuSetting !== 'Bottom'"
@@ -52,7 +52,7 @@ import {
electronAPI,
isElectron,
isNativeWindow,
showNativeMenu
showNativeSystemMenu
} from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()

View File

@@ -0,0 +1,48 @@
import type { Size, Vector2 } from '@comfyorg/litegraph'
import { CSSProperties, ref } from 'vue'
import { app } from '@/scripts/app'
import { useCanvasStore } from '@/stores/graphStore'
export interface PositionConfig {
/* The position of the element on litegraph canvas */
pos: Vector2
/* The size of the element on litegraph canvas */
size: Size
/* The scale factor of the canvas */
scale?: number
}
export function useAbsolutePosition() {
const canvasStore = useCanvasStore()
const style = ref<CSSProperties>({
position: 'fixed',
left: '0px',
top: '0px',
width: '0px',
height: '0px'
})
const updatePosition = (
config: PositionConfig,
extraStyle?: CSSProperties
) => {
const { pos, size, scale = canvasStore.canvas?.ds?.scale ?? 1 } = config
const [left, top] = app.canvasPosToClientPos(pos)
const [width, height] = size
style.value = {
...style.value,
left: `${left}px`,
top: `${top}px`,
width: `${width * scale}px`,
height: `${height * scale}px`,
...extraStyle
}
}
return {
style,
updatePosition
}
}

View File

@@ -0,0 +1,16 @@
/**
* Chain multiple callbacks together.
*
* @param originalCallback - The original callback to chain.
* @param callbacks - The callbacks to chain.
* @returns A new callback that chains the original callback with the callbacks.
*/
export const useChainCallback = <T extends (...args: any[]) => void>(
originalCallback: T | undefined,
...callbacks: ((...args: Parameters<T>) => void)[]
) => {
return (...args: Parameters<T>) => {
originalCallback?.(...args)
callbacks.forEach((callback) => callback(...args))
}
}

View File

@@ -366,6 +366,7 @@ export function useCoreCommands(): ComfyCommand[] {
versionAdded: '1.3.11',
function: () => {
toggleSelectedNodesMode(LGraphEventMode.NEVER)
app.canvas.setDirty(true, true)
}
},
{
@@ -375,6 +376,7 @@ export function useCoreCommands(): ComfyCommand[] {
versionAdded: '1.3.11',
function: () => {
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
app.canvas.setDirty(true, true)
}
},
{
@@ -386,6 +388,7 @@ export function useCoreCommands(): ComfyCommand[] {
getSelectedNodes().forEach((node) => {
node.pin(!node.pinned)
})
app.canvas.setDirty(true, true)
}
},
{
@@ -399,6 +402,7 @@ export function useCoreCommands(): ComfyCommand[] {
item.pin(!item.pinned)
}
}
app.canvas.setDirty(true, true)
}
},
{
@@ -410,6 +414,7 @@ export function useCoreCommands(): ComfyCommand[] {
getSelectedNodes().forEach((node) => {
node.collapse()
})
app.canvas.setDirty(true, true)
}
},
{
@@ -566,6 +571,16 @@ export function useCoreCommands(): ComfyCommand[] {
function: () => {
window.open('https://forum.comfy.org/', '_blank')
}
},
{
id: 'Comfy.Canvas.DeleteSelectedItems',
icon: 'pi pi-trash',
label: 'Delete Selected Items',
versionAdded: '1.10.5',
function: () => {
app.canvas.deleteSelected()
app.canvas.setDirty(true, true)
}
}
]
}

View File

@@ -0,0 +1,49 @@
import type { LGraphNode } from '@comfyorg/litegraph'
type DragHandler = (e: DragEvent) => boolean
type DropHandler<T> = (files: File[]) => Promise<T[]>
interface DragAndDropOptions<T> {
onDragOver?: DragHandler
onDrop: DropHandler<T>
fileFilter?: (file: File) => boolean
}
/**
* Adds drag and drop file handling to a node
*/
export const useNodeDragAndDrop = <T>(
node: LGraphNode,
options: DragAndDropOptions<T>
) => {
const { onDragOver, onDrop, fileFilter = () => true } = options
const hasFiles = (items: DataTransferItemList) =>
!!Array.from(items).find((f) => f.kind === 'file')
const filterFiles = (files: FileList) => Array.from(files).filter(fileFilter)
const hasValidFiles = (files: FileList) => filterFiles(files).length > 0
const isDraggingFiles = (e: DragEvent | undefined) => {
if (!e?.dataTransfer?.items) return false
return onDragOver?.(e) ?? hasFiles(e.dataTransfer.items)
}
const isDraggingValidFiles = (e: DragEvent | undefined) => {
if (!e?.dataTransfer?.files) return false
return hasValidFiles(e.dataTransfer.files)
}
node.onDragOver = isDraggingFiles
node.onDragDrop = function (e: DragEvent) {
if (!isDraggingValidFiles(e)) return false
const files = filterFiles(e.dataTransfer!.files)
onDrop(files).then((results) => {
if (!results?.length) return
})
return true
}
}

Some files were not shown because too many files have changed in this diff Show More