mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-24 06:17:32 +00:00
Compare commits
17 Commits
command-bo
...
v1.26.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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`
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 |
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
544
package-lock.json
generated
544
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",
|
||||
@@ -80,6 +80,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 +1002,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 +3104,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 +3168,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 +4803,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",
|
||||
@@ -8469,16 +8797,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 +8880,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 +9185,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 +10872,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",
|
||||
@@ -12388,6 +12846,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 +13215,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 +15313,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 +17922,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",
|
||||
|
||||
@@ -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",
|
||||
@@ -23,6 +23,7 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "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"
|
||||
@@ -56,6 +57,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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
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>
|
||||
@@ -23,6 +23,7 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
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'
|
||||
@@ -805,6 +806,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { toString } from 'lodash'
|
||||
|
||||
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'
|
||||
@@ -2027,7 +2028,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 +2332,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 'lodash'
|
||||
|
||||
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>` +
|
||||
@@ -6065,7 +6066,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 +6234,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 +8253,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' }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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 'lodash'
|
||||
|
||||
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,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": "سير العمل"
|
||||
}
|
||||
}
|
||||
1681
src/locales/ar/main.json
Normal file
1681
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,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convert Selection to Subgraph"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
|
||||
@@ -946,6 +946,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 +975,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 +1081,8 @@
|
||||
"User": "User",
|
||||
"Credits": "Credits",
|
||||
"API Nodes": "API Nodes",
|
||||
"Notification Preferences": "Notification Preferences"
|
||||
"Notification Preferences": "Notification Preferences",
|
||||
"3DViewer": "3DViewer"
|
||||
},
|
||||
"serverConfigItems": {
|
||||
"listen": {
|
||||
@@ -1431,12 +1434,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 +1496,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 +1670,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"
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,10 @@
|
||||
"Hidden": "Hidden"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"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."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Initial Background Color",
|
||||
"tooltip": "Controls the default background color of the 3D scene. This setting determines the background appearance when a new 3D widget is created, but can be adjusted individually for each widget after creation."
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Reiniciar"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Abrir visor 3D (Beta) para el nodo seleccionado"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Explorar plantillas"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir selección en subgrafo"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Salir de subgrafo"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajustar grupo al contenido"
|
||||
},
|
||||
@@ -243,7 +249,7 @@
|
||||
"label": "Alternar panel inferior de controles de vista"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "Mostrar diálogo de combinaciones de teclas"
|
||||
"label": "Mostrar diálogo de atajos de teclado"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Alternar Modo de Enfoque"
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "Aplicando textura...",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"camera": "Cámara",
|
||||
"cameraType": {
|
||||
"orthographic": "Ortográfica",
|
||||
"perspective": "Perspectiva"
|
||||
},
|
||||
"clearRecording": "Borrar grabación",
|
||||
"edgeThreshold": "Umbral de borde",
|
||||
"export": "Exportar",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "Malla"
|
||||
},
|
||||
"model": "Modelo",
|
||||
"openIn3DViewer": "Abrir en el visor 3D",
|
||||
"previewOutput": "Vista previa de salida",
|
||||
"removeBackgroundImage": "Eliminar imagen de fondo",
|
||||
"resizeNodeMatchOutput": "Redimensionar nodo para coincidir con la salida",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "Cambiar cámara",
|
||||
"switchingMaterialMode": "Cambiando modo de material...",
|
||||
"upDirection": "Dirección hacia arriba",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Subir imagen de fondo",
|
||||
"uploadTexture": "Subir textura"
|
||||
"uploadTexture": "Subir textura",
|
||||
"viewer": {
|
||||
"apply": "Aplicar",
|
||||
"cameraSettings": "Configuración de la cámara",
|
||||
"cameraType": "Tipo de cámara",
|
||||
"cancel": "Cancelar",
|
||||
"exportSettings": "Configuración de exportación",
|
||||
"lightSettings": "Configuración de la luz",
|
||||
"modelSettings": "Configuración del modelo",
|
||||
"sceneSettings": "Configuración de la escena",
|
||||
"title": "Visor 3D (Beta)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Requiere ComfyUI {version}:",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "Guía de usuario de escritorio",
|
||||
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
|
||||
"Edit": "Editar",
|
||||
"Exit Subgraph": "Salir de subgrafo",
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
"Open": "Abrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
|
||||
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
|
||||
"Open DevTools": "Abrir DevTools",
|
||||
"Open Inputs Folder": "Abrir carpeta de entradas",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
"renderBypassState": "Mostrar estado de omisión",
|
||||
"renderErrorState": "Mostrar estado de error",
|
||||
"showGroups": "Mostrar marcos/grupos",
|
||||
"showLinks": "Mostrar enlaces"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
"missingModels": "Modelos faltantes",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visor 3D",
|
||||
"API Nodes": "Nodos API",
|
||||
"About": "Acerca de",
|
||||
"Appearance": "Apariencia",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "Error al exportar modelo como {format}",
|
||||
"failedToFetchBalance": "No se pudo obtener el saldo: {error}",
|
||||
"failedToFetchLogs": "Error al obtener los registros del servidor",
|
||||
"failedToInitializeLoad3dViewer": "No se pudo inicializar el visor 3D",
|
||||
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
|
||||
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "Recto"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "Habilitar visor 3D (Beta)",
|
||||
"tooltip": "Activa el visor 3D (Beta) para los nodos seleccionados. Esta función te permite visualizar e interactuar con modelos 3D directamente dentro del visor 3D a tamaño completo."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Color de fondo inicial",
|
||||
"tooltip": "Controla el color de fondo predeterminado de la escena 3D. Esta configuración determina la apariencia del fondo cuando se crea un nuevo widget 3D, pero puede ajustarse individualmente para cada widget después de su creación."
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Redémarrer"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Parcourir les modèles"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Convertir la sélection en sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Quitter le sous-graphe"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajuster le groupe au contenu"
|
||||
},
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "Application de la texture...",
|
||||
"backgroundColor": "Couleur de fond",
|
||||
"camera": "Caméra",
|
||||
"cameraType": {
|
||||
"orthographic": "Orthographique",
|
||||
"perspective": "Perspective"
|
||||
},
|
||||
"clearRecording": "Effacer l'enregistrement",
|
||||
"edgeThreshold": "Seuil de Bordure",
|
||||
"export": "Exportation",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "Fil de fer"
|
||||
},
|
||||
"model": "Modèle",
|
||||
"openIn3DViewer": "Ouvrir dans la visionneuse 3D",
|
||||
"previewOutput": "Aperçu de la sortie",
|
||||
"removeBackgroundImage": "Supprimer l'image de fond",
|
||||
"resizeNodeMatchOutput": "Redimensionner le nœud pour correspondre à la sortie",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "Changer de caméra",
|
||||
"switchingMaterialMode": "Changement de mode de matériau...",
|
||||
"upDirection": "Direction Haut",
|
||||
"upDirections": {
|
||||
"original": "Original"
|
||||
},
|
||||
"uploadBackgroundImage": "Télécharger l'image de fond",
|
||||
"uploadTexture": "Télécharger Texture"
|
||||
"uploadTexture": "Télécharger Texture",
|
||||
"viewer": {
|
||||
"apply": "Appliquer",
|
||||
"cameraSettings": "Paramètres de la caméra",
|
||||
"cameraType": "Type de caméra",
|
||||
"cancel": "Annuler",
|
||||
"exportSettings": "Paramètres d’exportation",
|
||||
"lightSettings": "Paramètres de l’éclairage",
|
||||
"modelSettings": "Paramètres du modèle",
|
||||
"sceneSettings": "Paramètres de la scène",
|
||||
"title": "Visionneuse 3D (Bêta)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Nécessite ComfyUI {version} :",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
"Edit": "Éditer",
|
||||
"Exit Subgraph": "Quitter le sous-graphe",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
"Open": "Ouvrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
|
||||
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
|
||||
"Open DevTools": "Ouvrir DevTools",
|
||||
"Open Inputs Folder": "Ouvrir le dossier des entrées",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
"renderBypassState": "Afficher l’état de contournement",
|
||||
"renderErrorState": "Afficher l’état d’erreur",
|
||||
"showGroups": "Afficher les cadres/groupes",
|
||||
"showLinks": "Afficher les liens"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
"missingModels": "Modèles manquants",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "Visionneuse 3D",
|
||||
"API Nodes": "Nœuds API",
|
||||
"About": "À Propos",
|
||||
"Appearance": "Apparence",
|
||||
@@ -1157,7 +1186,7 @@
|
||||
"node": "Nœud",
|
||||
"panelControls": "Contrôles du panneau",
|
||||
"queue": "File d’attente",
|
||||
"view": "Affichage",
|
||||
"view": "Vue",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
"viewControls": "Contrôles d’affichage"
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
|
||||
"failedToFetchBalance": "Échec de la récupération du solde : {error}",
|
||||
"failedToFetchLogs": "Échec de la récupération des journaux du serveur",
|
||||
"failedToInitializeLoad3dViewer": "Échec de l'initialisation du visualiseur 3D",
|
||||
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
|
||||
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "Droit"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "Activer le visualiseur 3D (Bêta)",
|
||||
"tooltip": "Active le visualiseur 3D (Bêta) pour les nœuds sélectionnés. Cette fonctionnalité vous permet de visualiser et d’interagir avec des modèles 3D directement dans le visualiseur 3D en taille réelle."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Couleur de fond initiale",
|
||||
"tooltip": "Contrôle la couleur de fond par défaut de la scène 3D. Ce paramètre détermine l'apparence du fond lors de la création d'un nouveau widget 3D, mais peut être ajusté individuellement pour chaque widget après la création."
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "再起動"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "選択したノードの3Dビューアー(ベータ)を開く"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "テンプレートを参照"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "選択範囲をサブグラフに変換"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "サブグラフを終了"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "グループを内容に合わせて調整"
|
||||
},
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "テクスチャを適用中...",
|
||||
"backgroundColor": "背景色",
|
||||
"camera": "カメラ",
|
||||
"cameraType": {
|
||||
"orthographic": "オルソグラフィック",
|
||||
"perspective": "パースペクティブ"
|
||||
},
|
||||
"clearRecording": "録画をクリア",
|
||||
"edgeThreshold": "エッジ閾値",
|
||||
"export": "エクスポート",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "ワイヤーフレーム"
|
||||
},
|
||||
"model": "モデル",
|
||||
"openIn3DViewer": "3Dビューアで開く",
|
||||
"previewOutput": "出力のプレビュー",
|
||||
"removeBackgroundImage": "背景画像を削除",
|
||||
"resizeNodeMatchOutput": "ノードを出力に合わせてリサイズ",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "カメラを切り替える",
|
||||
"switchingMaterialMode": "マテリアルモードの切り替え中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "オリジナル"
|
||||
},
|
||||
"uploadBackgroundImage": "背景画像をアップロード",
|
||||
"uploadTexture": "テクスチャをアップロード"
|
||||
"uploadTexture": "テクスチャをアップロード",
|
||||
"viewer": {
|
||||
"apply": "適用",
|
||||
"cameraSettings": "カメラ設定",
|
||||
"cameraType": "カメラタイプ",
|
||||
"cancel": "キャンセル",
|
||||
"exportSettings": "エクスポート設定",
|
||||
"lightSettings": "ライト設定",
|
||||
"modelSettings": "モデル設定",
|
||||
"sceneSettings": "シーン設定",
|
||||
"title": "3Dビューア(ベータ)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} が必要です:",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
"Edit": "編集",
|
||||
"Exit Subgraph": "サブグラフを終了",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
"Open": "開く",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "選択したノードの3Dビューアー(ベータ)を開く",
|
||||
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
|
||||
"Open DevTools": "DevToolsを開く",
|
||||
"Open Inputs Folder": "入力フォルダを開く",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
"renderBypassState": "バイパス状態を表示",
|
||||
"renderErrorState": "エラー状態を表示",
|
||||
"showGroups": "フレーム/グループを表示",
|
||||
"showLinks": "リンクを表示"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
"missingModels": "モデルが見つかりません",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3Dビューア",
|
||||
"API Nodes": "APIノード",
|
||||
"About": "情報",
|
||||
"Appearance": "外観",
|
||||
@@ -1151,7 +1180,7 @@
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"keyboardShortcuts": "キーボードショートカット",
|
||||
"manageShortcuts": "ショートカット管理",
|
||||
"manageShortcuts": "ショートカットの管理",
|
||||
"noKeybinding": "キー割り当てなし",
|
||||
"subcategories": {
|
||||
"node": "ノード",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
|
||||
"failedToFetchBalance": "残高の取得に失敗しました: {error}",
|
||||
"failedToFetchLogs": "サーバーログの取得に失敗しました",
|
||||
"failedToInitializeLoad3dViewer": "3Dビューアの初期化に失敗しました",
|
||||
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
|
||||
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "ストレート"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "3Dビューアーを有効化(ベータ)",
|
||||
"tooltip": "選択したノードで3Dビューアー(ベータ)を有効にします。この機能により、フルサイズの3Dビューアー内で3Dモデルを直接可視化し、操作できます。"
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初期背景色",
|
||||
"tooltip": "3Dシーンのデフォルト背景色を設定します。この設定は新しい3Dウィジェット作成時の背景の見た目を決定しますが、作成後に各ウィジェットごとに個別に調整できます。"
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "재시작"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "선택한 노드에 대해 3D 뷰어(베타) 열기"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "템플릿 탐색"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "선택 영역을 서브그래프로 변환"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "서브그래프 종료"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "그룹을 내용에 맞게 맞추기"
|
||||
},
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "텍스처 적용 중...",
|
||||
"backgroundColor": "배경색",
|
||||
"camera": "카메라",
|
||||
"cameraType": {
|
||||
"orthographic": "직교",
|
||||
"perspective": "원근"
|
||||
},
|
||||
"clearRecording": "녹화 지우기",
|
||||
"edgeThreshold": "엣지 임계값",
|
||||
"export": "내보내기",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "와이어프레임"
|
||||
},
|
||||
"model": "모델",
|
||||
"openIn3DViewer": "3D 뷰어에서 열기",
|
||||
"previewOutput": "출력 미리보기",
|
||||
"removeBackgroundImage": "배경 이미지 제거",
|
||||
"resizeNodeMatchOutput": "노드 크기를 출력에 맞추기",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "카메라 전환",
|
||||
"switchingMaterialMode": "재질 모드 전환 중...",
|
||||
"upDirection": "위 방향",
|
||||
"upDirections": {
|
||||
"original": "원본"
|
||||
},
|
||||
"uploadBackgroundImage": "배경 이미지 업로드",
|
||||
"uploadTexture": "텍스처 업로드"
|
||||
"uploadTexture": "텍스처 업로드",
|
||||
"viewer": {
|
||||
"apply": "적용",
|
||||
"cameraSettings": "카메라 설정",
|
||||
"cameraType": "카메라 유형",
|
||||
"cancel": "취소",
|
||||
"exportSettings": "내보내기 설정",
|
||||
"lightSettings": "조명 설정",
|
||||
"modelSettings": "모델 설정",
|
||||
"sceneSettings": "씬 설정",
|
||||
"title": "3D 뷰어 (베타)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "ComfyUI {version} 이상 필요:",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
"Exit Subgraph": "서브그래프 종료",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
"Open": "열기",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
|
||||
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
|
||||
"Open DevTools": "개발자 도구 열기",
|
||||
"Open Inputs Folder": "입력 폴더 열기",
|
||||
@@ -808,7 +829,7 @@
|
||||
"Restart": "재시작",
|
||||
"Save": "저장",
|
||||
"Save As": "다른 이름으로 저장",
|
||||
"Show Keybindings Dialog": "단축키 대화상자 표시",
|
||||
"Show Keybindings Dialog": "키 바인딩 대화상자 표시",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
"renderBypassState": "바이패스 상태 렌더링",
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
"missingModels": "모델이 없습니다",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D뷰어",
|
||||
"API Nodes": "API 노드",
|
||||
"About": "정보",
|
||||
"Appearance": "모양",
|
||||
@@ -1149,7 +1178,7 @@
|
||||
"Workflow": "워크플로"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "기본",
|
||||
"essentials": "필수",
|
||||
"keyboardShortcuts": "키보드 단축키",
|
||||
"manageShortcuts": "단축키 관리",
|
||||
"noKeybinding": "단축키 없음",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
|
||||
"failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}",
|
||||
"failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다",
|
||||
"failedToInitializeLoad3dViewer": "3D 뷰어 초기화에 실패했습니다",
|
||||
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
|
||||
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
|
||||
"fileLoadError": "{fileName}에서 워크플로를 찾을 수 없습니다",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "직선"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "3D 뷰어 활성화 (베타)",
|
||||
"tooltip": "선택한 노드에 대해 3D 뷰어(베타)를 활성화합니다. 이 기능을 통해 전체 크기의 3D 뷰어에서 3D 모델을 직접 시각화하고 상호작용할 수 있습니다."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "초기 배경색",
|
||||
"tooltip": "3D 장면의 기본 배경색을 설정합니다. 이 설정은 새 3D 위젯이 생성될 때 배경의 모양을 결정하지만, 생성 후 각 위젯별로 개별적으로 조정할 수 있습니다."
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "Перезагрузить"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "Открыть 3D-просмотрщик (бета) для выбранного узла"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "Просмотр шаблонов"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "Преобразовать выделенное в подграф"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Выйти из подграфа"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Подогнать группу к содержимому"
|
||||
},
|
||||
@@ -240,7 +246,7 @@
|
||||
"label": "Показать/скрыть основную нижнюю панель"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "Показать/скрыть нижнюю панель управления просмотром"
|
||||
"label": "Показать или скрыть нижнюю панель управления просмотром"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "Показать диалог клавиш"
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "Применение текстуры...",
|
||||
"backgroundColor": "Цвет фона",
|
||||
"camera": "Камера",
|
||||
"cameraType": {
|
||||
"orthographic": "Ортографическая",
|
||||
"perspective": "Перспективная"
|
||||
},
|
||||
"clearRecording": "Очистить запись",
|
||||
"edgeThreshold": "Пороговое значение края",
|
||||
"export": "Экспорт",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "Каркас"
|
||||
},
|
||||
"model": "Модель",
|
||||
"openIn3DViewer": "Открыть в 3D просмотрщике",
|
||||
"previewOutput": "Предварительный просмотр",
|
||||
"removeBackgroundImage": "Удалить фоновое изображение",
|
||||
"resizeNodeMatchOutput": "Изменить размер узла под вывод",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "Переключить камеру",
|
||||
"switchingMaterialMode": "Переключение режима материала...",
|
||||
"upDirection": "Направление Вверх",
|
||||
"upDirections": {
|
||||
"original": "Оригинал"
|
||||
},
|
||||
"uploadBackgroundImage": "Загрузить фоновое изображение",
|
||||
"uploadTexture": "Загрузить текстуру"
|
||||
"uploadTexture": "Загрузить текстуру",
|
||||
"viewer": {
|
||||
"apply": "Применить",
|
||||
"cameraSettings": "Настройки камеры",
|
||||
"cameraType": "Тип камеры",
|
||||
"cancel": "Отмена",
|
||||
"exportSettings": "Настройки экспорта",
|
||||
"lightSettings": "Настройки освещения",
|
||||
"modelSettings": "Настройки модели",
|
||||
"sceneSettings": "Настройки сцены",
|
||||
"title": "3D Просмотрщик (Бета)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "Требуется ComfyUI {version}:",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
"Exit Subgraph": "Выйти из подграфа",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
"Open": "Открыть",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
|
||||
"Open DevTools": "Открыть инструменты разработчика",
|
||||
"Open Inputs Folder": "Открыть папку входных данных",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
"renderBypassState": "Отображать состояние обхода",
|
||||
"renderErrorState": "Отображать состояние ошибки",
|
||||
"showGroups": "Показать фреймы/группы",
|
||||
"showLinks": "Показать связи"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
"missingModels": "Отсутствующие модели",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D-просмотрщик",
|
||||
"API Nodes": "API-узлы",
|
||||
"About": "О программе",
|
||||
"Appearance": "Внешний вид",
|
||||
@@ -1152,7 +1181,7 @@
|
||||
"essentials": "Основные",
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"manageShortcuts": "Управление горячими клавишами",
|
||||
"noKeybinding": "Нет привязки клавиши",
|
||||
"noKeybinding": "Нет сочетания клавиш",
|
||||
"subcategories": {
|
||||
"node": "Узел",
|
||||
"panelControls": "Управление панелью",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "Не удалось экспортировать модель как {format}",
|
||||
"failedToFetchBalance": "Не удалось получить баланс: {error}",
|
||||
"failedToFetchLogs": "Не удалось получить серверные логи",
|
||||
"failedToInitializeLoad3dViewer": "Не удалось инициализировать 3D просмотрщик",
|
||||
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
|
||||
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "Прямой"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "Включить 3D-просмотрщик (Бета)",
|
||||
"tooltip": "Включает 3D-просмотрщик (Бета) для выбранных узлов. Эта функция позволяет визуализировать и взаимодействовать с 3D-моделями прямо в полноразмерном 3D-просмотрщике."
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "Начальный цвет фона",
|
||||
"tooltip": "Управляет цветом фона по умолчанию для 3D-сцены. Этот параметр определяет внешний вид фона при создании нового 3D-виджета, но может быть изменён индивидуально для каждого виджета после создания."
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "重新啟動"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "為選取的節點開啟 3D 檢視器(Beta)"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "瀏覽範本"
|
||||
},
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "將選取內容轉換為子圖"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "離開子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "調整群組以符合內容"
|
||||
},
|
||||
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "正在套用材質貼圖...",
|
||||
"backgroundColor": "背景顏色",
|
||||
"camera": "相機",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透視"
|
||||
},
|
||||
"clearRecording": "清除錄影",
|
||||
"edgeThreshold": "邊緣閾值",
|
||||
"export": "匯出",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "線框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 檢視器中開啟",
|
||||
"previewOutput": "預覽輸出",
|
||||
"removeBackgroundImage": "移除背景圖片",
|
||||
"resizeNodeMatchOutput": "調整節點以符合輸出",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "切換相機",
|
||||
"switchingMaterialMode": "正在切換材質模式...",
|
||||
"upDirection": "上方方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上傳背景圖片",
|
||||
"uploadTexture": "上傳材質貼圖"
|
||||
"uploadTexture": "上傳材質貼圖",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"cameraSettings": "相機設定",
|
||||
"cameraType": "相機類型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "匯出設定",
|
||||
"lightSettings": "燈光設定",
|
||||
"modelSettings": "模型設定",
|
||||
"sceneSettings": "場景設定",
|
||||
"title": "3D 檢視器(測試版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "桌面應用程式使用指南",
|
||||
"Duplicate Current Workflow": "複製目前工作流程",
|
||||
"Edit": "編輯",
|
||||
"Exit Subgraph": "離開子圖",
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "新增",
|
||||
"Next Opened Workflow": "下一個已開啟的工作流程",
|
||||
"Open": "開啟",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
|
||||
"Open DevTools": "開啟開發者工具",
|
||||
"Open Inputs Folder": "開啟輸入資料夾",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不要再顯示此訊息",
|
||||
"missingModels": "缺少模型",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 檢視器",
|
||||
"API Nodes": "API 節點",
|
||||
"About": "關於",
|
||||
"Appearance": "外觀",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "無法將模型匯出為 {format}",
|
||||
"failedToFetchBalance": "取得餘額失敗:{error}",
|
||||
"failedToFetchLogs": "無法取得伺服器日誌",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
|
||||
"failedToInitiateCreditPurchase": "啟動點數購買失敗:{error}",
|
||||
"failedToPurchaseCredits": "購買點數失敗:{error}",
|
||||
"fileLoadError": "無法在 {fileName} 中找到工作流程",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "直線"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "啟用 3D 檢視器(測試版)",
|
||||
"tooltip": "為所選節點啟用 3D 檢視器(測試版)。此功能可讓您在全尺寸 3D 檢視器中直接瀏覽與互動 3D 模型。"
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景顏色",
|
||||
"tooltip": "控制 3D 場景的預設背景顏色。此設定決定新建立 3D 元件時的背景外觀,但每個元件在建立後都可單獨調整。"
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"Comfy-Desktop_Restart": {
|
||||
"label": "重启"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "為所選節點開啟 3D 檢視器(Beta 版)"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "浏览模板"
|
||||
},
|
||||
@@ -72,7 +75,7 @@
|
||||
"label": "锁定视图"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "画布切换小地图"
|
||||
"label": "畫布切換小地圖"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "忽略/取消忽略选中节点"
|
||||
@@ -119,6 +122,9 @@
|
||||
"Comfy_Graph_ConvertToSubgraph": {
|
||||
"label": "将选区转换为子图"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "退出子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "适应节点框到内容"
|
||||
},
|
||||
@@ -237,13 +243,13 @@
|
||||
"label": "切换日志底部面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "切换基本下方面板"
|
||||
"label": "切換基本下方面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "切换检视控制底部面板"
|
||||
"label": "切換檢視控制底部面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "显示快捷键对话框"
|
||||
"label": "顯示快捷鍵對話框"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切换焦点模式"
|
||||
|
||||
@@ -410,7 +410,7 @@
|
||||
"resetView": "重置视图",
|
||||
"selectMode": "选择模式",
|
||||
"toggleLinkVisibility": "切换连线可见性",
|
||||
"toggleMinimap": "切换小地图",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
@@ -569,6 +569,10 @@
|
||||
"applyingTexture": "应用纹理中...",
|
||||
"backgroundColor": "背景颜色",
|
||||
"camera": "摄影机",
|
||||
"cameraType": {
|
||||
"orthographic": "正交",
|
||||
"perspective": "透视"
|
||||
},
|
||||
"clearRecording": "清除录制",
|
||||
"edgeThreshold": "边缘阈值",
|
||||
"export": "导出",
|
||||
@@ -589,6 +593,7 @@
|
||||
"wireframe": "线框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 檢視器中開啟",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"resizeNodeMatchOutput": "调整节点以匹配输出",
|
||||
@@ -599,8 +604,22 @@
|
||||
"switchCamera": "切换摄影机类型",
|
||||
"switchingMaterialMode": "切换材质模式中...",
|
||||
"upDirection": "上方向",
|
||||
"upDirections": {
|
||||
"original": "原始"
|
||||
},
|
||||
"uploadBackgroundImage": "上传背景图片",
|
||||
"uploadTexture": "上传纹理"
|
||||
"uploadTexture": "上传纹理",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"cameraSettings": "相機設定",
|
||||
"cameraType": "相機類型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "匯出設定",
|
||||
"lightSettings": "燈光設定",
|
||||
"modelSettings": "模型設定",
|
||||
"sceneSettings": "場景設定",
|
||||
"title": "3D 檢視器(測試版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
"coreNodesFromVersion": "需要 ComfyUI {version}:",
|
||||
@@ -746,7 +765,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||
"Canvas Toggle Lock": "切换视图锁定",
|
||||
"Canvas Toggle Minimap": "画布切换小地图",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Updates": "检查更新",
|
||||
"Clear Pending Tasks": "清除待处理任务",
|
||||
"Clear Workflow": "清除工作流",
|
||||
@@ -765,6 +784,7 @@
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
"Exit Subgraph": "退出子圖",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
@@ -784,6 +804,7 @@
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
"Open": "打开",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為所選節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "打开自定义节点文件夹",
|
||||
"Open DevTools": "打开开发者工具",
|
||||
"Open Inputs Folder": "打开输入文件夹",
|
||||
@@ -808,21 +829,21 @@
|
||||
"Restart": "重启",
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Keybindings Dialog": "显示快捷键对话框",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Essential Bottom Panel": "切换基本下方面板",
|
||||
"Toggle Essential Bottom Panel": "切換基本下方面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle View Controls Bottom Panel": "切换检视控制下方面板",
|
||||
"Toggle Workflows Sidebar": "切换工作流程侧边栏",
|
||||
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
@@ -831,6 +852,13 @@
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
"missingModels": "缺少模型",
|
||||
@@ -1097,6 +1125,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 檢視器",
|
||||
"API Nodes": "API 节点",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
@@ -1149,7 +1178,7 @@
|
||||
"Workflow": "工作流"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本",
|
||||
"essentials": "基本功能",
|
||||
"keyboardShortcuts": "鍵盤快捷鍵",
|
||||
"manageShortcuts": "管理快捷鍵",
|
||||
"noKeybinding": "無快捷鍵",
|
||||
@@ -1573,6 +1602,7 @@
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
|
||||
@@ -119,6 +119,10 @@
|
||||
"Straight": "直角线"
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "啟用 3D 檢視器(測試版)",
|
||||
"tooltip": "為所選節點啟用 3D 檢視器(測試版)。此功能可讓您直接在全尺寸 3D 檢視器中瀏覽並互動 3D 模型。"
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景颜色",
|
||||
"tooltip": "控制3D场景的默认背景颜色。此设置决定新建3D组件时的背景外观,但每个组件在创建后都可以单独调整。"
|
||||
|
||||
@@ -471,6 +471,11 @@ const zSettings = z.object({
|
||||
'Comfy.InstalledVersion': z.string().nullable(),
|
||||
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
|
||||
'Comfy.Minimap.Visible': z.boolean(),
|
||||
'Comfy.Minimap.NodeColors': z.boolean(),
|
||||
'Comfy.Minimap.ShowLinks': z.boolean(),
|
||||
'Comfy.Minimap.ShowGroups': z.boolean(),
|
||||
'Comfy.Minimap.RenderBypassState': z.boolean(),
|
||||
'Comfy.Minimap.RenderErrorState': z.boolean(),
|
||||
'Comfy.Canvas.NavigationMode': z.string(),
|
||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||
'Comfy-Desktop.SendStatistics': z.boolean(),
|
||||
@@ -489,6 +494,7 @@ const zSettings = z.object({
|
||||
'Comfy.Load3D.LightIntensityMinimum': z.number(),
|
||||
'Comfy.Load3D.LightAdjustmentIncrement': z.number(),
|
||||
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
|
||||
'Comfy.Load3D.3DViewerEnable': z.boolean(),
|
||||
'pysssss.SnapToGrid': z.boolean(),
|
||||
/** VHS setting is used for queue video preview support. */
|
||||
'VHS.AdvancedPreviews': z.string(),
|
||||
|
||||
@@ -385,8 +385,15 @@ export class ComfyApp {
|
||||
static pasteFromClipspace(node: LGraphNode) {
|
||||
if (ComfyApp.clipspace) {
|
||||
// image paste
|
||||
const combinedImgSrc =
|
||||
ComfyApp.clipspace.imgs?.[ComfyApp.clipspace.combinedIndex].src
|
||||
let combinedImgSrc: string | undefined
|
||||
if (
|
||||
ComfyApp.clipspace.combinedIndex !== undefined &&
|
||||
ComfyApp.clipspace.imgs &&
|
||||
ComfyApp.clipspace.combinedIndex < ComfyApp.clipspace.imgs.length
|
||||
) {
|
||||
combinedImgSrc =
|
||||
ComfyApp.clipspace.imgs[ComfyApp.clipspace.combinedIndex].src
|
||||
}
|
||||
if (ComfyApp.clipspace.imgs && node.imgs) {
|
||||
if (node.images && ComfyApp.clipspace.images) {
|
||||
if (ComfyApp.clipspace['img_paste_mode'] == 'selected') {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0.15em solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import './spinner.css'
|
||||
|
||||
export function createSpinner() {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`
|
||||
return div.firstElementChild
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
@@ -11,6 +12,7 @@ export const useKeybindingService = () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const keybindHandler = async function (event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
@@ -32,6 +34,19 @@ export const useKeybindingService = () => {
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
// Special handling for Escape key - let dialogs handle it first
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.metaKey
|
||||
) {
|
||||
// If dialogs are open, don't execute the keybinding - let the dialog handle it
|
||||
if (dialogStore.dialogStack.length > 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default browser behavior first, then execute the command
|
||||
event.preventDefault()
|
||||
await commandStore.execute(keybinding.commandId)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
type Load3dReadyCallback = (load3d: Load3d | Load3dAnimation) => void
|
||||
|
||||
const viewerInstances = new Map<NodeId, any>()
|
||||
|
||||
export class Load3dService {
|
||||
private static instance: Load3dService
|
||||
private nodeToLoad3dMap = new Map<LGraphNode, Load3d | Load3dAnimation>()
|
||||
@@ -126,6 +130,110 @@ export class Load3dService {
|
||||
}
|
||||
this.pendingCallbacks.clear()
|
||||
}
|
||||
|
||||
getOrCreateViewer(node: LGraphNode) {
|
||||
if (!viewerInstances.has(node.id)) {
|
||||
viewerInstances.set(node.id, useLoad3dViewer(node))
|
||||
}
|
||||
|
||||
return viewerInstances.get(node.id)
|
||||
}
|
||||
|
||||
removeViewer(node: LGraphNode) {
|
||||
const viewer = viewerInstances.get(node.id)
|
||||
|
||||
if (viewer) {
|
||||
viewer.cleanup()
|
||||
}
|
||||
|
||||
viewerInstances.delete(node.id)
|
||||
}
|
||||
|
||||
async copyLoad3dState(source: Load3d, target: Load3d | Load3dAnimation) {
|
||||
const sourceModel = source.modelManager.currentModel
|
||||
|
||||
if (sourceModel) {
|
||||
const modelClone = sourceModel.clone()
|
||||
|
||||
target.getModelManager().currentModel = modelClone
|
||||
target.getSceneManager().scene.add(modelClone)
|
||||
|
||||
target.getModelManager().materialMode =
|
||||
source.getModelManager().materialMode
|
||||
|
||||
target.getModelManager().currentUpDirection =
|
||||
source.getModelManager().currentUpDirection
|
||||
|
||||
target.setMaterialMode(source.getModelManager().materialMode)
|
||||
target.setUpDirection(source.getModelManager().currentUpDirection)
|
||||
|
||||
if (source.getModelManager().appliedTexture) {
|
||||
target.getModelManager().appliedTexture =
|
||||
source.getModelManager().appliedTexture
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCameraType = source.getCurrentCameraType()
|
||||
const sourceCameraState = source.getCameraState()
|
||||
|
||||
target.toggleCamera(sourceCameraType)
|
||||
target.setCameraState(sourceCameraState)
|
||||
|
||||
target.setBackgroundColor(source.getSceneManager().currentBackgroundColor)
|
||||
|
||||
target.toggleGrid(source.getSceneManager().gridHelper.visible)
|
||||
|
||||
const sourceBackgroundInfo = source
|
||||
.getSceneManager()
|
||||
.getCurrentBackgroundInfo()
|
||||
if (sourceBackgroundInfo.type === 'image') {
|
||||
const sourceNode = this.getNodeByLoad3d(source)
|
||||
const backgroundPath = sourceNode?.properties?.[
|
||||
'Background Image'
|
||||
] as string
|
||||
if (backgroundPath) {
|
||||
await target.setBackgroundImage(backgroundPath)
|
||||
}
|
||||
}
|
||||
|
||||
target.setLightIntensity(
|
||||
source.getLightingManager().lights[1]?.intensity || 1
|
||||
)
|
||||
|
||||
if (sourceCameraType === 'perspective') {
|
||||
target.setFOV(source.getCameraManager().perspectiveCamera.fov)
|
||||
}
|
||||
|
||||
const sourceNode = this.getNodeByLoad3d(source)
|
||||
if (sourceNode?.properties?.['Edge Threshold']) {
|
||||
target.setEdgeThreshold(sourceNode.properties['Edge Threshold'] as number)
|
||||
}
|
||||
}
|
||||
|
||||
handleViewportRefresh(load3d: Load3d | null) {
|
||||
if (!load3d) return
|
||||
|
||||
load3d.handleResize()
|
||||
|
||||
const currentType = load3d.getCurrentCameraType()
|
||||
|
||||
load3d.toggleCamera(
|
||||
currentType === 'perspective' ? 'orthographic' : 'perspective'
|
||||
)
|
||||
load3d.toggleCamera(currentType)
|
||||
|
||||
load3d.getControlsManager().controls.update()
|
||||
}
|
||||
|
||||
async handleViewerClose(node: LGraphNode) {
|
||||
const viewer = useLoad3dService().getOrCreateViewer(node)
|
||||
|
||||
if (viewer.needApplyChanges.value) {
|
||||
await viewer.applyChanges()
|
||||
}
|
||||
|
||||
useLoad3dService().removeViewer(node)
|
||||
}
|
||||
}
|
||||
|
||||
export const useLoad3dService = () => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
export interface ApiNodeCost {
|
||||
name: string
|
||||
cost: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an API node's cost and pricing details
|
||||
*/
|
||||
export interface ApiNodeCostData {
|
||||
/** The vendor/company providing the API service (e.g., 'OpenAI', 'Stability') */
|
||||
vendor: string
|
||||
/** The human-readable name of the node as displayed in the UI */
|
||||
nodeName: string
|
||||
/** Parameters that affect pricing (e.g., 'size | quality', 'duration', '-' if none) */
|
||||
pricingParams: string
|
||||
/** The price range per run (e.g., '$0.05', '$0.04 x n', 'dynamic') */
|
||||
pricePerRunRange: string
|
||||
/** Formatted price string for display in the UI */
|
||||
displayPrice: string
|
||||
/** URL to the vendor's pricing documentation page */
|
||||
rateDocumentationUrl?: string
|
||||
}
|
||||
|
||||
export type ApiNodeCostRecord = Record<string, ApiNodeCostData>
|
||||
@@ -239,3 +239,11 @@ function compressSubgraphWidgetInputSlots(
|
||||
compressSubgraphWidgetInputSlots(subgraph.definitions?.subgraphs, visited)
|
||||
}
|
||||
}
|
||||
|
||||
export function isLoad3dNode(node: LGraphNode) {
|
||||
return (
|
||||
node &&
|
||||
node.type &&
|
||||
(node.type === 'Load3D' || node.type === 'Load3DAnimation')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,6 +189,10 @@ const onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const onExecutionSuccess = async () => {
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
const reconnectingMessage: ToastMessageOptions = {
|
||||
severity: 'error',
|
||||
summary: t('g.reconnecting')
|
||||
@@ -214,6 +218,7 @@ const onReconnected = () => {
|
||||
|
||||
onMounted(() => {
|
||||
api.addEventListener('status', onStatus)
|
||||
api.addEventListener('execution_success', onExecutionSuccess)
|
||||
api.addEventListener('reconnecting', onReconnecting)
|
||||
api.addEventListener('reconnected', onReconnected)
|
||||
executionStore.bindExecutionEvents()
|
||||
@@ -227,6 +232,7 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
api.removeEventListener('status', onStatus)
|
||||
api.removeEventListener('execution_success', onExecutionSuccess)
|
||||
api.removeEventListener('reconnecting', onReconnecting)
|
||||
api.removeEventListener('reconnected', onReconnected)
|
||||
executionStore.unbindExecutionEvents()
|
||||
|
||||
606
tests-ui/tests/composables/useLoad3dViewer.test.ts
Normal file
606
tests-ui/tests/composables/useLoad3dViewer.test.ts
Normal file
@@ -0,0 +1,606 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useLoad3dViewer } from '@/composables/useLoad3dViewer'
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
vi.mock('@/services/load3dService', () => ({
|
||||
useLoad3dService: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/extensions/core/load3d/Load3dUtils', () => ({
|
||||
default: {
|
||||
uploadFile: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: vi.fn((key) => key)
|
||||
}))
|
||||
|
||||
vi.mock('@/extensions/core/load3d/Load3d', () => ({
|
||||
default: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useLoad3dViewer', () => {
|
||||
let mockLoad3d: any
|
||||
let mockSourceLoad3d: any
|
||||
let mockLoad3dService: any
|
||||
let mockToastStore: any
|
||||
let mockNode: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockNode = {
|
||||
properties: {
|
||||
'Background Color': '#282828',
|
||||
'Show Grid': true,
|
||||
'Camera Type': 'perspective',
|
||||
FOV: 75,
|
||||
'Light Intensity': 1,
|
||||
'Camera Info': null,
|
||||
'Background Image': '',
|
||||
'Up Direction': 'original',
|
||||
'Material Mode': 'original',
|
||||
'Edge Threshold': 85
|
||||
},
|
||||
graph: {
|
||||
setDirtyCanvas: vi.fn()
|
||||
}
|
||||
} as any
|
||||
|
||||
mockLoad3d = {
|
||||
setBackgroundColor: vi.fn(),
|
||||
toggleGrid: vi.fn(),
|
||||
toggleCamera: vi.fn(),
|
||||
setFOV: vi.fn(),
|
||||
setLightIntensity: vi.fn(),
|
||||
setBackgroundImage: vi.fn().mockResolvedValue(undefined),
|
||||
setUpDirection: vi.fn(),
|
||||
setMaterialMode: vi.fn(),
|
||||
setEdgeThreshold: vi.fn(),
|
||||
exportModel: vi.fn().mockResolvedValue(undefined),
|
||||
handleResize: vi.fn(),
|
||||
updateStatusMouseOnViewer: vi.fn(),
|
||||
getCameraState: vi.fn().mockReturnValue({
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
target: { x: 0, y: 0, z: 0 },
|
||||
zoom: 1,
|
||||
cameraType: 'perspective'
|
||||
}),
|
||||
forceRender: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
|
||||
mockSourceLoad3d = {
|
||||
getCurrentCameraType: vi.fn().mockReturnValue('perspective'),
|
||||
getCameraState: vi.fn().mockReturnValue({
|
||||
position: { x: 1, y: 1, z: 1 },
|
||||
target: { x: 0, y: 0, z: 0 },
|
||||
zoom: 1,
|
||||
cameraType: 'perspective'
|
||||
}),
|
||||
sceneManager: {
|
||||
currentBackgroundColor: '#282828',
|
||||
gridHelper: { visible: true },
|
||||
getCurrentBackgroundInfo: vi.fn().mockReturnValue({
|
||||
type: 'color',
|
||||
value: '#282828'
|
||||
})
|
||||
},
|
||||
lightingManager: {
|
||||
lights: [null, { intensity: 1 }]
|
||||
},
|
||||
cameraManager: {
|
||||
perspectiveCamera: { fov: 75 }
|
||||
},
|
||||
modelManager: {
|
||||
currentUpDirection: 'original',
|
||||
materialMode: 'original'
|
||||
},
|
||||
setBackgroundImage: vi.fn().mockResolvedValue(undefined),
|
||||
forceRender: vi.fn()
|
||||
}
|
||||
|
||||
vi.mocked(Load3d).mockImplementation(() => mockLoad3d)
|
||||
|
||||
mockLoad3dService = {
|
||||
copyLoad3dState: vi.fn().mockResolvedValue(undefined),
|
||||
handleViewportRefresh: vi.fn(),
|
||||
getLoad3d: vi.fn().mockReturnValue(mockSourceLoad3d)
|
||||
}
|
||||
vi.mocked(useLoad3dService).mockReturnValue(mockLoad3dService)
|
||||
|
||||
mockToastStore = {
|
||||
addAlert: vi.fn()
|
||||
}
|
||||
vi.mocked(useToastStore).mockReturnValue(mockToastStore)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
|
||||
expect(viewer.backgroundColor.value).toBe('')
|
||||
expect(viewer.showGrid.value).toBe(true)
|
||||
expect(viewer.cameraType.value).toBe('perspective')
|
||||
expect(viewer.fov.value).toBe(75)
|
||||
expect(viewer.lightIntensity.value).toBe(1)
|
||||
expect(viewer.backgroundImage.value).toBe('')
|
||||
expect(viewer.hasBackgroundImage.value).toBe(false)
|
||||
expect(viewer.upDirection.value).toBe('original')
|
||||
expect(viewer.materialMode.value).toBe('original')
|
||||
expect(viewer.edgeThreshold.value).toBe(85)
|
||||
})
|
||||
|
||||
it('should initialize viewer with source Load3d state', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
expect(Load3d).toHaveBeenCalledWith(containerRef, {
|
||||
disablePreview: true,
|
||||
isViewerMode: true,
|
||||
node: mockNode
|
||||
})
|
||||
|
||||
expect(mockLoad3dService.copyLoad3dState).toHaveBeenCalledWith(
|
||||
mockSourceLoad3d,
|
||||
mockLoad3d
|
||||
)
|
||||
|
||||
expect(viewer.cameraType.value).toBe('perspective')
|
||||
expect(viewer.backgroundColor.value).toBe('#282828')
|
||||
expect(viewer.showGrid.value).toBe(true)
|
||||
expect(viewer.lightIntensity.value).toBe(1)
|
||||
expect(viewer.fov.value).toBe(75)
|
||||
expect(viewer.upDirection.value).toBe('original')
|
||||
expect(viewer.materialMode.value).toBe('original')
|
||||
expect(viewer.edgeThreshold.value).toBe(85)
|
||||
})
|
||||
|
||||
it('should handle background image during initialization', async () => {
|
||||
mockSourceLoad3d.sceneManager.getCurrentBackgroundInfo.mockReturnValue({
|
||||
type: 'image',
|
||||
value: ''
|
||||
})
|
||||
mockNode.properties['Background Image'] = 'test-image.jpg'
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
expect(viewer.backgroundImage.value).toBe('test-image.jpg')
|
||||
expect(viewer.hasBackgroundImage.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle initialization errors', async () => {
|
||||
vi.mocked(Load3d).mockImplementationOnce(() => {
|
||||
throw new Error('Load3d creation failed')
|
||||
})
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
'toastMessages.failedToInitializeLoad3dViewer'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('state watchers', () => {
|
||||
it('should update background color when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundColor.value = '#ff0000'
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setBackgroundColor).toHaveBeenCalledWith('#ff0000')
|
||||
})
|
||||
|
||||
it('should update grid visibility when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.showGrid.value = false
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.toggleGrid).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should update camera type when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.cameraType.value = 'orthographic'
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.toggleCamera).toHaveBeenCalledWith('orthographic')
|
||||
})
|
||||
|
||||
it('should update FOV when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.fov.value = 90
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setFOV).toHaveBeenCalledWith(90)
|
||||
})
|
||||
|
||||
it('should update light intensity when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.lightIntensity.value = 2
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setLightIntensity).toHaveBeenCalledWith(2)
|
||||
})
|
||||
|
||||
it('should update background image when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundImage.value = 'new-bg.jpg'
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setBackgroundImage).toHaveBeenCalledWith('new-bg.jpg')
|
||||
expect(viewer.hasBackgroundImage.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should update up direction when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.upDirection.value = '+y'
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setUpDirection).toHaveBeenCalledWith('+y')
|
||||
})
|
||||
|
||||
it('should update material mode when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.materialMode.value = 'wireframe'
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setMaterialMode).toHaveBeenCalledWith('wireframe')
|
||||
})
|
||||
|
||||
it('should update edge threshold when state changes', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.edgeThreshold.value = 90
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.setEdgeThreshold).toHaveBeenCalledWith(90)
|
||||
})
|
||||
|
||||
it('should handle watcher errors gracefully', async () => {
|
||||
mockLoad3d.setBackgroundColor.mockImplementationOnce(() => {
|
||||
throw new Error('Color update failed')
|
||||
})
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundColor.value = '#ff0000'
|
||||
await nextTick()
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
'toastMessages.failedToUpdateBackgroundColor'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportModel', () => {
|
||||
it('should export model successfully', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
await viewer.exportModel('glb')
|
||||
|
||||
expect(mockLoad3d.exportModel).toHaveBeenCalledWith('glb')
|
||||
})
|
||||
|
||||
it('should handle export errors', async () => {
|
||||
mockLoad3d.exportModel.mockRejectedValueOnce(new Error('Export failed'))
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
await viewer.exportModel('glb')
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
'toastMessages.failedToExportModel'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not export when load3d is not initialized', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
|
||||
await viewer.exportModel('glb')
|
||||
|
||||
expect(mockLoad3d.exportModel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('UI interaction methods', () => {
|
||||
it('should handle resize', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.handleResize()
|
||||
|
||||
expect(mockLoad3d.handleResize).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle mouse enter', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.handleMouseEnter()
|
||||
|
||||
expect(mockLoad3d.updateStatusMouseOnViewer).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should handle mouse leave', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.handleMouseLeave()
|
||||
|
||||
expect(mockLoad3d.updateStatusMouseOnViewer).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreInitialState', () => {
|
||||
it('should restore all properties to initial values', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
mockNode.properties['Background Color'] = '#ff0000'
|
||||
mockNode.properties['Show Grid'] = false
|
||||
|
||||
viewer.restoreInitialState()
|
||||
|
||||
expect(mockNode.properties['Background Color']).toBe('#282828')
|
||||
expect(mockNode.properties['Show Grid']).toBe(true)
|
||||
expect(mockNode.properties['Camera Type']).toBe('perspective')
|
||||
expect(mockNode.properties['FOV']).toBe(75)
|
||||
expect(mockNode.properties['Light Intensity']).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyChanges', () => {
|
||||
it('should apply all changes to source and node', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundColor.value = '#ff0000'
|
||||
viewer.showGrid.value = false
|
||||
|
||||
const result = await viewer.applyChanges()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockNode.properties['Background Color']).toBe('#ff0000')
|
||||
expect(mockNode.properties['Show Grid']).toBe(false)
|
||||
expect(mockLoad3dService.copyLoad3dState).toHaveBeenCalledWith(
|
||||
mockLoad3d,
|
||||
mockSourceLoad3d
|
||||
)
|
||||
expect(mockSourceLoad3d.forceRender).toHaveBeenCalled()
|
||||
expect(mockNode.graph.setDirtyCanvas).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should handle background image during apply', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundImage.value = 'new-bg.jpg'
|
||||
|
||||
await viewer.applyChanges()
|
||||
|
||||
expect(mockSourceLoad3d.setBackgroundImage).toHaveBeenCalledWith(
|
||||
'new-bg.jpg'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false when no load3d instances', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
|
||||
const result = await viewer.applyChanges()
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('refreshViewport', () => {
|
||||
it('should refresh viewport', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.refreshViewport()
|
||||
|
||||
expect(mockLoad3dService.handleViewportRefresh).toHaveBeenCalledWith(
|
||||
mockLoad3d
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleBackgroundImageUpdate', () => {
|
||||
it('should upload and set background image', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue('uploaded-image.jpg')
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(Load3dUtils.uploadFile).toHaveBeenCalledWith(file, '3d')
|
||||
expect(viewer.backgroundImage.value).toBe('uploaded-image.jpg')
|
||||
expect(viewer.hasBackgroundImage.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should use resource folder for upload', async () => {
|
||||
mockNode.properties['Resource Folder'] = 'subfolder'
|
||||
vi.mocked(Load3dUtils.uploadFile).mockResolvedValue('uploaded-image.jpg')
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(Load3dUtils.uploadFile).toHaveBeenCalledWith(file, '3d/subfolder')
|
||||
})
|
||||
|
||||
it('should clear background image when file is null', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.backgroundImage.value = 'existing.jpg'
|
||||
viewer.hasBackgroundImage.value = true
|
||||
|
||||
await viewer.handleBackgroundImageUpdate(null)
|
||||
|
||||
expect(viewer.backgroundImage.value).toBe('')
|
||||
expect(viewer.hasBackgroundImage.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle upload errors', async () => {
|
||||
vi.mocked(Load3dUtils.uploadFile).mockRejectedValueOnce(
|
||||
new Error('Upload failed')
|
||||
)
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
const file = new File([''], 'test.jpg', { type: 'image/jpeg' })
|
||||
await viewer.handleBackgroundImageUpdate(file)
|
||||
|
||||
expect(mockToastStore.addAlert).toHaveBeenCalledWith(
|
||||
'toastMessages.failedToUploadBackgroundImage'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should clean up resources', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
viewer.cleanup()
|
||||
|
||||
expect(mockLoad3d.remove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle cleanup when not initialized', () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
|
||||
expect(() => viewer.cleanup()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing container ref', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
|
||||
await viewer.initializeViewer(null as any, mockSourceLoad3d)
|
||||
|
||||
expect(Load3d).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle orthographic camera', async () => {
|
||||
mockSourceLoad3d.getCurrentCameraType.mockReturnValue('orthographic')
|
||||
mockSourceLoad3d.cameraManager = {} // No perspective camera
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
expect(viewer.cameraType.value).toBe('orthographic')
|
||||
})
|
||||
|
||||
it('should handle missing lights', async () => {
|
||||
mockSourceLoad3d.lightingManager.lights = []
|
||||
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d)
|
||||
|
||||
expect(viewer.lightIntensity.value).toBe(1) // Default value
|
||||
})
|
||||
})
|
||||
})
|
||||
202
tests-ui/tests/services/keybindingService.escape.test.ts
Normal file
202
tests-ui/tests/services/keybindingService.escape.test.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
|
||||
// Mock stores
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: vi.fn(() => ({
|
||||
dialogStack: []
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('keybindingService - Escape key handling', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let mockCommandExecute: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Mock command store execute
|
||||
mockCommandExecute = vi.fn()
|
||||
const commandStore = useCommandStore()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
// Reset dialog store mock to empty
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: []
|
||||
} as any)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
it('should register Escape key for ExitSubgraph command', () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Check that the Escape keybinding exists in core keybindings
|
||||
const escapeKeybinding = CORE_KEYBINDINGS.find(
|
||||
(kb) =>
|
||||
kb.combo.key === 'Escape' && kb.commandId === 'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
expect(escapeKeybinding).toBeDefined()
|
||||
|
||||
// Check that it was registered in the store
|
||||
const registeredBinding = keybindingStore.getKeybinding(
|
||||
new KeyComboImpl({ key: 'Escape' })
|
||||
)
|
||||
expect(registeredBinding).toBeDefined()
|
||||
expect(registeredBinding?.commandId).toBe('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should execute ExitSubgraph command when Escape is pressed', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
// Mock event methods
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should not execute command when Escape is pressed with modifiers', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute command when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [inputElement])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close dialogs when no keybinding is registered', async () => {
|
||||
// Remove the Escape keybinding
|
||||
const keybindingStore = useKeybindingStore()
|
||||
keybindingStore.unsetKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
// Create a mock dialog
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.open = true
|
||||
dialog.close = vi.fn()
|
||||
document.body.appendChild(dialog)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(dialog.close).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(dialog)
|
||||
})
|
||||
|
||||
it('should allow user to rebind Escape key to different command', async () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Add a user keybinding for Escape to a different command
|
||||
keybindingStore.addUserKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Custom.Command',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Custom.Command')
|
||||
expect(mockCommandExecute).not.toHaveBeenCalledWith(
|
||||
'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute Escape keybinding when dialogs are open', async () => {
|
||||
// Mock dialog store to have open dialogs
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' }]
|
||||
} as any)
|
||||
|
||||
// Re-create keybinding service to pick up new mock
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not call preventDefault or execute command
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user