Compare commits
74 Commits
dont-merge
...
v1.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dcf7eca74 | ||
|
|
f94831d054 | ||
|
|
c502b86c31 | ||
|
|
86b65d481a | ||
|
|
064e982f01 | ||
|
|
f43eac7c71 | ||
|
|
d7c9a43aba | ||
|
|
dee3f5824a | ||
|
|
9b88909caa | ||
|
|
3fa512957c | ||
|
|
b012f243b3 | ||
|
|
6cb33d9431 | ||
|
|
85d04f6814 | ||
|
|
40da43861e | ||
|
|
40deb19634 | ||
|
|
abfc7481d3 | ||
|
|
ec94811637 | ||
|
|
cea5a4a3dd | ||
|
|
0937c1f2cd | ||
|
|
8074d797b0 | ||
|
|
c3c6ec627b | ||
|
|
02d77002c9 | ||
|
|
3e31045fbb | ||
|
|
78146c86f4 | ||
|
|
365fd1e047 | ||
|
|
fbb6c2f825 | ||
|
|
9c7d86ee49 | ||
|
|
bc43cf0290 | ||
|
|
45c59f9e84 | ||
|
|
014a65411e | ||
|
|
6c6d86a30b | ||
|
|
08a6867c00 | ||
|
|
dbbe67dfcd | ||
|
|
40fa1d37bc | ||
|
|
0d6bc669f5 | ||
|
|
e4444d4074 | ||
|
|
cbf5dff633 | ||
|
|
9de8450deb | ||
|
|
3b0e3d635b | ||
|
|
d1a682bc01 | ||
|
|
01ffc9e4eb | ||
|
|
54e42178f7 | ||
|
|
25e5ab3a36 | ||
|
|
28dd6a2702 | ||
|
|
3b3df250cd | ||
|
|
6441a86619 | ||
|
|
79db202925 | ||
|
|
f7556e0015 | ||
|
|
141e64354c | ||
|
|
79452ce267 | ||
|
|
4d8a5eacba | ||
|
|
8f5a9a50aa | ||
|
|
7bc48c5074 | ||
|
|
e04ea07774 | ||
|
|
75af956279 | ||
|
|
434a2307a2 | ||
|
|
336b3caf9a | ||
|
|
c757fbaeb4 | ||
|
|
fd27b3d580 | ||
|
|
0658698a13 | ||
|
|
b2375a150c | ||
|
|
9ebb5b2a0c | ||
|
|
d6a5deccd8 | ||
|
|
3f4d11c63a | ||
|
|
44498739fc | ||
|
|
764ec9f7d0 | ||
|
|
e3234aa0aa | ||
|
|
df11c99393 | ||
|
|
317ea8b932 | ||
|
|
108884a304 | ||
|
|
9f1992ca59 | ||
|
|
39f245fd97 | ||
|
|
2d2fa5bfe9 | ||
|
|
bfb1b80cd7 |
18
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -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`.
|
||||
|
||||

|
||||
|
||||
</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
|
||||
|
||||
11
.github/workflows/eslint.yaml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/format.yaml
vendored
@@ -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
|
||||
|
||||
23
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/i18n.yaml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Update Locales
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main, master, dev* ]
|
||||
|
||||
25
.github/workflows/release.yaml
vendored
@@ -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 }}
|
||||
|
||||
1
.github/workflows/test-browser-exp.yaml
vendored
@@ -1,5 +1,4 @@
|
||||
# Setting test expectation screenshots for Playwright
|
||||
|
||||
name: Update Playwright Expectations
|
||||
|
||||
on:
|
||||
|
||||
9
.github/workflows/test-ui.yaml
vendored
@@ -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:
|
||||
|
||||
14
.github/workflows/vitest.yaml
vendored
@@ -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
|
||||
|
||||
29
README.md
@@ -468,6 +468,35 @@ We will support custom icons later.
|
||||

|
||||
</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:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Development
|
||||
|
||||
### Tech Stack
|
||||
|
||||
@@ -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
|
||||
|
||||
63
browser_tests/assets/missing_nodes_converted_widget.json
Normal 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
|
||||
}
|
||||
@@ -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/)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 135 KiB |
@@ -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 }
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
@@ -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(
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
187
browser_tests/selectionToolbox.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 59 KiB |
29
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
88
src/components/common/ColorCustomizationSelector.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!--
|
||||
A refresh button that disables and shows a progress spinner whilst active.
|
||||
|
||||
|
||||
Usage:
|
||||
```vue
|
||||
<RefreshButton
|
||||
|
||||
@@ -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: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -280,7 +280,7 @@ watch(activeCategory, (_, oldValue) => {
|
||||
|
||||
<style>
|
||||
.settings-tab-panels {
|
||||
padding-top: 0px !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
103
src/components/graph/SelectionOverlay.vue
Normal 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>
|
||||
98
src/components/graph/SelectionToolbox.vue
Normal 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>
|
||||
@@ -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` }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
140
src/components/graph/selectionToolbox/ColorPickerButton.vue
Normal 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>
|
||||
168
src/components/load3d/Load3D.vue
Normal 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>
|
||||
208
src/components/load3d/Load3DAnimation.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
164
src/components/load3d/Load3DAnimationScene.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
107
src/components/load3d/Load3DScene.vue
Normal 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>
|
||||
56
src/components/load3d/LoadingOverlay.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -254,7 +254,7 @@ useEventListener(document, 'litegraph:canvas', canvasEventHandler)
|
||||
}
|
||||
@media all and (max-width: 768px) {
|
||||
.invisible-dialog-root {
|
||||
margin-left: 0px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -133,7 +133,7 @@ onUnmounted(() => {
|
||||
left: 0;
|
||||
height: 1.5rem;
|
||||
vertical-align: top;
|
||||
width: 0px;
|
||||
width: 0;
|
||||
}
|
||||
.model-lib-model-icon {
|
||||
background-size: cover;
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
onMousedown: onMaskMouseDown,
|
||||
onMouseup: onMaskMouseUp,
|
||||
'data-mask': true
|
||||
},
|
||||
prevButton: {
|
||||
style: 'position: fixed !important'
|
||||
},
|
||||
nextButton: {
|
||||
style: 'position: fixed !important'
|
||||
}
|
||||
}"
|
||||
>
|
||||
|
||||
@@ -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`
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
48
src/composables/element/useAbsolutePosition.ts
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/composables/functional/useChainCallback.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
49
src/composables/useNodeDragAndDrop.ts
Normal 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
|
||||
}
|
||||
}
|
||||