Compare commits

..

17 Commits

Author SHA1 Message Date
Benjamin Lu
0ba1d1127c Add right side panel 2025-07-02 17:55:58 -04:00
Benjamin Lu
ed5b371175 feat: Add rightSidebarTabStore for managing right sidebar visibility 2025-07-02 17:26:41 -04:00
Benjamin Lu
943bf70326 Remove references to Comfy.Sidebar.Location 2025-07-02 17:14:26 -04:00
Benjamin Lu
7c95a8d466 Delete plan... 2025-07-02 16:58:12 -04:00
Benjamin Lu
3ec765a07b deprecate Comfy.Sidebar.Location setting ID 2025-07-02 16:29:41 -04:00
Benjamin Lu
11f342309e Add plan/progress to Claude.md 2025-07-02 16:28:50 -04:00
Benjamin Lu
f30eba1c12 Empty the existing claude.md 2025-07-02 14:39:46 -04:00
Christian Byrne
5cc1a8dea2 [test] Add release notification browser tests (#4311)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-02 01:29:09 -07:00
Christian Byrne
959ab3b3ec [feat] Add ESLint i18n enforcement and fix hardcoded strings (#4327)
Co-authored-by: github-actions <github-actions@github.com>
2025-07-02 00:41:46 -07:00
Terry Jia
35ff882ff2 [3d] better solution to support reading extra resource/texture (#4209) 2025-07-01 21:25:18 -07:00
filtered
f57f97cfcd [TS] Remove frontend-only typing from litegraph (#4325) 2025-07-01 20:07:05 -07:00
Christian Byrne
8f825c066b [docs] add code quality guidelines for i18n, async cleanup, and error handling (#4305) 2025-07-01 17:13:55 -07:00
Comfy Org PR Bot
e6f90e3101 1.24.0-0 (#4321)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-01 22:06:28 +00:00
filtered
d68391a80a [CI] Fix prerelease version tag not set (#4322) 2025-07-01 15:06:08 -07:00
filtered
df710945c9 [CI] Skip i18n in unrelated PRs (#4320) 2025-07-01 14:53:48 -07:00
filtered
8d6360074d Use prerelease flag for draft releases (#4319) 2025-07-01 14:51:24 -07:00
filtered
26c106c3e4 Allow prerelease using version bump action (#4318) 2025-07-01 14:29:55 -07:00
44 changed files with 896 additions and 933 deletions

View File

@@ -3,6 +3,12 @@ name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
paths-ignore:
- '.github/**'
- '.husky/**'
- '.vscode/**'
- 'browser_tests/**'
- 'tests-ui/**'
jobs:
update-locales:

View File

@@ -15,6 +15,7 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'Release')
outputs:
version: ${{ steps.current_version.outputs.version }}
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -24,6 +25,15 @@ jobs:
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if prerelease
id: check_prerelease
run: |
VERSION=${{ steps.current_version.outputs.version }}
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -62,9 +72,9 @@ jobs:
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' }}
draft: ${{ github.event.pull_request.base.ref != 'main' }}
prerelease: false
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
publish_pypi:

View File

@@ -8,10 +8,12 @@ on:
required: true
default: 'patch'
type: 'choice'
options:
- patch
- minor
- major
options: [patch, minor, major, prepatch, preminor, premajor, prerelease]
pre_release:
description: Pre-release ID (suffix)
required: false
default: ''
type: string
jobs:
bump-version:
@@ -33,19 +35,25 @@ jobs:
- name: Bump version
id: bump-version
run: |
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
npm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Format PR string
id: capitalised
run: |
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Bump version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
title: '${{ steps.bump-version.outputs.NEW_VERSION }}'
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
Automated version bump to ${{ steps.bump-version.outputs.NEW_VERSION }}
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
labels: |
Release
Release

View File

@@ -1,54 +0,0 @@
- use `npm run` to see what commands are available
- For component communication, prefer Vue's event-based pattern (emit/@event-name) for state changes and notifications; use defineExpose with refs only for imperative operations that need direct control (like form.validate(), modal.open(), or editor.focus()); events promote loose coupling and are better for reusable components, while exposed methods are acceptable for tightly-coupled component pairs or when wrapping third-party libraries that require imperative APIs
- After making code changes, follow this general process: (1) Create unit tests, component tests, browser tests (if appropriate for each), (2) run unit tests, component tests, and browser tests until passing, (3) run typecheck, lint, format (with prettier) -- you can use `npm run` command to see the scripts available, (4) check if any READMEs (including nested) or documentation needs to be updated, (5) Decide whether the changes are worth adding new content to the external documentation for (or would requires changes to the external documentation) at https://docs.comfy.org, then present your suggestion
- When referencing PrimeVue, you can get all the docs here: https://primevue.org. Do this instead of making up or inferring names of Components
- When trying to set tailwind classes for dark theme, use "dark-theme:" prefix rather than "dark:"
- Never add lines to PR descriptions or commit messages that say "Generated with Claude Code"
- When making PR names and commit messages, if you are going to add a prefix like "docs:", "feat:", "bugfix:", use square brackets around the prefix term and do not use a colon (e.g., should be "[docs]" rather than "docs:").
- When I reference GitHub Repos related to Comfy-Org, you should proactively fetch or read the associated information in the repo. To do so, you should exhaust all options: (1) Check if we have a local copy of the repo, (2) Use the GitHub API to fetch the information; you may want to do this IN ADDITION to the other options, especially for reading specific branches/PRs/comments/reviews/metadata, and (3) curl the GitHub website and parse the html or json responses
- For information about ComfyUI, ComfyUI_frontend, or ComfyUI-Manager, you can web search or download these wikis: https://deepwiki.com/Comfy-Org/ComfyUI-Manager, https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview, https://deepwiki.com/comfyanonymous/ComfyUI/2-core-architecture
- If a question/project is related to Comfy-Org, Comfy, or ComfyUI ecosystem, you should proactively use the Comfy docs to answer the question. The docs may be referenced with URLs like https://docs.comfy.org
- When operating inside a repo, check for README files at key locations in the repo detailing info about the contents of that folder. E.g., top-level key folders like tests-ui, browser_tests, composables, extensions/core, stores, services often have their own README.md files. When writing code, make sure to frequently reference these README files to understand the overall architecture and design of the project. Pay close attention to the snippets to learn particular patterns that seem to be there for a reason, as you should emulate those.
- Prefer running single tests, and not the whole test suite, for performance
- If using a lesser known or complex CLI tool, run the --help to see the documentation before deciding what to run, even if just for double-checking or verifying things.
- IMPORTANT: the most important goal when writing code is to create clean, best-practices, sustainable, and scalable public APIs and interfaces. Our app is used by thousands of users and we have thousands of mods/extensions that are constantly changing and updating; and we are also always updating. That's why it is IMPORTANT that we design systems and write code that follows practices of domain-driven design, object-oriented design, and design patterns (such that you can assure stability while allowing for all components around you to change and evolve). We ABSOLUTELY prioritize clean APIs and public interfaces that clearly define and restrict how/what the mods/extensions can access.
- If any of these technologies are referenced, you can proactively read their docs at these locations: https://primevue.org/theming, https://primevue.org/forms/, https://www.electronjs.org/docs/latest/api/browser-window, https://vitest.dev/guide/browser/, https://atlassian.design/components/pragmatic-drag-and-drop/core-package/drop-targets/, https://playwright.dev/docs/api/class-test, https://playwright.dev/docs/api/class-electron, https://www.algolia.com/doc/api-reference/rest-api/, https://pyav.org/docs/develop/cookbook/basics.html
- IMPORTANT: Never add Co-Authored by Claude or any reference to Claude or Claude Code in commit messages, PR descriptions, titles, or any documentation whatsoever
- The npm script to type check is called "typecheck" NOT "type check"
- Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
- when we are solving an issue we know the link/number for, we should add "Fixes #n" (where n is the issue number) to the PR description.
- Never write css if you can accomplish the same thing with tailwind utility classes
- Utilize ref and reactive for reactive state
- Implement computed properties with computed()
- Use watch and watchEffect for side effects
- Implement lifecycle hooks with onMounted, onUpdated, etc.
- Utilize provide/inject for dependency injection
- Use vue 3.5 style of default prop declaration. Do not define a `props` variable; instead, destructure props. Since vue 3.5, destructuring props does not strip them of reactivity.
- Use Tailwind CSS for styling
- Leverage VueUse functions for performance-enhancing styles
- Use lodash for utility functions
- Implement proper props and emits definitions
- Utilize Vue 3's Teleport component when needed
- Use Suspense for async components
- Implement proper error handling
- Follow Vue 3 style guide and naming conventions
- Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
- Avoid using `@ts-expect-error` to work around type issues. We needed to employ it to migrate to TypeScript, but it should not be viewed as an accepted practice or standard.
- DO NOT use deprecated PrimeVue components. Use these replacements instead:
* `Dropdown` → Use `Select` (import from 'primevue/select')
* `OverlayPanel` → Use `Popover` (import from 'primevue/popover')
* `Calendar` → Use `DatePicker` (import from 'primevue/datepicker')
* `InputSwitch` → Use `ToggleSwitch` (import from 'primevue/toggleswitch')
* `Sidebar` → Use `Drawer` (import from 'primevue/drawer')
* `Chips` → Use `AutoComplete` with multiple enabled and typeahead disabled
* `TabMenu` → Use `Tabs` without panels
* `Steps` → Use `Stepper` without panels
* `InlineMessage` → Use `Message` component
* Use `api.apiURL()` for all backend API calls and routes
- Actual API endpoints like /prompt, /queue, /view, etc.
- Image previews: `api.apiURL('/view?...')`
- Any backend-generated content or dynamic routes
* Use `api.fileURL()` for static files served from the public folder:
- Templates: `api.fileURL('/templates/default.json')`
- Extensions: `api.fileURL(extensionPath)` for loading JS modules
- Any static assets that exist in the public directory

View File

@@ -285,7 +285,6 @@ export class ComfyPage {
url.includes('api.comfy.org') ||
url.includes('stagingapi.comfy.org')
) {
console.log('Mocking releases API')
await route.fulfill({
status: 200,
contentType: 'application/json',

View File

@@ -0,0 +1,133 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Release Notifications', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('should show help center with release information', async ({
comfyPage
}) => {
// Mock release API with test data instead of empty array
await comfyPage.page.route('**/releases**', async (route) => {
const url = route.request().url()
if (
url.includes('api.comfy.org') ||
url.includes('stagingapi.comfy.org')
) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{
id: 1,
project: 'comfyui',
version: 'v0.3.44',
attention: 'medium',
content:
'## New Features\n\n- Added awesome feature\n- Fixed important bug',
published_at: new Date().toISOString()
}
])
})
} else {
await route.continue()
}
})
// Setup with release mocking disabled for this test
await comfyPage.setup({ mockReleases: false })
// Open help center
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
await helpCenterButton.waitFor({ state: 'visible' })
await helpCenterButton.click()
// Verify help center menu appears
const helpMenu = comfyPage.page.locator('.help-center-menu')
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows the release
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Should show the release version
await expect(
whatsNewSection.locator('text=Comfy v0.3.44 Release')
).toBeVisible()
// Close help center by dismissable mask
await comfyPage.page.click('.help-center-backdrop')
await expect(helpMenu).not.toBeVisible()
})
test('should not show release notifications when mocked (default behavior)', async ({
comfyPage
}) => {
// Use default setup (mockReleases: true)
await comfyPage.setup()
// Open help center
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
await helpCenterButton.waitFor({ state: 'visible' })
await helpCenterButton.click()
// Verify help center menu appears
const helpMenu = comfyPage.page.locator('.help-center-menu')
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows no releases
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(whatsNewSection).toBeVisible()
// Should show "No recent releases" message
await expect(
whatsNewSection.locator('text=No recent releases')
).toBeVisible()
// Should not show any popups or toasts
await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible()
await expect(
comfyPage.page.locator('.release-notification-toast')
).not.toBeVisible()
})
test('should handle release API errors gracefully', async ({ comfyPage }) => {
// Mock API to return an error
await comfyPage.page.route('**/releases**', async (route) => {
const url = route.request().url()
if (
url.includes('api.comfy.org') ||
url.includes('stagingapi.comfy.org')
) {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
})
} else {
await route.continue()
}
})
// Setup with release mocking disabled
await comfyPage.setup({ mockReleases: false })
// Open help center
const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn')
await helpCenterButton.waitFor({ state: 'visible' })
await helpCenterButton.click()
// Verify help center still works despite API error
const helpMenu = comfyPage.page.locator('.help-center-menu')
await expect(helpMenu).toBeVisible()
// Should show no releases due to error
const whatsNewSection = comfyPage.page.locator('.whats-new-section')
await expect(
whatsNewSection.locator('text=No recent releases')
).toBeVisible()
})
})

