mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
Merge branch 'main' into feat/new-workflow-templates
This commit is contained in:
@@ -1,30 +1,85 @@
|
||||
# Create Hotfix Release
|
||||
|
||||
This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step.
|
||||
This command creates patch/hotfix releases for ComfyUI Frontend by backporting fixes to stable core branches. It handles both automated backports (preferred) and manual cherry-picking (fallback).
|
||||
|
||||
**Process Overview:**
|
||||
1. **Check automated backports first** (via labels)
|
||||
2. **Skip to version bump** if backports already merged
|
||||
3. **Manual cherry-picking** if automation failed
|
||||
4. **Create patch release** with version bump
|
||||
5. **Publish GitHub release** (manually uncheck "latest")
|
||||
6. **Update ComfyUI requirements.txt** via PR
|
||||
|
||||
<task>
|
||||
Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS
|
||||
Create a hotfix release by backporting commits/PRs from main to a core branch: $ARGUMENTS
|
||||
|
||||
Expected format: Comma-separated list of commits or PR numbers
|
||||
Examples:
|
||||
- `abc123,def456,ghi789` (commits)
|
||||
- `#1234,#5678` (PRs)
|
||||
- `abc123,#1234,def456` (mixed)
|
||||
- `#1234,#5678` (PRs - preferred)
|
||||
- `abc123,def456` (commit hashes)
|
||||
- `#1234,abc123` (mixed)
|
||||
|
||||
If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs.
|
||||
If no arguments provided, the command will guide you through identifying commits/PRs to backport.
|
||||
</task>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure:
|
||||
- You have push access to the repository
|
||||
- GitHub CLI (`gh`) is authenticated
|
||||
- You're on a clean working tree
|
||||
- You understand the commits/PRs you're cherry-picking
|
||||
- Push access to repository
|
||||
- GitHub CLI (`gh`) authenticated
|
||||
- Clean working tree
|
||||
- Understanding of what fixes need backporting
|
||||
|
||||
## Hotfix Release Process
|
||||
|
||||
### Step 1: Identify Target Core Branch
|
||||
### Step 1: Try Automated Backports First
|
||||
|
||||
**Check if automated backports were attempted:**
|
||||
|
||||
1. **For each PR, check existing backport labels:**
|
||||
```bash
|
||||
gh pr view #1234 --json labels | jq -r '.labels[].name'
|
||||
```
|
||||
|
||||
2. **If no backport labels exist, add them now:**
|
||||
```bash
|
||||
# Add backport labels (this triggers automated backports)
|
||||
gh pr edit #1234 --add-label "needs-backport"
|
||||
gh pr edit #1234 --add-label "1.24" # Replace with target version
|
||||
```
|
||||
|
||||
3. **Check for existing backport PRs:**
|
||||
```bash
|
||||
# Check for backport PRs created by automation
|
||||
PR_NUMBER=${ARGUMENTS%%,*} # Extract first PR number from arguments
|
||||
PR_NUMBER=${PR_NUMBER#\#} # Remove # prefix
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --json number,title,state,baseRefName
|
||||
```
|
||||
|
||||
4. **Handle existing backport scenarios:**
|
||||
|
||||
**Scenario A: Automated backports already merged**
|
||||
```bash
|
||||
# Check if backport PRs were merged to core branches
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --state merged
|
||||
```
|
||||
- If backport PRs are merged → Skip to Step 10 (Version Bump)
|
||||
- **CONFIRMATION**: Automated backports completed, proceeding to version bump?
|
||||
|
||||
**Scenario B: Automated backport PRs exist but not merged**
|
||||
```bash
|
||||
# Show open backport PRs that need merging
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --state open
|
||||
```
|
||||
- **ACTION REQUIRED**: Merge the existing backport PRs first
|
||||
- Use: `gh pr merge [PR_NUMBER] --merge` for each backport PR
|
||||
- After merging, return to this command and skip to Step 10 (Version Bump)
|
||||
- **CONFIRMATION**: Have you merged all backport PRs? Ready to proceed to version bump?
|
||||
|
||||
**Scenario C: No automated backports or they failed**
|
||||
- Continue to Step 2 for manual cherry-picking
|
||||
- **CONFIRMATION**: Proceeding with manual cherry-picking because automation failed?
|
||||
|
||||
### Step 2: Identify Target Core Branch
|
||||
|
||||
1. Fetch the current ComfyUI requirements.txt from master branch:
|
||||
```bash
|
||||
@@ -36,7 +91,7 @@ Before starting, ensure:
|
||||
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
|
||||
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
|
||||
|
||||
### Step 2: Parse and Validate Arguments
|
||||
### Step 3: Parse and Validate Arguments
|
||||
|
||||
1. Parse the comma-separated list of commits/PRs
|
||||
2. For each item:
|
||||
@@ -49,7 +104,7 @@ Before starting, ensure:
|
||||
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
|
||||
4. Validate all commit hashes exist in the repository
|
||||
|
||||
### Step 3: Analyze Target Changes
|
||||
### Step 4: Analyze Target Changes
|
||||
|
||||
1. For each commit/PR to cherry-pick:
|
||||
- Display commit hash, author, date
|
||||
@@ -60,7 +115,7 @@ Before starting, ensure:
|
||||
2. Identify potential conflicts by checking changed files
|
||||
3. **CONFIRMATION REQUIRED**: Proceed with these commits?
|
||||
|
||||
### Step 4: Create Hotfix Branch
|
||||
### Step 5: Create Hotfix Branch
|
||||
|
||||
1. Checkout the core branch (e.g., `core/1.23`)
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
@@ -69,7 +124,7 @@ Before starting, ensure:
|
||||
- Example: `hotfix/1.23.4-20241120`
|
||||
5. **CONFIRMATION REQUIRED**: Created branch correctly?
|
||||
|
||||
### Step 5: Cherry-pick Changes
|
||||
### Step 6: Cherry-pick Changes
|
||||
|
||||
For each commit:
|
||||
1. Attempt cherry-pick: `git cherry-pick <commit>`
|
||||
@@ -83,7 +138,7 @@ For each commit:
|
||||
- Run validation: `pnpm typecheck && pnpm lint`
|
||||
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
|
||||
|
||||
### Step 6: Create PR to Core Branch
|
||||
### Step 7: Create PR to Core Branch
|
||||
|
||||
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
|
||||
2. Create PR using gh CLI:
|
||||
@@ -100,7 +155,7 @@ For each commit:
|
||||
- Impact assessment
|
||||
5. **CONFIRMATION REQUIRED**: PR created correctly?
|
||||
|
||||
### Step 7: Wait for Tests
|
||||
### Step 8: Wait for Tests
|
||||
|
||||
1. Monitor PR checks: `gh pr checks`
|
||||
2. Display test results as they complete
|
||||
@@ -111,7 +166,7 @@ For each commit:
|
||||
4. Wait for all required checks to pass
|
||||
5. **CONFIRMATION REQUIRED**: All tests passing?
|
||||
|
||||
### Step 8: Merge Hotfix PR
|
||||
### Step 9: Merge Hotfix PR
|
||||
|
||||
1. Verify all checks have passed
|
||||
2. Check for required approvals
|
||||
@@ -119,7 +174,7 @@ For each commit:
|
||||
4. Delete the hotfix branch
|
||||
5. **CONFIRMATION REQUIRED**: PR merged successfully?
|
||||
|
||||
### Step 9: Create Version Bump
|
||||
### Step 10: Create Version Bump
|
||||
|
||||
1. Checkout the core branch: `git checkout core/X.Y`
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
@@ -131,7 +186,7 @@ For each commit:
|
||||
7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
|
||||
8. **CONFIRMATION REQUIRED**: Version bump correct?
|
||||
|
||||
### Step 10: Create Release PR
|
||||
### Step 11: Create Release PR
|
||||
|
||||
1. Push release branch: `git push origin release/1.23.5`
|
||||
2. Create PR with Release label:
|
||||
@@ -184,7 +239,7 @@ For each commit:
|
||||
```
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
### Step 12: Monitor Release Process
|
||||
|
||||
1. Wait for PR checks to pass
|
||||
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
|
||||
@@ -199,7 +254,102 @@ For each commit:
|
||||
- PyPI upload
|
||||
- pnpm types publication
|
||||
|
||||
### Step 12: Post-Release Verification
|
||||
### Step 13: Manually Publish Draft Release
|
||||
|
||||
**CRITICAL**: The release workflow creates a DRAFT release. You must manually publish it:
|
||||
|
||||
1. **Go to GitHub Releases:** https://github.com/Comfy-Org/ComfyUI_frontend/releases
|
||||
2. **Find the DRAFT release** (e.g., "v1.23.5 Draft")
|
||||
3. **Click "Edit release"**
|
||||
4. **UNCHECK "Set as the latest release"** ⚠️ **CRITICAL**
|
||||
- This prevents the hotfix from showing as "latest"
|
||||
- Main branch should always be "latest release"
|
||||
5. **Click "Publish release"**
|
||||
6. **CONFIRMATION REQUIRED**: Draft release published with "latest" unchecked?
|
||||
|
||||
### Step 14: Create ComfyUI Requirements.txt Update PR
|
||||
|
||||
**IMPORTANT**: Create PR to update ComfyUI's requirements.txt via fork:
|
||||
|
||||
1. **Setup fork (if needed):**
|
||||
```bash
|
||||
# Check if fork already exists
|
||||
if gh repo view ComfyUI --json owner | jq -r '.owner.login' | grep -q "$(gh api user --jq .login)"; then
|
||||
echo "Fork already exists"
|
||||
else
|
||||
# Fork the ComfyUI repository
|
||||
gh repo fork comfyanonymous/ComfyUI --clone=false
|
||||
echo "Created fork of ComfyUI"
|
||||
fi
|
||||
```
|
||||
|
||||
2. **Clone fork and create branch:**
|
||||
```bash
|
||||
# Clone your fork (or use existing clone)
|
||||
GITHUB_USER=$(gh api user --jq .login)
|
||||
if [ ! -d "ComfyUI-fork" ]; then
|
||||
gh repo clone ${GITHUB_USER}/ComfyUI ComfyUI-fork
|
||||
fi
|
||||
|
||||
cd ComfyUI-fork
|
||||
git checkout master
|
||||
git pull origin master
|
||||
|
||||
# Create update branch
|
||||
BRANCH_NAME="update-frontend-${NEW_VERSION}"
|
||||
git checkout -b ${BRANCH_NAME}
|
||||
```
|
||||
|
||||
3. **Update requirements.txt:**
|
||||
```bash
|
||||
# Update the version in requirements.txt
|
||||
sed -i "s/comfyui-frontend-package==[0-9].*$/comfyui-frontend-package==${NEW_VERSION}/" requirements.txt
|
||||
|
||||
# Verify the change
|
||||
grep "comfyui-frontend-package" requirements.txt
|
||||
|
||||
# Commit the change
|
||||
git add requirements.txt
|
||||
git commit -m "Bump frontend to ${NEW_VERSION}"
|
||||
git push origin ${BRANCH_NAME}
|
||||
```
|
||||
|
||||
4. **Create PR from fork:**
|
||||
```bash
|
||||
# Create PR using gh CLI from fork
|
||||
gh pr create \
|
||||
--repo comfyanonymous/ComfyUI \
|
||||
--title "Bump frontend to ${NEW_VERSION}" \
|
||||
--body "$(cat <<EOF
|
||||
Bump frontend to ${NEW_VERSION}
|
||||
|
||||
\`\`\`
|
||||
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${NEW_VERSION}
|
||||
\`\`\`
|
||||
|
||||
- Diff: [Comfy-Org/ComfyUI_frontend: v${OLD_VERSION}...v${NEW_VERSION}](https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${OLD_VERSION}...v${NEW_VERSION})
|
||||
- PyPI Package: https://pypi.org/project/comfyui-frontend-package/${NEW_VERSION}/
|
||||
- npm Types: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${NEW_VERSION}
|
||||
|
||||
## Changes
|
||||
|
||||
- Fix: [Brief description of hotfixes included]
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
5. **Clean up:**
|
||||
```bash
|
||||
# Return to original directory
|
||||
cd ..
|
||||
|
||||
# Keep fork directory for future updates
|
||||
echo "Fork directory 'ComfyUI-fork' kept for future use"
|
||||
```
|
||||
|
||||
6. **CONFIRMATION REQUIRED**: ComfyUI requirements.txt PR created from fork?
|
||||
|
||||
### Step 15: Post-Release Verification
|
||||
|
||||
1. Verify GitHub release:
|
||||
```bash
|
||||
@@ -213,12 +363,14 @@ For each commit:
|
||||
```bash
|
||||
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
|
||||
```
|
||||
4. Generate release summary with:
|
||||
4. Monitor ComfyUI requirements.txt PR for approval/merge
|
||||
5. Generate release summary with:
|
||||
- Version released
|
||||
- Commits included
|
||||
- Issues fixed
|
||||
- Distribution status
|
||||
5. **CONFIRMATION REQUIRED**: Release completed successfully?
|
||||
- ComfyUI integration status
|
||||
6. **CONFIRMATION REQUIRED**: Hotfix release fully completed?
|
||||
|
||||
## Safety Checks
|
||||
|
||||
@@ -240,19 +392,28 @@ If something goes wrong:
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Always try automated backports first** - This command is for when automation fails
|
||||
- Core branch version will be behind main - this is expected
|
||||
- The "Release" label triggers the PyPI/npm publication
|
||||
- **CRITICAL**: Always uncheck "Set as latest release" for hotfix releases
|
||||
- **Must create ComfyUI requirements.txt PR** - Hotfix isn't complete without it
|
||||
- PR numbers must include the `#` prefix
|
||||
- Mixed commits/PRs are supported but review carefully
|
||||
- Always wait for full test suite before proceeding
|
||||
|
||||
## Expected Timeline
|
||||
## Modern Workflow Context
|
||||
|
||||
- Step 1-3: ~10 minutes (analysis)
|
||||
- Steps 4-6: ~15-30 minutes (cherry-picking)
|
||||
- Step 7: ~10-20 minutes (tests)
|
||||
- Steps 8-10: ~10 minutes (version bump)
|
||||
- Step 11-12: ~15-20 minutes (release)
|
||||
- Total: ~60-90 minutes
|
||||
**Primary Backport Method:** Automated via `needs-backport` + `X.YY` labels
|
||||
**This Command Usage:**
|
||||
- Smart path detection - skip to version bump if backports already merged
|
||||
- Fallback to manual cherry-picking only when automation fails/has conflicts
|
||||
**Complete Hotfix:** Includes GitHub release publishing + ComfyUI requirements.txt integration
|
||||
|
||||
This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released.
|
||||
## Workflow Paths
|
||||
|
||||
- **Path A:** Backports already merged → Skip to Step 10 (Version Bump)
|
||||
- **Path B:** Backport PRs need merging → Merge them → Skip to Step 10 (Version Bump)
|
||||
- **Path C:** No/failed backports → Manual cherry-picking (Steps 2-9) → Version Bump (Step 10)
|
||||
|
||||
|
||||
This process ensures a complete hotfix release with proper GitHub publishing, ComfyUI integration, and multiple safety checkpoints.
|
||||
@@ -4,3 +4,6 @@
|
||||
|
||||
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
|
||||
c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||
|
||||
# Reorganize renderer components into domain-driven folder structure
|
||||
c8a83a9caede7bdb5f8598c5492b07d08c339d49
|
||||
|
||||
27
.github/workflows/backport.yaml
vendored
27
.github/workflows/backport.yaml
vendored
@@ -2,7 +2,7 @@ name: Auto Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
types: [closed, labeled]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
@@ -25,7 +25,27 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Check if backports already exist
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
# Check for existing backport PRs for this PR number
|
||||
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||
|
||||
if [ -z "$EXISTING_BACKPORTS" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found existing backport PRs:"
|
||||
echo "$EXISTING_BACKPORTS"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Extract version labels
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: versions
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
@@ -52,6 +72,7 @@ jobs:
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
@@ -109,7 +130,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Create PR for each successful backport
|
||||
if: steps.backport.outputs.success
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
run: |
|
||||
@@ -141,7 +162,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Comment on failures
|
||||
if: failure() && steps.backport.outputs.failed
|
||||
if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
||||
@@ -128,45 +128,6 @@ jobs:
|
||||
echo "- Critical security patches"
|
||||
echo "- Documentation updates"
|
||||
|
||||
- name: Create branch protection rules
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
|
||||
# Create branch protection using GitHub API
|
||||
echo "Setting up branch protection for $BRANCH_NAME..."
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \
|
||||
-d '{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"contexts": ["lint-and-format", "test", "playwright-tests"]
|
||||
},
|
||||
"enforce_admins": false,
|
||||
"required_pull_request_reviews": {
|
||||
"required_approving_review_count": 1,
|
||||
"dismiss_stale_reviews": true
|
||||
},
|
||||
"restrictions": null,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false
|
||||
}')
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then
|
||||
echo "✅ Branch protection successfully applied"
|
||||
else
|
||||
echo "⚠️ Failed to apply branch protection (HTTP $HTTP_CODE)"
|
||||
echo "Response: $BODY"
|
||||
# Don't fail the workflow, just warn
|
||||
fi
|
||||
|
||||
- name: Post summary
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
npx lint-staged
|
||||
npx tsx scripts/check-unused-i18n-keys.ts
|
||||
pnpm exec lint-staged
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
|
||||
5
.husky/pre-push
Executable file
5
.husky/pre-push
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run Knip with cache via package script
|
||||
pnpm knip
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Position } from './types'
|
||||
* - {@link Mouse.move}
|
||||
* - {@link Mouse.up}
|
||||
*/
|
||||
export interface DragOptions {
|
||||
interface DragOptions {
|
||||
button?: 'left' | 'right' | 'middle'
|
||||
clickCount?: number
|
||||
steps?: number
|
||||
|
||||
@@ -453,6 +453,32 @@ export class ComfyPage {
|
||||
await workflowsTab.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a screenshot to the test report.
|
||||
* By default, screenshots are only taken in non-CI environments.
|
||||
* @param name - Name for the screenshot attachment
|
||||
* @param options - Optional configuration
|
||||
* @param options.runInCI - Whether to take screenshot in CI (default: false)
|
||||
* @param options.fullPage - Whether to capture full page (default: false)
|
||||
*/
|
||||
async attachScreenshot(
|
||||
name: string,
|
||||
options: { runInCI?: boolean; fullPage?: boolean } = {}
|
||||
) {
|
||||
const { runInCI = false, fullPage = false } = options
|
||||
|
||||
// Skip in CI unless explicitly requested
|
||||
if (process.env.CI && !runInCI) {
|
||||
return
|
||||
}
|
||||
|
||||
const testInfo = comfyPageFixture.info()
|
||||
await testInfo.attach(name, {
|
||||
body: await this.page.screenshot({ fullPage }),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click()
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
constructor(public readonly page: Page) {}
|
||||
private readonly menuLocator: Locator
|
||||
private readonly menuTrigger: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.menuLocator = page.locator('.comfy-command-menu')
|
||||
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||
}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
return await this.page
|
||||
@@ -15,10 +21,33 @@ export class Topbar {
|
||||
.innerText()
|
||||
}
|
||||
|
||||
getMenuItem(itemLabel: string): Locator {
|
||||
/**
|
||||
* Get a menu item by its label, optionally within a specific parent container
|
||||
*/
|
||||
getMenuItem(itemLabel: string, parent?: Locator): Locator {
|
||||
if (parent) {
|
||||
return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`)
|
||||
}
|
||||
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible submenu (last visible submenu in case of nested menus)
|
||||
*/
|
||||
getVisibleSubmenu(): Locator {
|
||||
return this.page.locator('.p-tieredmenu-submenu:visible').last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a menu item has an active checkmark
|
||||
*/
|
||||
async isMenuItemActive(menuItem: Locator): Promise<boolean> {
|
||||
const checkmark = menuItem.locator('.pi-check')
|
||||
const classes = await checkmark.getAttribute('class')
|
||||
return classes ? !classes.includes('invisible') : false
|
||||
}
|
||||
|
||||
getWorkflowTab(tabName: string): Locator {
|
||||
return this.page
|
||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||
@@ -66,10 +95,50 @@ export class Topbar {
|
||||
|
||||
async openTopbarMenu() {
|
||||
await this.page.waitForTimeout(1000)
|
||||
await this.page.locator('.comfyui-logo-wrapper').click()
|
||||
const menu = this.page.locator('.comfy-command-menu')
|
||||
await menu.waitFor({ state: 'visible' })
|
||||
return menu
|
||||
await this.menuTrigger.click()
|
||||
await this.menuLocator.waitFor({ state: 'visible' })
|
||||
return this.menuLocator
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the topbar menu by clicking outside
|
||||
*/
|
||||
async closeTopbarMenu() {
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
await expect(this.menuLocator).not.toBeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a submenu by hovering over a menu item
|
||||
*/
|
||||
async openSubmenu(menuItemLabel: string): Promise<Locator> {
|
||||
const menuItem = this.getMenuItem(menuItemLabel)
|
||||
await menuItem.hover()
|
||||
const submenu = this.getVisibleSubmenu()
|
||||
await submenu.waitFor({ state: 'visible' })
|
||||
return submenu
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme menu items and interact with theme switching
|
||||
*/
|
||||
async getThemeMenuItems() {
|
||||
const themeSubmenu = await this.openSubmenu('Theme')
|
||||
return {
|
||||
submenu: themeSubmenu,
|
||||
darkTheme: this.getMenuItem('Dark (Default)', themeSubmenu),
|
||||
lightTheme: this.getMenuItem('Light', themeSubmenu)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific theme
|
||||
*/
|
||||
async switchTheme(theme: 'dark' | 'light') {
|
||||
const { darkTheme, lightTheme } = await this.getThemeMenuItems()
|
||||
const themeItem = theme === 'dark' ? darkTheme : lightTheme
|
||||
const themeLabel = themeItem.locator('.p-menubar-item-label')
|
||||
await themeLabel.click()
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
@@ -79,9 +148,7 @@ export class Topbar {
|
||||
|
||||
const menu = await this.openTopbarMenu()
|
||||
const tabName = path[0]
|
||||
const topLevelMenuItem = this.page.locator(
|
||||
`.p-menubar-item-label:text-is("${tabName}")`
|
||||
)
|
||||
const topLevelMenuItem = this.getMenuItem(tabName)
|
||||
const topLevelMenu = menu
|
||||
.locator('.p-tieredmenu-item')
|
||||
.filter({ has: topLevelMenuItem })
|
||||
|
||||
@@ -134,7 +134,7 @@ export class SubgraphSlotReference {
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSlotReference {
|
||||
class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly index: number,
|
||||
@@ -201,7 +201,7 @@ export class NodeSlotReference {
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWidgetReference {
|
||||
class NodeWidgetReference {
|
||||
constructor(
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
|
||||
@@ -178,6 +178,72 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('Can navigate Theme menu and switch between Dark and Light themes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { topbar } = comfyPage.menu
|
||||
|
||||
// Take initial screenshot with default theme
|
||||
await comfyPage.attachScreenshot('theme-initial')
|
||||
|
||||
// Open the topbar menu
|
||||
const menu = await topbar.openTopbarMenu()
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
// Get theme menu items
|
||||
const {
|
||||
submenu: themeSubmenu,
|
||||
darkTheme: darkThemeItem,
|
||||
lightTheme: lightThemeItem
|
||||
} = await topbar.getThemeMenuItems()
|
||||
|
||||
await expect(darkThemeItem).toBeVisible()
|
||||
await expect(lightThemeItem).toBeVisible()
|
||||
|
||||
// Switch to Light theme
|
||||
await topbar.switchTheme('light')
|
||||
|
||||
// Verify menu stays open and Light theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeSubmenu).toBeVisible()
|
||||
|
||||
// Check that Light theme is active
|
||||
expect(await topbar.isMenuItemActive(lightThemeItem)).toBe(true)
|
||||
|
||||
// Screenshot with light theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-light-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "light"
|
||||
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light')
|
||||
|
||||
// Close menu to see theme change
|
||||
await topbar.closeTopbarMenu()
|
||||
|
||||
// Re-open menu and get theme items again
|
||||
await topbar.openTopbarMenu()
|
||||
const themeItems2 = await topbar.getThemeMenuItems()
|
||||
|
||||
// Switch back to Dark theme
|
||||
await topbar.switchTheme('dark')
|
||||
|
||||
// Verify menu stays open and Dark theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeItems2.submenu).toBeVisible()
|
||||
|
||||
// Check that Dark theme is active and Light theme is not
|
||||
expect(await topbar.isMenuItemActive(themeItems2.darkTheme)).toBe(true)
|
||||
expect(await topbar.isMenuItemActive(themeItems2.lightTheme)).toBe(false)
|
||||
|
||||
// Screenshot with dark theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-dark-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "dark"
|
||||
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark')
|
||||
|
||||
// Close menu
|
||||
await topbar.closeTopbarMenu()
|
||||
})
|
||||
})
|
||||
|
||||
// Only test 'Top' to reduce test time.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 96 KiB |
@@ -1,4 +1,4 @@
|
||||
# 4. Centralized Layout Management with CRDT
|
||||
# 3. Centralized Layout Management with CRDT
|
||||
|
||||
Date: 2025-08-27
|
||||
|
||||
@@ -12,6 +12,7 @@ An Architecture Decision Record captures an important architectural decision mad
|
||||
|-----|-------|--------|------|
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
|
||||
@@ -62,6 +62,41 @@ export default [
|
||||
'@typescript-eslint/prefer-as-const': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||
// Restrict deprecated PrimeVue components
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'primevue/calendar',
|
||||
message:
|
||||
'Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from "primevue/datepicker"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/dropdown',
|
||||
message:
|
||||
'Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from "primevue/select"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/inputswitch',
|
||||
message:
|
||||
'InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from "primevue/toggleswitch"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/overlaypanel',
|
||||
message:
|
||||
'OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from "primevue/popover"'
|
||||
},
|
||||
{
|
||||
name: 'primevue/sidebar',
|
||||
message:
|
||||
'Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from "primevue/drawer"'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
// i18n rules
|
||||
'@intlify/vue-i18n/no-raw-text': [
|
||||
'error',
|
||||
|
||||
@@ -2,84 +2,56 @@ import type { KnipConfig } from 'knip'
|
||||
|
||||
const config: KnipConfig = {
|
||||
entry: [
|
||||
'build/**/*.ts',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'{build,scripts}/**/*.{js,ts}',
|
||||
'src/assets/css/style.css',
|
||||
'src/main.ts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts'
|
||||
],
|
||||
project: [
|
||||
'browser_tests/**/*.{js,ts}',
|
||||
'build/**/*.{js,ts,vue}',
|
||||
'scripts/**/*.{js,ts}',
|
||||
'src/**/*.{js,ts,vue}',
|
||||
'tests-ui/**/*.{js,ts,vue}',
|
||||
'*.{js,ts,mts}'
|
||||
'src/scripts/ui/menu/index.ts',
|
||||
'src/types/index.ts'
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'],
|
||||
ignoreBinaries: ['only-allow', 'openapi-typescript'],
|
||||
ignoreDependencies: [
|
||||
// Weird importmap things
|
||||
'@iconify/json',
|
||||
'@primeuix/forms',
|
||||
'@primeuix/styled',
|
||||
'@primeuix/utils',
|
||||
'@primevue/icons',
|
||||
'@iconify/json',
|
||||
'tailwindcss',
|
||||
'tailwindcss-primeui', // Need to figure out why tailwind plugin isn't applying
|
||||
// Dev
|
||||
'@trivago/prettier-plugin-sort-imports'
|
||||
],
|
||||
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',
|
||||
// Vitest litegraph config
|
||||
'vitest.litegraph.config.ts',
|
||||
// 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',
|
||||
// Design system components (may not be used immediately)
|
||||
'src/components/button/IconGroup.vue',
|
||||
'src/components/button/MoreButton.vue',
|
||||
'src/components/button/TextButton.vue',
|
||||
'src/components/card/CardTitle.vue',
|
||||
'src/components/card/CardDescription.vue',
|
||||
'src/components/input/SingleSelect.vue',
|
||||
'src/types/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Generated file: openapi
|
||||
'src/types/comfyRegistryTypes.ts'
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
// Vue-specific configuration
|
||||
vue: true,
|
||||
tailwind: 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: {
|
||||
classMembers: 'off'
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
css: (text: string) =>
|
||||
[
|
||||
...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g)
|
||||
].join('\n')
|
||||
},
|
||||
vite: {
|
||||
config: ['vite?(.*).config.mts']
|
||||
},
|
||||
vitest: {
|
||||
config: ['vitest?(.*).config.ts'],
|
||||
entry: [
|
||||
'**/*.{bench,test,test-d,spec}.?(c|m)[jt]s?(x)',
|
||||
'**/__mocks__/**/*.[jt]s?(x)'
|
||||
]
|
||||
},
|
||||
playwright: {
|
||||
config: ['playwright?(.*).config.ts'],
|
||||
entry: ['**/*.@(spec|test).?(c|m)[jt]s?(x)', 'browser_tests/**/*.ts']
|
||||
},
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch'
|
||||
],
|
||||
// Include dependencies analysis
|
||||
includeEntryExports: true
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.27.1",
|
||||
"version": "1.27.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -25,8 +25,8 @@
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
"lint": "eslint src --cache",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint": "eslint src --cache --concurrency=auto",
|
||||
"lint:fix": "eslint src --cache --fix --concurrency=auto",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip --cache",
|
||||
@@ -62,7 +62,7 @@
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-storybook": "^9.1.1",
|
||||
@@ -84,7 +84,7 @@
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"unplugin-icons": "^0.22.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
||||
559
pnpm-lock.yaml
generated
559
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,10 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import { useConflictDetection } from '@/composables/useConflictDetection'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { useConflictDetection } from './composables/useConflictDetection'
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
|
||||
@@ -40,6 +40,7 @@ import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbIt
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { forEachSubgraphNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
@@ -52,6 +53,9 @@ const workflowStore = useWorkflowStore()
|
||||
const navigationStore = useSubgraphNavigationStore()
|
||||
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
||||
const workflowName = computed(() => workflowStore.activeWorkflow?.filename)
|
||||
const isBlueprint = computed(() =>
|
||||
useSubgraphStore().isSubgraphBlueprint(workflowStore.activeWorkflow)
|
||||
)
|
||||
const collapseTabs = ref(false)
|
||||
const overflowingTabs = ref(false)
|
||||
|
||||
@@ -89,6 +93,7 @@ const home = computed(() => ({
|
||||
label: workflowName.value,
|
||||
icon: 'pi pi-home',
|
||||
key: 'root',
|
||||
isBlueprint: isBlueprint.value,
|
||||
command: () => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
</a>
|
||||
<Menu
|
||||
@@ -48,6 +49,7 @@
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Menu, { MenuState } from 'primevue/menu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -121,7 +123,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
command: async () => {
|
||||
await workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)
|
||||
},
|
||||
visible: isRoot
|
||||
visible: isRoot && !props.item.isBlueprint
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
@@ -153,12 +155,26 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
await useCommandStore().execute('Comfy.ClearWorkflow')
|
||||
}
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
visible: props.item.key === 'root' && props.item.isBlueprint
|
||||
},
|
||||
{
|
||||
label: t('subgraphStore.publish'),
|
||||
icon: 'pi pi-copy',
|
||||
command: async () => {
|
||||
await workflowService.saveWorkflowAs(workflowStore.activeWorkflow!)
|
||||
},
|
||||
visible: props.item.key === 'root' && props.item.isBlueprint
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
visible: isRoot
|
||||
},
|
||||
{
|
||||
label: t('breadcrumbsMenu.deleteWorkflow'),
|
||||
label: props.item.isBlueprint
|
||||
? t('breadcrumbsMenu.deleteBlueprint')
|
||||
: t('breadcrumbsMenu.deleteWorkflow'),
|
||||
icon: 'pi pi-times',
|
||||
command: async () => {
|
||||
await workflowService.deleteWorkflow(workflowStore.activeWorkflow!)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class="preview-box border rounded p-2 w-16 h-16 flex items-center justify-center"
|
||||
:class="{ 'bg-gray-100 dark:bg-gray-800': !modelValue }"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-gray-800': !modelValue }"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue"
|
||||
|
||||
@@ -16,6 +16,21 @@
|
||||
{{ hint }}
|
||||
</Message>
|
||||
<div class="flex gap-4 justify-end">
|
||||
<div
|
||||
v-if="type === 'overwriteBlueprint'"
|
||||
class="flex gap-4 justify-start"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="doNotAskAgain"
|
||||
class="flex gap-4 justify-start"
|
||||
input-id="doNotAskAgain"
|
||||
binary
|
||||
/>
|
||||
<label for="doNotAskAgain" severity="secondary">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:label="$t('g.cancel')"
|
||||
icon="pi pi-undo"
|
||||
@@ -38,7 +53,7 @@
|
||||
@click="onConfirm"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="type === 'overwrite'"
|
||||
v-else-if="type === 'overwrite' || type === 'overwriteBlueprint'"
|
||||
:label="$t('g.overwrite')"
|
||||
severity="warn"
|
||||
icon="pi pi-save"
|
||||
@@ -74,10 +89,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ConfirmationDialogType } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
@@ -87,14 +106,20 @@ const props = defineProps<{
|
||||
hint?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const onCancel = () => useDialogStore().closeDialog()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
const onDeny = () => {
|
||||
props.onConfirm(false)
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
if (props.type === 'overwriteBlueprint' && doNotAskAgain.value)
|
||||
void useSettingStore().set('Comfy.Workflow.WarnBlueprintOverwrite', false)
|
||||
props.onConfirm(true)
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ const showContactSupport = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
await systemStatsStore.refetchSystemStats()
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -54,19 +54,12 @@
|
||||
import Button from 'primevue/button'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import { useMissingNodes } from '@/composables/nodePack/useMissingNodes'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -81,6 +74,7 @@ const { missingNodePacks, isLoading, error, missingCoreNodes } =
|
||||
useMissingNodes()
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const managerState = useManagerState()
|
||||
|
||||
// Check if any of the missing packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
@@ -111,47 +105,21 @@ const uniqueNodes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
|
||||
// Show manager buttons unless manager is disabled
|
||||
const showManagerButtons = computed(() => {
|
||||
return managerStateStore.managerUIState !== ManagerUIState.DISABLED
|
||||
return managerState.shouldShowManagerButtons.value
|
||||
})
|
||||
|
||||
// Only show Install All button for NEW_UI (new manager with v4 support)
|
||||
const showInstallAllButton = computed(() => {
|
||||
return managerStateStore.managerUIState === ManagerUIState.NEW_UI
|
||||
return managerState.shouldShowInstallButton.value
|
||||
})
|
||||
|
||||
const openManager = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
useDialogService().showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, show toast
|
||||
const { t } = useI18n()
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
break
|
||||
}
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.Missing,
|
||||
showToastOnLegacyError: true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -42,9 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Message from 'primevue/message'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
@@ -60,20 +59,11 @@ const hasMissingCoreNodes = computed(() => {
|
||||
return Object.keys(props.missingCoreNodes).length > 0
|
||||
})
|
||||
|
||||
const currentComfyUIVersion = ref<string | null>(null)
|
||||
whenever(
|
||||
hasMissingCoreNodes,
|
||||
async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
currentComfyUIVersion.value =
|
||||
systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// Use computed for reactive version tracking
|
||||
const currentComfyUIVersion = computed<string | null>(() => {
|
||||
if (!hasMissingCoreNodes.value) return null
|
||||
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
})
|
||||
|
||||
const sortedMissingCoreNodes = computed(() => {
|
||||
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- Conflict Warning Banner -->
|
||||
<div
|
||||
v-if="shouldShowManagerBanner"
|
||||
class="bg-yellow-600 bg-opacity-20 border border-yellow-400 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
class="bg-yellow-500/20 rounded-lg p-4 mt-3 mb-4 flex items-center gap-6 relative"
|
||||
>
|
||||
<i class="pi pi-exclamation-triangle text-yellow-600 text-lg"></i>
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
@@ -46,14 +46,15 @@
|
||||
{{ $t('manager.conflicts.warningBanner.button') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 w-6 h-6 border-none outline-none bg-transparent flex items-center justify-center text-yellow-600 rounded transition-colors"
|
||||
:aria-label="$t('g.close')"
|
||||
<IconButton
|
||||
class="absolute top-0 right-0"
|
||||
type="transparent"
|
||||
@click="dismissWarningBanner"
|
||||
>
|
||||
<i class="pi pi-times text-sm"></i>
|
||||
</button>
|
||||
<i
|
||||
class="pi pi-times text-neutral-900 dark-theme:text-white text-xs"
|
||||
></i>
|
||||
</IconButton>
|
||||
</div>
|
||||
<RegistrySearchBar
|
||||
v-model:searchQuery="searchQuery"
|
||||
@@ -138,6 +139,7 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
:has-conflict="hasConflicts"
|
||||
:conflict-info="conflictInfo"
|
||||
/>
|
||||
<PackEnableToggle v-else :node-pack="nodePack" />
|
||||
<PackEnableToggle
|
||||
v-else
|
||||
:has-conflict="hasConflicts"
|
||||
:node-pack="nodePack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
// eslint-disable-next-line no-restricted-imports -- TODO: Migrate to Select component
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
|
||||
import type { SearchOption } from '@/types/comfyManagerTypes'
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
<script setup lang="ts">
|
||||
import Divider from 'primevue/divider'
|
||||
import Tag from 'primevue/tag'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
|
||||
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
|
||||
@@ -44,10 +43,4 @@ import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const aboutPanelStore = useAboutPanelStore()
|
||||
|
||||
onMounted(async () => {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
<!-- TransformPane for Vue node rendering -->
|
||||
<TransformPane
|
||||
v-if="isVueNodesEnabled && canvasStore.canvas && comfyAppReady"
|
||||
:canvas="canvasStore.canvas as LGraphCanvas"
|
||||
v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady"
|
||||
:canvas="comfyApp.canvas"
|
||||
@transform-update="handleTransformUpdate"
|
||||
>
|
||||
<!-- Vue nodes rendered based on graph nodes -->
|
||||
@@ -44,7 +44,6 @@
|
||||
:node-data="nodeData"
|
||||
:position="nodePositions.get(nodeData.id)"
|
||||
:size="nodeSizes.get(nodeData.id)"
|
||||
:selected="nodeData.selected"
|
||||
:readonly="false"
|
||||
:executing="executionStore.executingNodeId === nodeData.id"
|
||||
:error="
|
||||
@@ -79,6 +78,7 @@ import {
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
@@ -89,21 +89,16 @@ import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitter
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import MiniMap from '@/components/graph/MiniMap.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import TransformPane from '@/components/graph/TransformPane.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useTransformState } from '@/composables/element/useTransformState'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type {
|
||||
NodeState,
|
||||
VueNodeData
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { useNodeEventHandlers } from '@/composables/graph/useNodeEventHandlers'
|
||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
import { useCanvasDrop } from '@/composables/useCanvasDrop'
|
||||
import { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'
|
||||
@@ -116,13 +111,10 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
||||
import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync'
|
||||
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import TransformPane from '@/renderer/core/layout/TransformPane.vue'
|
||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
||||
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
@@ -155,7 +147,6 @@ const workspaceStore = useWorkspaceStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const toastStore = useToastStore()
|
||||
const layoutMutations = useLayoutMutations()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
@@ -172,280 +163,43 @@ const selectionToolboxEnabled = computed(() =>
|
||||
|
||||
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
|
||||
// Feature flags (Vue-related)
|
||||
// Feature flags
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
const isVueNodesEnabled = computed(() => shouldRenderVueNodes.value)
|
||||
|
||||
// Vue node lifecycle management - initialize after graph is ready
|
||||
let nodeManager: ReturnType<typeof useGraphNodeManager> | null = null
|
||||
let cleanupNodeManager: (() => void) | null = null
|
||||
|
||||
// Slot layout sync management
|
||||
let slotSync: ReturnType<typeof useSlotLayoutSync> | null = null
|
||||
let linkSync: ReturnType<typeof useLinkLayoutSync> | null = null
|
||||
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
|
||||
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
|
||||
const nodePositions = ref<ReadonlyMap<string, { x: number; y: number }>>(
|
||||
new Map()
|
||||
// Vue node system
|
||||
const vueNodeLifecycle = useVueNodeLifecycle(isVueNodesEnabled)
|
||||
const viewportCulling = useViewportCulling(
|
||||
isVueNodesEnabled,
|
||||
vueNodeLifecycle.vueNodeData,
|
||||
vueNodeLifecycle.nodeDataTrigger,
|
||||
vueNodeLifecycle.nodeManager
|
||||
)
|
||||
const nodeSizes = ref<ReadonlyMap<string, { width: number; height: number }>>(
|
||||
new Map()
|
||||
)
|
||||
let detectChangesInRAF = () => {}
|
||||
const nodeEventHandlers = useNodeEventHandlers(vueNodeLifecycle.nodeManager)
|
||||
|
||||
// Initialize node manager when graph becomes available
|
||||
// Add a reactivity trigger to force computed re-evaluation
|
||||
const nodeDataTrigger = ref(0)
|
||||
|
||||
const initializeNodeManager = () => {
|
||||
if (!comfyApp.graph || nodeManager) return
|
||||
nodeManager = useGraphNodeManager(comfyApp.graph)
|
||||
cleanupNodeManager = nodeManager.cleanup
|
||||
// Use the manager's reactive maps directly
|
||||
vueNodeData.value = nodeManager.vueNodeData
|
||||
nodeState.value = nodeManager.nodeState
|
||||
nodePositions.value = nodeManager.nodePositions
|
||||
nodeSizes.value = nodeManager.nodeSizes
|
||||
detectChangesInRAF = nodeManager.detectChangesInRAF
|
||||
|
||||
// Initialize layout system with existing nodes
|
||||
const nodes = comfyApp.graph._nodes.map((node: any) => ({
|
||||
id: node.id.toString(),
|
||||
pos: node.pos,
|
||||
size: node.size
|
||||
}))
|
||||
layoutStore.initializeFromLiteGraph(nodes)
|
||||
|
||||
// Seed reroutes into the Layout Store so hit-testing uses the new path
|
||||
for (const reroute of comfyApp.graph.reroutes.values()) {
|
||||
const [x, y] = reroute.pos
|
||||
const parent = reroute.parentId ?? undefined
|
||||
const linkIds = Array.from(reroute.linkIds)
|
||||
layoutMutations.createReroute(reroute.id, { x, y }, parent, linkIds)
|
||||
}
|
||||
|
||||
// Seed existing links into the Layout Store (topology only)
|
||||
for (const link of comfyApp.graph._links.values()) {
|
||||
layoutMutations.createLink(
|
||||
link.id,
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot
|
||||
)
|
||||
}
|
||||
|
||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||
const { startSync } = useLayoutSync()
|
||||
startSync(canvasStore.canvas)
|
||||
|
||||
// Initialize slot layout sync for hit detection
|
||||
slotSync = useSlotLayoutSync()
|
||||
if (canvasStore.canvas) {
|
||||
slotSync.start(canvasStore.canvas as LGraphCanvas)
|
||||
}
|
||||
|
||||
// Initialize link layout sync for event-driven updates
|
||||
linkSync = useLinkLayoutSync()
|
||||
if (canvasStore.canvas) {
|
||||
linkSync.start(canvasStore.canvas as LGraphCanvas)
|
||||
}
|
||||
|
||||
// Force computed properties to re-evaluate
|
||||
nodeDataTrigger.value++
|
||||
}
|
||||
|
||||
const disposeNodeManagerAndSyncs = () => {
|
||||
if (!nodeManager) return
|
||||
try {
|
||||
cleanupNodeManager?.()
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
nodeManager = null
|
||||
cleanupNodeManager = null
|
||||
|
||||
// Clean up slot layout sync
|
||||
if (slotSync) {
|
||||
slotSync.stop()
|
||||
slotSync = null
|
||||
}
|
||||
|
||||
// Clean up link layout sync
|
||||
if (linkSync) {
|
||||
linkSync.stop()
|
||||
linkSync = null
|
||||
}
|
||||
|
||||
// Reset reactive maps to inert defaults
|
||||
vueNodeData.value = new Map()
|
||||
nodeState.value = new Map()
|
||||
nodePositions.value = new Map()
|
||||
nodeSizes.value = new Map()
|
||||
}
|
||||
|
||||
// Watch for transformPaneEnabled to gate the node manager lifecycle
|
||||
watch(
|
||||
() => isVueNodesEnabled.value && Boolean(comfyApp.graph),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
initializeNodeManager()
|
||||
} else {
|
||||
disposeNodeManagerAndSyncs()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Transform state for viewport culling
|
||||
const { syncWithCanvas } = useTransformState()
|
||||
|
||||
const nodesToRender = computed(() => {
|
||||
// Early return for zero overhead when Vue nodes are disabled
|
||||
if (!isVueNodesEnabled.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Access trigger to force re-evaluation after nodeManager initialization
|
||||
void nodeDataTrigger.value
|
||||
|
||||
if (!comfyApp.graph) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allNodes = Array.from(vueNodeData.value.values())
|
||||
|
||||
// Apply viewport culling - check if node bounds intersect with viewport
|
||||
if (nodeManager && canvasStore.canvas && comfyApp.canvas) {
|
||||
const canvas = canvasStore.canvas
|
||||
const manager = nodeManager
|
||||
|
||||
// Ensure transform is synced before checking visibility
|
||||
syncWithCanvas(comfyApp.canvas)
|
||||
|
||||
const ds = canvas.ds
|
||||
|
||||
// Work in screen space - viewport is simply the canvas element size
|
||||
const viewport_width = canvas.canvas.width
|
||||
const viewport_height = canvas.canvas.height
|
||||
|
||||
// Add margin that represents a constant distance in canvas space
|
||||
// Convert canvas units to screen pixels by multiplying by scale
|
||||
const canvasMarginDistance = 200 // Fixed margin in canvas units
|
||||
const margin_x = canvasMarginDistance * ds.scale
|
||||
const margin_y = canvasMarginDistance * ds.scale
|
||||
|
||||
const filtered = allNodes.filter((nodeData) => {
|
||||
const node = manager.getNode(nodeData.id)
|
||||
if (!node) return false
|
||||
|
||||
// Transform node position to screen space (same as DOM widgets)
|
||||
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
||||
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
||||
const screen_width = node.size[0] * ds.scale
|
||||
const screen_height = node.size[1] * ds.scale
|
||||
|
||||
// Check if node bounds intersect with expanded viewport (in screen space)
|
||||
const isVisible = !(
|
||||
screen_x + screen_width < -margin_x ||
|
||||
screen_x > viewport_width + margin_x ||
|
||||
screen_y + screen_height < -margin_y ||
|
||||
screen_y > viewport_height + margin_y
|
||||
)
|
||||
|
||||
return isVisible
|
||||
})
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
return allNodes
|
||||
})
|
||||
|
||||
let lastScale = 1
|
||||
let lastOffsetX = 0
|
||||
let lastOffsetY = 0
|
||||
const nodePositions = vueNodeLifecycle.nodePositions
|
||||
const nodeSizes = vueNodeLifecycle.nodeSizes
|
||||
const nodesToRender = viewportCulling.nodesToRender
|
||||
|
||||
const handleTransformUpdate = () => {
|
||||
// Skip all work if Vue nodes are disabled
|
||||
if (!isVueNodesEnabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync transform state only when it changes (avoids reflows)
|
||||
if (comfyApp.canvas?.ds) {
|
||||
const currentScale = comfyApp.canvas.ds.scale
|
||||
const currentOffsetX = comfyApp.canvas.ds.offset[0]
|
||||
const currentOffsetY = comfyApp.canvas.ds.offset[1]
|
||||
|
||||
if (
|
||||
currentScale !== lastScale ||
|
||||
currentOffsetX !== lastOffsetX ||
|
||||
currentOffsetY !== lastOffsetY
|
||||
) {
|
||||
syncWithCanvas(comfyApp.canvas)
|
||||
lastScale = currentScale
|
||||
lastOffsetX = currentOffsetX
|
||||
lastOffsetY = currentOffsetY
|
||||
}
|
||||
}
|
||||
|
||||
// Detect node changes during transform updates
|
||||
detectChangesInRAF()
|
||||
|
||||
// Trigger reactivity for nodesToRender
|
||||
void nodesToRender.value.length
|
||||
viewportCulling.handleTransformUpdate(
|
||||
vueNodeLifecycle.detectChangesInRAF.value
|
||||
)
|
||||
}
|
||||
const handleNodeSelect = nodeEventHandlers.handleNodeSelect
|
||||
const handleNodeCollapse = nodeEventHandlers.handleNodeCollapse
|
||||
const handleNodeTitleUpdate = nodeEventHandlers.handleNodeTitleUpdate
|
||||
|
||||
// Node event handlers
|
||||
const handleNodeSelect = (event: PointerEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager) return
|
||||
|
||||
const node = nodeManager.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
canvasStore.canvas.deselectAllNodes()
|
||||
}
|
||||
|
||||
canvasStore.canvas.selectNode(node)
|
||||
|
||||
// Bring node to front when clicked (similar to LiteGraph behavior)
|
||||
// Skip if node is pinned
|
||||
if (!node.flags?.pinned) {
|
||||
layoutMutations.setSource(LayoutSource.Vue)
|
||||
layoutMutations.bringNodeToFront(nodeData.id)
|
||||
}
|
||||
node.selected = true
|
||||
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
// Handle node collapse state changes
|
||||
const handleNodeCollapse = (nodeId: string, collapsed: boolean) => {
|
||||
if (!nodeManager) return
|
||||
|
||||
const node = nodeManager.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Use LiteGraph's collapse method if the state needs to change
|
||||
const currentCollapsed = node.flags?.collapsed ?? false
|
||||
if (currentCollapsed !== collapsed) {
|
||||
node.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle node title updates
|
||||
const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => {
|
||||
if (!nodeManager) return
|
||||
|
||||
const node = nodeManager.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Update the node title in LiteGraph for persistence
|
||||
node.title = newTitle
|
||||
}
|
||||
// Provide selection state to all Vue nodes
|
||||
const selectedNodeIds = computed(
|
||||
() =>
|
||||
new Set(
|
||||
canvasStore.selectedItems
|
||||
.filter((item) => item.id !== undefined)
|
||||
.map((item) => String(item.id))
|
||||
)
|
||||
)
|
||||
provide(SelectedNodeIdsKey, selectedNodeIds)
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
@@ -653,29 +407,7 @@ onMounted(async () => {
|
||||
|
||||
comfyAppReady.value = true
|
||||
|
||||
// Set up Vue node initialization only when enabled
|
||||
if (isVueNodesEnabled.value) {
|
||||
// Set up a one-time listener for when the first node is added
|
||||
// This handles the case where Vue nodes are enabled but the graph starts empty
|
||||
// TODO: Replace this with a reactive graph mutations observer when available
|
||||
if (comfyApp.graph && !nodeManager && comfyApp.graph._nodes.length === 0) {
|
||||
const originalOnNodeAdded = comfyApp.graph.onNodeAdded
|
||||
comfyApp.graph.onNodeAdded = function (node: any) {
|
||||
// Restore original handler
|
||||
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
||||
|
||||
// Initialize node manager if needed
|
||||
if (isVueNodesEnabled.value && !nodeManager) {
|
||||
initializeNodeManager()
|
||||
}
|
||||
|
||||
// Call original handler
|
||||
if (originalOnNodeAdded) {
|
||||
originalOnNodeAdded.call(this, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vueNodeLifecycle.setupEmptyGraphListener()
|
||||
|
||||
comfyApp.canvas.onSelectionChange = useChainCallback(
|
||||
comfyApp.canvas.onSelectionChange,
|
||||
@@ -719,17 +451,6 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (nodeManager) {
|
||||
nodeManager.cleanup()
|
||||
nodeManager = null
|
||||
}
|
||||
if (slotSync) {
|
||||
slotSync.stop()
|
||||
slotSync = null
|
||||
}
|
||||
if (linkSync) {
|
||||
linkSync.stop()
|
||||
linkSync = null
|
||||
}
|
||||
vueNodeLifecycle.cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div
|
||||
ref="toolboxRef"
|
||||
style="transform: translate(var(--tb-x), var(--tb-y))"
|
||||
class="fixed left-0 top-0 z-40"
|
||||
class="fixed left-0 top-0 z-40 pointer-events-none"
|
||||
>
|
||||
<Transition name="slide-up">
|
||||
<Panel
|
||||
v-if="visible"
|
||||
class="rounded-lg selection-toolbox"
|
||||
class="rounded-lg selection-toolbox pointer-events-auto"
|
||||
:pt="{
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
@@ -21,6 +21,7 @@
|
||||
<Load3DViewerButton />
|
||||
<MaskEditorButton />
|
||||
<ConvertToSubgraphButton />
|
||||
<PublishSubgraphButton />
|
||||
<DeleteButton />
|
||||
<RefreshSelectionButton />
|
||||
<ExtensionCommandButton
|
||||
@@ -49,6 +50,7 @@ import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewer
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
|
||||
import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'
|
||||
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
@@ -83,7 +85,6 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
<style scoped>
|
||||
.selection-toolbox {
|
||||
transform: translateX(-50%) translateY(-120%);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
|
||||
@@ -147,7 +147,8 @@ watch(
|
||||
showColorPicker.value = false
|
||||
selectedColorOption.value = null
|
||||
currentColorOption.value = getItemsColorOption(newSelectedItems)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Button
|
||||
v-show="isVisible"
|
||||
v-tooltip.top="{
|
||||
value: t('commands.Comfy_PublishSubgraph.label'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.PublishSubgraph')"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:book-open />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const isVisible = computed(() => {
|
||||
return (
|
||||
canvasStore.selectedItems?.length === 1 &&
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
</script>
|
||||
@@ -142,11 +142,12 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
|
||||
import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
|
||||
@@ -191,7 +192,6 @@ const { t, locale } = useI18n()
|
||||
const releaseStore = useReleaseStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -313,8 +313,11 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
icon: PuzzleIcon,
|
||||
label: t('helpCenter.managerExtension'),
|
||||
showRedDot: shouldShowManagerRedDot.value,
|
||||
action: () => {
|
||||
dialogService.showManagerDialog()
|
||||
action: async () => {
|
||||
await useManagerState().openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="container" class="node-lib-node-container">
|
||||
<TreeExplorerTreeNode :node="node">
|
||||
<TreeExplorerTreeNode :node="node" @contextmenu="handleContextMenu">
|
||||
<template #before-label>
|
||||
<Tag
|
||||
v-if="nodeDef.experimental"
|
||||
@@ -13,7 +13,30 @@
|
||||
severity="danger"
|
||||
/>
|
||||
</template>
|
||||
<template #actions>
|
||||
<template
|
||||
v-if="nodeDef.name.startsWith(useSubgraphStore().typePrefix)"
|
||||
#actions
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
severity="danger"
|
||||
@click.stop="deleteBlueprint"
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="editBlueprint"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:square-pen />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else #actions>
|
||||
<Button
|
||||
class="bookmark-button"
|
||||
size="small"
|
||||
@@ -40,10 +63,13 @@
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
<ContextMenu ref="menu" :model="menuItems" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import Tag from 'primevue/tag'
|
||||
import {
|
||||
CSSProperties,
|
||||
@@ -53,14 +79,18 @@ import {
|
||||
onUnmounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
openNodeHelp: (nodeDef: ComfyNodeDefImpl) => void
|
||||
@@ -80,6 +110,33 @@ const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
const toggleBookmark = async () => {
|
||||
await nodeBookmarkStore.toggleBookmark(nodeDef.value)
|
||||
}
|
||||
const editBlueprint = async () => {
|
||||
if (!props.node.data)
|
||||
throw new Error(
|
||||
'Failed to edit subgraph blueprint lacking backing node data'
|
||||
)
|
||||
await useSubgraphStore().editBlueprint(props.node.data.name)
|
||||
}
|
||||
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
const menuItems = computed<MenuItem[]>(() => {
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
label: t('g.delete'),
|
||||
icon: 'pi pi-trash',
|
||||
severity: 'error',
|
||||
command: deleteBlueprint
|
||||
}
|
||||
]
|
||||
return items
|
||||
})
|
||||
function handleContextMenu(event: Event) {
|
||||
if (!nodeDef.value.name.startsWith(useSubgraphStore().typePrefix)) return
|
||||
menu.value?.show(event)
|
||||
}
|
||||
function deleteBlueprint() {
|
||||
if (!props.node.data) return
|
||||
void useSubgraphStore().deleteBlueprint(props.node.data.name)
|
||||
}
|
||||
|
||||
const previewRef = ref<InstanceType<typeof NodePreview> | null>(null)
|
||||
const nodePreviewStyle = ref<CSSProperties>({
|
||||
|
||||
@@ -28,29 +28,7 @@
|
||||
@show="onMenuShow"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
<div
|
||||
v-if="item.key === 'theme'"
|
||||
class="flex items-center gap-4 px-4 py-5"
|
||||
@click.stop.prevent
|
||||
>
|
||||
{{ item.label }}
|
||||
<SelectButton
|
||||
:options="[darkLabel, lightLabel]"
|
||||
:model-value="activeTheme"
|
||||
@click.stop.prevent
|
||||
@update:model-value="onThemeChange"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<div class="flex items-center gap-2">
|
||||
<i v-if="option === lightLabel" class="pi pi-sun" />
|
||||
<i v-if="option === darkLabel" class="pi pi-moon" />
|
||||
<span>{{ option }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
<a
|
||||
v-else
|
||||
class="p-menubar-item-link px-4 py-2"
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
@@ -95,7 +73,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import TieredMenu, {
|
||||
type TieredMenuMethods,
|
||||
type TieredMenuState
|
||||
@@ -106,27 +83,28 @@ import { useI18n } from 'vue-i18n'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useManagerState } from '@/composables/useManagerState'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
@@ -159,36 +137,33 @@ const showSettings = (defaultPanel?: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const managerStateStore = useManagerStateStore()
|
||||
|
||||
const showManageExtensions = async () => {
|
||||
const state = managerStateStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
showSettings('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
showSettings('extension')
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
useDialogService().showManagerDialog()
|
||||
break
|
||||
}
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
}
|
||||
|
||||
const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
const themeMenuItems = computed(() => {
|
||||
return colorPaletteStore.palettes.map((palette) => ({
|
||||
key: `theme-${palette.id}`,
|
||||
label: palette.name,
|
||||
parentPath: 'theme',
|
||||
comfyCommand: {
|
||||
active: () => colorPaletteStore.activePaletteId === palette.id
|
||||
},
|
||||
command: async () => {
|
||||
await colorPaletteService.loadColorPalette(palette.id)
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
const extraMenuItems = computed(() => [
|
||||
{ separator: true },
|
||||
{
|
||||
key: 'theme',
|
||||
label: t('menu.theme')
|
||||
label: t('menu.theme'),
|
||||
items: themeMenuItems.value
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
@@ -211,19 +186,6 @@ const extraMenuItems = computed<MenuItem[]>(() => [
|
||||
}
|
||||
])
|
||||
|
||||
const lightLabel = computed(() => t('menu.light'))
|
||||
const darkLabel = computed(() => t('menu.dark'))
|
||||
|
||||
const activeTheme = computed(() => {
|
||||
return colorPaletteStore.completedActivePalette.light_theme
|
||||
? lightLabel.value
|
||||
: darkLabel.value
|
||||
})
|
||||
|
||||
const onThemeChange = async () => {
|
||||
await commandStore.execute('Comfy.ToggleTheme')
|
||||
}
|
||||
|
||||
const translatedItems = computed(() => {
|
||||
const items = menuItemsStore.menuItems.map(translateMenuItem)
|
||||
let helpIndex = items.findIndex((item) => item.key === 'Help')
|
||||
@@ -308,7 +270,12 @@ const handleItemClick = (item: MenuItem, event: MouseEvent) => {
|
||||
}
|
||||
|
||||
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
|
||||
// Check if this item has siblings with active state (either from store or theme items)
|
||||
return (
|
||||
item.parentPath &&
|
||||
(item.parentPath === 'theme' ||
|
||||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -332,6 +299,18 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.comfy-command-menu {
|
||||
--p-tieredmenu-item-focus-background: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 15%,
|
||||
transparent
|
||||
);
|
||||
--p-tieredmenu-item-active-background: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 10%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
.comfy-command-menu ul {
|
||||
background-color: var(--comfy-menu-secondary-bg) !important;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { type ComputedRef, computed } from 'vue'
|
||||
|
||||
import { type ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
export type SubcategoryRule = {
|
||||
type SubcategoryRule = {
|
||||
pattern: string | RegExp
|
||||
subcategory: string
|
||||
}
|
||||
|
||||
export type SubcategoryConfig = {
|
||||
type SubcategoryConfig = {
|
||||
defaultSubcategory: string
|
||||
rules: SubcategoryRule[]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ function intersect(a: Rect, b: Rect): [number, number, number, number] | null {
|
||||
return [x1, y1, x2 - x1, y2 - y1]
|
||||
}
|
||||
|
||||
export interface ClippingOptions {
|
||||
interface ClippingOptions {
|
||||
margin?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import type { LGraphCanvas } from '../../lib/litegraph/src/litegraph'
|
||||
|
||||
export interface CanvasTransformSyncOptions {
|
||||
interface CanvasTransformSyncOptions {
|
||||
/**
|
||||
* Whether to automatically start syncing when canvas is available
|
||||
* @default true
|
||||
@@ -10,7 +10,7 @@ export interface CanvasTransformSyncOptions {
|
||||
autoStart?: boolean
|
||||
}
|
||||
|
||||
export interface CanvasTransformSyncCallbacks {
|
||||
interface CanvasTransformSyncCallbacks {
|
||||
/**
|
||||
* Called when sync starts
|
||||
*/
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
* Vue node lifecycle management for LiteGraph integration
|
||||
* Provides event-driven reactivity with performance optimizations
|
||||
*/
|
||||
import { nextTick, reactive, readonly } from 'vue'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { type Bounds, QuadTree } from '@/renderer/core/spatial/QuadTree'
|
||||
import type { WidgetValue } from '@/types/simplifiedWidget'
|
||||
import type { SpatialIndexDebugInfo } from '@/types/spatialIndex'
|
||||
|
||||
import type { LGraph, LGraphNode } from '../../lib/litegraph/src/litegraph'
|
||||
import { type Bounds, QuadTree } from '../../utils/spatial/QuadTree'
|
||||
|
||||
export interface NodeState {
|
||||
visible: boolean
|
||||
@@ -19,14 +20,14 @@ export interface NodeState {
|
||||
culled: boolean
|
||||
}
|
||||
|
||||
export interface NodeMetadata {
|
||||
interface NodeMetadata {
|
||||
lastRenderTime: number
|
||||
cachedBounds: DOMRect | null
|
||||
lodLevel: 'high' | 'medium' | 'low'
|
||||
spatialIndex?: QuadTree<string>
|
||||
}
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
interface PerformanceMetrics {
|
||||
fps: number
|
||||
frameTime: number
|
||||
updateTime: number
|
||||
@@ -60,12 +61,12 @@ export interface VueNodeData {
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpatialMetrics {
|
||||
interface SpatialMetrics {
|
||||
queryTime: number
|
||||
nodesInIndex: number
|
||||
}
|
||||
|
||||
export interface GraphNodeManager {
|
||||
interface GraphNodeManager {
|
||||
// Reactive state - safe data extracted from LiteGraph nodes
|
||||
vueNodeData: ReadonlyMap<string, VueNodeData>
|
||||
nodeState: ReadonlyMap<string, NodeState>
|
||||
@@ -235,11 +236,15 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
// Check if it's a File array
|
||||
if (Array.isArray(value) && value.every((item) => item instanceof File)) {
|
||||
return value as File[]
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value.every((item): item is File => item instanceof File)
|
||||
) {
|
||||
return value
|
||||
}
|
||||
// Otherwise it's a generic object
|
||||
return value as object
|
||||
return value
|
||||
}
|
||||
// If none of the above, return undefined
|
||||
console.warn(`Invalid widget value type: ${typeof value}`, value)
|
||||
@@ -591,6 +596,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
|
||||
/**
|
||||
* Handles node addition to the graph - sets up Vue state and spatial indexing
|
||||
* Defers position extraction until after potential configure() calls
|
||||
*/
|
||||
const handleNodeAdded = (
|
||||
node: LGraphNode,
|
||||
@@ -604,7 +610,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
// Set up widget callbacks BEFORE extracting data (critical order)
|
||||
setupNodeWidgetCallbacks(node)
|
||||
|
||||
// Extract safe data for Vue (now with proper callbacks)
|
||||
// Extract safe data for Vue
|
||||
vueNodeData.set(id, extractVueNodeData(node))
|
||||
|
||||
// Set up reactive tracking state
|
||||
@@ -614,27 +620,48 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
lastUpdate: performance.now(),
|
||||
culled: false
|
||||
})
|
||||
nodePositions.set(id, { x: node.pos[0], y: node.pos[1] })
|
||||
nodeSizes.set(id, { width: node.size[0], height: node.size[1] })
|
||||
attachMetadata(node)
|
||||
|
||||
// Add to spatial index for viewport culling
|
||||
const bounds: Bounds = {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
width: node.size[0],
|
||||
height: node.size[1]
|
||||
const initializeVueNodeLayout = () => {
|
||||
// Extract actual positions after configure() has potentially updated them
|
||||
const nodePosition = { x: node.pos[0], y: node.pos[1] }
|
||||
const nodeSize = { width: node.size[0], height: node.size[1] }
|
||||
|
||||
nodePositions.set(id, nodePosition)
|
||||
nodeSizes.set(id, nodeSize)
|
||||
attachMetadata(node)
|
||||
|
||||
// Add to spatial index for viewport culling with final positions
|
||||
const nodeBounds: Bounds = {
|
||||
x: nodePosition.x,
|
||||
y: nodePosition.y,
|
||||
width: nodeSize.width,
|
||||
height: nodeSize.height
|
||||
}
|
||||
spatialIndex.insert(id, nodeBounds, id)
|
||||
|
||||
// Add node to layout store with final positions
|
||||
setSource(LayoutSource.Canvas)
|
||||
void createNode(id, {
|
||||
position: nodePosition,
|
||||
size: nodeSize,
|
||||
zIndex: node.order || 0,
|
||||
visible: true
|
||||
})
|
||||
}
|
||||
spatialIndex.insert(id, bounds, id)
|
||||
|
||||
// Add node to layout store
|
||||
setSource(LayoutSource.Canvas)
|
||||
void createNode(id, {
|
||||
position: { x: node.pos[0], y: node.pos[1] },
|
||||
size: { width: node.size[0], height: node.size[1] },
|
||||
zIndex: node.order || 0,
|
||||
visible: true
|
||||
})
|
||||
// Check if we're in the middle of configuring the graph (workflow loading)
|
||||
if (window.app?.configuringGraph) {
|
||||
// During workflow loading - defer layout initialization until configure completes
|
||||
// Chain our callback with any existing onAfterGraphConfigured callback
|
||||
node.onAfterGraphConfigured = useChainCallback(
|
||||
node.onAfterGraphConfigured,
|
||||
initializeVueNodeLayout
|
||||
)
|
||||
} else {
|
||||
// Not during workflow loading - initialize layout immediately
|
||||
// This handles individual node additions during normal operation
|
||||
initializeVueNodeLayout()
|
||||
}
|
||||
|
||||
// Call original callback if provided
|
||||
if (originalCallback) {
|
||||
@@ -789,16 +816,10 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
}
|
||||
|
||||
return {
|
||||
vueNodeData: readonly(vueNodeData) as ReadonlyMap<string, VueNodeData>,
|
||||
nodeState: readonly(nodeState) as ReadonlyMap<string, NodeState>,
|
||||
nodePositions: readonly(nodePositions) as ReadonlyMap<
|
||||
string,
|
||||
{ x: number; y: number }
|
||||
>,
|
||||
nodeSizes: readonly(nodeSizes) as ReadonlyMap<
|
||||
string,
|
||||
{ width: number; height: number }
|
||||
>,
|
||||
vueNodeData,
|
||||
nodeState,
|
||||
nodePositions,
|
||||
nodeSizes,
|
||||
getNode,
|
||||
setupEventListeners,
|
||||
cleanup,
|
||||
@@ -807,7 +828,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
|
||||
detectChangesInRAF,
|
||||
getVisibleNodeIds,
|
||||
performanceMetrics,
|
||||
spatialMetrics: readonly(spatialMetrics),
|
||||
spatialMetrics,
|
||||
getSpatialIndexDebugInfo: () => spatialIndex.getDebugInfo()
|
||||
}
|
||||
}
|
||||
|
||||
217
src/composables/graph/useNodeEventHandlers.ts
Normal file
217
src/composables/graph/useNodeEventHandlers.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Node Event Handlers Composable
|
||||
*
|
||||
* Handles all Vue node interaction events including:
|
||||
* - Node selection with multi-select support
|
||||
* - Node collapse/expand state management
|
||||
* - Node title editing and updates
|
||||
* - Layout mutations for visual feedback
|
||||
* - Integration with LiteGraph canvas selection system
|
||||
*/
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface NodeManager {
|
||||
getNode: (id: string) => any
|
||||
}
|
||||
|
||||
export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const layoutMutations = useLayoutMutations()
|
||||
|
||||
/**
|
||||
* Handle node selection events
|
||||
* Supports single selection and multi-select with Ctrl/Cmd
|
||||
*/
|
||||
const handleNodeSelect = (event: PointerEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
const isMultiSelect = event.ctrlKey || event.metaKey
|
||||
|
||||
if (isMultiSelect) {
|
||||
// Ctrl/Cmd+click -> toggle selection
|
||||
if (node.selected) {
|
||||
canvasStore.canvas.deselect(node)
|
||||
} else {
|
||||
canvasStore.canvas.select(node)
|
||||
}
|
||||
} else {
|
||||
// Regular click -> single select
|
||||
canvasStore.canvas.deselectAll()
|
||||
canvasStore.canvas.select(node)
|
||||
}
|
||||
|
||||
// Bring node to front when clicked (similar to LiteGraph behavior)
|
||||
// Skip if node is pinned to avoid unwanted movement
|
||||
if (!node.flags?.pinned) {
|
||||
layoutMutations.setSource(LayoutSource.Vue)
|
||||
layoutMutations.bringNodeToFront(nodeData.id)
|
||||
}
|
||||
|
||||
// Update canvas selection tracking
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node collapse/expand state changes
|
||||
* Uses LiteGraph's native collapse method for proper state management
|
||||
*/
|
||||
const handleNodeCollapse = (nodeId: string, collapsed: boolean) => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Use LiteGraph's collapse method if the state needs to change
|
||||
const currentCollapsed = node.flags?.collapsed ?? false
|
||||
if (currentCollapsed !== collapsed) {
|
||||
node.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node title updates
|
||||
* Updates the title in LiteGraph for persistence across sessions
|
||||
*/
|
||||
const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeId)
|
||||
if (!node) return
|
||||
|
||||
// Update the node title in LiteGraph for persistence
|
||||
node.title = newTitle
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node double-click events
|
||||
* Can be used for custom actions like opening node editor
|
||||
*/
|
||||
const handleNodeDoubleClick = (
|
||||
event: PointerEvent,
|
||||
nodeData: VueNodeData
|
||||
) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Prevent default browser behavior
|
||||
event.preventDefault()
|
||||
|
||||
// TODO: add custom double-click behavior here
|
||||
// For now, ensure node is selected
|
||||
if (!node.selected) {
|
||||
handleNodeSelect(event, nodeData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node right-click context menu events
|
||||
* Integrates with LiteGraph's context menu system
|
||||
*/
|
||||
const handleNodeRightClick = (event: PointerEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Prevent default context menu
|
||||
event.preventDefault()
|
||||
|
||||
// Select the node if not already selected
|
||||
if (!node.selected) {
|
||||
handleNodeSelect(event, nodeData)
|
||||
}
|
||||
|
||||
// Let LiteGraph handle the context menu
|
||||
// The canvas will handle showing the appropriate context menu
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node drag start events
|
||||
* Prepares node for dragging and sets appropriate visual state
|
||||
*/
|
||||
const handleNodeDragStart = (event: DragEvent, nodeData: VueNodeData) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return
|
||||
|
||||
// Ensure node is selected before dragging
|
||||
if (!node.selected) {
|
||||
// Create a synthetic pointer event for selection
|
||||
const syntheticEvent = new PointerEvent('pointerdown', {
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
bubbles: true
|
||||
})
|
||||
handleNodeSelect(syntheticEvent, nodeData)
|
||||
}
|
||||
|
||||
// Set drag data for potential drop operations
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('application/comfy-node-id', nodeData.id)
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch select multiple nodes
|
||||
* Useful for selection toolbox or area selection
|
||||
*/
|
||||
const selectNodes = (nodeIds: string[], addToSelection = false) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
if (!addToSelection) {
|
||||
canvasStore.canvas.deselectAllNodes()
|
||||
}
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
const node = nodeManager.value?.getNode(nodeId)
|
||||
if (node && canvasStore.canvas) {
|
||||
canvasStore.canvas.selectNode(node)
|
||||
node.selected = true
|
||||
}
|
||||
})
|
||||
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect specific nodes
|
||||
*/
|
||||
const deselectNodes = (nodeIds: string[]) => {
|
||||
if (!canvasStore.canvas || !nodeManager.value) return
|
||||
|
||||
nodeIds.forEach((nodeId) => {
|
||||
const node = nodeManager.value?.getNode(nodeId)
|
||||
if (node) {
|
||||
node.selected = false
|
||||
}
|
||||
})
|
||||
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
|
||||
return {
|
||||
// Core event handlers
|
||||
handleNodeSelect,
|
||||
handleNodeCollapse,
|
||||
handleNodeTitleUpdate,
|
||||
handleNodeDoubleClick,
|
||||
handleNodeRightClick,
|
||||
handleNodeDragStart,
|
||||
|
||||
// Batch operations
|
||||
selectNodes,
|
||||
deselectNodes
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
/**
|
||||
* Composable for spatial indexing of nodes using QuadTree
|
||||
* Integrates with useGraphNodeManager for efficient viewport culling
|
||||
*/
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
import { type Bounds, QuadTree } from '@/utils/spatial/QuadTree'
|
||||
|
||||
export interface SpatialIndexOptions {
|
||||
worldBounds?: Bounds
|
||||
maxDepth?: number
|
||||
maxItemsPerNode?: number
|
||||
updateDebounceMs?: number
|
||||
}
|
||||
|
||||
interface SpatialMetrics {
|
||||
queryTime: number
|
||||
totalNodes: number
|
||||
visibleNodes: number
|
||||
treeDepth: number
|
||||
rebuildCount: number
|
||||
}
|
||||
|
||||
export const useSpatialIndex = (options: SpatialIndexOptions = {}) => {
|
||||
// Default world bounds (can be expanded dynamically)
|
||||
const defaultBounds: Bounds = {
|
||||
x: -10000,
|
||||
y: -10000,
|
||||
width: 20000,
|
||||
height: 20000
|
||||
}
|
||||
|
||||
// QuadTree instance
|
||||
const quadTree = ref<QuadTree<string> | null>(null)
|
||||
|
||||
// Performance metrics
|
||||
const metrics = reactive<SpatialMetrics>({
|
||||
queryTime: 0,
|
||||
totalNodes: 0,
|
||||
visibleNodes: 0,
|
||||
treeDepth: 0,
|
||||
rebuildCount: 0
|
||||
})
|
||||
|
||||
// Initialize QuadTree
|
||||
const initialize = (bounds: Bounds = defaultBounds) => {
|
||||
quadTree.value = new QuadTree<string>(bounds, {
|
||||
maxDepth: options.maxDepth ?? 6,
|
||||
maxItemsPerNode: options.maxItemsPerNode ?? 4
|
||||
})
|
||||
metrics.rebuildCount++
|
||||
}
|
||||
|
||||
// Add or update node in spatial index
|
||||
const updateNode = (
|
||||
nodeId: string,
|
||||
position: { x: number; y: number },
|
||||
size: { width: number; height: number }
|
||||
) => {
|
||||
if (!quadTree.value) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
const bounds: Bounds = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: size.width,
|
||||
height: size.height
|
||||
}
|
||||
|
||||
// Use insert instead of update - insert handles both new and existing nodes
|
||||
quadTree.value!.insert(nodeId, bounds, nodeId)
|
||||
metrics.totalNodes = quadTree.value!.size
|
||||
}
|
||||
|
||||
// Batch update for multiple nodes
|
||||
const batchUpdate = (
|
||||
updates: Array<{
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
size: { width: number; height: number }
|
||||
}>
|
||||
) => {
|
||||
if (!quadTree.value) {
|
||||
initialize()
|
||||
}
|
||||
|
||||
for (const update of updates) {
|
||||
const bounds: Bounds = {
|
||||
x: update.position.x,
|
||||
y: update.position.y,
|
||||
width: update.size.width,
|
||||
height: update.size.height
|
||||
}
|
||||
// Use insert instead of update - insert handles both new and existing nodes
|
||||
quadTree.value!.insert(update.id, bounds, update.id)
|
||||
}
|
||||
|
||||
metrics.totalNodes = quadTree.value!.size
|
||||
}
|
||||
|
||||
// Remove node from spatial index
|
||||
const removeNode = (nodeId: string) => {
|
||||
if (!quadTree.value) return
|
||||
|
||||
quadTree.value.remove(nodeId)
|
||||
metrics.totalNodes = quadTree.value.size
|
||||
}
|
||||
|
||||
// Query nodes within viewport bounds
|
||||
const queryViewport = (viewportBounds: Bounds): string[] => {
|
||||
if (!quadTree.value) return []
|
||||
|
||||
const startTime = performance.now()
|
||||
const nodeIds = quadTree.value.query(viewportBounds)
|
||||
const queryTime = performance.now() - startTime
|
||||
|
||||
metrics.queryTime = queryTime
|
||||
metrics.visibleNodes = nodeIds.length
|
||||
|
||||
return nodeIds
|
||||
}
|
||||
|
||||
// Get nodes within a radius (for proximity queries)
|
||||
const queryRadius = (
|
||||
center: { x: number; y: number },
|
||||
radius: number
|
||||
): string[] => {
|
||||
if (!quadTree.value) return []
|
||||
|
||||
const bounds: Bounds = {
|
||||
x: center.x - radius,
|
||||
y: center.y - radius,
|
||||
width: radius * 2,
|
||||
height: radius * 2
|
||||
}
|
||||
|
||||
return quadTree.value.query(bounds)
|
||||
}
|
||||
|
||||
// Clear all nodes
|
||||
const clear = () => {
|
||||
if (!quadTree.value) return
|
||||
|
||||
quadTree.value.clear()
|
||||
metrics.totalNodes = 0
|
||||
metrics.visibleNodes = 0
|
||||
}
|
||||
|
||||
// Rebuild tree (useful after major layout changes)
|
||||
const rebuild = (
|
||||
nodes: Map<
|
||||
string,
|
||||
{
|
||||
position: { x: number; y: number }
|
||||
size: { width: number; height: number }
|
||||
}
|
||||
>
|
||||
) => {
|
||||
initialize()
|
||||
|
||||
const updates = Array.from(nodes.entries()).map(([id, data]) => ({
|
||||
id,
|
||||
position: data.position,
|
||||
size: data.size
|
||||
}))
|
||||
|
||||
batchUpdate(updates)
|
||||
}
|
||||
|
||||
// Debounced update for performance
|
||||
const debouncedUpdateNode = useDebounceFn(
|
||||
updateNode,
|
||||
options.updateDebounceMs ?? 16
|
||||
)
|
||||
|
||||
return {
|
||||
// Core functions
|
||||
initialize,
|
||||
updateNode,
|
||||
batchUpdate,
|
||||
removeNode,
|
||||
queryViewport,
|
||||
queryRadius,
|
||||
clear,
|
||||
rebuild,
|
||||
|
||||
// Debounced version for high-frequency updates
|
||||
debouncedUpdateNode,
|
||||
|
||||
// Metrics
|
||||
metrics: computed(() => metrics),
|
||||
|
||||
// Direct access to QuadTree (for advanced usage)
|
||||
quadTree: computed(() => quadTree.value)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useDebounceFn, useEventListener, useThrottleFn } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
export interface TransformSettlingOptions {
|
||||
interface TransformSettlingOptions {
|
||||
/**
|
||||
* Delay in ms before transform is considered "settled" after last interaction
|
||||
* @default 200
|
||||
|
||||
212
src/composables/graph/useViewportCulling.ts
Normal file
212
src/composables/graph/useViewportCulling.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Viewport Culling Composable
|
||||
*
|
||||
* Handles viewport culling optimization for Vue nodes including:
|
||||
* - Transform state synchronization
|
||||
* - Visible node calculation with screen space transforms
|
||||
* - Adaptive margin computation based on zoom level
|
||||
* - Performance optimizations for large graphs
|
||||
*/
|
||||
import { type Ref, computed, readonly, ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useTransformState } from '@/renderer/core/layout/useTransformState'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface NodeManager {
|
||||
getNode: (id: string) => any
|
||||
}
|
||||
|
||||
export function useViewportCulling(
|
||||
isVueNodesEnabled: Ref<boolean>,
|
||||
vueNodeData: Ref<ReadonlyMap<string, VueNodeData>>,
|
||||
nodeDataTrigger: Ref<number>,
|
||||
nodeManager: Ref<NodeManager | null>
|
||||
) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const { syncWithCanvas } = useTransformState()
|
||||
|
||||
// Transform tracking for performance optimization
|
||||
const lastScale = ref(1)
|
||||
const lastOffsetX = ref(0)
|
||||
const lastOffsetY = ref(0)
|
||||
|
||||
// Current transform state
|
||||
const currentTransformState = computed(() => ({
|
||||
scale: lastScale.value,
|
||||
offsetX: lastOffsetX.value,
|
||||
offsetY: lastOffsetY.value
|
||||
}))
|
||||
|
||||
/**
|
||||
* Computed property that returns nodes visible in the current viewport
|
||||
* Implements sophisticated culling algorithm with adaptive margins
|
||||
*/
|
||||
const nodesToRender = computed(() => {
|
||||
if (!isVueNodesEnabled.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Access trigger to force re-evaluation after nodeManager initialization
|
||||
void nodeDataTrigger.value
|
||||
|
||||
if (!comfyApp.graph) {
|
||||
return []
|
||||
}
|
||||
|
||||
const allNodes = Array.from(vueNodeData.value.values())
|
||||
|
||||
// Apply viewport culling - check if node bounds intersect with viewport
|
||||
// TODO: use quadtree
|
||||
if (nodeManager.value && canvasStore.canvas && comfyApp.canvas) {
|
||||
const canvas = canvasStore.canvas
|
||||
const manager = nodeManager.value
|
||||
|
||||
// Ensure transform is synced before checking visibility
|
||||
syncWithCanvas(comfyApp.canvas)
|
||||
|
||||
const ds = canvas.ds
|
||||
|
||||
// Work in screen space - viewport is simply the canvas element size
|
||||
const viewport_width = canvas.canvas.width
|
||||
const viewport_height = canvas.canvas.height
|
||||
|
||||
// Add margin that represents a constant distance in canvas space
|
||||
// Convert canvas units to screen pixels by multiplying by scale
|
||||
const canvasMarginDistance = 200 // Fixed margin in canvas units
|
||||
const margin_x = canvasMarginDistance * ds.scale
|
||||
const margin_y = canvasMarginDistance * ds.scale
|
||||
|
||||
const filtered = allNodes.filter((nodeData) => {
|
||||
const node = manager.getNode(nodeData.id)
|
||||
if (!node) return false
|
||||
|
||||
// Transform node position to screen space (same as DOM widgets)
|
||||
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
||||
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
||||
const screen_width = node.size[0] * ds.scale
|
||||
const screen_height = node.size[1] * ds.scale
|
||||
|
||||
// Check if node bounds intersect with expanded viewport (in screen space)
|
||||
const isVisible = !(
|
||||
screen_x + screen_width < -margin_x ||
|
||||
screen_x > viewport_width + margin_x ||
|
||||
screen_y + screen_height < -margin_y ||
|
||||
screen_y > viewport_height + margin_y
|
||||
)
|
||||
|
||||
return isVisible
|
||||
})
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
return allNodes
|
||||
})
|
||||
|
||||
/**
|
||||
* Handle transform updates with performance optimization
|
||||
* Only syncs when transform actually changes to avoid unnecessary reflows
|
||||
*/
|
||||
const handleTransformUpdate = (detectChangesInRAF: () => void) => {
|
||||
// Skip all work if Vue nodes are disabled
|
||||
if (!isVueNodesEnabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync transform state only when it changes (avoids reflows)
|
||||
if (comfyApp.canvas?.ds) {
|
||||
const currentScale = comfyApp.canvas.ds.scale
|
||||
const currentOffsetX = comfyApp.canvas.ds.offset[0]
|
||||
const currentOffsetY = comfyApp.canvas.ds.offset[1]
|
||||
|
||||
if (
|
||||
currentScale !== lastScale.value ||
|
||||
currentOffsetX !== lastOffsetX.value ||
|
||||
currentOffsetY !== lastOffsetY.value
|
||||
) {
|
||||
syncWithCanvas(comfyApp.canvas)
|
||||
lastScale.value = currentScale
|
||||
lastOffsetX.value = currentOffsetX
|
||||
lastOffsetY.value = currentOffsetY
|
||||
}
|
||||
}
|
||||
|
||||
// Detect node changes during transform updates
|
||||
detectChangesInRAF()
|
||||
|
||||
// Trigger reactivity for nodesToRender
|
||||
void nodesToRender.value.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate if a specific node is visible in viewport
|
||||
* Useful for individual node visibility checks
|
||||
*/
|
||||
const isNodeVisible = (nodeData: VueNodeData): boolean => {
|
||||
if (!nodeManager.value || !canvasStore.canvas || !comfyApp.canvas) {
|
||||
return true // Default to visible if culling not available
|
||||
}
|
||||
|
||||
const canvas = canvasStore.canvas
|
||||
const node = nodeManager.value.getNode(nodeData.id)
|
||||
if (!node) return false
|
||||
|
||||
syncWithCanvas(comfyApp.canvas)
|
||||
const ds = canvas.ds
|
||||
|
||||
const viewport_width = canvas.canvas.width
|
||||
const viewport_height = canvas.canvas.height
|
||||
const canvasMarginDistance = 200
|
||||
const margin_x = canvasMarginDistance * ds.scale
|
||||
const margin_y = canvasMarginDistance * ds.scale
|
||||
|
||||
const screen_x = (node.pos[0] + ds.offset[0]) * ds.scale
|
||||
const screen_y = (node.pos[1] + ds.offset[1]) * ds.scale
|
||||
const screen_width = node.size[0] * ds.scale
|
||||
const screen_height = node.size[1] * ds.scale
|
||||
|
||||
return !(
|
||||
screen_x + screen_width < -margin_x ||
|
||||
screen_x > viewport_width + margin_x ||
|
||||
screen_y + screen_height < -margin_y ||
|
||||
screen_y > viewport_height + margin_y
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get viewport bounds information for debugging
|
||||
*/
|
||||
const getViewportInfo = () => {
|
||||
if (!canvasStore.canvas || !comfyApp.canvas) {
|
||||
return null
|
||||
}
|
||||
|
||||
const canvas = canvasStore.canvas
|
||||
const ds = canvas.ds
|
||||
|
||||
return {
|
||||
viewport_width: canvas.canvas.width,
|
||||
viewport_height: canvas.canvas.height,
|
||||
scale: ds.scale,
|
||||
offset: [ds.offset[0], ds.offset[1]],
|
||||
margin_distance: 200,
|
||||
margin_x: 200 * ds.scale,
|
||||
margin_y: 200 * ds.scale
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodesToRender,
|
||||
handleTransformUpdate,
|
||||
isNodeVisible,
|
||||
getViewportInfo,
|
||||
|
||||
// Transform state
|
||||
currentTransformState: readonly(currentTransformState),
|
||||
lastScale: readonly(lastScale),
|
||||
lastOffsetX: readonly(lastOffsetX),
|
||||
lastOffsetY: readonly(lastOffsetY)
|
||||
}
|
||||
}
|
||||
246
src/composables/graph/useVueNodeLifecycle.ts
Normal file
246
src/composables/graph/useVueNodeLifecycle.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Vue Node Lifecycle Management Composable
|
||||
*
|
||||
* Handles the complete lifecycle of Vue node rendering system including:
|
||||
* - Node manager initialization and cleanup
|
||||
* - Layout store synchronization
|
||||
* - Slot and link sync management
|
||||
* - Reactive state management for node data, positions, and sizes
|
||||
* - Memory management and proper cleanup
|
||||
*/
|
||||
import { type Ref, computed, readonly, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type {
|
||||
NodeState,
|
||||
VueNodeData
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
||||
import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync'
|
||||
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
export function useVueNodeLifecycle(isVueNodesEnabled: Ref<boolean>) {
|
||||
const canvasStore = useCanvasStore()
|
||||
const layoutMutations = useLayoutMutations()
|
||||
|
||||
const nodeManager = shallowRef<ReturnType<typeof useGraphNodeManager> | null>(
|
||||
null
|
||||
)
|
||||
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
||||
|
||||
// Sync management
|
||||
const slotSync = shallowRef<ReturnType<typeof useSlotLayoutSync> | null>(null)
|
||||
const slotSyncStarted = ref(false)
|
||||
const linkSync = shallowRef<ReturnType<typeof useLinkLayoutSync> | null>(null)
|
||||
|
||||
// Vue node data state
|
||||
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
|
||||
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
|
||||
const nodePositions = ref<ReadonlyMap<string, { x: number; y: number }>>(
|
||||
new Map()
|
||||
)
|
||||
const nodeSizes = ref<ReadonlyMap<string, { width: number; height: number }>>(
|
||||
new Map()
|
||||
)
|
||||
|
||||
// Change detection function
|
||||
const detectChangesInRAF = ref<() => void>(() => {})
|
||||
|
||||
// Trigger for forcing computed re-evaluation
|
||||
const nodeDataTrigger = ref(0)
|
||||
|
||||
const isNodeManagerReady = computed(() => nodeManager.value !== null)
|
||||
|
||||
const initializeNodeManager = () => {
|
||||
if (!comfyApp.graph || nodeManager.value) return
|
||||
|
||||
// Initialize the core node manager
|
||||
const manager = useGraphNodeManager(comfyApp.graph)
|
||||
nodeManager.value = manager
|
||||
cleanupNodeManager.value = manager.cleanup
|
||||
|
||||
// Use the manager's data maps
|
||||
vueNodeData.value = manager.vueNodeData
|
||||
nodeState.value = manager.nodeState
|
||||
nodePositions.value = manager.nodePositions
|
||||
nodeSizes.value = manager.nodeSizes
|
||||
detectChangesInRAF.value = manager.detectChangesInRAF
|
||||
|
||||
// Initialize layout system with existing nodes
|
||||
const nodes = comfyApp.graph._nodes.map((node: LGraphNode) => ({
|
||||
id: node.id.toString(),
|
||||
pos: [node.pos[0], node.pos[1]] as [number, number],
|
||||
size: [node.size[0], node.size[1]] as [number, number]
|
||||
}))
|
||||
layoutStore.initializeFromLiteGraph(nodes)
|
||||
|
||||
// Seed reroutes into the Layout Store so hit-testing uses the new path
|
||||
for (const reroute of comfyApp.graph.reroutes.values()) {
|
||||
const [x, y] = reroute.pos
|
||||
const parent = reroute.parentId ?? undefined
|
||||
const linkIds = Array.from(reroute.linkIds)
|
||||
layoutMutations.createReroute(reroute.id, { x, y }, parent, linkIds)
|
||||
}
|
||||
|
||||
// Seed existing links into the Layout Store (topology only)
|
||||
for (const link of comfyApp.graph._links.values()) {
|
||||
layoutMutations.createLink(
|
||||
link.id,
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot
|
||||
)
|
||||
}
|
||||
|
||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||
const { startSync } = useLayoutSync()
|
||||
startSync(canvasStore.canvas)
|
||||
|
||||
// Initialize link layout sync for event-driven updates
|
||||
const linkSyncManager = useLinkLayoutSync()
|
||||
linkSync.value = linkSyncManager
|
||||
if (comfyApp.canvas) {
|
||||
linkSyncManager.start(comfyApp.canvas)
|
||||
}
|
||||
|
||||
// Force computed properties to re-evaluate
|
||||
nodeDataTrigger.value++
|
||||
}
|
||||
|
||||
const disposeNodeManagerAndSyncs = () => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
try {
|
||||
cleanupNodeManager.value?.()
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
nodeManager.value = null
|
||||
cleanupNodeManager.value = null
|
||||
|
||||
// Clean up link layout sync
|
||||
if (linkSync.value) {
|
||||
linkSync.value.stop()
|
||||
linkSync.value = null
|
||||
}
|
||||
|
||||
// Reset reactive maps to clean state
|
||||
vueNodeData.value = new Map()
|
||||
nodeState.value = new Map()
|
||||
nodePositions.value = new Map()
|
||||
nodeSizes.value = new Map()
|
||||
|
||||
// Reset change detection function
|
||||
detectChangesInRAF.value = () => {}
|
||||
}
|
||||
|
||||
// Watch for Vue nodes enabled state changes
|
||||
watch(
|
||||
() => isVueNodesEnabled.value && Boolean(comfyApp.graph),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
initializeNodeManager()
|
||||
} else {
|
||||
disposeNodeManagerAndSyncs()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Consolidated watch for slot layout sync management
|
||||
watch(
|
||||
[() => canvasStore.canvas, () => isVueNodesEnabled.value],
|
||||
([canvas, vueMode], [, oldVueMode]) => {
|
||||
const modeChanged = vueMode !== oldVueMode
|
||||
|
||||
// Clear stale slot layouts when switching modes
|
||||
if (modeChanged) {
|
||||
layoutStore.clearAllSlotLayouts()
|
||||
}
|
||||
|
||||
// Switching to Vue
|
||||
if (vueMode && slotSyncStarted.value) {
|
||||
slotSync.value?.stop()
|
||||
slotSyncStarted.value = false
|
||||
}
|
||||
|
||||
// Switching to LG
|
||||
const shouldRun = Boolean(canvas?.graph) && !vueMode
|
||||
if (shouldRun && !slotSyncStarted.value && canvas) {
|
||||
// Initialize slot sync if not already created
|
||||
if (!slotSync.value) {
|
||||
slotSync.value = useSlotLayoutSync()
|
||||
}
|
||||
const started = slotSync.value.attemptStart(canvas as LGraphCanvas)
|
||||
slotSyncStarted.value = started
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Handle case where Vue nodes are enabled but graph starts empty
|
||||
const setupEmptyGraphListener = () => {
|
||||
if (
|
||||
isVueNodesEnabled.value &&
|
||||
comfyApp.graph &&
|
||||
!nodeManager.value &&
|
||||
comfyApp.graph._nodes.length === 0
|
||||
) {
|
||||
const originalOnNodeAdded = comfyApp.graph.onNodeAdded
|
||||
comfyApp.graph.onNodeAdded = function (node: LGraphNode) {
|
||||
// Restore original handler
|
||||
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
||||
|
||||
// Initialize node manager if needed
|
||||
if (isVueNodesEnabled.value && !nodeManager.value) {
|
||||
initializeNodeManager()
|
||||
}
|
||||
|
||||
// Call original handler
|
||||
if (originalOnNodeAdded) {
|
||||
originalOnNodeAdded.call(this, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function for component unmounting
|
||||
const cleanup = () => {
|
||||
if (nodeManager.value) {
|
||||
nodeManager.value.cleanup()
|
||||
nodeManager.value = null
|
||||
}
|
||||
if (slotSyncStarted.value) {
|
||||
slotSync.value?.stop()
|
||||
slotSyncStarted.value = false
|
||||
}
|
||||
slotSync.value = null
|
||||
if (linkSync.value) {
|
||||
linkSync.value.stop()
|
||||
linkSync.value = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vueNodeData,
|
||||
nodeState,
|
||||
nodePositions,
|
||||
nodeSizes,
|
||||
nodeDataTrigger: readonly(nodeDataTrigger),
|
||||
nodeManager: readonly(nodeManager),
|
||||
detectChangesInRAF: readonly(detectChangesInRAF),
|
||||
isNodeManagerReady,
|
||||
|
||||
// Lifecycle methods
|
||||
initializeNodeManager,
|
||||
disposeNodeManagerAndSyncs,
|
||||
setupEmptyGraphListener,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,7 @@ import { type Ref, ref, watch } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
|
||||
export interface UseWidgetValueOptions<
|
||||
T extends WidgetValue = WidgetValue,
|
||||
U = T
|
||||
> {
|
||||
interface UseWidgetValueOptions<T extends WidgetValue = WidgetValue, U = T> {
|
||||
/** The widget configuration from LiteGraph */
|
||||
widget: SimplifiedWidget<T>
|
||||
/** The current value from parent component */
|
||||
@@ -22,10 +19,7 @@ export interface UseWidgetValueOptions<
|
||||
transform?: (value: U) => T
|
||||
}
|
||||
|
||||
export interface UseWidgetValueReturn<
|
||||
T extends WidgetValue = WidgetValue,
|
||||
U = T
|
||||
> {
|
||||
interface UseWidgetValueReturn<T extends WidgetValue = WidgetValue, U = T> {
|
||||
/** Local value for immediate UI updates */
|
||||
localValue: Ref<T>
|
||||
/** Handler for user interactions */
|
||||
|
||||
@@ -35,7 +35,7 @@ const createContainer = () => {
|
||||
const createTimeout = (ms: number) =>
|
||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), ms))
|
||||
|
||||
export const useNodePreview = <T extends MediaElement>(
|
||||
const useNodePreview = <T extends MediaElement>(
|
||||
node: LGraphNode,
|
||||
options: NodePreviewOptions<T>
|
||||
) => {
|
||||
|
||||
@@ -1031,6 +1031,15 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
StabilityUpscaleFastNode: {
|
||||
displayPrice: '$0.01/Run'
|
||||
},
|
||||
StabilityTextToAudio: {
|
||||
displayPrice: '$0.20/Run'
|
||||
},
|
||||
StabilityAudioToAudio: {
|
||||
displayPrice: '$0.20/Run'
|
||||
},
|
||||
StabilityAudioInpaint: {
|
||||
displayPrice: '$0.20/Run'
|
||||
},
|
||||
VeoVideoGenerationNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
const durationWidget = node.widgets?.find(
|
||||
@@ -1053,7 +1062,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
) as IComboWidget
|
||||
|
||||
if (!modelWidget || !generateAudioWidget) {
|
||||
return '$2.00-6.00/Run (varies with model & audio generation)'
|
||||
return '$0.80-3.20/Run (varies with model & audio generation)'
|
||||
}
|
||||
|
||||
const model = String(modelWidget.value)
|
||||
@@ -1061,13 +1070,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
String(generateAudioWidget.value).toLowerCase() === 'true'
|
||||
|
||||
if (model.includes('veo-3.0-fast-generate-001')) {
|
||||
return generateAudio ? '$3.20/Run' : '$2.00/Run'
|
||||
return generateAudio ? '$1.20/Run' : '$0.80/Run'
|
||||
} else if (model.includes('veo-3.0-generate-001')) {
|
||||
return generateAudio ? '$6.00/Run' : '$4.00/Run'
|
||||
return generateAudio ? '$3.20/Run' : '$1.60/Run'
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return '$2.00-6.00/Run'
|
||||
return '$0.80-3.20/Run'
|
||||
}
|
||||
},
|
||||
LumaImageNode: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type ComputedRef, ref } from 'vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
export interface UseComputedWithWidgetWatchOptions {
|
||||
interface UseComputedWithWidgetWatchOptions {
|
||||
/**
|
||||
* Names of widgets to observe for changes.
|
||||
* If not provided, all widgets will be observed.
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type NodePack = components['schemas']['Node']
|
||||
|
||||
export type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
|
||||
type SelectionState = 'all-installed' | 'none-installed' | 'mixed'
|
||||
|
||||
/**
|
||||
* Composable for managing multi-package selection states
|
||||
|
||||
@@ -61,7 +61,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => {
|
||||
const nodeDef = nodeDefStore.nodeDefsByName[nodeName]
|
||||
if (nodeDef?.nodeSource.type === 'core') {
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
await systemStatsStore.refetchSystemStats()
|
||||
}
|
||||
return {
|
||||
id: CORE_NODES_PACK_NAME,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { paramsToCacheKey } from '@/utils/formatUtil'
|
||||
|
||||
const DEFAULT_MAX_SIZE = 50
|
||||
|
||||
export interface CachedRequestOptions {
|
||||
interface CachedRequestOptions {
|
||||
/**
|
||||
* Maximum number of items to store in the cache
|
||||
* @default 50
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { until } from '@vueuse/core'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue'
|
||||
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
NodePackRequirements,
|
||||
SystemEnvironment
|
||||
} from '@/types/conflictDetectionTypes'
|
||||
import { normalizePackId } from '@/utils/packUtils'
|
||||
import {
|
||||
cleanVersion,
|
||||
satisfiesVersion,
|
||||
@@ -78,9 +80,8 @@ export function useConflictDetection() {
|
||||
try {
|
||||
// Get system stats from store (primary source of system information)
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
if (!systemStatsStore.systemStats) {
|
||||
await systemStatsStore.fetchSystemStats()
|
||||
}
|
||||
// Wait for systemStats to be initialized if not already
|
||||
await until(systemStatsStore.isInitialized)
|
||||
|
||||
// Fetch version information from backend (with error resilience)
|
||||
const [frontendVersion] = await Promise.allSettled([
|
||||
@@ -127,7 +128,7 @@ export function useConflictDetection() {
|
||||
}
|
||||
|
||||
systemEnvironment.value = environment
|
||||
console.log(
|
||||
console.debug(
|
||||
'[ConflictDetection] System environment detection completed:',
|
||||
environment
|
||||
)
|
||||
@@ -427,7 +428,7 @@ export function useConflictDetection() {
|
||||
Object.entries(bulkResult).forEach(([packageId, failInfo]) => {
|
||||
if (failInfo !== null) {
|
||||
importFailures[packageId] = failInfo
|
||||
console.log(
|
||||
console.debug(
|
||||
`[ConflictDetection] Import failure found for ${packageId}:`,
|
||||
failInfo
|
||||
)
|
||||
@@ -500,7 +501,7 @@ export function useConflictDetection() {
|
||||
*/
|
||||
async function performConflictDetection(): Promise<ConflictDetectionResponse> {
|
||||
if (isDetecting.value) {
|
||||
console.log('[ConflictDetection] Already detecting, skipping')
|
||||
console.debug('[ConflictDetection] Already detecting, skipping')
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Already detecting conflicts',
|
||||
@@ -556,7 +557,10 @@ export function useConflictDetection() {
|
||||
detectionSummary.value = summary
|
||||
lastDetectionTime.value = new Date().toISOString()
|
||||
|
||||
console.log('[ConflictDetection] Conflict detection completed:', summary)
|
||||
console.debug(
|
||||
'[ConflictDetection] Conflict detection completed:',
|
||||
summary
|
||||
)
|
||||
|
||||
// Store conflict results for later UI display
|
||||
// Dialog will be shown based on specific events, not on app mount
|
||||
@@ -568,7 +572,7 @@ export function useConflictDetection() {
|
||||
// Merge conflicts for packages with the same name
|
||||
const mergedConflicts = mergeConflictsByPackageName(conflictedResults)
|
||||
|
||||
console.log(
|
||||
console.debug(
|
||||
'[ConflictDetection] Conflicts detected (stored for UI):',
|
||||
mergedConflicts
|
||||
)
|
||||
@@ -632,11 +636,22 @@ export function useConflictDetection() {
|
||||
/**
|
||||
* Error-resilient initialization (called on app mount).
|
||||
* Async function that doesn't block UI setup.
|
||||
* Ensures proper order: installed -> system_stats -> versions bulk -> import_fail_info_bulk
|
||||
* Ensures proper order: system_stats -> manager state -> installed -> versions bulk -> import_fail_info_bulk
|
||||
*/
|
||||
async function initializeConflictDetection(): Promise<void> {
|
||||
try {
|
||||
// Simply perform conflict detection
|
||||
// Check if manager is new Manager before proceeding
|
||||
const { useManagerState } = await import('@/composables/useManagerState')
|
||||
const managerState = useManagerState()
|
||||
|
||||
if (!managerState.isNewManagerUI.value) {
|
||||
console.debug(
|
||||
'[ConflictDetection] Manager is not new Manager, skipping conflict detection'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Manager is new Manager, perform conflict detection
|
||||
// The useInstalledPacks will handle fetching installed list if needed
|
||||
await performConflictDetection()
|
||||
} catch (error) {
|
||||
@@ -671,13 +686,13 @@ export function useConflictDetection() {
|
||||
* Check if conflicts should trigger modal display after "What's New" dismissal
|
||||
*/
|
||||
async function shouldShowConflictModalAfterUpdate(): Promise<boolean> {
|
||||
console.log(
|
||||
console.debug(
|
||||
'[ConflictDetection] Checking if conflict modal should show after update...'
|
||||
)
|
||||
|
||||
// Ensure conflict detection has run
|
||||
if (detectionResults.value.length === 0) {
|
||||
console.log(
|
||||
console.debug(
|
||||
'[ConflictDetection] No detection results, running conflict detection...'
|
||||
)
|
||||
await performConflictDetection()
|
||||
@@ -689,7 +704,7 @@ export function useConflictDetection() {
|
||||
const hasActualConflicts = hasConflicts.value
|
||||
const canShowModal = acknowledgment.shouldShowConflictModal.value
|
||||
|
||||
console.log('[ConflictDetection] Modal check:', {
|
||||
console.debug('[ConflictDetection] Modal check:', {
|
||||
hasConflicts: hasActualConflicts,
|
||||
canShowModal: canShowModal,
|
||||
conflictedPackagesCount: conflictedPackages.value.length
|
||||
@@ -860,9 +875,7 @@ function mergeConflictsByPackageName(
|
||||
|
||||
conflicts.forEach((conflict) => {
|
||||
// Normalize package name by removing version suffix (@1_0_3) for consistent merging
|
||||
const normalizedPackageName = conflict.package_name.includes('@')
|
||||
? conflict.package_name.substring(0, conflict.package_name.indexOf('@'))
|
||||
: conflict.package_name
|
||||
const normalizedPackageName = normalizePackId(conflict.package_name)
|
||||
|
||||
if (mergedMap.has(normalizedPackageName)) {
|
||||
// Package already exists, merge conflicts
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { ManagerUIState, useManagerState } from '@/composables/useManagerState'
|
||||
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
|
||||
import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
@@ -20,18 +21,14 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import {
|
||||
ManagerUIState,
|
||||
useManagerStateStore
|
||||
} from '@/stores/managerStateStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { type ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
@@ -117,6 +114,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await workflowService.saveWorkflow(workflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.PublishSubgraph',
|
||||
icon: 'pi pi-save',
|
||||
label: 'Publish Subgraph',
|
||||
menubarLabel: 'Publish',
|
||||
function: async () => {
|
||||
await useSubgraphStore().publishSubgraph()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.SaveWorkflowAs',
|
||||
icon: 'pi pi-save',
|
||||
@@ -186,8 +192,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const subgraph = app.canvas.subgraph
|
||||
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
|
||||
nonIoNodes.forEach((node) => subgraph.remove(node))
|
||||
} else {
|
||||
app.graph.clear()
|
||||
}
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
@@ -734,34 +738,9 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: async () => {
|
||||
const managerState = useManagerStateStore().managerUIState
|
||||
|
||||
switch (managerState) {
|
||||
case ManagerUIState.DISABLED:
|
||||
dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('error', error)
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog()
|
||||
break
|
||||
}
|
||||
await useManagerState().openManager({
|
||||
showToastOnLegacyError: true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -769,33 +748,25 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
icon: 'pi pi-sync',
|
||||
label: 'Check for Custom Node Updates',
|
||||
versionAdded: '1.17.0',
|
||||
function: () => {
|
||||
const managerStore = useManagerStateStore()
|
||||
const state = managerStore.managerUIState
|
||||
function: async () => {
|
||||
const managerState = useManagerState()
|
||||
const state = managerState.managerUIState.value
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.notAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
useCommandStore()
|
||||
.execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
.catch(() => {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
dialogService.showSettingsDialog('extension')
|
||||
})
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog()
|
||||
break
|
||||
// For DISABLED state, show error toast instead of opening settings
|
||||
if (state === ManagerUIState.DISABLED) {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.notAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.UpdateAvailable,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -804,32 +775,10 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Install Missing Custom Nodes',
|
||||
versionAdded: '1.17.0',
|
||||
function: async () => {
|
||||
const managerStore = useManagerStateStore()
|
||||
const state = managerStore.managerUIState
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
// When manager is disabled, open the extensions panel in settings
|
||||
dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI:
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.Menu.ToggleVisibility'
|
||||
)
|
||||
} catch {
|
||||
// If legacy command doesn't exist, fall back to extensions panel
|
||||
dialogService.showSettingsDialog('extension')
|
||||
}
|
||||
break
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
dialogService.showManagerDialog({
|
||||
initialTab: ManagerTab.Missing
|
||||
})
|
||||
break
|
||||
}
|
||||
await useManagerState().openManager({
|
||||
initialTab: ManagerTab.Missing,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -935,8 +884,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.OpenManagerDialog',
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
label: 'Manager',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
function: async () => {
|
||||
await useManagerState().openManager({
|
||||
initialTab: ManagerTab.All,
|
||||
showToastOnLegacyError: false
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1001,18 +953,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Custom Nodes (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute(
|
||||
'Comfy.Manager.CustomNodesManager.ToggleVisibility'
|
||||
)
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
await useManagerState().openManager({
|
||||
legacyCommand: 'Comfy.Manager.CustomNodesManager.ToggleVisibility',
|
||||
showToastOnLegacyError: true,
|
||||
isLegacyOnly: true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1021,16 +966,10 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Manager Menu (Legacy)',
|
||||
versionAdded: '1.16.4',
|
||||
function: async () => {
|
||||
try {
|
||||
await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
|
||||
} catch (error) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
await useManagerState().openManager({
|
||||
showToastOnLegacyError: true,
|
||||
isLegacyOnly: true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore'
|
||||
|
||||
export interface UseFrontendVersionMismatchWarningOptions {
|
||||
interface UseFrontendVersionMismatchWarningOptions {
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type Ref, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
export interface UseIntersectionObserverOptions
|
||||
extends IntersectionObserverInit {
|
||||
interface UseIntersectionObserverOptions extends IntersectionObserverInit {
|
||||
immediate?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Ref, computed, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
export interface LazyPaginationOptions {
|
||||
interface LazyPaginationOptions {
|
||||
itemsPerPage?: number
|
||||
initialPage?: number
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Ref, computed, ref } from 'vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { components } from '@/types/generatedManagerTypes'
|
||||
import { normalizePackKeys } from '@/utils/packUtils'
|
||||
|
||||
type ManagerTaskHistory = Record<
|
||||
string,
|
||||
@@ -98,7 +99,8 @@ export const useManagerQueue = (
|
||||
taskHistory.value = filterHistoryByClientId(state.history)
|
||||
|
||||
if (state.installed_packs) {
|
||||
installedPacks.value = state.installed_packs
|
||||
// Normalize pack keys to ensure consistent access
|
||||
installedPacks.value = normalizePackKeys(state.installed_packs)
|
||||
}
|
||||
updateProcessingState()
|
||||
}
|
||||
|
||||
203
src/composables/useManagerState.ts
Normal file
203
src/composables/useManagerState.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, readonly } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { ManagerTab } from '@/types/comfyManagerTypes'
|
||||
|
||||
export enum ManagerUIState {
|
||||
DISABLED = 'disabled',
|
||||
LEGACY_UI = 'legacy',
|
||||
NEW_UI = 'new'
|
||||
}
|
||||
|
||||
export function useManagerState() {
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
const { systemStats, isInitialized: systemInitialized } =
|
||||
storeToRefs(systemStatsStore)
|
||||
|
||||
/**
|
||||
* The current manager UI state.
|
||||
* Computed once and cached until dependencies change (which they don't during runtime).
|
||||
* This follows Vue's conventions and provides better performance through caching.
|
||||
*/
|
||||
const managerUIState = readonly(
|
||||
computed((): ManagerUIState => {
|
||||
// Wait for systemStats to be initialized
|
||||
if (!systemInitialized.value) {
|
||||
// Default to DISABLED while loading
|
||||
return ManagerUIState.DISABLED
|
||||
}
|
||||
|
||||
// Get current values
|
||||
const clientSupportsV4 =
|
||||
api.getClientFeatureFlags().supports_manager_v4_ui ?? false
|
||||
|
||||
const serverSupportsV4 = api.getServerFeature(
|
||||
'extension.manager.supports_v4'
|
||||
)
|
||||
|
||||
// Check command line args first (highest priority)
|
||||
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
|
||||
return ManagerUIState.DISABLED
|
||||
}
|
||||
|
||||
if (
|
||||
systemStats.value?.system?.argv?.includes('--enable-manager-legacy-ui')
|
||||
) {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// Both client and server support v4 = NEW_UI
|
||||
if (clientSupportsV4 && serverSupportsV4 === true) {
|
||||
return ManagerUIState.NEW_UI
|
||||
}
|
||||
|
||||
// Server supports v4 but client doesn't = LEGACY_UI
|
||||
if (serverSupportsV4 === true && !clientSupportsV4) {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// Server explicitly doesn't support v4 = LEGACY_UI
|
||||
if (serverSupportsV4 === false) {
|
||||
return ManagerUIState.LEGACY_UI
|
||||
}
|
||||
|
||||
// If server feature flags haven't loaded yet, default to NEW_UI
|
||||
// This is a temporary state - feature flags are exchanged immediately on WebSocket connection
|
||||
// NEW_UI is the safest default since v2 API is the current standard
|
||||
// If the server doesn't support v2, API calls will fail with 404 and be handled gracefully
|
||||
if (serverSupportsV4 === undefined) {
|
||||
return ManagerUIState.NEW_UI
|
||||
}
|
||||
|
||||
// Should never reach here, but if we do, disable manager
|
||||
return ManagerUIState.DISABLED
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if manager is enabled (not DISABLED)
|
||||
*/
|
||||
const isManagerEnabled = readonly(
|
||||
computed((): boolean => {
|
||||
return managerUIState.value !== ManagerUIState.DISABLED
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if manager UI is in NEW_UI mode
|
||||
*/
|
||||
const isNewManagerUI = readonly(
|
||||
computed((): boolean => {
|
||||
return managerUIState.value === ManagerUIState.NEW_UI
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if manager UI is in LEGACY_UI mode
|
||||
*/
|
||||
const isLegacyManagerUI = readonly(
|
||||
computed((): boolean => {
|
||||
return managerUIState.value === ManagerUIState.LEGACY_UI
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if install button should be shown (only in NEW_UI mode)
|
||||
*/
|
||||
const shouldShowInstallButton = readonly(
|
||||
computed((): boolean => {
|
||||
return isNewManagerUI.value
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if manager buttons should be shown (when manager is not disabled)
|
||||
*/
|
||||
const shouldShowManagerButtons = readonly(
|
||||
computed((): boolean => {
|
||||
return isManagerEnabled.value
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Opens the manager UI based on current state
|
||||
* Centralizes the logic for opening manager across the app
|
||||
* @param options - Optional configuration for opening the manager
|
||||
* @param options.initialTab - Initial tab to show (for NEW_UI mode)
|
||||
* @param options.legacyCommand - Legacy command to execute (for LEGACY_UI mode)
|
||||
* @param options.showToastOnLegacyError - Whether to show toast on legacy command failure
|
||||
* @param options.isLegacyOnly - If true, shows error in NEW_UI mode instead of opening manager
|
||||
*/
|
||||
const openManager = async (options?: {
|
||||
initialTab?: ManagerTab
|
||||
legacyCommand?: string
|
||||
showToastOnLegacyError?: boolean
|
||||
isLegacyOnly?: boolean
|
||||
}): Promise<void> => {
|
||||
const state = managerUIState.value
|
||||
const dialogService = useDialogService()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
switch (state) {
|
||||
case ManagerUIState.DISABLED:
|
||||
dialogService.showSettingsDialog('extension')
|
||||
break
|
||||
|
||||
case ManagerUIState.LEGACY_UI: {
|
||||
const command =
|
||||
options?.legacyCommand || 'Comfy.Manager.Menu.ToggleVisibility'
|
||||
try {
|
||||
await commandStore.execute(command)
|
||||
} catch {
|
||||
// If legacy command doesn't exist
|
||||
if (options?.showToastOnLegacyError !== false) {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
// Fallback to extensions panel if not showing toast
|
||||
if (options?.showToastOnLegacyError === false) {
|
||||
dialogService.showSettingsDialog('extension')
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
if (options?.isLegacyOnly) {
|
||||
// Legacy command is not available in NEW_UI mode
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
dialogService.showManagerDialog({ initialTab: ManagerTab.All })
|
||||
} else {
|
||||
dialogService.showManagerDialog(
|
||||
options?.initialTab ? { initialTab: options.initialTab } : undefined
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
managerUIState,
|
||||
isManagerEnabled,
|
||||
isNewManagerUI,
|
||||
isLegacyManagerUI,
|
||||
shouldShowInstallButton,
|
||||
shouldShowManagerButtons,
|
||||
openManager
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { validateComfyWorkflow } from '@/schemas/comfyWorkflowSchema'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { fixBadLinks } from '@/utils/linkFixer'
|
||||
|
||||
export interface ValidationResult {
|
||||
interface ValidationResult {
|
||||
graphData: ComfyWorkflowJSON | null
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const CORE_MENU_COMMANDS = [
|
||||
'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
]
|
||||
],
|
||||
[['View'], []],
|
||||
[
|
||||
['Help'],
|
||||
[
|
||||
|
||||
@@ -186,6 +186,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.WarnBlueprintOverwrite',
|
||||
name: 'Require confirmation to overwrite an existing subgraph blueprint',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ZoomSpeed',
|
||||
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Default colors for node slot types
|
||||
* Mirrors LiteGraph's slot_default_color_by_type
|
||||
*/
|
||||
export const SLOT_TYPE_COLORS: Record<string, string> = {
|
||||
const SLOT_TYPE_COLORS: Record<string, string> = {
|
||||
number: '#AAD',
|
||||
string: '#DCA',
|
||||
boolean: '#DAA',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/**
|
||||
* All supported image formats that can contain workflow data
|
||||
*/
|
||||
export const IMAGE_WORKFLOW_FORMATS = {
|
||||
const IMAGE_WORKFLOW_FORMATS = {
|
||||
extensions: ['.png', '.webp', '.svg', '.avif'],
|
||||
mimeTypes: ['image/png', 'image/webp', 'image/svg+xml', 'image/avif']
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const IMAGE_WORKFLOW_FORMATS = {
|
||||
/**
|
||||
* All supported audio formats that can contain workflow data
|
||||
*/
|
||||
export const AUDIO_WORKFLOW_FORMATS = {
|
||||
const AUDIO_WORKFLOW_FORMATS = {
|
||||
extensions: ['.mp3', '.ogg', '.flac'],
|
||||
mimeTypes: ['audio/mpeg', 'audio/ogg', 'audio/flac', 'audio/x-flac']
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const AUDIO_WORKFLOW_FORMATS = {
|
||||
/**
|
||||
* All supported video formats that can contain workflow data
|
||||
*/
|
||||
export const VIDEO_WORKFLOW_FORMATS = {
|
||||
const VIDEO_WORKFLOW_FORMATS = {
|
||||
extensions: ['.mp4', '.mov', '.m4v', '.webm'],
|
||||
mimeTypes: ['video/mp4', 'video/quicktime', 'video/x-m4v', 'video/webm']
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const VIDEO_WORKFLOW_FORMATS = {
|
||||
/**
|
||||
* All supported 3D model formats that can contain workflow data
|
||||
*/
|
||||
export const MODEL_WORKFLOW_FORMATS = {
|
||||
const MODEL_WORKFLOW_FORMATS = {
|
||||
extensions: ['.glb'],
|
||||
mimeTypes: ['model/gltf-binary']
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const MODEL_WORKFLOW_FORMATS = {
|
||||
/**
|
||||
* All supported data formats that directly contain workflow data
|
||||
*/
|
||||
export const DATA_WORKFLOW_FORMATS = {
|
||||
const DATA_WORKFLOW_FORMATS = {
|
||||
extensions: ['.json', '.latent', '.safetensors'],
|
||||
mimeTypes: ['application/json']
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const DATA_WORKFLOW_FORMATS = {
|
||||
/**
|
||||
* Combines all supported formats into a single object
|
||||
*/
|
||||
export const ALL_WORKFLOW_FORMATS = {
|
||||
const ALL_WORKFLOW_FORMATS = {
|
||||
extensions: [
|
||||
...IMAGE_WORKFLOW_FORMATS.extensions,
|
||||
...AUDIO_WORKFLOW_FORMATS.extensions,
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface CaptureResult {
|
||||
lineart: string
|
||||
}
|
||||
|
||||
export interface BaseManager {
|
||||
interface BaseManager {
|
||||
init(): void
|
||||
dispose(): void
|
||||
reset(): void
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ImageLayerFilenames {
|
||||
interface ImageLayerFilenames {
|
||||
maskedImage: string
|
||||
paint: string
|
||||
paintedImage: string
|
||||
|
||||
@@ -314,6 +314,7 @@ export class LGraph
|
||||
if (this._nodes) {
|
||||
for (const _node of this._nodes) {
|
||||
_node.onRemoved?.()
|
||||
this.onNodeRemoved?.(_node)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1482,6 +1483,12 @@ export class LGraph
|
||||
if (items.size === 0)
|
||||
throw new Error('Cannot convert to subgraph: nothing to convert')
|
||||
const { state, revision, config } = this
|
||||
const firstChild = [...items][0]
|
||||
if (items.size === 1 && firstChild instanceof LGraphGroup) {
|
||||
items = new Set([firstChild])
|
||||
firstChild.recomputeInsideNodes()
|
||||
firstChild.children.forEach((n) => items.add(n))
|
||||
}
|
||||
|
||||
const {
|
||||
boundaryLinks,
|
||||
|
||||
@@ -165,7 +165,7 @@ interface IDialogOptions {
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link LGraphCanvas.state} */
|
||||
export interface LGraphCanvasState {
|
||||
interface LGraphCanvasState {
|
||||
/** {@link Positionable} items are being dragged on the canvas. */
|
||||
draggingItems: boolean
|
||||
/** The canvas itself is being dragged. */
|
||||
@@ -667,6 +667,7 @@ export class LGraphCanvas
|
||||
_bg_img?: HTMLImageElement
|
||||
_pattern?: CanvasPattern
|
||||
_pattern_img?: HTMLImageElement
|
||||
bg_tint?: string | CanvasGradient | CanvasPattern
|
||||
// TODO: This looks like another panel thing
|
||||
prompt_box?: PromptDialog | null
|
||||
search_box?: HTMLDivElement
|
||||
@@ -3754,13 +3755,7 @@ export class LGraphCanvas
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies canvas items to an internal, app-specific clipboard backed by local storage.
|
||||
* When called without parameters, it copies {@link selectedItems}.
|
||||
* @param items The items to copy. If nullish, all selected items are copied.
|
||||
*/
|
||||
copyToClipboard(items?: Iterable<Positionable>): void {
|
||||
_serializeItems(items?: Iterable<Positionable>): ClipboardItems {
|
||||
const serialisable: Required<ClipboardItems> = {
|
||||
nodes: [],
|
||||
groups: [],
|
||||
@@ -3818,10 +3813,18 @@ export class LGraphCanvas
|
||||
const cloned = subgraph.clone(true).asSerialisable()
|
||||
serialisable.subgraphs.push(cloned)
|
||||
}
|
||||
return serialisable
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies canvas items to an internal, app-specific clipboard backed by local storage.
|
||||
* When called without parameters, it copies {@link selectedItems}.
|
||||
* @param items The items to copy. If nullish, all selected items are copied.
|
||||
*/
|
||||
copyToClipboard(items?: Iterable<Positionable>): void {
|
||||
localStorage.setItem(
|
||||
'litegrapheditor_clipboard',
|
||||
JSON.stringify(serialisable)
|
||||
JSON.stringify(this._serializeItems(items))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3853,6 +3856,14 @@ export class LGraphCanvas
|
||||
*/
|
||||
_pasteFromClipboard(
|
||||
options: IPasteFromClipboardOptions = {}
|
||||
): ClipboardPasteResult | undefined {
|
||||
const data = localStorage.getItem('litegrapheditor_clipboard')
|
||||
if (!data) return
|
||||
return this._deserializeItems(JSON.parse(data), options)
|
||||
}
|
||||
_deserializeItems(
|
||||
parsed: ClipboardItems,
|
||||
options: IPasteFromClipboardOptions
|
||||
): ClipboardPasteResult | undefined {
|
||||
const { connectInputs = false, position = this.graph_mouse } = options
|
||||
|
||||
@@ -3863,15 +3874,11 @@ export class LGraphCanvas
|
||||
)
|
||||
return
|
||||
|
||||
const data = localStorage.getItem('litegrapheditor_clipboard')
|
||||
if (!data) return
|
||||
|
||||
const { graph } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
graph.beforeChange()
|
||||
|
||||
// Parse & initialise
|
||||
const parsed: ClipboardItems = JSON.parse(data)
|
||||
parsed.nodes ??= []
|
||||
parsed.groups ??= []
|
||||
parsed.reroutes ??= []
|
||||
@@ -5093,6 +5100,16 @@ export class LGraphCanvas
|
||||
ctx.globalAlpha = 1.0
|
||||
ctx.imageSmoothingEnabled = true
|
||||
}
|
||||
if (this.bg_tint) {
|
||||
ctx.fillStyle = this.bg_tint
|
||||
ctx.fillRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
)
|
||||
ctx.fillStyle = 'transparent'
|
||||
}
|
||||
|
||||
// groups
|
||||
if (this.graph._groups.length) {
|
||||
|
||||
@@ -94,19 +94,19 @@ export type NodeId = number | string
|
||||
|
||||
export type NodeProperty = string | number | boolean | object
|
||||
|
||||
export interface INodePropertyInfo {
|
||||
interface INodePropertyInfo {
|
||||
name: string
|
||||
type?: string
|
||||
default_value: NodeProperty | undefined
|
||||
}
|
||||
|
||||
export interface IMouseOverData {
|
||||
interface IMouseOverData {
|
||||
inputId?: number
|
||||
outputId?: number
|
||||
overWidget?: IBaseWidget
|
||||
}
|
||||
|
||||
export interface ConnectByTypeOptions {
|
||||
interface ConnectByTypeOptions {
|
||||
/** @deprecated Events */
|
||||
createEventInCase?: boolean
|
||||
/** Allow our wildcard slot to connect to typed slots on remote node. Default: true */
|
||||
@@ -118,12 +118,12 @@ export interface ConnectByTypeOptions {
|
||||
}
|
||||
|
||||
/** Internal type used for type safety when implementing generic checks for inputs & outputs */
|
||||
export interface IGenericLinkOrLinks {
|
||||
interface IGenericLinkOrLinks {
|
||||
links?: INodeOutputSlot['links']
|
||||
link?: INodeInputSlot['link']
|
||||
}
|
||||
|
||||
export interface FindFreeSlotOptions {
|
||||
interface FindFreeSlotOptions {
|
||||
/** Slots matching these types will be ignored. Default: [] */
|
||||
typesNotAccepted?: ISlotType[]
|
||||
/** If true, the slot itself is returned instead of the index. Default: false */
|
||||
@@ -743,35 +743,6 @@ export class LGraphNode
|
||||
error: this.#getErrorStrokeStyle,
|
||||
selected: this.#getSelectedStrokeStyle
|
||||
}
|
||||
|
||||
// Assign onMouseDown implementation
|
||||
this.onMouseDown = (
|
||||
// @ts-expect-error - CanvasPointerEvent type needs fixing
|
||||
e: CanvasPointerEvent,
|
||||
pos: Point,
|
||||
canvas: LGraphCanvas
|
||||
): boolean => {
|
||||
// Check for title button clicks (only if not collapsed)
|
||||
if (this.title_buttons?.length && !this.flags.collapsed) {
|
||||
// pos contains the offset from the node's position, so we need to use node-relative coordinates
|
||||
const nodeRelativeX = pos[0]
|
||||
const nodeRelativeY = pos[1]
|
||||
|
||||
for (let i = 0; i < this.title_buttons.length; i++) {
|
||||
const button = this.title_buttons[i]
|
||||
if (
|
||||
button.visible &&
|
||||
button.isPointInside(nodeRelativeX, nodeRelativeY)
|
||||
) {
|
||||
this.onTitleButtonClick(button, canvas)
|
||||
return true // Prevent default behavior
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false // Allow default behavior
|
||||
}
|
||||
|
||||
// Initialize property manager with tracked properties
|
||||
this.changeTracker = new LGraphNodeProperties(this)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import { ToOutputRenderLink } from './ToOutputRenderLink'
|
||||
* References are only held atomically within a function, never passed.
|
||||
* The concrete implementation may be replaced or proxied without side-effects.
|
||||
*/
|
||||
export interface LinkConnectorState {
|
||||
interface LinkConnectorState {
|
||||
/**
|
||||
* The type of slot that links are being connected **to**.
|
||||
* - When `undefined`, no operation is being performed.
|
||||
@@ -68,7 +68,7 @@ type RenderLinkUnion =
|
||||
| ToInputFromIoNodeLink
|
||||
| ToOutputFromIoNodeLink
|
||||
|
||||
export interface LinkConnectorExport {
|
||||
interface LinkConnectorExport {
|
||||
renderLinks: RenderLink[]
|
||||
inputLinks: LLink[]
|
||||
outputLinks: LLink[]
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface IDrawBoundingOptions {
|
||||
lineWidth?: number
|
||||
}
|
||||
|
||||
export interface IDrawTextInAreaOptions {
|
||||
interface IDrawTextInAreaOptions {
|
||||
/** The canvas to draw the text on. */
|
||||
ctx: CanvasRenderingContext2D
|
||||
/** The text to draw. */
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface HasBoundingRect {
|
||||
}
|
||||
|
||||
/** An object containing a set of child objects */
|
||||
export interface Parent<TChild> {
|
||||
interface Parent<TChild> {
|
||||
/** All objects owned by the parent object. */
|
||||
readonly children?: ReadonlySet<TChild>
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export interface LinkSegment {
|
||||
readonly origin_slot: number | undefined
|
||||
}
|
||||
|
||||
export interface IInputOrOutput {
|
||||
interface IInputOrOutput {
|
||||
// If an input, this will be defined
|
||||
input?: INodeInputSlot | null
|
||||
// If an output, this will be defined
|
||||
@@ -273,7 +273,7 @@ export type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> =
|
||||
>
|
||||
|
||||
/** Union of property names that are of type Match */
|
||||
export type KeysOfType<T, Match> = Exclude<
|
||||
type KeysOfType<T, Match> = Exclude<
|
||||
{ [P in keyof T]: T[P] extends Match ? P : never }[keyof T],
|
||||
undefined
|
||||
>
|
||||
@@ -445,7 +445,7 @@ export interface IContextMenuValue<
|
||||
): void | boolean
|
||||
}
|
||||
|
||||
export interface IContextMenuSubmenu<TValue = unknown>
|
||||
interface IContextMenuSubmenu<TValue = unknown>
|
||||
extends IContextMenuOptions<TValue> {
|
||||
options: ConstructorParameters<typeof ContextMenu<TValue>>[0]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { CanvasEventDetail } from './types/events'
|
||||
import type { RenderShape, TitleMode } from './types/globalEnums'
|
||||
|
||||
// Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in `configure`)
|
||||
export { Subgraph, type GraphOrSubgraph } from './subgraph/Subgraph'
|
||||
export { Subgraph } from './subgraph/Subgraph'
|
||||
|
||||
export const LiteGraph = new LiteGraphGlobal()
|
||||
|
||||
@@ -23,7 +23,7 @@ loadPolyfills()
|
||||
/** @deprecated Use {@link Point} instead. */
|
||||
export type Vector2 = Point
|
||||
|
||||
export interface IContextMenuItem {
|
||||
interface IContextMenuItem {
|
||||
content: string
|
||||
callback?: ContextMenuEventListener
|
||||
/** Used as innerHTML for extra child element */
|
||||
@@ -36,7 +36,7 @@ export interface IContextMenuItem {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type ContextMenuEventListener = (
|
||||
type ContextMenuEventListener = (
|
||||
value: IContextMenuItem,
|
||||
options: IContextMenuOptions,
|
||||
event: MouseEvent,
|
||||
@@ -74,58 +74,38 @@ export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||
|
||||
// End backwards compat
|
||||
|
||||
export { InputIndicators } from './canvas/InputIndicators'
|
||||
export { LinkConnector } from './canvas/LinkConnector'
|
||||
export { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
||||
export { CanvasPointer } from './CanvasPointer'
|
||||
export * as Constants from './constants'
|
||||
export { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from './constants'
|
||||
export { SUBGRAPH_INPUT_ID } from './constants'
|
||||
export { ContextMenu } from './ContextMenu'
|
||||
export { CurveEditor } from './CurveEditor'
|
||||
|
||||
export { DragAndScale } from './DragAndScale'
|
||||
export { LabelPosition, SlotDirection, SlotShape, SlotType } from './draw'
|
||||
export { strokeShape } from './draw'
|
||||
|
||||
export { Rectangle } from './infrastructure/Rectangle'
|
||||
export { RecursionError } from './infrastructure/RecursionError'
|
||||
export type {
|
||||
CanvasColour,
|
||||
ColorOption,
|
||||
ConnectingLink,
|
||||
Direction,
|
||||
IBoundaryNodes,
|
||||
IColorable,
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
IFoundSlot,
|
||||
IInputOrOutput,
|
||||
INodeFlags,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
INodeSlot,
|
||||
ISlotType,
|
||||
KeysOfType,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
MethodNames,
|
||||
Point,
|
||||
Positionable,
|
||||
ReadonlyLinkNetwork,
|
||||
ReadOnlyPoint,
|
||||
ReadOnlyRect,
|
||||
Rect,
|
||||
Size
|
||||
} from './interfaces'
|
||||
export { LGraph } from './LGraph'
|
||||
export {
|
||||
BadgePosition,
|
||||
LGraphBadge,
|
||||
type LGraphBadgeOptions
|
||||
} from './LGraphBadge'
|
||||
export { LGraphCanvas, type LGraphCanvasState } from './LGraphCanvas'
|
||||
export { BadgePosition, LGraphBadge } from './LGraphBadge'
|
||||
export { LGraphCanvas } from './LGraphCanvas'
|
||||
export { LGraphGroup } from './LGraphGroup'
|
||||
export { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
|
||||
export { LGraphNode, type NodeId } from './LGraphNode'
|
||||
export { COMFY_VUE_NODE_DIMENSIONS } from './LiteGraphGlobal'
|
||||
export { type LinkId, LLink } from './LLink'
|
||||
export { LLink } from './LLink'
|
||||
export { createBounds } from './measure'
|
||||
export { Reroute, type RerouteId } from './Reroute'
|
||||
export {
|
||||
@@ -136,23 +116,18 @@ export {
|
||||
export { SubgraphNode } from './subgraph/SubgraphNode'
|
||||
export type { CanvasPointerEvent } from './types/events'
|
||||
export {
|
||||
CanvasItem,
|
||||
EaseFunction,
|
||||
LGraphEventMode,
|
||||
LinkDirection,
|
||||
LinkMarkerShape,
|
||||
RenderShape,
|
||||
TitleMode
|
||||
RenderShape
|
||||
} from './types/globalEnums'
|
||||
export type {
|
||||
ExportedSubgraph,
|
||||
ExportedSubgraphInstance,
|
||||
ExportedSubgraphIONode,
|
||||
ISerialisedGraph,
|
||||
ISerialisedNode,
|
||||
SerialisableGraph,
|
||||
SerialisableLLink,
|
||||
SubgraphIO
|
||||
SerialisableGraph
|
||||
} from './types/serialisation'
|
||||
export type { IWidget } from './types/widgets'
|
||||
export { isColorable } from './utils/type'
|
||||
@@ -161,19 +136,14 @@ export type { UUID } from './utils/uuid'
|
||||
export { truncateText } from './utils/textUtils'
|
||||
export { getWidgetStep } from './utils/widget'
|
||||
export { distributeSpace, type SpaceRequest } from './utils/spaceDistribution'
|
||||
export { BaseSteppedWidget } from './widgets/BaseSteppedWidget'
|
||||
|
||||
export { BaseWidget } from './widgets/BaseWidget'
|
||||
export { BooleanWidget } from './widgets/BooleanWidget'
|
||||
export { ButtonWidget } from './widgets/ButtonWidget'
|
||||
export { ComboWidget } from './widgets/ComboWidget'
|
||||
export { KnobWidget } from './widgets/KnobWidget'
|
||||
|
||||
export { LegacyWidget } from './widgets/LegacyWidget'
|
||||
export { NumberWidget } from './widgets/NumberWidget'
|
||||
export { SliderWidget } from './widgets/SliderWidget'
|
||||
export { TextWidget } from './widgets/TextWidget'
|
||||
|
||||
export { isComboWidget } from './widgets/widgetMap'
|
||||
// Additional test-specific exports
|
||||
export { LGraphButton, type LGraphButtonOptions } from './LGraphButton'
|
||||
export { LGraphButton } from './LGraphButton'
|
||||
export { MovingOutputLink } from './canvas/MovingOutputLink'
|
||||
export { ToOutputRenderLink } from './canvas/ToOutputRenderLink'
|
||||
export { ToInputFromIoNodeLink } from './canvas/ToInputFromIoNodeLink'
|
||||
|
||||
@@ -18,9 +18,7 @@ type CommonIoSlotProps = SharedIntersection<
|
||||
ISerialisableNodeOutput
|
||||
>
|
||||
|
||||
export function shallowCloneCommonProps(
|
||||
slot: CommonIoSlotProps
|
||||
): CommonIoSlotProps {
|
||||
function shallowCloneCommonProps(slot: CommonIoSlotProps): CommonIoSlotProps {
|
||||
const {
|
||||
color_off,
|
||||
color_on,
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { SubgraphInputNode } from './SubgraphInputNode'
|
||||
import type { SubgraphOutput } from './SubgraphOutput'
|
||||
import type { SubgraphOutputNode } from './SubgraphOutputNode'
|
||||
|
||||
export interface SubgraphSlotDrawOptions {
|
||||
interface SubgraphSlotDrawOptions {
|
||||
ctx: CanvasRenderingContext2D
|
||||
colorContext: DefaultConnectionColors
|
||||
lowQuality?: boolean
|
||||
|
||||
@@ -27,7 +27,7 @@ import { SubgraphInputNode } from './SubgraphInputNode'
|
||||
import type { SubgraphOutput } from './SubgraphOutput'
|
||||
import { SubgraphOutputNode } from './SubgraphOutputNode'
|
||||
|
||||
export interface FilteredItems {
|
||||
interface FilteredItems {
|
||||
nodes: Set<LGraphNode>
|
||||
reroutes: Set<Reroute>
|
||||
groups: Set<LGraphGroup>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { LGraphNode } from '../LGraphNode'
|
||||
import type { LinkReleaseContextExtended } from '../litegraph'
|
||||
|
||||
/** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */
|
||||
export interface ICanvasPosition {
|
||||
interface ICanvasPosition {
|
||||
/** X co-ordinate of the event, in graph space (NOT canvas space) */
|
||||
canvasX: number
|
||||
/** Y co-ordinate of the event, in graph space (NOT canvas space) */
|
||||
@@ -14,7 +14,7 @@ export interface ICanvasPosition {
|
||||
}
|
||||
|
||||
/** For Canvas*Event */
|
||||
export interface IDeltaPosition {
|
||||
interface IDeltaPosition {
|
||||
deltaX: number
|
||||
deltaY: number
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export interface IDeltaPosition {
|
||||
* Workaround for Firefox returning 0 on offsetX/Y props
|
||||
* See https://github.com/Comfy-Org/litegraph.js/issues/403 for details
|
||||
*/
|
||||
export interface IOffsetWorkaround {
|
||||
interface IOffsetWorkaround {
|
||||
/** See {@link MouseEvent.offsetX}. This workaround is required (2024-12-31) to support Firefox, which always returns 0 */
|
||||
safeOffsetX: number
|
||||
/** See {@link MouseEvent.offsetY}. This workaround is required (2024-12-31) to support Firefox, which always returns 0 */
|
||||
@@ -45,7 +45,7 @@ interface LegacyMouseEvent {
|
||||
export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {}
|
||||
|
||||
/** MouseEvent with canvasX/Y and deltaX/Y properties */
|
||||
export interface CanvasMouseEvent
|
||||
interface CanvasMouseEvent
|
||||
extends MouseEvent,
|
||||
Readonly<CanvasPointerExtensions>,
|
||||
LegacyMouseEvent {}
|
||||
@@ -57,29 +57,29 @@ export type CanvasEventDetail =
|
||||
| EmptyDoubleClickEventDetail
|
||||
| EmptyReleaseEventDetail
|
||||
|
||||
export interface GenericEventDetail {
|
||||
interface GenericEventDetail {
|
||||
subType: 'before-change' | 'after-change'
|
||||
}
|
||||
|
||||
export interface OriginalEvent {
|
||||
interface OriginalEvent {
|
||||
originalEvent: CanvasPointerEvent
|
||||
}
|
||||
|
||||
export interface EmptyReleaseEventDetail extends OriginalEvent {
|
||||
interface EmptyReleaseEventDetail extends OriginalEvent {
|
||||
subType: 'empty-release'
|
||||
linkReleaseContext: LinkReleaseContextExtended
|
||||
}
|
||||
|
||||
export interface EmptyDoubleClickEventDetail extends OriginalEvent {
|
||||
interface EmptyDoubleClickEventDetail extends OriginalEvent {
|
||||
subType: 'empty-double-click'
|
||||
}
|
||||
|
||||
export interface GroupDoubleClickEventDetail extends OriginalEvent {
|
||||
interface GroupDoubleClickEventDetail extends OriginalEvent {
|
||||
subType: 'group-double-click'
|
||||
group: LGraphGroup
|
||||
}
|
||||
|
||||
export interface NodeDoubleClickEventDetail extends OriginalEvent {
|
||||
interface NodeDoubleClickEventDetail extends OriginalEvent {
|
||||
subType: 'node-double-click'
|
||||
node: LGraphNode
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface Serialisable<SerialisableObject> {
|
||||
asSerialisable(): SerialisableObject
|
||||
}
|
||||
|
||||
export interface BaseExportedGraph {
|
||||
interface BaseExportedGraph {
|
||||
/** Unique graph ID. Automatically generated if not provided. */
|
||||
id: UUID
|
||||
/** The revision number of this graph. Not automatically incremented; intended for use by a downstream save function. */
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface IWidgetOptions<TValues = unknown[]> {
|
||||
callback?: IWidget['callback']
|
||||
}
|
||||
|
||||
export interface IWidgetSliderOptions extends IWidgetOptions<number[]> {
|
||||
interface IWidgetSliderOptions extends IWidgetOptions<number[]> {
|
||||
min: number
|
||||
max: number
|
||||
step2: number
|
||||
@@ -38,7 +38,7 @@ export interface IWidgetSliderOptions extends IWidgetOptions<number[]> {
|
||||
marker_color?: CanvasColour
|
||||
}
|
||||
|
||||
export interface IWidgetKnobOptions extends IWidgetOptions<number[]> {
|
||||
interface IWidgetKnobOptions extends IWidgetOptions<number[]> {
|
||||
min: number
|
||||
max: number
|
||||
step2: number
|
||||
@@ -144,7 +144,7 @@ export interface IButtonWidget
|
||||
}
|
||||
|
||||
/** A custom widget - accepts any value and has no built-in special handling */
|
||||
export interface ICustomWidget extends IBaseWidget<string | object, 'custom'> {
|
||||
interface ICustomWidget extends IBaseWidget<string | object, 'custom'> {
|
||||
type: 'custom'
|
||||
value: string | object
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export interface IMarkdownWidget extends IBaseWidget<string, 'markdown'> {
|
||||
}
|
||||
|
||||
/** Image display widget */
|
||||
export interface IImageWidget extends IBaseWidget<string, 'image'> {
|
||||
interface IImageWidget extends IBaseWidget<string, 'image'> {
|
||||
type: 'image'
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface DrawWidgetOptions {
|
||||
showText?: boolean
|
||||
}
|
||||
|
||||
export interface DrawTruncatingTextOptions extends DrawWidgetOptions {
|
||||
interface DrawTruncatingTextOptions extends DrawWidgetOptions {
|
||||
/** The canvas context to draw the text on. */
|
||||
ctx: CanvasRenderingContext2D
|
||||
/** The amount of padding to add to the left of the text. */
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
createTestSubgraphNode
|
||||
} from './subgraphHelpers'
|
||||
|
||||
export interface SubgraphFixtures {
|
||||
interface SubgraphFixtures {
|
||||
/** A minimal subgraph with no inputs, outputs, or nodes */
|
||||
emptySubgraph: Subgraph
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { createUuidv4 } from '@/lib/litegraph/src/utils/uuid'
|
||||
|
||||
export interface TestSubgraphOptions {
|
||||
interface TestSubgraphOptions {
|
||||
id?: UUID
|
||||
name?: string
|
||||
nodeCount?: number
|
||||
@@ -27,20 +27,20 @@ export interface TestSubgraphOptions {
|
||||
outputs?: Array<{ name: string; type: ISlotType }>
|
||||
}
|
||||
|
||||
export interface TestSubgraphNodeOptions {
|
||||
interface TestSubgraphNodeOptions {
|
||||
id?: NodeId
|
||||
pos?: [number, number]
|
||||
size?: [number, number]
|
||||
}
|
||||
|
||||
export interface NestedSubgraphOptions {
|
||||
interface NestedSubgraphOptions {
|
||||
depth?: number
|
||||
nodesPerLevel?: number
|
||||
inputsPerSubgraph?: number
|
||||
outputsPerSubgraph?: number
|
||||
}
|
||||
|
||||
export interface SubgraphStructureExpectation {
|
||||
interface SubgraphStructureExpectation {
|
||||
inputCount?: number
|
||||
outputCount?: number
|
||||
nodeCount?: number
|
||||
@@ -49,7 +49,7 @@ export interface SubgraphStructureExpectation {
|
||||
hasOutputNode?: boolean
|
||||
}
|
||||
|
||||
export interface CapturedEvent<T = unknown> {
|
||||
interface CapturedEvent<T = unknown> {
|
||||
type: string
|
||||
detail: T
|
||||
timestamp: number
|
||||
@@ -422,6 +422,3 @@ export function createEventCapture<T = unknown>(
|
||||
capturedEvents.filter((e) => e.type === type)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export expect from vitest for convenience
|
||||
export { expect } from 'vitest'
|
||||
|
||||
@@ -197,6 +197,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "فتح سير عمل"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "نشر الرسم البياني الفرعي"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "إضافة الأمر إلى قائمة الانتظار"
|
||||
},
|
||||
|
||||
@@ -813,6 +813,7 @@
|
||||
"Pin/Unpin Selected Items": "تثبيت/إلغاء تثبيت العناصر المحددة",
|
||||
"Pin/Unpin Selected Nodes": "تثبيت/إلغاء تثبيت العقد المحددة",
|
||||
"Previous Opened Workflow": "سير العمل السابق المفتوح",
|
||||
"Publish": "نشر",
|
||||
"Queue Panel": "لوحة الانتظار",
|
||||
"Queue Prompt": "قائمة انتظار التعليمات",
|
||||
"Queue Prompt (Front)": "قائمة انتظار التعليمات (أمامي)",
|
||||
@@ -842,6 +843,7 @@
|
||||
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
|
||||
"Unlock Canvas": "فتح قفل اللوحة",
|
||||
"Unpack the selected Subgraph": "فك تجميع الرسم البياني الفرعي المحدد",
|
||||
"View": "عرض",
|
||||
"Workflows": "سير العمل",
|
||||
"Zoom In": "تكبير",
|
||||
"Zoom Out": "تصغير",
|
||||
@@ -1254,6 +1256,10 @@
|
||||
},
|
||||
"workflows": "سير العمل"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "اسم المخطط الفرعي",
|
||||
"saveBlueprint": "احفظ المخطط الفرعي في المكتبة"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "إضافة إلى العلامات",
|
||||
"closeOtherTabs": "إغلاق التبويبات الأخرى",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Open Workflow"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Publish Subgraph"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "Queue Prompt"
|
||||
},
|
||||
|
||||
@@ -968,6 +968,18 @@
|
||||
"enterFilename": "Enter the filename",
|
||||
"saveWorkflow": "Save workflow"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"confirmDeleteTitle": "Delete blueprint?",
|
||||
"confirmDelete": "This action will permanently remove the blueprint from your library",
|
||||
"saveBlueprint": "Save Subgraph to Library",
|
||||
"overwriteBlueprintTitle": "Overwrite existing blueprint?",
|
||||
"overwriteBlueprint": "Saving will overwrite the current blueprint with your changes",
|
||||
"blueprintName": "Subgraph name",
|
||||
"publish": "Publish Subgraph",
|
||||
"publishSuccess": "Saved to Nodes Library",
|
||||
"publishSuccessMessage": "You can find your subgraph blueprint in the nodes library under \"Subgraph Blueprints\"",
|
||||
"loadFailure": "Failed to load subgraph blueprints"
|
||||
},
|
||||
"electronFileDownload": {
|
||||
"inProgress": "In Progress",
|
||||
"pause": "Pause Download",
|
||||
@@ -1023,6 +1035,7 @@
|
||||
"menuLabels": {
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"View": "View",
|
||||
"Manager": "Manager",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
@@ -1096,6 +1109,7 @@
|
||||
"Clipspace": "Clipspace",
|
||||
"Manager": "Manager",
|
||||
"Open": "Open",
|
||||
"Publish": "Publish",
|
||||
"Queue Prompt": "Queue Prompt",
|
||||
"Queue Prompt (Front)": "Queue Prompt (Front)",
|
||||
"Queue Selected Output Nodes": "Queue Selected Output Nodes",
|
||||
@@ -1778,6 +1792,7 @@
|
||||
"duplicate": "Duplicate",
|
||||
"clearWorkflow": "Clear Workflow",
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"deleteBlueprint": "Delete Blueprint",
|
||||
"enterNewName": "Enter new name"
|
||||
},
|
||||
"shortcuts": {
|
||||
@@ -1801,4 +1816,4 @@
|
||||
"renderBypassState": "Render Bypass State",
|
||||
"renderErrorState": "Render Error State"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Abrir Flujo de Trabajo"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Publicar subgrafo"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "Prompt de Cola"
|
||||
},
|
||||
|
||||
@@ -825,7 +825,11 @@
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unload Models": "Descargar modelos",
|
||||
"Unload Models and Execution Cache": "Descargar modelos y caché de ejecución",
|
||||
"Unlock Canvas": "Desbloquear lienzo",
|
||||
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
|
||||
"View": "Ver",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar",
|
||||
"Zoom to fit": "Ajustar al tamaño"
|
||||
@@ -1219,6 +1223,10 @@
|
||||
},
|
||||
"workflows": "Flujos de trabajo"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "Nombre del subgrafo",
|
||||
"saveBlueprint": "Guardar subgrafo en la biblioteca"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Agregar a marcadores",
|
||||
"closeOtherTabs": "Cerrar otras pestañas",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Ouvrir le flux de travail"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Publier le sous-graphe"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "Invite de file d'attente"
|
||||
},
|
||||
|
||||
@@ -825,7 +825,11 @@
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unload Models": "Décharger les modèles",
|
||||
"Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution",
|
||||
"Unlock Canvas": "Déverrouiller le canevas",
|
||||
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
|
||||
"View": "Afficher",
|
||||
"Workflow": "Flux de travail",
|
||||
"Workflows": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"Zoom to fit": "Ajuster à l'écran"
|
||||
@@ -1220,6 +1224,10 @@
|
||||
},
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "Nom du sous-graphe",
|
||||
"saveBlueprint": "Enregistrer le sous-graphe dans la bibliothèque"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Ajouter aux Favoris",
|
||||
"closeOtherTabs": "Fermer les autres onglets",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "ワークフローを開く"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "サブグラフを公開"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "キュープロンプト"
|
||||
},
|
||||
|
||||
@@ -824,7 +824,11 @@
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unload Models": "モデルのアンロード",
|
||||
"Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード",
|
||||
"Unlock Canvas": "キャンバスのロックを解除",
|
||||
"Unpack the selected Subgraph": "選択したサブグラフを展開",
|
||||
"View": "表示",
|
||||
"Workflow": "ワークフロー",
|
||||
"Workflows": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Zoom to fit": "全体表示にズーム"
|
||||
@@ -1218,6 +1222,10 @@
|
||||
},
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "サブグラフ名",
|
||||
"saveBlueprint": "サブグラフをライブラリに保存"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "ブックマークに追加",
|
||||
"closeOtherTabs": "他のタブを閉じる",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"label": "업데이트 확인"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "사용자 정의 노드 폴더 열기"
|
||||
"label": "커스텀 노드 폴더 열기"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "입력 폴더 열기"
|
||||
@@ -129,7 +129,7 @@
|
||||
"label": "선택 영역을 서브그래프로 변환"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "서브그래프 종료"
|
||||
"label": "서브그래프 나가기"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "그룹을 내용에 맞게 맞추기"
|
||||
@@ -138,7 +138,7 @@
|
||||
"label": "선택한 노드 그룹화"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "선택한 서브그래프 풀기"
|
||||
"label": "선택한 서브그래프 묶음 풀기"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "선택한 노드를 그룹 노드로 변환"
|
||||
@@ -171,13 +171,13 @@
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowCustomNodesMenu": {
|
||||
"label": "사용자 정의 노드 (베타)"
|
||||
"label": "커스텀 노드 (베타)"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager_ShowLegacyCustomNodesMenu": {
|
||||
"label": "커스텀 노드 (레거시)"
|
||||
"label": "커스텀 노드 (구버전)"
|
||||
},
|
||||
"Comfy_Manager_ShowLegacyManagerMenu": {
|
||||
"label": "매니저 메뉴 (레거시)"
|
||||
"label": "매니저 메뉴 (구버전)"
|
||||
},
|
||||
"Comfy_Manager_ShowMissingPacks": {
|
||||
"label": "누락된 팩 설치"
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "워크플로 열기"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "서브그래프 게시"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "실행 큐에 프롬프트 추가"
|
||||
},
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"commandProhibited": "명령 {command}은 금지되었습니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"commandProhibited": "{command}는 금지된 명령입니다. 자세한 정보는 관리자에게 문의하십시오.",
|
||||
"community": "커뮤니티",
|
||||
"completed": "완료됨",
|
||||
"confirm": "확인",
|
||||
@@ -326,7 +326,7 @@
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래되었습니다. 백엔드는 {requiredVersion} 이상이 필요합니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래된 버전입니다. 백엔드는 {requiredVersion} 이상 버전이 필요합니다.",
|
||||
"goToNode": "노드로 이동",
|
||||
"help": "도움말",
|
||||
"icon": "아이콘",
|
||||
@@ -353,7 +353,7 @@
|
||||
"micPermissionDenied": "마이크 권한이 거부되었습니다",
|
||||
"migrate": "이전(migrate)",
|
||||
"missing": "누락됨",
|
||||
"moreWorkflows": "더 많은 워크플로우",
|
||||
"moreWorkflows": "더 많은 워크플로",
|
||||
"name": "이름",
|
||||
"newFolder": "새 폴더",
|
||||
"next": "다음",
|
||||
@@ -464,8 +464,8 @@
|
||||
"appPathLocationTooltip": "ComfyUI의 앱 에셋 디렉토리. ComfyUI 코드 및 에셋을 저장합니다.",
|
||||
"cannotWrite": "선택한 경로에 쓸 수 없습니다",
|
||||
"chooseInstallationLocation": "설치 위치 선택",
|
||||
"customNodes": "사용자 정의 노드",
|
||||
"customNodesDescription": "기존 ComfyUI 설치에서 사용자 정의 노드를 다시 설치합니다.",
|
||||
"customNodes": "커스텀 노드",
|
||||
"customNodesDescription": "기존 ComfyUI 설치에서 커스텀 노드를 다시 설치합니다.",
|
||||
"desktopAppSettings": "데스크탑 앱 설정",
|
||||
"desktopAppSettingsDescription": "ComfyUI가 데스크탑에서 어떻게 작동하는지 구성하세요. 이 설정은 나중에 변경할 수 있습니다.",
|
||||
"desktopSettings": "데스크탑 설정",
|
||||
@@ -489,7 +489,7 @@
|
||||
"helpImprove": "ComfyUI 개선에 도움을 주세요",
|
||||
"installLocation": "설치 위치",
|
||||
"installLocationDescription": "ComfyUI의 사용자 데이터 디렉토리를 선택하십시오. 선택한 위치에 Python 환경이 설치됩니다. 선택한 디스크에 충분한 공간(~15GB)이 남아 있는지 확인하십시오.",
|
||||
"installLocationTooltip": "ComfyUI의 사용자 데이터 디렉토리. 저장소:\n- Python 환경\n- 모델\n- 사용자 정의 노드\n",
|
||||
"installLocationTooltip": "ComfyUI의 사용자 데이터 디렉토리. 저장소:\n- Python 환경\n- 모델\n- 커스텀 노드\n",
|
||||
"insufficientFreeSpace": "공간이 부족합니다 - 최소한의 여유 공간",
|
||||
"isOneDrive": "OneDrive에 설치하면 문제가 발생할 수 있습니다. OneDrive가 아닌 위치에 설치하는 것을 강력히 권장합니다.",
|
||||
"manualConfiguration": {
|
||||
@@ -633,9 +633,9 @@
|
||||
"installationQueue": "설치 대기열",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"latestVersion": "최신",
|
||||
"legacyManagerUI": "레거시 UI 사용",
|
||||
"legacyManagerUIDescription": "레거시 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 레거시 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"legacyManagerUI": "구버전 매니저 UI 사용",
|
||||
"legacyManagerUIDescription": "구버전 매니저 UI를 사용하려면, ComfyUI를 --enable-manager-legacy-ui로 시작하세요",
|
||||
"legacyMenuNotAvailable": "이 버전의 ComfyUI에서는 구버전 매니저 메뉴를 사용할 수 없습니다. 대신 새로운 매니저 메뉴를 사용하십시오.",
|
||||
"license": "라이선스",
|
||||
"loadingVersions": "버전 로딩 중...",
|
||||
"nightlyVersion": "최신 테스트 버전(nightly)",
|
||||
@@ -663,7 +663,7 @@
|
||||
"pending": "대기 중",
|
||||
"unknown": "알 수 없음"
|
||||
},
|
||||
"title": "사용자 정의 노드 관리자",
|
||||
"title": "커스텀 노드 관리자",
|
||||
"totalNodes": "총 노드",
|
||||
"tryAgainLater": "나중에 다시 시도해 주세요.",
|
||||
"tryDifferentSearch": "다른 검색어를 시도해 주세요.",
|
||||
@@ -750,8 +750,8 @@
|
||||
"Contact Support": "고객 지원 문의",
|
||||
"Convert Selection to Subgraph": "선택 영역을 서브그래프로 변환",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes (Legacy)": "커스텀 노드(레거시)",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Custom Nodes (Legacy)": "커스텀 노드(구버전)",
|
||||
"Custom Nodes Manager": "커스텀 노드 관리자",
|
||||
"Decrease Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 줄이기",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
@@ -770,7 +770,7 @@
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
"Manager": "매니저",
|
||||
"Manager Menu (Legacy)": "매니저 메뉴(레거시)",
|
||||
"Manager Menu (Legacy)": "매니저 메뉴(구버전)",
|
||||
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
@@ -779,7 +779,7 @@
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
"Open": "열기",
|
||||
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
|
||||
"Open Custom Nodes Folder": "커스텀 노드 폴더 열기",
|
||||
"Open DevTools": "개발자 도구 열기",
|
||||
"Open Inputs Folder": "입력 폴더 열기",
|
||||
"Open Logs Folder": "로그 폴더 열기",
|
||||
@@ -814,20 +814,24 @@
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "대기열 사이드바 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로우 사이드바 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unload Models": "모델 언로드",
|
||||
"Unload Models and Execution Cache": "모델 및 실행 캐시 언로드",
|
||||
"Unlock Canvas": "캔버스 잠금 해제",
|
||||
"Unpack the selected Subgraph": "선택한 서브그래프 묶음 풀기",
|
||||
"View": "보기",
|
||||
"Workflow": "워크플로",
|
||||
"Workflows": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
"Zoom to fit": "화면에 맞추기"
|
||||
@@ -838,10 +842,10 @@
|
||||
"renderErrorState": "에러 상태 렌더링",
|
||||
"showGroups": "프레임/그룹 표시",
|
||||
"showLinks": "링크 표시",
|
||||
"sideToolbar_modelLibrary": "sideToolbar.모델 라이브러리",
|
||||
"sideToolbar_nodeLibrary": "sideToolbar.노드 라이브러리",
|
||||
"sideToolbar_queue": "sideToolbar.대기열",
|
||||
"sideToolbar_workflows": "sideToolbar.워크플로우"
|
||||
"sideToolbar_modelLibrary": "사이드툴바.모델 라이브러리",
|
||||
"sideToolbar_nodeLibrary": "사이드툴바.노드 라이브러리",
|
||||
"sideToolbar_queue": "사이드툴바.대기열",
|
||||
"sideToolbar_workflows": "사이드툴바.워크플로"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
@@ -1007,7 +1011,7 @@
|
||||
"name": "DirectML 장치 번호"
|
||||
},
|
||||
"disable-all-custom-nodes": {
|
||||
"name": "모든 사용자 정의 노드 로드 비활성화."
|
||||
"name": "모든 커스텀 노드 로드 비활성화."
|
||||
},
|
||||
"disable-ipex-optimize": {
|
||||
"name": "IPEX 최적화 비활성화"
|
||||
@@ -1221,6 +1225,10 @@
|
||||
},
|
||||
"workflows": "워크플로"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "서브그래프 이름",
|
||||
"saveBlueprint": "서브그래프를 라이브러리에 저장"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "북마크에 추가",
|
||||
"closeOtherTabs": "다른 탭 닫기",
|
||||
@@ -1239,7 +1247,7 @@
|
||||
"Basics": "기본",
|
||||
"ComfyUI Examples": "ComfyUI 예시",
|
||||
"ControlNet": "컨트롤넷",
|
||||
"Custom Nodes": "사용자 정의 노드",
|
||||
"Custom Nodes": "커스텀 노드",
|
||||
"Flux": "FLUX",
|
||||
"Image": "이미지",
|
||||
"Image API": "이미지 API",
|
||||
@@ -1628,7 +1636,7 @@
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "닫기",
|
||||
"frontendNewer": "프론트엔드 버전 {frontendVersion}이(가) 백엔드 버전 {backendVersion}과(와) 호환되지 않을 수 있습니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래되었습니다. 백엔드는 {requiredVersion} 이상 버전을 필요로 합니다.",
|
||||
"frontendOutdated": "프론트엔드 버전 {frontendVersion}이(가) 오래된 버전입니다. 백엔드는 {requiredVersion} 이상 버전이 필요합니다.",
|
||||
"title": "버전 호환성 경고",
|
||||
"updateFrontend": "프론트엔드 업데이트"
|
||||
},
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Открыть рабочий процесс"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "Опубликовать подграф"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "Очередь запросов"
|
||||
},
|
||||
|
||||
@@ -825,7 +825,11 @@
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unload Models": "Выгрузить модели",
|
||||
"Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения",
|
||||
"Unlock Canvas": "Разблокировать холст",
|
||||
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
|
||||
"View": "Вид",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zoom to fit": "Масштабировать по размеру"
|
||||
@@ -1219,6 +1223,10 @@
|
||||
},
|
||||
"workflows": "Рабочие процессы"
|
||||
},
|
||||
"subgraphStore": {
|
||||
"blueprintName": "Имя подграфа",
|
||||
"saveBlueprint": "Сохранить подграф в библиотеку"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Добавить в закладки",
|
||||
"closeOtherTabs": "Закрыть другие вкладки",
|
||||
|
||||
@@ -215,6 +215,9 @@
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "開啟工作流程"
|
||||
},
|
||||
"Comfy_PublishSubgraph": {
|
||||
"label": "發布子圖"
|
||||
},
|
||||
"Comfy_QueuePrompt": {
|
||||
"label": "將提示詞加入佇列"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user