Compare commits
90 Commits
task-runne
...
v1.9.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbccd20ff | ||
|
|
fa0682d66e | ||
|
|
f7b613c6cb | ||
|
|
292af3fe3f | ||
|
|
2c12df12ab | ||
|
|
d0e99beaa7 | ||
|
|
6b64b74f6c | ||
|
|
0af4768dd2 | ||
|
|
5f850ddaa4 | ||
|
|
9fb3235df4 | ||
|
|
628facaa75 | ||
|
|
eb5a4b65ab | ||
|
|
98c197e8b1 | ||
|
|
90914a40ba | ||
|
|
821816955f | ||
|
|
b4121008cd | ||
|
|
3730c2b36f | ||
|
|
77be5ac514 | ||
|
|
83759b9a4a | ||
|
|
b8f187713e | ||
|
|
b8088ad782 | ||
|
|
a37671b154 | ||
|
|
57bc7ad312 | ||
|
|
5f59fbdead | ||
|
|
4eed9c7e53 | ||
|
|
ee6197785a | ||
|
|
f5d6ad07e8 | ||
|
|
45207dabbc | ||
|
|
4699360147 | ||
|
|
8ef3b87e59 | ||
|
|
d3a6baf8cd | ||
|
|
b4d679d31f | ||
|
|
7afc1baf7d | ||
|
|
7bdad335ca | ||
|
|
94065b6c21 | ||
|
|
a205a5cca5 | ||
|
|
788d6cf514 | ||
|
|
766710cf37 | ||
|
|
e019277ba0 | ||
|
|
97e5c9c6d2 | ||
|
|
52e42b5339 | ||
|
|
c42cdf5cd9 | ||
|
|
c07ec659a7 | ||
|
|
cbcbeab9d9 | ||
|
|
bf9d2affb4 | ||
|
|
2c8c8718e9 | ||
|
|
475e38ddb4 | ||
|
|
430f051c64 | ||
|
|
29b5f606b0 | ||
|
|
55d63a8aef | ||
|
|
99009a18f7 | ||
|
|
e3ab0e4d68 | ||
|
|
0e1ae41c0c | ||
|
|
f12d4a2d6f | ||
|
|
7bd8527bca | ||
|
|
cb356d50b8 | ||
|
|
d2e9943e79 | ||
|
|
27e4bd2592 | ||
|
|
82e0c3a8b6 | ||
|
|
2852720b2c | ||
|
|
38b8a68e50 | ||
|
|
44321e4692 | ||
|
|
e992bd6571 | ||
|
|
e971ba31e0 | ||
|
|
28b163cdd5 | ||
|
|
652125de1f | ||
|
|
c5d153cf16 | ||
|
|
9459f599b6 | ||
|
|
326839db88 | ||
|
|
30fdc70218 | ||
|
|
9c42c31968 | ||
|
|
44aa1bf8c3 | ||
|
|
caad27e28d | ||
|
|
e8136ff0ae | ||
|
|
157475cb2e | ||
|
|
93dc50a95a | ||
|
|
3f787e2dbf | ||
|
|
b54e270b10 | ||
|
|
0ab1d974c0 | ||
|
|
95ff01a67b | ||
|
|
6f05ce6cc2 | ||
|
|
b8bef57522 | ||
|
|
46500bf3dd | ||
|
|
1bcc00cd33 | ||
|
|
08d2322817 | ||
|
|
8cfc1c4682 | ||
|
|
c7bce87b8d | ||
|
|
8f6b594a9f | ||
|
|
1c9b300396 | ||
|
|
cd5283c4b7 |
3
.github/workflows/release.yaml
vendored
@@ -40,9 +40,10 @@ jobs:
|
||||
files: |
|
||||
dist.zip
|
||||
tag_name: v${{ steps.current_version.outputs.version }}
|
||||
draft: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: "true"
|
||||
generate_release_notes: true
|
||||
publish_types:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
|
||||
1
.gitignore
vendored
@@ -18,6 +18,7 @@ dist-ssr
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.json
|
||||
!.vscode/settings.json.default
|
||||
!.vscode/launch.json.default
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
|
||||
16
.vscode/launch.json.default
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome on frontend dev",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMaps": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 19,
|
||||
"type": "workflow>two_VAE_decode",
|
||||
"pos": [
|
||||
1368.800048828125,
|
||||
768.7999877929688
|
||||
],
|
||||
"size": [
|
||||
418.1999816894531,
|
||||
86
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "VAEDecode IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {}
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"node_versions": {},
|
||||
"ue_links": [],
|
||||
"groupNodes": {
|
||||
"two_VAE_decode": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": -1,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1368.800048828125,
|
||||
768.7999877929688
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null,
|
||||
"localized_name": "samples"
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null,
|
||||
"localized_name": "vae"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null,
|
||||
"localized_name": "IMAGE"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1368.800048828125,
|
||||
873.7999877929688
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null,
|
||||
"localized_name": "samples"
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null,
|
||||
"localized_name": "vae"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null,
|
||||
"localized_name": "IMAGE"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"index": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"external": [],
|
||||
"config": {
|
||||
"1": {
|
||||
"input": {
|
||||
"samples": {
|
||||
"visible": false
|
||||
},
|
||||
"vae": {
|
||||
"visible": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
35
browser_tests/assets/widgets/boolean_widget.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "DevToolsNodeWithBooleanInput",
|
||||
"pos": [
|
||||
0,
|
||||
30
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {
|
||||
"collapsed": false
|
||||
},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithBooleanInput"
|
||||
},
|
||||
"widgets_values": [
|
||||
false
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
|
||||
import { Keybinding } from '../src/types/keyBindingTypes'
|
||||
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||
@@ -66,9 +66,71 @@ test.describe('Missing models warning', () => {
|
||||
}, comfyPage.url)
|
||||
})
|
||||
|
||||
test('Should display a warning when missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should not display a warning when no missing models are found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const modelFoldersRes = {
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
name: 'clip',
|
||||
folders: ['ComfyUI/models/clip']
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
'**/api/experiment/models',
|
||||
(route) => route.fulfill(modelFoldersRes),
|
||||
{ times: 1 }
|
||||
)
|
||||
|
||||
// Reload page to trigger indexing of model folders
|
||||
await comfyPage.setup()
|
||||
|
||||
const clipModelsRes = {
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
{
|
||||
name: 'fake_model.safetensors',
|
||||
pathIndex: 0
|
||||
}
|
||||
])
|
||||
}
|
||||
comfyPage.page.route(
|
||||
'**/api/experiment/models/clip',
|
||||
(route) => route.fulfill(clipModelsRes),
|
||||
{ times: 1 }
|
||||
)
|
||||
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('should show on tutorial workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.TutorialCompleted', false)
|
||||
await comfyPage.setup({ clearStorage: true })
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
expect(await comfyPage.getSetting('Comfy.TutorialCompleted')).toBe(true)
|
||||
})
|
||||
|
||||
// Flaky test after parallelization
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
|
||||
test.skip('Should display a warning when missing models are found', async ({
|
||||
test.skip('Should download missing model when clicking download button', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// The fake_model.safetensors is served by
|
||||
@@ -86,6 +148,49 @@ test.describe('Missing models warning', () => {
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
|
||||
})
|
||||
|
||||
test.describe('Do not show again checkbox', () => {
|
||||
let checkbox: Locator
|
||||
let closeButton: Locator
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
true
|
||||
)
|
||||
await comfyPage.loadWorkflow('missing_models')
|
||||
|
||||
checkbox = comfyPage.page.getByLabel("Don't show this again")
|
||||
closeButton = comfyPage.page.getByLabel('Close')
|
||||
})
|
||||
|
||||
test('Should disable warning dialog when checkbox is checked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await checkbox.click()
|
||||
const changeSettingPromise = comfyPage.page.waitForRequest(
|
||||
'**/api/settings/Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
await closeButton.click()
|
||||
await changeSettingPromise
|
||||
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
expect(settingValue).toBe(false)
|
||||
})
|
||||
|
||||
test('Should keep warning dialog enabled when checkbox is unchecked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await closeButton.click()
|
||||
|
||||
const settingValue = await comfyPage.getSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
expect(settingValue).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Settings', () => {
|
||||
@@ -148,7 +253,7 @@ test.describe('Settings', () => {
|
||||
|
||||
// Save keybinding
|
||||
const saveButton = comfyPage.page
|
||||
.getByLabel('Comfy.NewBlankWorkflow')
|
||||
.getByLabel('New Blank Workflow')
|
||||
.getByLabel('Save')
|
||||
await saveButton.click()
|
||||
|
||||
|
||||
@@ -904,7 +904,9 @@ export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -83,6 +83,12 @@ export class NodeWidgetReference {
|
||||
y: pos[1]
|
||||
}
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.node.comfyPage.canvas.click({
|
||||
position: await this.getPosition()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeReference {
|
||||
|
||||
@@ -134,6 +134,37 @@ test.describe('Group Node', () => {
|
||||
expect(await manage2.getSelectedNodeType()).toBe('g2')
|
||||
})
|
||||
|
||||
test('Preserves hidden input configuration when containing duplicate node types', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('group_node_identical_nodes_hidden_inputs')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const groupNodeId = 19
|
||||
const groupNodeName = 'two_VAE_decode'
|
||||
|
||||
const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
|
||||
const {
|
||||
extra: { groupNodes }
|
||||
} = window['app'].graph
|
||||
const { nodes } = groupNodes[nodeName]
|
||||
return nodes.reduce((acc: number, node) => {
|
||||
return acc + node.inputs.length
|
||||
}, 0)
|
||||
}, groupNodeName)
|
||||
|
||||
const visibleInputCount = await comfyPage.page.evaluate((id) => {
|
||||
const node = window['app'].graph.getNodeById(id)
|
||||
return node.inputs.length
|
||||
}, groupNodeId)
|
||||
|
||||
// Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each)
|
||||
expect(totalInputCount).toBe(4)
|
||||
|
||||
// Verify there are 2 visible inputs (2 have been hidden in config)
|
||||
expect(visibleInputCount).toBe(2)
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -102,6 +102,18 @@ test.describe('Node Right Click Menu', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert Widget to Input').click()
|
||||
await comfyPage.nextFrame()
|
||||
// The submenu has an identical entry as the base menu - use last
|
||||
await comfyPage.page.getByText('Convert width to input').last().click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'right-click-node-widget-converted.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can convert widget without submenu', async ({ comfyPage }) => {
|
||||
// Right-click the width widget
|
||||
await comfyPage.rightClickEmptyLatentNode()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
|
||||
await comfyPage.page.getByText('Convert width to input').click()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 96 KiB |
@@ -27,3 +27,16 @@ test.describe('Combo text widget', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Boolean widget', () => {
|
||||
test('Can toggle', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/boolean_widget')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
|
||||
const node = (await comfyPage.getFirstNodeRef())!
|
||||
const widget = await node.getWidget(0)
|
||||
await widget.click()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'boolean_widget_toggled.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 97 KiB |
12
global.d.ts
vendored
@@ -1,3 +1,15 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
* Used by the electron API. This is a WICG non-standard API, but is guaranteed to exist in Electron.
|
||||
* It is `undefined` in Firefox and older browsers.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/windowControlsOverlay
|
||||
*/
|
||||
windowControlsOverlay?: {
|
||||
/** When `true`, the window is using custom window style. */
|
||||
visible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
return [
|
||||
`prettier --write ${fileNames.join(' ')}`,
|
||||
`eslint --fix ${fileNames.join(' ')}`
|
||||
`eslint --fix ${fileNames.join(' ')}`,
|
||||
`prettier --write ${fileNames.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
401
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.3",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.11",
|
||||
"@comfyorg/litegraph": "^0.8.61",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.67",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"axios": "^1.7.4",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -87,7 +88,7 @@
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
@@ -1937,15 +1938,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.11.tgz",
|
||||
"integrity": "sha512-RGJeWwXjyv0Ojj7xkZKgcRxC1nFv1nh7qEWpNBiofxVgFiap9Ei79b/KJYxNE0no4BoYqRMaRg+sFtCE6yEukA==",
|
||||
"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==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.61",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.61.tgz",
|
||||
"integrity": "sha512-7DroJ0PLgI9TFvQR//6rf0NRXRvV60hapxVX5lmKzNn4Mn2Ni/JsB2ypNLKeSU5sacNyu8QT3W5Jdpafl7lcnA==",
|
||||
"version": "0.8.67",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.67.tgz",
|
||||
"integrity": "sha512-yGytBGJHpEpesZ+2eusepbHlUlztYBu25h4eSIlMildceMFI/oEVTDj3gdmVgaReEs3uAZVnfSBqIPwqthHGJA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -5667,25 +5668,62 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
|
||||
"integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
|
||||
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "2.0.5",
|
||||
"@vitest/utils": "2.0.5",
|
||||
"chai": "^5.1.1",
|
||||
"@vitest/spy": "2.1.9",
|
||||
"@vitest/utils": "2.1.9",
|
||||
"chai": "^5.1.2",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
|
||||
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "2.1.9",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
|
||||
"integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
|
||||
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5696,13 +5734,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
|
||||
"integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
|
||||
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "2.0.5",
|
||||
"@vitest/utils": "2.1.9",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -5710,14 +5748,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
|
||||
"integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
|
||||
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.0.5",
|
||||
"magic-string": "^0.30.10",
|
||||
"@vitest/pretty-format": "2.1.9",
|
||||
"magic-string": "^0.30.12",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -5725,44 +5763,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
|
||||
"integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
|
||||
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^3.0.0"
|
||||
"tinyspy": "^3.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
|
||||
"integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
|
||||
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.0.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"loupe": "^3.1.1",
|
||||
"@vitest/pretty-format": "2.1.9",
|
||||
"loupe": "^3.1.2",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "2.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz",
|
||||
@@ -6135,6 +6162,15 @@
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-serialize": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.13.0.tgz",
|
||||
"integrity": "sha512-kGs8o6LWAmN1l2NpMp01/YkpxbmO4UrfWybeGu79Khw5K9+Krp7XhXbBTOTc3GJRRhd6EmILjpR8k5+odY39YQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
@@ -7115,9 +7151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
|
||||
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8394,6 +8430,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
|
||||
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -8958,6 +9001,16 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
|
||||
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -9356,16 +9409,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-func-name": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
@@ -13354,14 +13397,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
|
||||
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
|
||||
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-func-name": "^2.0.1"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
@@ -13373,9 +13413,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.11",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
||||
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
@@ -16819,9 +16860,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
|
||||
"integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz",
|
||||
"integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -17249,10 +17290,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
|
||||
"integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
|
||||
"dev": true
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.0.1",
|
||||
@@ -17275,9 +17317,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
|
||||
"integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -18526,16 +18568,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
|
||||
"integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
|
||||
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.3.5",
|
||||
"debug": "^4.3.7",
|
||||
"es-module-lexer": "^1.5.4",
|
||||
"pathe": "^1.1.2",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -18648,30 +18690,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
|
||||
"integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
|
||||
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@vitest/expect": "2.0.5",
|
||||
"@vitest/pretty-format": "^2.0.5",
|
||||
"@vitest/runner": "2.0.5",
|
||||
"@vitest/snapshot": "2.0.5",
|
||||
"@vitest/spy": "2.0.5",
|
||||
"@vitest/utils": "2.0.5",
|
||||
"chai": "^5.1.1",
|
||||
"debug": "^4.3.5",
|
||||
"execa": "^8.0.1",
|
||||
"magic-string": "^0.30.10",
|
||||
"@vitest/expect": "2.1.9",
|
||||
"@vitest/mocker": "2.1.9",
|
||||
"@vitest/pretty-format": "^2.1.9",
|
||||
"@vitest/runner": "2.1.9",
|
||||
"@vitest/snapshot": "2.1.9",
|
||||
"@vitest/spy": "2.1.9",
|
||||
"@vitest/utils": "2.1.9",
|
||||
"chai": "^5.1.2",
|
||||
"debug": "^4.3.7",
|
||||
"expect-type": "^1.1.0",
|
||||
"magic-string": "^0.30.12",
|
||||
"pathe": "^1.1.2",
|
||||
"std-env": "^3.7.0",
|
||||
"tinybench": "^2.8.0",
|
||||
"tinypool": "^1.0.0",
|
||||
"std-env": "^3.8.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.1",
|
||||
"tinypool": "^1.0.1",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "2.0.5",
|
||||
"vite-node": "2.1.9",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -18686,8 +18729,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "2.0.5",
|
||||
"@vitest/ui": "2.0.5",
|
||||
"@vitest/browser": "2.1.9",
|
||||
"@vitest/ui": "2.1.9",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -18712,150 +18755,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.8.2",
|
||||
"version": "1.9.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -76,15 +76,15 @@
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest": "^2.1.9",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.11",
|
||||
"@comfyorg/litegraph": "^0.8.61",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.16",
|
||||
"@comfyorg/litegraph": "^0.8.67",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -97,6 +97,7 @@
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"axios": "^1.7.4",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -349,8 +349,8 @@
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [{
|
||||
"name": "v1-5-pruned-emaonly.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
|
||||
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -440,8 +440,8 @@
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
"models": [{
|
||||
"name": "v1-5-pruned-emaonly.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly.safetensors?download=true",
|
||||
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -765,6 +765,30 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
/* Load3d styles */
|
||||
.comfy-load-3d,
|
||||
.comfy-load-3d-animation,
|
||||
.comfy-preview-3d,
|
||||
.comfy-preview-3d-animation{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfy-load-3d canvas,
|
||||
.comfy-load-3d-animation canvas,
|
||||
.comfy-preview-3d canvas,
|
||||
.comfy-preview-3d-animation canvas{
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* End of Load3d styles */
|
||||
|
||||
/* [Desktop] Electron window specific styles */
|
||||
.app-drag {
|
||||
app-region: drag;
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from 'vue'
|
||||
import { Ref, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
|
||||
const emit = defineEmits<{
|
||||
created: [ReturnType<typeof useTerminal>, Ref<HTMLElement>]
|
||||
unmounted: []
|
||||
}>()
|
||||
const terminalEl = ref<HTMLElement>()
|
||||
const rootEl = ref<HTMLElement>()
|
||||
emit('created', useTerminal(terminalEl), rootEl)
|
||||
|
||||
onUnmounted(() => emit('unmounted'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
v-else
|
||||
class="pi pi-palette"
|
||||
:style="{ fontSize: '1.2rem' }"
|
||||
v-tooltip="$t('g.customColor')"
|
||||
v-tooltip="$t('color.custom')"
|
||||
></i>
|
||||
</template>
|
||||
</SelectButton>
|
||||
|
||||
@@ -35,6 +35,7 @@ import CustomFormValue from '@/components/common/CustomFormValue.vue'
|
||||
import FormColorPicker from '@/components/common/FormColorPicker.vue'
|
||||
import FormImageUpload from '@/components/common/FormImageUpload.vue'
|
||||
import InputSlider from '@/components/common/InputSlider.vue'
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { FormItem } from '@/types/settingTypes'
|
||||
|
||||
const formValue = defineModel<any>('formValue')
|
||||
@@ -91,6 +92,8 @@ function getFormComponent(item: FormItem): Component {
|
||||
return FormImageUpload
|
||||
case 'color':
|
||||
return FormColorPicker
|
||||
case 'url':
|
||||
return UrlInput
|
||||
default:
|
||||
return InputText
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
nodeContent: ({ context }) => ({
|
||||
class: 'group/tree-node',
|
||||
onClick: (e: MouseEvent) =>
|
||||
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
|
||||
onContextmenu: (e: MouseEvent) =>
|
||||
@@ -69,7 +70,7 @@ const renderedRoots = computed<RenderedTreeExplorerNode[]>(() => {
|
||||
})
|
||||
const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.getIcon) {
|
||||
const icon = node.getIcon(node)
|
||||
const icon = node.getIcon()
|
||||
if (icon) {
|
||||
return icon
|
||||
}
|
||||
@@ -94,7 +95,7 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
children,
|
||||
type: node.leaf ? 'node' : 'folder',
|
||||
totalLeaves,
|
||||
badgeText: node.getBadgeText ? node.getBadgeText(node) : null
|
||||
badgeText: node.getBadgeText ? node.getBadgeText() : null
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = async (
|
||||
@@ -105,7 +106,7 @@ const onNodeContentClick = async (
|
||||
selectionKeys.value = {}
|
||||
}
|
||||
if (node.handleClick) {
|
||||
await node.handleClick(node, e)
|
||||
await node.handleClick(e)
|
||||
}
|
||||
emit('nodeClick', node, e)
|
||||
}
|
||||
@@ -127,7 +128,7 @@ const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
renameEditingNode.value = node
|
||||
}
|
||||
const deleteCommand = async (node: RenderedTreeExplorerNode) => {
|
||||
await node.handleDelete?.(node)
|
||||
await node.handleDelete?.()
|
||||
emit('nodeDelete', node)
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() =>
|
||||
|
||||
@@ -27,13 +27,16 @@
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
</div>
|
||||
<div class="node-actions">
|
||||
<div
|
||||
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<slot name="actions" :node="props.node"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
|
||||
import Badge from 'primevue/badge'
|
||||
import { Ref, computed, inject, ref } from 'vue'
|
||||
|
||||
@@ -80,7 +83,7 @@ const isEditing = computed(
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
async (newName: string) => {
|
||||
await props.node.handleRename(props.node, newName)
|
||||
await props.node.handleRename(newName)
|
||||
},
|
||||
props.node.handleError,
|
||||
() => {
|
||||
@@ -102,7 +105,17 @@ if (props.node.draggable) {
|
||||
}
|
||||
},
|
||||
onDragStart: () => emit('dragStart', props.node),
|
||||
onDrop: () => emit('dragEnd', props.node)
|
||||
onDrop: () => emit('dragEnd', props.node),
|
||||
onGenerateDragPreview: props.node.renderDragPreview
|
||||
? ({ nativeSetDragImage }) => {
|
||||
setCustomNativeDragPreview({
|
||||
render: ({ container }) => {
|
||||
return props.node.renderDragPreview(container)
|
||||
},
|
||||
nativeSetDragImage
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,7 +124,7 @@ if (props.node.droppable) {
|
||||
onDrop: async (event) => {
|
||||
const dndData = event.source.data as TreeExplorerDragAndDropData
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
await props.node.handleDrop?.(props.node, dndData)
|
||||
await props.node.handleDrop?.(dndData)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node, dndData.data)
|
||||
}
|
||||
|
||||
116
src/components/common/UrlInput.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<IconField class="w-full">
|
||||
<InputText
|
||||
v-bind="$attrs"
|
||||
:model-value="internalValue"
|
||||
class="w-full"
|
||||
:invalid="validationState === ValidationState.INVALID"
|
||||
@update:model-value="handleInput"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
<InputIcon
|
||||
:class="{
|
||||
'pi pi-spin pi-spinner text-neutral-400':
|
||||
validationState === ValidationState.LOADING,
|
||||
'pi pi-check text-green-500 cursor-pointer':
|
||||
validationState === ValidationState.VALID,
|
||||
'pi pi-times text-red-500 cursor-pointer':
|
||||
validationState === ValidationState.INVALID
|
||||
}"
|
||||
@click="validateUrl(props.modelValue)"
|
||||
/>
|
||||
</IconField>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
import { checkUrlReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
validateUrlFn?: (url: string) => Promise<boolean>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'state-change': [state: ValidationState]
|
||||
}>()
|
||||
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
|
||||
// Add internal value state
|
||||
const internalValue = ref(props.modelValue)
|
||||
|
||||
// Watch for external modelValue changes
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newValue: string) => {
|
||||
internalValue.value = newValue
|
||||
await validateUrl(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
watch(validationState, (newState) => {
|
||||
emit('state-change', newState)
|
||||
})
|
||||
|
||||
// Validate on mount
|
||||
onMounted(async () => {
|
||||
await validateUrl(props.modelValue)
|
||||
})
|
||||
|
||||
const handleInput = (value: string) => {
|
||||
// Update internal value without emitting
|
||||
internalValue.value = value
|
||||
// Reset validation state when user types
|
||||
validationState.value = ValidationState.IDLE
|
||||
}
|
||||
|
||||
const handleBlur = async () => {
|
||||
// Emit the update only on blur
|
||||
emit('update:modelValue', internalValue.value)
|
||||
}
|
||||
|
||||
// Default validation implementation
|
||||
const defaultValidateUrl = async (url: string): Promise<boolean> => {
|
||||
if (!isValidUrl(url)) return false
|
||||
try {
|
||||
return await checkUrlReachable(url)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const validateUrl = async (value: string) => {
|
||||
if (validationState.value === ValidationState.LOADING) return
|
||||
|
||||
const url = value.trim()
|
||||
|
||||
// Reset state
|
||||
validationState.value = ValidationState.IDLE
|
||||
|
||||
// Skip validation if empty
|
||||
if (!url) return
|
||||
|
||||
validationState.value = ValidationState.LOADING
|
||||
try {
|
||||
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
|
||||
validationState.value = isValid
|
||||
? ValidationState.VALID
|
||||
: ValidationState.INVALID
|
||||
} catch {
|
||||
validationState.value = ValidationState.INVALID
|
||||
}
|
||||
}
|
||||
|
||||
// Add inheritAttrs option to prevent attrs from being applied to root element
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
</script>
|
||||
158
src/components/common/__tests__/UrlInput.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
|
||||
import UrlInput from '../UrlInput.vue'
|
||||
|
||||
describe('UrlInput', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: any, options = {}) => {
|
||||
return mount(UrlInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { IconField, InputIcon, InputText }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
it('passes through additional attributes to input element', () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
disabled: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').attributes('disabled')).toBe('')
|
||||
})
|
||||
|
||||
it('emits update:modelValue on blur', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('https://test.com')
|
||||
await input.trigger('blur')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
|
||||
'https://test.com'
|
||||
])
|
||||
})
|
||||
|
||||
it('renders spinner when validation is loading', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () =>
|
||||
new Promise(() => {
|
||||
// Never resolves, simulating perpetual loading state
|
||||
})
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-spinner').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders check icon when validation is valid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders cross icon when validation is invalid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(false)
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-times').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('validates on mount', async () => {
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('triggers validation when clicking the validation icon', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for initial validation
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Click the validation icon
|
||||
await wrapper.find('.pi-check').trigger('click')
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(validationCount).toBe(2) // Once on mount, once on click
|
||||
})
|
||||
|
||||
it('prevents multiple simultaneous validations', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
modelValue: '',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
return new Promise(() => {
|
||||
// Never resolves, simulating perpetual loading state
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Trigger multiple validations in quick succession
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
wrapper.find('.pi-spinner').trigger('click')
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(validationCount).toBe(1) // Only the initial validation should occur
|
||||
})
|
||||
})
|
||||
@@ -2,9 +2,15 @@
|
||||
<NoResultsPlaceholder
|
||||
class="pb-0"
|
||||
icon="pi pi-exclamation-circle"
|
||||
title="Missing Models"
|
||||
message="When loading the graph, the following models were not found"
|
||||
:title="t('missingModelsDialog.missingModels')"
|
||||
:message="t('missingModelsDialog.missingModelsMessage')"
|
||||
/>
|
||||
<div class="flex gap-1 mb-4">
|
||||
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
|
||||
<label for="doNotAskAgain">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<ListBox :options="missingModels" class="comfy-missing-models">
|
||||
<template #option="{ option }">
|
||||
<Suspense v-if="isElectron()">
|
||||
@@ -25,11 +31,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
@@ -58,6 +67,10 @@ const props = defineProps<{
|
||||
paths: Record<string, string[]>
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
const modelDownloads = ref<Record<string, ModelInfo>>({})
|
||||
const missingModels = computed(() => {
|
||||
return props.missingModels.map((model) => {
|
||||
@@ -107,6 +120,12 @@ const missingModels = computed(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (doNotAskAgain.value) {
|
||||
useSettingStore().set('Comfy.Workflow.ShowMissingModelsWarning', false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched"
|
||||
v-if="$field?.error && $field.touched && $field.value"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
@@ -105,6 +105,7 @@ import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import _ from 'lodash'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
@@ -218,7 +219,7 @@ const createCaptureContext = async (
|
||||
? formData.notifyOnResolution
|
||||
: false,
|
||||
isElectron: isElectron(),
|
||||
...props.tags
|
||||
..._.mapValues(props.tags, (tag) => _.trim(tag).replace(/[\n\r\t]/g, ' '))
|
||||
},
|
||||
extra: {
|
||||
details: formData.details,
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
class="min-w-96"
|
||||
v-model:visible="editDialogVisible"
|
||||
modal
|
||||
:header="currentEditingCommand?.id"
|
||||
:header="currentEditingCommand?.label"
|
||||
@hide="cancelEdit"
|
||||
>
|
||||
<div>
|
||||
@@ -151,6 +151,7 @@ const { t } = useI18n()
|
||||
interface ICommandData {
|
||||
id: string
|
||||
keybinding: KeybindingImpl | null
|
||||
label: string
|
||||
}
|
||||
|
||||
const commandsData = computed<ICommandData[]>(() => {
|
||||
@@ -213,6 +214,17 @@ function removeKeybinding(commandData: ICommandData) {
|
||||
}
|
||||
|
||||
function captureKeybinding(event: KeyboardEvent) {
|
||||
// Allow the use of keyboard shortcuts when adding keyboard shortcuts
|
||||
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
cancelEdit()
|
||||
return
|
||||
case 'Enter':
|
||||
saveKeybinding()
|
||||
return
|
||||
}
|
||||
}
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
newBindingKeyCombo.value = keyCombo
|
||||
}
|
||||
|
||||
@@ -29,18 +29,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CanvasPointer,
|
||||
ContextMenu,
|
||||
DragAndScale,
|
||||
LGraph,
|
||||
LGraphBadge,
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import { CanvasPointer, LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
@@ -53,37 +42,30 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { useCanvasDrop } from '@/hooks/canvasDropHooks'
|
||||
import { useGlobalLitegraph } from '@/hooks/litegraphHooks'
|
||||
import { useContextMenuTranslation } from '@/hooks/translationHooks'
|
||||
import { useWorkflowPersistence } from '@/hooks/workflowPersistenceHooks'
|
||||
import { i18n } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import {
|
||||
ModelNodeProvider,
|
||||
useModelToNodeStore
|
||||
} from '@/stores/modelToNodeStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const litegraphService = useLitegraphService()
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
@@ -95,29 +77,6 @@ const canvasMenuEnabled = computed(() =>
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
const openWorkflows = computed(() => workspaceStore?.workflow?.openWorkflows)
|
||||
const activeWorkflow = computed(() => workspaceStore?.workflow?.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
@@ -184,6 +143,17 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const lowQualityRenderingZoomThreshold = settingStore.get(
|
||||
'LiteGraph.Canvas.LowQualityRenderingZoomThreshold'
|
||||
)
|
||||
if (canvasStore.canvas) {
|
||||
canvasStore.canvas.low_quality_zoom_threshold =
|
||||
lowQualityRenderingZoomThreshold
|
||||
canvasStore.canvas.setDirty(/* fg */ true, /* bg */ true)
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const linkMarkerShape = settingStore.get('Comfy.Graph.LinkMarkers')
|
||||
const { canvas } = canvasStore
|
||||
@@ -269,98 +239,30 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const persistCurrentWorkflow = () => {
|
||||
const workflow = JSON.stringify(comfyApp.serializeGraph())
|
||||
localStorage.setItem('workflow', workflow)
|
||||
if (api.clientId) {
|
||||
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
|
||||
watchEffect(() => {
|
||||
LiteGraph.context_menu_scaling = settingStore.get(
|
||||
'LiteGraph.ContextMenu.Scaling'
|
||||
)
|
||||
})
|
||||
|
||||
const loadCustomNodesI18n = async () => {
|
||||
try {
|
||||
const i18nData = await api.getCustomNodesI18n()
|
||||
Object.entries(i18nData).forEach(([locale, message]) => {
|
||||
i18n.global.mergeLocaleMessage(locale, message)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom nodes i18n', error)
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
|
||||
// When the activeWorkflow changes, the graph has already been loaded.
|
||||
// Saving the current state of the graph to the localStorage.
|
||||
persistCurrentWorkflow()
|
||||
}
|
||||
})
|
||||
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
litegraphService.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = litegraphService.addNodeOnGraph(
|
||||
provider.nodeDef,
|
||||
{
|
||||
pos
|
||||
}
|
||||
)
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets.find(
|
||||
(widget) => widget.name === targetProvider.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
const workflowPersistence = useWorkflowPersistence()
|
||||
useCanvasDrop(canvasRef)
|
||||
|
||||
onMounted(async () => {
|
||||
// Backward compatible
|
||||
// Assign all properties of lg to window
|
||||
window['LiteGraph'] = LiteGraph
|
||||
window['LGraph'] = LGraph
|
||||
window['LLink'] = LLink
|
||||
window['LGraphNode'] = LGraphNode
|
||||
window['LGraphGroup'] = LGraphGroup
|
||||
window['DragAndScale'] = DragAndScale
|
||||
window['LGraphCanvas'] = LGraphCanvas
|
||||
window['ContextMenu'] = ContextMenu
|
||||
window['LGraphBadge'] = LGraphBadge
|
||||
useGlobalLitegraph()
|
||||
useContextMenuTranslation()
|
||||
|
||||
comfyApp.vueAppReady = true
|
||||
|
||||
@@ -368,6 +270,7 @@ onMounted(async () => {
|
||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init(comfyApp)
|
||||
await loadCustomNodesI18n()
|
||||
await settingStore.loadSettingValues()
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
@@ -387,17 +290,9 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable)
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
// Restore workflow and workflow tabs state from storage
|
||||
await workflowPersistence.restorePreviousWorkflow()
|
||||
workflowPersistence.restoreWorkflowTabsState()
|
||||
|
||||
// Start watching for locale change after the initial value is loaded.
|
||||
watch(
|
||||
|
||||
@@ -17,10 +17,12 @@ import { nextTick, ref } from 'vue'
|
||||
import { st } from '@/i18n'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingStore = useSettingStore()
|
||||
const tooltipRef = ref<HTMLDivElement>()
|
||||
const tooltipText = ref('')
|
||||
const left = ref<string>()
|
||||
@@ -110,7 +112,10 @@ const onMouseMove = (e: MouseEvent) => {
|
||||
clearTimeout(idleTimeout)
|
||||
|
||||
if ((e.target as Node).nodeName !== 'CANVAS') return
|
||||
idleTimeout = window.setTimeout(onIdle, 500)
|
||||
idleTimeout = window.setTimeout(
|
||||
onIdle,
|
||||
settingStore.get('LiteGraph.Node.TooltipDelay')
|
||||
)
|
||||
}
|
||||
|
||||
useEventListener(window, 'mousemove', onMouseMove)
|
||||
|
||||
114
src/components/install/MirrorsConfiguration.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<Panel
|
||||
:header="$t('install.settings.mirrorSettings')"
|
||||
toggleable
|
||||
:collapsed="!showMirrorInputs"
|
||||
pt:root="bg-neutral-800 border-none w-[600px]"
|
||||
>
|
||||
<template
|
||||
v-for="([item, modelValue], index) in mirrors"
|
||||
:key="item.settingId + item.mirror"
|
||||
>
|
||||
<Divider v-if="index > 0" />
|
||||
|
||||
<MirrorItem
|
||||
:item="item"
|
||||
v-model="modelValue.value"
|
||||
@state-change="validationStates[index] = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #icons>
|
||||
<i
|
||||
:class="{
|
||||
'pi pi-spin pi-spinner text-neutral-400':
|
||||
validationState === ValidationState.LOADING,
|
||||
'pi pi-check text-green-500':
|
||||
validationState === ValidationState.VALID,
|
||||
'pi pi-times text-red-500':
|
||||
validationState === ValidationState.INVALID
|
||||
}"
|
||||
v-tooltip="validationStateTooltip"
|
||||
/>
|
||||
</template>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CUDA_TORCH_URL,
|
||||
NIGHTLY_CPU_TORCH_URL,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Divider from 'primevue/divider'
|
||||
import Panel from 'primevue/panel'
|
||||
import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
const showMirrorInputs = ref(false)
|
||||
const { device } = defineProps<{ device: TorchDeviceType }>()
|
||||
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
|
||||
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
|
||||
const torchMirror = defineModel<string>('torchMirror', { required: true })
|
||||
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
return {
|
||||
settingId,
|
||||
mirror: NIGHTLY_CPU_TORCH_URL,
|
||||
fallbackMirror: NIGHTLY_CPU_TORCH_URL
|
||||
}
|
||||
case 'nvidia':
|
||||
return {
|
||||
settingId,
|
||||
mirror: CUDA_TORCH_URL,
|
||||
fallbackMirror: CUDA_TORCH_URL
|
||||
}
|
||||
case 'cpu':
|
||||
default:
|
||||
return {
|
||||
settingId,
|
||||
mirror: PYPI_MIRROR.mirror,
|
||||
fallbackMirror: PYPI_MIRROR.fallbackMirror
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mirrors = computed<[UVMirror, ModelRef<string>][]>(() => [
|
||||
[PYTHON_MIRROR, pythonMirror],
|
||||
[PYPI_MIRROR, pypiMirror],
|
||||
[getTorchMirrorItem(device), torchMirror]
|
||||
])
|
||||
|
||||
const validationStates = ref<ValidationState[]>(
|
||||
mirrors.value.map(() => ValidationState.IDLE)
|
||||
)
|
||||
const validationState = computed(() => {
|
||||
return mergeValidationStates(validationStates.value)
|
||||
})
|
||||
const validationStateTooltip = computed(() => {
|
||||
switch (validationState.value) {
|
||||
case ValidationState.INVALID:
|
||||
return t('install.settings.mirrorsUnreachable')
|
||||
case ValidationState.VALID:
|
||||
return t('install.settings.mirrorsReachable')
|
||||
default:
|
||||
return t('install.settings.checkingMirrors')
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// Check if user is in China and set fallback mirrors directly
|
||||
if (await isInChina()) {
|
||||
for (const [item, modelValue] of mirrors.value) {
|
||||
modelValue.value = item.fallbackMirror
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
64
src/components/install/mirror/MirrorItem.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="w-full">
|
||||
<h3 class="text-lg font-medium text-neutral-100">
|
||||
{{ $t(`settings.${normalizedSettingId}.name`) }}
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-400 mt-1">
|
||||
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
|
||||
</p>
|
||||
</div>
|
||||
<UrlInput
|
||||
v-model="modelValue"
|
||||
:validate-url-fn="
|
||||
(mirror: string) =>
|
||||
checkMirrorReachable(mirror + (item.validationPathSuffix ?? ''))
|
||||
"
|
||||
@state-change="validationState = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { UVMirror } from '@/constants/uvMirrors'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: UVMirror
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'state-change': [state: ValidationState]
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>('modelValue', { required: true })
|
||||
const validationState = ref<ValidationState>(ValidationState.IDLE)
|
||||
|
||||
const normalizedSettingId = computed(() => {
|
||||
return normalizeI18nKey(item.settingId)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Set mirror value if not already set
|
||||
if (!modelValue.value) {
|
||||
modelValue.value = item.mirror
|
||||
}
|
||||
})
|
||||
|
||||
watch(validationState, (newState) => {
|
||||
emit('state-change', newState)
|
||||
|
||||
// Set fallback mirror if default mirror is invalid
|
||||
if (
|
||||
newState === ValidationState.INVALID &&
|
||||
modelValue.value === item.mirror
|
||||
) {
|
||||
modelValue.value = item.fallbackMirror
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -3,9 +3,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons, type PrimeIconsOptions } from '@primevue/core/api'
|
||||
import Tag, { TagProps } from 'primevue/tag'
|
||||
import { ref, watch } from 'vue'
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
@@ -16,25 +16,21 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
// Bindings
|
||||
const icon = ref<string>(null)
|
||||
const severity = ref<TagProps['severity']>(null)
|
||||
const value = ref<PrimeIconsOptions[keyof PrimeIconsOptions]>(null)
|
||||
const icon = computed(() => {
|
||||
if (props.refreshing) return PrimeIcons.QUESTION
|
||||
if (props.error) return PrimeIcons.TIMES
|
||||
return PrimeIcons.CHECK
|
||||
})
|
||||
|
||||
const updateBindings = () => {
|
||||
if (props.refreshing) {
|
||||
icon.value = PrimeIcons.QUESTION
|
||||
severity.value = 'info'
|
||||
value.value = t('maintenance.refreshing')
|
||||
} else if (props.error) {
|
||||
icon.value = PrimeIcons.TIMES
|
||||
severity.value = 'danger'
|
||||
value.value = t('g.error')
|
||||
} else {
|
||||
icon.value = PrimeIcons.CHECK
|
||||
severity.value = 'success'
|
||||
value.value = t('maintenance.OK')
|
||||
}
|
||||
}
|
||||
const severity = computed(() => {
|
||||
if (props.refreshing) return 'info'
|
||||
if (props.error) return 'danger'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
watch(props, updateBindings, { deep: true })
|
||||
const value = computed(() => {
|
||||
if (props.refreshing) return t('maintenance.refreshing')
|
||||
if (props.error) return t('g.error')
|
||||
return t('maintenance.OK')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
:class="{ 'opacity-65': state.state !== 'error' }"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="state.state === 'error'"
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
@@ -38,7 +38,7 @@
|
||||
</Card>
|
||||
|
||||
<i
|
||||
v-if="!isLoading && state.state === 'OK'"
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
class="task-card-ok pi pi-check"
|
||||
/>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@ import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const state = computed(() => taskStore.getState(props.task))
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
@@ -68,14 +68,14 @@ defineEmits<{
|
||||
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
state.value.state === 'error'
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => state.value.refreshing)
|
||||
const reactiveExecuting = computed(() => state.value.executing)
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<tr
|
||||
class="border-neutral-700 border-solid border-y"
|
||||
:class="{
|
||||
'opacity-50': state.state === 'resolved',
|
||||
'opacity-75': isLoading && state.state !== 'resolved'
|
||||
'opacity-50': runner.resolved,
|
||||
'opacity-75': isLoading && runner.resolved
|
||||
}"
|
||||
>
|
||||
<td class="text-center w-16">
|
||||
<TaskListStatusIcon :state="state.state" :loading="isLoading" />
|
||||
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-block">{{ task.name }}</p>
|
||||
@@ -51,7 +51,7 @@ import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
import TaskListStatusIcon from './TaskListStatusIcon.vue'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const state = computed(() => taskStore.getState(props.task))
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
@@ -65,14 +65,14 @@ defineEmits<{
|
||||
|
||||
// Binding
|
||||
const severity = computed<VueSeverity>(() =>
|
||||
state.value.state === 'error' || state.value.state === 'warning'
|
||||
runner.value.state === 'error' || runner.value.state === 'warning'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => state.value.refreshing)
|
||||
const reactiveExecuting = computed(() => state.value.executing)
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
|
||||
@@ -60,8 +60,9 @@
|
||||
<!-- FilterAndValue -->
|
||||
<template v-slot:chip="{ value }">
|
||||
<SearchFilterChip
|
||||
v-if="Array.isArray(value) && value.length === 2"
|
||||
:key="`${value[0].id}-${value[1]}`"
|
||||
@remove="onRemoveFilter($event, value)"
|
||||
@remove="onRemoveFilter($event, value as FilterAndValue)"
|
||||
:text="value[1]"
|
||||
:badge="value[0].invokeSequence.toUpperCase()"
|
||||
:badge-class="value[0].invokeSequence + '-badge'"
|
||||
|
||||
@@ -64,10 +64,7 @@ import {
|
||||
} from '@/stores/modelStore'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
@@ -126,7 +123,7 @@ const renderedRoot = computed<TreeExplorerNode<ModelOrFolder>>(() => {
|
||||
: node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: () => {
|
||||
getIcon() {
|
||||
if (model) {
|
||||
return model.image ? 'pi pi-image' : 'pi pi-file'
|
||||
}
|
||||
@@ -137,7 +134,7 @@ const renderedRoot = computed<TreeExplorerNode<ModelOrFolder>>(() => {
|
||||
}
|
||||
return 'pi pi-folder'
|
||||
},
|
||||
getBadgeText: () => {
|
||||
getBadgeText() {
|
||||
// Return null to apply default badge text
|
||||
// Return empty string to hide badge
|
||||
if (!folder) {
|
||||
@@ -147,11 +144,8 @@ const renderedRoot = computed<TreeExplorerNode<ModelOrFolder>>(() => {
|
||||
},
|
||||
children,
|
||||
draggable: node.leaf,
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ModelOrFolder>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
handleClick(e: MouseEvent) {
|
||||
if (this.leaf) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
const node = useLitegraphService().addNodeOnGraph(provider.nodeDef)
|
||||
|
||||
@@ -66,11 +66,12 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Popover from 'primevue/popover'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { Ref, computed, nextTick, ref } from 'vue'
|
||||
import { Ref, computed, h, nextTick, ref, render } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
|
||||
@@ -83,10 +84,7 @@ import {
|
||||
buildNodeDefTree,
|
||||
useNodeDefStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { sortedTree } from '@/utils/treeUtil'
|
||||
|
||||
import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue'
|
||||
@@ -118,21 +116,25 @@ const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
||||
label: node.leaf ? node.data.display_name : node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
if (node.leaf) {
|
||||
getIcon() {
|
||||
if (this.leaf) {
|
||||
return 'pi pi-circle-fill'
|
||||
}
|
||||
},
|
||||
children,
|
||||
draggable: node.leaf,
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
useLitegraphService().addNodeOnGraph(node.data)
|
||||
renderDragPreview(container) {
|
||||
const vnode = h(NodePreview, { nodeDef: node.data })
|
||||
render(vnode, container)
|
||||
return () => {
|
||||
render(null, container)
|
||||
}
|
||||
},
|
||||
handleClick(e: MouseEvent) {
|
||||
if (this.leaf) {
|
||||
useLitegraphService().addNodeOnGraph(this.data)
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
toggleNodeOnEvent(e, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full"
|
||||
class="comfy-vue-side-bar-container flex flex-col h-full group/sidebar-tab"
|
||||
:class="props.class"
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header">
|
||||
@@ -11,7 +11,11 @@
|
||||
</span>
|
||||
</template>
|
||||
<template #end>
|
||||
<slot name="tool-buttons"></slot>
|
||||
<div
|
||||
class="flex flex-row motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 touch:w-auto touch:opacity-100 transition-all duration-200"
|
||||
>
|
||||
<slot name="tool-buttons"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
<slot name="header"></slot>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
icon="pi pi-th-large"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
|
||||
@@ -14,6 +15,7 @@
|
||||
<Button
|
||||
class="open-workflow-button"
|
||||
icon="pi pi-folder-open"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.openWorkflow')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
|
||||
@@ -21,6 +23,7 @@
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.newBlankWorkflow')"
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
text
|
||||
@@ -242,24 +245,18 @@ const renderTreeNode = (
|
||||
|
||||
const workflow: ComfyWorkflow = node.data
|
||||
|
||||
const handleClick = (
|
||||
node: TreeExplorerNode<ComfyWorkflow>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
function handleClick(this: TreeExplorerNode<ComfyWorkflow>, e: MouseEvent) {
|
||||
if (this.leaf) {
|
||||
workflowService.openWorkflow(workflow)
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
toggleNodeOnEvent(e, this)
|
||||
}
|
||||
}
|
||||
|
||||
const actions = node.leaf
|
||||
? {
|
||||
handleClick,
|
||||
handleRename: async (
|
||||
node: TreeExplorerNode<ComfyWorkflow>,
|
||||
newName: string
|
||||
) => {
|
||||
async handleRename(newName: string) {
|
||||
const newPath =
|
||||
type === WorkflowTreeType.Browse
|
||||
? workflow.directory + '/' + appendJsonExt(newName)
|
||||
@@ -269,10 +266,10 @@ const renderTreeNode = (
|
||||
},
|
||||
handleDelete: workflow.isTemporary
|
||||
? undefined
|
||||
: async () => {
|
||||
: async function () {
|
||||
await workflowService.deleteWorkflow(workflow)
|
||||
},
|
||||
contextMenuItems: (node: TreeExplorerNode<ComfyWorkflow>) => {
|
||||
contextMenuItems() {
|
||||
return [
|
||||
{
|
||||
label: t('g.insert'),
|
||||
|
||||
@@ -139,8 +139,8 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
label: node.leaf ? node.data.display_name : node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
if (node.leaf) {
|
||||
getIcon() {
|
||||
if (this.leaf) {
|
||||
return 'pi pi-circle-fill'
|
||||
}
|
||||
const customization =
|
||||
@@ -152,10 +152,7 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
children: sortedChildren,
|
||||
draggable: node.leaf,
|
||||
droppable: !node.leaf,
|
||||
handleDrop: (
|
||||
node: TreeExplorerNode<ComfyNodeDefImpl>,
|
||||
data: TreeExplorerDragAndDropData<ComfyNodeDefImpl>
|
||||
) => {
|
||||
handleDrop(data: TreeExplorerDragAndDropData<ComfyNodeDefImpl>) {
|
||||
const nodeDefToAdd = data.data.data
|
||||
// Remove bookmark if the source is the top level bookmarked node.
|
||||
if (nodeBookmarkStore.isBookmarked(nodeDefToAdd)) {
|
||||
@@ -165,12 +162,9 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
const nodePath = folderNodeDef.category + '/' + nodeDefToAdd.name
|
||||
nodeBookmarkStore.addBookmark(nodePath)
|
||||
},
|
||||
handleClick: (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
useLitegraphService().addNodeOnGraph(node.data)
|
||||
handleClick(e: MouseEvent) {
|
||||
if (this.leaf) {
|
||||
useLitegraphService().addNodeOnGraph(this.data)
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
@@ -179,9 +173,13 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
...(node.leaf
|
||||
? {}
|
||||
: {
|
||||
handleRename,
|
||||
handleDelete: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
nodeBookmarkStore.deleteBookmarkFolder(node.data)
|
||||
handleRename(newName: string) {
|
||||
if (this.data && this.data.isDummyFolder) {
|
||||
nodeBookmarkStore.renameBookmarkFolder(this.data, newName)
|
||||
}
|
||||
},
|
||||
handleDelete() {
|
||||
nodeBookmarkStore.deleteBookmarkFolder(this.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -212,12 +210,6 @@ defineExpose({
|
||||
addNewBookmarkFolder
|
||||
})
|
||||
|
||||
const handleRename = (node: TreeNode, newName: string) => {
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
nodeBookmarkStore.renameBookmarkFolder(node.data, newName)
|
||||
}
|
||||
}
|
||||
|
||||
const showCustomizationDialog = ref(false)
|
||||
const initialIcon = ref(nodeBookmarkStore.defaultBookmarkIcon)
|
||||
const initialColor = ref(nodeBookmarkStore.defaultBookmarkColor)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow && !showTopMenu"
|
||||
v-show="isNativeWindow() && !showTopMenu"
|
||||
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
</template>
|
||||
@@ -50,7 +50,12 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { electronAPI, isElectron, showNativeMenu } from '@/utils/envUtil'
|
||||
import {
|
||||
electronAPI,
|
||||
isElectron,
|
||||
isNativeWindow,
|
||||
showNativeMenu
|
||||
} from '@/utils/envUtil'
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -64,10 +69,6 @@ const teleportTarget = computed(() =>
|
||||
? '.comfyui-body-top'
|
||||
: '.comfyui-body-bottom'
|
||||
)
|
||||
const isNativeWindow = computed(
|
||||
() =>
|
||||
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
|
||||
)
|
||||
const showTopMenu = computed(
|
||||
() => betaMenuEnabled.value && !workspaceState.focusMode
|
||||
)
|
||||
|
||||
@@ -157,7 +157,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
id: 'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
name: 'Show missing models warning',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
@@ -290,7 +290,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Node ID badge mode',
|
||||
type: 'combo',
|
||||
options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],
|
||||
defaultValue: NodeBadgeMode.ShowAll
|
||||
defaultValue: NodeBadgeMode.None
|
||||
},
|
||||
{
|
||||
id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
|
||||
@@ -363,6 +363,18 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Node.TooltipDelay',
|
||||
name: 'Tooltip Delay',
|
||||
type: 'number',
|
||||
attrs: {
|
||||
min: 100,
|
||||
max: 3000,
|
||||
step: 50
|
||||
},
|
||||
defaultValue: 500,
|
||||
versionAdded: '1.9.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.EnableTooltips',
|
||||
category: ['LiteGraph', 'Node', 'EnableTooltips'],
|
||||
@@ -654,7 +666,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Canvas.MaximumFps',
|
||||
name: 'Maxium FPS',
|
||||
name: 'Maximum FPS',
|
||||
tooltip:
|
||||
'The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0',
|
||||
type: 'slider',
|
||||
@@ -708,5 +720,32 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: 'after',
|
||||
options: ['before', 'after'],
|
||||
versionModified: '1.6.10'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.TutorialCompleted',
|
||||
name: 'Tutorial completed',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.8.7'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.ContextMenu.Scaling',
|
||||
name: 'Scale node combo widget menus (lists) when zoomed in',
|
||||
defaultValue: false,
|
||||
type: 'boolean',
|
||||
versionAdded: '1.8.8'
|
||||
},
|
||||
{
|
||||
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
|
||||
name: 'Low quality rendering zoom threshold',
|
||||
tooltip: 'Render low quality shapes when zoomed out',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.01
|
||||
},
|
||||
defaultValue: 0.6,
|
||||
versionAdded: '1.9.1'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -31,8 +31,10 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
execute: () => openUrl('https://git-scm.com/downloads/'),
|
||||
name: 'Download git',
|
||||
shortDescription: 'Open the git download page.',
|
||||
errorDescription:
|
||||
'Git is missing. Please download and install git, then restart ComfyUI Desktop.',
|
||||
description:
|
||||
'Git is required to download and manage custom nodes and other extensions. This fixer simply opens the download page in your browser. You must download and install git manually.',
|
||||
'Git is required to download and manage custom nodes and other extensions. This task opens the download page in your default browser, where you can download the latest version of git. Once you have installed git, please restart ComfyUI Desktop.',
|
||||
button: {
|
||||
icon: PrimeIcons.EXTERNAL_LINK,
|
||||
text: 'Download'
|
||||
|
||||
34
src/constants/uvMirrors.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
export const PYTHON_MIRROR: UVMirror = {
|
||||
settingId: 'Comfy-Desktop.UV.PythonInstallMirror',
|
||||
mirror:
|
||||
'https://github.com/astral-sh/python-build-standalone/releases/download',
|
||||
fallbackMirror:
|
||||
'https://bgithub.xyz/astral-sh/python-build-standalone/releases/download',
|
||||
validationPathSuffix:
|
||||
'/20250115/cpython-3.10.16+20250115-aarch64-apple-darwin-debug-full.tar.zst.sha256'
|
||||
}
|
||||
|
||||
export const PYPI_MIRROR: UVMirror = {
|
||||
settingId: 'Comfy-Desktop.UV.PypiInstallMirror',
|
||||
mirror: 'https://pypi.org/simple/',
|
||||
fallbackMirror: 'https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple'
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
|
||||
;(async () => {
|
||||
if (!isElectron()) return
|
||||
@@ -55,6 +56,40 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
electronAPI.Config.setWindowStyle(newValue)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.PythonInstallMirror',
|
||||
name: 'Python Install Mirror',
|
||||
tooltip: `Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme.`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn(mirror: string) {
|
||||
return checkMirrorReachable(
|
||||
mirror + PYTHON_MIRROR.validationPathSuffix
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.PypiInstallMirror',
|
||||
name: 'Pypi Install Mirror',
|
||||
tooltip: `Default pip install mirror`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn: checkMirrorReachable
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.UV.TorchInstallMirror',
|
||||
name: 'Torch Install Mirror',
|
||||
tooltip: `Pip install mirror for pytorch`,
|
||||
type: 'url',
|
||||
defaultValue: '',
|
||||
attrs: {
|
||||
validateUrlFn: checkMirrorReachable
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
134
src/extensions/core/load3d/Load3DConfiguration.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
class Load3DConfiguration {
|
||||
constructor(private load3d: Load3d) {}
|
||||
|
||||
configure(
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
material: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
upDirection: IWidget,
|
||||
fov: IWidget,
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
this.setupModelHandling(
|
||||
modelWidget,
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
this.setupMaterial(material)
|
||||
this.setupLighting(lightIntensity)
|
||||
this.setupDirection(upDirection)
|
||||
this.setupCamera(fov)
|
||||
this.setupDefaultProperties()
|
||||
}
|
||||
|
||||
private setupModelHandling(
|
||||
modelWidget: IWidget,
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
const onModelWidgetUpdate = this.createModelUpdateHandler(
|
||||
loadFolder,
|
||||
cameraState,
|
||||
postModelUpdateFunc
|
||||
)
|
||||
if (modelWidget.value) {
|
||||
onModelWidgetUpdate(modelWidget.value)
|
||||
}
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
}
|
||||
|
||||
private setupMaterial(material: IWidget) {
|
||||
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
|
||||
this.load3d.setMaterialMode(value)
|
||||
}
|
||||
this.load3d.setMaterialMode(
|
||||
material.value as 'original' | 'normal' | 'wireframe'
|
||||
)
|
||||
}
|
||||
|
||||
private setupLighting(lightIntensity: IWidget) {
|
||||
lightIntensity.callback = (value: number) => {
|
||||
this.load3d.setLightIntensity(value)
|
||||
}
|
||||
this.load3d.setLightIntensity(lightIntensity.value as number)
|
||||
}
|
||||
|
||||
private setupDirection(upDirection: IWidget) {
|
||||
upDirection.callback = (
|
||||
value: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) => {
|
||||
this.load3d.setUpDirection(value)
|
||||
}
|
||||
this.load3d.setUpDirection(
|
||||
upDirection.value as 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
)
|
||||
}
|
||||
|
||||
private setupCamera(fov: IWidget) {
|
||||
fov.callback = (value: number) => {
|
||||
this.load3d.setFOV(value)
|
||||
}
|
||||
this.load3d.setFOV(fov.value as number)
|
||||
}
|
||||
|
||||
private setupDefaultProperties() {
|
||||
const cameraType = this.load3d.loadNodeProperty(
|
||||
'Camera Type',
|
||||
'perspective'
|
||||
)
|
||||
this.load3d.toggleCamera(cameraType)
|
||||
|
||||
const showGrid = this.load3d.loadNodeProperty('Show Grid', true)
|
||||
this.load3d.toggleGrid(showGrid)
|
||||
|
||||
const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828')
|
||||
|
||||
this.load3d.setBackgroundColor(bgColor)
|
||||
}
|
||||
|
||||
private createModelUpdateHandler(
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any,
|
||||
postModelUpdateFunc?: (load3d: Load3d) => void
|
||||
) {
|
||||
let isFirstLoad = true
|
||||
return async (value: string | number | boolean | object) => {
|
||||
if (!value) return
|
||||
|
||||
const filename = value as string
|
||||
const modelUrl = api.apiURL(
|
||||
Load3dUtils.getResourceURL(
|
||||
...Load3dUtils.splitFilePath(filename),
|
||||
loadFolder
|
||||
)
|
||||
)
|
||||
|
||||
await this.load3d.loadModel(modelUrl, filename)
|
||||
|
||||
if (postModelUpdateFunc) {
|
||||
postModelUpdateFunc(this.load3d)
|
||||
}
|
||||
|
||||
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
|
||||
try {
|
||||
this.load3d.setCameraState(cameraState)
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore camera state:', error)
|
||||
}
|
||||
isFirstLoad = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3DConfiguration
|
||||
997
src/extensions/core/load3d/Load3d.ts
Normal file
@@ -0,0 +1,997 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3d {
|
||||
scene: THREE.Scene
|
||||
perspectiveCamera: THREE.PerspectiveCamera
|
||||
orthographicCamera: THREE.OrthographicCamera
|
||||
activeCamera: THREE.Camera
|
||||
renderer: THREE.WebGLRenderer
|
||||
controls: OrbitControls
|
||||
gltfLoader: GLTFLoader
|
||||
objLoader: OBJLoader
|
||||
mtlLoader: MTLLoader
|
||||
fbxLoader: FBXLoader
|
||||
stlLoader: STLLoader
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
|
||||
animationFrameId: number | null = null
|
||||
gridHelper: THREE.GridHelper
|
||||
lights: THREE.Light[] = []
|
||||
clock: THREE.Clock
|
||||
normalMaterial: THREE.MeshNormalMaterial
|
||||
standardMaterial: THREE.MeshStandardMaterial
|
||||
wireframeMaterial: THREE.MeshBasicMaterial
|
||||
depthMaterial: THREE.MeshDepthMaterial
|
||||
originalMaterials: WeakMap<THREE.Mesh, THREE.Material | THREE.Material[]> =
|
||||
new WeakMap()
|
||||
|
||||
materialMode: 'original' | 'normal' | 'wireframe' | 'depth' = 'original'
|
||||
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
|
||||
'original'
|
||||
originalRotation: THREE.Euler | null = null
|
||||
viewHelper: ViewHelper = {} as ViewHelper
|
||||
viewHelperContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
cameraSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
gridSwitcherContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
node: LGraphNode = {} as LGraphNode
|
||||
bgColorInput: HTMLInputElement = {} as HTMLInputElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000)
|
||||
this.perspectiveCamera.position.set(5, 5, 5)
|
||||
|
||||
const frustumSize = 10
|
||||
this.orthographicCamera = new THREE.OrthographicCamera(
|
||||
-frustumSize / 2,
|
||||
frustumSize / 2,
|
||||
frustumSize / 2,
|
||||
-frustumSize / 2,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
this.orthographicCamera.position.set(5, 5, 5)
|
||||
|
||||
this.activeCamera = this.perspectiveCamera
|
||||
|
||||
this.perspectiveCamera.lookAt(0, 0, 0)
|
||||
this.orthographicCamera.lookAt(0, 0, 0)
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
this.renderer.setClearColor(0x282828)
|
||||
this.renderer.autoClear = false
|
||||
|
||||
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
|
||||
|
||||
container.appendChild(rendererDomElement)
|
||||
|
||||
this.controls = new OrbitControls(
|
||||
this.activeCamera,
|
||||
this.renderer.domElement
|
||||
)
|
||||
this.controls.enableDamping = true
|
||||
|
||||
this.controls.addEventListener('end', () => {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
})
|
||||
|
||||
this.gltfLoader = new GLTFLoader()
|
||||
this.objLoader = new OBJLoader()
|
||||
this.mtlLoader = new MTLLoader()
|
||||
this.fbxLoader = new FBXLoader()
|
||||
this.stlLoader = new STLLoader()
|
||||
this.clock = new THREE.Clock()
|
||||
|
||||
this.setupLights()
|
||||
|
||||
this.gridHelper = new THREE.GridHelper(10, 10)
|
||||
this.gridHelper.position.set(0, 0, 0)
|
||||
this.scene.add(this.gridHelper)
|
||||
|
||||
this.normalMaterial = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.wireframeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
|
||||
this.depthMaterial = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
this.standardMaterial = this.createSTLMaterial()
|
||||
|
||||
this.createViewHelper(container)
|
||||
|
||||
this.createGridSwitcher(container)
|
||||
|
||||
this.createCameraSwitcher(container)
|
||||
|
||||
this.createColorPicker(container)
|
||||
|
||||
this.handleResize()
|
||||
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
setNode(node: LGraphNode) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
storeNodeProperty(name: string, value: any) {
|
||||
if (this.node) {
|
||||
this.node.properties[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeProperty(name: string, defaultValue: any) {
|
||||
if (
|
||||
!this.node ||
|
||||
!this.node.properties ||
|
||||
!(name in this.node.properties)
|
||||
) {
|
||||
return defaultValue
|
||||
}
|
||||
return this.node.properties[name]
|
||||
}
|
||||
|
||||
createViewHelper(container: Element | HTMLElement) {
|
||||
this.viewHelperContainer = document.createElement('div')
|
||||
|
||||
this.viewHelperContainer.style.position = 'absolute'
|
||||
this.viewHelperContainer.style.bottom = '0'
|
||||
this.viewHelperContainer.style.left = '0'
|
||||
this.viewHelperContainer.style.width = '128px'
|
||||
this.viewHelperContainer.style.height = '128px'
|
||||
this.viewHelperContainer.addEventListener('pointerup', (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
this.viewHelper.handleClick(event)
|
||||
})
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
container.appendChild(this.viewHelperContainer)
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
|
||||
this.viewHelper.center = this.controls.target
|
||||
}
|
||||
|
||||
createGridSwitcher(container: Element | HTMLElement) {
|
||||
this.gridSwitcherContainer = document.createElement('div')
|
||||
this.gridSwitcherContainer.style.position = 'absolute'
|
||||
this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
|
||||
this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
|
||||
this.gridSwitcherContainer.style.width = '20px'
|
||||
this.gridSwitcherContainer.style.height = '20px'
|
||||
this.gridSwitcherContainer.style.cursor = 'pointer'
|
||||
this.gridSwitcherContainer.style.alignItems = 'center'
|
||||
this.gridSwitcherContainer.style.justifyContent = 'center'
|
||||
this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
|
||||
|
||||
const gridIcon = document.createElement('div')
|
||||
gridIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M3 3h18v18H3z"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M3 15h18"/>
|
||||
<path d="M9 3v18"/>
|
||||
<path d="M15 3v18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
const updateButtonState = () => {
|
||||
if (this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor =
|
||||
'rgba(255, 255, 255, 0.2)'
|
||||
} else {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonState()
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.title = 'Toggle Grid'
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleGrid(!this.gridHelper.visible)
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.appendChild(gridIcon)
|
||||
container.appendChild(this.gridSwitcherContainer)
|
||||
}
|
||||
|
||||
createCameraSwitcher(container: Element | HTMLElement) {
|
||||
this.cameraSwitcherContainer = document.createElement('div')
|
||||
this.cameraSwitcherContainer.style.position = 'absolute'
|
||||
this.cameraSwitcherContainer.style.top = '3px'
|
||||
this.cameraSwitcherContainer.style.left = '3px'
|
||||
this.cameraSwitcherContainer.style.width = '20px'
|
||||
this.cameraSwitcherContainer.style.height = '20px'
|
||||
this.cameraSwitcherContainer.style.cursor = 'pointer'
|
||||
this.cameraSwitcherContainer.style.alignItems = 'center'
|
||||
this.cameraSwitcherContainer.style.justifyContent = 'center'
|
||||
|
||||
const cameraIcon = document.createElement('div')
|
||||
cameraIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"/>
|
||||
<path d="m12 12 4-2.4"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
`
|
||||
this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.title =
|
||||
'Switch Camera (Perspective/Orthographic)'
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleCamera()
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.appendChild(cameraIcon)
|
||||
|
||||
container.appendChild(this.cameraSwitcherContainer)
|
||||
}
|
||||
|
||||
createColorPicker(container: Element | HTMLElement) {
|
||||
const colorPickerContainer = document.createElement('div')
|
||||
colorPickerContainer.style.position = 'absolute'
|
||||
colorPickerContainer.style.top = '53px'
|
||||
colorPickerContainer.style.left = '3px'
|
||||
colorPickerContainer.style.width = '20px'
|
||||
colorPickerContainer.style.height = '20px'
|
||||
colorPickerContainer.style.cursor = 'pointer'
|
||||
colorPickerContainer.style.alignItems = 'center'
|
||||
colorPickerContainer.style.justifyContent = 'center'
|
||||
colorPickerContainer.title = 'Background Color'
|
||||
|
||||
const colorInput = document.createElement('input')
|
||||
colorInput.type = 'color'
|
||||
colorInput.style.opacity = '0'
|
||||
colorInput.style.position = 'absolute'
|
||||
colorInput.style.width = '100%'
|
||||
colorInput.style.height = '100%'
|
||||
colorInput.style.cursor = 'pointer'
|
||||
|
||||
const colorIcon = document.createElement('div')
|
||||
colorIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M12 3v18"/>
|
||||
<path d="M3 12h18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
colorInput.addEventListener('input', (event) => {
|
||||
const color = (event.target as HTMLInputElement).value
|
||||
this.setBackgroundColor(color)
|
||||
this.storeNodeProperty('Background Color', color)
|
||||
})
|
||||
|
||||
this.bgColorInput = colorInput
|
||||
colorPickerContainer.appendChild(colorInput)
|
||||
colorPickerContainer.appendChild(colorIcon)
|
||||
container.appendChild(colorPickerContainer)
|
||||
}
|
||||
|
||||
setFOV(fov: number) {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.fov = fov
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
}
|
||||
|
||||
getCameraState() {
|
||||
const currentType = this.getCurrentCameraType()
|
||||
return {
|
||||
position: this.activeCamera.position.clone(),
|
||||
target: this.controls.target.clone(),
|
||||
zoom:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? this.activeCamera.zoom
|
||||
: (this.activeCamera as THREE.PerspectiveCamera).zoom,
|
||||
cameraType: currentType
|
||||
}
|
||||
}
|
||||
|
||||
setCameraState(state: {
|
||||
position: THREE.Vector3
|
||||
target: THREE.Vector3
|
||||
zoom: number
|
||||
cameraType: 'perspective' | 'orthographic'
|
||||
}) {
|
||||
if (
|
||||
this.activeCamera !==
|
||||
(state.cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera)
|
||||
) {
|
||||
//this.toggleCamera(state.cameraType)
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(state.position)
|
||||
|
||||
this.controls.target.copy(state.target)
|
||||
|
||||
if (this.activeCamera instanceof THREE.OrthographicCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
} else if (this.activeCamera instanceof THREE.PerspectiveCamera) {
|
||||
this.activeCamera.zoom = state.zoom
|
||||
this.activeCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
setUpDirection(
|
||||
direction: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
|
||||
) {
|
||||
if (!this.currentModel) return
|
||||
|
||||
if (!this.originalRotation && this.currentModel.rotation) {
|
||||
this.originalRotation = this.currentModel.rotation.clone()
|
||||
}
|
||||
|
||||
this.currentUpDirection = direction
|
||||
|
||||
if (this.originalRotation) {
|
||||
this.currentModel.rotation.copy(this.originalRotation)
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 'original':
|
||||
break
|
||||
case '-x':
|
||||
this.currentModel.rotation.z = Math.PI / 2
|
||||
break
|
||||
case '+x':
|
||||
this.currentModel.rotation.z = -Math.PI / 2
|
||||
break
|
||||
case '-y':
|
||||
this.currentModel.rotation.x = Math.PI
|
||||
break
|
||||
case '+y':
|
||||
break
|
||||
case '-z':
|
||||
this.currentModel.rotation.x = Math.PI / 2
|
||||
break
|
||||
case '+z':
|
||||
this.currentModel.rotation.x = -Math.PI / 2
|
||||
break
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
|
||||
setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') {
|
||||
this.materialMode = mode
|
||||
|
||||
if (this.currentModel) {
|
||||
if (mode === 'depth') {
|
||||
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
} else {
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
}
|
||||
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
switch (mode) {
|
||||
case 'depth':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
const depthMat = new THREE.MeshDepthMaterial({
|
||||
depthPacking: THREE.BasicDepthPacking,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
|
||||
depthMat.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.cameraType = {
|
||||
value:
|
||||
this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 1.0
|
||||
: 0.0
|
||||
}
|
||||
|
||||
shader.fragmentShader = `
|
||||
uniform float cameraType;
|
||||
${shader.fragmentShader}
|
||||
`
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
/gl_FragColor\s*=\s*vec4\(\s*vec3\(\s*1.0\s*-\s*fragCoordZ\s*\)\s*,\s*opacity\s*\)\s*;/,
|
||||
`
|
||||
float depth = 1.0 - fragCoordZ;
|
||||
if (cameraType > 0.5) {
|
||||
depth = pow(depth, 400.0);
|
||||
} else {
|
||||
depth = pow(depth, 0.6);
|
||||
}
|
||||
gl_FragColor = vec4(vec3(depth), opacity);
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
depthMat.customProgramCacheKey = () => {
|
||||
return this.activeCamera instanceof THREE.OrthographicCamera
|
||||
? 'ortho'
|
||||
: 'persp'
|
||||
}
|
||||
|
||||
child.material = depthMat
|
||||
break
|
||||
case 'normal':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshNormalMaterial({
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide,
|
||||
normalScale: new THREE.Vector2(1, 1),
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
child.geometry.computeVertexNormals()
|
||||
break
|
||||
|
||||
case 'wireframe':
|
||||
if (!this.originalMaterials.has(child)) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
child.material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
wireframe: true,
|
||||
transparent: false,
|
||||
opacity: 1.0
|
||||
})
|
||||
break
|
||||
|
||||
case 'original':
|
||||
const originalMaterial = this.originalMaterials.get(child)
|
||||
if (originalMaterial) {
|
||||
child.material = originalMaterial
|
||||
} else {
|
||||
child.material = this.standardMaterial
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
}
|
||||
|
||||
setupLights() {
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
||||
this.scene.add(ambientLight)
|
||||
this.lights.push(ambientLight)
|
||||
|
||||
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
mainLight.position.set(0, 10, 10)
|
||||
this.scene.add(mainLight)
|
||||
this.lights.push(mainLight)
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.5)
|
||||
backLight.position.set(0, 10, -10)
|
||||
this.scene.add(backLight)
|
||||
this.lights.push(backLight)
|
||||
|
||||
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
leftFillLight.position.set(-10, 0, 0)
|
||||
this.scene.add(leftFillLight)
|
||||
this.lights.push(leftFillLight)
|
||||
|
||||
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
||||
rightFillLight.position.set(10, 0, 0)
|
||||
this.scene.add(rightFillLight)
|
||||
this.lights.push(rightFillLight)
|
||||
|
||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2)
|
||||
bottomLight.position.set(0, -10, 0)
|
||||
this.scene.add(bottomLight)
|
||||
this.lights.push(bottomLight)
|
||||
}
|
||||
|
||||
toggleCamera(cameraType?: 'perspective' | 'orthographic') {
|
||||
const oldCamera = this.activeCamera
|
||||
|
||||
const position = oldCamera.position.clone()
|
||||
const rotation = oldCamera.rotation.clone()
|
||||
const target = this.controls.target.clone()
|
||||
|
||||
if (!cameraType) {
|
||||
this.activeCamera =
|
||||
oldCamera === this.perspectiveCamera
|
||||
? this.orthographicCamera
|
||||
: this.perspectiveCamera
|
||||
} else {
|
||||
this.activeCamera =
|
||||
cameraType === 'perspective'
|
||||
? this.perspectiveCamera
|
||||
: this.orthographicCamera
|
||||
|
||||
if (oldCamera === this.activeCamera) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.activeCamera.position.copy(position)
|
||||
this.activeCamera.rotation.copy(rotation)
|
||||
|
||||
if (this.materialMode === 'depth' && oldCamera !== this.activeCamera) {
|
||||
this.setMaterialMode('depth')
|
||||
}
|
||||
|
||||
this.controls.object = this.activeCamera
|
||||
this.controls.target.copy(target)
|
||||
this.controls.update()
|
||||
|
||||
this.viewHelper.dispose()
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
this.viewHelper.center = this.controls.target
|
||||
|
||||
this.storeNodeProperty('Camera Type', this.getCurrentCameraType())
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
getCurrentCameraType(): 'perspective' | 'orthographic' {
|
||||
return this.activeCamera === this.perspectiveCamera
|
||||
? 'perspective'
|
||||
: 'orthographic'
|
||||
}
|
||||
|
||||
toggleGrid(showGrid: boolean) {
|
||||
if (this.gridHelper) {
|
||||
this.gridHelper.visible = showGrid
|
||||
|
||||
this.storeNodeProperty('Show Grid', showGrid)
|
||||
}
|
||||
}
|
||||
|
||||
setLightIntensity(intensity: number) {
|
||||
this.lights.forEach((light) => {
|
||||
if (light instanceof THREE.DirectionalLight) {
|
||||
if (light === this.lights[1]) {
|
||||
light.intensity = intensity * 0.8
|
||||
} else if (light === this.lights[2]) {
|
||||
light.intensity = intensity * 0.5
|
||||
} else if (light === this.lights[5]) {
|
||||
light.intensity = intensity * 0.2
|
||||
} else {
|
||||
light.intensity = intensity * 0.3
|
||||
}
|
||||
} else if (light instanceof THREE.AmbientLight) {
|
||||
light.intensity = intensity * 0.5
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
}
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
|
||||
clearModel() {
|
||||
const objectsToRemove: THREE.Object3D[] = []
|
||||
|
||||
this.scene.traverse((object) => {
|
||||
const isEnvironmentObject =
|
||||
object === this.gridHelper ||
|
||||
this.lights.includes(object as THREE.Light) ||
|
||||
object === this.perspectiveCamera ||
|
||||
object === this.orthographicCamera
|
||||
|
||||
if (!isEnvironmentObject) {
|
||||
objectsToRemove.push(object)
|
||||
}
|
||||
})
|
||||
|
||||
objectsToRemove.forEach((obj) => {
|
||||
if (obj.parent && obj.parent !== this.scene) {
|
||||
obj.parent.remove(obj)
|
||||
} else {
|
||||
this.scene.remove(obj)
|
||||
}
|
||||
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose()
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
obj.material?.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.resetScene()
|
||||
}
|
||||
|
||||
protected resetScene() {
|
||||
this.currentModel = null
|
||||
this.originalRotation = null
|
||||
|
||||
const defaultDistance = 10
|
||||
this.perspectiveCamera.position.set(
|
||||
defaultDistance,
|
||||
defaultDistance,
|
||||
defaultDistance
|
||||
)
|
||||
this.orthographicCamera.position.set(
|
||||
defaultDistance,
|
||||
defaultDistance,
|
||||
defaultDistance
|
||||
)
|
||||
|
||||
this.perspectiveCamera.lookAt(0, 0, 0)
|
||||
this.orthographicCamera.lookAt(0, 0, 0)
|
||||
|
||||
const frustumSize = 10
|
||||
const aspect =
|
||||
this.renderer.domElement.width / this.renderer.domElement.height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
|
||||
this.controls.target.set(0, 0, 0)
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
this.materialMode = 'original'
|
||||
this.originalMaterials = new WeakMap()
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
}
|
||||
|
||||
this.controls.dispose()
|
||||
this.viewHelper.dispose()
|
||||
this.renderer.dispose()
|
||||
this.renderer.domElement.remove()
|
||||
this.scene.clear()
|
||||
}
|
||||
|
||||
protected async loadModelInternal(
|
||||
url: string,
|
||||
fileExtension: string
|
||||
): Promise<THREE.Object3D | null> {
|
||||
let model: THREE.Object3D | null = null
|
||||
|
||||
switch (fileExtension) {
|
||||
case 'stl':
|
||||
const geometry = await this.stlLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = geometry
|
||||
|
||||
geometry.computeVertexNormals()
|
||||
const mesh = new THREE.Mesh(geometry, this.standardMaterial)
|
||||
const group = new THREE.Group()
|
||||
group.add(mesh)
|
||||
model = group
|
||||
break
|
||||
|
||||
case 'fbx':
|
||||
const fbxModel = await this.fbxLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = fbxModel
|
||||
|
||||
model = fbxModel
|
||||
|
||||
fbxModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'obj':
|
||||
if (this.materialMode === 'original') {
|
||||
const mtlUrl = url.replace(/\.obj([^.]*$)/, '.mtl$1')
|
||||
try {
|
||||
const materials = await this.mtlLoader.loadAsync(mtlUrl)
|
||||
materials.preload()
|
||||
this.objLoader.setMaterials(materials)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'No MTL file found or error loading it, continuing without materials'
|
||||
)
|
||||
}
|
||||
}
|
||||
model = await this.objLoader.loadAsync(url)
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'gltf':
|
||||
case 'glb':
|
||||
const gltf = await this.gltfLoader.loadAsync(url)
|
||||
|
||||
this.originalModel = gltf
|
||||
|
||||
model = gltf.scene
|
||||
gltf.scene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.computeVertexNormals()
|
||||
this.originalMaterials.set(child, child.material)
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
async loadModel(url: string, originalFileName?: string) {
|
||||
try {
|
||||
this.clearModel()
|
||||
|
||||
let fileExtension: string | undefined
|
||||
if (originalFileName) {
|
||||
fileExtension = originalFileName.split('.').pop()?.toLowerCase()
|
||||
} else {
|
||||
const filename = new URLSearchParams(url.split('?')[1]).get('filename')
|
||||
fileExtension = filename?.split('.').pop()?.toLowerCase()
|
||||
}
|
||||
|
||||
if (!fileExtension) {
|
||||
useToastStore().addAlert('Could not determine file type')
|
||||
return
|
||||
}
|
||||
|
||||
let model = await this.loadModelInternal(url, fileExtension)
|
||||
|
||||
if (model) {
|
||||
this.currentModel = model
|
||||
await this.setupModel(model)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading model:', error)
|
||||
}
|
||||
}
|
||||
|
||||
protected async setupModel(model: THREE.Object3D) {
|
||||
const box = new THREE.Box3().setFromObject(model)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const targetSize = 5
|
||||
const scale = targetSize / maxDim
|
||||
model.scale.multiplyScalar(scale)
|
||||
|
||||
box.setFromObject(model)
|
||||
box.getCenter(center)
|
||||
box.getSize(size)
|
||||
|
||||
model.position.set(-center.x, -box.min.y, -center.z)
|
||||
|
||||
this.scene.add(model)
|
||||
|
||||
if (this.materialMode !== 'original') {
|
||||
this.setMaterialMode(this.materialMode)
|
||||
}
|
||||
|
||||
if (this.currentUpDirection !== 'original') {
|
||||
this.setUpDirection(this.currentUpDirection)
|
||||
}
|
||||
|
||||
await this.setupCamera(size)
|
||||
}
|
||||
|
||||
protected async setupCamera(size: THREE.Vector3) {
|
||||
const distance = Math.max(size.x, size.z) * 2
|
||||
const height = size.y * 2
|
||||
|
||||
this.perspectiveCamera.position.set(distance, height, distance)
|
||||
this.orthographicCamera.position.set(distance, height, distance)
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.lookAt(0, size.y / 2, 0)
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = Math.max(size.x, size.y, size.z) * 2
|
||||
const aspect =
|
||||
this.renderer.domElement.width / this.renderer.domElement.height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.lookAt(0, size.y / 2, 0)
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.controls.target.set(0, size.y / 2, 0)
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||
this.renderer.toneMappingExposure = 1
|
||||
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const parentElement = this.renderer?.domElement?.parentElement
|
||||
|
||||
if (!parentElement) {
|
||||
console.warn('Parent element not found')
|
||||
return
|
||||
}
|
||||
|
||||
const width = parentElement?.clientWidth
|
||||
const height = parentElement?.clientHeight
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
}
|
||||
|
||||
captureScene(
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<{ scene: string; mask: string }> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const originalWidth = this.renderer.domElement.width
|
||||
const originalHeight = this.renderer.domElement.height
|
||||
const originalClearColor = this.renderer.getClearColor(
|
||||
new THREE.Color()
|
||||
)
|
||||
const originalClearAlpha = this.renderer.getClearAlpha()
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
this.orthographicCamera.bottom = -frustumSize / 2
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(originalClearColor, originalClearAlpha)
|
||||
this.renderer.setSize(originalWidth, originalHeight)
|
||||
this.handleResize()
|
||||
|
||||
resolve({ scene: sceneData, mask: maskData })
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
createSTLMaterial() {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: 0x808080,
|
||||
metalness: 0.1,
|
||||
roughness: 0.8,
|
||||
flatShading: false,
|
||||
side: THREE.DoubleSide
|
||||
})
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string) {
|
||||
this.renderer.setClearColor(new THREE.Color(color))
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.bgColorInput) {
|
||||
this.bgColorInput.value = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3d
|
||||
359
src/extensions/core/load3d/Load3dAnimation.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
|
||||
class Load3dAnimation extends Load3d {
|
||||
currentAnimation: THREE.AnimationMixer | null = null
|
||||
animationActions: THREE.AnimationAction[] = []
|
||||
animationClips: THREE.AnimationClip[] = []
|
||||
selectedAnimationIndex: number = 0
|
||||
isAnimationPlaying: boolean = false
|
||||
|
||||
animationSpeed: number = 1.0
|
||||
playPauseContainer: HTMLDivElement = {} as HTMLDivElement
|
||||
animationSelect: HTMLSelectElement = {} as HTMLSelectElement
|
||||
speedSelect: HTMLSelectElement = {} as HTMLSelectElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
super(container)
|
||||
this.createPlayPauseButton(container)
|
||||
this.createAnimationList(container)
|
||||
this.createSpeedSelect(container)
|
||||
}
|
||||
|
||||
createAnimationList(container: Element | HTMLElement) {
|
||||
this.animationSelect = document.createElement('select')
|
||||
Object.assign(this.animationSelect.style, {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
left: '50%',
|
||||
transform: 'translateX(15px)',
|
||||
width: '90px',
|
||||
height: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
padding: '0 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
outline: 'none'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('mouseenter', () => {
|
||||
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('mouseleave', () => {
|
||||
this.animationSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.animationSelect.addEventListener('change', (event) => {
|
||||
const select = event.target as HTMLSelectElement
|
||||
this.updateSelectedAnimation(select.selectedIndex)
|
||||
})
|
||||
|
||||
container.appendChild(this.animationSelect)
|
||||
}
|
||||
|
||||
updateAnimationList() {
|
||||
this.animationSelect.innerHTML = ''
|
||||
this.animationClips.forEach((clip, index) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = index.toString()
|
||||
option.text = clip.name || `Animation ${index + 1}`
|
||||
option.selected = index === this.selectedAnimationIndex
|
||||
this.animationSelect.appendChild(option)
|
||||
})
|
||||
}
|
||||
|
||||
createPlayPauseButton(container: Element | HTMLElement) {
|
||||
this.playPauseContainer = document.createElement('div')
|
||||
this.playPauseContainer.style.position = 'absolute'
|
||||
this.playPauseContainer.style.top = '3px'
|
||||
this.playPauseContainer.style.left = '50%'
|
||||
this.playPauseContainer.style.transform = 'translateX(-50%)'
|
||||
this.playPauseContainer.style.width = '20px'
|
||||
this.playPauseContainer.style.height = '20px'
|
||||
this.playPauseContainer.style.cursor = 'pointer'
|
||||
this.playPauseContainer.style.alignItems = 'center'
|
||||
this.playPauseContainer.style.justifyContent = 'center'
|
||||
|
||||
const updateButtonState = () => {
|
||||
const icon = this.playPauseContainer.querySelector('svg')
|
||||
if (icon) {
|
||||
if (this.isAnimationPlaying) {
|
||||
icon.innerHTML = `
|
||||
<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>
|
||||
`
|
||||
this.playPauseContainer.title = 'Pause Animation'
|
||||
} else {
|
||||
icon.innerHTML = `
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
`
|
||||
this.playPauseContainer.title = 'Play Animation'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const playIcon = document.createElement('div')
|
||||
playIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
this.playPauseContainer.addEventListener('mouseenter', () => {
|
||||
this.playPauseContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.playPauseContainer.addEventListener('mouseleave', () => {
|
||||
this.playPauseContainer.style.backgroundColor = 'transparent'
|
||||
})
|
||||
|
||||
this.playPauseContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleAnimation()
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.playPauseContainer.appendChild(playIcon)
|
||||
container.appendChild(this.playPauseContainer)
|
||||
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
createSpeedSelect(container: Element | HTMLElement) {
|
||||
this.speedSelect = document.createElement('select')
|
||||
Object.assign(this.speedSelect.style, {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-75px)',
|
||||
width: '60px',
|
||||
height: '20px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
padding: '0 8px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
outline: 'none'
|
||||
})
|
||||
|
||||
const speeds = [0.1, 0.5, 1, 1.5, 2]
|
||||
speeds.forEach((speed) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = speed.toString()
|
||||
option.text = `${speed}x`
|
||||
option.selected = speed === 1
|
||||
this.speedSelect.appendChild(option)
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('mouseenter', () => {
|
||||
this.speedSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('mouseleave', () => {
|
||||
this.speedSelect.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.speedSelect.addEventListener('change', (event) => {
|
||||
const select = event.target as HTMLSelectElement
|
||||
const newSpeed = parseFloat(select.value)
|
||||
this.setAnimationSpeed(newSpeed)
|
||||
})
|
||||
|
||||
container.appendChild(this.speedSelect)
|
||||
}
|
||||
|
||||
protected async setupModel(model: THREE.Object3D) {
|
||||
await super.setupModel(model)
|
||||
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
}
|
||||
|
||||
let animations: THREE.AnimationClip[] = []
|
||||
if (model.animations?.length > 0) {
|
||||
animations = model.animations
|
||||
} else if (this.originalModel && 'animations' in this.originalModel) {
|
||||
animations = (
|
||||
this.originalModel as unknown as { animations: THREE.AnimationClip[] }
|
||||
).animations
|
||||
}
|
||||
|
||||
if (animations.length > 0) {
|
||||
this.animationClips = animations
|
||||
if (model.type === 'Scene') {
|
||||
this.currentAnimation = new THREE.AnimationMixer(model)
|
||||
} else {
|
||||
this.currentAnimation = new THREE.AnimationMixer(this.currentModel!)
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.updateSelectedAnimation(0)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.playPauseContainer.style.display = 'block'
|
||||
} else {
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
if (this.animationClips.length > 0) {
|
||||
this.playPauseContainer.style.display = 'block'
|
||||
this.animationSelect.style.display = 'block'
|
||||
this.speedSelect.style.display = 'block'
|
||||
this.updateAnimationList()
|
||||
} else {
|
||||
this.playPauseContainer.style.display = 'none'
|
||||
this.animationSelect.style.display = 'none'
|
||||
this.speedSelect.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
setAnimationSpeed(speed: number) {
|
||||
this.animationSpeed = speed
|
||||
this.animationActions.forEach((action) => {
|
||||
action.setEffectiveTimeScale(speed)
|
||||
})
|
||||
}
|
||||
|
||||
updateSelectedAnimation(index: number) {
|
||||
if (
|
||||
!this.currentAnimation ||
|
||||
!this.animationClips ||
|
||||
index >= this.animationClips.length
|
||||
) {
|
||||
console.warn('Invalid animation update request')
|
||||
return
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation.stopAllAction()
|
||||
this.animationActions = []
|
||||
|
||||
this.selectedAnimationIndex = index
|
||||
const clip = this.animationClips[index]
|
||||
|
||||
const action = this.currentAnimation.clipAction(clip)
|
||||
|
||||
action.setEffectiveTimeScale(this.animationSpeed)
|
||||
|
||||
action.reset()
|
||||
action.clampWhenFinished = false
|
||||
action.loop = THREE.LoopRepeat
|
||||
|
||||
if (this.isAnimationPlaying) {
|
||||
action.play()
|
||||
} else {
|
||||
action.play()
|
||||
action.paused = true
|
||||
}
|
||||
|
||||
this.animationActions = [action]
|
||||
|
||||
this.updateAnimationList()
|
||||
}
|
||||
|
||||
clearModel() {
|
||||
if (this.currentAnimation) {
|
||||
this.animationActions.forEach((action) => {
|
||||
action.stop()
|
||||
})
|
||||
this.currentAnimation = null
|
||||
}
|
||||
this.animationActions = []
|
||||
this.animationClips = []
|
||||
this.selectedAnimationIndex = 0
|
||||
this.isAnimationPlaying = false
|
||||
this.animationSpeed = 1.0
|
||||
|
||||
super.clearModel()
|
||||
|
||||
if (this.animationSelect) {
|
||||
this.animationSelect.style.display = 'none'
|
||||
this.animationSelect.innerHTML = ''
|
||||
}
|
||||
|
||||
if (this.speedSelect) {
|
||||
this.speedSelect.style.display = 'none'
|
||||
this.speedSelect.value = '1'
|
||||
}
|
||||
}
|
||||
|
||||
getAnimationNames(): string[] {
|
||||
return this.animationClips.map((clip, index) => {
|
||||
return clip.name || `Animation ${index + 1}`
|
||||
})
|
||||
}
|
||||
|
||||
toggleAnimation(play?: boolean) {
|
||||
if (!this.currentAnimation || this.animationActions.length === 0) {
|
||||
console.warn('No animation to toggle')
|
||||
return
|
||||
}
|
||||
|
||||
this.isAnimationPlaying = play ?? !this.isAnimationPlaying
|
||||
|
||||
const icon = this.playPauseContainer.querySelector('svg')
|
||||
if (icon) {
|
||||
if (this.isAnimationPlaying) {
|
||||
icon.innerHTML = '<path d="M6 4h4v16H6zM14 4h4v16h-4z"/>'
|
||||
this.playPauseContainer.title = 'Pause Animation'
|
||||
} else {
|
||||
icon.innerHTML = '<path d="M8 5v14l11-7z"/>'
|
||||
this.playPauseContainer.title = 'Play Animation'
|
||||
}
|
||||
}
|
||||
|
||||
this.animationActions.forEach((action) => {
|
||||
if (this.isAnimationPlaying) {
|
||||
action.paused = false
|
||||
if (action.time === 0 || action.time === action.getClip().duration) {
|
||||
action.reset()
|
||||
}
|
||||
} else {
|
||||
action.paused = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
|
||||
if (!this.viewHelper.animating) {
|
||||
this.storeNodeProperty('Camera Info', this.getCameraState())
|
||||
}
|
||||
}
|
||||
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dAnimation
|
||||
122
src/extensions/core/load3d/Load3dUtils.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3dUtils {
|
||||
static async uploadTempImage(imageData: string, prefix: string) {
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `${prefix}_${Date.now()}.png`
|
||||
const file = new File([blob], name)
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', 'threed')
|
||||
body.append('type', 'temp')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
const err = `Error uploading temp image: ${resp.status} - ${resp.statusText}`
|
||||
useToastStore().addAlert(err)
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
static async uploadFile(
|
||||
load3d: Load3d,
|
||||
file: File,
|
||||
fileInput?: HTMLInputElement
|
||||
) {
|
||||
let uploadPath
|
||||
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', '3d')
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
|
||||
if (data.subfolder) path = data.subfolder + '/' + path
|
||||
|
||||
uploadPath = path
|
||||
|
||||
const modelUrl = api.apiURL(
|
||||
this.getResourceURL(...this.splitFilePath(path), 'input')
|
||||
)
|
||||
await load3d.loadModel(modelUrl, file.name)
|
||||
|
||||
const fileExt = file.name.split('.').pop()?.toLowerCase()
|
||||
if (fileExt === 'obj' && fileInput?.files) {
|
||||
try {
|
||||
const mtlFile = Array.from(fileInput.files).find((f) =>
|
||||
f.name.toLowerCase().endsWith('.mtl')
|
||||
)
|
||||
|
||||
if (mtlFile) {
|
||||
const mtlFormData = new FormData()
|
||||
mtlFormData.append('image', mtlFile)
|
||||
mtlFormData.append('subfolder', '3d')
|
||||
|
||||
await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body: mtlFormData
|
||||
})
|
||||
}
|
||||
} catch (mtlError) {
|
||||
console.warn('Failed to upload MTL file:', mtlError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
useToastStore().addAlert(
|
||||
error instanceof Error ? error.message : 'Upload failed'
|
||||
)
|
||||
}
|
||||
|
||||
return uploadPath
|
||||
}
|
||||
|
||||
static splitFilePath(path: string): [string, string] {
|
||||
const folder_separator = path.lastIndexOf('/')
|
||||
if (folder_separator === -1) {
|
||||
return ['', path]
|
||||
}
|
||||
return [
|
||||
path.substring(0, folder_separator),
|
||||
path.substring(folder_separator + 1)
|
||||
]
|
||||
}
|
||||
|
||||
static getResourceURL(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): string {
|
||||
const params = [
|
||||
'filename=' + encodeURIComponent(filename),
|
||||
'type=' + type,
|
||||
'subfolder=' + subfolder,
|
||||
app.getRandParam().substring(1)
|
||||
].join('&')
|
||||
|
||||
return `/view?${params}`
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dUtils
|
||||
@@ -12,13 +12,13 @@ app.registerExtension({
|
||||
registerCustomNodes() {
|
||||
class NoteNode extends LGraphNode {
|
||||
static category: string
|
||||
static collapsable: boolean
|
||||
static title_mode: number
|
||||
|
||||
color = LGraphCanvas.node_colors.yellow.color
|
||||
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
isVirtualNode: boolean
|
||||
collapsable: boolean
|
||||
title_mode: number
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title)
|
||||
|
||||
@@ -782,7 +782,27 @@ app.registerExtension({
|
||||
? origGetExtraMenuOptions.apply(this, arguments)
|
||||
: undefined
|
||||
|
||||
const getPointerCanvasPos = () => {
|
||||
const pos = this.graph?.list_of_graphcanvas?.at(0)?.graph_mouse
|
||||
return pos ? { canvasX: pos[0], canvasY: pos[1] } : undefined
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
const { canvasX, canvasY } = getPointerCanvasPos()
|
||||
const widget = this.getWidgetOnPos(canvasX, canvasY)
|
||||
// @ts-expect-error custom widget type
|
||||
if (widget && widget.type !== CONVERTED_TYPE) {
|
||||
const config = getConfig.call(this, widget.name) ?? [
|
||||
widget.type,
|
||||
widget.options || {}
|
||||
]
|
||||
if (isConvertibleWidget(widget, config)) {
|
||||
options.push({
|
||||
content: `Convert ${widget.name} to input`,
|
||||
callback: () => convertToInput(this, widget, config) && false
|
||||
})
|
||||
}
|
||||
}
|
||||
let toInput = []
|
||||
let toWidget = []
|
||||
for (const w of this.widgets) {
|
||||
|
||||
30
src/hooks/bottomPanelTabs/useTerminalBuffer.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { SerializeAddon } from '@xterm/addon-serialize'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { markRaw, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useTerminalBuffer() {
|
||||
const serializeAddon = new SerializeAddon()
|
||||
const terminal = markRaw(new Terminal({ convertEol: true }))
|
||||
|
||||
const copyTo = (destinationTerminal: Terminal) => {
|
||||
destinationTerminal.write(serializeAddon.serialize())
|
||||
}
|
||||
|
||||
const write = (message: string) => terminal.write(message)
|
||||
|
||||
const serialize = () => serializeAddon.serialize()
|
||||
|
||||
onMounted(() => {
|
||||
terminal.loadAddon(serializeAddon)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
terminal.dispose()
|
||||
})
|
||||
|
||||
return {
|
||||
copyTo,
|
||||
serialize,
|
||||
write
|
||||
}
|
||||
}
|
||||
77
src/hooks/canvasDropHooks.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { ComfyModelDef } from '@/stores/modelStore'
|
||||
import { ModelNodeProvider } from '@/stores/modelToNodeStore'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
|
||||
usePragmaticDroppable(() => canvasRef.value, {
|
||||
getDropEffect: (args): Exclude<DataTransfer['dropEffect'], 'none'> =>
|
||||
args.source.data.type === 'tree-explorer-node' ? 'copy' : 'move',
|
||||
onDrop: (event) => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX,
|
||||
loc.clientY + LiteGraph.NODE_TITLE_HEIGHT
|
||||
])
|
||||
litegraphService.addNodeOnGraph(nodeDef, { pos })
|
||||
} else if (node.data instanceof ComfyModelDef) {
|
||||
const model = node.data
|
||||
const pos = comfyApp.clientPosToCanvasPos([loc.clientX, loc.clientY])
|
||||
const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1])
|
||||
let targetProvider: ModelNodeProvider | null = null
|
||||
let targetGraphNode: LGraphNode | null = null
|
||||
if (nodeAtPos) {
|
||||
const providers = modelToNodeStore.getAllNodeProviders(
|
||||
model.directory
|
||||
)
|
||||
for (const provider of providers) {
|
||||
if (provider.nodeDef.name === nodeAtPos.comfyClass) {
|
||||
targetGraphNode = nodeAtPos
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetGraphNode) {
|
||||
const provider = modelToNodeStore.getNodeProvider(model.directory)
|
||||
if (provider) {
|
||||
targetGraphNode = litegraphService.addNodeOnGraph(
|
||||
provider.nodeDef,
|
||||
{
|
||||
pos
|
||||
}
|
||||
)
|
||||
targetProvider = provider
|
||||
}
|
||||
}
|
||||
if (targetGraphNode) {
|
||||
const widget = targetGraphNode.widgets?.find(
|
||||
(widget) => widget.name === targetProvider?.key
|
||||
)
|
||||
if (widget) {
|
||||
widget.value = model.file_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
27
src/hooks/litegraphHooks.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
ContextMenu,
|
||||
DragAndScale,
|
||||
LGraph,
|
||||
LGraphBadge,
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
|
||||
/**
|
||||
* Assign all properties of LiteGraph to window to make it backward compatible.
|
||||
*/
|
||||
export const useGlobalLitegraph = () => {
|
||||
window['LiteGraph'] = LiteGraph
|
||||
window['LGraph'] = LGraph
|
||||
window['LLink'] = LLink
|
||||
window['LGraphNode'] = LGraphNode
|
||||
window['LGraphGroup'] = LGraphGroup
|
||||
window['DragAndScale'] = DragAndScale
|
||||
window['LGraphCanvas'] = LGraphCanvas
|
||||
window['ContextMenu'] = ContextMenu
|
||||
window['LGraphBadge'] = LGraphBadge
|
||||
}
|
||||
104
src/hooks/translationHooks.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type {
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
INodeInputSlot,
|
||||
IWidget
|
||||
} from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
|
||||
import { st, te } from '@/i18n'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Add translation for litegraph context menu.
|
||||
*/
|
||||
export const useContextMenuTranslation = () => {
|
||||
const f = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
const getCanvasCenterMenuOptions = function (
|
||||
this: LGraphCanvas,
|
||||
...args: Parameters<typeof f>
|
||||
) {
|
||||
const res = f.apply(this, args) as ReturnType<typeof f>
|
||||
for (const item of res) {
|
||||
if (item?.content) {
|
||||
item.content = st(`contextMenu.${item.content}`, item.content)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions
|
||||
|
||||
function translateMenus(
|
||||
values: (IContextMenuValue | string)[] | undefined,
|
||||
options: IContextMenuOptions
|
||||
) {
|
||||
if (!values) return
|
||||
const reInput = /Convert (.*) to input/
|
||||
const reWidget = /Convert (.*) to widget/
|
||||
const cvt = st('contextMenu.Convert ', 'Convert ')
|
||||
const tinp = st('contextMenu. to input', ' to input')
|
||||
const twgt = st('contextMenu. to widget', ' to widget')
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string') continue
|
||||
|
||||
translateMenus(value?.submenu?.options, options)
|
||||
if (!value?.content) {
|
||||
continue
|
||||
}
|
||||
if (te(`contextMenu.${value.content}`)) {
|
||||
value.content = st(`contextMenu.${value.content}`, value.content)
|
||||
}
|
||||
|
||||
// for capture translation text of input and widget
|
||||
const extraInfo: any = options.extra || options.parentMenu?.options?.extra
|
||||
// widgets and inputs
|
||||
const matchInput = value.content?.match(reInput)
|
||||
if (matchInput) {
|
||||
let match = matchInput[1]
|
||||
extraInfo?.inputs?.find((i: INodeInputSlot) => {
|
||||
if (i.name != match) return false
|
||||
match = i.label ? i.label : i.name
|
||||
})
|
||||
extraInfo?.widgets?.find((i: IWidget) => {
|
||||
if (i.name != match) return false
|
||||
match = i.label ? i.label : i.name
|
||||
})
|
||||
value.content = cvt + match + tinp
|
||||
continue
|
||||
}
|
||||
const matchWidget = value.content?.match(reWidget)
|
||||
if (matchWidget) {
|
||||
let match = matchWidget[1]
|
||||
extraInfo?.inputs?.find((i: INodeInputSlot) => {
|
||||
if (i.name != match) return false
|
||||
match = i.label ? i.label : i.name
|
||||
})
|
||||
extraInfo?.widgets?.find((i: IWidget) => {
|
||||
if (i.name != match) return false
|
||||
match = i.label ? i.label : i.name
|
||||
})
|
||||
value.content = cvt + match + twgt
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OriginalContextMenu = LiteGraph.ContextMenu
|
||||
function ContextMenu(
|
||||
values: (IContextMenuValue | string)[],
|
||||
options: IContextMenuOptions
|
||||
) {
|
||||
if (options.title) {
|
||||
options.title = st(
|
||||
`nodeDefs.${normalizeI18nKey(options.title)}.display_name`,
|
||||
options.title
|
||||
)
|
||||
}
|
||||
translateMenus(values, options)
|
||||
const ctx = new OriginalContextMenu(values, options)
|
||||
return ctx
|
||||
}
|
||||
|
||||
LiteGraph.ContextMenu = ContextMenu as unknown as typeof LiteGraph.ContextMenu
|
||||
}
|
||||
130
src/hooks/workflowPersistenceHooks.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { computed, watch, watchEffect } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
|
||||
export function useWorkflowPersistence() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const persistCurrentWorkflow = () => {
|
||||
const workflow = JSON.stringify(comfyApp.serializeGraph())
|
||||
localStorage.setItem('workflow', workflow)
|
||||
if (api.clientId) {
|
||||
sessionStorage.setItem(`workflow:${api.clientId}`, workflow)
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorkflowFromStorage = async (
|
||||
json: string | null,
|
||||
workflowName: string | null
|
||||
) => {
|
||||
if (!json) return false
|
||||
const workflow = JSON.parse(json)
|
||||
await comfyApp.loadGraphData(workflow, true, true, workflowName)
|
||||
return true
|
||||
}
|
||||
|
||||
const loadPreviousWorkflowFromStorage = async () => {
|
||||
const workflowName = getStorageValue('Comfy.PreviousWorkflow')
|
||||
const clientId = api.initialClientId ?? api.clientId
|
||||
|
||||
// Try loading from session storage first
|
||||
if (clientId) {
|
||||
const sessionWorkflow = sessionStorage.getItem(`workflow:${clientId}`)
|
||||
if (await loadWorkflowFromStorage(sessionWorkflow, workflowName)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to local storage
|
||||
const localWorkflow = localStorage.getItem('workflow')
|
||||
return await loadWorkflowFromStorage(localWorkflow, workflowName)
|
||||
}
|
||||
|
||||
const loadDefaultWorkflow = async () => {
|
||||
if (!settingStore.get('Comfy.TutorialCompleted')) {
|
||||
await settingStore.set('Comfy.TutorialCompleted', true)
|
||||
await useModelStore().loadModelFolders()
|
||||
await useWorkflowService().loadTutorialWorkflow()
|
||||
} else {
|
||||
await comfyApp.loadGraphData()
|
||||
}
|
||||
}
|
||||
|
||||
const restorePreviousWorkflow = async () => {
|
||||
try {
|
||||
const restored = await loadPreviousWorkflowFromStorage()
|
||||
if (!restored) {
|
||||
await loadDefaultWorkflow()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading previous workflow', err)
|
||||
await loadDefaultWorkflow()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup watchers
|
||||
watchEffect(() => {
|
||||
if (workflowStore.activeWorkflow) {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
setStorageValue('Comfy.PreviousWorkflow', workflow.key)
|
||||
// When the activeWorkflow changes, the graph has already been loaded.
|
||||
// Saving the current state of the graph to the localStorage.
|
||||
persistCurrentWorkflow()
|
||||
}
|
||||
})
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Restore workflow tabs states
|
||||
const openWorkflows = computed(() => workflowStore.openWorkflows)
|
||||
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(
|
||||
() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
}
|
||||
)
|
||||
|
||||
// Get storage values before setting watchers
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
|
||||
const restoreWorkflowTabsState = () => {
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable) {
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restorePreviousWorkflow,
|
||||
restoreWorkflowTabsState
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,8 @@
|
||||
"workflow": "Workflow",
|
||||
"success": "Success",
|
||||
"ok": "OK",
|
||||
"feedback": "Feedback"
|
||||
"feedback": "Feedback",
|
||||
"continue": "Continue"
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (Optional)",
|
||||
@@ -100,6 +101,39 @@
|
||||
"yellow": "Yellow",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"contextMenu": {
|
||||
"Inputs": "Inputs",
|
||||
"Outputs": "Outputs",
|
||||
"Properties": "Properties",
|
||||
"Properties Panel": "Properties Panel",
|
||||
"Title": "Title",
|
||||
"Mode": "Mode",
|
||||
"Resize": "Resize",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Pin": "Pin",
|
||||
"Unpin": "Unpin",
|
||||
"Clone": "Clone",
|
||||
"Remove": "Remove",
|
||||
"Colors": "Colors",
|
||||
"Shapes": "Shapes",
|
||||
"Bypass": "Bypass",
|
||||
"Copy (Clipspace)": "Copy (Clipspace)",
|
||||
"Convert Widget to Input": "Convert Widget to Input",
|
||||
"Convert Input to Widget": "Convert Input to Widget",
|
||||
"Add Node": "Add Node",
|
||||
"Add Group": "Add Group",
|
||||
"Convert to Group Node": "Convert to Group Node",
|
||||
"Manage Group Nodes": "Manage Group Nodes",
|
||||
"Add Group For Selected Nodes": "Add Group For Selected Nodes",
|
||||
"Save Selected as Template": "Save Selected as Template",
|
||||
"Node Templates": "Node Templates",
|
||||
"Manage": "Manage",
|
||||
"Convert ": "Convert ",
|
||||
" to input": " to input",
|
||||
" to widget": " to widget",
|
||||
"Search": "Search"
|
||||
},
|
||||
"icon": {
|
||||
"bookmark": "Bookmark",
|
||||
"folder": "Folder",
|
||||
@@ -212,7 +246,13 @@
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
},
|
||||
"viewFullPolicy": "View full policy"
|
||||
}
|
||||
},
|
||||
"pythonMirrorPlaceholder": "Enter Python mirror URL",
|
||||
"pypiMirrorPlaceholder": "Enter PyPI mirror URL",
|
||||
"checkingMirrors": "Checking network access to python mirrors...",
|
||||
"mirrorsReachable": "Network access to python mirrors is good",
|
||||
"mirrorsUnreachable": "Network access to some python mirrors is bad",
|
||||
"mirrorSettings": "Mirror Settings"
|
||||
},
|
||||
"customNodes": "Custom Nodes",
|
||||
"customNodesDescription": "Reinstall custom nodes from existing ComfyUI installations.",
|
||||
@@ -471,7 +511,9 @@
|
||||
"Server-Config": "Server-Config",
|
||||
"About": "About",
|
||||
"EditTokenWeight": "Edit Token Weight",
|
||||
"CustomColorPalettes": "Custom Color Palettes"
|
||||
"CustomColorPalettes": "Custom Color Palettes",
|
||||
"UV": "UV",
|
||||
"ContextMenu": "Context Menu"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -694,6 +736,7 @@
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Maintenance",
|
||||
"allOk": "No issues were detected.",
|
||||
"status": "Status",
|
||||
"detected": "Detected",
|
||||
@@ -703,10 +746,18 @@
|
||||
"Skipped": "Skipped",
|
||||
"showManual": "Show maintenance tasks",
|
||||
"confirmTitle": "Are you sure?",
|
||||
"terminalDefaultMessage": "When you run a troubleshooting command, any output will be shown here.",
|
||||
"consoleLogs": "Console Logs",
|
||||
"error": {
|
||||
"toastTitle": "Task error",
|
||||
"taskFailed": "Task failed to run.",
|
||||
"cannotContinue": "Unable to continue - errors remain",
|
||||
"defaultDescription": "An error occurred while running a maintenance task."
|
||||
}
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Don't show this again",
|
||||
"missingModels": "Missing Models",
|
||||
"missingModelsMessage": "When loading the graph, the following models were not found"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Send anonymous usage metrics"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi Install Mirror",
|
||||
"tooltip": "Default pip install mirror"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python Install Mirror",
|
||||
"tooltip": "Managed Python installations are downloaded from the Astral python-build-standalone project. This variable can be set to a mirror URL to use a different source for Python installations. The provided URL will replace https://github.com/astral-sh/python-build-standalone/releases/download in, e.g., https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Distributions can be read from a local directory by using the file:// URL scheme."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch Install Mirror",
|
||||
"tooltip": "Pip install mirror for pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Window Style",
|
||||
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "Topbar (2nd-row)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Low quality rendering zoom threshold",
|
||||
"tooltip": "Render low quality shapes when zoomed out"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Maxium FPS",
|
||||
"name": "Maximum FPS",
|
||||
"tooltip": "The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Scale node combo widget menus (lists) when zoomed in"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Tooltip Delay"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Always snap to grid"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
"red": "Rouge",
|
||||
"yellow": "Jaune"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " en entrée",
|
||||
" to widget": " en widget",
|
||||
"Add Group": "Ajouter un Groupe",
|
||||
"Add Group For Selected Nodes": "Ajouter un Groupe pour les Nœuds Sélectionnés",
|
||||
"Add Node": "Ajouter un Nœud",
|
||||
"Bypass": "Contourner",
|
||||
"Clone": "Cloner",
|
||||
"Collapse": "Réduire",
|
||||
"Colors": "Couleurs",
|
||||
"Convert ": "Convertir ",
|
||||
"Convert Input to Widget": "Convertir l'Entrée en Widget",
|
||||
"Convert Widget to Input": "Convertir le Widget en Entrée",
|
||||
"Convert to Group Node": "Convertir en Nœud de Groupe",
|
||||
"Copy (Clipspace)": "Copier (Clipspace)",
|
||||
"Expand": "Développer",
|
||||
"Inputs": "Entrées",
|
||||
"Manage": "Gérer",
|
||||
"Manage Group Nodes": "Gérer les Nœuds de Groupe",
|
||||
"Mode": "Mode",
|
||||
"Node Templates": "Modèles de Nœuds",
|
||||
"Outputs": "Sorties",
|
||||
"Pin": "Épingler",
|
||||
"Properties": "Propriétés",
|
||||
"Properties Panel": "Panneau des Propriétés",
|
||||
"Remove": "Supprimer",
|
||||
"Resize": "Redimensionner",
|
||||
"Save Selected as Template": "Enregistrer la Sélection comme Modèle",
|
||||
"Search": "Recherche",
|
||||
"Shapes": "Formes",
|
||||
"Title": "Titre",
|
||||
"Unpin": "Désépingler"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "AUDIO",
|
||||
"BOOLEAN": "BOOLEAN",
|
||||
@@ -74,6 +107,7 @@
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"confirm": "Confirmer",
|
||||
"continue": "Continuer",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"currentUser": "Utilisateur actuel",
|
||||
"customize": "Personnaliser",
|
||||
@@ -220,6 +254,7 @@
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des métriques d'utilisation anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté.",
|
||||
"autoUpdate": "Mises à jour automatiques",
|
||||
"autoUpdateDescription": "Téléchargez et installez automatiquement les mises à jour lorsqu'elles deviennent disponibles. Vous serez toujours informé avant l'installation des mises à jour.",
|
||||
"checkingMirrors": "Vérification de l'accès réseau aux miroirs python...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
@@ -239,7 +274,12 @@
|
||||
},
|
||||
"errorUpdatingConsent": "Erreur de mise à jour du consentement",
|
||||
"errorUpdatingConsentDetail": "Échec de la mise à jour des paramètres de consentement aux métriques",
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données"
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données",
|
||||
"mirrorSettings": "Paramètres du Miroir",
|
||||
"mirrorsReachable": "L'accès réseau aux miroirs python est bon",
|
||||
"mirrorsUnreachable": "L'accès au réseau à certains miroirs python est mauvais",
|
||||
"pypiMirrorPlaceholder": "Entrez l'URL du miroir PyPI",
|
||||
"pythonMirrorPlaceholder": "Entrez l'URL du miroir Python"
|
||||
},
|
||||
"systemLocations": "Emplacements système",
|
||||
"unhandledError": "Erreur inconnue",
|
||||
@@ -267,15 +307,19 @@
|
||||
"Skipped": "Ignoré",
|
||||
"allOk": "Aucun problème détecté.",
|
||||
"confirmTitle": "Êtes-vous sûr ?",
|
||||
"consoleLogs": "Journaux de la console",
|
||||
"detected": "Détecté",
|
||||
"error": {
|
||||
"cannotContinue": "Impossible de continuer - des erreurs subsistent",
|
||||
"defaultDescription": "Une erreur s'est produite lors de l'exécution d'une tâche de maintenance.",
|
||||
"taskFailed": "La tâche a échoué.",
|
||||
"toastTitle": "Erreur de tâche"
|
||||
},
|
||||
"refreshing": "Actualisation",
|
||||
"showManual": "Afficher les tâches de maintenance",
|
||||
"status": "Statut"
|
||||
"status": "Statut",
|
||||
"terminalDefaultMessage": "Lorsque vous exécutez une commande de dépannage, toute sortie sera affichée ici.",
|
||||
"title": "Maintenance"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
@@ -370,6 +414,11 @@
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
"missingModels": "Modèles manquants",
|
||||
"missingModelsMessage": "Lors du chargement du graphique, les modèles suivants n'ont pas été trouvés"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "modèles_3d",
|
||||
@@ -598,6 +647,7 @@
|
||||
"ColorPalette": "Palette de Couleurs",
|
||||
"Comfy": "Confort",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "Menu Contextuel",
|
||||
"CustomColorPalettes": "Palettes de Couleurs Personnalisées",
|
||||
"DevMode": "Mode Développeur",
|
||||
"EditTokenWeight": "Modifier le Poids du Jeton",
|
||||
@@ -628,6 +678,7 @@
|
||||
"Settings": "Paramètres",
|
||||
"Sidebar": "Barre Latérale",
|
||||
"Tree Explorer": "Explorateur d'Arbre",
|
||||
"UV": "UV",
|
||||
"Validation": "Validation",
|
||||
"Window": "Fenêtre",
|
||||
"Workflow": "Flux de Travail"
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Envoyer des métriques d'utilisation anonymes"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Miroir d'installation Pypi",
|
||||
"tooltip": "Miroir d'installation pip par défaut"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Miroir d'installation Python",
|
||||
"tooltip": "Les installations Python gérées sont téléchargées depuis le projet Astral python-build-standalone. Cette variable peut être définie sur une URL de miroir pour utiliser une source différente pour les installations Python. L'URL fournie remplacera https://github.com/astral-sh/python-build-standalone/releases/download dans, par exemple, https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Les distributions peuvent être lues à partir d'un répertoire local en utilisant le schéma d'URL file://."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Miroir d'installation Torch",
|
||||
"tooltip": "Miroir d'installation Pip pour pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Style de fenêtre",
|
||||
"options": {
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "Barre supérieure (2ème rangée)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Seuil de zoom pour le rendu de faible qualité",
|
||||
"tooltip": "Rendre des formes de faible qualité lorsqu'on est dézoomé"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "FPS maximum",
|
||||
"tooltip": "Le nombre maximum d'images par seconde que le canevas est autorisé à rendre. Limite l'utilisation du GPU au détriment de la fluidité. Si 0, le taux de rafraîchissement de l'écran est utilisé. Par défaut : 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Mise à l'échelle des menus de widgets combinés de nœuds (listes) lors du zoom"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Délai d'infobulle"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Toujours aligner sur la grille"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
"red": "赤",
|
||||
"yellow": "黄色"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 入力へ",
|
||||
" to widget": " ウィジェットへ",
|
||||
"Add Group": "グループを追加",
|
||||
"Add Group For Selected Nodes": "選択したノードのグループを追加",
|
||||
"Add Node": "ノードを追加",
|
||||
"Bypass": "バイパス",
|
||||
"Clone": "クローン",
|
||||
"Collapse": "折りたたむ",
|
||||
"Colors": "色",
|
||||
"Convert ": "変換 ",
|
||||
"Convert Input to Widget": "入力をウィジェットに変換",
|
||||
"Convert Widget to Input": "ウィジェットを入力に変換",
|
||||
"Convert to Group Node": "グループノードに変換",
|
||||
"Copy (Clipspace)": "コピー (Clipspace)",
|
||||
"Expand": "展開",
|
||||
"Inputs": "入力",
|
||||
"Manage": "管理",
|
||||
"Manage Group Nodes": "グループノードを管理",
|
||||
"Mode": "モード",
|
||||
"Node Templates": "ノードテンプレート",
|
||||
"Outputs": "出力",
|
||||
"Pin": "ピン",
|
||||
"Properties": "プロパティ",
|
||||
"Properties Panel": "プロパティパネル",
|
||||
"Remove": "削除",
|
||||
"Resize": "リサイズ",
|
||||
"Save Selected as Template": "選択したものをテンプレートとして保存",
|
||||
"Search": "検索",
|
||||
"Shapes": "形",
|
||||
"Title": "タイトル",
|
||||
"Unpin": "ピン解除"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "オーディオ",
|
||||
"BOOLEAN": "ブール",
|
||||
@@ -74,6 +107,7 @@
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"confirm": "確認",
|
||||
"continue": "続ける",
|
||||
"copyToClipboard": "クリップボードにコピー",
|
||||
"currentUser": "現在のユーザー",
|
||||
"customize": "カスタマイズ",
|
||||
@@ -220,6 +254,7 @@
|
||||
"allowMetricsDescription": "匿名の使用状況メトリクスを送信してComfyUIを改善します。個人情報やワークフローの内容は収集されません。",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateDescription": "更新が利用可能になると、自動的にダウンロードおよびインストールを行います。インストール前に通知が表示されます。",
|
||||
"checkingMirrors": "Pythonミラーへのネットワークアクセスを確認中...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
@@ -239,7 +274,12 @@
|
||||
},
|
||||
"errorUpdatingConsent": "同意の更新エラー",
|
||||
"errorUpdatingConsentDetail": "メトリクスの同意設定の更新に失敗しました",
|
||||
"learnMoreAboutData": "データ収集の詳細を見る"
|
||||
"learnMoreAboutData": "データ収集の詳細を見る",
|
||||
"mirrorSettings": "ミラー設定",
|
||||
"mirrorsReachable": "Pythonミラーへのネットワークアクセスは良好です",
|
||||
"mirrorsUnreachable": "一部のpythonミラーへのネットワークアクセスが悪い",
|
||||
"pypiMirrorPlaceholder": "PyPIミラーURLを入力してください",
|
||||
"pythonMirrorPlaceholder": "PythonミラーURLを入力してください"
|
||||
},
|
||||
"systemLocations": "システムの場所",
|
||||
"unhandledError": "未知のエラー",
|
||||
@@ -267,15 +307,19 @@
|
||||
"Skipped": "スキップされました",
|
||||
"allOk": "問題は検出されませんでした。",
|
||||
"confirmTitle": "よろしいですか?",
|
||||
"consoleLogs": "コンソールログ",
|
||||
"detected": "検出されました",
|
||||
"error": {
|
||||
"cannotContinue": "続行できません - エラーが残っています",
|
||||
"defaultDescription": "メンテナンスタスクの実行中にエラーが発生しました。",
|
||||
"taskFailed": "タスクの実行に失敗しました。",
|
||||
"toastTitle": "タスクエラー"
|
||||
},
|
||||
"refreshing": "更新中",
|
||||
"showManual": "メンテナンスタスクを表示",
|
||||
"status": "ステータス"
|
||||
"status": "ステータス",
|
||||
"terminalDefaultMessage": "トラブルシューティングコマンドを実行すると、出力はここに表示されます。",
|
||||
"title": "メンテナンス"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
@@ -370,6 +414,11 @@
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
"missingModels": "モデルが見つかりません",
|
||||
"missingModelsMessage": "グラフを読み込む際に、次のモデルが見つかりませんでした"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "3Dモデル",
|
||||
@@ -598,6 +647,7 @@
|
||||
"ColorPalette": "カラーパレット",
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfyデスクトップ",
|
||||
"ContextMenu": "コンテキストメニュー",
|
||||
"CustomColorPalettes": "カスタムカラーパレット",
|
||||
"DevMode": "開発モード",
|
||||
"EditTokenWeight": "トークンの重みを編集",
|
||||
@@ -628,6 +678,7 @@
|
||||
"Settings": "設定",
|
||||
"Sidebar": "サイドバー",
|
||||
"Tree Explorer": "ツリーエクスプローラー",
|
||||
"UV": "UV",
|
||||
"Validation": "検証",
|
||||
"Window": "ウィンドウ",
|
||||
"Workflow": "ワークフロー"
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "匿名の使用統計を送信する"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi インストールミラー",
|
||||
"tooltip": "デフォルトの pip インストールミラー"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python Install Mirror",
|
||||
"tooltip": "管理されたPythonのインストールは、Astral python-build-standaloneプロジェクトからダウンロードされます。この変数は、Pythonのインストールのための異なるソースを使用するためのミラーURLに設定することができます。提供されたURLは、例えば、https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gzの中でhttps://github.com/astral-sh/python-build-standalone/releases/downloadを置き換えます。ディストリビューションは、file:// URLスキームを使用してローカルディレクトリから読み取ることができます。"
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch Install Mirror",
|
||||
"tooltip": "pytorchのpipインストールミラー"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "ウィンドウスタイル",
|
||||
"options": {
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "トップバー(2行目)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低品質レンダリングズーム閾値",
|
||||
"tooltip": "ズームアウト時に低品質の形状をレンダリングする"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "キャンバスがレンダリングできる最大フレーム数です。スムーズさの代わりにGPU使用量を制限します。0の場合、画面のリフレッシュレートが使用されます。デフォルト:0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "ズームイン時にノードコンボウィジェットメニュー(リスト)をスケーリングする"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "ツールチップ遅延"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "常にグリッドにスナップ"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
"red": "빨간색",
|
||||
"yellow": "노란색"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 입력으로",
|
||||
" to widget": " 위젯으로",
|
||||
"Add Group": "그룹 추가",
|
||||
"Add Group For Selected Nodes": "선택된 노드에 대한 그룹 추가",
|
||||
"Add Node": "노드 추가",
|
||||
"Bypass": "우회",
|
||||
"Clone": "복제",
|
||||
"Collapse": "축소",
|
||||
"Colors": "색상",
|
||||
"Convert ": "변환 ",
|
||||
"Convert Input to Widget": "입력을 위젯으로 변환",
|
||||
"Convert Widget to Input": "위젯을 입력으로 변환",
|
||||
"Convert to Group Node": "그룹 노드로 변환",
|
||||
"Copy (Clipspace)": "복사 (Clipspace)",
|
||||
"Expand": "확장",
|
||||
"Inputs": "입력",
|
||||
"Manage": "관리",
|
||||
"Manage Group Nodes": "그룹 노드 관리",
|
||||
"Mode": "모드",
|
||||
"Node Templates": "노드 템플릿",
|
||||
"Outputs": "출력",
|
||||
"Pin": "고정",
|
||||
"Properties": "속성",
|
||||
"Properties Panel": "속성 패널",
|
||||
"Remove": "제거",
|
||||
"Resize": "크기 조정",
|
||||
"Save Selected as Template": "선택 항목을 템플릿으로 저장",
|
||||
"Search": "검색",
|
||||
"Shapes": "형태",
|
||||
"Title": "제목",
|
||||
"Unpin": "고정 해제"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "오디오",
|
||||
"BOOLEAN": "논리값",
|
||||
@@ -74,6 +107,7 @@
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"confirm": "확인",
|
||||
"continue": "계속",
|
||||
"copyToClipboard": "클립보드에 복사",
|
||||
"currentUser": "현재 사용자",
|
||||
"customize": "사용자 정의",
|
||||
@@ -180,11 +214,11 @@
|
||||
"cpuMode": "CPU 모드",
|
||||
"cpuModeDescription": "CPU 모드는 개발자와 드문 경우에만 사용됩니다.",
|
||||
"cpuModeDescription2": "이것이 필요한지 확실하지 않다면, 이 상자를 무시하고 위에서 GPU를 선택하세요.",
|
||||
"customComfyNeedsPython": "파이썬이 설정되지 않으면 ComfyUI가 작동하지 않습니다",
|
||||
"customComfyNeedsPython": "Python이 설정되지 않으면 ComfyUI가 작동하지 않습니다",
|
||||
"customInstallRequirements": "모든 요구 사항과 종속성 설치 (예: 사용자 정의 torch)",
|
||||
"customManualVenv": "파이썬 venv를 수동으로 구성",
|
||||
"customManualVenv": "Python venv를 수동으로 구성",
|
||||
"customMayNotWork": "이것은 전혀 지원되지 않으며, 작동하지 않을 수 있습니다",
|
||||
"customSkipsPython": "이 옵션은 일반 파이썬 설정을 건너뜁니다.",
|
||||
"customSkipsPython": "이 옵션은 일반 Python 설정을 건너뜁니다.",
|
||||
"enableCpuMode": "CPU 모드 활성화",
|
||||
"mpsDescription": "Apple Metal Performance Shaders는 pytorch nightly를 사용하여 지원됩니다.",
|
||||
"nvidiaDescription": "NVIDIA 장치는 pytorch CUDA 빌드를 사용하여 직접 지원됩니다.",
|
||||
@@ -220,6 +254,7 @@
|
||||
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
|
||||
"autoUpdate": "자동 업데이트",
|
||||
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
|
||||
"checkingMirrors": "Python 미러에 대한 네트워크 액세스 확인 중...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
@@ -239,7 +274,12 @@
|
||||
},
|
||||
"errorUpdatingConsent": "데이터 수집 동의 설정 업데이트 오류",
|
||||
"errorUpdatingConsentDetail": "데이터 수집 동의 설정 업데이트에 실패했습니다",
|
||||
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
|
||||
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기",
|
||||
"mirrorSettings": "미러 URL 설정",
|
||||
"mirrorsReachable": "Python 미러에 대한 네트워크 액세스가 좋습니다",
|
||||
"mirrorsUnreachable": "일부 Python 미러에 대한 네트워크 접근이 나쁩니다",
|
||||
"pypiMirrorPlaceholder": "PyPI 미러 URL 입력",
|
||||
"pythonMirrorPlaceholder": "Python 미러 URL 입력"
|
||||
},
|
||||
"systemLocations": "시스템 위치",
|
||||
"unhandledError": "알 수 없는 오류",
|
||||
@@ -267,15 +307,19 @@
|
||||
"Skipped": "건너뜀",
|
||||
"allOk": "문제가 발견되지 않았습니다.",
|
||||
"confirmTitle": "확실합니까?",
|
||||
"consoleLogs": "콘솔 로그",
|
||||
"detected": "감지됨",
|
||||
"error": {
|
||||
"cannotContinue": "계속할 수 없습니다 - 오류가 남아 있습니다",
|
||||
"defaultDescription": "유지 보수 작업을 실행하는 동안 오류가 발생했습니다.",
|
||||
"taskFailed": "작업 실행에 실패했습니다.",
|
||||
"toastTitle": "작업 오류"
|
||||
},
|
||||
"refreshing": "새로 고침 중",
|
||||
"showManual": "유지 보수 작업 보기",
|
||||
"status": "상태"
|
||||
"status": "상태",
|
||||
"terminalDefaultMessage": "문제 해결 명령을 실행하면 출력이 여기에 표시됩니다.",
|
||||
"title": "유지 보수"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 큐",
|
||||
@@ -370,6 +414,11 @@
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
"missingModels": "모델이 없습니다",
|
||||
"missingModelsMessage": "그래프를 로드할 때 다음 모델을 찾을 수 없었습니다"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "3D 모델",
|
||||
@@ -582,7 +631,7 @@
|
||||
"process": {
|
||||
"error": "ComfyUI Desktop을 시작할 수 없습니다",
|
||||
"initial-state": "로딩 중...",
|
||||
"python-setup": "파이썬 환경 설정 중...",
|
||||
"python-setup": "Python 환경 설정 중...",
|
||||
"ready": "마무리 중...",
|
||||
"starting-server": "ComfyUI 서버 시작 중..."
|
||||
},
|
||||
@@ -598,6 +647,7 @@
|
||||
"ColorPalette": "색상 팔레트",
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
"ContextMenu": "컨텍스트 메뉴",
|
||||
"CustomColorPalettes": "사용자 정의 색상 팔레트",
|
||||
"DevMode": "개발자 모드",
|
||||
"EditTokenWeight": "토큰 가중치 편집",
|
||||
@@ -628,6 +678,7 @@
|
||||
"Settings": "설정",
|
||||
"Sidebar": "사이드바",
|
||||
"Tree Explorer": "트리 탐색기",
|
||||
"UV": "UV",
|
||||
"Validation": "검증",
|
||||
"Window": "창",
|
||||
"Workflow": "워크플로"
|
||||
|
||||
@@ -672,7 +672,7 @@
|
||||
}
|
||||
},
|
||||
"ConditioningStableAudio": {
|
||||
"display_name": "ConditioningStableAudio",
|
||||
"display_name": "Stable Audio 조건 설정",
|
||||
"inputs": {
|
||||
"negative": {
|
||||
"name": "부정 조건"
|
||||
@@ -716,7 +716,7 @@
|
||||
}
|
||||
},
|
||||
"ConditioningZeroOut": {
|
||||
"display_name": "조건 (제로 아웃)",
|
||||
"display_name": "조건 (0으로 출력)",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
"name": "조건"
|
||||
@@ -908,7 +908,7 @@
|
||||
}
|
||||
},
|
||||
"CreateHookKeyframesFromFloats": {
|
||||
"display_name": "부동 소수점으로 후크 키프레임 생성",
|
||||
"display_name": "실수로 후크 키프레임 생성",
|
||||
"inputs": {
|
||||
"end_percent": {
|
||||
"name": "종료 퍼센트"
|
||||
@@ -1163,11 +1163,11 @@
|
||||
}
|
||||
},
|
||||
"DevToolsObjectPatchNode": {
|
||||
"description": "오브젝트 패치를 적용하는 노드",
|
||||
"display_name": "오브젝트 패치 노드",
|
||||
"description": "객체 패치를 적용하는 노드",
|
||||
"display_name": "객체 패치 노드",
|
||||
"inputs": {
|
||||
"dummy_float": {
|
||||
"name": "더미 플로트"
|
||||
"name": "더미 실수"
|
||||
},
|
||||
"model": {
|
||||
"name": "모델"
|
||||
@@ -1257,7 +1257,7 @@
|
||||
}
|
||||
},
|
||||
"EmptyCosmosLatentVideo": {
|
||||
"display_name": "EmptyCosmosLatentVideo",
|
||||
"display_name": "빈 잠재 비디오 (Cosmos)",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
"name": "배치 크기"
|
||||
@@ -1940,7 +1940,7 @@
|
||||
"tooltip": "Classifier-Free Guidance 스케일은 창의성과 프롬프트 준수를 균형 있게 조절합니다. 값이 높을수록 프롬프트와 더 밀접하게 일치하는 이미지가 생성되지만, 너무 높은 값은 품질에 부정적인 영향을 미칠 수 있습니다."
|
||||
},
|
||||
"denoise": {
|
||||
"name": "노이즈_제거",
|
||||
"name": "노이즈 제거양",
|
||||
"tooltip": "적용되는 노이즈 제거의 양으로, 낮은 값은 초기 이미지의 구조를 유지하여 이미지 간 샘플링을 가능하게 합니다."
|
||||
},
|
||||
"latent_image": {
|
||||
@@ -2581,7 +2581,7 @@
|
||||
"tooltip": "LoRA가 적용될 확산 모델입니다."
|
||||
},
|
||||
"strength_clip": {
|
||||
"name": "클립 강도",
|
||||
"name": "clip 강도",
|
||||
"tooltip": "CLIP 모델을 적용하는 강도입니다. 이 값은 음수가 될 수 있습니다."
|
||||
},
|
||||
"strength_model": {
|
||||
@@ -4404,7 +4404,7 @@
|
||||
}
|
||||
},
|
||||
"RescaleCFG": {
|
||||
"display_name": "CFG 크기 재조정",
|
||||
"display_name": "CFG 리스케일",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "모델"
|
||||
@@ -4855,7 +4855,7 @@
|
||||
"display_name": "CLIP 후크 설정",
|
||||
"inputs": {
|
||||
"apply_to_conds": {
|
||||
"name": "조건에_적용"
|
||||
"name": "조건에 적용"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
@@ -4864,7 +4864,7 @@
|
||||
"name": "후크"
|
||||
},
|
||||
"schedule_clip": {
|
||||
"name": "스케줄_클립"
|
||||
"name": "clip 스케쥴 사용"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4913,8 +4913,8 @@
|
||||
}
|
||||
},
|
||||
"SkipLayerGuidanceDiT": {
|
||||
"description": "모든 DiT 모델에서 사용할 수 있는 SkipLayerGuidance 노드의 범용 버전입니다.",
|
||||
"display_name": "SkipLayerGuidanceDiT",
|
||||
"description": "모든 DiT 모델에서 사용할 수 있는 '레이어 건너뛰기 가이던스' 노드의 범용 버전입니다.",
|
||||
"display_name": "레이어 건너뛰기 가이던스 (DiT)",
|
||||
"inputs": {
|
||||
"double_layers": {
|
||||
"name": "double_layers"
|
||||
@@ -4926,13 +4926,13 @@
|
||||
"name": "모델"
|
||||
},
|
||||
"rescaling_scale": {
|
||||
"name": "크기 재조정 크기"
|
||||
"name": "리스케일 크기"
|
||||
},
|
||||
"scale": {
|
||||
"name": "크기"
|
||||
},
|
||||
"single_layers": {
|
||||
"name": "단일_레이어"
|
||||
"name": "single_layers"
|
||||
},
|
||||
"start_percent": {
|
||||
"name": "시작 퍼센트"
|
||||
@@ -4940,14 +4940,14 @@
|
||||
}
|
||||
},
|
||||
"SkipLayerGuidanceSD3": {
|
||||
"description": "모든 DiT 모델에서 사용할 수 있는 SkipLayerGuidance 노드의 범용 버전입니다.",
|
||||
"display_name": "SkipLayerGuidanceSD3",
|
||||
"description": "SD3 용 레이어 건너뛰기 가이던스 노드입니다.",
|
||||
"display_name": "레이어 건너뛰기 가이던스 (SD3)",
|
||||
"inputs": {
|
||||
"end_percent": {
|
||||
"name": "종료 퍼센트"
|
||||
},
|
||||
"layers": {
|
||||
"name": "레이어"
|
||||
"name": "layers"
|
||||
},
|
||||
"model": {
|
||||
"name": "모델"
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "익명 사용 통계 보내기"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "PyPI 설치 미러",
|
||||
"tooltip": "기본 pip 설치 미러"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python 설치 미러",
|
||||
"tooltip": "관리되는 Python 설치파일은 Astral python-build-standalone 프로젝트에서 다운로드됩니다. 이 변수는 Python 설치파일의 다른 출처를 사용하기 위해 미러 URL로 설정할 수 있습니다. 예를 들어, 제공된 URL은 https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz에서 https://github.com/astral-sh/python-build-standalone/releases/download 를 대체합니다. 배포판은 file:// URL 스키마를 사용하여 로컬 디렉토리에서 읽을 수 있습니다."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "torch 설치 미러",
|
||||
"tooltip": "pytorch를 위한 pip 설치 미러"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "창 스타일",
|
||||
"options": {
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "상단바 (2번째 행)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "저품질 렌더링 줌 임계값",
|
||||
"tooltip": "줌 아웃시 저품질 도형 렌더링"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "최대 FPS",
|
||||
"tooltip": "캔버스가 렌더링할 수 있는 최대 프레임 수입니다. 부드럽게 동작하도록 GPU 사용률을 제한 합니다. 0이면 화면 주사율로 작동 합니다. 기본값: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "확대시 노드 콤보 위젯 메뉴 (목록) 스케일링"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "툴팁 지연"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "항상 그리드에 스냅"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
"red": "Красный",
|
||||
"yellow": "Жёлтый"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " во вход",
|
||||
" to widget": " в виджет",
|
||||
"Add Group": "Добавить группу",
|
||||
"Add Group For Selected Nodes": "Добавить группу для выбранных узлов",
|
||||
"Add Node": "Добавить узел",
|
||||
"Bypass": "Обход",
|
||||
"Clone": "Клонировать",
|
||||
"Collapse": "Свернуть",
|
||||
"Colors": "Цвета",
|
||||
"Convert ": "Преобразовать ",
|
||||
"Convert Input to Widget": "Преобразовать вход в виджет",
|
||||
"Convert Widget to Input": "Преобразовать виджет во вход",
|
||||
"Convert to Group Node": "Преобразовать в групповой узел",
|
||||
"Copy (Clipspace)": "Копировать (Clipspace)",
|
||||
"Expand": "Развернуть",
|
||||
"Inputs": "Входы",
|
||||
"Manage": "Управлять",
|
||||
"Manage Group Nodes": "Управление групповыми узлами",
|
||||
"Mode": "Режим",
|
||||
"Node Templates": "Шаблоны узлов",
|
||||
"Outputs": "Выходы",
|
||||
"Pin": "Закрепить",
|
||||
"Properties": "Свойства",
|
||||
"Properties Panel": "Панель свойств",
|
||||
"Remove": "Удалить",
|
||||
"Resize": "Изменить размер",
|
||||
"Save Selected as Template": "Сохранить выбранное как шаблон",
|
||||
"Search": "Поиск",
|
||||
"Shapes": "Формы",
|
||||
"Title": "Заголовок",
|
||||
"Unpin": "Открепить"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "АУДИО",
|
||||
"BOOLEAN": "БУЛЕВО",
|
||||
@@ -74,6 +107,7 @@
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"confirm": "Подтвердить",
|
||||
"continue": "Продолжить",
|
||||
"copyToClipboard": "Скопировать в буфер обмена",
|
||||
"currentUser": "Текущий пользователь",
|
||||
"customize": "Настроить",
|
||||
@@ -220,6 +254,7 @@
|
||||
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные метрики использования. Личная информация или содержание рабочего процесса не будут собираться.",
|
||||
"autoUpdate": "Автоматические обновления",
|
||||
"autoUpdateDescription": "Автоматически загружать и устанавливать обновления, когда они становятся доступными. Вы всегда будете уведомлены перед установкой обновлений.",
|
||||
"checkingMirrors": "Проверка доступа к зеркалам python по сети...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "Сообщение об ошибке и трассировка стека",
|
||||
@@ -239,7 +274,12 @@
|
||||
},
|
||||
"errorUpdatingConsent": "Ошибка обновления согласия",
|
||||
"errorUpdatingConsentDetail": "Не удалось обновить настройки согласия на метрики",
|
||||
"learnMoreAboutData": "Узнать больше о сборе данных"
|
||||
"learnMoreAboutData": "Узнать больше о сборе данных",
|
||||
"mirrorSettings": "Настройки зеркала",
|
||||
"mirrorsReachable": "Сетевой доступ к зеркалам python хороший",
|
||||
"mirrorsUnreachable": "Сетевой доступ к некоторым зеркалам python плохой",
|
||||
"pypiMirrorPlaceholder": "Введите URL-зеркало PyPI",
|
||||
"pythonMirrorPlaceholder": "Введите URL-зеркало Python"
|
||||
},
|
||||
"systemLocations": "Системные места",
|
||||
"unhandledError": "Неизвестная ошибка",
|
||||
@@ -267,15 +307,19 @@
|
||||
"Skipped": "Пропущено",
|
||||
"allOk": "Проблем не обнаружено.",
|
||||
"confirmTitle": "Вы уверены?",
|
||||
"consoleLogs": "Консольные журналы",
|
||||
"detected": "Обнаружено",
|
||||
"error": {
|
||||
"cannotContinue": "Невозможно продолжить - остались ошибки",
|
||||
"defaultDescription": "Произошла ошибка при выполнении задачи по обслуживанию.",
|
||||
"taskFailed": "Не удалось выполнить задачу.",
|
||||
"toastTitle": "Ошибка задачи"
|
||||
},
|
||||
"refreshing": "Обновление",
|
||||
"showManual": "Показать задачи по обслуживанию",
|
||||
"status": "Статус"
|
||||
"status": "Статус",
|
||||
"terminalDefaultMessage": "Когда вы запускаете команду для устранения неполадок, любой вывод будет отображаться здесь.",
|
||||
"title": "Обслуживание"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "Автоочередь",
|
||||
@@ -370,6 +414,11 @@
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
"missingModels": "Отсутствующие модели",
|
||||
"missingModelsMessage": "При загрузке графа следующие модели не были найдены"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "3d_модели",
|
||||
@@ -598,6 +647,7 @@
|
||||
"ColorPalette": "Цветовая палитра",
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Десктопный Comfy",
|
||||
"ContextMenu": "Контекстное меню",
|
||||
"CustomColorPalettes": "Пользовательские цветовые палитры",
|
||||
"DevMode": "Режим разработчика",
|
||||
"EditTokenWeight": "Редактировать вес токена",
|
||||
@@ -628,6 +678,7 @@
|
||||
"Settings": "Настройки",
|
||||
"Sidebar": "Боковая панель",
|
||||
"Tree Explorer": "Дерево проводника",
|
||||
"UV": "UV",
|
||||
"Validation": "Валидация",
|
||||
"Window": "Окно",
|
||||
"Workflow": "Рабочий процесс"
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Отправлять анонимную статистику использования"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi Установить Зеркало",
|
||||
"tooltip": "Зеркало установки pip по умолчанию"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Зеркало для установки Python",
|
||||
"tooltip": "Управляемые установки Python загружаются из проекта Astral python-build-standalone. Эта переменная может быть установлена на URL-адрес зеркала для использования другого источника для установок Python. Предоставленный URL заменит https://github.com/astral-sh/python-build-standalone/releases/download в, например, https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Дистрибутивы могут быть прочитаны из локального каталога, используя схему URL file://."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Установочное Зеркало Torch",
|
||||
"tooltip": "Зеркало для установки pip для pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Стиль окна",
|
||||
"options": {
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "Топбар (2-й ряд)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Порог масштабирования для рендеринга низкого качества",
|
||||
"tooltip": "Рендеринг фигур низкого качества при уменьшении масштаба"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Максимум FPS",
|
||||
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "Масштабирование комбинированных виджетов меню узлов (списков) при увеличении"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "Задержка всплывающей подсказки"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Всегда привязываться к сетке"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
"red": "红色",
|
||||
"yellow": "黄色"
|
||||
},
|
||||
"contextMenu": {
|
||||
" to input": " 为输入",
|
||||
" to widget": " 为控件",
|
||||
"Add Group": "添加组",
|
||||
"Add Group For Selected Nodes": "为选定节点添加组",
|
||||
"Add Node": "添加节点",
|
||||
"Bypass": "绕过",
|
||||
"Clone": "克隆",
|
||||
"Collapse": "折叠",
|
||||
"Colors": "颜色",
|
||||
"Convert ": "转换 ",
|
||||
"Convert Input to Widget": "将输入转换为控件",
|
||||
"Convert Widget to Input": "将控件转换为输入",
|
||||
"Convert to Group Node": "转换为组节点",
|
||||
"Copy (Clipspace)": "复制 (Clipspace)",
|
||||
"Expand": "展开",
|
||||
"Inputs": "输入",
|
||||
"Manage": "管理",
|
||||
"Manage Group Nodes": "管理组节点",
|
||||
"Mode": "模式",
|
||||
"Node Templates": "节点模板",
|
||||
"Outputs": "输出",
|
||||
"Pin": "固定",
|
||||
"Properties": "属性",
|
||||
"Properties Panel": "属性面板",
|
||||
"Remove": "删除",
|
||||
"Resize": "调整大小",
|
||||
"Save Selected as Template": "将选定节点另存为模板",
|
||||
"Search": "搜索",
|
||||
"Shapes": "形状",
|
||||
"Title": "标题",
|
||||
"Unpin": "取消固定"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "音频",
|
||||
"BOOLEAN": "布尔",
|
||||
@@ -74,6 +107,7 @@
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"confirm": "确认",
|
||||
"continue": "继续",
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"currentUser": "当前用户",
|
||||
"customize": "自定义",
|
||||
@@ -220,6 +254,7 @@
|
||||
"allowMetricsDescription": "通过发送匿名使用情况指标来帮助改进ComfyUI。不会收集任何个人信息或工作流内容。",
|
||||
"autoUpdate": "自动更新",
|
||||
"autoUpdateDescription": "更新可用时自动更新。您将在安装更新之前收到通知。",
|
||||
"checkingMirrors": "正在检查到Python镜像的网络访问...",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "错误报告和堆栈跟踪",
|
||||
@@ -239,7 +274,12 @@
|
||||
},
|
||||
"errorUpdatingConsent": "更新同意错误",
|
||||
"errorUpdatingConsentDetail": "无法更新度量同意设置",
|
||||
"learnMoreAboutData": "了解更多关于数据收集的信息"
|
||||
"learnMoreAboutData": "了解更多关于数据收集的信息",
|
||||
"mirrorSettings": "镜像设置",
|
||||
"mirrorsReachable": "到Python镜像的网络访问良好",
|
||||
"mirrorsUnreachable": "对某些python镜像的网络访问不佳",
|
||||
"pypiMirrorPlaceholder": "输入PyPI镜像URL",
|
||||
"pythonMirrorPlaceholder": "输入Python镜像URL"
|
||||
},
|
||||
"systemLocations": "系统位置",
|
||||
"unhandledError": "未知错误",
|
||||
@@ -267,15 +307,19 @@
|
||||
"Skipped": "跳过",
|
||||
"allOk": "未检测到任何问题。",
|
||||
"confirmTitle": "你确定吗?",
|
||||
"consoleLogs": "控制台日志",
|
||||
"detected": "检测到",
|
||||
"error": {
|
||||
"cannotContinue": "无法继续 - 仍有错误",
|
||||
"defaultDescription": "运行维护任务时发生错误。",
|
||||
"taskFailed": "任务运行失败。",
|
||||
"toastTitle": "任务错误"
|
||||
},
|
||||
"refreshing": "刷新中",
|
||||
"showManual": "显示维护任务",
|
||||
"status": "状态"
|
||||
"status": "状态",
|
||||
"terminalDefaultMessage": "当你运行一个故障排除命令时,任何输出都会在这里显示。",
|
||||
"title": "维护"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自动执行",
|
||||
@@ -370,6 +414,11 @@
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
"missingModels": "缺少模型",
|
||||
"missingModelsMessage": "加载图表时,未找到以下模型"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"3d": "3d",
|
||||
"3d_models": "3D模型",
|
||||
@@ -598,6 +647,7 @@
|
||||
"ColorPalette": "色彩主题",
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy桌面版",
|
||||
"ContextMenu": "上下文菜单",
|
||||
"CustomColorPalettes": "自定义色彩主题",
|
||||
"DevMode": "开发模式",
|
||||
"EditTokenWeight": "编辑令牌权重",
|
||||
@@ -628,6 +678,7 @@
|
||||
"Settings": "设置",
|
||||
"Sidebar": "侧边栏",
|
||||
"Tree Explorer": "树形浏览器",
|
||||
"UV": "UV",
|
||||
"Validation": "验证",
|
||||
"Window": "窗口",
|
||||
"Workflow": "工作流"
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "发送匿名使用情况统计"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "Pypi 安装镜像",
|
||||
"tooltip": "默认 pip 安装镜像"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "Python安装镜像",
|
||||
"tooltip": "管理的Python安装包从Astral python-build-standalone项目下载。此变量可以设置为镜像URL,以使用不同的Python安装源。提供的URL将替换https://github.com/astral-sh/python-build-standalone/releases/download,例如,在https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz中。可以通过使用file:// URL方案从本地目录读取分发包。"
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "Torch安装镜像",
|
||||
"tooltip": "用于pytorch的Pip安装镜像"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "窗口样式",
|
||||
"options": {
|
||||
@@ -305,10 +317,20 @@
|
||||
"Topbar (2nd-row)": "顶部栏 (第二行)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "低质量渲染缩放阈值",
|
||||
"tooltip": "在缩小时渲染低质量形状"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "最大FPS",
|
||||
"tooltip": "画布允许渲染的最大帧数。限制GPU使用以换取流畅度。如果为0,则使用屏幕刷新率。默认值:0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "放大时缩放节点组合部件菜单(列表)"
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "工具提示延迟"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "始终吸附到网格"
|
||||
}
|
||||
|
||||
@@ -877,6 +877,15 @@ export class ComfyApi extends EventTarget {
|
||||
async getFolderPaths(): Promise<Record<string, string[]>> {
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom nodes i18n data from the server.
|
||||
*
|
||||
* @returns The custom nodes i18n data
|
||||
*/
|
||||
async getCustomNodesI18n(): Promise<Record<string, any>> {
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi()
|
||||
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
LGraphCanvas,
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
LiteGraph,
|
||||
strokeShape
|
||||
} from '@comfyorg/litegraph'
|
||||
import { Vector2 } from '@comfyorg/litegraph'
|
||||
import type { Rect, Vector2 } from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { shallowReactive } from 'vue'
|
||||
@@ -422,7 +423,6 @@ export class ComfyApp {
|
||||
this.dragOverNode = null
|
||||
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
|
||||
// If you drag multiple files it will call it multiple times with the same file
|
||||
// @ts-expect-error This is not a standard event. TODO fix it.
|
||||
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
|
||||
return
|
||||
}
|
||||
@@ -462,7 +462,6 @@ export class ComfyApp {
|
||||
this.canvas.adjustMouseEvent(e)
|
||||
const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY)
|
||||
if (node) {
|
||||
// @ts-expect-error This is not a standard event. TODO fix it.
|
||||
if (node.onDragOver && node.onDragOver(e)) {
|
||||
this.dragOverNode = node
|
||||
|
||||
@@ -768,49 +767,19 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
if (color) {
|
||||
const shape =
|
||||
node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.globalAlpha = 0.8
|
||||
ctx.beginPath()
|
||||
if (shape == LiteGraph.BOX_SHAPE)
|
||||
ctx.rect(
|
||||
-6,
|
||||
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT
|
||||
)
|
||||
else if (
|
||||
shape == LiteGraph.ROUND_SHAPE ||
|
||||
(shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)
|
||||
)
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
this.round_radius * 2
|
||||
)
|
||||
else if (shape == LiteGraph.CARD_SHAPE)
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
[this.round_radius * 2, this.round_radius * 2, 2, 2]
|
||||
)
|
||||
else if (shape == LiteGraph.CIRCLE_SHAPE)
|
||||
ctx.arc(
|
||||
size[0] * 0.5,
|
||||
size[1] * 0.5,
|
||||
size[0] * 0.5 + 6,
|
||||
0,
|
||||
Math.PI * 2
|
||||
)
|
||||
ctx.strokeStyle = color
|
||||
ctx.stroke()
|
||||
ctx.strokeStyle = fgcolor
|
||||
ctx.globalAlpha = 1
|
||||
const area: Rect = [
|
||||
0,
|
||||
-LiteGraph.NODE_TITLE_HEIGHT,
|
||||
size[0],
|
||||
size[1] + LiteGraph.NODE_TITLE_HEIGHT
|
||||
]
|
||||
strokeShape(ctx, area, {
|
||||
shape: node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE,
|
||||
thickness: lineWidth,
|
||||
colour: color,
|
||||
title_height: LiteGraph.NODE_TITLE_HEIGHT,
|
||||
collapsed: node.collapsed
|
||||
})
|
||||
}
|
||||
|
||||
if (self.progress && node.id === +self.runningNodeId) {
|
||||
@@ -1037,33 +1006,6 @@ export class ComfyApp {
|
||||
await useExtensionService().invokeExtensionsAsync('init')
|
||||
await this.registerNodes()
|
||||
|
||||
// Load previous workflow
|
||||
let restored = false
|
||||
try {
|
||||
const loadWorkflow = async (json) => {
|
||||
if (json) {
|
||||
const workflow = JSON.parse(json)
|
||||
const workflowName = getStorageValue('Comfy.PreviousWorkflow')
|
||||
await this.loadGraphData(workflow, true, true, workflowName)
|
||||
return true
|
||||
}
|
||||
}
|
||||
const clientId = api.initialClientId ?? api.clientId
|
||||
restored =
|
||||
(clientId &&
|
||||
(await loadWorkflow(
|
||||
sessionStorage.getItem(`workflow:${clientId}`)
|
||||
))) ||
|
||||
(await loadWorkflow(localStorage.getItem('workflow')))
|
||||
} catch (err) {
|
||||
console.error('Error loading previous workflow', err)
|
||||
}
|
||||
|
||||
// We failed to restore a workflow so load the default
|
||||
if (!restored) {
|
||||
await this.loadGraphData()
|
||||
}
|
||||
|
||||
this.#addDrawNodeHandler()
|
||||
this.#addDrawGroupsHandler()
|
||||
this.#addDropHandler()
|
||||
@@ -1313,17 +1255,19 @@ export class ComfyApp {
|
||||
graphData.models &&
|
||||
useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')
|
||||
) {
|
||||
const modelStore = useModelStore()
|
||||
for (const m of graphData.models) {
|
||||
const models_available = await useModelStore().getLoadedModelFolder(
|
||||
m.directory
|
||||
)
|
||||
if (models_available === null) {
|
||||
// @ts-expect-error
|
||||
m.directory_invalid = true
|
||||
missingModels.push(m)
|
||||
} else if (!(m.name in models_available.models)) {
|
||||
missingModels.push(m)
|
||||
}
|
||||
const modelFolder = await modelStore.getLoadedModelFolder(m.directory)
|
||||
// @ts-expect-error
|
||||
if (!modelFolder) m.directory_invalid = true
|
||||
|
||||
const modelsAvailable = modelFolder?.models
|
||||
const modelExists =
|
||||
modelsAvailable &&
|
||||
Object.values(modelsAvailable).some(
|
||||
(model) => model.file_name === m.name
|
||||
)
|
||||
if (!modelExists) missingModels.push(m)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
const { offset, scale } = app.canvas.ds
|
||||
|
||||
const hidden =
|
||||
(!!options.hideOnZoom && scale < 0.5) ||
|
||||
(!!options.hideOnZoom && app.canvas.low_quality) ||
|
||||
widget.computedHeight <= 0 ||
|
||||
// @ts-expect-error Used by widgetInputs.ts
|
||||
widget.type === 'converted-widget' ||
|
||||
|
||||