View File

@@ -1,4 +1,5 @@
import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
@@ -45,7 +46,8 @@ export default [
},
{
plugins: {
'unused-imports': unusedImports
'unused-imports': unusedImports,
'@intlify/vue-i18n': pluginI18n
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
@@ -53,7 +55,41 @@ export default [
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
'unused-imports/no-unused-imports': 'error',
'vue/no-v-html': 'off'
'vue/no-v-html': 'off',
// i18n rules
'@intlify/vue-i18n/no-raw-text': [
'error',
{
// Ignore strings that are:
// 1. Less than 2 characters
// 2. Only symbols/numbers/whitespace (no letters)
// 3. Match specific patterns
ignorePattern:
'^[^a-zA-Z]*$|^.{0,1}$|^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$',
ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'],
// Brand names and technical terms that shouldn't be translated
ignoreText: [
'ComfyUI',
'GitHub',
'OpenAI',
'API',
'URL',
'JSON',
'YAML',
'GPU',
'CPU',
'RAM',
'GB',
'MB',
'KB',
'ms',
'fps',
'px',
'App Data:',
'App Path:'
]
}
]
}
}
]

415
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.23.4",
"version": "1.24.0-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.23.4",
"version": "1.24.0-0",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -53,6 +53,7 @@
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.5",
"@iconify/json": "^2.2.245",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
@@ -2349,6 +2350,106 @@
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@intlify/eslint-plugin-vue-i18n/-/eslint-plugin-vue-i18n-3.2.0.tgz",
"integrity": "sha512-TOIrD4tJE48WMyVIB8bNeQJJPYo1Prpqnm9Xpn1UZmcqlELhm8hmP8QyJnkgesfbG7hyiX/kvo63W7ClEQmhpg==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^3.0.0",
"@intlify/core-base": "^9.12.0",
"@intlify/message-compiler": "^9.12.0",
"debug": "^4.3.4",
"eslint-compat-utils": "^0.6.0",
"glob": "^10.3.3",
"globals": "^15.0.0",
"ignore": "^6.0.0",
"import-fresh": "^3.3.0",
"is-language-code": "^3.1.0",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
"jsonc-eslint-parser": "^2.3.0",
"lodash": "^4.17.21",
"parse5": "^7.1.2",
"semver": "^7.5.4",
"synckit": "^0.9.0",
"vue-eslint-parser": "^9.3.1",
"yaml-eslint-parser": "^1.2.2"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"eslint": "^8.0.0 || ^9.0.0-0"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/@pkgr/core": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz",
"integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/ignore": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz",
"integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/synckit": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz",
"integrity": "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==",
"dev": true,
"dependencies": {
"@pkgr/core": "^0.1.0",
"tslib": "^2.6.2"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.3",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.3.tgz",
@@ -2702,53 +2803,6 @@
"react": ">=18"
}
},
"node_modules/@lobehub/i18n-cli/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@lobehub/i18n-cli/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@lobehub/i18n-cli/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@microsoft/api-extractor": {
"version": "7.48.0",
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.48.0.tgz",
@@ -7672,6 +7726,33 @@
}
}
},
"node_modules/eslint-compat-utils": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz",
"integrity": "sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==",
"dev": true,
"dependencies": {
"semver": "^7.5.4"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-compat-utils/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz",
@@ -8818,6 +8899,26 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -8830,6 +8931,30 @@
"node": ">= 6"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/global-directory": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
@@ -9668,6 +9793,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-language-code": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-language-code/-/is-language-code-3.1.0.tgz",
"integrity": "sha512-zJdQ3QTeLye+iphMeK3wks+vXSRFKh68/Pnlw7aOfApFSEIOhYa8P9vwwa6QrImNNBMJTiL1PpYF0f4BxDuEgA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.14.0"
}
},
"node_modules/is-lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz",
@@ -9926,53 +10060,6 @@
"node": ">=14"
}
},
"node_modules/js-beautify/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/js-beautify/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-beautify/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -10150,6 +10237,65 @@
"node": ">=6"
}
},
"node_modules/jsonc-eslint-parser": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz",
"integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==",
"dev": true,
"dependencies": {
"acorn": "^8.5.0",
"eslint-visitor-keys": "^3.0.0",
"espree": "^9.0.0",
"semver": "^7.3.5"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/jsonc-eslint-parser/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/jsonc-eslint-parser/node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/jsonc-eslint-parser/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jsondiffpatch": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
@@ -12635,8 +12781,6 @@
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"entities": "^4.4.0"
},
@@ -14723,15 +14867,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sucrase/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/sucrase/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -14741,44 +14876,6 @@
"node": ">= 6"
}
},
"node_modules/sucrase/node_modules/glob": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz",
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
@@ -17478,6 +17575,34 @@
"node": ">= 14"
}
},
"node_modules/yaml-eslint-parser": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.0.tgz",
"integrity": "sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^3.0.0",
"yaml": "^2.0.0"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
}
},
"node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.23.4",
"version": "1.24.0-0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -31,6 +31,7 @@
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.5",
"@iconify/json": "^2.2.245",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",

