mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-05 20:10:07 +00:00
Compare commits
27 Commits
command-bo
...
pr/subgrap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fcf5c8348 | ||
|
|
ef1852d551 | ||
|
|
983ebb2ba7 | ||
|
|
db71365768 | ||
|
|
17d7ba8bcb | ||
|
|
24a386c766 | ||
|
|
c42c9315f4 | ||
|
|
d068b8351e | ||
|
|
1cf8087be0 | ||
|
|
9c31d708a2 | ||
|
|
9a70e927aa | ||
|
|
dc444faa75 | ||
|
|
a055ec2dff | ||
|
|
2138ceea80 | ||
|
|
7972550f6b | ||
|
|
c7baf3c340 | ||
|
|
8403bd0e3e | ||
|
|
90f54414ab | ||
|
|
505c242ff4 | ||
|
|
fbc6edde25 | ||
|
|
2c215a6251 | ||
|
|
71a43193df | ||
|
|
d0d13bfe4c | ||
|
|
a1a8d48544 | ||
|
|
d22d62b670 | ||
|
|
8e357c41e3 | ||
|
|
c4912dcd54 |
@@ -210,29 +210,52 @@ echo "Last stable release: $LAST_STABLE"
|
||||
echo "WARNING: PR #$PR not on main branch!"
|
||||
done
|
||||
```
|
||||
3. Create comprehensive release notes including:
|
||||
- **Version Change**: Show version bump details
|
||||
- **Changelog** grouped by type:
|
||||
- 🚀 **Features** (feat:)
|
||||
- 🐛 **Bug Fixes** (fix:)
|
||||
- 💥 **Breaking Changes** (BREAKING CHANGE)
|
||||
- 📚 **Documentation** (docs:)
|
||||
- 🔧 **Maintenance** (chore:, refactor:)
|
||||
- ⬆️ **Dependencies** (deps:, dependency updates)
|
||||
- **Litegraph Changes** (if version updated):
|
||||
- 🚀 Features: ${LITEGRAPH_FEATURES}
|
||||
- 🐛 Bug Fixes: ${LITEGRAPH_FIXES}
|
||||
- 💥 Breaking Changes: ${LITEGRAPH_BREAKING}
|
||||
- 🔧 Other Changes: ${LITEGRAPH_OTHER}
|
||||
- **Other Major Dependencies**: ${OTHER_DEP_CHANGES}
|
||||
- Include PR numbers and links
|
||||
- Add issue references (Fixes #123)
|
||||
4. **Save release notes:**
|
||||
3. Create standardized release notes using this exact template:
|
||||
```bash
|
||||
# Save release notes for PR and GitHub release
|
||||
echo "$RELEASE_NOTES" > release-notes-${NEW_VERSION}.md
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/${BASE_TAG}...v${NEW_VERSION}
|
||||
EOF
|
||||
```
|
||||
5. **CONTENT REVIEW**: Release notes clear and comprehensive with dependency details?
|
||||
4. **Parse commits and populate template:**
|
||||
- Group commits by conventional commit type (feat:, fix:, chore:, etc.)
|
||||
- Extract PR numbers from commit messages
|
||||
- For breaking changes, analyze if changes affect:
|
||||
- Public APIs (app object, api module)
|
||||
- Extension/workspace manager APIs
|
||||
- Node schema, workflow schema, or other public schemas
|
||||
- Any other public-facing interfaces
|
||||
- For dependency updates, list version changes with PR numbers
|
||||
- Remove empty sections (e.g., if no documentation changes)
|
||||
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
|
||||
5. **CONTENT REVIEW**: Release notes follow standard format?
|
||||
|
||||
### Step 9: Create Version Bump PR
|
||||
|
||||
@@ -273,38 +296,12 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
--body-file release-notes-${NEW_VERSION}.md \
|
||||
--label "Release"
|
||||
```
|
||||
3. **Add required sections to PR body:**
|
||||
3. **Update PR with release notes:**
|
||||
```bash
|
||||
# Create PR body with release notes plus required sections
|
||||
cat > pr-body.md << EOF
|
||||
${RELEASE_NOTES}
|
||||
|
||||
## Breaking Changes
|
||||
${BREAKING_CHANGES:-None}
|
||||
|
||||
## Testing Performed
|
||||
- ✅ Full test suite (unit, component)
|
||||
- ✅ TypeScript compilation
|
||||
- ✅ Linting checks
|
||||
- ✅ Build verification
|
||||
- ✅ Security audit
|
||||
|
||||
## Distribution Channels
|
||||
- GitHub Release (with dist.zip)
|
||||
- PyPI Package (comfyui-frontend-package)
|
||||
- npm Package (@comfyorg/comfyui-frontend-types)
|
||||
|
||||
## Post-Release Tasks
|
||||
- [ ] Verify all distribution channels
|
||||
- [ ] Update external documentation
|
||||
- [ ] Monitor for issues
|
||||
EOF
|
||||
# For workflow-created PRs, update the body with our release notes
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
4. Update PR with enhanced description:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file pr-body.md
|
||||
```
|
||||
5. **PR REVIEW**: Version bump PR created and enhanced correctly?
|
||||
4. **PR REVIEW**: Version bump PR created with standardized release notes?
|
||||
|
||||
### Step 10: Critical Release PR Verification
|
||||
|
||||
@@ -592,55 +589,46 @@ The command implements multiple quality gates:
|
||||
- Draft release status
|
||||
- Python package specs require that prereleases use alpha/beta/rc as the preid
|
||||
|
||||
## Common Issues and Solutions
|
||||
## Critical Implementation Notes
|
||||
|
||||
### Issue: Pre-release Version Confusion
|
||||
**Problem**: Not sure whether to promote pre-release or create new version
|
||||
**Solution**:
|
||||
- Follow semver standards: a prerelease version is followed by a normal release. It should have the same major, minor, and patch versions as the prerelease.
|
||||
When executing this release process, pay attention to these key aspects:
|
||||
|
||||
### Issue: Wrong Commit Count
|
||||
**Problem**: Changelog includes commits from other branches
|
||||
**Solution**: Always use `--first-parent` flag with git log
|
||||
### Version Handling
|
||||
- For pre-release versions (e.g., 1.24.0-rc.1), the next stable release should be the same version without the suffix (1.24.0)
|
||||
- Never skip version numbers - follow semantic versioning strictly
|
||||
|
||||
**Update**: Sometimes update-locales doesn't add [skip ci] - always verify!
|
||||
### Commit History Analysis
|
||||
- **ALWAYS** use `--first-parent` flag with git log to avoid including commits from merged feature branches
|
||||
- Verify PR merge targets before including them in changelogs:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
|
||||
### Issue: Missing PRs in Changelog
|
||||
**Problem**: PR was merged to different branch
|
||||
**Solution**: Verify PR merge target with:
|
||||
```bash
|
||||
gh pr view ${PR_NUMBER} --json baseRefName
|
||||
```
|
||||
### Release Workflow Triggers
|
||||
- The "Release" label on the PR is **CRITICAL** - without it, PyPI/npm publishing won't occur
|
||||
- Check for `[skip ci]` in commit messages before merging - this blocks the release workflow
|
||||
- If you encounter `[skip ci]`, push an empty commit to override it:
|
||||
```bash
|
||||
git commit --allow-empty -m "Trigger release workflow"
|
||||
```
|
||||
|
||||
### Issue: Incomplete Dependency Changelog
|
||||
**Problem**: Litegraph or other dependency updates only show version bump, not actual changes
|
||||
**Solution**: The command now automatically:
|
||||
- Detects litegraph version changes between releases
|
||||
- Clones the litegraph repository temporarily
|
||||
- Extracts and categorizes changes between versions
|
||||
- Includes detailed litegraph changelog in release notes
|
||||
- Cleans up temporary files after analysis
|
||||
### PR Creation Details
|
||||
- Version bump PRs come from `comfy-pr-bot`, not `github-actions`
|
||||
- The workflow typically completes in 20-30 seconds
|
||||
- Always wait for the PR to be created before trying to edit it
|
||||
|
||||
### Issue: Release Failed Due to [skip ci]
|
||||
**Problem**: Release workflow didn't trigger after merge
|
||||
**Prevention**: Always avoid this scenario
|
||||
- Ensure that `[skip ci]` or similar flags are NOT in the `HEAD` commit message of the PR
|
||||
- Push a new, empty commit to the PR
|
||||
- Always double-check this immediately before merging
|
||||
### Breaking Changes Detection
|
||||
- Analyze changes to public-facing APIs:
|
||||
- The `app` object and its methods
|
||||
- The `api` module exports
|
||||
- Extension and workspace manager interfaces
|
||||
- Node schema, workflow schema, and other public schemas
|
||||
- Any modifications to these require marking as breaking changes
|
||||
|
||||
**Recovery Strategy**:
|
||||
1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1)
|
||||
2. Merge the revert PR
|
||||
3. Run version bump workflow again
|
||||
4. This creates a fresh PR without [skip ci]
|
||||
Benefits: Cleaner than creating extra version numbers
|
||||
|
||||
## Key Learnings & Notes
|
||||
|
||||
1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions`
|
||||
2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds
|
||||
3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't
|
||||
4. **Recovery Options**: Reverting version is cleaner than creating extra versions
|
||||
5. **Dependency Tracking**: Command now automatically includes litegraph and major dependency changes in changelogs
|
||||
6. **Litegraph Integration**: Temporary cloning of litegraph repo provides detailed change analysis between versions
|
||||
### Recovery Procedures
|
||||
If the release workflow fails to trigger:
|
||||
1. Create a revert PR to restore the previous version
|
||||
2. Merge the revert
|
||||
3. Re-run the version bump workflow
|
||||
4. This approach is cleaner than creating extra version numbers
|
||||
|
||||
|
||||
@@ -138,14 +138,50 @@ For each commit:
|
||||
```bash
|
||||
gh pr create --base core/X.Y --head release/1.23.5 \
|
||||
--title "[Release] v1.23.5" \
|
||||
--body "..." \
|
||||
--body "Release notes will be added shortly..." \
|
||||
--label "Release"
|
||||
```
|
||||
3. **CRITICAL**: Verify "Release" label is added
|
||||
4. PR description should include:
|
||||
- Version: `1.23.4` → `1.23.5`
|
||||
- Included fixes (link to previous PR)
|
||||
- Release notes for users
|
||||
4. Create standardized release notes:
|
||||
```bash
|
||||
cat > release-notes-${NEW_VERSION}.md << 'EOF'
|
||||
## ⚠️ Breaking Changes
|
||||
<!-- List breaking changes if any, otherwise remove this entire section -->
|
||||
- Breaking change description (#PR_NUMBER)
|
||||
|
||||
---
|
||||
|
||||
## What's Changed
|
||||
|
||||
### 🚀 Features
|
||||
<!-- List features here, one per line with PR reference -->
|
||||
- Feature description (#PR_NUMBER)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
<!-- List bug fixes here, one per line with PR reference -->
|
||||
- Bug fix description (#PR_NUMBER)
|
||||
|
||||
### 🔧 Maintenance
|
||||
<!-- List refactoring, chore, and other maintenance items -->
|
||||
- Maintenance item description (#PR_NUMBER)
|
||||
|
||||
### 📚 Documentation
|
||||
<!-- List documentation changes if any, remove section if empty -->
|
||||
- Documentation update description (#PR_NUMBER)
|
||||
|
||||
### ⬆️ Dependencies
|
||||
<!-- List dependency updates -->
|
||||
- Updated dependency from vX.X.X to vY.Y.Y (#PR_NUMBER)
|
||||
|
||||
**Full Changelog**: https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${CURRENT_VERSION}...v${NEW_VERSION}
|
||||
EOF
|
||||
```
|
||||
- For hotfixes, typically only populate the "Bug Fixes" section
|
||||
- Include links to the cherry-picked PRs/commits
|
||||
- Update the PR body with the release notes:
|
||||
```bash
|
||||
gh pr edit ${PR_NUMBER} --body-file release-notes-${NEW_VERSION}.md
|
||||
```
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
|
||||
131
.claude/commands/pr.md
Normal file
131
.claude/commands/pr.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Create PR
|
||||
|
||||
Automate PR creation with proper tags, labels, and concise summary.
|
||||
|
||||
## Step 1: Check Prerequisites
|
||||
|
||||
```bash
|
||||
# Ensure you have uncommitted changes
|
||||
git status
|
||||
|
||||
# If changes exist, commit them first
|
||||
git add .
|
||||
git commit -m "[tag] Your commit message"
|
||||
```
|
||||
|
||||
## Step 2: Push and Create PR
|
||||
|
||||
You'll create the PR with the following structure:
|
||||
|
||||
### PR Tags (use in title)
|
||||
|
||||
- `[feat]` - New features → label: `enhancement`
|
||||
- `[bugfix]` - Bug fixes → label: `verified bug`
|
||||
- `[refactor]` - Code restructuring → label: `enhancement`
|
||||
- `[docs]` - Documentation → label: `documentation`
|
||||
- `[test]` - Test changes → label: `enhancement`
|
||||
- `[ci]` - CI/CD changes → label: `enhancement`
|
||||
|
||||
### Label Mapping
|
||||
|
||||
#### General Labels
|
||||
|
||||
- Feature/Enhancement: `enhancement`
|
||||
- Bug fixes: `verified bug`
|
||||
- Documentation: `documentation`
|
||||
- Dependencies: `dependencies`
|
||||
- Performance: `Performance`
|
||||
- Desktop app: `Electron`
|
||||
|
||||
#### Product Area Labels
|
||||
|
||||
**Core Features**
|
||||
|
||||
- `area:nodes` - Node-related functionality
|
||||
- `area:workflows` - Workflow management
|
||||
- `area:queue` - Queue system
|
||||
- `area:models` - Model handling
|
||||
- `area:templates` - Template system
|
||||
- `area:subgraph` - Subgraph functionality
|
||||
|
||||
**UI Components**
|
||||
|
||||
- `area:ui` - General user interface improvements
|
||||
- `area:widgets` - Widget system
|
||||
- `area:dom-widgets` - DOM-based widgets
|
||||
- `area:links` - Connection links between nodes
|
||||
- `area:groups` - Node grouping functionality
|
||||
- `area:reroutes` - Reroute nodes
|
||||
- `area:previews` - Preview functionality
|
||||
- `area:minimap` - Minimap navigation
|
||||
- `area:floating-toolbox` - Floating toolbar
|
||||
- `area:mask-editor` - Mask editing tools
|
||||
|
||||
**Navigation & Organization**
|
||||
|
||||
- `area:navigation` - Navigation system
|
||||
- `area:search` - Search functionality
|
||||
- `area:workspace-management` - Workspace features
|
||||
- `area:topbar-menu` - Top bar menu
|
||||
- `area:help-menu` - Help menu system
|
||||
|
||||
**System Features**
|
||||
|
||||
- `area:settings` - Settings/preferences
|
||||
- `area:hotkeys` - Keyboard shortcuts
|
||||
- `area:undo-redo` - Undo/redo system
|
||||
- `area:customization` - Customization features
|
||||
- `area:auth` - Authentication
|
||||
- `area:comms` - Communication/networking
|
||||
|
||||
**Development & Infrastructure**
|
||||
|
||||
- `area:CI/CD` - CI/CD pipeline
|
||||
- `area:testing` - Testing infrastructure
|
||||
- `area:vue-migration` - Vue migration work
|
||||
- `area:manager` - ComfyUI Manager integration
|
||||
|
||||
**Platform-Specific**
|
||||
|
||||
- `area:mobile` - Mobile support
|
||||
- `area:3d` - 3D-related features
|
||||
|
||||
**Special Areas**
|
||||
|
||||
- `area:i18n` - Translation/internationalization
|
||||
- `area:CNR` - Comfy Node Registry
|
||||
|
||||
## Step 3: Execute PR Creation
|
||||
|
||||
```bash
|
||||
# First, push your branch
|
||||
git push -u origin $(git branch --show-current)
|
||||
|
||||
# Then create the PR (replace placeholders)
|
||||
gh pr create \
|
||||
--title "[TAG] Brief description" \
|
||||
--body "$(cat <<'EOF'
|
||||
## Summary
|
||||
One sentence describing what changed and why.
|
||||
|
||||
## Changes
|
||||
- **What**: Core functionality added/modified
|
||||
- **Breaking**: Any breaking changes (if none, omit this line)
|
||||
- **Dependencies**: New dependencies (if none, omit this line)
|
||||
|
||||
## Review Focus
|
||||
- Critical design decisions or edge cases that need attention
|
||||
|
||||
Fixes #ISSUE_NUMBER
|
||||
EOF
|
||||
)" \
|
||||
--label "APPROPRIATE_LABEL" \
|
||||
--base main
|
||||
```
|
||||
|
||||
## Additional Options
|
||||
|
||||
- Add multiple labels: `--label "enhancement,Performance"`
|
||||
- Request reviewers: `--reviewer @username`
|
||||
- Mark as draft: `--draft`
|
||||
- Open in browser after creation: `--web`
|
||||
@@ -49,7 +49,7 @@ DO NOT use deprecated PrimeVue components. Use these replacements instead:
|
||||
|
||||
## Development Guidelines
|
||||
1. Leverage VueUse functions for performance-enhancing styles
|
||||
2. Use lodash for utility functions
|
||||
2. Use es-toolkit for utility functions
|
||||
3. Use TypeScript for type safety
|
||||
4. Implement proper props and emits definitions
|
||||
5. Utilize Vue 3's Teleport component when needed
|
||||
|
||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -18,7 +18,7 @@ Use Tailwind CSS for styling
|
||||
|
||||
Leverage VueUse functions for performance-enhancing styles
|
||||
|
||||
Use lodash for utility functions
|
||||
Use es-toolkit for utility functions
|
||||
|
||||
Use TypeScript for type safety
|
||||
|
||||
|
||||
20
.github/pull_request_template.md
vendored
Normal file
20
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## Summary
|
||||
|
||||
<!-- One sentence describing what changed and why. -->
|
||||
|
||||
## Changes
|
||||
|
||||
- **What**: <!-- Core functionality added/modified -->
|
||||
- **Breaking**: <!-- Any breaking changes (if none, remove this line) -->
|
||||
- **Dependencies**: <!-- New dependencies (if none, remove this line) -->
|
||||
|
||||
## Review Focus
|
||||
|
||||
<!-- Critical design decisions or edge cases that need attention -->
|
||||
|
||||
<!-- If this PR fixes an issue, uncomment and update the line below -->
|
||||
<!-- Fixes #ISSUE_NUMBER -->
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
<!-- Add screenshots or video recording to help explain your changes -->
|
||||
1
.github/workflows/lint-and-format.yaml
vendored
1
.github/workflows/lint-and-format.yaml
vendored
@@ -56,6 +56,7 @@ jobs:
|
||||
run: |
|
||||
npm run lint
|
||||
npm run format:check
|
||||
npm run knip
|
||||
|
||||
- name: Comment on PR about auto-fix
|
||||
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
5
.github/workflows/update-manager-types.yaml
vendored
5
.github/workflows/update-manager-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated ComfyUI-Manager API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
5
.github/workflows/update-registry-types.yaml
vendored
5
.github/workflows/update-registry-types.yaml
vendored
@@ -61,6 +61,11 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated Comfy Registry API types..."
|
||||
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,6 +7,9 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
|
||||
@@ -75,7 +75,7 @@ The development of successive minor versions overlaps. For example, while versio
|
||||
<summary>v1.5: Native translation (i18n)</summary>
|
||||
|
||||
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
|
||||
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, Korean, or Arabic. This native
|
||||
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
|
||||
|
||||
More details available here: https://blog.comfy.org/p/native-localization-support-i18n
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"id": "dec788c2-9829-4a5d-a1ee-d6f0a678b42a",
|
||||
"revision": 0,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [413, 389],
|
||||
"size": [425.27801513671875, 180.6060791015625],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [6]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": ["text, watermark"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": [415, 186],
|
||||
"size": [422.84503173828125, 164.31304931640625],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"slot_index": 0,
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": [473, 609],
|
||||
"size": [315, 106],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [512, 512, 1]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"pos": [863, 186],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": 6
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"slot_index": 0,
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "VAEDecode",
|
||||
"pos": [1209, 188],
|
||||
"size": [210, 46],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"slot_index": 0,
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "SaveImage",
|
||||
"pos": [1451, 189],
|
||||
"size": [210, 58],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [26, 474],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": [1]
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": [3, 5]
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 4, 0, 3, 0, "MODEL"],
|
||||
[2, 5, 0, 3, 3, "LATENT"],
|
||||
[3, 4, 1, 6, 0, "CLIP"],
|
||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[5, 4, 1, 7, 0, "CLIP"],
|
||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||
[7, 3, 0, 8, 0, "LATENT"],
|
||||
[8, 4, 2, 8, 1, "VAE"],
|
||||
[9, 8, 0, 9, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [372.66668701171875, 415.33331298828125],
|
||||
"linkIds": [3]
|
||||
}
|
||||
],
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 3,
|
||||
"parentId": 1
|
||||
}
|
||||
],
|
||||
"frontendVersion": "1.26.1"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import fs from 'fs'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import type { Request, Route } from 'playwright'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@@ -75,7 +75,9 @@ export default class TaskHistory {
|
||||
|
||||
private async handleGetView(route: Route) {
|
||||
const fileName = getFilenameParam(route.request())
|
||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
||||
if (!this.outputContentTypes.has(fileName)) {
|
||||
return route.continue()
|
||||
}
|
||||
|
||||
const asset = this.loadAsset(fileName)
|
||||
return route.fulfill({
|
||||
|
||||
@@ -24,8 +24,14 @@ test.describe('Minimap', () => {
|
||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||
await expect(minimapViewport).toBeVisible()
|
||||
|
||||
await expect(minimapContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapContainer).toHaveCSS('z-index', '1000')
|
||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
||||
|
||||
// position and z-index validation moved to the parent container of the minimap
|
||||
const minimapMainContainer = comfyPage.page.locator(
|
||||
'.minimap-main-container'
|
||||
)
|
||||
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
|
||||
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
|
||||
})
|
||||
|
||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||
|
||||
@@ -100,4 +100,29 @@ test.describe('LiteGraph Native Reroute Node', () => {
|
||||
'native_reroute_context_menu.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can delete link that is connected to two reroutes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
|
||||
await comfyPage.loadWorkflow(
|
||||
'reroute/single-native-reroute-default-workflow'
|
||||
)
|
||||
|
||||
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
|
||||
// since the link is a bezier curve and not a straight line.
|
||||
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
|
||||
|
||||
// Click the middle point of the link to open the context menu.
|
||||
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
|
||||
|
||||
// Click the "Delete" context menu option.
|
||||
await comfyPage.page
|
||||
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'native_reroute_delete_from_midpoint_context_menu.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -317,6 +317,25 @@ test.describe('Workflows sidebar', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
})
|
||||
|
||||
const { workflowsTab } = comfyPage.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
await workflowsTab
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Duplicate')
|
||||
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
await comfyPage.setupWorkflowsDirectory({
|
||||
'workflow1.json': 'default.json'
|
||||
|
||||
@@ -704,4 +704,103 @@ test.describe('Subgraph Operations', () => {
|
||||
expect(finalCount).toBe(parentCount)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Navigation Hotkeys', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
|
||||
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Change the Exit Subgraph keybinding from Escape to Alt+Q
|
||||
await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'q',
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [
|
||||
{
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: {
|
||||
key: 'Escape',
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Reload the page
|
||||
await comfyPage.page.reload()
|
||||
await comfyPage.page.waitForTimeout(1024)
|
||||
|
||||
// Navigate into subgraph
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Test that Escape no longer exits subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Test that Alt+Q now exits subgraph
|
||||
await comfyPage.page.keyboard.press('Alt+q')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
|
||||
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
|
||||
|
||||
// Verify we're in a subgraph
|
||||
if (!(await isInSubgraph(comfyPage))) {
|
||||
throw new Error('Not in subgraph')
|
||||
}
|
||||
|
||||
// Open settings dialog using hotkey
|
||||
await comfyPage.page.keyboard.press('Control+,')
|
||||
await comfyPage.page.waitForSelector('.settings-container', {
|
||||
state: 'visible'
|
||||
})
|
||||
|
||||
// Press Escape - should close dialog, not exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Dialog should be closed
|
||||
await expect(
|
||||
comfyPage.page.locator('.settings-container')
|
||||
).not.toBeVisible()
|
||||
|
||||
// Should still be in subgraph
|
||||
expect(await isInSubgraph(comfyPage)).toBe(true)
|
||||
|
||||
// Press Escape again - now should exit subgraph
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await isInSubgraph(comfyPage)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,10 @@ export default [
|
||||
ignores: [
|
||||
'src/scripts/*',
|
||||
'src/extensions/core/*',
|
||||
'src/types/vue-shim.d.ts'
|
||||
'src/types/vue-shim.d.ts',
|
||||
// Generated files that don't need linting
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
75
knip.config.ts
Normal file
75
knip.config.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { KnipConfig } from 'knip'
|
||||
|
||||
const config: KnipConfig = {
|
||||
entry: [
|
||||
'src/main.ts',
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'eslint.config.js',
|
||||
'tailwind.config.js',
|
||||
'postcss.config.js',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts',
|
||||
'vitest.config.ts',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
project: [
|
||||
'src/**/*.{js,ts,vue}',
|
||||
'tests-ui/**/*.{js,ts,vue}',
|
||||
'browser_tests/**/*.{js,ts}',
|
||||
'scripts/**/*.{js,ts}'
|
||||
],
|
||||
ignore: [
|
||||
// Generated files
|
||||
'dist/**',
|
||||
'types/**',
|
||||
'node_modules/**',
|
||||
// Config files that might not show direct usage
|
||||
'.husky/**',
|
||||
// Temporary or cache files
|
||||
'.vite/**',
|
||||
'coverage/**',
|
||||
// i18n config
|
||||
'.i18nrc.cjs',
|
||||
// Test setup files
|
||||
'browser_tests/globalSetup.ts',
|
||||
'browser_tests/globalTeardown.ts',
|
||||
'browser_tests/utils/**',
|
||||
// Scripts
|
||||
'scripts/**',
|
||||
// Vite config files
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
// Auto generated manager types
|
||||
'src/types/generatedManagerTypes.ts'
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
// Vue-specific configuration
|
||||
vue: true,
|
||||
// Only check for unused files, disable all other rules
|
||||
// TODO: Gradually enable other rules - see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4888
|
||||
rules: {
|
||||
binaries: 'off',
|
||||
classMembers: 'off',
|
||||
dependencies: 'off',
|
||||
devDependencies: 'off',
|
||||
duplicates: 'off',
|
||||
enumMembers: 'off',
|
||||
exports: 'off',
|
||||
nsExports: 'off',
|
||||
nsTypes: 'off',
|
||||
types: 'off',
|
||||
unlisted: 'off'
|
||||
},
|
||||
// Include dependencies analysis
|
||||
includeEntryExports: true,
|
||||
// Workspace configuration for monorepo-like structure
|
||||
workspaces: {
|
||||
'.': {
|
||||
entry: ['src/main.ts']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
565
package-lock.json
generated
565
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -30,12 +30,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -62,7 +62,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -80,6 +79,7 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -1001,6 +1001,40 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
"integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
|
||||
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz",
|
||||
"integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -3069,6 +3103,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz",
|
||||
"integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.5",
|
||||
"@emnapi/runtime": "^1.4.5",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -3120,6 +3167,275 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.6.1.tgz",
|
||||
"integrity": "sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-android-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-xjL/FKKc5p8JkFWiH7pJWSzsewif3fRf1rw2qiRxRvq1uIa6l7Zoa14Zq2TNWEsqDjdeOrlJtfWiPNRnevK0oQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-arm64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.6.1.tgz",
|
||||
"integrity": "sha512-u0yrJ3NHE0zyCjiYpIyz4Vmov21MA0yFKbhHgixDU/G6R6nvC8ZpuSFql3+7C8ttAK9p8WpqOGweepfcilH5Bw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-darwin-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-2lox165h1EhzxcC8edUy0znXC/hnAbUPaMpYKVlzLpB2AoYmgU4/pmofFApj+axm2FXpNamjcppld8EoHo06rw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-freebsd-x64": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.6.1.tgz",
|
||||
"integrity": "sha512-F45MhEQ7QbHfsvZtVNuA/9obu3il7QhpXYmCMfxn7Zt9nfAOw4pQ8hlS5DroHVp3rW35u9F7x0sixk/QEAi3qQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-r+3+MTTl0tD4NoWbfTIItAxJvuyIU7V0fwPDXrv7Uj64vZ3OYaiyV+lVaeU89Bk/FUUQxeUpWBwdKNKHjyRNQw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.6.1.tgz",
|
||||
"integrity": "sha512-TBTZ63otsWZ72Z8ZNK2JVS0HW1w9zgOixJTFDNrYPUUW1pXGa28KAjQ1yGawj242WLAdu3lwdNIWtkxeO2BLxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-SjwhNynjSG2yMdyA0f7wz7Yvo3ppejO+ET7n2oiI7ApCXrwxMzeRWjBzQt+oVWr2HzVOfaEcDS9rMtnR83ulig==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-f4EMidK6rosInBzPMnJ0Ri4RttFCvvLNUNDFUBtELW/MFkBwPTDlvbsmW0u0Mk/ruBQ2WmRfOZ6tT62kWMcX2Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-1umENVKeUsrWnf5IlF/6SM7DCv8G6CoKI2LnYR6qhZuLYDPS4PBZ0Jow3UDV9Rtbv5KRPcA3/uXjI88ntWIcOQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-Hjyp1FRdJhsEpIxsZq5VcDuFc8abC0Bgy8DWEa31trCKoTz7JqA7x3E2dkFbrAKsEFmZZ0NvuG5Ip3oIRARhow==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-ODJOJng6f3QxpAXhLel3kyWs8rPsJeo9XIZHzA7p//e+5kLMDU7bTVk4eZnUHuxsqsB8MEvPCicJkKCEuur5Ag==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-hCzRiLhqe1ZOpHTsTGKp7gnMJRORlbCthawBueer2u22RVAka74pV/+4pP1tqM07mSlQn7VATuWaDw9gCl+cVg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.6.1.tgz",
|
||||
"integrity": "sha512-JansPD8ftOzMYIC3NfXJ68tt63LEcIAx44Blx6BAd7eY880KX7A0KN3hluCrelCz5aQkPaD95g8HBiJmKaEi2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.6.1.tgz",
|
||||
"integrity": "sha512-R78ES1rd4z2x5NrFPtSWb/ViR1B8wdl+QN2X8DdtoYcqZE/4tvWtn9ZTCXMEzUp23tchJ2wUB+p6hXoonkyLpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.6.1.tgz",
|
||||
"integrity": "sha512-qAR3tYIf3afkij/XYunZtlz3OH2Y4ni10etmCFIJB5VRGsqJyI6Hl+2dXHHGJNwbwjXjSEH/KWJBpVroF3TxBw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-QqygWygIuemGkaBA48POOTeinbVvlamqh6ucm8arGDGz/mB5O00gXWxed12/uVrYEjeqbMkla/CuL3fjL3EKvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-ia32-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-N2+kkWwt/bk0JTCxhPuK8t8JMp3nd0n2OhwOkU8KO4a7roAJEa4K1SZVjMv5CqUIr5sx2CxtXRBoFDiORX5oBg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.6.1.tgz",
|
||||
"integrity": "sha512-DfMg3cU9bJUbN62Prbp4fGCtLgexuwyEaQGtZAp8xmi1Ii26uflOGx0FJkFTF6lVMSFoIRFvIL8gsw5/ZdHrMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@pinia/testing": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.5.tgz",
|
||||
@@ -4486,6 +4802,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/argparse": {
|
||||
"version": "1.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz",
|
||||
@@ -4559,12 +4886,6 @@
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
|
||||
"integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
@@ -7670,6 +7991,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.9",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz",
|
||||
"integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -8469,16 +8799,17 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.4"
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
@@ -8551,6 +8882,16 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-package-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz",
|
||||
"integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"walk-up-path": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
@@ -8846,6 +9187,22 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/formatly": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/formatly/-/formatly-0.2.4.tgz",
|
||||
"integrity": "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fd-package-json": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"formatly": "bin/index.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
@@ -10517,6 +10874,109 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.62.0.tgz",
|
||||
"integrity": "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/webpro"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/knip"
|
||||
},
|
||||
{
|
||||
"type": "polar",
|
||||
"url": "https://polar.sh/webpro-nl"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"formatly": "^0.2.4",
|
||||
"jiti": "^2.4.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"oxc-resolver": "^11.1.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.1",
|
||||
"smol-toml": "^1.3.4",
|
||||
"strip-json-comments": "5.0.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-validation-error": "^3.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"knip": "bin/knip.js",
|
||||
"knip-bun": "bin/knip-bun.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18",
|
||||
"typescript": ">=5.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/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,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/knip/node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/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,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/knip/node_modules/strip-json-comments": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.2.tgz",
|
||||
"integrity": "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||
@@ -10992,7 +11452,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
@@ -12388,6 +12849,22 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
|
||||
"integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"napi-postinstall": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/napi-postinstall"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -12741,6 +13218,41 @@
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oxc-resolver": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.6.1.tgz",
|
||||
"integrity": "sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxc-resolver/binding-android-arm-eabi": "11.6.1",
|
||||
"@oxc-resolver/binding-android-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-arm64": "11.6.1",
|
||||
"@oxc-resolver/binding-darwin-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-freebsd-x64": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm-musleabihf": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-arm64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-ppc64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-riscv64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-s390x-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-gnu": "11.6.1",
|
||||
"@oxc-resolver/binding-linux-x64-musl": "11.6.1",
|
||||
"@oxc-resolver/binding-wasm32-wasi": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-arm64-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-ia32-msvc": "11.6.1",
|
||||
"@oxc-resolver/binding-win32-x64-msvc": "11.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
@@ -14804,6 +15316,19 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz",
|
||||
"integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyyynthia"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -17400,6 +17925,16 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/walk-up-path": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
|
||||
"integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.1",
|
||||
"version": "1.26.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -21,8 +21,11 @@
|
||||
"test:component": "vitest run src/components/",
|
||||
"prepare": "husky || true",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint": "eslint src --cache",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts"
|
||||
@@ -38,7 +41,6 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
@@ -56,6 +58,7 @@
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.2",
|
||||
@@ -96,12 +99,12 @@
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "^2.1.7",
|
||||
|
||||
@@ -51,7 +51,7 @@ const template = await fetch('/templates/default.json')
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use lodash for utility functions
|
||||
- Use es-toolkit for utility functions
|
||||
- Implement proper TypeScript types
|
||||
- Follow Vue 3 composition API style guide
|
||||
- Use vue-i18n for ALL user-facing strings in `src/locales/en/main.json`
|
||||
|
||||
@@ -616,7 +616,8 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.comfy-load-3d canvas,
|
||||
.comfy-load-3d-animation canvas,
|
||||
.comfy-preview-3d canvas,
|
||||
.comfy-preview-3d-animation canvas{
|
||||
.comfy-preview-3d-animation canvas,
|
||||
.comfy-load-3d-viewer canvas{
|
||||
display: flex;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Breadcrumb from 'primevue/breadcrumb'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { computed, onUpdated, ref, watch } from 'vue'
|
||||
@@ -98,18 +97,6 @@ const home = computed(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Escape exits from the current subgraph.
|
||||
useEventListener(document, 'keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Check for overflow on breadcrumb items and collapse/expand the breadcrumb to fit
|
||||
let overflowObserver: ReturnType<typeof useOverflowObserver> | undefined
|
||||
watch(breadcrumbElement, (el) => {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex justify-between text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
<div>{{ t('apiNodesCostBreakdown.costPerRun') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.name"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
node.name
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-base font-medium leading-tight">
|
||||
{{ node.cost.toFixed(costPrecision) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
<template v-if="showTotal && nodes.length > 1">
|
||||
<Divider class="my-2" />
|
||||
<div class="flex justify-between items-center border-t px-3">
|
||||
<span class="text-sm">{{ t('apiNodesCostBreakdown.totalCost') }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-yellow-500 p-1"
|
||||
/>
|
||||
<span>{{ totalCost.toFixed(costPrecision) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ApiNodeCost } from '@/types/apiNodeTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
nodes,
|
||||
showTotal = true,
|
||||
costPrecision = 3
|
||||
} = defineProps<{
|
||||
nodes: ApiNodeCost[]
|
||||
showTotal?: boolean
|
||||
costPrecision?: number
|
||||
}>()
|
||||
|
||||
const totalCost = computed(() =>
|
||||
nodes.reduce((sum, node) => sum + node.cost, 0)
|
||||
)
|
||||
</script>
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 h-full">
|
||||
<div class="flex text-xs">
|
||||
<div>{{ t('apiNodesCostBreakdown.title') }}</div>
|
||||
</div>
|
||||
<ScrollPanel class="flex-grow h-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="nodeName in nodeNames"
|
||||
:key="nodeName"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--p-content-border-color)]"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-medium leading-tight">{{
|
||||
nodeName
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nodeNames } = defineProps<{ nodeNames: string[] }>()
|
||||
</script>
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import { useElementSize, useScroll, whenever } from '@vueuse/core'
|
||||
import { clamp, debounce } from 'lodash'
|
||||
import { clamp, debounce } from 'es-toolkit/compat'
|
||||
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
type GridState = {
|
||||
|
||||
@@ -36,6 +36,7 @@ import ListBox from 'primevue/listbox'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -169,8 +169,8 @@ 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 _ from 'es-toolkit/compat'
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { merge } from 'lodash'
|
||||
import { merge } from 'es-toolkit/compat'
|
||||
import Button from 'primevue/button'
|
||||
import {
|
||||
computed,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import PackEnableToggle from './PackEnableToggle.vue'
|
||||
|
||||
// Mock debounce to execute immediately
|
||||
vi.mock('lodash', () => ({
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
|
||||
}))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<div class="w-[100%] flex justify-between items-center">
|
||||
<div class="flex justify-start items-center">
|
||||
<div class="w-1 h-6 rounded-md" />
|
||||
<div class="w-6 h-6 relative overflow-hidden">
|
||||
<i class="pi pi-box text-xl text-muted" style="opacity: 0.6" />
|
||||
</div>
|
||||
<div class="px-3 py-2 rounded-md flex justify-start items-start gap-2.5">
|
||||
<div class="text-right justify-start text-sm font-bold leading-none">
|
||||
{{ $t('manager.nodePack') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex justify-start items-center gap-3">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
</script>
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stubTrue } from 'lodash'
|
||||
import { stubTrue } from 'es-toolkit/compat'
|
||||
import AutoComplete, {
|
||||
AutoCompleteOptionSelectEvent
|
||||
} from 'primevue/autocomplete'
|
||||
|
||||
@@ -1,39 +1,68 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap absolute right-[90px] z-[1000]"
|
||||
:class="{
|
||||
'bottom-[20px]': !bottomPanelStore.bottomPanelVisible,
|
||||
'bottom-[280px]': bottomPanelStore.bottomPanelVisible
|
||||
}"
|
||||
:style="containerStyles"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
<MiniMapPanel
|
||||
v-if="showOptionsPanel"
|
||||
:panel-styles="panelStyles"
|
||||
:node-colors="nodeColors"
|
||||
:show-links="showLinks"
|
||||
:show-groups="showGroups"
|
||||
:render-bypass="renderBypass"
|
||||
:render-error="renderError"
|
||||
@update-option="updateOption"
|
||||
/>
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="litegraph-minimap relative"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<Button
|
||||
class="absolute z-10"
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="toggleOptionsPanel"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:settings-2 />
|
||||
</template>
|
||||
</Button>
|
||||
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="minimap-canvas"
|
||||
/>
|
||||
|
||||
<div class="minimap-viewport" :style="viewportStyles" />
|
||||
|
||||
<div
|
||||
class="absolute inset-0"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@pointerleave="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
import MiniMapPanel from './MiniMapPanel.vue'
|
||||
|
||||
const minimap = useMinimap()
|
||||
const canvasStore = useCanvasStore()
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
|
||||
const {
|
||||
initialized,
|
||||
@@ -44,6 +73,13 @@ const {
|
||||
viewportStyles,
|
||||
width,
|
||||
height,
|
||||
panelStyles,
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
updateOption,
|
||||
init,
|
||||
destroy,
|
||||
handlePointerDown,
|
||||
@@ -52,6 +88,12 @@ const {
|
||||
handleWheel
|
||||
} = minimap
|
||||
|
||||
const showOptionsPanel = ref(false)
|
||||
|
||||
const toggleOptionsPanel = () => {
|
||||
showOptionsPanel.value = !showOptionsPanel.value
|
||||
}
|
||||
|
||||
watch(
|
||||
() => canvasStore.canvas,
|
||||
async (canvas) => {
|
||||
|
||||
97
src/components/graph/MiniMapPanel.vue
Normal file
97
src/components/graph/MiniMapPanel.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div
|
||||
class="minimap-panel p-3 mr-2 flex flex-col gap-3 text-sm"
|
||||
:style="panelStyles"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="node-colors"
|
||||
name="node-colors"
|
||||
:model-value="nodeColors"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.NodeColors', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:palette />
|
||||
<label for="node-colors">{{ $t('minimap.nodeColors') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-links"
|
||||
name="show-links"
|
||||
:model-value="showLinks"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:route />
|
||||
<label for="show-links">{{ $t('minimap.showLinks') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="show-groups"
|
||||
name="show-groups"
|
||||
:model-value="showGroups"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:frame />
|
||||
<label for="show-groups">{{ $t('minimap.showGroups') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-bypass"
|
||||
name="render-bypass"
|
||||
:model-value="renderBypass"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:circle-slash-2 />
|
||||
<label for="render-bypass">{{ $t('minimap.renderBypassState') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
input-id="render-error"
|
||||
name="render-error"
|
||||
:model-value="renderError"
|
||||
binary
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
|
||||
"
|
||||
/>
|
||||
<i-lucide:message-circle-warning />
|
||||
<label for="render-error">{{ $t('minimap.renderErrorState') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
|
||||
import { MinimapOptionKey } from '@/composables/useMinimap'
|
||||
|
||||
defineProps<{
|
||||
panelStyles: any
|
||||
nodeColors: boolean
|
||||
showLinks: boolean
|
||||
showGroups: boolean
|
||||
renderBypass: boolean
|
||||
renderError: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
updateOption: [key: MinimapOptionKey, value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
@@ -13,6 +13,7 @@
|
||||
<BypassButton />
|
||||
<PinButton />
|
||||
<EditModelButton />
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<DeleteButton />
|
||||
@@ -38,6 +39,7 @@ import EditModelButton from '@/components/graph/selectionToolbox/EditModelButton
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
|
||||
import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-if="isUnpackVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:expand />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="isConvertVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
|
||||
showDelay: 1000
|
||||
@@ -20,6 +34,7 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { LGraphGroup, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -27,7 +42,31 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isVisible = computed(() => {
|
||||
const isUnpackVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.selectedItems?.length === 1 &&
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
|
||||
const isConvertVisible = computed(() => {
|
||||
const items = canvasStore.selectedItems
|
||||
if (!items || items.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Unpack button takes precedence for single subgraph node
|
||||
if (items.length === 1 && items[0] instanceof SubgraphNode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Hide if ALL selected items are groups
|
||||
const allAreGroups = items.every((item) => item instanceof LGraphGroup)
|
||||
if (allAreGroups) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise, show it, assuming there's some selection
|
||||
return (
|
||||
canvasStore.groupSelected ||
|
||||
canvasStore.rerouteSelected ||
|
||||
|
||||
38
src/components/graph/selectionToolbox/Load3DViewerButton.vue
Normal file
38
src/components/graph/selectionToolbox/Load3DViewerButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="is3DNode"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_3DViewer_Open3DViewer.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="pi pi-pencil"
|
||||
@click="open3DViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const is3DNode = computed(() => {
|
||||
const enable3DViewer = useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
|
||||
|
||||
return nodes.length === 1 && nodes.some(isLoad3dNode) && enable3DViewer
|
||||
})
|
||||
|
||||
const open3DViewer = () => {
|
||||
void commandStore.execute('Comfy.3DViewer.Open3DViewer')
|
||||
}
|
||||
</script>
|
||||
@@ -58,8 +58,19 @@
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
<div
|
||||
v-if="showRecordingControls"
|
||||
v-if="enable3DViewer"
|
||||
class="absolute top-12 right-2 z-20 pointer-events-auto"
|
||||
>
|
||||
<ViewerControls :node="node" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showRecordingControls"
|
||||
class="absolute right-2 z-20 pointer-events-auto"
|
||||
:class="{
|
||||
'top-12': !enable3DViewer,
|
||||
'top-24': enable3DViewer
|
||||
}"
|
||||
>
|
||||
<RecordingControls
|
||||
:node="node"
|
||||
@@ -82,6 +93,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Load3DControls from '@/components/load3d/Load3DControls.vue'
|
||||
import Load3DScene from '@/components/load3d/Load3DScene.vue'
|
||||
import RecordingControls from '@/components/load3d/controls/RecordingControls.vue'
|
||||
import ViewerControls from '@/components/load3d/controls/ViewerControls.vue'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import {
|
||||
CameraType,
|
||||
@@ -91,6 +103,7 @@ import {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -121,6 +134,9 @@ const isRecording = ref(false)
|
||||
const hasRecording = ref(false)
|
||||
const recordingDuration = ref(0)
|
||||
const showRecordingControls = ref(!inputSpec.isPreview)
|
||||
const enable3DViewer = computed(() =>
|
||||
useSettingStore().get('Comfy.Load3D.3DViewerEnable')
|
||||
)
|
||||
|
||||
const showPreviewButton = computed(() => {
|
||||
return !type.includes('Preview')
|
||||
|
||||
149
src/components/load3d/Load3dViewerContent.vue
Normal file
149
src/components/load3d/Load3dViewerContent.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div
|
||||
ref="viewerContentRef"
|
||||
class="flex w-full"
|
||||
:class="[maximized ? 'h-full' : 'h-[70vh]']"
|
||||
@mouseenter="viewer.handleMouseEnter"
|
||||
@mouseleave="viewer.handleMouseLeave"
|
||||
>
|
||||
<div ref="mainContentRef" class="flex-1 relative">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="absolute w-full h-full comfy-load-3d-viewer"
|
||||
@resize="viewer.handleResize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-72 flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="space-y-2">
|
||||
<div class="p-2 space-y-4">
|
||||
<SceneControls
|
||||
v-model:background-color="viewer.backgroundColor.value"
|
||||
v-model:show-grid="viewer.showGrid.value"
|
||||
:has-background-image="viewer.hasBackgroundImage.value"
|
||||
@update-background-image="viewer.handleBackgroundImageUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<ModelControls
|
||||
v-model:up-direction="viewer.upDirection.value"
|
||||
v-model:material-mode="viewer.materialMode.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<CameraControls
|
||||
v-model:camera-type="viewer.cameraType.value"
|
||||
v-model:fov="viewer.fov.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<LightControls
|
||||
v-model:light-intensity="viewer.lightIntensity.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 space-y-4">
|
||||
<ExportControls @export-model="viewer.exportModel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
:label="t('g.cancel')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onBeforeUnmount, onMounted, ref, toRaw } from 'vue'
|
||||
|
||||
import CameraControls from '@/components/load3d/controls/viewer/CameraControls.vue'
|
||||
import ExportControls from '@/components/load3d/controls/viewer/ExportControls.vue'
|
||||
import LightControls from '@/components/load3d/controls/viewer/LightControls.vue'
|
||||
import ModelControls from '@/components/load3d/controls/viewer/ModelControls.vue'
|
||||
import SceneControls from '@/components/load3d/controls/viewer/SceneControls.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const props = defineProps<{
|
||||
node: LGraphNode
|
||||
}>()
|
||||
|
||||
const viewerContentRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const mainContentRef = ref<HTMLDivElement>()
|
||||
const maximized = ref(false)
|
||||
const mutationObserver = ref<MutationObserver | null>(null)
|
||||
|
||||
const viewer = useLoad3dService().getOrCreateViewer(toRaw(props.node))
|
||||
|
||||
onMounted(async () => {
|
||||
const source = useLoad3dService().getLoad3d(props.node)
|
||||
if (source && containerRef.value) {
|
||||
await viewer.initializeViewer(containerRef.value, source)
|
||||
}
|
||||
|
||||
if (viewerContentRef.value) {
|
||||
mutationObserver.value = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'maximized'
|
||||
) {
|
||||
maximized.value =
|
||||
(mutation.target as HTMLElement).getAttribute('maximized') ===
|
||||
'true'
|
||||
|
||||
setTimeout(() => {
|
||||
viewer.refreshViewport()
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
mutationObserver.value.observe(viewerContentRef.value, {
|
||||
attributes: true,
|
||||
attributeFilter: ['maximized']
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('resize', viewer.handleResize)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
viewer.restoreInitialState()
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', viewer.handleResize)
|
||||
|
||||
if (mutationObserver.value) {
|
||||
mutationObserver.value.disconnect()
|
||||
mutationObserver.value = null
|
||||
}
|
||||
|
||||
// we will manually cleanup the viewer in dialog close handler
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-panel-content) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
52
src/components/load3d/controls/ViewerControls.vue
Normal file
52
src/components/load3d/controls/ViewerControls.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="relative bg-gray-700 bg-opacity-30 rounded-lg">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button class="p-button-rounded p-button-text" @click="openIn3DViewer">
|
||||
<i
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.openIn3DViewer'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="pi pi-expand text-white text-lg"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tooltip } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const vTooltip = Tooltip
|
||||
|
||||
const { node } = defineProps<{
|
||||
node: LGraphNode
|
||||
}>()
|
||||
|
||||
const openIn3DViewer = () => {
|
||||
const props = { node: node }
|
||||
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-load3d-viewer',
|
||||
title: t('load3d.viewer.title'),
|
||||
component: Load3DViewerContent,
|
||||
props: props,
|
||||
dialogComponentProps: {
|
||||
style: 'width: 80vw; height: 80vh;',
|
||||
maximizable: true,
|
||||
onClose: async () => {
|
||||
await useLoad3dService().handleViewerClose(props.node)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
37
src/components/load3d/controls/viewer/CameraControls.vue
Normal file
37
src/components/load3d/controls/viewer/CameraControls.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<label>
|
||||
{{ t('load3d.viewer.cameraType') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="cameraType"
|
||||
:options="cameras"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="showFOVButton" class="space-y-4">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider v-model="fov" :min="10" :max="150" :step="1" aria-label="fov" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import Slider from 'primevue/slider'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const cameras = [
|
||||
{ title: t('load3d.cameraType.perspective'), value: 'perspective' },
|
||||
{ title: t('load3d.cameraType.orthographic'), value: 'orthographic' }
|
||||
]
|
||||
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
</script>
|
||||
37
src/components/load3d/controls/viewer/ExportControls.vue
Normal file
37
src/components/load3d/controls/viewer/ExportControls.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Select
|
||||
v-model="exportFormat"
|
||||
:options="exportFormats"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
>
|
||||
</Select>
|
||||
|
||||
<Button severity="secondary" text rounded @click="exportModel(exportFormat)">
|
||||
{{ t('load3d.export') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Select from 'primevue/select'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'exportModel', format: string): void
|
||||
}>()
|
||||
|
||||
const exportFormats = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' }
|
||||
]
|
||||
|
||||
const exportFormat = ref('obj')
|
||||
|
||||
const exportModel = (format: string) => {
|
||||
emit('exportModel', format)
|
||||
}
|
||||
</script>
|
||||
30
src/components/load3d/controls/viewer/LightControls.vue
Normal file
30
src/components/load3d/controls/viewer/LightControls.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<label>{{ t('load3d.lightIntensity') }}</label>
|
||||
|
||||
<Slider
|
||||
v-model="lightIntensity"
|
||||
class="w-full"
|
||||
:min="lightIntensityMinimum"
|
||||
:max="lightIntensityMaximum"
|
||||
:step="lightAdjustmentIncrement"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Slider from 'primevue/slider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const lightIntensity = defineModel<number>('lightIntensity')
|
||||
|
||||
const lightIntensityMaximum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMaximum'
|
||||
)
|
||||
const lightIntensityMinimum = useSettingStore().get(
|
||||
'Comfy.Load3D.LightIntensityMinimum'
|
||||
)
|
||||
const lightAdjustmentIncrement = useSettingStore().get(
|
||||
'Comfy.Load3D.LightAdjustmentIncrement'
|
||||
)
|
||||
</script>
|
||||
52
src/components/load3d/controls/viewer/ModelControls.vue
Normal file
52
src/components/load3d/controls/viewer/ModelControls.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label>{{ t('load3d.upDirection') }}</label>
|
||||
<Select
|
||||
v-model="upDirection"
|
||||
:options="upDirectionOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{ t('load3d.materialMode') }}</label>
|
||||
<Select
|
||||
v-model="materialMode"
|
||||
:options="materialModeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { MaterialMode, UpDirection } from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
|
||||
const upDirectionOptions = [
|
||||
{ label: t('load3d.upDirections.original'), value: 'original' },
|
||||
{ label: '-X', value: '-x' },
|
||||
{ label: '+X', value: '+x' },
|
||||
{ label: '-Y', value: '-y' },
|
||||
{ label: '+Y', value: '+y' },
|
||||
{ label: '-Z', value: '-z' },
|
||||
{ label: '+Z', value: '+z' }
|
||||
]
|
||||
|
||||
const materialModeOptions = computed(() => {
|
||||
return [
|
||||
{ label: t('load3d.materialModes.original'), value: 'original' },
|
||||
{ label: t('load3d.materialModes.normal'), value: 'normal' },
|
||||
{ label: t('load3d.materialModes.wireframe'), value: 'wireframe' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
82
src/components/load3d/controls/viewer/SceneControls.vue
Normal file
82
src/components/load3d/controls/viewer/SceneControls.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<label>
|
||||
{{ t('load3d.backgroundColor') }}
|
||||
</label>
|
||||
<input v-model="backgroundColor" type="color" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox v-model="showGrid" input-id="showGrid" binary name="showGrid" />
|
||||
<label for="showGrid" class="pl-2">
|
||||
{{ t('load3d.showGrid') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasBackgroundImage">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="t('load3d.uploadBackgroundImage')"
|
||||
icon="pi pi-image"
|
||||
class="w-full"
|
||||
@click="openImagePicker"
|
||||
/>
|
||||
<input
|
||||
ref="imagePickerRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBackgroundImage" class="space-y-2">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="t('load3d.removeBackgroundImage')"
|
||||
icon="pi pi-times"
|
||||
class="w-full"
|
||||
@click="removeBackgroundImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const backgroundColor = defineModel<string>('backgroundColor')
|
||||
const showGrid = defineModel<boolean>('showGrid')
|
||||
|
||||
defineProps<{
|
||||
hasBackgroundImage?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateBackgroundImage', file: File | null): void
|
||||
}>()
|
||||
|
||||
const imagePickerRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const openImagePicker = () => {
|
||||
imagePickerRef.value?.click()
|
||||
}
|
||||
|
||||
const handleImageUpload = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files[0]) {
|
||||
emit('updateBackgroundImage', input.files[0])
|
||||
}
|
||||
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const removeBackgroundImage = () => {
|
||||
emit('updateBackgroundImage', null)
|
||||
}
|
||||
</script>
|
||||
@@ -82,7 +82,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
@@ -265,6 +265,14 @@ const renderTreeNode = (
|
||||
const workflow = node.data
|
||||
await workflowService.insertWorkflow(workflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('g.duplicate'),
|
||||
icon: 'pi pi-file-export',
|
||||
command: async () => {
|
||||
const workflow = node.data
|
||||
await workflowService.duplicateWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { Ref, markRaw, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useTerminal(element: Ref<HTMLElement | undefined>) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutationObserver, useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { readonly, ref } from 'vue'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
|
||||
@@ -418,7 +418,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
|
||||
// Pricing matrix from CSV data based on mode string content
|
||||
if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('v2-1-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run' // price is the same as for v2-master model
|
||||
}
|
||||
return '$1.40/Run' // price is the same as for v2-master model
|
||||
} else if (modeValue.includes('v2-master')) {
|
||||
if (modeValue.includes('10s')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
@@ -558,6 +563,32 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
MinimaxTextToVideoNode: {
|
||||
displayPrice: '$0.43/Run'
|
||||
},
|
||||
MinimaxHailuoVideoNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'resolution'
|
||||
) as IComboWidget
|
||||
const durationWidget = node.widgets?.find(
|
||||
(w) => w.name === 'duration'
|
||||
) as IComboWidget
|
||||
|
||||
if (!resolutionWidget || !durationWidget) {
|
||||
return '$0.28-0.56/Run (varies with resolution & duration)'
|
||||
}
|
||||
|
||||
const resolution = String(resolutionWidget.value)
|
||||
const duration = String(durationWidget.value)
|
||||
|
||||
if (resolution.includes('768P')) {
|
||||
if (duration.includes('6')) return '$0.28/Run'
|
||||
if (duration.includes('10')) return '$0.56/Run'
|
||||
} else if (resolution.includes('1080P')) {
|
||||
if (duration.includes('6')) return '$0.49/Run'
|
||||
}
|
||||
|
||||
return '$0.43/Run' // default median
|
||||
}
|
||||
},
|
||||
OpenAIDalle2: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const sizeWidget = node.widgets?.find(
|
||||
@@ -1278,9 +1309,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
// Google Veo video generation
|
||||
if (model.includes('veo-2.0')) {
|
||||
return '$0.5/second'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00016/$0.0006 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-flash')) {
|
||||
return '$0.0003/$0.0025 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
} else if (model.includes('gemini-2.5-pro')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
}
|
||||
// For other Gemini models, show token-based pricing info
|
||||
@@ -1358,6 +1393,7 @@ export const useNodePricing = () => {
|
||||
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
|
||||
KlingSingleImageVideoEffectNode: ['effect_scene'],
|
||||
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
|
||||
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
||||
OpenAIDalle3: ['size', 'quality'],
|
||||
OpenAIDalle2: ['size', 'n'],
|
||||
OpenAIGPTImage1: ['quality', 'n'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { groupBy } from 'lodash'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
|
||||
|
||||
@@ -21,8 +21,10 @@ import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { type ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
@@ -796,6 +798,22 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
canvas.select(node)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.UnpackSubgraph',
|
||||
icon: 'pi pi-sitemap',
|
||||
label: 'Unpack the selected Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
category: 'essentials' as const,
|
||||
function: () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||
|
||||
const subgraphNode = app.canvas.selectedItems.values().next().value
|
||||
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleBottomPanel.Shortcuts',
|
||||
icon: 'pi pi-key',
|
||||
@@ -805,6 +823,21 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
bottomPanelStore.togglePanel('shortcuts')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ExitSubgraph',
|
||||
icon: 'pi pi-arrow-up',
|
||||
label: 'Exit Subgraph',
|
||||
versionAdded: '1.20.1',
|
||||
function: () => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
if (!canvas.graph) return
|
||||
|
||||
canvas.setGraph(
|
||||
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
376
src/composables/useLoad3dViewer.ts
Normal file
376
src/composables/useLoad3dViewer.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import { ref, toRaw, watch } from 'vue'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import {
|
||||
CameraType,
|
||||
MaterialMode,
|
||||
UpDirection
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
interface Load3dViewerState {
|
||||
backgroundColor: string
|
||||
showGrid: boolean
|
||||
cameraType: CameraType
|
||||
fov: number
|
||||
lightIntensity: number
|
||||
cameraState: any
|
||||
backgroundImage: string
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
edgeThreshold: number
|
||||
}
|
||||
|
||||
export const useLoad3dViewer = (node: LGraphNode) => {
|
||||
const backgroundColor = ref('')
|
||||
const showGrid = ref(true)
|
||||
const cameraType = ref<CameraType>('perspective')
|
||||
const fov = ref(75)
|
||||
const lightIntensity = ref(1)
|
||||
const backgroundImage = ref('')
|
||||
const hasBackgroundImage = ref(false)
|
||||
const upDirection = ref<UpDirection>('original')
|
||||
const materialMode = ref<MaterialMode>('original')
|
||||
const edgeThreshold = ref(85)
|
||||
const needApplyChanges = ref(true)
|
||||
|
||||
let load3d: Load3d | null = null
|
||||
let sourceLoad3d: Load3d | null = null
|
||||
|
||||
const initialState = ref<Load3dViewerState>({
|
||||
backgroundColor: '#282828',
|
||||
showGrid: true,
|
||||
cameraType: 'perspective',
|
||||
fov: 75,
|
||||
lightIntensity: 1,
|
||||
cameraState: null,
|
||||
backgroundImage: '',
|
||||
upDirection: 'original',
|
||||
materialMode: 'original',
|
||||
edgeThreshold: 85
|
||||
})
|
||||
|
||||
watch(backgroundColor, (newColor) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setBackgroundColor(newColor)
|
||||
} catch (error) {
|
||||
console.error('Error updating background color:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateBackgroundColor', { color: newColor })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(showGrid, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.toggleGrid(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error toggling grid:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToToggleGrid', { show: newValue ? 'on' : 'off' })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(cameraType, (newCameraType) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.toggleCamera(newCameraType)
|
||||
} catch (error) {
|
||||
console.error('Error toggling camera:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToToggleCamera', { camera: newCameraType })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(fov, (newFov) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setFOV(Number(newFov))
|
||||
} catch (error) {
|
||||
console.error('Error updating FOV:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateFOV', { fov: newFov })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(lightIntensity, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setLightIntensity(Number(newValue))
|
||||
} catch (error) {
|
||||
console.error('Error updating light intensity:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateLightIntensity', { intensity: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(backgroundImage, async (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
await load3d.setBackgroundImage(newValue)
|
||||
hasBackgroundImage.value = !!newValue
|
||||
} catch (error) {
|
||||
console.error('Error updating background image:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToUpdateBackgroundImage'))
|
||||
}
|
||||
})
|
||||
|
||||
watch(upDirection, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setUpDirection(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating up direction:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateUpDirection', { direction: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(materialMode, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setMaterialMode(newValue)
|
||||
} catch (error) {
|
||||
console.error('Error updating material mode:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateMaterialMode', { mode: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
watch(edgeThreshold, (newValue) => {
|
||||
if (!load3d) return
|
||||
try {
|
||||
load3d.setEdgeThreshold(Number(newValue))
|
||||
} catch (error) {
|
||||
console.error('Error updating edge threshold:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToUpdateEdgeThreshold', { threshold: newValue })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const initializeViewer = async (
|
||||
containerRef: HTMLElement,
|
||||
source: Load3d
|
||||
) => {
|
||||
if (!containerRef) return
|
||||
|
||||
sourceLoad3d = source
|
||||
|
||||
try {
|
||||
load3d = new Load3d(containerRef, {
|
||||
node: node,
|
||||
disablePreview: true,
|
||||
isViewerMode: true
|
||||
})
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, load3d)
|
||||
|
||||
const sourceCameraType = source.getCurrentCameraType()
|
||||
const sourceCameraState = source.getCameraState()
|
||||
|
||||
cameraType.value = sourceCameraType
|
||||
backgroundColor.value = source.sceneManager.currentBackgroundColor
|
||||
showGrid.value = source.sceneManager.gridHelper.visible
|
||||
lightIntensity.value = (node.properties['Light Intensity'] as number) || 1
|
||||
|
||||
const backgroundInfo = source.sceneManager.getCurrentBackgroundInfo()
|
||||
if (
|
||||
backgroundInfo.type === 'image' &&
|
||||
node.properties['Background Image']
|
||||
) {
|
||||
backgroundImage.value = node.properties['Background Image'] as string
|
||||
hasBackgroundImage.value = true
|
||||
} else {
|
||||
backgroundImage.value = ''
|
||||
hasBackgroundImage.value = false
|
||||
}
|
||||
|
||||
if (sourceCameraType === 'perspective') {
|
||||
fov.value = source.cameraManager.perspectiveCamera.fov
|
||||
}
|
||||
|
||||
upDirection.value = source.modelManager.currentUpDirection
|
||||
materialMode.value = source.modelManager.materialMode
|
||||
edgeThreshold.value = (node.properties['Edge Threshold'] as number) || 85
|
||||
|
||||
initialState.value = {
|
||||
backgroundColor: backgroundColor.value,
|
||||
showGrid: showGrid.value,
|
||||
cameraType: cameraType.value,
|
||||
fov: fov.value,
|
||||
lightIntensity: lightIntensity.value,
|
||||
cameraState: sourceCameraState,
|
||||
backgroundImage: backgroundImage.value,
|
||||
upDirection: upDirection.value,
|
||||
materialMode: materialMode.value,
|
||||
edgeThreshold: edgeThreshold.value
|
||||
}
|
||||
|
||||
const width = node.widgets?.find((w) => w.name === 'width')
|
||||
const height = node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
if (width && height) {
|
||||
load3d.setTargetSize(
|
||||
toRaw(width).value as number,
|
||||
toRaw(height).value as number
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing Load3d viewer:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToInitializeLoad3dViewer')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const exportModel = async (format: string) => {
|
||||
if (!load3d) return
|
||||
|
||||
try {
|
||||
await load3d.exportModel(format)
|
||||
} catch (error) {
|
||||
console.error('Error exporting model:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToExportModel', { format: format.toUpperCase() })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
load3d?.handleResize()
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
load3d?.updateStatusMouseOnViewer(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
load3d?.updateStatusMouseOnViewer(false)
|
||||
}
|
||||
|
||||
const restoreInitialState = () => {
|
||||
const nodeValue = node
|
||||
|
||||
needApplyChanges.value = false
|
||||
|
||||
if (nodeValue.properties) {
|
||||
nodeValue.properties['Background Color'] =
|
||||
initialState.value.backgroundColor
|
||||
nodeValue.properties['Show Grid'] = initialState.value.showGrid
|
||||
nodeValue.properties['Camera Type'] = initialState.value.cameraType
|
||||
nodeValue.properties['FOV'] = initialState.value.fov
|
||||
nodeValue.properties['Light Intensity'] =
|
||||
initialState.value.lightIntensity
|
||||
nodeValue.properties['Camera Info'] = initialState.value.cameraState
|
||||
nodeValue.properties['Background Image'] =
|
||||
initialState.value.backgroundImage
|
||||
}
|
||||
}
|
||||
|
||||
const applyChanges = async () => {
|
||||
if (!sourceLoad3d || !load3d) return false
|
||||
|
||||
const viewerCameraState = load3d.getCameraState()
|
||||
const nodeValue = node
|
||||
|
||||
if (nodeValue.properties) {
|
||||
nodeValue.properties['Background Color'] = backgroundColor.value
|
||||
nodeValue.properties['Show Grid'] = showGrid.value
|
||||
nodeValue.properties['Camera Type'] = cameraType.value
|
||||
nodeValue.properties['FOV'] = fov.value
|
||||
nodeValue.properties['Light Intensity'] = lightIntensity.value
|
||||
nodeValue.properties['Camera Info'] = viewerCameraState
|
||||
nodeValue.properties['Background Image'] = backgroundImage.value
|
||||
}
|
||||
|
||||
await useLoad3dService().copyLoad3dState(load3d, sourceLoad3d)
|
||||
|
||||
if (backgroundImage.value) {
|
||||
await sourceLoad3d.setBackgroundImage(backgroundImage.value)
|
||||
}
|
||||
|
||||
sourceLoad3d.forceRender()
|
||||
|
||||
if (nodeValue.graph) {
|
||||
nodeValue.graph.setDirtyCanvas(true, true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const refreshViewport = () => {
|
||||
useLoad3dService().handleViewportRefresh(load3d)
|
||||
}
|
||||
|
||||
const handleBackgroundImageUpdate = async (file: File | null) => {
|
||||
if (!file) {
|
||||
backgroundImage.value = ''
|
||||
hasBackgroundImage.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const resourceFolder =
|
||||
(node.properties['Resource Folder'] as string) || ''
|
||||
const subfolder = resourceFolder.trim()
|
||||
? `3d/${resourceFolder.trim()}`
|
||||
: '3d'
|
||||
|
||||
const uploadPath = await Load3dUtils.uploadFile(file, subfolder)
|
||||
|
||||
if (uploadPath) {
|
||||
backgroundImage.value = uploadPath
|
||||
hasBackgroundImage.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading background image:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToUploadBackgroundImage'))
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
load3d?.remove()
|
||||
load3d = null
|
||||
sourceLoad3d = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
backgroundColor,
|
||||
showGrid,
|
||||
cameraType,
|
||||
fov,
|
||||
lightIntensity,
|
||||
backgroundImage,
|
||||
hasBackgroundImage,
|
||||
upDirection,
|
||||
materialMode,
|
||||
edgeThreshold,
|
||||
needApplyChanges,
|
||||
|
||||
// Methods
|
||||
initializeViewer,
|
||||
exportModel,
|
||||
handleResize,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
restoreInitialState,
|
||||
applyChanges,
|
||||
refreshViewport,
|
||||
handleBackgroundImageUpdate,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
@@ -16,6 +17,13 @@ interface GraphCallbacks {
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export type MinimapOptionKey =
|
||||
| 'Comfy.Minimap.NodeColors'
|
||||
| 'Comfy.Minimap.ShowLinks'
|
||||
| 'Comfy.Minimap.ShowGroups'
|
||||
| 'Comfy.Minimap.RenderBypassState'
|
||||
| 'Comfy.Minimap.RenderErrorState'
|
||||
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -27,6 +35,27 @@ export function useMinimap() {
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
const nodeColors = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.NodeColors')
|
||||
)
|
||||
const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))
|
||||
const showGroups = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.ShowGroups')
|
||||
)
|
||||
const renderBypass = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderBypassState')
|
||||
)
|
||||
const renderError = computed(() =>
|
||||
settingStore.get('Comfy.Minimap.RenderErrorState')
|
||||
)
|
||||
|
||||
const updateOption = async (key: MinimapOptionKey, value: boolean) => {
|
||||
await settingStore.set(key, value)
|
||||
|
||||
needsFullRedraw.value = true
|
||||
updateMinimap()
|
||||
}
|
||||
|
||||
const initialized = ref(false)
|
||||
const bounds = ref({
|
||||
minX: 0,
|
||||
@@ -63,10 +92,19 @@ export function useMinimap() {
|
||||
const nodeColor = computed(
|
||||
() => (isLightTheme.value ? '#3DA8E099' : '#0B8CE999') // lighter blue for light theme
|
||||
)
|
||||
const nodeColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#D9D9D9' : '#353535') // this is the default node color when using nodeColors setting
|
||||
)
|
||||
const linkColor = computed(
|
||||
() => (isLightTheme.value ? '#FFB347' : '#F99614') // lighter orange for light theme
|
||||
() => (isLightTheme.value ? '#616161' : '#B3B3B3') // lighter orange for light theme
|
||||
)
|
||||
const slotColor = computed(() => linkColor.value)
|
||||
const groupColor = computed(() =>
|
||||
isLightTheme.value ? '#A2D3EC' : '#1F547A'
|
||||
)
|
||||
const bypassColor = computed(() =>
|
||||
isLightTheme.value ? '#DBDBDB' : '#4B184B'
|
||||
)
|
||||
|
||||
const containerRect = ref({
|
||||
left: 0,
|
||||
@@ -116,6 +154,14 @@ export function useMinimap() {
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const panelStyles = computed(() => ({
|
||||
width: `210px`,
|
||||
height: `${height}px`,
|
||||
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
|
||||
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
const viewportStyles = computed(() => ({
|
||||
transform: `translate(${viewportTransform.value.x}px, ${viewportTransform.value.y}px)`,
|
||||
width: `${viewportTransform.value.width}px`,
|
||||
@@ -189,6 +235,25 @@ export function useMinimap() {
|
||||
return Math.min(scaleX, scaleY) * 0.9
|
||||
}
|
||||
|
||||
const renderGroups = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g || !g._groups || g._groups.length === 0) return
|
||||
|
||||
for (const group of g._groups) {
|
||||
const x = (group.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||
const y = (group.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||
const w = group.size[0] * scale.value
|
||||
const h = group.size[1] * scale.value
|
||||
|
||||
ctx.fillStyle = groupColor.value
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
const renderNodes = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
@@ -203,9 +268,29 @@ export function useMinimap() {
|
||||
const w = node.size[0] * scale.value
|
||||
const h = node.size[1] * scale.value
|
||||
|
||||
let color = nodeColor.value
|
||||
|
||||
if (renderBypass.value && node.mode === LGraphEventMode.BYPASS) {
|
||||
color = bypassColor.value
|
||||
} else if (nodeColors.value) {
|
||||
color = nodeColorDefault.value
|
||||
|
||||
if (node.bgcolor) {
|
||||
color = isLightTheme.value
|
||||
? adjustColor(node.bgcolor, { lightness: 0.5 })
|
||||
: node.bgcolor
|
||||
}
|
||||
}
|
||||
|
||||
// Render solid node blocks
|
||||
ctx.fillStyle = nodeColor.value
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
if (renderError.value && node.has_errors) {
|
||||
ctx.strokeStyle = '#FF0000'
|
||||
ctx.lineWidth = 0.3
|
||||
ctx.strokeRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,9 +303,9 @@ export function useMinimap() {
|
||||
if (!g) return
|
||||
|
||||
ctx.strokeStyle = linkColor.value
|
||||
ctx.lineWidth = 1.4
|
||||
ctx.lineWidth = 0.3
|
||||
|
||||
const slotRadius = 3.7 * Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const slotRadius = Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
@@ -304,8 +389,15 @@ export function useMinimap() {
|
||||
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||
|
||||
if (showGroups.value) {
|
||||
renderGroups(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
if (showLinks.value) {
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
}
|
||||
|
||||
renderNodes(ctx, offsetX, offsetY)
|
||||
renderConnections(ctx, offsetX, offsetY)
|
||||
|
||||
needsFullRedraw.value = false
|
||||
updateFlags.value.nodes = false
|
||||
@@ -690,9 +782,16 @@ export function useMinimap() {
|
||||
canvasRef,
|
||||
containerStyles,
|
||||
viewportStyles,
|
||||
panelStyles,
|
||||
width,
|
||||
height,
|
||||
|
||||
nodeColors,
|
||||
showLinks,
|
||||
showGroups,
|
||||
renderBypass,
|
||||
renderError,
|
||||
|
||||
init,
|
||||
destroy,
|
||||
toggle,
|
||||
@@ -701,6 +800,7 @@ export function useMinimap() {
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
handleWheel,
|
||||
setMinimapRef
|
||||
setMinimapRef,
|
||||
updateOption
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { orderBy } from 'lodash'
|
||||
import { orderBy } from 'es-toolkit/compat'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
@@ -88,6 +89,11 @@ export function useWorkflowPersistence() {
|
||||
)
|
||||
api.addEventListener('graphChanged', persistCurrentWorkflow)
|
||||
|
||||
// Clean up event listener when component unmounts
|
||||
tryOnScopeDispose(() => {
|
||||
api.removeEventListener('graphChanged', persistCurrentWorkflow)
|
||||
})
|
||||
|
||||
// Restore workflow tabs states
|
||||
const openWorkflows = computed(() => workflowStore.openWorkflows)
|
||||
const activeWorkflow = computed(() => workflowStore.activeWorkflow)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
@@ -190,5 +190,11 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'k'
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -300,7 +300,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{ value: 'ja', text: '日本語' },
|
||||
{ value: 'ko', text: '한국어' },
|
||||
{ value: 'fr', text: 'Français' },
|
||||
{ value: 'es', text: 'Español' }
|
||||
{ value: 'es', text: 'Español' },
|
||||
{ value: 'ar', text: 'عربي' }
|
||||
],
|
||||
defaultValue: () => navigator.language.split('-')[0] || 'en'
|
||||
},
|
||||
@@ -830,6 +831,41 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.NodeColors',
|
||||
name: 'Display node with its original color on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: false,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowLinks',
|
||||
name: 'Display links on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.ShowGroups',
|
||||
name: 'Display node groups on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderBypassState',
|
||||
name: 'Render bypass state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.RenderErrorState',
|
||||
name: 'Render error state on minimap',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.26.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.AutoSaveDelay',
|
||||
name: 'Auto Save Delay (ms)',
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
export const CORE_TEMPLATES = [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Basics',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'default',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from text descriptions.'
|
||||
},
|
||||
{
|
||||
name: 'image2image',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform existing images using text prompts.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/img2img/'
|
||||
},
|
||||
{
|
||||
name: 'lora',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Apply LoRA models for specialized styles or subjects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
},
|
||||
{
|
||||
name: 'inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Edit specific parts of images seamlessly.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/'
|
||||
},
|
||||
{
|
||||
name: 'inpain_model_outpainting',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images beyond their original boundaries.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting'
|
||||
},
|
||||
{
|
||||
name: 'embedding_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use textual inversion for consistent styles',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/textual_inversion_embeddings/'
|
||||
},
|
||||
{
|
||||
name: 'gligen_textbox_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Specify the location and size of objects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/gligen/'
|
||||
},
|
||||
{
|
||||
name: 'lora_multiple',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple LoRA models for unique results.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Flux',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'flux_dev_checkpoint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images using Flux development models.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-dev-1'
|
||||
},
|
||||
{
|
||||
name: 'flux_schnell',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images quickly with Flux Schnell.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-schnell-1'
|
||||
},
|
||||
{
|
||||
name: 'flux_fill_inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Fill in missing parts of images.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
},
|
||||
{
|
||||
name: 'flux_fill_outpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images using Flux outpainting.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
},
|
||||
{
|
||||
name: 'flux_canny_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from edge detection.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
},
|
||||
{
|
||||
name: 'flux_depth_lora_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware LoRA.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
},
|
||||
{
|
||||
name: 'flux_redux_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer style from a reference image to guide image generation with Flux.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#redux'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'ControlNet',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image generation with reference images.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/'
|
||||
},
|
||||
{
|
||||
name: '2_pass_pose_worship',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from pose references.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#pose-controlnet'
|
||||
},
|
||||
{
|
||||
name: 'depth_controlnet',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware generation.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'depth_t2i_adapter',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Quickly generate depth-aware images with a T2I adapter.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'mixing_controlnets',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple ControlNet models together.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#mixing-controlnets'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Upscaling',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'hiresfix_latent_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance image quality in latent space.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
|
||||
},
|
||||
{
|
||||
name: 'esrgan_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models to enhance image quality.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
|
||||
},
|
||||
{
|
||||
name: 'hiresfix_esrgan_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models during intermediate steps.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#non-latent-upscaling'
|
||||
},
|
||||
{
|
||||
name: 'latent_upscale_different_prompt_model',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Upscale and change prompt across passes',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#more-examples'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Video',
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
name: 'ltxv_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate videos from text descriptions.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#text-to-video'
|
||||
},
|
||||
{
|
||||
name: 'ltxv_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Convert still images into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#image-to-video'
|
||||
},
|
||||
{
|
||||
name: 'mochi_text_to_video_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create videos with Mochi model.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/mochi/'
|
||||
},
|
||||
{
|
||||
name: 'hunyuan_video_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate videos using Hunyuan model.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/'
|
||||
},
|
||||
{
|
||||
name: 'image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform images into animated videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
},
|
||||
{
|
||||
name: 'txt_to_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from text and then convert them into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'SD3.5',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'sd3.5_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images with SD 3.5.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_canny_controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Use edge detection to guide image generation with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_depth',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create depth-aware images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
},
|
||||
{
|
||||
name: 'sd3.5_large_blur',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from blurred reference images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'SDXL',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'sdxl_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create high-quality images with SDXL.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_refiner_prompt_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance SDXL outputs with refiners.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_revision_text_prompts',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer concepts from reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_revision_zero_positive',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Add text prompts alongside reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
{
|
||||
name: 'sdxlturbo_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images in a single step with SDXL Turbo.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Area Composition',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'area_composition',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image composition with areas.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
{
|
||||
name: 'area_composition_reversed',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Reverse area composition workflow.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
{
|
||||
name: 'area_composition_square_area_for_subject',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create consistent subject placement.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/#increasing-consistency-of-images-with-area-composition'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: '3D',
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
name: 'stable_zero123_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate 3D views from single images.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/3d/'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'Audio',
|
||||
type: 'audio',
|
||||
templates: [
|
||||
{
|
||||
name: 'stable_audio_example',
|
||||
mediaType: 'audio',
|
||||
mediaSubtype: 'mp3',
|
||||
description: 'Generate audio from text descriptions.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/audio/'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -2,6 +2,7 @@ import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
@@ -9,10 +10,13 @@ import { t } from '@/i18n'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
|
||||
async function handleModelUpload(files: FileList, node: any) {
|
||||
if (!files?.length) return
|
||||
@@ -174,6 +178,51 @@ useExtensionService().registerExtension({
|
||||
},
|
||||
defaultValue: 0.5,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.3DViewerEnable',
|
||||
category: ['3D', '3DViewer', 'Enable'],
|
||||
name: 'Enable 3D Viewer (Beta)',
|
||||
tooltip:
|
||||
'Enables the 3D Viewer (Beta) for selected nodes. This feature allows you to visualize and interact with 3D models directly within the full size 3d viewer.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
id: 'Comfy.3DViewer.Open3DViewer',
|
||||
icon: 'pi pi-pencil',
|
||||
label: 'Open 3D Viewer (Beta) for Selected Node',
|
||||
function: () => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
if (!selectedNodes || Object.keys(selectedNodes).length !== 1) return
|
||||
|
||||
const selectedNode = selectedNodes[Object.keys(selectedNodes)[0]]
|
||||
|
||||
if (!isLoad3dNode(selectedNode)) return
|
||||
|
||||
ComfyApp.copyToClipspace(selectedNode)
|
||||
// @ts-expect-error clipspace_return_node is an extension property added at runtime
|
||||
ComfyApp.clipspace_return_node = selectedNode
|
||||
|
||||
const props = { node: selectedNode }
|
||||
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-load3d-viewer',
|
||||
title: t('load3d.viewer.title'),
|
||||
component: Load3DViewerContent,
|
||||
props: props,
|
||||
dialogComponentProps: {
|
||||
style: 'width: 80vw; height: 80vh;',
|
||||
maximizable: true,
|
||||
onClose: async () => {
|
||||
await useLoad3dService().handleViewerClose(props.node)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
getCustomWidgets() {
|
||||
|
||||
@@ -179,12 +179,16 @@ export class CameraManager implements CameraManagerInterface {
|
||||
}
|
||||
|
||||
handleResize(width: number, height: number): void {
|
||||
const aspect = width / height
|
||||
this.updateAspectRatio(aspect)
|
||||
}
|
||||
|
||||
updateAspectRatio(aspect: number): void {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.aspect = aspect
|
||||
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
|
||||
|
||||
@@ -9,11 +9,11 @@ import { EventManager } from './EventManager'
|
||||
import { LightingManager } from './LightingManager'
|
||||
import { LoaderManager } from './LoaderManager'
|
||||
import { ModelExporter } from './ModelExporter'
|
||||
import { ModelManager } from './ModelManager'
|
||||
import { NodeStorage } from './NodeStorage'
|
||||
import { PreviewManager } from './PreviewManager'
|
||||
import { RecordingManager } from './RecordingManager'
|
||||
import { SceneManager } from './SceneManager'
|
||||
import { SceneModelManager } from './SceneModelManager'
|
||||
import { ViewHelperManager } from './ViewHelperManager'
|
||||
import {
|
||||
CameraState,
|
||||
@@ -29,22 +29,28 @@ class Load3d {
|
||||
protected animationFrameId: number | null = null
|
||||
node: LGraphNode
|
||||
|
||||
protected eventManager: EventManager
|
||||
protected nodeStorage: NodeStorage
|
||||
protected sceneManager: SceneManager
|
||||
protected cameraManager: CameraManager
|
||||
protected controlsManager: ControlsManager
|
||||
protected lightingManager: LightingManager
|
||||
protected viewHelperManager: ViewHelperManager
|
||||
protected previewManager: PreviewManager
|
||||
protected loaderManager: LoaderManager
|
||||
protected modelManager: ModelManager
|
||||
protected recordingManager: RecordingManager
|
||||
eventManager: EventManager
|
||||
nodeStorage: NodeStorage
|
||||
sceneManager: SceneManager
|
||||
cameraManager: CameraManager
|
||||
controlsManager: ControlsManager
|
||||
lightingManager: LightingManager
|
||||
viewHelperManager: ViewHelperManager
|
||||
previewManager: PreviewManager
|
||||
loaderManager: LoaderManager
|
||||
modelManager: SceneModelManager
|
||||
recordingManager: RecordingManager
|
||||
|
||||
STATUS_MOUSE_ON_NODE: boolean
|
||||
STATUS_MOUSE_ON_SCENE: boolean
|
||||
STATUS_MOUSE_ON_VIEWER: boolean
|
||||
INITIAL_RENDER_DONE: boolean = false
|
||||
|
||||
targetWidth: number = 512
|
||||
targetHeight: number = 512
|
||||
targetAspectRatio: number = 1
|
||||
isViewerMode: boolean = false
|
||||
|
||||
constructor(
|
||||
container: Element | HTMLElement,
|
||||
options: Load3DOptions = {
|
||||
@@ -54,6 +60,16 @@ class Load3d {
|
||||
) {
|
||||
this.node = options.node || ({} as LGraphNode)
|
||||
this.clock = new THREE.Clock()
|
||||
this.isViewerMode = options.isViewerMode || false
|
||||
|
||||
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
|
||||
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
if (widthWidget && heightWidget) {
|
||||
this.targetWidth = widthWidget.value as number
|
||||
this.targetHeight = heightWidget.value as number
|
||||
this.targetAspectRatio = this.targetWidth / this.targetHeight
|
||||
}
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
@@ -109,7 +125,11 @@ class Load3d {
|
||||
this.sceneManager.backgroundCamera
|
||||
)
|
||||
|
||||
this.modelManager = new ModelManager(
|
||||
if (options.disablePreview) {
|
||||
this.previewManager.togglePreview(false)
|
||||
}
|
||||
|
||||
this.modelManager = new SceneModelManager(
|
||||
this.sceneManager.scene,
|
||||
this.renderer,
|
||||
this.eventManager,
|
||||
@@ -142,6 +162,7 @@ class Load3d {
|
||||
|
||||
this.STATUS_MOUSE_ON_NODE = false
|
||||
this.STATUS_MOUSE_ON_SCENE = false
|
||||
this.STATUS_MOUSE_ON_VIEWER = false
|
||||
|
||||
this.handleResize()
|
||||
this.startAnimation()
|
||||
@@ -151,6 +172,41 @@ class Load3d {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
getEventManager(): EventManager {
|
||||
return this.eventManager
|
||||
}
|
||||
|
||||
getNodeStorage(): NodeStorage {
|
||||
return this.nodeStorage
|
||||
}
|
||||
getSceneManager(): SceneManager {
|
||||
return this.sceneManager
|
||||
}
|
||||
getCameraManager(): CameraManager {
|
||||
return this.cameraManager
|
||||
}
|
||||
getControlsManager(): ControlsManager {
|
||||
return this.controlsManager
|
||||
}
|
||||
getLightingManager(): LightingManager {
|
||||
return this.lightingManager
|
||||
}
|
||||
getViewHelperManager(): ViewHelperManager {
|
||||
return this.viewHelperManager
|
||||
}
|
||||
getPreviewManager(): PreviewManager {
|
||||
return this.previewManager
|
||||
}
|
||||
getLoaderManager(): LoaderManager {
|
||||
return this.loaderManager
|
||||
}
|
||||
getModelManager(): SceneModelManager {
|
||||
return this.modelManager
|
||||
}
|
||||
getRecordingManager(): RecordingManager {
|
||||
return this.recordingManager
|
||||
}
|
||||
|
||||
forceRender(): void {
|
||||
const delta = this.clock.getDelta()
|
||||
this.viewHelperManager.update(delta)
|
||||
@@ -172,12 +228,43 @@ class Load3d {
|
||||
}
|
||||
|
||||
renderMainScene(): void {
|
||||
const width = this.renderer.domElement.clientWidth
|
||||
const height = this.renderer.domElement.clientHeight
|
||||
const containerWidth = this.renderer.domElement.clientWidth
|
||||
const containerHeight = this.renderer.domElement.clientHeight
|
||||
|
||||
this.renderer.setViewport(0, 0, width, height)
|
||||
this.renderer.setScissor(0, 0, width, height)
|
||||
this.renderer.setScissorTest(true)
|
||||
if (this.isViewerMode) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
let offsetX: number = 0
|
||||
let offsetY: number = 0
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
offsetX = (containerWidth - renderWidth) / 2
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
offsetY = (containerHeight - renderHeight) / 2
|
||||
}
|
||||
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
this.renderer.setClearColor(0x0a0a0a)
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.setViewport(offsetX, offsetY, renderWidth, renderHeight)
|
||||
this.renderer.setScissor(offsetX, offsetY, renderWidth, renderHeight)
|
||||
|
||||
const renderAspectRatio = renderWidth / renderHeight
|
||||
this.cameraManager.updateAspectRatio(renderAspectRatio)
|
||||
} else {
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
}
|
||||
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(
|
||||
@@ -243,10 +330,15 @@ class Load3d {
|
||||
this.STATUS_MOUSE_ON_SCENE = onScene
|
||||
}
|
||||
|
||||
updateStatusMouseOnViewer(onViewer: boolean): void {
|
||||
this.STATUS_MOUSE_ON_VIEWER = onViewer
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return (
|
||||
this.STATUS_MOUSE_ON_NODE ||
|
||||
this.STATUS_MOUSE_ON_SCENE ||
|
||||
this.STATUS_MOUSE_ON_VIEWER ||
|
||||
this.isRecording() ||
|
||||
!this.INITIAL_RENDER_DONE
|
||||
)
|
||||
@@ -308,6 +400,34 @@ class Load3d {
|
||||
this.sceneManager.backgroundTexture
|
||||
)
|
||||
|
||||
if (
|
||||
this.isViewerMode &&
|
||||
this.sceneManager.backgroundTexture &&
|
||||
this.sceneManager.backgroundMesh
|
||||
) {
|
||||
const containerWidth = this.renderer.domElement.clientWidth
|
||||
const containerHeight = this.renderer.domElement.clientHeight
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
|
||||
this.sceneManager.updateBackgroundSize(
|
||||
this.sceneManager.backgroundTexture,
|
||||
this.sceneManager.backgroundMesh,
|
||||
renderWidth,
|
||||
renderHeight
|
||||
)
|
||||
}
|
||||
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
@@ -340,6 +460,10 @@ class Load3d {
|
||||
return this.cameraManager.getCurrentCameraType()
|
||||
}
|
||||
|
||||
getCurrentModel(): THREE.Object3D | null {
|
||||
return this.modelManager.currentModel
|
||||
}
|
||||
|
||||
setCameraState(state: CameraState): void {
|
||||
this.cameraManager.setCameraState(state)
|
||||
|
||||
@@ -397,6 +521,9 @@ class Load3d {
|
||||
}
|
||||
|
||||
setTargetSize(width: number, height: number): void {
|
||||
this.targetWidth = width
|
||||
this.targetHeight = height
|
||||
this.targetAspectRatio = width / height
|
||||
this.previewManager.setTargetSize(width, height)
|
||||
this.forceRender()
|
||||
}
|
||||
@@ -422,13 +549,30 @@ class Load3d {
|
||||
return
|
||||
}
|
||||
|
||||
const width = parentElement.clientWidth
|
||||
const height = parentElement.clientHeight
|
||||
const containerWidth = parentElement.clientWidth
|
||||
const containerHeight = parentElement.clientHeight
|
||||
|
||||
this.cameraManager.handleResize(width, height)
|
||||
this.sceneManager.handleResize(width, height)
|
||||
if (this.isViewerMode) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
|
||||
this.cameraManager.handleResize(renderWidth, renderHeight)
|
||||
this.sceneManager.handleResize(renderWidth, renderHeight)
|
||||
} else {
|
||||
this.cameraManager.handleResize(containerWidth, containerHeight)
|
||||
this.sceneManager.handleResize(containerWidth, containerHeight)
|
||||
}
|
||||
|
||||
this.renderer.setSize(containerWidth, containerHeight)
|
||||
|
||||
this.previewManager.handleResize()
|
||||
this.forceRender()
|
||||
|
||||
@@ -27,10 +27,6 @@ class Load3dAnimation extends Load3d {
|
||||
this.overrideAnimationLoop()
|
||||
}
|
||||
|
||||
private getCurrentModel(): THREE.Object3D | null {
|
||||
return this.modelManager.currentModel
|
||||
}
|
||||
|
||||
private overrideAnimationLoop(): void {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
UpDirection
|
||||
} from './interfaces'
|
||||
|
||||
export class ModelManager implements ModelManagerInterface {
|
||||
export class SceneModelManager implements ModelManagerInterface {
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel:
|
||||
| THREE.Object3D
|
||||
@@ -663,6 +663,12 @@ export class ModelManager implements ModelManagerInterface {
|
||||
this.originalMaterials = new WeakMap()
|
||||
}
|
||||
|
||||
addModelToScene(model: THREE.Object3D): void {
|
||||
this.currentModel = model
|
||||
|
||||
this.scene.add(this.currentModel)
|
||||
}
|
||||
|
||||
async setupModel(model: THREE.Object3D): Promise<void> {
|
||||
this.currentModel = model
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
Taken from: https://github.com/gkjohnson/threejs-sandbox/tree/master/conditional-lines
|
||||
under MIT license
|
||||
*/
|
||||
import { BufferAttribute, BufferGeometry, Vector3 } from 'three'
|
||||
|
||||
const vec = new Vector3()
|
||||
export class OutsideEdgesGeometry extends BufferGeometry {
|
||||
constructor(geometry) {
|
||||
super()
|
||||
|
||||
const edgeInfo = {}
|
||||
const index = geometry.index
|
||||
const position = geometry.attributes.position
|
||||
for (let i = 0, l = index.count; i < l; i += 3) {
|
||||
const indices = [index.getX(i + 0), index.getX(i + 1), index.getX(i + 2)]
|
||||
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const index0 = indices[j]
|
||||
const index1 = indices[(j + 1) % 3]
|
||||
|
||||
const hash = `${index0}_${index1}`
|
||||
const reverseHash = `${index1}_${index0}`
|
||||
if (reverseHash in edgeInfo) {
|
||||
delete edgeInfo[reverseHash]
|
||||
} else {
|
||||
edgeInfo[hash] = [index0, index1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const edgePositions = []
|
||||
for (const key in edgeInfo) {
|
||||
const [i0, i1] = edgeInfo[key]
|
||||
|
||||
vec.fromBufferAttribute(position, i0)
|
||||
edgePositions.push(vec.x, vec.y, vec.z)
|
||||
|
||||
vec.fromBufferAttribute(position, i1)
|
||||
edgePositions.push(vec.x, vec.y, vec.z)
|
||||
}
|
||||
|
||||
this.setAttribute(
|
||||
'position',
|
||||
new BufferAttribute(new Float32Array(edgePositions), 3, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ export interface EventCallback {
|
||||
export interface Load3DOptions {
|
||||
node?: LGraphNode
|
||||
inputSpec?: CustomInputSpec
|
||||
disablePreview?: boolean
|
||||
isViewerMode?: boolean
|
||||
}
|
||||
|
||||
export interface CaptureResult {
|
||||
@@ -159,6 +161,7 @@ export interface ModelManagerInterface {
|
||||
clearModel(): void
|
||||
reset(): void
|
||||
setupModel(model: THREE.Object3D): Promise<void>
|
||||
addModelToScene(model: THREE.Object3D): void
|
||||
setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void
|
||||
setUpDirection(direction: UpDirection): void
|
||||
materialMode: MaterialMode
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { debounce } from 'lodash'
|
||||
import _ from 'lodash'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
@@ -3976,13 +3976,19 @@ class UIManager {
|
||||
const mainImageFilename =
|
||||
new URL(mainImageUrl).searchParams.get('filename') ?? undefined
|
||||
|
||||
const combinedImageFilename =
|
||||
let combinedImageFilename: string | null | undefined
|
||||
if (
|
||||
ComfyApp.clipspace?.combinedIndex !== undefined &&
|
||||
ComfyApp.clipspace?.imgs?.[ComfyApp.clipspace.combinedIndex]?.src
|
||||
? new URL(
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace.combinedIndex].src
|
||||
).searchParams.get('filename')
|
||||
: undefined
|
||||
ComfyApp.clipspace?.imgs &&
|
||||
ComfyApp.clipspace.combinedIndex < ComfyApp.clipspace.imgs.length &&
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace.combinedIndex]?.src
|
||||
) {
|
||||
combinedImageFilename = new URL(
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace.combinedIndex].src
|
||||
).searchParams.get('filename')
|
||||
} else {
|
||||
combinedImageFilename = undefined
|
||||
}
|
||||
|
||||
const imageLayerFilenames =
|
||||
mainImageFilename !== undefined
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import arCommands from './locales/ar/commands.json'
|
||||
import ar from './locales/ar/main.json'
|
||||
import arNodes from './locales/ar/nodeDefs.json'
|
||||
import arSettings from './locales/ar/settings.json'
|
||||
import enCommands from './locales/en/commands.json'
|
||||
import en from './locales/en/main.json'
|
||||
import enNodes from './locales/en/nodeDefs.json'
|
||||
@@ -50,7 +54,8 @@ const messages = {
|
||||
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
fr: buildLocale(fr, frNodes, frCommands, frSettings),
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings)
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings),
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings)
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
|
||||
@@ -495,6 +495,16 @@
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.graphmenu-entry.danger,
|
||||
.litemenu-entry.danger {
|
||||
color: var(--error-text) !important;
|
||||
}
|
||||
|
||||
.litegraph .litemenu-entry.danger:hover:not(.disabled) {
|
||||
color: var(--error-text) !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.graphmenu-entry.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,19 @@ export class CanvasPointer {
|
||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||
|
||||
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
|
||||
static trackpadThreshold = 60
|
||||
|
||||
/**
|
||||
* The minimum time between "wheel" events to allow switching between trackpad
|
||||
* and mouse modes.
|
||||
*
|
||||
* This prevents trackpad "flick" panning from registering as regular mouse wheel.
|
||||
* After a flick gesture is complete, the automatic wheel events are sent with
|
||||
* reduced frequency, but much higher deltaX and deltaY values.
|
||||
*/
|
||||
static trackpadMaxGap = 200
|
||||
|
||||
/** The element this PointerState should capture input against when dragging. */
|
||||
element: Element
|
||||
/** Pointer ID used by drag capture. */
|
||||
@@ -77,6 +90,9 @@ export class CanvasPointer {
|
||||
/** The last pointerup event for the primary button */
|
||||
eUp?: CanvasPointerEvent
|
||||
|
||||
/** The last pointermove event that was treated as a trackpad gesture. */
|
||||
lastTrackpadEvent?: WheelEvent
|
||||
|
||||
/**
|
||||
* If set, as soon as the mouse moves outside the click drift threshold, this action is run once.
|
||||
* @param pointer [DEPRECATED] This parameter will be removed in a future release.
|
||||
@@ -257,6 +273,35 @@ export class CanvasPointer {
|
||||
delete this.onDragStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given wheel event is part of a continued trackpad gesture.
|
||||
* @param e The wheel event to check
|
||||
* @returns `true` if the event is part of a continued trackpad gesture, otherwise `false`
|
||||
*/
|
||||
#isContinuationOfGesture(e: WheelEvent): boolean {
|
||||
const { lastTrackpadEvent } = this
|
||||
if (!lastTrackpadEvent) return false
|
||||
|
||||
return (
|
||||
e.timeStamp - lastTrackpadEvent.timeStamp < CanvasPointer.trackpadMaxGap
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given wheel event is part of a trackpad gesture.
|
||||
* @param e The wheel event to check
|
||||
* @returns `true` if the event is part of a trackpad gesture, otherwise `false`
|
||||
*/
|
||||
isTrackpadGesture(e: WheelEvent): boolean {
|
||||
if (this.#isContinuationOfGesture(e)) {
|
||||
this.lastTrackpadEvent = e
|
||||
return true
|
||||
}
|
||||
|
||||
const threshold = CanvasPointer.trackpadThreshold
|
||||
return Math.abs(e.deltaX) < threshold && Math.abs(e.deltaY) < threshold
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of this {@link CanvasPointer} instance.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { Point, Rect } from './interfaces'
|
||||
import { LGraphCanvas } from './litegraph'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
@@ -34,7 +36,6 @@ import {
|
||||
alignToContainer,
|
||||
createBounds
|
||||
} from './measure'
|
||||
import { stringOrEmpty } from './strings'
|
||||
import { SubgraphInput } from './subgraph/SubgraphInput'
|
||||
import { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
import { SubgraphOutput } from './subgraph/SubgraphOutput'
|
||||
@@ -1663,6 +1664,254 @@ export class LGraph
|
||||
return { subgraph, node: subgraphNode as SubgraphNode }
|
||||
}
|
||||
|
||||
unpackSubgraph(subgraphNode: SubgraphNode) {
|
||||
if (!(subgraphNode instanceof SubgraphNode))
|
||||
throw new Error('Can only unpack Subgraph Nodes')
|
||||
this.beforeChange()
|
||||
const center = [0, 0]
|
||||
for (const node of subgraphNode.subgraph.nodes) {
|
||||
center[0] += node.pos[0] + node.size[0] / 2
|
||||
center[1] += node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
center[0] /= subgraphNode.subgraph.nodes.length
|
||||
center[1] /= subgraphNode.subgraph.nodes.length
|
||||
|
||||
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
|
||||
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
|
||||
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
|
||||
const nodeIdMap = new Map<NodeId, NodeId>()
|
||||
for (const n_info of movedNodes) {
|
||||
const node = LiteGraph.createNode(String(n_info.type), n_info.title)
|
||||
if (!node) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
nodeIdMap.set(n_info.id, ++this.last_node_id)
|
||||
node.id = this.last_node_id
|
||||
n_info.id = this.last_node_id
|
||||
|
||||
this.add(node, true)
|
||||
node.configure(n_info)
|
||||
node.pos[0] += offsetX
|
||||
node.pos[1] += offsetY
|
||||
for (const input of node.inputs) {
|
||||
input.link = null
|
||||
}
|
||||
}
|
||||
//cleanup reoute.linkIds now, but leave link.parentIds dangling
|
||||
for (const islot of subgraphNode.inputs) {
|
||||
if (!islot.link) continue
|
||||
const link = this.links.get(islot.link)
|
||||
if (!link) {
|
||||
console.warn('Broken link', islot, islot.link)
|
||||
continue
|
||||
}
|
||||
for (const reroute of LLink.getReroutes(this, link)) {
|
||||
reroute.linkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
for (const oslot of subgraphNode.outputs) {
|
||||
for (const linkId of oslot.links ?? []) {
|
||||
const link = this.links.get(linkId)
|
||||
if (!link) {
|
||||
console.warn('Broken link', oslot, linkId)
|
||||
continue
|
||||
}
|
||||
for (const reroute of LLink.getReroutes(this, link)) {
|
||||
reroute.linkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newLinks: [
|
||||
NodeId,
|
||||
number,
|
||||
NodeId,
|
||||
number,
|
||||
LinkId,
|
||||
RerouteId | undefined,
|
||||
RerouteId | undefined,
|
||||
boolean
|
||||
][] = []
|
||||
for (const [, link] of subgraphNode.subgraph._links) {
|
||||
let externalParentId: RerouteId | undefined
|
||||
if (link.origin_id === SUBGRAPH_INPUT_ID) {
|
||||
const outerLinkId = subgraphNode.inputs[link.origin_slot].link
|
||||
if (!outerLinkId) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
const outerLink = this.links[outerLinkId]
|
||||
link.origin_id = outerLink.origin_id
|
||||
link.origin_slot = outerLink.origin_slot
|
||||
externalParentId = outerLink.parentId
|
||||
} else {
|
||||
const origin_id = nodeIdMap.get(link.origin_id)
|
||||
if (!origin_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
link.origin_id = origin_id
|
||||
}
|
||||
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
|
||||
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
|
||||
[]) {
|
||||
const sublink = this.links[linkId]
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
sublink.target_id,
|
||||
sublink.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
sublink.parentId,
|
||||
true
|
||||
])
|
||||
sublink.parentId = undefined
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
const target_id = nodeIdMap.get(link.target_id)
|
||||
if (!target_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
}
|
||||
link.target_id = target_id
|
||||
}
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
externalParentId,
|
||||
false
|
||||
])
|
||||
}
|
||||
this.remove(subgraphNode)
|
||||
this.subgraphs.delete(subgraphNode.subgraph.id)
|
||||
const linkIdMap = new Map<LinkId, LinkId[]>()
|
||||
for (const newLink of newLinks) {
|
||||
let created: LLink | null | undefined
|
||||
if (newLink[0] == SUBGRAPH_INPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink[2]]
|
||||
created = this.inputNode.slots[newLink[1]].connect(
|
||||
tnode.inputs[newLink[3]],
|
||||
tnode
|
||||
)
|
||||
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink[0]]
|
||||
created = this.outputNode.slots[newLink[3]].connect(
|
||||
tnode.outputs[newLink[1]],
|
||||
tnode
|
||||
)
|
||||
} else {
|
||||
created = this._nodes_by_id[newLink[0]].connect(
|
||||
newLink[1],
|
||||
this._nodes_by_id[newLink[2]],
|
||||
newLink[3]
|
||||
)
|
||||
}
|
||||
if (!created) {
|
||||
console.error('Failed to create link')
|
||||
continue
|
||||
}
|
||||
//This is a little unwieldy since Map.has isn't a type guard
|
||||
const linkIds = linkIdMap.get(newLink[4]) ?? []
|
||||
linkIds.push(created.id)
|
||||
if (!linkIdMap.has(newLink[4])) {
|
||||
linkIdMap.set(newLink[4], linkIds)
|
||||
}
|
||||
newLink[4] = created.id
|
||||
}
|
||||
const rerouteIdMap = new Map<RerouteId, RerouteId>()
|
||||
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
|
||||
if (
|
||||
reroute.parentId !== undefined &&
|
||||
rerouteIdMap.get(reroute.parentId) === undefined
|
||||
) {
|
||||
console.error('Missing Parent ID')
|
||||
}
|
||||
const migratedReroute = new Reroute(++this.state.lastRerouteId, this, [
|
||||
reroute.pos[0] + offsetX,
|
||||
reroute.pos[1] + offsetY
|
||||
])
|
||||
rerouteIdMap.set(reroute.id, migratedReroute.id)
|
||||
this.reroutes.set(migratedReroute.id, migratedReroute)
|
||||
}
|
||||
//iterate over newly created links to update reroute parentIds
|
||||
for (const newLink of newLinks) {
|
||||
const linkInstance = this.links.get(newLink[4])
|
||||
if (!linkInstance) {
|
||||
continue
|
||||
}
|
||||
let instance: Reroute | LLink | undefined = linkInstance
|
||||
let parentId: RerouteId | undefined = newLink[6]
|
||||
if (newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
parentId = newLink[5]
|
||||
while (parentId) {
|
||||
const migratedId = rerouteIdMap.get(parentId)
|
||||
if (!migratedId) throw new Error('Broken Id link when unpacking')
|
||||
instance.parentId = migratedId
|
||||
instance = this.reroutes.get(migratedId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
const oldReroute = subgraphNode.subgraph.reroutes.get(parentId)
|
||||
if (!oldReroute) throw new Error('Broken Id link when unpacking')
|
||||
parentId = oldReroute.parentId
|
||||
}
|
||||
if (!newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: LGraphNode[] = []
|
||||
for (const nodeId of nodeIdMap.values()) {
|
||||
const node = this._nodes_by_id[nodeId]
|
||||
nodes.push(node)
|
||||
node._setConcreteSlots()
|
||||
node.arrange()
|
||||
}
|
||||
const reroutes = [...rerouteIdMap.values()]
|
||||
.map((i) => this.reroutes.get(i))
|
||||
.filter((x): x is Reroute => !!x)
|
||||
|
||||
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
|
||||
this.afterChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path of subgraph node IDs into a list of subgraph nodes.
|
||||
* Not intended to be run from subgraphs.
|
||||
@@ -2027,7 +2276,7 @@ export class LGraph
|
||||
if (url instanceof Blob || url instanceof File) {
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', (event) => {
|
||||
const result = stringOrEmpty(event.target?.result)
|
||||
const result = toString(event.target?.result)
|
||||
const data = JSON.parse(result)
|
||||
this.configure(data)
|
||||
callback?.()
|
||||
@@ -2331,6 +2580,9 @@ export class Subgraph
|
||||
nodes: this.nodes.map((node) => node.serialize()),
|
||||
groups: this.groups.map((group) => group.serialize()),
|
||||
links: [...this.links.values()].map((x) => x.asSerialisable()),
|
||||
reroutes: this.reroutes.size
|
||||
? [...this.reroutes.values()].map((x) => x.asSerialisable())
|
||||
: undefined,
|
||||
extra: this.extra
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
@@ -55,7 +57,6 @@ import {
|
||||
snapPoint
|
||||
} from './measure'
|
||||
import { NodeInputSlot } from './node/NodeInputSlot'
|
||||
import { stringOrEmpty } from './strings'
|
||||
import { Subgraph } from './subgraph/Subgraph'
|
||||
import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase'
|
||||
import { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
@@ -1244,7 +1245,7 @@ export class LGraphCanvas
|
||||
value = LGraphCanvas.getPropertyPrintableValue(value, info.values)
|
||||
|
||||
// value could contain invalid html characters, clean that
|
||||
value = LGraphCanvas.decodeHTML(stringOrEmpty(value))
|
||||
value = LGraphCanvas.decodeHTML(toString(value))
|
||||
entries.push({
|
||||
content:
|
||||
`<span class='property_name'>${info.label || i}</span>` +
|
||||
@@ -3455,10 +3456,6 @@ export class LGraphCanvas
|
||||
processMouseWheel(e: WheelEvent): void {
|
||||
if (!this.graph || !this.allow_dragcanvas) return
|
||||
|
||||
// TODO: Mouse wheel zoom rewrite
|
||||
// @ts-expect-error wheelDeltaY is non-standard property on WheelEvent
|
||||
const delta = e.wheelDeltaY ?? e.detail * -60
|
||||
|
||||
this.adjustMouseEvent(e)
|
||||
|
||||
const pos: Point = [e.clientX, e.clientY]
|
||||
@@ -3466,35 +3463,34 @@ export class LGraphCanvas
|
||||
|
||||
let { scale } = this.ds
|
||||
|
||||
if (
|
||||
LiteGraph.canvasNavigationMode === 'legacy' ||
|
||||
(LiteGraph.canvasNavigationMode === 'standard' && e.ctrlKey)
|
||||
) {
|
||||
if (delta > 0) {
|
||||
scale *= this.zoom_speed
|
||||
} else if (delta < 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
} else if (
|
||||
LiteGraph.macTrackpadGestures &&
|
||||
(!LiteGraph.macGesturesRequireMac || navigator.userAgent.includes('Mac'))
|
||||
) {
|
||||
if (e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
|
||||
if (e.deltaY > 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
} else if (e.deltaY < 0) {
|
||||
scale *= this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
} else if (e.ctrlKey) {
|
||||
// Detect if this is a trackpad gesture or mouse wheel
|
||||
const isTrackpad = this.pointer.isTrackpadGesture(e)
|
||||
|
||||
if (e.ctrlKey || LiteGraph.canvasNavigationMode === 'legacy') {
|
||||
// Legacy mode or standard mode with ctrl - use wheel for zoom
|
||||
if (isTrackpad) {
|
||||
// Trackpad gesture - use smooth scaling
|
||||
scale *= 1 + e.deltaY * (1 - this.zoom_speed) * 0.18
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY], false)
|
||||
} else if (e.shiftKey) {
|
||||
this.ds.offset[0] -= e.deltaY * 1.18 * (1 / scale)
|
||||
} else {
|
||||
this.ds.offset[0] -= e.deltaX * 1.18 * (1 / scale)
|
||||
this.ds.offset[1] -= e.deltaY * 1.18 * (1 / scale)
|
||||
// Mouse wheel - use stepped scaling
|
||||
if (e.deltaY < 0) {
|
||||
scale *= this.zoom_speed
|
||||
} else if (e.deltaY > 0) {
|
||||
scale *= 1 / this.zoom_speed
|
||||
}
|
||||
this.ds.changeScale(scale, [e.clientX, e.clientY])
|
||||
}
|
||||
} else {
|
||||
// Standard mode without ctrl - use wheel / gestures to pan
|
||||
// Trackpads and mice work on significantly different scales
|
||||
const factor = isTrackpad ? 0.18 : 0.008_333
|
||||
|
||||
if (!isTrackpad && e.shiftKey && e.deltaX === 0) {
|
||||
this.ds.offset[0] -= e.deltaY * (1 + factor) * (1 / scale)
|
||||
} else {
|
||||
this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale)
|
||||
this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6065,7 +6061,7 @@ export class LGraphCanvas
|
||||
}
|
||||
ctx.fillStyle = '#FFF'
|
||||
ctx.fillText(
|
||||
stringOrEmpty(node.order),
|
||||
toString(node.order),
|
||||
node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5,
|
||||
node.pos[1] - 6
|
||||
)
|
||||
@@ -6233,9 +6229,17 @@ export class LGraphCanvas
|
||||
break
|
||||
}
|
||||
|
||||
case 'Delete':
|
||||
graph.removeLink(segment.id)
|
||||
case 'Delete': {
|
||||
// segment can be a Reroute object, in which case segment.id is the reroute id
|
||||
const linkId =
|
||||
segment instanceof Reroute
|
||||
? segment.linkIds.values().next().value
|
||||
: segment.id
|
||||
if (linkId !== undefined) {
|
||||
graph.removeLink(linkId)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -8244,7 +8248,9 @@ export class LGraphCanvas
|
||||
if (_slot.removable) {
|
||||
menu_info.push(null)
|
||||
menu_info.push(
|
||||
_slot.locked ? 'Cannot remove' : { content: 'Remove Slot', slot }
|
||||
_slot.locked
|
||||
? 'Cannot remove'
|
||||
: { content: 'Remove Slot', slot, className: 'danger' }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ export class LiteGraphGlobal {
|
||||
]
|
||||
|
||||
/**
|
||||
* @deprecated Removed; has no effect.
|
||||
* If `true`, mouse wheel events will be interpreted as trackpad gestures.
|
||||
* Tested on MacBook M4 Pro.
|
||||
* @default false
|
||||
@@ -292,6 +293,7 @@ export class LiteGraphGlobal {
|
||||
macTrackpadGestures: boolean = false
|
||||
|
||||
/**
|
||||
* @deprecated Removed; has no effect.
|
||||
* If both this setting and {@link macTrackpadGestures} are `true`, trackpad gestures will
|
||||
* only be enabled when the browser user agent includes "Mac".
|
||||
* @default true
|
||||
|
||||
@@ -17,6 +17,8 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { EmptySubgraphInput } from '@/lib/litegraph/src/subgraph/EmptySubgraphInput'
|
||||
import { EmptySubgraphOutput } from '@/lib/litegraph/src/subgraph/EmptySubgraphOutput'
|
||||
import { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import { SubgraphInputNode } from '@/lib/litegraph/src/subgraph/SubgraphInputNode'
|
||||
@@ -638,20 +640,64 @@ export class LinkConnector {
|
||||
|
||||
if (connectingTo === 'input' && ioNode instanceof SubgraphOutputNode) {
|
||||
const output = ioNode.getSlotInPosition(canvasX, canvasY)
|
||||
if (!output) throw new Error('No output slot found for link.')
|
||||
if (!output) {
|
||||
this.dropOnNothing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Track the actual slot to use for all connections
|
||||
let targetSlot = output
|
||||
|
||||
for (const link of renderLinks) {
|
||||
link.connectToSubgraphOutput(output, this.events)
|
||||
link.connectToSubgraphOutput(targetSlot, this.events)
|
||||
|
||||
// If we just connected to an EmptySubgraphOutput, check if we should reuse the slot
|
||||
if (output instanceof EmptySubgraphOutput && ioNode.slots.length > 0) {
|
||||
// Get the last created slot (newest one)
|
||||
const createdSlot = ioNode.slots[ioNode.slots.length - 1]
|
||||
|
||||
// Only reuse the slot if the next link's type would be compatible
|
||||
// Otherwise, keep using EmptySubgraphOutput to create a new slot
|
||||
const nextLink = renderLinks[renderLinks.indexOf(link) + 1]
|
||||
if (nextLink && link.fromSlot.type === nextLink.fromSlot.type) {
|
||||
targetSlot = createdSlot
|
||||
} else {
|
||||
// Reset to EmptySubgraphOutput for different types
|
||||
targetSlot = output
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
connectingTo === 'output' &&
|
||||
ioNode instanceof SubgraphInputNode
|
||||
) {
|
||||
const input = ioNode.getSlotInPosition(canvasX, canvasY)
|
||||
if (!input) throw new Error('No input slot found for link.')
|
||||
if (!input) {
|
||||
this.dropOnNothing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Same logic for SubgraphInputNode if needed
|
||||
let targetSlot = input
|
||||
|
||||
for (const link of renderLinks) {
|
||||
link.connectToSubgraphInput(input, this.events)
|
||||
link.connectToSubgraphInput(targetSlot, this.events)
|
||||
|
||||
// If we just connected to an EmptySubgraphInput, check if we should reuse the slot
|
||||
if (input instanceof EmptySubgraphInput && ioNode.slots.length > 0) {
|
||||
// Get the last created slot (newest one)
|
||||
const createdSlot = ioNode.slots[ioNode.slots.length - 1]
|
||||
|
||||
// Only reuse the slot if the next link's type would be compatible
|
||||
// Otherwise, keep using EmptySubgraphInput to create a new slot
|
||||
const nextLink = renderLinks[renderLinks.indexOf(link) + 1]
|
||||
if (nextLink && link.fromSlot.type === nextLink.fromSlot.type) {
|
||||
targetSlot = createdSlot
|
||||
} else {
|
||||
// Reset to EmptySubgraphInput for different types
|
||||
targetSlot = input
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type {
|
||||
ReadOnlyRect,
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import type { ISlotType } from './litegraph'
|
||||
|
||||
/**
|
||||
* Uses the standard String() function to coerce to string, unless the value is null or undefined - then null.
|
||||
* @param value The value to convert
|
||||
* @returns String(value) or null
|
||||
*/
|
||||
export function stringOrNull(value: unknown): string | null {
|
||||
return value == null ? null : String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the standard String() function to coerce to string, unless the value is null or undefined - then an empty string
|
||||
* @param value The value to convert
|
||||
* @returns String(value) or ""
|
||||
*/
|
||||
export function stringOrEmpty(value: unknown): string {
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
|
||||
export function parseSlotTypes(type: ISlotType): string[] {
|
||||
return type == '' || type == '0'
|
||||
? ['*']
|
||||
|
||||
@@ -191,7 +191,7 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param event The event that triggered the context menu.
|
||||
*/
|
||||
protected showSlotContextMenu(slot: TSlot, event: CanvasPointerEvent): void {
|
||||
const options: IContextMenuValue[] = this.#getSlotMenuOptions(slot)
|
||||
const options: (IContextMenuValue | null)[] = this.#getSlotMenuOptions(slot)
|
||||
if (!(options.length > 0)) return
|
||||
|
||||
new LiteGraph.ContextMenu(options, {
|
||||
@@ -208,20 +208,26 @@ export abstract class SubgraphIONodeBase<
|
||||
* @param slot The slot to get the context menu options for.
|
||||
* @returns The context menu options.
|
||||
*/
|
||||
#getSlotMenuOptions(slot: TSlot): IContextMenuValue[] {
|
||||
const options: IContextMenuValue[] = []
|
||||
#getSlotMenuOptions(slot: TSlot): (IContextMenuValue | null)[] {
|
||||
const options: (IContextMenuValue | null)[] = []
|
||||
|
||||
// Disconnect option if slot has connections
|
||||
if (slot !== this.emptySlot && slot.linkIds.length > 0) {
|
||||
options.push({ content: 'Disconnect Links', value: 'disconnect' })
|
||||
}
|
||||
|
||||
// Remove / rename slot option (except for the empty slot)
|
||||
// Rename slot option (except for the empty slot)
|
||||
if (slot !== this.emptySlot) {
|
||||
options.push(
|
||||
{ content: 'Remove Slot', value: 'remove' },
|
||||
{ content: 'Rename Slot', value: 'rename' }
|
||||
)
|
||||
options.push({ content: 'Rename Slot', value: 'rename' })
|
||||
}
|
||||
|
||||
if (slot !== this.emptySlot) {
|
||||
options.push(null) // separator
|
||||
options.push({
|
||||
content: 'Remove Slot',
|
||||
value: 'remove',
|
||||
className: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { pull } from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
@@ -9,7 +11,6 @@ import type {
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { removeFromArray } from '@/lib/litegraph/src/utils/collections'
|
||||
|
||||
import type { SubgraphInput } from './SubgraphInput'
|
||||
import type { SubgraphOutputNode } from './SubgraphOutputNode'
|
||||
@@ -59,7 +60,7 @@ export class SubgraphOutput extends SubgraphSlot {
|
||||
existingLink.disconnect(subgraph, 'input')
|
||||
const resolved = existingLink.resolve(subgraph)
|
||||
const links = resolved.output?.links
|
||||
if (links) removeFromArray(links, existingLink.id)
|
||||
if (links) pull(links, existingLink.id)
|
||||
}
|
||||
|
||||
const link = new LLink(
|
||||
|
||||
@@ -112,11 +112,3 @@ export function findFreeSlotOfType<T extends { type: ISlotType }>(
|
||||
}
|
||||
return wildSlot ?? occupiedSlot ?? occupiedWildSlot
|
||||
}
|
||||
|
||||
export function removeFromArray<T>(array: T[], value: T): boolean {
|
||||
const index = array.indexOf(value)
|
||||
const found = index !== -1
|
||||
|
||||
if (found) array.splice(index, 1)
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { IKnobWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { getWidgetStep } from '@/lib/litegraph/src/utils/widget'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
|
||||
import type { ISliderWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from 'lodash'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import { beforeEach, describe, expect, vi } from 'vitest'
|
||||
|
||||
import { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal'
|
||||
|
||||
197
src/lib/litegraph/test/subgraph/SubgraphConversion.test.ts
Normal file
197
src/lib/litegraph/test/subgraph/SubgraphConversion.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ISlotType,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from './fixtures/subgraphHelpers'
|
||||
|
||||
function createNode(
|
||||
graph: LGraph,
|
||||
inputs: ISlotType[] = [],
|
||||
outputs: ISlotType[] = [],
|
||||
title?: string
|
||||
) {
|
||||
const type = JSON.stringify({ inputs, outputs })
|
||||
if (!LiteGraph.registered_node_types[type]) {
|
||||
class testnode extends LGraphNode {
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
let i_count = 0
|
||||
for (const input of inputs) this.addInput('input_' + i_count++, input)
|
||||
let o_count = 0
|
||||
for (const output of outputs)
|
||||
this.addOutput('output_' + o_count++, output)
|
||||
}
|
||||
}
|
||||
LiteGraph.registered_node_types[type] = testnode
|
||||
}
|
||||
const node = LiteGraph.createNode(type, title)
|
||||
if (!node) {
|
||||
throw new Error('Failed to create node')
|
||||
}
|
||||
graph.add(node)
|
||||
return node
|
||||
}
|
||||
describe('SubgraphConversion', () => {
|
||||
describe('Subgraph Unpacking Functionality', () => {
|
||||
it('Should keep interior nodes and links', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const node1 = createNode(subgraph, [], ['number'])
|
||||
const node2 = createNode(subgraph, ['number'])
|
||||
node1.connect(0, node2, 0)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.nodes.length).toBe(2)
|
||||
expect(graph.links.size).toBe(1)
|
||||
})
|
||||
it('Should merge boundry links', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'value', type: 'number' }],
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const innerNode1 = createNode(subgraph, [], ['number'])
|
||||
const innerNode2 = createNode(subgraph, ['number'], [])
|
||||
subgraph.inputNode.slots[0].connect(innerNode2.inputs[0], innerNode2)
|
||||
subgraph.outputNode.slots[0].connect(innerNode1.outputs[0], innerNode1)
|
||||
|
||||
const outerNode1 = createNode(graph, [], ['number'])
|
||||
const outerNode2 = createNode(graph, ['number'])
|
||||
outerNode1.connect(0, subgraphNode, 0)
|
||||
subgraphNode.connect(0, outerNode2, 0)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.nodes.length).toBe(4)
|
||||
expect(graph.links.size).toBe(2)
|
||||
})
|
||||
it('Should keep reroutes', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number'])
|
||||
const innerLink = subgraph.outputNode.slots[0].connect(
|
||||
inner.outputs[0],
|
||||
inner
|
||||
)
|
||||
assert(innerLink)
|
||||
|
||||
const outer = createNode(graph, ['number'])
|
||||
const outerLink = subgraphNode.connect(0, outer, 0)
|
||||
assert(outerLink)
|
||||
|
||||
subgraph.createReroute([10, 10], innerLink)
|
||||
graph.createReroute([10, 10], outerLink)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(2)
|
||||
})
|
||||
it('Should map reroutes onto split outputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [
|
||||
{ name: 'value1', type: 'number' },
|
||||
{ name: 'value2', type: 'number' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner = createNode(subgraph, [], ['number', 'number'])
|
||||
const innerLink1 = subgraph.outputNode.slots[0].connect(
|
||||
inner.outputs[0],
|
||||
inner
|
||||
)
|
||||
const innerLink2 = subgraph.outputNode.slots[1].connect(
|
||||
inner.outputs[1],
|
||||
inner
|
||||
)
|
||||
const outer1 = createNode(graph, ['number'])
|
||||
const outer2 = createNode(graph, ['number'])
|
||||
const outer3 = createNode(graph, ['number'])
|
||||
const outerLink1 = subgraphNode.connect(0, outer1, 0)
|
||||
assert(innerLink1 && innerLink2 && outerLink1)
|
||||
subgraphNode.connect(0, outer2, 0)
|
||||
subgraphNode.connect(1, outer3, 0)
|
||||
|
||||
subgraph.createReroute([10, 10], innerLink1)
|
||||
subgraph.createReroute([10, 20], innerLink2)
|
||||
graph.createReroute([10, 10], outerLink1)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(3)
|
||||
expect(graph.links.size).toBe(3)
|
||||
let linkRefCount = 0
|
||||
for (const reroute of graph.reroutes.values()) {
|
||||
linkRefCount += reroute.linkIds.size
|
||||
}
|
||||
expect(linkRefCount).toBe(4)
|
||||
})
|
||||
it('Should map reroutes onto split inputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: 'value1', type: 'number' },
|
||||
{ name: 'value2', type: 'number' }
|
||||
]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const graph = subgraphNode.graph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const inner1 = createNode(subgraph, ['number', 'number'])
|
||||
const inner2 = createNode(subgraph, ['number'])
|
||||
const innerLink1 = subgraph.inputNode.slots[0].connect(
|
||||
inner1.inputs[0],
|
||||
inner1
|
||||
)
|
||||
const innerLink2 = subgraph.inputNode.slots[1].connect(
|
||||
inner1.inputs[1],
|
||||
inner1
|
||||
)
|
||||
const innerLink3 = subgraph.inputNode.slots[1].connect(
|
||||
inner2.inputs[0],
|
||||
inner2
|
||||
)
|
||||
assert(innerLink1 && innerLink2 && innerLink3)
|
||||
const outer = createNode(graph, [], ['number'])
|
||||
const outerLink1 = outer.connect(0, subgraphNode, 0)
|
||||
const outerLink2 = outer.connect(0, subgraphNode, 1)
|
||||
assert(outerLink1 && outerLink2)
|
||||
|
||||
graph.createReroute([10, 10], outerLink1)
|
||||
graph.createReroute([10, 20], outerLink2)
|
||||
subgraph.createReroute([10, 10], innerLink1)
|
||||
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(3)
|
||||
expect(graph.links.size).toBe(3)
|
||||
let linkRefCount = 0
|
||||
for (const reroute of graph.reroutes.values()) {
|
||||
linkRefCount += reroute.linkIds.size
|
||||
}
|
||||
expect(linkRefCount).toBe(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,289 +0,0 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import type { CapturedEvent } from './subgraphHelpers'
|
||||
|
||||
/**
|
||||
* Extended captured event with additional metadata not in the base infrastructure
|
||||
*/
|
||||
export interface ExtendedCapturedEvent<T = unknown> extends CapturedEvent<T> {
|
||||
defaultPrevented: boolean
|
||||
bubbles: boolean
|
||||
cancelable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an enhanced event capture that includes additional event properties
|
||||
* This extends the basic createEventCapture with more metadata
|
||||
*/
|
||||
export function createExtendedEventCapture<T = unknown>(
|
||||
eventTarget: EventTarget,
|
||||
eventTypes: string[]
|
||||
) {
|
||||
const capturedEvents: ExtendedCapturedEvent<T>[] = []
|
||||
const listeners: Array<() => void> = []
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
const listener = (event: Event) => {
|
||||
capturedEvents.push({
|
||||
type: eventType,
|
||||
detail: (event as CustomEvent<T>).detail,
|
||||
timestamp: Date.now(),
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
bubbles: event.bubbles,
|
||||
cancelable: event.cancelable
|
||||
})
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(eventType, listener)
|
||||
listeners.push(() => eventTarget.removeEventListener(eventType, listener))
|
||||
}
|
||||
|
||||
return {
|
||||
events: capturedEvents,
|
||||
clear: () => {
|
||||
capturedEvents.length = 0
|
||||
},
|
||||
cleanup: () => {
|
||||
for (const cleanup of listeners) cleanup()
|
||||
},
|
||||
getEventsByType: (type: string) =>
|
||||
capturedEvents.filter((e) => e.type === type),
|
||||
getLatestEvent: () => capturedEvents.at(-1),
|
||||
getFirstEvent: () => capturedEvents[0],
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to be captured
|
||||
*/
|
||||
async waitForEvent(
|
||||
type: string,
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>> {
|
||||
const existingEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (existingEvent) return existingEvent
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
reject(new Error(`Event ${type} not received within ${timeoutMs}ms`))
|
||||
}, timeoutMs)
|
||||
|
||||
const eventListener = (_event: Event) => {
|
||||
const capturedEvent = capturedEvents.find((e) => e.type === type)
|
||||
if (capturedEvent) {
|
||||
clearTimeout(timeout)
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
resolve(capturedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for a sequence of events to occur in order
|
||||
*/
|
||||
async waitForSequence(
|
||||
expectedSequence: string[],
|
||||
timeoutMs: number = 1000
|
||||
): Promise<ExtendedCapturedEvent<T>[]> {
|
||||
// Check if sequence is already complete
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)
|
||||
) {
|
||||
return capturedEvents.slice(0, expectedSequence.length)
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
const actual = capturedEvents.map((e) => e.type).join(', ')
|
||||
const expected = expectedSequence.join(', ')
|
||||
reject(
|
||||
new Error(
|
||||
`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`
|
||||
)
|
||||
)
|
||||
}, timeoutMs)
|
||||
|
||||
const checkSequence = () => {
|
||||
if (capturedEvents.length >= expectedSequence.length) {
|
||||
const actualSequence = capturedEvents
|
||||
.slice(0, expectedSequence.length)
|
||||
.map((e) => e.type)
|
||||
if (
|
||||
JSON.stringify(actualSequence) ===
|
||||
JSON.stringify(expectedSequence)
|
||||
) {
|
||||
cleanup()
|
||||
resolve(capturedEvents.slice(0, expectedSequence.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventListener = () => checkSequence()
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.removeEventListener(type, eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for all expected event types
|
||||
for (const type of expectedSequence) {
|
||||
eventTarget.addEventListener(type, eventListener)
|
||||
}
|
||||
|
||||
// Initial check in case events already exist
|
||||
checkSequence()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for memory leak testing
|
||||
*/
|
||||
export interface MemoryLeakTestOptions {
|
||||
cycles?: number
|
||||
instancesPerCycle?: number
|
||||
gcAfterEach?: boolean
|
||||
maxMemoryGrowth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory leak test factory
|
||||
* Useful for testing that event listeners and references are properly cleaned up
|
||||
*/
|
||||
export function createMemoryLeakTest<T>(
|
||||
// @ts-expect-error TODO: Fix after merge - T does not satisfy constraint 'object'
|
||||
setupFn: () => { ref: WeakRef<T>; cleanup: () => void },
|
||||
options: MemoryLeakTestOptions = {}
|
||||
) {
|
||||
const {
|
||||
cycles = 1,
|
||||
instancesPerCycle = 1,
|
||||
gcAfterEach = true,
|
||||
maxMemoryGrowth = 0
|
||||
} = options
|
||||
|
||||
return async () => {
|
||||
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
||||
const refs: WeakRef<T>[] = []
|
||||
const initialMemory = process.memoryUsage?.()?.heapUsed || 0
|
||||
|
||||
for (let cycle = 0; cycle < cycles; cycle++) {
|
||||
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
||||
const cycleRefs: WeakRef<T>[] = []
|
||||
|
||||
for (let instance = 0; instance < instancesPerCycle; instance++) {
|
||||
const { ref, cleanup } = setupFn()
|
||||
cycleRefs.push(ref)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
refs.push(...cycleRefs)
|
||||
|
||||
if (gcAfterEach && global.gc) {
|
||||
global.gc()
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Final garbage collection
|
||||
if (global.gc) {
|
||||
global.gc()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
// Check if objects were collected
|
||||
const uncollectedRefs = refs.filter((ref) => ref.deref() !== undefined)
|
||||
if (uncollectedRefs.length > 0) {
|
||||
console.warn(
|
||||
`${uncollectedRefs.length} objects were not garbage collected`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory growth check
|
||||
if (maxMemoryGrowth > 0 && process.memoryUsage) {
|
||||
const finalMemory = process.memoryUsage().heapUsed
|
||||
const memoryGrowth = finalMemory - initialMemory
|
||||
|
||||
if (memoryGrowth > maxMemoryGrowth) {
|
||||
throw new Error(
|
||||
`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a performance monitor for event operations
|
||||
*/
|
||||
export function createEventPerformanceMonitor() {
|
||||
const measurements: Array<{
|
||||
operation: string
|
||||
duration: number
|
||||
timestamp: number
|
||||
}> = []
|
||||
|
||||
return {
|
||||
measure: <T>(operation: string, fn: () => T): T => {
|
||||
const start = performance.now()
|
||||
const result = fn()
|
||||
const end = performance.now()
|
||||
|
||||
measurements.push({
|
||||
operation,
|
||||
duration: end - start,
|
||||
timestamp: start
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
getMeasurements: () => [...measurements],
|
||||
|
||||
getAverageDuration: (operation: string) => {
|
||||
const operationMeasurements = measurements.filter(
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (operationMeasurements.length === 0) return 0
|
||||
|
||||
const totalDuration = operationMeasurements.reduce(
|
||||
(sum, m) => sum + m.duration,
|
||||
0
|
||||
)
|
||||
return totalDuration / operationMeasurements.length
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
measurements.length = 0
|
||||
},
|
||||
|
||||
assertPerformance: (operation: string, maxDuration: number) => {
|
||||
// @ts-expect-error 'this' implicitly has type 'any'
|
||||
const measurements = this.getMeasurements()
|
||||
const relevantMeasurements = measurements.filter(
|
||||
// @ts-expect-error Parameter 'm' implicitly has an 'any' type
|
||||
(m) => m.operation === operation
|
||||
)
|
||||
if (relevantMeasurements.length === 0) return
|
||||
|
||||
const avgDuration =
|
||||
// @ts-expect-error Parameter 'sum' and 'm' implicitly have 'any' type
|
||||
relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) /
|
||||
relevantMeasurements.length
|
||||
expect(avgDuration).toBeLessThan(maxDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
273
src/locales/ar/commands.json
Normal file
273
src/locales/ar/commands.json
Normal file
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"Comfy-Desktop_CheckForUpdates": {
|
||||
"label": "التحقق من التحديثات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "فتح مجلد العقد المخصصة"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "فتح مجلد المدخلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "فتح مجلد السجلات"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "فتح extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "فتح مجلد النماذج"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "فتح مجلد المخرجات"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "فتح أدوات المطور"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "دليل المستخدم لسطح المكتب"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "خروج"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "إعادة التثبيت"
|
||||
},
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "إعادة التشغيل"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "تصفح القوالب"
|
||||
},
|
||||
"Comfy_Canvas_AddEditModelStep": {
|
||||
"label": "إضافة خطوة تحرير النموذج"
|
||||
},
|
||||
"Comfy_Canvas_DeleteSelectedItems": {
|
||||
"label": "حذف العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "تعديل العرض ليناسب العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Down": {
|
||||
"label": "تحريك العقد المحددة للأسفل"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Left": {
|
||||
"label": "تحريك العقد المحددة لليسار"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Right": {
|
||||
"label": "تحريك العقد المحددة لليمين"
|
||||
},
|
||||
"Comfy_Canvas_MoveSelectedNodes_Up": {
|
||||
"label": "تحريك العقد المحددة للأعلى"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "إعادة تعيين العرض"
|
||||
},
|
||||
"Comfy_Canvas_Resize": {
|
||||
"label": "تغيير حجم العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLinkVisibility": {
|
||||
"label": "تبديل رؤية الروابط في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "تبديل القفل في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "تبديل الخريطة المصغرة في اللوحة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "تجاوز/إلغاء تجاوز العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "طي/توسيع العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "كتم/إلغاء كتم العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العقد المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "تكبير"
|
||||
},
|
||||
"Comfy_Canvas_ZoomOut": {
|
||||
"label": "تصغير"
|
||||
},
|
||||
"Comfy_ClearPendingTasks": {
|
||||
"label": "مسح المهام المعلقة"
|
||||
},
|
||||
"Comfy_ClearWorkflow": {
|
||||
"label": "مسح سير العمل"
|
||||
},
|
||||
"Comfy_ContactSupport": {
|
||||
"label": "الاتصال بالدعم"
|
||||
},
|
||||
"Comfy_DuplicateWorkflow": {
|
||||
"label": "تكرار سير العمل الحالي"
|
||||
},
|
||||
"Comfy_ExportWorkflow": {
|
||||
"label": "تصدير سير العمل"
|
||||
},
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "تصدير سير العمل (تنسيق API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "إرسال ملاحظات"
|
||||
},
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "تحويل التحديد إلى رسم فرعي"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "الخروج من الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "ضبط المجموعة على المحتويات"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "تجميع العقد المحددة"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "تحويل العقد المحددة إلى عقدة مجموعة"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "إدارة عقد المجموعات"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "إلغاء تجميع عقد المجموعات المحددة"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "حول ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "فتح خادم Comfy-Org على Discord"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "فتح مستندات ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "فتح منتدى ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "فتح مشكلات ComfyUI"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "إيقاف مؤقت"
|
||||
},
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "تحميل سير العمل الافتراضي"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "تبديل مدير العقد المخصصة"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "تبديل شريط تقدم مدير العقد المخصصة"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "تقليل حجم الفرشاة في محرر القناع"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "زيادة حجم الفرشاة في محرر القناع"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "فتح محرر القناع للعقدة المحددة"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "سير عمل جديد فارغ"
|
||||
},
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "فتح سير عمل"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "إضافة الأمر إلى قائمة الانتظار"
|
||||
},
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "إضافة الأمر إلى مقدمة قائمة الانتظار"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "إدراج عقد الإخراج المحددة في قائمة الانتظار"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "إعادة"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "تحديث تعريفات العقد"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "حفظ سير العمل"
|
||||
},
|
||||
"Comfy_SaveWorkflowAs": {
|
||||
"label": "حفظ سير العمل باسم"
|
||||
},
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "عرض نافذة الإعدادات"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "تبديل النمط (فاتح/داكن)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "تراجع"
|
||||
},
|
||||
"Comfy_User_OpenSignInDialog": {
|
||||
"label": "فتح نافذة تسجيل الدخول"
|
||||
},
|
||||
"Comfy_User_SignOut": {
|
||||
"label": "تسجيل الخروج"
|
||||
},
|
||||
"Workspace_CloseWorkflow": {
|
||||
"label": "إغلاق سير العمل الحالي"
|
||||
},
|
||||
"Workspace_NextOpenedWorkflow": {
|
||||
"label": "سير العمل التالي المفتوح"
|
||||
},
|
||||
"Workspace_PreviousOpenedWorkflow": {
|
||||
"label": "سير العمل السابق المفتوح"
|
||||
},
|
||||
"Workspace_SearchBox_Toggle": {
|
||||
"label": "تبديل مربع البحث"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel": {
|
||||
"label": "تبديل اللوحة السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_command-terminal": {
|
||||
"label": "تبديل لوحة الطرفية السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "تبديل لوحة السجلات السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "تبديل اللوحة السفلية الأساسية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "تبديل لوحة تحكم العرض السفلية"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "عرض مربع حوار اختصارات لوحة المفاتيح"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "تبديل وضع التركيز"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_model-library": {
|
||||
"label": "تبديل الشريط الجانبي لمكتبة النماذج",
|
||||
"tooltip": "مكتبة النماذج"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "تبديل الشريط الجانبي لمكتبة العقد",
|
||||
"tooltip": "مكتبة العقد"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "تبديل الشريط الجانبي لقائمة الانتظار",
|
||||
"tooltip": "قائمة الانتظار"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_workflows": {
|
||||
"label": "تبديل الشريط الجانبي لسير العمل",
|
||||
"tooltip": "سير العمل"
|
||||
}
|
||||
}
|
||||
1682
src/locales/ar/main.json
Normal file
1682
src/locales/ar/main.json
Normal file
File diff suppressed because it is too large
Load Diff
8660
src/locales/ar/nodeDefs.json
Normal file
8660
src/locales/ar/nodeDefs.json
Normal file
File diff suppressed because it is too large
Load Diff
416
src/locales/ar/settings.json
Normal file
416
src/locales/ar/settings.json
Normal file
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"Comfy-Desktop_AutoUpdate": {
|
||||
"name": "التحقق تلقائيًا من التحديثات"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "إرسال إحصائيات الاستخدام المجهولة"
|
||||
},
|
||||
"Comfy-Desktop_UV_PypiInstallMirror": {
|
||||
"name": "مرآة تثبيت Pypi",
|
||||
"tooltip": "مرآة التثبيت الافتراضية لـ pip"
|
||||
},
|
||||
"Comfy-Desktop_UV_PythonInstallMirror": {
|
||||
"name": "مرآة تثبيت بايثون",
|
||||
"tooltip": "يتم تحميل تثبيتات بايثون المدارة من مشروع Astral python-build-standalone. يمكن تعيين هذا المتغير إلى عنوان مرآة لاستخدام مصدر مختلف لتثبيتات بايثون. سيحل العنوان المقدم محل 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://."
|
||||
},
|
||||
"Comfy-Desktop_UV_TorchInstallMirror": {
|
||||
"name": "مرآة تثبيت Torch",
|
||||
"tooltip": "مرآة تثبيت pip لـ pytorch"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "نمط النافذة",
|
||||
"options": {
|
||||
"custom": "مخصص",
|
||||
"default": "افتراضي"
|
||||
},
|
||||
"tooltip": "مخصص: استبدال شريط عنوان النظام بالقائمة العلوية لـ ComfyUI"
|
||||
},
|
||||
"Comfy_Canvas_BackgroundImage": {
|
||||
"name": "صورة خلفية اللوحة",
|
||||
"tooltip": "رابط صورة لخلفية اللوحة. يمكنك النقر بزر الفأرة الأيمن على صورة في لوحة النتائج واختيار \"تعيين كخلفية\" لاستخدامها، أو رفع صورتك الخاصة باستخدام زر الرفع."
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "وضع تنقل اللوحة",
|
||||
"options": {
|
||||
"Left-Click Pan (Legacy)": "سحب بالنقر الأيسر (قديم)",
|
||||
"Standard (New)": "قياسي (جديد)"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
"name": "عرض صندوق أدوات التحديد"
|
||||
},
|
||||
"Comfy_ConfirmClear": {
|
||||
"name": "طلب التأكيد عند مسح سير العمل"
|
||||
},
|
||||
"Comfy_DOMClippingEnabled": {
|
||||
"name": "تمكين قص عناصر DOM (قد يقلل التمكين من الأداء)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "تمكين خيارات وضع المطور (حفظ API، إلخ)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "تعطيل تقريب عناصر التحكم العائمة الافتراضية",
|
||||
"tooltip": "(يتطلب إعادة تحميل الصفحة) لا يمكن تعطيل التقريب عندما يتم تعيينه من العقدة في الخلفية."
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "تعطيل منزلقات أدوات العقد"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "دقة تحكم +Ctrl فوق/تحت"
|
||||
},
|
||||
"Comfy_EnableTooltips": {
|
||||
"name": "تمكين التلميحات"
|
||||
},
|
||||
"Comfy_EnableWorkflowViewRestore": {
|
||||
"name": "حفظ واستعادة موقع اللوحة ومستوى التكبير في سير العمل"
|
||||
},
|
||||
"Comfy_FloatRoundingPrecision": {
|
||||
"name": "عدد أرقام التقريب العشرية لأدوات التحكم العائمة [0 = تلقائي]",
|
||||
"tooltip": "(يتطلب إعادة تحميل الصفحة)"
|
||||
},
|
||||
"Comfy_Graph_CanvasInfo": {
|
||||
"name": "عرض معلومات اللوحة في الزاوية السفلى اليسرى (الإطارات في الثانية، إلخ)"
|
||||
},
|
||||
"Comfy_Graph_CanvasMenu": {
|
||||
"name": "عرض قائمة لوحة الرسم البياني"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "تمكين اختصار التكبير السريع (Ctrl + Shift + سحب)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "علامات منتصف الروابط",
|
||||
"options": {
|
||||
"Arrow": "سهم",
|
||||
"Circle": "دائرة",
|
||||
"None": "لا شيء"
|
||||
}
|
||||
},
|
||||
"Comfy_Graph_ZoomSpeed": {
|
||||
"name": "سرعة تكبير اللوحة"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "تباعد حول العقد المحددة في المجموعة"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "انقر مزدوج على عنوان المجموعة للتحرير"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "الإجراء عند تحرير الرابط (بدون مفتاح تعديل)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRelease_ActionShift": {
|
||||
"name": "الإجراء عند تحرير الرابط (Shift)",
|
||||
"options": {
|
||||
"context menu": "قائمة السياق",
|
||||
"no action": "لا إجراء",
|
||||
"search box": "صندوق البحث"
|
||||
}
|
||||
},
|
||||
"Comfy_LinkRenderMode": {
|
||||
"name": "وضع عرض الروابط",
|
||||
"options": {
|
||||
"Hidden": "مخفي",
|
||||
"Linear": "خطي",
|
||||
"Spline": "منحنى",
|
||||
"Straight": "مستقيم"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "تمكين عارض ثلاثي الأبعاد (تجريبي)",
|
||||
"tooltip": "تمكين عارض ثلاثي الأبعاد (تجريبي) للعقد المحددة. تتيح هذه الميزة عرض النماذج ثلاثية الأبعاد والتفاعل معها مباشرة داخل العارض ثلاثي الأبعاد بحجمه الكامل."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "لون الخلفية الابتدائي",
|
||||
"tooltip": "يحدد لون الخلفية الافتراضي للمشهد ثلاثي الأبعاد. يمكن تعديل هذا اللون لكل عنصر ثلاثي الأبعاد بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_CameraType": {
|
||||
"name": "نوع الكاميرا الابتدائي",
|
||||
"options": {
|
||||
"orthographic": "متعامد",
|
||||
"perspective": "منظور"
|
||||
},
|
||||
"tooltip": "يحدد ما إذا كانت الكاميرا منظور أو متعامدة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد. يمكن تعديل هذا الإعداد لكل عنصر بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_LightAdjustmentIncrement": {
|
||||
"name": "زيادة تعديل الضوء",
|
||||
"tooltip": "يتحكم في حجم الخطوة عند تعديل شدة الإضاءة في المشاهد ثلاثية الأبعاد. قيمة أصغر تسمح بتحكم أدق، وأكبر قيمة تعطي تغييرات أكثر وضوحًا."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensity": {
|
||||
"name": "شدة الإضاءة الابتدائية",
|
||||
"tooltip": "يحدد مستوى سطوع الإضاءة الافتراضي في المشهد ثلاثي الأبعاد. يمكن تعديله لكل عنصر بعد الإنشاء."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMaximum": {
|
||||
"name": "أقصى شدة إضاءة",
|
||||
"tooltip": "يحدد الحد الأقصى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
|
||||
},
|
||||
"Comfy_Load3D_LightIntensityMinimum": {
|
||||
"name": "أدنى شدة إضاءة",
|
||||
"tooltip": "يحدد الحد الأدنى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
|
||||
},
|
||||
"Comfy_Load3D_ShowGrid": {
|
||||
"name": "رؤية الشبكة الابتدائية",
|
||||
"tooltip": "يتحكم في ظهور الشبكة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
|
||||
},
|
||||
"Comfy_Load3D_ShowPreview": {
|
||||
"name": "رؤية المعاينة الابتدائية",
|
||||
"tooltip": "يتحكم في ظهور شاشة المعاينة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
|
||||
},
|
||||
"Comfy_Locale": {
|
||||
"name": "اللغة"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "مضاعف سرعة تعديل الفرشاة",
|
||||
"tooltip": "يتحكم في سرعة تغير حجم الفرشاة وصلابتها أثناء التعديل. القيم الأعلى تعني تغييرات أسرع."
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "تقييد تعديل الفرشاة إلى المحور السائد",
|
||||
"tooltip": "عند التمكين، تؤثر التعديلات على الحجم أو الصلابة فقط بناءً على الاتجاه الذي تتحرك فيه أكثر."
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "استخدام محرر القناع الجديد",
|
||||
"tooltip": "التحويل إلى واجهة محرر القناع الجديدة"
|
||||
},
|
||||
"Comfy_ModelLibrary_AutoLoadAll": {
|
||||
"name": "تحميل جميع مجلدات النماذج تلقائيًا",
|
||||
"tooltip": "إذا كانت صحيحة، سيتم تحميل جميع المجلدات عند فتح مكتبة النماذج (قد يسبب تأخيرًا أثناء التحميل). إذا كانت خاطئة، يتم تحميل مجلدات النماذج على مستوى الجذر فقط عند النقر عليها."
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "اسم العرض في شجرة مكتبة النماذج",
|
||||
"options": {
|
||||
"filename": "اسم الملف",
|
||||
"title": "العنوان"
|
||||
},
|
||||
"tooltip": "اختر \"اسم الملف\" لعرض اسم الملف المبسط بدون المجلد أو الامتداد \".safetensors\" في قائمة النماذج. اختر \"العنوان\" لعرض عنوان بيانات النموذج القابل للتكوين."
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "وضع شارة معرف العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "وضع شارة دورة حياة العقدة",
|
||||
"options": {
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "وضع شارة مصدر العقدة",
|
||||
"options": {
|
||||
"Hide built-in": "إخفاء المدمج",
|
||||
"None": "لا شيء",
|
||||
"Show all": "عرض الكل"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_ShowApiPricing": {
|
||||
"name": "عرض شارة تسعير عقدة API"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "تنفيذ مربع بحث العقدة",
|
||||
"options": {
|
||||
"default": "افتراضي",
|
||||
"litegraph (legacy)": "لايت جراف (قديم)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "معاينة العقدة",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "عرض فئة العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "عرض اسم معرف العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "عرض تكرار العقدة في نتائج البحث",
|
||||
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "عدد اقتراحات العقد",
|
||||
"tooltip": "خاص بمربع بحث / قائمة السياق في لايت جراف فقط"
|
||||
},
|
||||
"Comfy_Node_AllowImageSizeDraw": {
|
||||
"name": "عرض العرض × الارتفاع تحت معاينة الصورة"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "التثبيت التلقائي للرابط إلى فتحة العقدة",
|
||||
"tooltip": "عند سحب رابط فوق عقدة، يتم تثبيت الرابط تلقائيًا على فتحة إدخال صالحة في العقدة"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "الحفاظ على جميع الروابط عند حذف العقد",
|
||||
"tooltip": "عند حذف عقدة، حاول إعادة توصيل جميع روابط الإدخال والإخراج (تجاوز العقدة المحذوفة)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "النقر المزدوج على عنوان العقدة للتحرير"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "النقر الأوسط ينشئ عقدة إعادة توجيه جديدة"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "شفافية العقدة"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "عرض العقدة المهجورة في البحث",
|
||||
"tooltip": "العقد المهجورة مخفية افتراضيًا في واجهة المستخدم، لكنها تظل فعالة في سير العمل الحالي الذي يستخدمها."
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "عرض العقدة التجريبية في البحث",
|
||||
"tooltip": "يتم تمييز العقد التجريبية في واجهة المستخدم وقد تخضع لتغييرات كبيرة أو إزالتها في الإصدارات المستقبلية. استخدمها بحذر في سير العمل الإنتاجي."
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "تثبيت يبرز العقدة",
|
||||
"tooltip": "عند سحب رابط فوق عقدة تحتوي على فتحة إدخال صالحة، يتم تمييز العقدة"
|
||||
},
|
||||
"Comfy_Notification_ShowVersionUpdates": {
|
||||
"name": "عرض تحديثات الإصدار",
|
||||
"tooltip": "عرض التحديثات للنماذج الجديدة والميزات الرئيسية."
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "تأخير انحراف نقرة المؤشر",
|
||||
"tooltip": "بعد الضغط على زر المؤشر، هذا هو الوقت الأقصى (بالملي ثانية) الذي يمكن تجاهل حركة المؤشر خلاله.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
|
||||
},
|
||||
"Comfy_Pointer_ClickDrift": {
|
||||
"name": "انحراف نقرة المؤشر (أقصى مسافة)",
|
||||
"tooltip": "إذا تحرك المؤشر أكثر من هذه المسافة أثناء الضغط على زر، يعتبر سحبًا بدلاً من نقرة.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
|
||||
},
|
||||
"Comfy_Pointer_DoubleClickTime": {
|
||||
"name": "فترة النقر المزدوج (قصوى)",
|
||||
"tooltip": "الوقت الأقصى بالملي ثانية بين النقرتين في النقر المزدوج. زيادة هذه القيمة قد تساعد إذا لم يتم تسجيل النقرات المزدوجة أحيانًا."
|
||||
},
|
||||
"Comfy_PreviewFormat": {
|
||||
"name": "تنسيق صورة المعاينة",
|
||||
"tooltip": "عند عرض معاينة في ويدجت الصورة، يتم تحويلها إلى صورة خفيفة الوزن، مثل webp، jpeg، webp;50، إلخ."
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "طلب اسم الملف عند حفظ سير العمل"
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "حد عدد الدُفعات",
|
||||
"tooltip": "العدد الأقصى للمهام التي تضاف إلى القائمة بنقرة زر واحدة"
|
||||
},
|
||||
"Comfy_Queue_MaxHistoryItems": {
|
||||
"name": "حجم تاريخ قائمة الانتظار",
|
||||
"tooltip": "العدد الأقصى للمهام المعروضة في تاريخ قائمة الانتظار."
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "موقع الشريط الجانبي",
|
||||
"options": {
|
||||
"left": "يسار",
|
||||
"right": "يمين"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_Size": {
|
||||
"name": "حجم الشريط الجانبي",
|
||||
"options": {
|
||||
"normal": "عادي",
|
||||
"small": "صغير"
|
||||
}
|
||||
},
|
||||
"Comfy_Sidebar_UnifiedWidth": {
|
||||
"name": "عرض موحد للشريط الجانبي"
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "حجم الالتصاق بالشبكة",
|
||||
"tooltip": "عند سحب وتغيير حجم العقد مع الضغط على shift، يتم محاذاتها إلى الشبكة، هذا يتحكم في حجم تلك الشبكة."
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "حجم خط ويدجت منطقة النص"
|
||||
},
|
||||
"Comfy_TextareaWidget_Spellcheck": {
|
||||
"name": "التحقق من الإملاء في ويدجت منطقة النص"
|
||||
},
|
||||
"Comfy_TreeExplorer_ItemPadding": {
|
||||
"name": "حشو عناصر مستعرض الشجرة"
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "استخدام القائمة الجديدة",
|
||||
"options": {
|
||||
"Bottom": "أسفل",
|
||||
"Disabled": "معطل",
|
||||
"Top": "أعلى"
|
||||
},
|
||||
"tooltip": "موقع شريط القائمة. على الأجهزة المحمولة، تُعرض القائمة دائمًا في الأعلى."
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "التحقق من صحة سير العمل"
|
||||
},
|
||||
"Comfy_WidgetControlMode": {
|
||||
"name": "وضع التحكم في الودجت",
|
||||
"options": {
|
||||
"after": "بعد",
|
||||
"before": "قبل"
|
||||
},
|
||||
"tooltip": "يتحكم في متى يتم تحديث قيم الودجت (توليد عشوائي/زيادة/نقصان)، إما قبل إدراج الطلب في الطابور أو بعده."
|
||||
},
|
||||
"Comfy_Window_UnloadConfirmation": {
|
||||
"name": "عرض تأكيد عند إغلاق النافذة"
|
||||
},
|
||||
"Comfy_Workflow_AutoSave": {
|
||||
"name": "الحفظ التلقائي",
|
||||
"options": {
|
||||
"after delay": "بعد تأخير",
|
||||
"off": "إيقاف"
|
||||
}
|
||||
},
|
||||
"Comfy_Workflow_AutoSaveDelay": {
|
||||
"name": "تأخير الحفظ التلقائي (بالملي ثانية)",
|
||||
"tooltip": "ينطبق فقط إذا تم تعيين الحفظ التلقائي إلى \"بعد تأخير\"."
|
||||
},
|
||||
"Comfy_Workflow_ConfirmDelete": {
|
||||
"name": "عرض تأكيد عند حذف سير العمل"
|
||||
},
|
||||
"Comfy_Workflow_Persist": {
|
||||
"name": "الاحتفاظ بحالة سير العمل واستعادتها عند (إعادة) تحميل الصفحة"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingModelsWarning": {
|
||||
"name": "عرض تحذير النماذج المفقودة"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "عرض تحذير العقد المفقودة"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "ترتيب معرفات العقد عند حفظ سير العمل"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "موضع تبويبات سير العمل المفتوحة",
|
||||
"options": {
|
||||
"Sidebar": "الشريط الجانبي",
|
||||
"Topbar": "شريط الأعلى",
|
||||
"Topbar (2nd-row)": "شريط الأعلى (الصف الثاني)"
|
||||
}
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "عتبة التكبير للرسم بجودة منخفضة",
|
||||
"tooltip": "عرض أشكال بجودة منخفضة عند التكبير للخارج"
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "الحد الأقصى للإطارات في الثانية",
|
||||
"tooltip": "الحد الأقصى لعدد الإطارات في الثانية التي يسمح للرسم أن يعرضها. يحد من استخدام GPU على حساب السلاسة. إذا كانت 0، يتم استخدام معدل تحديث الشاشة. الافتراضي: 0"
|
||||
},
|
||||
"LiteGraph_ContextMenu_Scaling": {
|
||||
"name": "تغيير مقياس قوائم ودجت كومبو العقدة عند التكبير"
|
||||
},
|
||||
"LiteGraph_Node_DefaultPadding": {
|
||||
"name": "تصغير العقد الجديدة دائمًا",
|
||||
"tooltip": "تغيير حجم العقد إلى أصغر حجم ممكن عند الإنشاء. عند التعطيل، يتم توسيع العقدة المضافة حديثًا قليلاً لإظهار قيم الودجت."
|
||||
},
|
||||
"LiteGraph_Node_TooltipDelay": {
|
||||
"name": "تأخير التلميح"
|
||||
},
|
||||
"LiteGraph_Reroute_SplineOffset": {
|
||||
"name": "إزاحة منحنى إعادة التوجيه",
|
||||
"tooltip": "إزاحة نقطة تحكم بيزير من نقطة مركز إعادة التوجيه"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "الالتصاق بالشبكة دائمًا"
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Restart"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Open 3D Viewer (Beta) for Selected Node"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Browse Templates"
|
||||
},
|
||||
@@ -119,6 +122,12 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convert Selection to Subgraph"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Unpack the selected Subgraph"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
|
||||
@@ -145,7 +145,8 @@
|
||||
"stopRecording": "Stop Recording",
|
||||
"micPermissionDenied": "Microphone permission denied",
|
||||
"noAudioRecorded": "No audio recorded",
|
||||
"nodesRunning": "nodes running"
|
||||
"nodesRunning": "nodes running",
|
||||
"duplicate": "Duplicate"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -946,6 +947,7 @@
|
||||
"Quit": "Quit",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Open 3D Viewer (Beta) for Selected Node",
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Add Edit Model Step": "Add Edit Model Step",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
@@ -974,6 +976,7 @@
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Convert Selection to Subgraph": "Convert Selection to Subgraph",
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
@@ -1079,7 +1082,8 @@
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences"
|
||||
"Notification Preferences": "Notification Preferences",
|
||||
"3DViewer": "3DViewer"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1431,12 +1435,31 @@
|
||||
"depth": "Depth",
|
||||
"lineart": "Lineart"
|
||||
},
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"startRecording": "Start Recording",
|
||||
"stopRecording": "Stop Recording",
|
||||
"exportRecording": "Export Recording",
|
||||
"clearRecording": "Clear Recording",
|
||||
"resizeNodeMatchOutput": "Resize Node to match output",
|
||||
"loadingBackgroundImage": "Loading Background Image"
|
||||
"loadingBackgroundImage": "Loading Background Image",
|
||||
"cameraType": {
|
||||
"perspective": "Perspective",
|
||||
"orthographic": "Orthographic"
|
||||
},
|
||||
"viewer": {
|
||||
"title": "3D Viewer (Beta)",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel",
|
||||
"cameraType": "Camera Type",
|
||||
"sceneSettings": "Scene Settings",
|
||||
"cameraSettings": "Camera Settings",
|
||||
"lightSettings": "Light Settings",
|
||||
"exportSettings": "Export Settings",
|
||||
"modelSettings": "Model Settings"
|
||||
},
|
||||
"openIn3DViewer": "Open in 3D Viewer"
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
@@ -1474,7 +1497,8 @@
|
||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||
"nothingSelected": "Nothing selected",
|
||||
"cannotCreateSubgraph": "Cannot create subgraph",
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph"
|
||||
"failedToConvertToSubgraph": "Failed to convert items to subgraph",
|
||||
"failedToInitializeLoad3dViewer": "Failed to initialize 3D Viewer"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
@@ -1647,5 +1671,12 @@
|
||||
"view": "View",
|
||||
"panelControls": "Panel Controls"
|
||||
}
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Node Colors",
|
||||
"showLinks": "Show Links",
|
||||
"showGroups": "Show Frames/Groups",
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user