View File

@@ -8,7 +8,6 @@
>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'left'"
class="side-bar-panel"
:min-size="10"
:size="20"
@@ -32,16 +31,6 @@
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</template>
@@ -55,9 +44,6 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const unifiedWidth = computed(() =>
settingStore.get('Comfy.Sidebar.UnifiedWidth')

View File

@@ -50,7 +50,7 @@
<Suspense v-for="panel in panels" :key="panel.node.key">
<component :is="panel.component" />
<template #fallback>
<div>Loading {{ panel.node.label }} panel...</div>
<div>{{ $t('g.loadingPanel', { panel: panel.node.label }) }}</div>
</template>
</Suspense>
</TabPanels>

View File

@@ -98,7 +98,7 @@
@keydown.stop.prevent="captureKeybinding"
/>
<Message v-if="existingKeybindingOnCombo" severity="warn">
Keybinding already exists on
{{ $t('g.keybindingAlreadyExists') }}
<Tag
severity="secondary"
:value="existingKeybindingOnCombo.commandId"

View File

@@ -22,7 +22,7 @@
</div>
<div class="flex items-center gap-4">
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
{{ comfyManagerStore.uncompletedCount }} of
{{ comfyManagerStore.uncompletedCount }} {{ $t('g.progressCountOf') }}
{{ comfyManagerStore.taskLogs.length }}
</span>
<div class="flex items-center">

View File

@@ -40,6 +40,7 @@
<SelectionToolbox />
</SelectionOverlay>
<DomWidgets />
<RightSideToolbar />
</template>
</template>
@@ -58,6 +59,7 @@ import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import RightSideToolbar from '@/components/sidebar/RightSideToolbar.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'

View File

@@ -65,7 +65,12 @@
<i class="pi pi-refresh help-menu-icon" aria-hidden="true" />
<div class="release-content">
<span class="release-title">
Comfy {{ release.version }} Release
{{
$t('g.releaseTitle', {
package: 'Comfy',
version: release.version
})
}}
</span>
<time class="release-date" :datetime="release.published_at">
<span class="normal-state">

View File

@@ -206,7 +206,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}
@@ -218,7 +222,14 @@ const handleUploadTexture = async (file: File) => {
}
try {
const texturePath = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
const texturePath = await Load3dUtils.uploadFile(file, subfolder)
await load3DSceneRef.value.load3d.applyTexture(texturePath)
node.properties['Texture'] = texturePath

View File

@@ -238,7 +238,11 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
return
}
backgroundImage.value = await Load3dUtils.uploadFile(file)
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim() ? `3d/${resourceFolder.trim()}` : '3d'
backgroundImage.value = await Load3dUtils.uploadFile(file, subfolder)
node.properties['Background Image'] = backgroundImage.value
}

View File

@@ -15,7 +15,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
<div class="_sb_dot headdot" />
{{ nodeDef.display_name }}
</div>
<div class="_sb_preview_badge">PREVIEW</div>
<div class="_sb_preview_badge">{{ $t('g.preview') }}</div>
<!-- Node slot I/O -->
<div

View File

@@ -27,7 +27,7 @@
@hide="reFocusInput"
>
<template #header>
<h3>Add node filter condition</h3>
<h3>{{ $t('g.addNodeFilterCondition') }}</h3>
</template>
<div class="_dialog-body">
<NodeSearchFilter @add-filter="onAddFilter" />

View File

@@ -0,0 +1,64 @@
<template>
<teleport to=".comfyui-body-right">
<div class="right-sidebar-container" :class="{ hidden: !isVisible }">
<div class="right-sidebar-header">
<h3></h3>
<Button
icon="pi pi-times"
text
severity="secondary"
size="small"
@click="rightSidebarTabStore.hide()"
/>
</div>
<div class="right-sidebar-content">
<!-- Content will go here -->
<p></p>
</div>
</div>
</teleport>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useRightSidebarTabStore } from '@/stores/workspace/rightSidebarTabStore'
const rightSidebarTabStore = useRightSidebarTabStore()
const isVisible = computed(() => rightSidebarTabStore.isVisible)
</script>
<style scoped>
.right-sidebar-container {
display: flex;
flex-direction: column;
width: 300px;
height: 100%;
background-color: var(--comfy-menu-bg);
border-left: 1px solid var(--border-color);
}
.right-sidebar-container.hidden {
display: none;
}
.right-sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.right-sidebar-header h3 {
margin: 0;
font-size: 1rem;
}
.right-sidebar-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<teleport :to="teleportTarget">
<teleport to=".comfyui-body-left">
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
<SidebarIcon
v-for="tab in tabs"
@@ -47,12 +47,6 @@ const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const teleportTarget = computed(() =>
settingStore.get('Comfy.Sidebar.Location') === 'left'
? '.comfyui-body-left'
: '.comfyui-body-right'
)
const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
)

View File

@@ -12,10 +12,8 @@
<Teleport to="#graph-canvas-container">
<div
v-if="isHelpCenterVisible"
class="help-center-popup"
class="help-center-popup sidebar-left"
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
}"
>
@@ -26,9 +24,8 @@
<!-- Release Notification Toast positioned within canvas area -->
<Teleport to="#graph-canvas-container">
<ReleaseNotificationToast
class="sidebar-left"
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
}"
/>
@@ -37,9 +34,8 @@
<!-- WhatsNew Popup positioned within canvas area -->
<Teleport to="#graph-canvas-container">
<WhatsNewPopup
class="sidebar-left"
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
}"
/>
@@ -73,10 +69,6 @@ const releaseStore = useReleaseStore()
const { shouldShowRedDot } = storeToRefs(releaseStore)
const isHelpCenterVisible = ref(false)
const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
const toggleHelpCenter = () => {
@@ -121,10 +113,6 @@ onMounted(async () => {
left: 1rem;
}
.help-center-popup.sidebar-right {
right: 1rem;
}
@keyframes slideInUp {
from {
opacity: 0;

View File

@@ -8,11 +8,11 @@
{{ modelDef.file_name }}
</div>
<div v-if="modelDef.architecture_id" class="model_preview_architecture">
<span class="model_preview_prefix">Architecture: </span>
<span class="model_preview_prefix">{{ $t('g.architecture') }}: </span>
{{ modelDef.architecture_id }}
</div>
<div v-if="modelDef.author" class="model_preview_author">
<span class="model_preview_prefix">Author: </span>
<span class="model_preview_prefix">{{ $t('g.author') }}: </span>
{{ modelDef.author }}
</div>
</div>
@@ -20,15 +20,15 @@
<img :src="modelDef.image" />
</div>
<div v-if="modelDef.usage_hint" class="model_preview_usage_hint">
<span class="model_preview_prefix">Usage hint: </span>
<span class="model_preview_prefix">{{ $t('g.usageHint') }}: </span>
{{ modelDef.usage_hint }}
</div>
<div v-if="modelDef.trigger_phrase" class="model_preview_trigger_phrase">
<span class="model_preview_prefix">Trigger phrase: </span>
<span class="model_preview_prefix">{{ $t('g.triggerPhrase') }}: </span>
{{ modelDef.trigger_phrase }}
</div>
<div v-if="modelDef.description" class="model_preview_description">
<span class="model_preview_prefix">Description: </span>
<span class="model_preview_prefix">{{ $t('g.description') }}: </span>
{{ modelDef.description }}
</div>
</div>

View File

@@ -31,7 +31,6 @@ import {
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import { ComfyModelDef } from '@/stores/modelStore'
import { useSettingStore } from '@/stores/settingStore'
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import ModelPreview from './ModelPreview.vue'
@@ -62,11 +61,6 @@ const modelPreviewStyle = ref<CSSProperties>({
left: '0px'
})
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const handleModelHover = async () => {
const hoverTarget = modelContentElement.value
if (!hoverTarget) return
@@ -80,11 +74,7 @@ const handleModelHover = async () => {
previewHeight > availableSpaceBelow
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
: `${targetRect.top - 40}px`
if (sidebarLocation.value === 'left') {
modelPreviewStyle.value.left = `${targetRect.right}px`
} else {
modelPreviewStyle.value.left = `${targetRect.left - 400}px`
}
modelPreviewStyle.value.left = `${targetRect.right}px`
await modelDef.value.load()
}

View File

@@ -58,7 +58,6 @@ import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
const props = defineProps<{
@@ -72,11 +71,6 @@ const nodeBookmarkStore = useNodeBookmarkStore()
const isBookmarked = computed(() =>
nodeBookmarkStore.isBookmarked(nodeDef.value)
)
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const toggleBookmark = async () => {
await nodeBookmarkStore.toggleBookmark(nodeDef.value)
}
@@ -101,11 +95,7 @@ const handleNodeHover = async () => {
previewHeight > availableSpaceBelow
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
: `${targetRect.top - 40}px`
if (sidebarLocation.value === 'left') {
nodePreviewStyle.value.left = `${targetRect.right}px`
} else {
nodePreviewStyle.value.left = `${targetRect.left - 400}px`
}
nodePreviewStyle.value.left = `${targetRect.right}px`
}
const container = ref<HTMLElement | null>(null)

View File

@@ -82,9 +82,4 @@ watch(
() => nextTick(updateToastPosition),
{ immediate: true }
)
watch(
() => settingStore.get('Comfy.Sidebar.Location'),
() => nextTick(updateToastPosition),
{ immediate: true }
)
</script>

View File

@@ -18,6 +18,19 @@
<Actionbar />
<CurrentUserButton class="flex-shrink-0" />
<BottomPanelToggleButton class="flex-shrink-0" />
<Button
v-tooltip="{ value: 'Toggle Right Sidebar', showDelay: 300 }"
class="flex-shrink-0"
:icon="
rightSidebarTabStore.isVisible
? 'pi pi-angle-double-right'
: 'pi pi-angle-double-left'
"
severity="secondary"
text
aria-label="Toggle Right Sidebar"
@click="rightSidebarTabStore.toggleVisibility()"
/>
<Button
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
class="flex-shrink-0"
@@ -53,6 +66,7 @@ import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useRightSidebarTabStore } from '@/stores/workspace/rightSidebarTabStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import {
electronAPI,
@@ -63,6 +77,7 @@ import {
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
const rightSidebarTabStore = useRightSidebarTabStore()
const workflowTabsPosition = computed(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')

View File

@@ -83,7 +83,8 @@ export const CORE_SETTINGS: SettingParams[] = [
name: 'Sidebar location',
type: 'combo',
options: ['left', 'right'],
defaultValue: 'left'
defaultValue: 'left',
deprecated: true
},
{
id: 'Comfy.Sidebar.Size',

View File

@@ -1,7 +1,4 @@
import type {
IComboWidget,
IStringWidget
} from '@comfyorg/litegraph/dist/types/widgets'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -17,6 +14,80 @@ import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
import { useToastStore } from '@/stores/toastStore'
async function handleModelUpload(files: FileList, node: any) {
if (!files?.length) return
const modelWidget = node.widgets?.find(
(w: any) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
try {
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
const uploadPath = await Load3dUtils.uploadFile(files[0], subfolder)
if (!uploadPath) {
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
return
}
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
} catch (error) {
console.error('Model upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
}
}
async function handleResourcesUpload(files: FileList, node: any) {
if (!files?.length) return
try {
const resourceFolder = (node.properties['Resource Folder'] as string) || ''
const subfolder = resourceFolder.trim()
? `3d/${resourceFolder.trim()}`
: '3d'
await Load3dUtils.uploadMultipleFiles(files, subfolder)
} catch (error) {
console.error('Extra resources upload failed:', error)
useToastStore().addAlert(t('toastMessages.extraResourcesUploadFailed'))
}
}
function createFileInput(
accept: string,
multiple: boolean = false
): HTMLInputElement {
const input = document.createElement('input')
input.type = 'file'
input.accept = accept
input.multiple = multiple
input.style.display = 'none'
return input
}
useExtensionService().registerExtension({
name: 'Comfy.Load3D',
settings: [
@@ -110,49 +181,34 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
LOAD_3D(node) {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.gltf,.glb,.obj,.fbx,.stl'
fileInput.style.display = 'none'
const fileInput = createFileInput('.gltf,.glb,.obj,.fbx,.stl', false)
node.properties['Resource Folder'] = ''
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
) as IComboWidget & { options: { values: string[] } }
node.properties['Texture'] = undefined
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
}
await handleModelUpload(fileInput.files!, node)
}
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
fileInput.click()
})
const resourcesInput = createFileInput('*', true)
resourcesInput.onchange = async () => {
await handleResourcesUpload(resourcesInput.files!, node)
resourcesInput.value = ''
}
node.addWidget(
'button',
'upload extra resources',
'uploadExtraResources',
() => {
resourcesInput.click()
}
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
@@ -264,46 +320,34 @@ useExtensionService().registerExtension({
getCustomWidgets() {
return {
LOAD_3D_ANIMATION(node) {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.gltf,.glb,.fbx'
fileInput.style.display = 'none'
const fileInput = createFileInput('.gltf,.glb,.fbx', false)
node.properties['Resource Folder'] = ''
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(uploadPath),
'input'
)
)
await useLoad3dService().getLoad3d(node)?.loadModel(modelUrl)
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
modelWidget.options?.values?.push(uploadPath)
}
modelWidget.value = uploadPath
}
}
await handleModelUpload(fileInput.files!, node)
}
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
fileInput.click()
})
const resourcesInput = createFileInput('*', true)
resourcesInput.onchange = async () => {
await handleResourcesUpload(resourcesInput.files!, node)
resourcesInput.value = ''
}
node.addWidget(
'button',
'upload extra resources',
'uploadExtraResources',
() => {
resourcesInput.click()
}
)
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()

View File

@@ -128,6 +128,9 @@ class Load3DConfiguration {
if (!value) return
const filename = value as string
this.setResourceFolder(filename)
const modelUrl = api.apiURL(
Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(filename),
@@ -173,6 +176,21 @@ class Load3DConfiguration {
}
}
}
private setResourceFolder(filename: string): void {
const pathParts = filename.split('/').filter((part) => part.trim())
if (pathParts.length <= 2) {
return
}
const subfolderParts = pathParts.slice(1, -1)
const subfolder = subfolderParts.join('/')
if (subfolder) {
this.load3d.node.properties['Resource Folder'] = subfolder
}
}
}
export default Load3DConfiguration

View File

@@ -118,11 +118,7 @@ class Load3d {
options
)
this.loaderManager = new LoaderManager(
this.modelManager,
this.eventManager,
options
)
this.loaderManager = new LoaderManager(this.modelManager, this.eventManager)
this.recordingManager = new RecordingManager(
this.sceneManager.scene,

View File

@@ -34,13 +34,14 @@ class Load3dUtils {
return await resp.json()
}
static async uploadFile(file: File) {
static async uploadFile(file: File, subfolder: string) {
let uploadPath
try {
const body = new FormData()
body.append('image', file)
body.append('subfolder', '3d')
body.append('subfolder', subfolder)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
@@ -96,6 +97,14 @@ class Load3dUtils {
return `/view?${params}`
}
static async uploadMultipleFiles(files: FileList, subfolder: string = '3d') {
const uploadPromises = Array.from(files).map((file) =>
this.uploadFile(file, subfolder)
)
await Promise.all(uploadPromises)
}
}
export default Load3dUtils

View File

@@ -1,16 +1,15 @@
import * as THREE from 'three'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { 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 { OverrideMTLLoader } from '@/extensions/core/load3d/threejsOverride/OverrideMTLLoader'
import { t } from '@/i18n'
import { useToastStore } from '@/stores/toastStore'
import {
EventManagerInterface,
Load3DOptions,
LoaderManagerInterface,
ModelManagerInterface
} from './interfaces'
@@ -18,7 +17,7 @@ import {
export class LoaderManager implements LoaderManagerInterface {
gltfLoader: GLTFLoader
objLoader: OBJLoader
mtlLoader: OverrideMTLLoader
mtlLoader: MTLLoader
fbxLoader: FBXLoader
stlLoader: STLLoader
@@ -27,21 +26,14 @@ export class LoaderManager implements LoaderManagerInterface {
constructor(
modelManager: ModelManagerInterface,
eventManager: EventManagerInterface,
options: Load3DOptions
eventManager: EventManagerInterface
) {
let loadRootFolder = 'input'
if (options && options.inputSpec?.isPreview) {
loadRootFolder = 'output'
}
this.modelManager = modelManager
this.eventManager = eventManager
this.gltfLoader = new GLTFLoader()
this.objLoader = new OBJLoader()
this.mtlLoader = new OverrideMTLLoader(loadRootFolder)
this.mtlLoader = new MTLLoader()
this.fbxLoader = new FBXLoader()
this.stlLoader = new STLLoader()
}
@@ -100,9 +92,31 @@ export class LoaderManager implements LoaderManagerInterface {
): Promise<THREE.Object3D | null> {
let model: THREE.Object3D | null = null
const params = new URLSearchParams(url.split('?')[1])
const filename = params.get('filename')
if (!filename) {
console.error('Missing filename in URL:', url)
return null
}
const loadRootFolder = params.get('type') === 'output' ? 'output' : 'input'
const subfolder = params.get('subfolder') ?? ''
const path =
'api/view?type=' +
loadRootFolder +
'&subfolder=' +
encodeURIComponent(subfolder) +
'&filename='
switch (fileExtension) {
case 'stl':
const geometry = await this.stlLoader.loadAsync(url)
this.stlLoader.setPath(path)
const geometry = await this.stlLoader.loadAsync(filename)
this.modelManager.setOriginalModel(geometry)
geometry.computeVertexNormals()
@@ -117,7 +131,10 @@ export class LoaderManager implements LoaderManagerInterface {
break
case 'fbx':
const fbxModel = await this.fbxLoader.loadAsync(url)
this.fbxLoader.setPath(path)
const fbxModel = await this.fbxLoader.loadAsync(filename)
this.modelManager.setOriginalModel(fbxModel)
model = fbxModel
@@ -130,18 +147,12 @@ export class LoaderManager implements LoaderManagerInterface {
case 'obj':
if (this.modelManager.materialMode === 'original') {
const mtlUrl = url.replace(/(filename=.*?)\.obj/, '$1.mtl')
const subfolderMatch = url.match(/[?&]subfolder=([^&]*)/)
const subfolder = subfolderMatch
? decodeURIComponent(subfolderMatch[1])
: '3d'
this.mtlLoader.setSubfolder(subfolder)
try {
const materials = await this.mtlLoader.loadAsync(mtlUrl)
this.mtlLoader.setPath(path)
const mtlFileName = filename.replace(/\.obj$/, '.mtl')
const materials = await this.mtlLoader.loadAsync(mtlFileName)
materials.preload()
this.objLoader.setMaterials(materials)
} catch (e) {
@@ -151,7 +162,8 @@ export class LoaderManager implements LoaderManagerInterface {
}
}
model = await this.objLoader.loadAsync(url)
this.objLoader.setPath(path)
model = await this.objLoader.loadAsync(filename)
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
this.modelManager.originalMaterials.set(child, child.material)
@@ -161,7 +173,10 @@ export class LoaderManager implements LoaderManagerInterface {
case 'gltf':
case 'glb':
const gltf = await this.gltfLoader.loadAsync(url)
this.gltfLoader.setPath(path)
const gltf = await this.gltfLoader.loadAsync(filename)
this.modelManager.setOriginalModel(gltf)
model = gltf.scene

View File

@@ -1,533 +0,0 @@
import {
Color,
ColorManagement,
DefaultLoadingManager,
FileLoader,
FrontSide,
Loader,
LoaderUtils,
MeshPhongMaterial,
RepeatWrapping,
SRGBColorSpace,
TextureLoader,
Vector2
} from 'three'
/**
* A loader for the MTL format.
*
* The Material Template Library format (MTL) or .MTL File Format is a companion file format
* to OBJ that describes surface shading (material) properties of objects within one or more
* OBJ files.
*
* ```js
* const loader = new MTLLoader();
* const materials = await loader.loadAsync( 'models/obj/male02/male02.mtl' );
*
* const objLoader = new OBJLoader();
* objLoader.setMaterials( materials );
* ```
*
* @augments Loader
* @three_import import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
*/
class OverrideMTLLoader extends Loader {
constructor(loadRootFolder, manager) {
super(manager)
this.loadRootFolder = loadRootFolder
}
setSubfolder(subfolder) {
this.subfolder = subfolder
}
/**
* Starts loading from the given URL and passes the loaded MTL asset
* to the `onLoad()` callback.
*
* @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
* @param {function(MaterialCreator)} onLoad - Executed when the loading process has been finished.
* @param {onProgressCallback} onProgress - Executed while the loading is in progress.
* @param {onErrorCallback} onError - Executed when errors occur.
*/
load(url, onLoad, onProgress, onError) {
const scope = this
const path = this.path === '' ? LoaderUtils.extractUrlBase(url) : this.path
const loader = new FileLoader(this.manager)
loader.setPath(this.path)
loader.setRequestHeader(this.requestHeader)
loader.setWithCredentials(this.withCredentials)
loader.load(
url,
function (text) {
try {
onLoad(scope.parse(text, path))
} catch (e) {
if (onError) {
onError(e)
} else {
console.error(e)
}
scope.manager.itemError(url)
}
},
onProgress,
onError
)
}
/**
* Sets the material options.
*
* @param {MTLLoader~MaterialOptions} value - The material options.
* @return {MTLLoader} A reference to this loader.
*/
setMaterialOptions(value) {
this.materialOptions = value
return this
}
/**
* Parses the given MTL data and returns the resulting material creator.
*
* @param {string} text - The raw MTL data as a string.
* @param {string} path - The URL base path.
* @return {MaterialCreator} The material creator.
*/
parse(text, path) {
const lines = text.split('\n')
let info = {}
const delimiter_pattern = /\s+/
const materialsInfo = {}
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
line = line.trim()
if (line.length === 0 || line.charAt(0) === '#') {
// Blank line or comment ignore
continue
}
const pos = line.indexOf(' ')
let key = pos >= 0 ? line.substring(0, pos) : line
key = key.toLowerCase()
let value = pos >= 0 ? line.substring(pos + 1) : ''
value = value.trim()
if (key === 'newmtl') {
// New material
info = { name: value }
materialsInfo[value] = info
} else {
if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') {
const ss = value.split(delimiter_pattern, 3)
info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]
} else {
info[key] = value
}
}
}
const materialCreator = new OverrideMaterialCreator(
this.resourcePath || path,
this.materialOptions,
this.loadRootFolder,
this.subfolder
)
materialCreator.setCrossOrigin(this.crossOrigin)
materialCreator.setManager(this.manager)
materialCreator.setMaterials(materialsInfo)
return materialCreator
}
}
/**
* Material options of `MTLLoader`.
*
* @typedef {Object} MTLLoader~MaterialOptions
* @property {(FrontSide|BackSide|DoubleSide)} [side=FrontSide] - Which side to apply the material.
* @property {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)} [wrap=RepeatWrapping] - What type of wrapping to apply for textures.
* @property {boolean} [normalizeRGB=false] - Whether RGB colors should be normalized to `0-1` from `0-255`.
* @property {boolean} [ignoreZeroRGBs=false] - Ignore values of RGBs (Ka,Kd,Ks) that are all 0's.
*/
class OverrideMaterialCreator {
constructor(baseUrl = '', options = {}, loadRootFolder, subfolder) {
this.baseUrl = baseUrl
this.options = options
this.materialsInfo = {}
this.materials = {}
this.materialsArray = []
this.nameLookup = {}
this.loadRootFolder = loadRootFolder
this.subfolder = subfolder
this.crossOrigin = 'anonymous'
this.side = this.options.side !== undefined ? this.options.side : FrontSide
this.wrap =
this.options.wrap !== undefined ? this.options.wrap : RepeatWrapping
}
setCrossOrigin(value) {
this.crossOrigin = value
return this
}
setManager(value) {
this.manager = value
}
setMaterials(materialsInfo) {
this.materialsInfo = this.convert(materialsInfo)
this.materials = {}
this.materialsArray = []
this.nameLookup = {}
}
convert(materialsInfo) {
if (!this.options) return materialsInfo
const converted = {}
for (const mn in materialsInfo) {
// Convert materials info into normalized form based on options
const mat = materialsInfo[mn]
const covmat = {}
converted[mn] = covmat
for (const prop in mat) {
let save = true
let value = mat[prop]
const lprop = prop.toLowerCase()
switch (lprop) {
case 'kd':
case 'ka':
case 'ks':
// Diffuse color (color under white light) using RGB values
if (this.options && this.options.normalizeRGB) {
value = [value[0] / 255, value[1] / 255, value[2] / 255]
}
if (this.options && this.options.ignoreZeroRGBs) {
if (value[0] === 0 && value[1] === 0 && value[2] === 0) {
// ignore
save = false
}
}
break
default:
break
}
if (save) {
covmat[lprop] = value
}
}
}
return converted
}
preload() {
for (const mn in this.materialsInfo) {
this.create(mn)
}
}
getIndex(materialName) {
return this.nameLookup[materialName]
}
getAsArray() {
let index = 0
for (const mn in this.materialsInfo) {
this.materialsArray[index] = this.create(mn)
this.nameLookup[mn] = index
index++
}
return this.materialsArray
}
create(materialName) {
if (this.materials[materialName] === undefined) {
this.createMaterial_(materialName)
}
return this.materials[materialName]
}
createMaterial_(materialName) {
// Create material
const scope = this
const mat = this.materialsInfo[materialName]
const params = {
name: materialName,
side: this.side
}
/**
* Override for ComfyUI api url
*/
function resolveURL(baseUrl, url, loadRootFolder, subfolder) {
if (typeof url !== 'string' || url === '') return ''
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1)
}
if (!baseUrl.endsWith('api')) {
baseUrl = '/api'
}
baseUrl =
baseUrl +
'/view?filename=' +
url +
'&type=' +
loadRootFolder +
'&subfolder=' +
subfolder
return baseUrl
}
function setMapForType(mapType, value) {
if (params[mapType]) return // Keep the first encountered texture
const texParams = scope.getTextureParams(value, params)
const map = scope.loadTexture(
resolveURL(
scope.baseUrl,
texParams.url,
scope.loadRootFolder,
scope.subfolder
)
)
map.repeat.copy(texParams.scale)
map.offset.copy(texParams.offset)
map.wrapS = scope.wrap
map.wrapT = scope.wrap
if (mapType === 'map' || mapType === 'emissiveMap') {
map.colorSpace = SRGBColorSpace
}
params[mapType] = map
}
for (const prop in mat) {
const value = mat[prop]
let n
if (value === '') continue
switch (prop.toLowerCase()) {
// Ns is material specular exponent
case 'kd':
// Diffuse color (color under white light) using RGB values
params.color = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'ks':
// Specular color (color when light is reflected from shiny surface) using RGB values
params.specular = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'ke':
// Emissive using RGB values
params.emissive = ColorManagement.toWorkingColorSpace(
new Color().fromArray(value),
SRGBColorSpace
)
break
case 'map_kd':
// Diffuse texture map
setMapForType('map', value)
break
case 'map_ks':
// Specular map
setMapForType('specularMap', value)
break
case 'map_ke':
// Emissive map
setMapForType('emissiveMap', value)
break
case 'norm':
setMapForType('normalMap', value)
break
case 'map_bump':
case 'bump':
// Bump texture map
setMapForType('bumpMap', value)
break
case 'disp':
// Displacement texture map
setMapForType('displacementMap', value)
break
case 'map_d':
// Alpha map
setMapForType('alphaMap', value)
params.transparent = true
break
case 'ns':
// The specular exponent (defines the focus of the specular highlight)
// A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
params.shininess = parseFloat(value)
break
case 'd':
n = parseFloat(value)
if (n < 1) {
params.opacity = n
params.transparent = true
}
break
case 'tr':
n = parseFloat(value)
if (this.options && this.options.invertTrProperty) n = 1 - n
if (n > 0) {
params.opacity = 1 - n
params.transparent = true
}
break
default:
break
}
}
this.materials[materialName] = new MeshPhongMaterial(params)
return this.materials[materialName]
}
getTextureParams(value, matParams) {
const texParams = {
scale: new Vector2(1, 1),
offset: new Vector2(0, 0)
}
const items = value.split(/\s+/)
let pos
pos = items.indexOf('-bm')
if (pos >= 0) {
matParams.bumpScale = parseFloat(items[pos + 1])
items.splice(pos, 2)
}
pos = items.indexOf('-mm')
if (pos >= 0) {
matParams.displacementBias = parseFloat(items[pos + 1])
matParams.displacementScale = parseFloat(items[pos + 2])
items.splice(pos, 3)
}
pos = items.indexOf('-s')
if (pos >= 0) {
texParams.scale.set(
parseFloat(items[pos + 1]),
parseFloat(items[pos + 2])
)
items.splice(pos, 4) // we expect 3 parameters here!
}
pos = items.indexOf('-o')
if (pos >= 0) {
texParams.offset.set(
parseFloat(items[pos + 1]),
parseFloat(items[pos + 2])
)
items.splice(pos, 4) // we expect 3 parameters here!
}
texParams.url = items.join(' ').trim()
return texParams
}
loadTexture(url, mapping, onLoad, onProgress, onError) {
const manager =
this.manager !== undefined ? this.manager : DefaultLoadingManager
let loader = manager.getHandler(url)
if (loader === null) {
loader = new TextureLoader(manager)
}
if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin)
const texture = loader.load(url, onLoad, onProgress, onError)
if (mapping !== undefined) texture.mapping = mapping
return texture
}
}
export { OverrideMTLLoader }

View File

@@ -32,6 +32,13 @@
"error": "Error",
"help": "Help",
"loading": "Loading",
"loadingPanel": "Loading {panel} panel...",
"preview": "PREVIEW",
"addNodeFilterCondition": "Add node filter condition",
"architecture": "Architecture",
"author": "Author",
"usageHint": "Usage hint",
"triggerPhrase": "Trigger phrase",
"findIssues": "Find Issues",
"reportIssue": "Send Report",
"reportIssueTooltip": "Submit the error report to Comfy Org",
@@ -123,7 +130,10 @@
"copy": "Copy",
"imageUrl": "Image URL",
"clear": "Clear",
"copyURL": "Copy URL"
"copyURL": "Copy URL",
"releaseTitle": "{package} {version} Release",
"progressCountOf": "of",
"keybindingAlreadyExists": "Keybinding already exists on"
},
"manager": {
"title": "Custom Nodes Manager",

View File

@@ -253,10 +253,13 @@
"g": {
"about": "Acerca de",
"add": "Añadir",
"addNodeFilterCondition": "Agregar condición de filtro de nodo",
"all": "Todo",
"amount": "Cantidad",
"apply": "Aplicar",
"architecture": "Arquitectura",
"audioFailedToLoad": "No se pudo cargar el audio",
"author": "Autor",
"back": "Atrás",
"cancel": "Cancelar",
"capture": "captura",
@@ -314,10 +317,12 @@
"installing": "Instalando",
"interrupted": "Interrumpido",
"keybinding": "Combinación de teclas",
"keybindingAlreadyExists": "La combinación de teclas ya existe en",
"learnMore": "Aprende más",
"loadAllFolders": "Cargar todas las carpetas",
"loadWorkflow": "Cargar flujo de trabajo",
"loading": "Cargando",
"loadingPanel": "Cargando panel {panel}...",
"login": "Iniciar sesión",
"logs": "Registros",
"migrate": "Migrar",
@@ -334,9 +339,12 @@
"ok": "OK",
"openNewIssue": "Abrir nuevo problema",
"overwrite": "Sobrescribir",
"preview": "VISTA PREVIA",
"progressCountOf": "de",
"reconnected": "Reconectado",
"reconnecting": "Reconectando",
"refresh": "Actualizar",
"releaseTitle": "Lanzamiento de {package} {version}",
"reloadToApplyChanges": "Recargar para aplicar cambios",
"rename": "Renombrar",
"reportIssue": "Enviar informe",
@@ -366,12 +374,14 @@
"systemInfo": "Información del sistema",
"terminal": "Terminal",
"title": "Título",
"triggerPhrase": "Frase de activación",
"unknownError": "Error desconocido",
"update": "Actualizar",
"updateAvailable": "Actualización Disponible",
"updated": "Actualizado",
"updating": "Actualizando",
"upload": "Subir",
"usageHint": "Sugerencia de uso",
"user": "Usuario",
"videoFailedToLoad": "Falló la carga del video",
"workflow": "Flujo de trabajo"

View File

@@ -253,10 +253,13 @@
"g": {
"about": "À propos",
"add": "Ajouter",
"addNodeFilterCondition": "Ajouter une condition de filtre de nœud",
"all": "Tout",
"amount": "Quantité",
"apply": "Appliquer",
"architecture": "Architecture",
"audioFailedToLoad": "Échec du chargement de l'audio",
"author": "Auteur",
"back": "Retour",
"cancel": "Annuler",
"capture": "capture",
@@ -314,10 +317,12 @@
"installing": "Installation",
"interrupted": "Interrompu",
"keybinding": "Raccourci clavier",
"keybindingAlreadyExists": "Le raccourci clavier existe déjà",
"learnMore": "En savoir plus",
"loadAllFolders": "Charger tous les dossiers",
"loadWorkflow": "Charger le flux de travail",
"loading": "Chargement",
"loadingPanel": "Chargement du panneau {panel}...",
"login": "Connexion",
"logs": "Journaux",
"migrate": "Migrer",
@@ -334,9 +339,12 @@
"ok": "OK",
"openNewIssue": "Ouvrir un nouveau problème",
"overwrite": "Écraser",
"preview": "APERÇU",
"progressCountOf": "sur",
"reconnected": "Reconnecté",
"reconnecting": "Reconnexion",
"refresh": "Rafraîchir",
"releaseTitle": "Publication de {package} {version}",
"reloadToApplyChanges": "Recharger pour appliquer les modifications",
"rename": "Renommer",
"reportIssue": "Envoyer le rapport",
@@ -366,12 +374,14 @@
"systemInfo": "Informations système",
"terminal": "Terminal",
"title": "Titre",
"triggerPhrase": "Phrase déclencheuse",
"unknownError": "Erreur inconnue",
"update": "Mettre à jour",
"updateAvailable": "Mise à jour disponible",
"updated": "Mis à jour",
"updating": "Mise à jour",
"upload": "Téléverser",
"usageHint": "Conseil d'utilisation",
"user": "Utilisateur",
"videoFailedToLoad": "Échec du chargement de la vidéo",
"workflow": "Flux de travail"

View File

@@ -253,10 +253,13 @@
"g": {
"about": "情報",
"add": "追加",
"addNodeFilterCondition": "ノードフィルター条件を追加",
"all": "すべて",
"amount": "量",
"apply": "適用する",
"architecture": "アーキテクチャ",
"audioFailedToLoad": "オーディオの読み込みに失敗しました",
"author": "作者",
"back": "戻る",
"cancel": "キャンセル",
"capture": "キャプチャ",
@@ -314,10 +317,12 @@
"installing": "インストール中",
"interrupted": "中断されました",
"keybinding": "キーバインディング",
"keybindingAlreadyExists": "このキー割り当てはすでに存在します",
"learnMore": "詳細を学ぶ",
"loadAllFolders": "すべてのフォルダーを読み込む",
"loadWorkflow": "ワークフローを読み込む",
"loading": "読み込み中",
"loadingPanel": "{panel} パネルを読み込み中...",
"login": "ログイン",
"logs": "ログ",
"migrate": "移行する",
@@ -334,9 +339,12 @@
"ok": "OK",
"openNewIssue": "新しい問題を開く",
"overwrite": "上書き",
"preview": "プレビュー",
"progressCountOf": "の",
"reconnected": "再接続されました",
"reconnecting": "再接続中",
"refresh": "更新",
"releaseTitle": "{package} {version} リリース",
"reloadToApplyChanges": "変更を適用するには再読み込みしてください",
"rename": "名前を変更",
"reportIssue": "報告する",
@@ -366,12 +374,14 @@
"systemInfo": "システム情報",
"terminal": "ターミナル",
"title": "タイトル",
"triggerPhrase": "トリガーフレーズ",
"unknownError": "不明なエラー",
"update": "更新",
"updateAvailable": "更新が利用可能",
"updated": "更新済み",
"updating": "更新中",
"upload": "アップロード",
"usageHint": "使用ヒント",
"user": "ユーザー",
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
"workflow": "ワークフロー"

View File

@@ -253,10 +253,13 @@
"g": {
"about": "정보",
"add": "추가",
"addNodeFilterCondition": "노드 필터 조건 추가",
"all": "모두",
"amount": "수량",
"apply": "적용",
"architecture": "아키텍처",
"audioFailedToLoad": "오디오를 불러오지 못했습니다",
"author": "작성자",
"back": "뒤로",
"cancel": "취소",
"capture": "캡처",
@@ -314,10 +317,12 @@
"installing": "설치 중",
"interrupted": "중단됨",
"keybinding": "키 바인딩",
"keybindingAlreadyExists": "단축키가 이미 존재합니다",
"learnMore": "더 알아보기",
"loadAllFolders": "모든 폴더 로드",
"loadWorkflow": "워크플로 로드",
"loading": "로딩 중",
"loadingPanel": "{panel} 패널 불러오는 중...",
"login": "로그인",
"logs": "로그",
"migrate": "이전(migrate)",
@@ -334,9 +339,12 @@
"ok": "확인",
"openNewIssue": "새 문제 열기",
"overwrite": "덮어쓰기",
"preview": "미리보기",
"progressCountOf": "중",
"reconnected": "재연결됨",
"reconnecting": "재연결 중",
"refresh": "새로 고침",
"releaseTitle": "{package} {version} 릴리스",
"reloadToApplyChanges": "변경 사항을 적용하려면 새로 고침하세요.",
"rename": "이름 바꾸기",
"reportIssue": "보고서 보내기",
@@ -366,12 +374,14 @@
"systemInfo": "시스템 정보",
"terminal": "터미널",
"title": "제목",
"triggerPhrase": "트리거 문구",
"unknownError": "알 수 없는 오류",
"update": "업데이트",
"updateAvailable": "업데이트 가능",
"updated": "업데이트 됨",
"updating": "업데이트 중",
"upload": "업로드",
"usageHint": "사용 힌트",
"user": "사용자",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로"

View File

@@ -253,10 +253,13 @@
"g": {
"about": "О программе",
"add": "Добавить",
"addNodeFilterCondition": "Добавить условие фильтрации узла",
"all": "Все",
"amount": "Количество",
"apply": "Применить",
"architecture": "Архитектура",
"audioFailedToLoad": "Не удалось загрузить аудио",
"author": "Автор",
"back": "Назад",
"cancel": "Отмена",
"capture": "захват",
@@ -314,10 +317,12 @@
"installing": "Установка",
"interrupted": "Прервано",
"keybinding": "Привязка клавиш",
"keybindingAlreadyExists": "Горячая клавиша уже существует",
"learnMore": "Узнать больше",
"loadAllFolders": "Загрузить все папки",
"loadWorkflow": "Загрузить рабочий процесс",
"loading": "Загрузка",
"loadingPanel": "Загрузка панели {panel}...",
"login": "Вход",
"logs": "Логи",
"migrate": "Мигрировать",
@@ -334,9 +339,12 @@
"ok": "ОК",
"openNewIssue": "Открыть новую проблему",
"overwrite": "Перезаписать",
"preview": "ПРЕДПРОСМОТР",
"progressCountOf": "из",
"reconnected": "Переподключено",
"reconnecting": "Переподключение",
"refresh": "Обновить",
"releaseTitle": "Релиз {package} {version}",
"reloadToApplyChanges": "Перезагрузите, чтобы применить изменения",
"rename": "Переименовать",
"reportIssue": "Отправить отчёт",
@@ -366,12 +374,14 @@
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"title": "Заголовок",
"triggerPhrase": "Триггерная фраза",
"unknownError": "Неизвестная ошибка",
"update": "Обновить",
"updateAvailable": "Доступно обновление",
"updated": "Обновлено",
"updating": "Обновление",
"upload": "Загрузить",
"usageHint": "Подсказка по использованию",
"user": "Пользователь",
"videoFailedToLoad": "Не удалось загрузить видео",
"workflow": "Рабочий процесс"

View File

@@ -253,10 +253,13 @@
"g": {
"about": "关于",
"add": "添加",
"addNodeFilterCondition": "添加节点筛选条件",
"all": "全部",
"amount": "数量",
"apply": "应用",
"architecture": "架构",
"audioFailedToLoad": "音频加载失败",
"author": "作者",
"back": "返回",
"cancel": "取消",
"capture": "捕获",
@@ -314,10 +317,12 @@
"installing": "正在安装",
"interrupted": "已中断",
"keybinding": "按键绑定",
"keybindingAlreadyExists": "快捷键已存在",
"learnMore": "了解更多",
"loadAllFolders": "加载所有文件夹",
"loadWorkflow": "加载工作流",
"loading": "加载中",
"loadingPanel": "正在加载{panel}面板...",
"login": "登录",
"logs": "日志",
"migrate": "迁移",
@@ -334,9 +339,12 @@
"ok": "确定",
"openNewIssue": "打开新问题",
"overwrite": "覆盖",
"preview": "预览",
"progressCountOf": "共",
"reconnected": "已重新连接",
"reconnecting": "重新连接中",
"refresh": "刷新",
"releaseTitle": "{package} {version} 发布",
"reloadToApplyChanges": "重新加载以应用更改",
"rename": "重命名",
"reportIssue": "发送报告",
@@ -366,12 +374,14 @@
"systemInfo": "系统信息",
"terminal": "终端",
"title": "标题",
"triggerPhrase": "触发短语",
"unknownError": "未知错误",
"update": "更新",
"updateAvailable": "有更新可用",
"updated": "已更新",
"updating": "更新中",
"upload": "上传",
"usageHint": "使用提示",
"user": "用户",
"videoFailedToLoad": "视频加载失败",
"workflow": "工作流"

View File

@@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useRightSidebarTabStore = defineStore('rightSidebarTab', () => {
const isVisible = ref(false)
const toggleVisibility = () => {
isVisible.value = !isVisible.value
}
const show = () => {
isVisible.value = true
}
const hide = () => {
isVisible.value = false
}
return {
isVisible,
toggleVisibility,
show,
hide
}
})

View File

@@ -56,6 +56,15 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
}
}
/**
* ComfyUI extensions of litegraph interfaces
*/
declare module '@comfyorg/litegraph/dist/interfaces' {
interface IWidgetLocator {
[key: symbol]: unknown
}
}
/**
* ComfyUI extensions of litegraph
*/

View File

@@ -317,6 +317,7 @@ const onGraphReady = () => {
z-index: 10;
grid-column: 3;
grid-row: 2;
display: flex;
}
.comfyui-body-bottom {