Compare commits
7 Commits
fix/small-
...
drjkl/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78cbfd2a7a | ||
|
|
9a3545b42b | ||
|
|
493fe667b7 | ||
|
|
7636b9eaa1 | ||
|
|
b661412c5b | ||
|
|
ba4c3525f4 | ||
|
|
58024fb4f4 |
@@ -294,6 +294,7 @@ echo "Last stable release: $LAST_STABLE"
|
||||
1. Run complete test suite:
|
||||
```bash
|
||||
pnpm test:unit
|
||||
pnpm test:component
|
||||
```
|
||||
2. Run type checking:
|
||||
```bash
|
||||
|
||||
@@ -120,6 +120,7 @@ echo "Available commands:"
|
||||
echo " pnpm dev - Start development server"
|
||||
echo " pnpm build - Build for production"
|
||||
echo " pnpm test:unit - Run unit tests"
|
||||
echo " pnpm test:component - Run component tests"
|
||||
echo " pnpm typecheck - Run TypeScript checks"
|
||||
echo " pnpm lint - Run ESLint"
|
||||
echo " pnpm format - Format code with Prettier"
|
||||
|
||||
31
.github/actions/setup-playwright/action.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Setup Playwright
|
||||
description: Cache and install Playwright browsers with dependencies
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect Playwright version
|
||||
id: detect-version
|
||||
shell: bash
|
||||
working-directory: ComfyUI_frontend
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
|
||||
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
id: cache-playwright-browsers
|
||||
with:
|
||||
path: '~/.cache/ms-playwright'
|
||||
key: ${{ runner.os }}-playwright-browsers-${{ steps.detect-version.outputs.playwright-version }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Install Playwright Browsers (operating system dependencies)
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
run: pnpm exec playwright install-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
11
.github/workflows/claude-pr-review.yml
vendored
@@ -29,9 +29,11 @@ jobs:
|
||||
- name: Check if we should proceed
|
||||
id: check-status
|
||||
run: |
|
||||
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format")) | {name, conclusion}')
|
||||
|
||||
if echo "$CHECK_RUNS" | grep -Eq '"conclusion": "(failure|cancelled|timed_out|action_required)"'; then
|
||||
# Get all check runs for this commit
|
||||
CHECK_RUNS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs --jq '.check_runs[] | select(.name | test("lint-and-format|test|playwright-tests")) | {name, conclusion}')
|
||||
|
||||
# Check if any required checks failed
|
||||
if echo "$CHECK_RUNS" | grep -q '"conclusion": "failure"'; then
|
||||
echo "Some CI checks failed - skipping Claude review"
|
||||
echo "proceed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -51,7 +53,6 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/head
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -85,4 +86,4 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -77,8 +77,9 @@ jobs:
|
||||
python main.py --cpu --multi-user &
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
- name: Setup Playwright
|
||||
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
@@ -13,9 +13,11 @@ jobs:
|
||||
update-locales:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Setup Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
@@ -26,8 +26,16 @@ jobs:
|
||||
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
i18n-tools-cache-${{ runner.os }}-
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Start dev server
|
||||
# Run electron dev server as it is a superset of the web dev server
|
||||
# We do want electron specific UIs to be translated.
|
||||
8
.github/workflows/lint-and-format.yaml
vendored
@@ -15,7 +15,9 @@ jobs:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -67,7 +69,7 @@ jobs:
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
git commit -m "[automated] Apply ESLint and Prettier fixes"
|
||||
git commit -m "[auto-fix] Apply ESLint and Prettier fixes"
|
||||
git push
|
||||
|
||||
- name: Final validation
|
||||
@@ -100,4 +102,4 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
|
||||
})
|
||||
})
|
||||
@@ -14,8 +14,16 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Run Playwright tests and update snapshots
|
||||
id: playwright-tests
|
||||
run: pnpm exec playwright test --update-snapshots
|
||||
@@ -39,6 +47,6 @@ jobs:
|
||||
git fetch origin ${{ github.head_ref }}
|
||||
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
|
||||
git add browser_tests
|
||||
git commit -m "[automated] Update test expectations"
|
||||
git commit -m "Update test expectations [skip ci]"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
working-directory: ComfyUI_frontend
|
||||
@@ -12,6 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout ComfyUI
|
||||
uses: actions/checkout@v5
|
||||
@@ -64,6 +65,12 @@ jobs:
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Playwright Version
|
||||
id: playwright-version
|
||||
run: |
|
||||
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --json | jq --raw-output '.[0].devDependencies["@playwright/test"].version')
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
@@ -116,8 +123,22 @@ jobs:
|
||||
working-directory: ComfyUI
|
||||
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
id: cache-playwright-browsers
|
||||
with:
|
||||
path: '~/.cache/ms-playwright'
|
||||
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Install Playwright Browsers (operating system dependencies)
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
run: pnpm exec playwright install-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
@@ -181,8 +202,22 @@ jobs:
|
||||
pip install wait-for-it
|
||||
working-directory: ComfyUI
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./ComfyUI_frontend/.github/actions/setup-playwright
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
id: cache-playwright-browsers
|
||||
with:
|
||||
path: '~/.cache/ms-playwright'
|
||||
key: '${{ runner.os }}-playwright-browsers-${{ needs.setup.outputs.playwright-version }}'
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Install Playwright Browsers (operating system dependencies)
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
run: pnpm exec playwright install-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Start ComfyUI server
|
||||
run: |
|
||||
44
.github/workflows/vitest-tests.yaml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Vitest Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, dev*, core/*, desktop/*]
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
coverage
|
||||
.vitest-cache
|
||||
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
vitest-cache-${{ runner.os }}-
|
||||
test-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Vitest tests
|
||||
run: pnpm test:unit
|
||||
46
.github/workflows/vitest.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Vitest Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master, dev*, core/*, desktop/* ]
|
||||
pull_request:
|
||||
branches-ignore: [ wip/*, draft/*, temp/* ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
coverage
|
||||
.vitest-cache
|
||||
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
vitest-cache-${{ runner.os }}-
|
||||
test-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run Vitest tests
|
||||
run: |
|
||||
pnpm test:component
|
||||
pnpm test:unit
|
||||
1
.gitignore
vendored
@@ -31,7 +31,6 @@ CLAUDE.local.md
|
||||
*.code-workspace
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.json
|
||||
!.vscode/custom-css.json
|
||||
!.vscode/settings.json.default
|
||||
!.vscode/launch.json.default
|
||||
.idea
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- `pnpm storybook`: Start Storybook development server
|
||||
- `pnpm build-storybook`: Build static Storybook
|
||||
- `pnpm test:unit`: Run unit tests (includes Storybook components)
|
||||
- `pnpm test:component`: Run component tests (includes Storybook components)
|
||||
|
||||
## Development Workflow for Storybook
|
||||
|
||||
|
||||
50
.vscode/custom-css.json
vendored
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"properties": [
|
||||
{
|
||||
"name": "app-region",
|
||||
"description": "Electron-specific CSS property that defines draggable regions in custom title bar windows. Setting 'drag' marks a rectangular area as draggable for moving the window; 'no-drag' excludes areas from the draggable region.",
|
||||
"values": [
|
||||
{
|
||||
"name": "drag",
|
||||
"description": "Marks the element as draggable for moving the Electron window"
|
||||
},
|
||||
{
|
||||
"name": "no-drag",
|
||||
"description": "Excludes the element from being used to drag the Electron window"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "Electron Window Customization",
|
||||
"url": "https://www.electronjs.org/docs/latest/tutorial/window-customization"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "speak",
|
||||
"description": "Deprecated CSS2 aural stylesheet property for controlling screen reader speech. Use ARIA attributes instead.",
|
||||
"values": [
|
||||
{
|
||||
"name": "auto",
|
||||
"description": "Content is read aurally if element is not a block and is visible"
|
||||
},
|
||||
{
|
||||
"name": "never",
|
||||
"description": "Content will not be read aurally"
|
||||
},
|
||||
{
|
||||
"name": "always",
|
||||
"description": "Content will be read aurally regardless of display settings"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "CSS-Tricks Reference",
|
||||
"url": "https://css-tricks.com/almanac/properties/s/speak/"
|
||||
}
|
||||
],
|
||||
"status": "obsolete"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json.default
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/tailwind.json",
|
||||
".vscode/custom-css.json"
|
||||
".vscode/tailwind.json"
|
||||
]
|
||||
}
|
||||
|
||||
36
.vscode/tailwind.json
vendored
@@ -7,7 +7,7 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#import-directive"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#import"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#theme-directive"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#theme"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -27,17 +27,17 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/theme#layers"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#layer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "DO NOT USE. IF YOU ARE CAUGHT USING @apply YOU WILL FACE SEVERE CONSEQUENCES.",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply-directive"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -47,7 +47,7 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#config-directive"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#config"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -57,7 +57,7 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#reference-directive"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#reference"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -67,27 +67,7 @@
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#plugin-directive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@custom-variant",
|
||||
"description": "Use the `@custom-variant` directive to add a custom variant to your project. Custom variants can be used with utilities like `hover`, `focus`, and responsive breakpoints. Use `@slot` inside the variant to indicate where the utility's styles should be inserted.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/adding-custom-styles#adding-custom-variants"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@utility",
|
||||
"description": "Use the `@utility` directive to add custom utilities to your project. Custom utilities work with all variants like `hover`, `focus`, and responsive variants. Use `--value()` to create functional utilities that accept arguments.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/adding-custom-styles#adding-custom-utilities"
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
- `pnpm dev:electron`: Dev server with Electron API mocks.
|
||||
- `pnpm build`: Type-check then production build to `dist/`.
|
||||
- `pnpm preview`: Preview the production build locally.
|
||||
- `pnpm test:unit`: Run Vitest unit tests.
|
||||
- `pnpm test:unit`: Run Vitest unit tests (`tests-ui/`).
|
||||
- `pnpm test:component`: Run component tests (`src/components/`).
|
||||
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
|
||||
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
|
||||
- `pnpm typecheck`: Vue TSC type checking.
|
||||
|
||||
@@ -18,6 +18,7 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
|
||||
- `pnpm build`: Build for production (via nx)
|
||||
- `pnpm lint`: Linting (via nx)
|
||||
- `pnpm format`: Prettier formatting
|
||||
- `pnpm test:component`: Run component tests with browser environment
|
||||
- `pnpm test:unit`: Run all unit tests
|
||||
- `pnpm test:browser`: Run E2E tests via Playwright
|
||||
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
|
||||
|
||||
@@ -213,6 +213,12 @@ Here's how Claude Code can use the Playwright MCP server to inspect the interfac
|
||||
- `pnpm i` to install all dependencies
|
||||
- `pnpm test:unit` to execute all unit tests
|
||||
|
||||
### Component Tests
|
||||
|
||||
Component tests verify Vue components in `src/components/`.
|
||||
|
||||
- `pnpm test:component` to execute all component tests
|
||||
|
||||
### Playwright Tests
|
||||
|
||||
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details.
|
||||
@@ -223,6 +229,7 @@ Before submitting a PR, ensure all tests pass:
|
||||
|
||||
```bash
|
||||
pnpm test:unit
|
||||
pnpm test:component
|
||||
pnpm test:browser
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
|
||||
@@ -16,7 +16,7 @@ Without this flag, parallel tests will conflict and fail randomly.
|
||||
|
||||
### ComfyUI devtools
|
||||
|
||||
ComfyUI_devtools is included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
|
||||
ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
|
||||
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
|
||||
|
||||
For local development, copy the devtools files to your ComfyUI installation:
|
||||
|
||||
@@ -151,8 +151,7 @@ class NodeSlotReference {
|
||||
const convertedPos =
|
||||
window['app'].canvas.ds.convertOffsetToCanvas(rawPos)
|
||||
|
||||
// Debug logging - convert Float64Arrays to regular arrays for visibility
|
||||
// eslint-disable-next-line no-console
|
||||
// Debug logging - convert Float32Arrays to regular arrays for visibility
|
||||
console.log(
|
||||
`NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
|
||||
{
|
||||
|
||||
@@ -53,10 +53,6 @@ test.describe('DOM Widget', () => {
|
||||
})
|
||||
|
||||
test('should reposition when layout changes', async ({ comfyPage }) => {
|
||||
test.skip(
|
||||
true,
|
||||
'Only recalculates when the Canvas size changes, need to recheck the logic'
|
||||
)
|
||||
// --- setup ---
|
||||
|
||||
const textareaWidget = comfyPage.page
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { Position } from '../../../../fixtures/types'
|
||||
|
||||
test.describe('Vue Node Moving', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
const getLoadCheckpointHeaderPos = async (comfyPage: ComfyPage) => {
|
||||
const loadCheckpointHeaderPos = await comfyPage.page
|
||||
.getByText('Load Checkpoint')
|
||||
.boundingBox()
|
||||
|
||||
if (!loadCheckpointHeaderPos)
|
||||
throw new Error('Load Checkpoint header not found')
|
||||
|
||||
return loadCheckpointHeaderPos
|
||||
}
|
||||
|
||||
const expectPosChanged = async (pos1: Position, pos2: Position) => {
|
||||
const diffX = Math.abs(pos2.x - pos1.x)
|
||||
const diffY = Math.abs(pos2.y - pos1.y)
|
||||
expect(diffX).toBeGreaterThan(0)
|
||||
expect(diffY).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
|
||||
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
|
||||
x: 256,
|
||||
y: 256
|
||||
})
|
||||
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
|
||||
})
|
||||
|
||||
test('@mobile should allow moving nodes by dragging on touch devices', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Disable minimap (gets in way of the node on small screens)
|
||||
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
|
||||
|
||||
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await comfyPage.panWithTouch(
|
||||
{
|
||||
x: 64,
|
||||
y: 64
|
||||
},
|
||||
loadCheckpointHeaderPos
|
||||
)
|
||||
|
||||
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
|
||||
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-moved-node-touch.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
@@ -37,9 +37,7 @@ export default defineConfig([
|
||||
allowDefaultProject: [
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts'
|
||||
'vite.types.config.mts'
|
||||
]
|
||||
},
|
||||
tsConfigRootDir: import.meta.dirname,
|
||||
@@ -92,7 +90,6 @@ export default defineConfig([
|
||||
}
|
||||
],
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||
@@ -210,11 +207,5 @@ export default defineConfig([
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.{test,spec,stories}.ts', '**/*.stories.vue'],
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -20,6 +20,7 @@ const config: KnipConfig = {
|
||||
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
|
||||
},
|
||||
'packages/registry-types': {
|
||||
entry: ['src/comfyRegistryTypes.ts'],
|
||||
project: ['src/**/*.{js,ts}']
|
||||
}
|
||||
},
|
||||
|
||||
213
package.json
@@ -1,120 +1,123 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.28.5",
|
||||
"version": "1.28.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"build-storybook": "storybook build",
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build": "pnpm typecheck && nx build",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
"dev": "nx serve",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
"build": "pnpm typecheck && nx build",
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"lint:fix": "eslint src --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "eslint src --cache",
|
||||
"locale": "lobe-i18n locale",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:all": "nx run test",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:component": "nx run test src/components/",
|
||||
"test:litegraph": "vitest run --config vitest.litegraph.config.ts",
|
||||
"test:unit": "nx run test tests-ui/tests",
|
||||
"preinstall": "pnpm dlx 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:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:no-cache": "eslint src",
|
||||
"lint:fix:no-cache": "eslint src --fix",
|
||||
"knip": "knip --cache",
|
||||
"knip:no-cache": "knip",
|
||||
"locale": "lobe-i18n locale",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"storybook": "nx storybook -p 6006",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:unit": "nx run test",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"zipdist": "node scripts/zipdist.js"
|
||||
"build-storybook": "storybook build",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "catalog:",
|
||||
"@intlify/eslint-plugin-vue-i18n": "catalog:",
|
||||
"@lobehub/i18n-cli": "catalog:",
|
||||
"@nx/eslint": "catalog:",
|
||||
"@nx/playwright": "catalog:",
|
||||
"@nx/storybook": "catalog:",
|
||||
"@nx/vite": "catalog:",
|
||||
"@pinia/testing": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@storybook/addon-docs": "catalog:",
|
||||
"@storybook/vue3": "catalog:",
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@trivago/prettier-plugin-sort-imports": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@types/jsdom": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@types/three": "catalog:",
|
||||
"@vitejs/plugin-vue": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"@vitest/ui": "catalog:",
|
||||
"@vue/test-utils": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
|
||||
"@lobehub/i18n-cli": "^1.25.1",
|
||||
"@nx/eslint": "21.4.1",
|
||||
"@nx/playwright": "21.4.1",
|
||||
"@nx/storybook": "21.4.1",
|
||||
"@nx/vite": "21.4.1",
|
||||
"@pinia/testing": "^0.1.5",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@storybook/addon-docs": "^9.1.1",
|
||||
"@storybook/vue3": "^9.1.1",
|
||||
"@storybook/vue3-vite": "^9.1.1",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-storybook": "^9.1.6",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"globals": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
"jiti": "catalog:",
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"tailwindcss-primeui": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"tw-animate-css": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"typescript-eslint": "catalog:",
|
||||
"unplugin-icons": "catalog:",
|
||||
"unplugin-vue-components": "catalog:",
|
||||
"globals": "^15.9.0",
|
||||
"happy-dom": "^15.11.0",
|
||||
"husky": "^9.0.11",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"knip": "^5.62.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"nx": "21.4.1",
|
||||
"prettier": "^3.3.2",
|
||||
"storybook": "^9.1.6",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"tailwindcss-primeui": "^0.6.1",
|
||||
"tsx": "^4.15.6",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"unplugin-icons": "^0.22.0",
|
||||
"unplugin-vue-components": "^0.28.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:",
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-vue-devtools": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vue-component-type-helpers": "catalog:",
|
||||
"vue-eslint-parser": "catalog:",
|
||||
"vue-tsc": "catalog:",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-vue-devtools": "^7.7.6",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-component-type-helpers": "^3.0.7",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.7",
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "catalog:"
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "catalog:",
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "0.4.73-0",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@comfyorg/registry-types": "workspace:*",
|
||||
"@comfyorg/tailwind-utils": "workspace:*",
|
||||
"@iconify/json": "catalog:",
|
||||
"@primeuix/forms": "catalog:",
|
||||
"@primeuix/styled": "catalog:",
|
||||
"@primeuix/utils": "catalog:",
|
||||
"@primevue/core": "catalog:",
|
||||
"@primevue/forms": "catalog:",
|
||||
"@primevue/icons": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
"@sentry/vue": "catalog:",
|
||||
"@iconify/json": "^2.2.380",
|
||||
"@primeuix/forms": "0.0.2",
|
||||
"@primeuix/styled": "0.3.2",
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "^4.2.5",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/icons": "4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
@@ -122,39 +125,39 @@
|
||||
"@tiptap/extension-table-header": "^2.10.4",
|
||||
"@tiptap/extension-table-row": "^2.10.4",
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@vueuse/integrations": "^13.9.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"algoliasearch": "catalog:",
|
||||
"axios": "catalog:",
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"chart.js": "^4.5.0",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "catalog:",
|
||||
"dotenv": "^16.4.5",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"fast-glob": "^3.3.3",
|
||||
"firebase": "catalog:",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "^11.0.3",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"marked": "^15.0.11",
|
||||
"pinia": "catalog:",
|
||||
"primeicons": "catalog:",
|
||||
"primevue": "catalog:",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.5",
|
||||
"reka-ui": "^2.5.0",
|
||||
"semver": "^7.7.2",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "catalog:",
|
||||
"vue-i18n": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
"vuefire": "catalog:",
|
||||
"yjs": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"zod-validation-error": "catalog:"
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.3",
|
||||
"vue-router": "^4.4.3",
|
||||
"vuefire": "^3.2.1",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "Shared design system for ComfyUI Frontend",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./tailwind-config": "./tailwind.config.ts",
|
||||
"./tailwind-config": {
|
||||
"import": "./tailwind.config.ts",
|
||||
"types": "./tailwind.config.ts"
|
||||
},
|
||||
"./css/*": "./src/css/*"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -17,12 +20,12 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "catalog:",
|
||||
"@iconify/tailwind": "catalog:"
|
||||
"@iconify-json/lucide": "^1.1.178",
|
||||
"@iconify/tailwind": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1"
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Everything below here to be cleaned up over time. */
|
||||
/* Everthing below here to be cleaned up over time. */
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
|
||||
@@ -1125,7 +1125,7 @@ export interface paths {
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Create a new custom node using admin privilege */
|
||||
/** Create a new custom node using admin priviledge */
|
||||
post: operations['adminCreateNode']
|
||||
delete?: never
|
||||
options?: never
|
||||
@@ -16383,7 +16383,7 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Webhook processed successfully */
|
||||
/** @description Webhook processed succesfully */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"./networkUtil": "./src/networkUtil.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "catalog:"
|
||||
"axios": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
@@ -22,6 +25,6 @@
|
||||
"tailwind-merge": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
887
pnpm-lock.yaml
generated
@@ -2,115 +2,6 @@ packages:
|
||||
- apps/**
|
||||
- packages/**
|
||||
|
||||
catalog:
|
||||
# Core frameworks
|
||||
typescript: ^5.9.2
|
||||
vue: ^3.5.13
|
||||
|
||||
# Build tools
|
||||
'@nx/eslint': 21.4.1
|
||||
'@nx/playwright': 21.4.1
|
||||
'@nx/storybook': 21.4.1
|
||||
'@nx/vite': 21.4.1
|
||||
nx: 21.4.1
|
||||
tsx: ^4.15.6
|
||||
vite: ^5.4.19
|
||||
'@vitejs/plugin-vue': ^5.1.4
|
||||
'vite-plugin-dts': ^4.5.4
|
||||
vue-tsc: ^3.0.7
|
||||
|
||||
# Testing
|
||||
'happy-dom': ^15.11.0
|
||||
jsdom: ^26.1.0
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@playwright/test': ^1.52.0
|
||||
'@vitest/coverage-v8': ^3.2.4
|
||||
'@vitest/ui': ^3.0.0
|
||||
vitest: ^3.2.4
|
||||
'@vue/test-utils': ^2.4.6
|
||||
|
||||
# Linting & Formatting
|
||||
'@eslint/js': ^9.35.0
|
||||
eslint: ^9.34.0
|
||||
'eslint-config-prettier': ^10.1.8
|
||||
'eslint-plugin-prettier': ^5.5.4
|
||||
'eslint-plugin-storybook': ^9.1.6
|
||||
'eslint-plugin-unused-imports': ^4.2.0
|
||||
'eslint-plugin-vue': ^10.4.0
|
||||
globals: ^15.9.0
|
||||
'@intlify/eslint-plugin-vue-i18n': ^4.1.0
|
||||
prettier: ^3.3.2
|
||||
'typescript-eslint': ^8.44.0
|
||||
'vue-eslint-parser': ^10.2.0
|
||||
|
||||
# Vue ecosystem
|
||||
'@sentry/vue': ^8.48.0
|
||||
'@vueuse/core': ^11.0.0
|
||||
'@vueuse/integrations': ^13.9.0
|
||||
'vite-plugin-html': ^3.2.2
|
||||
'vite-plugin-vue-devtools': ^7.7.6
|
||||
pinia: ^2.1.7
|
||||
'vue-i18n': ^9.14.3
|
||||
'vue-router': ^4.4.3
|
||||
vuefire: ^3.2.1
|
||||
|
||||
# PrimeVue UI framework
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
'@primevue/core': ^4.2.5
|
||||
'@primevue/forms': ^4.2.5
|
||||
'@primevue/icons': 4.2.5
|
||||
'@primevue/themes': ^4.2.5
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
|
||||
# Tailwind CSS and design
|
||||
'@iconify/json': ^2.2.380
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/tailwind': ^1.1.3
|
||||
'@tailwindcss/vite': ^4.1.12
|
||||
tailwindcss: ^4.1.12
|
||||
'tailwindcss-primeui': ^0.6.1
|
||||
'tw-animate-css': ^1.3.8
|
||||
'unplugin-icons': ^0.22.0
|
||||
'unplugin-vue-components': ^0.28.0
|
||||
|
||||
# Storybook
|
||||
'@storybook/addon-docs': ^9.1.1
|
||||
storybook: ^9.1.6
|
||||
'@storybook/vue3': ^9.1.1
|
||||
'@storybook/vue3-vite': ^9.1.1
|
||||
|
||||
# Data and validation
|
||||
algoliasearch: ^5.21.0
|
||||
axios: ^1.8.2
|
||||
firebase: ^11.6.0
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
'zod-validation-error': ^3.3.0
|
||||
|
||||
# Dev tools
|
||||
dotenv: ^16.4.5
|
||||
husky: ^9.0.11
|
||||
jiti: 2.4.2
|
||||
knip: ^5.62.0
|
||||
'lint-staged': ^15.2.7
|
||||
|
||||
# Type definitions
|
||||
'@types/fs-extra': ^11.0.4
|
||||
'@types/jsdom': ^21.1.7
|
||||
'@types/node': ^20.14.8
|
||||
'@types/semver': ^7.7.0
|
||||
'@types/three': ^0.169.0
|
||||
'vue-component-type-helpers': ^3.0.7
|
||||
'zod-to-json-schema': ^3.24.1
|
||||
|
||||
# i18n
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@lobehub/i18n-cli': ^1.25.1
|
||||
'@trivago/prettier-plugin-sort-imports': ^5.2.0
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
|
||||
@@ -44,6 +44,7 @@ const showContextMenu = (event: MouseEvent) => {
|
||||
onMounted(() => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
console.log('ComfyUI Front-end version:', config.app_version)
|
||||
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
|
||||
@@ -69,7 +69,7 @@ const terminalCreated = (
|
||||
await loadLogEntries()
|
||||
} catch (err) {
|
||||
console.error('Error loading logs', err)
|
||||
// On older backends the endpoints won't exist
|
||||
// On older backends the endpoints wont exist
|
||||
errorMessage.value =
|
||||
'Unable to load logs, please ensure you have updated your ComfyUI backend.'
|
||||
return
|
||||
|
||||
@@ -172,6 +172,7 @@
|
||||
v-for="template in isLoading ? [] : displayTemplates"
|
||||
:key="template.name"
|
||||
ref="cardRefs"
|
||||
v-memo="[template.name, hoveredTemplate === template.name]"
|
||||
ratio="smallSquare"
|
||||
type="workflow-template-card"
|
||||
:data-testid="`template-workflow-${template.name}`"
|
||||
|
||||
@@ -45,39 +45,37 @@
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons (hidden if host not whitelisted) -->
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-if="ssoAllowed">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@@ -151,7 +149,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
|
||||
import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
@@ -167,7 +164,6 @@ const authActions = useFirebaseAuthActions()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname))
|
||||
|
||||
const toggleState = () => {
|
||||
isSignIn.value = !isSignIn.value
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
id="graph-canvas"
|
||||
ref="canvasRef"
|
||||
tabindex="1"
|
||||
class="absolute inset-0 size-full touch-none"
|
||||
class="align-top w-full h-full touch-none"
|
||||
/>
|
||||
|
||||
<!-- TransformPane for Vue node rendering -->
|
||||
@@ -43,6 +43,7 @@
|
||||
v-for="nodeData in allNodes"
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:readonly="false"
|
||||
:error="
|
||||
executionStore.lastExecutionError?.node_id === nodeData.id
|
||||
? 'Execution error'
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<ColorPickerButton v-if="showColorPicker" />
|
||||
<FrameNodes v-if="showFrameNodes" />
|
||||
<ConvertToSubgraphButton v-if="showConvertToSubgraph" />
|
||||
<ConfigureSubgraph v-if="showSubgraphButtons" />
|
||||
<PublishSubgraphButton v-if="showSubgraphButtons" />
|
||||
<PublishSubgraphButton v-if="showPublishSubgraph" />
|
||||
<MaskEditorButton v-if="showMaskEditor" />
|
||||
<VerticalDivider
|
||||
v-if="showAnyPrimaryActions && showAnyControlActions"
|
||||
@@ -51,7 +50,6 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import ConfigureSubgraph from '@/components/graph/selectionToolbox/ConfigureSubgraph.vue'
|
||||
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
|
||||
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
@@ -114,7 +112,7 @@ const showInfoButton = computed(() => !!nodeDef.value)
|
||||
const showColorPicker = computed(() => hasAnySelection.value)
|
||||
const showConvertToSubgraph = computed(() => hasAnySelection.value)
|
||||
const showFrameNodes = computed(() => hasMultipleSelection.value)
|
||||
const showSubgraphButtons = computed(() => isSingleSubgraph.value)
|
||||
const showPublishSubgraph = computed(() => isSingleSubgraph.value)
|
||||
|
||||
const showBypass = computed(
|
||||
() =>
|
||||
@@ -132,7 +130,7 @@ const showAnyPrimaryActions = computed(
|
||||
showColorPicker.value ||
|
||||
showConvertToSubgraph.value ||
|
||||
showFrameNodes.value ||
|
||||
showSubgraphButtons.value
|
||||
showPublishSubgraph.value
|
||||
)
|
||||
|
||||
const showAnyControlActions = computed(() => showBypass.value)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: $t('Edit Subgraph Widgets'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
icon="icon-[lucide--settings-2]"
|
||||
@click="showSubgraphNodeDialog"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
</script>
|
||||
@@ -68,7 +68,7 @@ const updateDomClipping = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const isSelected = selectedNode === widgetState.widget.node
|
||||
const isSelected = selectedNode === widget.node
|
||||
const renderArea = selectedNode?.renderArea
|
||||
const offset = lgCanvas.ds.offset
|
||||
const scale = lgCanvas.ds.scale
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { computed, provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
@@ -202,4 +202,12 @@ const selectedSort = ref<string>('popular')
|
||||
const selectedNavItem = ref<string | null>('installed')
|
||||
|
||||
const gridStyle = computed(() => createGridStyle())
|
||||
|
||||
watch(searchText, (newQuery) => {
|
||||
console.log('searchText:', searchText.value, newQuery)
|
||||
})
|
||||
|
||||
watch(searchQuery, (newQuery) => {
|
||||
console.log('searchQuery:', searchQuery.value, newQuery)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -86,7 +86,7 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
||||
const t = (k: string) => k
|
||||
|
||||
const onClose = () => {
|
||||
// OnClose handler for story
|
||||
console.log('OnClose invoked')
|
||||
}
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { Ref } from 'vue'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { Rect } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
@@ -71,7 +71,7 @@ export function useSelectionToolboxPosition(
|
||||
visible.value = true
|
||||
|
||||
// Get bounds for all selected items
|
||||
const allBounds: ReadOnlyRect[] = []
|
||||
const allBounds: Rect[] = []
|
||||
for (const item of selectableItems) {
|
||||
// Skip items without valid IDs
|
||||
if (item.id == null) continue
|
||||
@@ -89,7 +89,7 @@ export function useSelectionToolboxPosition(
|
||||
}
|
||||
} else {
|
||||
// Fallback to LiteGraph bounds for regular nodes or non-string IDs
|
||||
if (item instanceof LGraphNode || item instanceof LGraphGroup) {
|
||||
if (item instanceof LGraphNode) {
|
||||
const bounds = item.getBounding()
|
||||
allBounds.push([bounds[0], bounds[1], bounds[2], bounds[3]] as const)
|
||||
}
|
||||
|
||||
@@ -300,6 +300,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
const durationValue = String(durationWidget.value)
|
||||
const modelValue = String(modelWidget.value)
|
||||
console.log('modelValue', modelValue)
|
||||
console.log('modeValue', modeValue)
|
||||
console.log('durationValue', durationValue)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {
|
||||
@@ -353,6 +356,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const modeValue = String(modeWidget.value)
|
||||
const durationValue = String(durationWidget.value)
|
||||
const modelValue = String(modelWidget.value)
|
||||
console.log('modelValue', modelValue)
|
||||
console.log('modeValue', modeValue)
|
||||
console.log('durationValue', durationValue)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (
|
||||
@@ -558,6 +564,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
const model = String(modelWidget.value)
|
||||
const resolution = String(resolutionWidget.value).toLowerCase()
|
||||
const duration = String(durationWidget.value)
|
||||
console.log('model', model)
|
||||
console.log('resolution', resolution)
|
||||
console.log('duration', duration)
|
||||
|
||||
if (model.includes('ray-flash-2')) {
|
||||
if (duration.includes('5s')) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { promoteRecommendedWidgets } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -910,7 +909,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
promoteRecommendedWidgets(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -344,7 +344,7 @@ export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
|
||||
type: 'number',
|
||||
defaultValue: null,
|
||||
tooltip:
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reserved depending on your OS.'
|
||||
'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.'
|
||||
},
|
||||
|
||||
// Misc settings
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced, watchDebounced } from '@vueuse/core'
|
||||
import {
|
||||
computed,
|
||||
customRef,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
triggerRef
|
||||
} from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import SubgraphNodeWidget from '@/core/graph/subgraph/SubgraphNodeWidget.vue'
|
||||
import {
|
||||
type WidgetItem,
|
||||
demoteWidget,
|
||||
isRecommendedWidget,
|
||||
matchesPropertyItem,
|
||||
matchesWidgetItem,
|
||||
promoteWidget,
|
||||
widgetItemToProperty
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import {
|
||||
type ProxyWidgetsProperty,
|
||||
parseProxyWidgets
|
||||
} from '@/core/schemas/proxyWidget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const draggableItems = ref()
|
||||
const searchQuery = ref<string>('')
|
||||
const debouncedQuery = refDebounced(searchQuery, 200)
|
||||
const proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({
|
||||
get() {
|
||||
track()
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
},
|
||||
set(value?: ProxyWidgetsProperty) {
|
||||
trigger()
|
||||
const node = activeNode.value
|
||||
if (!value) return
|
||||
if (!node) {
|
||||
console.error('Attempted to toggle widgets with no node selected')
|
||||
return
|
||||
}
|
||||
node.properties.proxyWidgets = value
|
||||
}
|
||||
}))
|
||||
|
||||
const activeNode = computed(() => {
|
||||
const node = canvasStore.selectedItems[0]
|
||||
if (node instanceof SubgraphNode) return node
|
||||
useDialogStore().closeDialog()
|
||||
return undefined
|
||||
})
|
||||
|
||||
const activeWidgets = computed<WidgetItem[]>({
|
||||
get() {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return proxyWidgets.value.flatMap(([id, name]: [string, string]) => {
|
||||
const wNode = node.subgraph._nodes_by_id[id]
|
||||
if (!wNode?.widgets) return []
|
||||
const w = wNode.widgets.find((w) => w.name === name)
|
||||
if (!w) return []
|
||||
return [[wNode, w]]
|
||||
})
|
||||
},
|
||||
set(value: WidgetItem[]) {
|
||||
const node = activeNode.value
|
||||
if (!node) {
|
||||
console.error('Attempted to toggle widgets with no node selected')
|
||||
return
|
||||
}
|
||||
//map back to id/name
|
||||
const widgets: ProxyWidgetsProperty = value.map(widgetItemToProperty)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
})
|
||||
|
||||
const interiorWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
for (const node of interiorNodes) {
|
||||
node.updateComputedDisabled()
|
||||
updatePreviews(node)
|
||||
}
|
||||
return interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
.filter(([_, w]: WidgetItem) => !w.computedDisabled)
|
||||
})
|
||||
|
||||
const candidateWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
const widgets = proxyWidgets.value
|
||||
return interiorWidgets.value.filter(
|
||||
(widgetItem: WidgetItem) => !widgets.some(matchesPropertyItem(widgetItem))
|
||||
)
|
||||
})
|
||||
const filteredCandidates = computed<WidgetItem[]>(() => {
|
||||
const query = debouncedQuery.value.toLowerCase()
|
||||
if (!query) return candidateWidgets.value
|
||||
return candidateWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
const recommendedWidgets = computed(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return [] //Not reachable
|
||||
return filteredCandidates.value.filter(isRecommendedWidget)
|
||||
})
|
||||
|
||||
const filteredActive = computed<WidgetItem[]>(() => {
|
||||
const query = debouncedQuery.value.toLowerCase()
|
||||
if (!query) return activeWidgets.value
|
||||
return activeWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
function toKey(item: WidgetItem) {
|
||||
return `${item[0].id}: ${item[1].name}`
|
||||
}
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
if (!n.widgets) return []
|
||||
return n.widgets.map((w: IBaseWidget) => [n, w])
|
||||
}
|
||||
function demote([node, widget]: WidgetItem) {
|
||||
const subgraphNode = activeNode.value
|
||||
if (!subgraphNode) return []
|
||||
demoteWidget(node, widget, [subgraphNode])
|
||||
triggerRef(proxyWidgets)
|
||||
}
|
||||
function promote([node, widget]: WidgetItem) {
|
||||
const subgraphNode = activeNode.value
|
||||
if (!subgraphNode) return []
|
||||
promoteWidget(node, widget, [subgraphNode])
|
||||
triggerRef(proxyWidgets)
|
||||
}
|
||||
function showAll() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const widgets = proxyWidgets.value
|
||||
const toAdd: ProxyWidgetsProperty =
|
||||
filteredCandidates.value.map(widgetItemToProperty)
|
||||
widgets.push(...toAdd)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
function hideAll() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
//Not great from a nesting perspective, but path is cold
|
||||
//and it cleans up potential error states
|
||||
proxyWidgets.value = proxyWidgets.value.filter(
|
||||
(widgetItem) => !filteredActive.value.some(matchesWidgetItem(widgetItem))
|
||||
)
|
||||
}
|
||||
function showRecommended() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const widgets = proxyWidgets.value
|
||||
const toAdd: ProxyWidgetsProperty =
|
||||
recommendedWidgets.value.map(widgetItemToProperty)
|
||||
//TODO: Add sort step here
|
||||
//Input should always be before output by default
|
||||
widgets.push(...toAdd)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
|
||||
function setDraggableState() {
|
||||
draggableList.value?.dispose()
|
||||
if (debouncedQuery.value || !draggableItems.value?.children?.length) return
|
||||
draggableList.value = new DraggableList(
|
||||
draggableItems.value,
|
||||
'.draggable-item'
|
||||
)
|
||||
//Original implementation plays really poorly with vue,
|
||||
//It has been modified to not add/remove elements
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
const newPosition = reorderedItems.indexOf(this.draggableItem)
|
||||
const aw = activeWidgets.value
|
||||
const [w] = aw.splice(oldPosition, 1)
|
||||
aw.splice(newPosition, 0, w)
|
||||
activeWidgets.value = aw
|
||||
}
|
||||
}
|
||||
watchDebounced(
|
||||
filteredActive,
|
||||
() => {
|
||||
setDraggableState()
|
||||
},
|
||||
{ debounce: 100 }
|
||||
)
|
||||
onMounted(() => {
|
||||
setDraggableState()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
draggableList.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
class="p-2"
|
||||
:placeholder="$t('g.search') + '...'"
|
||||
/>
|
||||
<div
|
||||
v-if="filteredActive.length"
|
||||
class="pt-1 pb-4 border-b-1 border-sand-100 dark-theme:border-charcoal-600"
|
||||
>
|
||||
<div class="flex py-0 px-4 justify-between">
|
||||
<div class="text-slate-100 text-[9px] font-semibold uppercase">
|
||||
{{ $t('subgraphStore.shown') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-blue-100 text-[11px] font-normal"
|
||||
@click.stop="hideAll"
|
||||
>
|
||||
{{ $t('subgraphStore.hideAll') }}</a
|
||||
>
|
||||
</div>
|
||||
<div ref="draggableItems">
|
||||
<div
|
||||
v-for="[node, widget] in filteredActive"
|
||||
:key="toKey([node, widget])"
|
||||
class="w-full draggable-item"
|
||||
style=""
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-title="node.title"
|
||||
:widget-name="widget.name"
|
||||
:is-shown="true"
|
||||
:is-draggable="!debouncedQuery"
|
||||
@toggle-visibility="demote([node, widget])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredCandidates.length" class="pt-1 pb-4">
|
||||
<div class="flex py-0 px-4 justify-between">
|
||||
<div class="text-slate-100 text-[9px] font-semibold uppercase">
|
||||
{{ $t('subgraphStore.hidden') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-blue-100 text-[11px] font-normal"
|
||||
@click.stop="showAll"
|
||||
>
|
||||
{{ $t('subgraphStore.showAll') }}</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="[node, widget] in filteredCandidates"
|
||||
:key="toKey([node, widget])"
|
||||
class="w-full"
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-title="node.title"
|
||||
:widget-name="widget.name"
|
||||
@toggle-visibility="promote([node, widget])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="recommendedWidgets.length"
|
||||
class="justify-center flex py-4 border-t-1 border-sand-100 dark-theme:border-charcoal-600"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
class="rounded border-none px-3 py-0.5"
|
||||
@click.stop="showRecommended"
|
||||
>
|
||||
{{ $t('subgraphStore.showRecommended') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeTitle: string
|
||||
widgetName: string
|
||||
isShown?: boolean
|
||||
isDraggable?: boolean
|
||||
}>()
|
||||
defineEmits<{
|
||||
(e: 'toggleVisibility'): void
|
||||
}>()
|
||||
|
||||
function classes() {
|
||||
return cn(
|
||||
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
|
||||
'bg-pure-white dark-theme:bg-charcoal-800',
|
||||
props.isDraggable
|
||||
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
|
||||
: ''
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :class="classes()">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'size-4 pointer-events-none',
|
||||
isDraggable ? 'icon-[lucide--grip-vertical]' : ''
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="flex-1 pointer-events-none">
|
||||
<div class="text-slate-100 text-[10px]">{{ nodeTitle }}</div>
|
||||
<div class="text-xs">{{ widgetName }}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
text
|
||||
:icon="isDraggable ? 'icon-[lucide--eye]' : 'icon-[lucide--eye-off]'"
|
||||
severity="secondary"
|
||||
@click.stop="$emit('toggleVisibility')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +1,13 @@
|
||||
import { demoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { useNodeImage } from '@/composables/node/useNodeImage'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { disconnectedWidget } from '@/lib/litegraph/src/widgets/DisconnectedWidget'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
@@ -48,33 +43,14 @@ function isProxyWidget(w: IBaseWidget): w is ProxyWidget {
|
||||
return (w as { _overlay?: Overlay })?._overlay?.isProxyWidget ?? false
|
||||
}
|
||||
|
||||
export function registerProxyWidgets(canvas: LGraphCanvas) {
|
||||
//NOTE: canvasStore hasn't been initialized yet
|
||||
canvas.canvas.addEventListener<'subgraph-opened'>('subgraph-opened', (e) => {
|
||||
const { subgraph, fromNode } = e.detail
|
||||
const proxyWidgets = parseProxyWidgets(fromNode.properties.proxyWidgets)
|
||||
for (const node of subgraph.nodes) {
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.promoted = proxyWidgets.some(
|
||||
([n, w]) => node.id == n && widget.name == w
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
SubgraphNode.prototype.onConfigure = onConfigure
|
||||
}
|
||||
|
||||
const originalOnConfigure = SubgraphNode.prototype.onConfigure
|
||||
const onConfigure = function (
|
||||
this: LGraphNode,
|
||||
serialisedNode: ISerialisedNode
|
||||
) {
|
||||
SubgraphNode.prototype.onConfigure = function (serialisedNode) {
|
||||
if (!this.isSubgraphNode())
|
||||
throw new Error("Can't add proxyWidgets to non-subgraphNode")
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
//Must give value to proxyWidgets prior to defining or it won't serialize
|
||||
this.properties.proxyWidgets ??= []
|
||||
this.properties.proxyWidgets ??= '[]'
|
||||
let proxyWidgets = this.properties.proxyWidgets
|
||||
|
||||
originalOnConfigure?.call(this, serialisedNode)
|
||||
@@ -86,16 +62,13 @@ const onConfigure = function (
|
||||
set: (property: string) => {
|
||||
const parsed = parseProxyWidgets(property)
|
||||
const { deactivateWidget, setWidget } = useDomWidgetStore()
|
||||
const isActiveGraph = useCanvasStore().canvas?.graph === this.graph
|
||||
if (isActiveGraph) {
|
||||
for (const w of this.widgets.filter((w) => isProxyWidget(w))) {
|
||||
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
||||
}
|
||||
for (const w of this.widgets.filter((w) => isProxyWidget(w))) {
|
||||
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
||||
}
|
||||
this.widgets = this.widgets.filter((w) => !isProxyWidget(w))
|
||||
for (const [nodeId, widgetName] of parsed) {
|
||||
const w = addProxyWidget(this, `${nodeId}`, widgetName)
|
||||
if (isActiveGraph && w instanceof DOMWidgetImpl) setWidget(w)
|
||||
if (w instanceof DOMWidgetImpl) setWidget(w)
|
||||
}
|
||||
proxyWidgets = property
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
@@ -113,23 +86,19 @@ function addProxyWidget(
|
||||
) {
|
||||
const name = `${nodeId}: ${widgetName}`
|
||||
const overlay = {
|
||||
//items specific for proxy management
|
||||
nodeId,
|
||||
graph: subgraphNode.subgraph,
|
||||
widgetName,
|
||||
//Items which normally exist on widgets
|
||||
afterQueued: undefined,
|
||||
computedHeight: undefined,
|
||||
isProxyWidget: true,
|
||||
label: name,
|
||||
last_y: undefined,
|
||||
graph: subgraphNode.subgraph,
|
||||
name,
|
||||
node: subgraphNode,
|
||||
onRemove: undefined,
|
||||
promoted: undefined,
|
||||
serialize: false,
|
||||
label: name,
|
||||
isProxyWidget: true,
|
||||
y: 0,
|
||||
last_y: undefined,
|
||||
width: undefined,
|
||||
y: 0
|
||||
computedHeight: undefined,
|
||||
afterQueued: undefined,
|
||||
onRemove: undefined,
|
||||
node: subgraphNode
|
||||
}
|
||||
return addProxyFromOverlay(subgraphNode, overlay)
|
||||
}
|
||||
@@ -141,20 +110,23 @@ function resolveLinkedWidget(
|
||||
if (!n) return [undefined, undefined]
|
||||
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
|
||||
}
|
||||
|
||||
function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
let [linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||
let backingWidget = linkedWidget ?? disconnectedWidget
|
||||
if (overlay.widgetName.startsWith('$$')) {
|
||||
if (overlay.widgetName == '$$canvas-image-preview')
|
||||
overlay.node = new Proxy(subgraphNode, {
|
||||
get(_t, p) {
|
||||
if (p !== 'imgs') return Reflect.get(subgraphNode, p)
|
||||
if (!linkedNode) return []
|
||||
const images =
|
||||
useNodeOutputStore().getNodeOutputs(linkedNode)?.images ?? []
|
||||
if (images !== linkedNode.images) {
|
||||
linkedNode.images = images
|
||||
useNodeImage(linkedNode).showPreview()
|
||||
}
|
||||
return linkedNode.imgs
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* A set of handlers which define widget interaction
|
||||
* Many arguments are shared between function calls
|
||||
@@ -163,7 +135,7 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
* @param {string} property - The name of the accessed value.
|
||||
* Checked for conditional logic, but never changed
|
||||
* @param {object} receiver - The object the result is set to
|
||||
* and the value used as 'this' if property is a get/set method
|
||||
* and the vlaue used as 'this' if property is a get/set method
|
||||
* @param {unknown} value - only used on set calls. The thing being assigned
|
||||
*/
|
||||
const handler = {
|
||||
@@ -183,12 +155,6 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
let redirectedReceiver = receiver
|
||||
if (property == 'value') redirectedReceiver = backingWidget
|
||||
else if (property == 'computedHeight') {
|
||||
if (overlay.widgetName.startsWith('$$') && linkedNode) {
|
||||
updatePreviews(linkedNode)
|
||||
}
|
||||
if (linkedNode && linkedWidget?.computedDisabled) {
|
||||
demoteWidget(linkedNode, linkedWidget, [subgraphNode])
|
||||
}
|
||||
//update linkage regularly, but no more than once per frame
|
||||
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||
backingWidget = linkedWidget ?? disconnectedWidget
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import {
|
||||
type ProxyWidgetsProperty,
|
||||
parseProxyWidgets
|
||||
} from '@/core/schemas/proxyWidget'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
export type WidgetItem = [LGraphNode, IBaseWidget]
|
||||
|
||||
function getProxyWidgets(node: SubgraphNode) {
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
}
|
||||
export function promoteWidget(
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) {
|
||||
const proxyWidgets = [
|
||||
...getProxyWidgets(parent),
|
||||
widgetItemToProperty([node, widget])
|
||||
]
|
||||
parent.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
widget.promoted = true
|
||||
}
|
||||
|
||||
export function demoteWidget(
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) {
|
||||
const proxyWidgets = getProxyWidgets(parent).filter(
|
||||
(widgetItem) => !matchesPropertyItem([node, widget])(widgetItem)
|
||||
)
|
||||
parent.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
widget.promoted = false
|
||||
}
|
||||
|
||||
export function matchesWidgetItem([nodeId, widgetName]: [string, string]) {
|
||||
return ([n, w]: WidgetItem) => n.id == nodeId && w.name === widgetName
|
||||
}
|
||||
export function matchesPropertyItem([n, w]: WidgetItem) {
|
||||
return ([nodeId, widgetName]: [string, string]) =>
|
||||
n.id == nodeId && w.name === widgetName
|
||||
}
|
||||
export function widgetItemToProperty([n, w]: WidgetItem): [string, string] {
|
||||
return [`${n.id}`, w.name]
|
||||
}
|
||||
|
||||
function getParentNodes(): SubgraphNode[] {
|
||||
//NOTE: support for determining parents of a subgraph is limited
|
||||
//This function will require rework to properly support linked subgraphs
|
||||
//Either by including actual parents in the navigation stack,
|
||||
//or by adding a new event for parent listeners to collect from
|
||||
const { navigationStack } = useSubgraphNavigationStore()
|
||||
const subgraph = navigationStack.at(-1)
|
||||
if (!subgraph) throw new Error("Can't promote widget when not in subgraph")
|
||||
const parentGraph = navigationStack.at(-2) ?? subgraph.rootGraph
|
||||
return parentGraph.nodes.filter(
|
||||
(node): node is SubgraphNode =>
|
||||
node.type === subgraph.id && node.isSubgraphNode()
|
||||
)
|
||||
}
|
||||
|
||||
export function addWidgetPromotionOptions(
|
||||
options: (IContextMenuValue<unknown> | null)[],
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode
|
||||
) {
|
||||
const parents = getParentNodes()
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
options.unshift({
|
||||
content: `Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
}
|
||||
})
|
||||
else {
|
||||
options.unshift({
|
||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
demoteWidget(node, widget, parents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
'LoadImage',
|
||||
'SaveImage',
|
||||
'PreviewImage'
|
||||
]
|
||||
const recommendedWidgetNames = ['seed']
|
||||
export function isRecommendedWidget([node, widget]: WidgetItem) {
|
||||
return (
|
||||
!widget.computedDisabled &&
|
||||
(recommendedNodes.includes(node.type) ||
|
||||
recommendedWidgetNames.includes(widget.name))
|
||||
)
|
||||
}
|
||||
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
return n.widgets?.map((w: IBaseWidget) => [n, w]) ?? []
|
||||
}
|
||||
export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
const interiorNodes = subgraphNode.subgraph.nodes
|
||||
for (const node of interiorNodes) {
|
||||
node.updateComputedDisabled()
|
||||
//NOTE: Since this operation is async, previews still don't exist after the single frame
|
||||
//Add an onLoad callback to updatePreviews?
|
||||
updatePreviews(node)
|
||||
}
|
||||
const filteredWidgets: WidgetItem[] = interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
.filter(isRecommendedWidget)
|
||||
const proxyWidgets: ProxyWidgetsProperty =
|
||||
filteredWidgets.map(widgetItemToProperty)
|
||||
subgraphNode.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import SubgraphNode from '@/core/graph/subgraph/SubgraphNode.vue'
|
||||
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const key = 'global-subgraph-node-config'
|
||||
|
||||
export function showSubgraphNodeDialog() {
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogComponentProps: DialogComponentProps = {
|
||||
modal: false,
|
||||
position: 'topright',
|
||||
pt: {
|
||||
root: {
|
||||
class: 'bg-pure-white dark-theme:bg-charcoal-800 mt-22'
|
||||
},
|
||||
header: {
|
||||
class: 'h-8 text-xs ml-3'
|
||||
}
|
||||
}
|
||||
}
|
||||
dialogStore.showDialog({
|
||||
title: 'Parameters',
|
||||
key,
|
||||
component: SubgraphNode,
|
||||
dialogComponentProps
|
||||
})
|
||||
}
|
||||
@@ -4,12 +4,18 @@ import { fromZodError } from 'zod-validation-error'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
const proxyWidgetsPropertySchema = z.array(z.tuple([z.string(), z.string()]))
|
||||
export type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
|
||||
export function parseProxyWidgets(
|
||||
property: NodeProperty | undefined
|
||||
): ProxyWidgetsProperty {
|
||||
const result = proxyWidgetsPropertySchema.safeParse(property)
|
||||
if (typeof property !== 'string') {
|
||||
throw new Error(
|
||||
'Invalid assignment for properties.proxyWidgets:\nValue must be a string'
|
||||
)
|
||||
}
|
||||
const parsed = JSON.parse(property)
|
||||
const result = proxyWidgetsPropertySchema.safeParse(parsed)
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
|
||||
@@ -147,7 +147,7 @@ app.registerExtension({
|
||||
// @ts-expect-error fixme ts strict error
|
||||
node[WEBCAM_READY].then((v) => {
|
||||
video = v
|
||||
// If width isn't specified then use video output resolution
|
||||
// If width isnt specified then use video output resolution
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (!w.value) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
|
||||
@@ -149,7 +149,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
target_slot: number
|
||||
) {
|
||||
// Fires before the link is made allowing us to reject it if it isn't valid
|
||||
// No widget, we can't connect
|
||||
// No widget, we cant connect
|
||||
if (!input.widget && !(input.type in ComfyWidgets)) {
|
||||
return false
|
||||
}
|
||||
@@ -388,7 +388,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
}
|
||||
|
||||
onLastDisconnect() {
|
||||
// We can't remove + re-add the output here as if you drag a link over the same link
|
||||
// We cant remove + re-add the output here as if you drag a link over the same link
|
||||
// it removes, then re-adds, causing it to break
|
||||
this.outputs[0].type = '*'
|
||||
this.outputs[0].name = 'connect to widget input'
|
||||
@@ -595,7 +595,7 @@ app.registerExtension({
|
||||
|
||||
this.graph?.add(node)
|
||||
|
||||
// Calculate a position that won't directly overlap another node
|
||||
// Calculate a position that wont directly overlap another node
|
||||
const pos: [number, number] = [
|
||||
this.pos[0] - node.size[0] - 30,
|
||||
this.pos[1]
|
||||
|
||||
@@ -146,8 +146,8 @@ Litegraph has no runtime dependencies. The build tooling has been tested on Node
|
||||
|
||||
Use GitHub actions to release normal versions.
|
||||
|
||||
1. Run the `Release a New Version` action, selecting the version increment type
|
||||
1. Merge the resolution PR
|
||||
1. Run the `Release a New Version` action, selecting the version incrment type
|
||||
1. Merge the resultion PR
|
||||
1. A GitHub release is automatically published on merge
|
||||
|
||||
### Pre-release
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Point, ReadOnlyRect, Rect } from './interfaces'
|
||||
import type { Point, Rect } from './interfaces'
|
||||
import { EaseFunction, Rectangle } from './litegraph'
|
||||
|
||||
export interface DragAndScaleState {
|
||||
@@ -188,10 +188,7 @@ export class DragAndScale {
|
||||
* Fits the view to the specified bounds.
|
||||
* @param bounds The bounds to fit the view to, defined by a rectangle.
|
||||
*/
|
||||
fitToBounds(
|
||||
bounds: ReadOnlyRect,
|
||||
{ zoom = 0.75 }: { zoom?: number } = {}
|
||||
): void {
|
||||
fitToBounds(bounds: Rect, { zoom = 0.75 }: { zoom?: number } = {}): void {
|
||||
const cw = this.element.width / window.devicePixelRatio
|
||||
const ch = this.element.height / window.devicePixelRatio
|
||||
let targetScale = this.scale
|
||||
@@ -223,7 +220,7 @@ export class DragAndScale {
|
||||
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
||||
*/
|
||||
animateToBounds(
|
||||
bounds: ReadOnlyRect,
|
||||
bounds: Readonly<Rect | Rectangle>,
|
||||
setDirty: () => void,
|
||||
{
|
||||
duration = 350,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
@@ -274,6 +275,8 @@ export class LGraph
|
||||
* @param o data from previous serialization [optional]
|
||||
*/
|
||||
constructor(o?: ISerialisedGraph | SerialisableGraph) {
|
||||
if (LiteGraph.debug) console.log('Graph created')
|
||||
|
||||
/** @see MapProxyHandler */
|
||||
const links = this._links
|
||||
MapProxyHandler.bindAllMethods(links)
|
||||
@@ -530,7 +533,7 @@ export class LGraph
|
||||
this.errors_in_execution = true
|
||||
if (LiteGraph.throw_errors) throw error
|
||||
|
||||
if (LiteGraph.debug) console.error('Error during execution:', error)
|
||||
if (LiteGraph.debug) console.log('Error during execution:', error)
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
@@ -1126,7 +1129,7 @@ export class LGraph
|
||||
/**
|
||||
* Snaps the provided items to a grid.
|
||||
*
|
||||
* Item positions are rounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
|
||||
* Item positions are reounded to the nearest multiple of {@link LiteGraph.CANVAS_GRID_SIZE}.
|
||||
*
|
||||
* When {@link LiteGraph.alwaysSnapToGrid} is enabled
|
||||
* and the grid size is falsy, a default of 1 is used.
|
||||
@@ -1165,7 +1168,7 @@ export class LGraph
|
||||
const ctor = LiteGraph.registered_node_types[node.type]
|
||||
if (node.constructor == ctor) continue
|
||||
|
||||
console.warn('node being replaced by newer version:', node.type)
|
||||
console.log('node being replaced by newer version:', node.type)
|
||||
const newnode = LiteGraph.createNode(node.type)
|
||||
if (!newnode) continue
|
||||
_nodes[i] = newnode
|
||||
@@ -1227,6 +1230,9 @@ export class LGraph
|
||||
|
||||
/* Called when something visually changed (not the graph!) */
|
||||
change(): void {
|
||||
if (LiteGraph.debug) {
|
||||
console.log('Graph changed')
|
||||
}
|
||||
this.canvasAction((c) => c.setDirty(true, true))
|
||||
this.on_change?.(this)
|
||||
}
|
||||
@@ -1621,6 +1627,12 @@ export class LGraph
|
||||
} else {
|
||||
throw new TypeError('Subgraph input node is not a SubgraphInput')
|
||||
}
|
||||
console.debug(
|
||||
'Reconnect input links in parent graph',
|
||||
{ ...link },
|
||||
this.links.get(link.id),
|
||||
this.links.get(link.id) === link
|
||||
)
|
||||
|
||||
for (const resolved of others) {
|
||||
resolved.link.disconnect(this)
|
||||
@@ -1696,7 +1708,12 @@ export class LGraph
|
||||
...subgraphNode.subgraph.groups
|
||||
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
|
||||
return {
|
||||
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
|
||||
boundingRect: new Rectangle(
|
||||
p.pos[0],
|
||||
p.pos[1],
|
||||
p.size?.[0] ?? 0,
|
||||
p.size?.[1] ?? 0
|
||||
)
|
||||
}
|
||||
})
|
||||
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]
|
||||
@@ -2222,7 +2239,7 @@ export class LGraph
|
||||
let node = LiteGraph.createNode(String(n_info.type), n_info.title)
|
||||
if (!node) {
|
||||
if (LiteGraph.debug)
|
||||
console.warn('Node not found or has errors:', n_info.type)
|
||||
console.log('Node not found or has errors:', n_info.type)
|
||||
|
||||
// in case of error we create a replacement node to avoid losing info
|
||||
node = new LGraphNode('')
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
import {
|
||||
type LinkRenderContext,
|
||||
LitegraphLinkAdapter
|
||||
} from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter'
|
||||
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
@@ -18,7 +17,6 @@ import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
|
||||
import { LLink, type LinkId } from './LLink'
|
||||
import { Reroute, type RerouteId } from './Reroute'
|
||||
import { LinkConnector } from './canvas/LinkConnector'
|
||||
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
||||
import { strokeShape } from './draw'
|
||||
import type {
|
||||
@@ -27,7 +25,6 @@ import type {
|
||||
} from './infrastructure/CustomEventTarget'
|
||||
import type { LGraphCanvasEventMap } from './infrastructure/LGraphCanvasEventMap'
|
||||
import { NullGraphError } from './infrastructure/NullGraphError'
|
||||
import { Rectangle } from './infrastructure/Rectangle'
|
||||
import type {
|
||||
CanvasColour,
|
||||
ColorOption,
|
||||
@@ -50,11 +47,10 @@ import type {
|
||||
NullableProperties,
|
||||
Point,
|
||||
Positionable,
|
||||
ReadOnlyRect,
|
||||
Rect,
|
||||
Size
|
||||
} from './interfaces'
|
||||
import { LiteGraph } from './litegraph'
|
||||
import { LiteGraph, Rectangle, SubgraphNode, createUuidv4 } from './litegraph'
|
||||
import {
|
||||
containsRect,
|
||||
createBounds,
|
||||
@@ -69,7 +65,6 @@ import { NodeInputSlot } from './node/NodeInputSlot'
|
||||
import type { Subgraph } from './subgraph/Subgraph'
|
||||
import { SubgraphIONodeBase } from './subgraph/SubgraphIONodeBase'
|
||||
import type { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
import { SubgraphNode } from './subgraph/SubgraphNode'
|
||||
import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode'
|
||||
import type {
|
||||
CanvasPointerEvent,
|
||||
@@ -91,7 +86,6 @@ import type { IBaseWidget } from './types/widgets'
|
||||
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
|
||||
import { findFirstNode, getAllNestedItems } from './utils/collections'
|
||||
import { resolveConnectingLinkColor } from './utils/linkColors'
|
||||
import { createUuidv4 } from './utils/uuid'
|
||||
import type { UUID } from './utils/uuid'
|
||||
import { BaseWidget } from './widgets/BaseWidget'
|
||||
import { toConcreteWidget } from './widgets/widgetMap'
|
||||
@@ -232,12 +226,6 @@ const cursors = {
|
||||
NW: 'nwse-resize'
|
||||
} as const
|
||||
|
||||
// Optimised buffers used during rendering
|
||||
const temp = new Rectangle()
|
||||
const temp_vec2: Point = [0, 0]
|
||||
const tmp_area = new Rectangle()
|
||||
const margin_area = new Rectangle()
|
||||
const link_bounding = new Rectangle()
|
||||
/**
|
||||
* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
|
||||
* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
|
||||
@@ -245,6 +233,13 @@ const link_bounding = new Rectangle()
|
||||
export class LGraphCanvas
|
||||
implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
{
|
||||
// Optimised buffers used during rendering
|
||||
static #temp = [0, 0, 0, 0] satisfies Rect
|
||||
static #temp_vec2 = [0, 0] satisfies Point
|
||||
static #tmp_area = [0, 0, 0, 0] satisfies Rect
|
||||
static #margin_area = [0, 0, 0, 0] satisfies Rect
|
||||
static #link_bounding = [0, 0, 0, 0] satisfies Rect
|
||||
|
||||
static DEFAULT_BACKGROUND_IMAGE =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
||||
|
||||
@@ -464,7 +459,7 @@ export class LGraphCanvas
|
||||
}
|
||||
|
||||
const baseFontSize = LiteGraph.NODE_TEXT_SIZE // 14px
|
||||
const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say basically a DPR of 2 increases the readability by 40%, 3 by 70%
|
||||
const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say bascially a DPR of 2 increases the readibility by 40%, 3 by 70%
|
||||
|
||||
// Calculate the zoom level where text becomes unreadable
|
||||
this._lowQualityZoomThreshold =
|
||||
@@ -550,7 +545,7 @@ export class LGraphCanvas
|
||||
linkMarkerShape: LinkMarkerShape = LinkMarkerShape.Circle
|
||||
links_render_mode: number
|
||||
/** Minimum font size in pixels before switching to low quality rendering.
|
||||
* This initializes first and if we can't get the value from the settings we default to 8px
|
||||
* This intializes first and if we cant get the value from the settings we default to 8px
|
||||
*/
|
||||
private _min_font_size_for_lod: number = 8
|
||||
|
||||
@@ -631,7 +626,7 @@ export class LGraphCanvas
|
||||
dirty_area?: Rect | null
|
||||
/** @deprecated Unused */
|
||||
node_in_panel?: LGraphNode | null
|
||||
last_mouse: Readonly<Point> = [0, 0]
|
||||
last_mouse: Point = [0, 0]
|
||||
last_mouseclick: number = 0
|
||||
graph: LGraph | Subgraph | null
|
||||
get _graph(): LGraph | Subgraph {
|
||||
@@ -1231,7 +1226,7 @@ export class LGraphCanvas
|
||||
className: 'event'
|
||||
})
|
||||
}
|
||||
// add callback for modifying the menu elements onMenuNodeOutputs
|
||||
// add callback for modifing the menu elements onMenuNodeOutputs
|
||||
const retEntries = node.onMenuNodeOutputs?.(entries)
|
||||
if (retEntries) entries = retEntries
|
||||
|
||||
@@ -1865,13 +1860,13 @@ export class LGraphCanvas
|
||||
this.#dirty()
|
||||
}
|
||||
|
||||
openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
|
||||
openSubgraph(subgraph: Subgraph): void {
|
||||
const { graph } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
const options = {
|
||||
bubbles: true,
|
||||
detail: { subgraph, closingGraph: graph, fromNode },
|
||||
detail: { subgraph, closingGraph: graph },
|
||||
cancelable: true
|
||||
}
|
||||
const mayContinue = this.canvas.dispatchEvent(
|
||||
@@ -2637,7 +2632,7 @@ export class LGraphCanvas
|
||||
pointer: CanvasPointer,
|
||||
node?: LGraphNode | undefined
|
||||
): void {
|
||||
const dragRect: Rect = [0, 0, 0, 0]
|
||||
const dragRect: [number, number, number, number] = [0, 0, 0, 0]
|
||||
|
||||
dragRect[0] = e.canvasX
|
||||
dragRect[1] = e.canvasY
|
||||
@@ -2797,7 +2792,7 @@ export class LGraphCanvas
|
||||
if (pos[1] < 0 && !inCollapse) {
|
||||
node.onNodeTitleDblClick?.(e, pos, this)
|
||||
} else if (node instanceof SubgraphNode) {
|
||||
this.openSubgraph(node.subgraph, node)
|
||||
this.openSubgraph(node.subgraph)
|
||||
}
|
||||
|
||||
node.onDblClick?.(e, pos, this)
|
||||
@@ -3177,7 +3172,7 @@ export class LGraphCanvas
|
||||
|
||||
LGraphCanvas.active_canvas = this
|
||||
this.adjustMouseEvent(e)
|
||||
const mouse: Readonly<Point> = [e.clientX, e.clientY]
|
||||
const mouse: Point = [e.clientX, e.clientY]
|
||||
this.mouse[0] = mouse[0]
|
||||
this.mouse[1] = mouse[1]
|
||||
const delta = [mouse[0] - this.last_mouse[0], mouse[1] - this.last_mouse[1]]
|
||||
@@ -3433,13 +3428,8 @@ export class LGraphCanvas
|
||||
|
||||
const deltaX = delta[0] / this.ds.scale
|
||||
const deltaY = delta[1] / this.ds.scale
|
||||
|
||||
if (LiteGraph.vueNodesMode) {
|
||||
this.moveChildNodesInGroupVueMode(allItems, deltaX, deltaY)
|
||||
} else {
|
||||
for (const item of allItems) {
|
||||
item.move(deltaX, deltaY, true)
|
||||
}
|
||||
for (const item of allItems) {
|
||||
item.move(deltaX, deltaY, true)
|
||||
}
|
||||
|
||||
this.#dirty()
|
||||
@@ -3910,7 +3900,7 @@ export class LGraphCanvas
|
||||
for (const item of [...parsed.nodes, ...parsed.reroutes]) {
|
||||
if (item.pos == null)
|
||||
throw new TypeError(
|
||||
'Invalid node encountered on paste. `pos` was null.'
|
||||
'Invalid node encounterd on paste. `pos` was null.'
|
||||
)
|
||||
|
||||
if (item.pos[0] < offsetX) offsetX = item.pos[0]
|
||||
@@ -4085,7 +4075,10 @@ export class LGraphCanvas
|
||||
this.setDirty(true)
|
||||
}
|
||||
|
||||
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect) {
|
||||
#handleMultiSelect(
|
||||
e: CanvasPointerEvent,
|
||||
dragRect: [number, number, number, number]
|
||||
) {
|
||||
// Process drag
|
||||
// Convert Point pair (pos, offset) to Rect
|
||||
const { graph, selectedItems, subgraph } = this
|
||||
@@ -4740,32 +4733,47 @@ export class LGraphCanvas
|
||||
for (const renderLink of renderLinks) {
|
||||
const {
|
||||
fromSlot,
|
||||
fromPos: pos,
|
||||
fromDirection,
|
||||
dragDirection
|
||||
fromPos: pos
|
||||
// fromDirection,
|
||||
// dragDirection
|
||||
} = renderLink
|
||||
const connShape = fromSlot.shape
|
||||
const connType = fromSlot.type
|
||||
|
||||
const colour = resolveConnectingLinkColor(connType)
|
||||
const color = resolveConnectingLinkColor(connType)
|
||||
|
||||
// the connection being dragged by the mouse
|
||||
if (this.linkRenderer) {
|
||||
this.linkRenderer.renderDraggingLink(
|
||||
ctx,
|
||||
pos,
|
||||
highlightPos,
|
||||
colour,
|
||||
fromDirection,
|
||||
dragDirection,
|
||||
{
|
||||
...this.buildLinkRenderContext(),
|
||||
linkMarkerShape: LinkMarkerShape.None
|
||||
}
|
||||
)
|
||||
if (
|
||||
this.linkRenderer &&
|
||||
renderLink.fromSlotIndex !== undefined &&
|
||||
renderLink.node !== undefined
|
||||
) {
|
||||
const { fromSlotIndex, node } = renderLink
|
||||
if (
|
||||
node instanceof LGraphNode &&
|
||||
('link' in fromSlot || 'links' in fromSlot)
|
||||
) {
|
||||
this.linkRenderer.renderDraggingLink(
|
||||
ctx,
|
||||
node,
|
||||
fromSlot,
|
||||
fromSlotIndex,
|
||||
highlightPos,
|
||||
this.buildLinkRenderContext(),
|
||||
{ fromInput: 'link' in fromSlot, color }
|
||||
// pos,
|
||||
// colour,
|
||||
// fromDirection,
|
||||
// dragDirection,
|
||||
// {
|
||||
// ...this.buildLinkRenderContext(),
|
||||
// linkMarkerShape: LinkMarkerShape.None
|
||||
// }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = colour
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||
@@ -4856,7 +4864,7 @@ export class LGraphCanvas
|
||||
}
|
||||
|
||||
/** Get the target snap / highlight point in graph space */
|
||||
#getHighlightPosition(): Readonly<Point> {
|
||||
#getHighlightPosition(): Point {
|
||||
return LiteGraph.snaps_for_comfy
|
||||
? this.linkConnector.state.snapLinksPos ??
|
||||
this._highlight_pos ??
|
||||
@@ -4871,7 +4879,7 @@ export class LGraphCanvas
|
||||
*/
|
||||
#renderSnapHighlight(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
highlightPos: Readonly<Point>
|
||||
highlightPos: Point
|
||||
): void {
|
||||
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
|
||||
if (!this._highlight_pos && !linkConnectorSnap) return
|
||||
@@ -5212,7 +5220,7 @@ export class LGraphCanvas
|
||||
|
||||
// clip if required (mask)
|
||||
const shape = node._shape || RenderShape.BOX
|
||||
const size = temp_vec2
|
||||
const size = LGraphCanvas.#temp_vec2
|
||||
size[0] = node.renderingSize[0]
|
||||
size[1] = node.renderingSize[1]
|
||||
|
||||
@@ -5408,8 +5416,11 @@ export class LGraphCanvas
|
||||
: true
|
||||
|
||||
// Normalised node dimensions
|
||||
const area = tmp_area
|
||||
area.set(node.boundingRect)
|
||||
const area = LGraphCanvas.#tmp_area
|
||||
area[0] = node.boundingRect[0]
|
||||
area[1] = node.boundingRect[1]
|
||||
area[2] = node.boundingRect[2]
|
||||
area[3] = node.boundingRect[3]
|
||||
area[0] -= node.pos[0]
|
||||
area[1] -= node.pos[1]
|
||||
|
||||
@@ -5510,8 +5521,11 @@ export class LGraphCanvas
|
||||
item: Positionable,
|
||||
shape = RenderShape.ROUND
|
||||
) {
|
||||
const snapGuide = temp
|
||||
snapGuide.set(item.boundingRect)
|
||||
const snapGuide = LGraphCanvas.#temp
|
||||
snapGuide[0] = item.boundingRect[0]
|
||||
snapGuide[1] = item.boundingRect[1]
|
||||
snapGuide[2] = item.boundingRect[2]
|
||||
snapGuide[3] = item.boundingRect[3]
|
||||
|
||||
// Not all items have pos equal to top-left of bounds
|
||||
const { pos } = item
|
||||
@@ -5557,10 +5571,10 @@ export class LGraphCanvas
|
||||
|
||||
const now = LiteGraph.getTime()
|
||||
const { visible_area } = this
|
||||
margin_area[0] = visible_area[0] - 20
|
||||
margin_area[1] = visible_area[1] - 20
|
||||
margin_area[2] = visible_area[2] + 40
|
||||
margin_area[3] = visible_area[3] + 40
|
||||
LGraphCanvas.#margin_area[0] = visible_area[0] - 20
|
||||
LGraphCanvas.#margin_area[1] = visible_area[1] - 20
|
||||
LGraphCanvas.#margin_area[2] = visible_area[2] + 40
|
||||
LGraphCanvas.#margin_area[3] = visible_area[3] + 40
|
||||
|
||||
// draw connections
|
||||
ctx.lineWidth = this.connections_width
|
||||
@@ -5781,13 +5795,18 @@ export class LGraphCanvas
|
||||
// Bounding box of all points (bezier overshoot on long links will be cut)
|
||||
const pointsX = points.map((x) => x[0])
|
||||
const pointsY = points.map((x) => x[1])
|
||||
link_bounding[0] = Math.min(...pointsX)
|
||||
link_bounding[1] = Math.min(...pointsY)
|
||||
link_bounding[2] = Math.max(...pointsX) - link_bounding[0]
|
||||
link_bounding[3] = Math.max(...pointsY) - link_bounding[1]
|
||||
LGraphCanvas.#link_bounding[0] = Math.min(...pointsX)
|
||||
LGraphCanvas.#link_bounding[1] = Math.min(...pointsY)
|
||||
LGraphCanvas.#link_bounding[2] =
|
||||
Math.max(...pointsX) - LGraphCanvas.#link_bounding[0]
|
||||
LGraphCanvas.#link_bounding[3] =
|
||||
Math.max(...pointsY) - LGraphCanvas.#link_bounding[1]
|
||||
|
||||
// skip links outside of the visible area of the canvas
|
||||
if (!overlapBounding(link_bounding, margin_area)) return
|
||||
if (
|
||||
!overlapBounding(LGraphCanvas.#link_bounding, LGraphCanvas.#margin_area)
|
||||
)
|
||||
return
|
||||
|
||||
const start_dir = startDirection || LinkDirection.RIGHT
|
||||
const end_dir = endDirection || LinkDirection.LEFT
|
||||
@@ -5946,8 +5965,8 @@ export class LGraphCanvas
|
||||
*/
|
||||
renderLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
a: Readonly<Point>,
|
||||
b: Readonly<Point>,
|
||||
a: Point,
|
||||
b: Point,
|
||||
link: LLink | null,
|
||||
skip_border: boolean,
|
||||
flow: number | null,
|
||||
@@ -5964,9 +5983,9 @@ export class LGraphCanvas
|
||||
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
||||
reroute?: Reroute
|
||||
/** Offset of the bezier curve control point from {@link a point a} (output side) */
|
||||
startControl?: Readonly<Point>
|
||||
startControl?: Point
|
||||
/** Offset of the bezier curve control point from {@link b point b} (input side) */
|
||||
endControl?: Readonly<Point>
|
||||
endControl?: Point
|
||||
/** Number of sublines (useful to represent vec3 or rgb) @todo If implemented, refactor calculations out of the loop */
|
||||
num_sublines?: number
|
||||
/** Whether this is a floating link segment */
|
||||
@@ -6410,7 +6429,7 @@ export class LGraphCanvas
|
||||
|
||||
return true
|
||||
}
|
||||
console.error(`failed creating ${nodeNewType}`)
|
||||
console.log(`failed creating ${nodeNewType}`)
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -6822,7 +6841,7 @@ export class LGraphCanvas
|
||||
canvas.focus()
|
||||
root_document.body.style.overflow = ''
|
||||
|
||||
// important, if canvas loses focus keys won't be captured
|
||||
// important, if canvas loses focus keys wont be captured
|
||||
setTimeout(() => canvas.focus(), 20)
|
||||
dialog.remove()
|
||||
}
|
||||
@@ -7099,7 +7118,7 @@ export class LGraphCanvas
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// console.warn("can't find slot " + options.slot_from);
|
||||
// console.warn("cant find slot " + options.slot_from);
|
||||
}
|
||||
}
|
||||
if (options.node_to) {
|
||||
@@ -7144,7 +7163,7 @@ export class LGraphCanvas
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// console.warn("can't find slot_nodeTO " + options.slot_from);
|
||||
// console.warn("cant find slot_nodeTO " + options.slot_from);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7482,7 +7501,7 @@ export class LGraphCanvas
|
||||
return dialog
|
||||
}
|
||||
|
||||
// TODO refactor, there are different dialog, some uses createDialog, some dont
|
||||
// TODO refactor, theer are different dialog, some uses createDialog, some dont
|
||||
createDialog(html: string, options: IDialogOptions): IDialog {
|
||||
const def_options = {
|
||||
checkForInput: false,
|
||||
@@ -8011,7 +8030,7 @@ export class LGraphCanvas
|
||||
if (Object.keys(this.selected_nodes).length > 1) {
|
||||
options.push(
|
||||
{
|
||||
content: 'Convert to Subgraph',
|
||||
content: 'Convert to Subgraph 🆕',
|
||||
callback: () => {
|
||||
if (!this.selectedItems.size)
|
||||
throw new Error('Convert to Subgraph: Nothing selected.')
|
||||
@@ -8046,7 +8065,7 @@ export class LGraphCanvas
|
||||
} else {
|
||||
options = [
|
||||
{
|
||||
content: 'Convert to Subgraph',
|
||||
content: 'Convert to Subgraph 🆕',
|
||||
callback: () => {
|
||||
// find groupnodes, degroup and select children
|
||||
if (this.selectedItems.size) {
|
||||
@@ -8437,7 +8456,7 @@ export class LGraphCanvas
|
||||
* Starts an animation to fit the view around the specified selection of nodes.
|
||||
* @param bounds The bounds to animate the view to, defined by a rectangle.
|
||||
*/
|
||||
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) {
|
||||
animateToBounds(bounds: Rect | Rectangle, options: AnimationOptions = {}) {
|
||||
const setDirty = () => this.setDirty(true, true)
|
||||
this.ds.animateToBounds(bounds, setDirty, options)
|
||||
}
|
||||
@@ -8459,120 +8478,4 @@ export class LGraphCanvas
|
||||
const setDirty = () => this.setDirty(true, true)
|
||||
this.ds.animateToBounds(bounds, setDirty, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate new position with delta
|
||||
*/
|
||||
private calculateNewPosition(
|
||||
node: LGraphNode,
|
||||
deltaX: number,
|
||||
deltaY: number
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: node.pos[0] + deltaX,
|
||||
y: node.pos[1] + deltaY
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply batched node position updates
|
||||
*/
|
||||
private applyNodePositionUpdates(
|
||||
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>,
|
||||
mutations: ReturnType<typeof useLayoutMutations>
|
||||
): void {
|
||||
for (const { node, newPos } of nodesToMove) {
|
||||
// Update LiteGraph position first so next drag uses correct base position
|
||||
node.pos[0] = newPos.x
|
||||
node.pos[1] = newPos.y
|
||||
// Then update layout store which will update Vue nodes
|
||||
mutations.moveNode(node.id, newPos)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize layout mutations with Canvas source
|
||||
*/
|
||||
private initLayoutMutations(): ReturnType<typeof useLayoutMutations> {
|
||||
const mutations = useLayoutMutations()
|
||||
mutations.setSource(LayoutSource.Canvas)
|
||||
return mutations
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all nodes that are children of groups in the selection
|
||||
*/
|
||||
private collectNodesInGroups(items: Set<Positionable>): Set<LGraphNode> {
|
||||
const nodesInGroups = new Set<LGraphNode>()
|
||||
for (const item of items) {
|
||||
if (item instanceof LGraphGroup) {
|
||||
for (const child of item._children) {
|
||||
if (child instanceof LGraphNode) {
|
||||
nodesInGroups.add(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodesInGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* Move group children (both nodes and non-nodes)
|
||||
*/
|
||||
private moveGroupChildren(
|
||||
group: LGraphGroup,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
nodesToMove: Array<{ node: LGraphNode; newPos: { x: number; y: number } }>
|
||||
): void {
|
||||
for (const child of group._children) {
|
||||
if (child instanceof LGraphNode) {
|
||||
const node = child as LGraphNode
|
||||
nodesToMove.push({
|
||||
node,
|
||||
newPos: this.calculateNewPosition(node, deltaX, deltaY)
|
||||
})
|
||||
} else {
|
||||
// Non-node children (nested groups, reroutes)
|
||||
child.move(deltaX, deltaY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveChildNodesInGroupVueMode(
|
||||
allItems: Set<Positionable>,
|
||||
deltaX: number,
|
||||
deltaY: number
|
||||
) {
|
||||
const mutations = this.initLayoutMutations()
|
||||
const nodesInMovingGroups = this.collectNodesInGroups(allItems)
|
||||
const nodesToMove: Array<{
|
||||
node: LGraphNode
|
||||
newPos: { x: number; y: number }
|
||||
}> = []
|
||||
|
||||
// First, collect all the moves we need to make
|
||||
for (const item of allItems) {
|
||||
const isNode = item instanceof LGraphNode
|
||||
if (isNode) {
|
||||
const node = item as LGraphNode
|
||||
if (nodesInMovingGroups.has(node)) {
|
||||
continue
|
||||
}
|
||||
nodesToMove.push({
|
||||
node,
|
||||
newPos: this.calculateNewPosition(node, deltaX, deltaY)
|
||||
})
|
||||
} else if (item instanceof LGraphGroup) {
|
||||
item.move(deltaX, deltaY, true)
|
||||
this.moveGroupChildren(item, deltaX, deltaY, nodesToMove)
|
||||
} else {
|
||||
// Other items (reroutes, etc.)
|
||||
item.move(deltaX, deltaY, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply all the node moves at once
|
||||
this.applyNodePositionUpdates(nodesToMove, mutations)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
|
||||
import type { LGraph } from './LGraph'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
@@ -13,7 +14,7 @@ import type {
|
||||
Positionable,
|
||||
Size
|
||||
} from './interfaces'
|
||||
import { LiteGraph, Rectangle } from './litegraph'
|
||||
import { LiteGraph } from './litegraph'
|
||||
import {
|
||||
containsCentre,
|
||||
containsRect,
|
||||
@@ -40,10 +41,15 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
title: string
|
||||
font?: string
|
||||
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
||||
_bounding = new Rectangle(10, 10, LGraphGroup.minWidth, LGraphGroup.minHeight)
|
||||
_bounding: [number, number, number, number] = [
|
||||
10,
|
||||
10,
|
||||
LGraphGroup.minWidth,
|
||||
LGraphGroup.minHeight
|
||||
]
|
||||
|
||||
_pos: Point = this._bounding.pos
|
||||
_size: Size = this._bounding.size
|
||||
_pos: Point = [10, 10]
|
||||
_size: Size = [LGraphGroup.minWidth, LGraphGroup.minHeight]
|
||||
/** @deprecated See {@link _children} */
|
||||
_nodes: LGraphNode[] = []
|
||||
_children: Set<Positionable> = new Set()
|
||||
@@ -102,12 +108,13 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
||||
}
|
||||
|
||||
get boundingRect() {
|
||||
return this._bounding
|
||||
}
|
||||
|
||||
getBounding() {
|
||||
return this._bounding
|
||||
get boundingRect(): Rectangle {
|
||||
return Rectangle.from([
|
||||
this._pos[0],
|
||||
this._pos[1],
|
||||
this._size[0],
|
||||
this._size[1]
|
||||
])
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
@@ -144,14 +151,17 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
configure(o: ISerialisedGroup): void {
|
||||
this.id = o.id
|
||||
this.title = o.title
|
||||
this._bounding.set(o.bounding)
|
||||
this._pos[0] = o.bounding[0]
|
||||
this._pos[1] = o.bounding[1]
|
||||
this._size[0] = o.bounding[2]
|
||||
this._size[1] = o.bounding[3]
|
||||
this.color = o.color
|
||||
this.flags = o.flags || this.flags
|
||||
if (o.font_size) this.font_size = o.font_size
|
||||
}
|
||||
|
||||
serialize(): ISerialisedGroup {
|
||||
const b = this._bounding
|
||||
const b = [this._pos[0], this._pos[1], this._size[0], this._size[1]]
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
@@ -209,7 +219,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
)
|
||||
|
||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||
strokeShape(ctx, this._bounding, {
|
||||
strokeShape(ctx, this.boundingRect, {
|
||||
title_height: this.titleHeight,
|
||||
padding
|
||||
})
|
||||
@@ -250,7 +260,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
|
||||
// Move nodes we overlap the centre point of
|
||||
for (const node of nodes) {
|
||||
if (containsCentre(this._bounding, node.boundingRect)) {
|
||||
if (containsCentre(this.boundingRect, node.boundingRect)) {
|
||||
this._nodes.push(node)
|
||||
children.add(node)
|
||||
}
|
||||
@@ -258,12 +268,13 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
|
||||
|
||||
// Move reroutes we overlap the centre point of
|
||||
for (const reroute of reroutes.values()) {
|
||||
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
|
||||
if (isPointInRect(reroute.pos, this.boundingRect)) children.add(reroute)
|
||||
}
|
||||
|
||||
// Move groups we wholly contain
|
||||
for (const group of groups) {
|
||||
if (containsRect(this._bounding, group._bounding)) children.add(group)
|
||||
if (containsRect(this.boundingRect, group.boundingRect))
|
||||
children.add(group)
|
||||
}
|
||||
|
||||
groups.sort((a, b) => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import type { Reroute, RerouteId } from './Reroute'
|
||||
import { getNodeInputOnPos, getNodeOutputOnPos } from './canvas/measureSlots'
|
||||
import type { IDrawBoundingOptions } from './draw'
|
||||
import { NullGraphError } from './infrastructure/NullGraphError'
|
||||
import type { ReadOnlyRectangle } from './infrastructure/Rectangle'
|
||||
import { Rectangle } from './infrastructure/Rectangle'
|
||||
import type {
|
||||
ColorOption,
|
||||
@@ -37,7 +36,6 @@ import type {
|
||||
ISlotType,
|
||||
Point,
|
||||
Positionable,
|
||||
ReadOnlyRect,
|
||||
Rect,
|
||||
Size
|
||||
} from './interfaces'
|
||||
@@ -166,8 +164,8 @@ input|output: every connection
|
||||
general properties:
|
||||
+ clip_area: if you render outside the node, it will be clipped
|
||||
+ unsafe_execution: not allowed for safe execution
|
||||
+ skip_repeated_outputs: when adding new outputs, it won't show if there is one already connected
|
||||
+ resizable: if set to false it won't be resizable with the mouse
|
||||
+ skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected
|
||||
+ resizable: if set to false it wont be resizable with the mouse
|
||||
+ widgets_start_y: widgets start at y distance from the top of the node
|
||||
|
||||
flags object:
|
||||
@@ -386,7 +384,7 @@ export class LGraphNode
|
||||
* Called once at the start of every frame. Caller may change the values in {@link out}, which will be reflected in {@link boundingRect}.
|
||||
* WARNING: Making changes to boundingRect via onBounding is poorly supported, and will likely result in strange behaviour.
|
||||
*/
|
||||
onBounding?(this: LGraphNode, out: Rect): void
|
||||
onBounding?(this: LGraphNode, out: Rectangle): void
|
||||
console?: string[]
|
||||
_level?: number
|
||||
_shape?: RenderShape
|
||||
@@ -412,12 +410,12 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link renderArea} */
|
||||
#renderArea = new Rectangle()
|
||||
#renderArea: [number, number, number, number] = [0, 0, 0, 0]
|
||||
/**
|
||||
* Rect describing the node area, including shadows and any protrusions.
|
||||
* Determines if the node is visible. Calculated once at the start of every frame.
|
||||
*/
|
||||
get renderArea(): ReadOnlyRect {
|
||||
get renderArea(): Rect {
|
||||
return this.#renderArea
|
||||
}
|
||||
|
||||
@@ -428,12 +426,12 @@ export class LGraphNode
|
||||
*
|
||||
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
|
||||
*/
|
||||
get boundingRect(): ReadOnlyRectangle {
|
||||
get boundingRect(): Rectangle {
|
||||
return this.#boundingRect
|
||||
}
|
||||
|
||||
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
|
||||
get boundingOffset(): Readonly<Point> {
|
||||
get boundingOffset(): Point {
|
||||
const {
|
||||
pos: [posX, posY],
|
||||
boundingRect: [bX, bY]
|
||||
@@ -441,10 +439,10 @@ export class LGraphNode
|
||||
return [posX - bX, posY - bY]
|
||||
}
|
||||
|
||||
/** {@link pos} and {@link size} values are backed by this {@link Rectangle}. */
|
||||
_posSize = new Rectangle()
|
||||
_pos: Point = this._posSize.pos
|
||||
_size: Size = this._posSize.size
|
||||
/** {@link pos} and {@link size} values are backed by this {@link Rect}. */
|
||||
_posSize: [number, number, number, number] = [0, 0, 0, 0]
|
||||
_pos: Point = [0, 0]
|
||||
_size: Size = [0, 0]
|
||||
|
||||
public get pos() {
|
||||
return this._pos
|
||||
@@ -901,7 +899,7 @@ export class LGraphNode
|
||||
|
||||
if (this.onSerialize?.(o))
|
||||
console.warn(
|
||||
"node onSerialize shouldn't return anything, data should be stored in the object pass in the first parameter"
|
||||
'node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter'
|
||||
)
|
||||
|
||||
return o
|
||||
@@ -1652,7 +1650,7 @@ export class LGraphNode
|
||||
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
|
||||
outputs ? outputs.length : 1
|
||||
)
|
||||
const size = out ?? [0, 0]
|
||||
const size = out || [0, 0]
|
||||
rows = Math.max(rows, 1)
|
||||
// although it should be graphcanvas.inner_text_font size
|
||||
const font_size = LiteGraph.NODE_TEXT_SIZE
|
||||
@@ -1949,7 +1947,7 @@ export class LGraphNode
|
||||
try {
|
||||
this.removeWidget(widget)
|
||||
} catch (error) {
|
||||
console.error('Failed to remove widget', error)
|
||||
console.debug('Failed to remove widget', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1977,7 +1975,7 @@ export class LGraphNode
|
||||
* @param out `x, y, width, height` are written to this array.
|
||||
* @param ctx The canvas context to use for measuring text.
|
||||
*/
|
||||
measure(out: Rect, ctx: CanvasRenderingContext2D): void {
|
||||
measure(out: Rectangle, ctx: CanvasRenderingContext2D): void {
|
||||
const titleMode = this.title_mode
|
||||
const renderTitle =
|
||||
titleMode != TitleMode.TRANSPARENT_TITLE &&
|
||||
@@ -2030,7 +2028,10 @@ export class LGraphNode
|
||||
this.onBounding?.(bounds)
|
||||
|
||||
const renderArea = this.#renderArea
|
||||
renderArea.set(bounds)
|
||||
renderArea[0] = bounds[0]
|
||||
renderArea[1] = bounds[1]
|
||||
renderArea[2] = bounds[2]
|
||||
renderArea[3] = bounds[3]
|
||||
// 4 offset for collapsed node connection points
|
||||
renderArea[0] -= 4
|
||||
renderArea[1] -= 4
|
||||
@@ -2350,7 +2351,7 @@ export class LGraphNode
|
||||
|
||||
/**
|
||||
* returns the output (or input) slot with a given type, -1 if not found
|
||||
* @param input use inputs instead of outputs
|
||||
* @param input uise inputs instead of outputs
|
||||
* @param type the type of the slot to find
|
||||
* @param returnObj if the obj itself wanted
|
||||
* @param preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway)
|
||||
@@ -2582,7 +2583,12 @@ export class LGraphNode
|
||||
if (slotIndex !== undefined)
|
||||
return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId)
|
||||
|
||||
// No compatible slot found - connection not possible
|
||||
console.debug(
|
||||
'[connectByType]: no way to connect type:',
|
||||
target_slotType,
|
||||
'to node:',
|
||||
target_node
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2615,7 +2621,7 @@ export class LGraphNode
|
||||
if (slotIndex !== undefined)
|
||||
return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId)
|
||||
|
||||
console.error(
|
||||
console.debug(
|
||||
'[connectByType]: no way to connect type:',
|
||||
source_slotType,
|
||||
'to node:',
|
||||
@@ -2655,7 +2661,7 @@ export class LGraphNode
|
||||
if (!graph) {
|
||||
// could be connected before adding it to a graph
|
||||
// due to link ids being associated with graphs
|
||||
console.error(
|
||||
console.log(
|
||||
"Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them."
|
||||
)
|
||||
return null
|
||||
@@ -2666,12 +2672,11 @@ export class LGraphNode
|
||||
slot = this.findOutputSlot(slot)
|
||||
if (slot == -1) {
|
||||
if (LiteGraph.debug)
|
||||
console.error(`Connect: Error, no slot of name ${slot}`)
|
||||
console.log(`Connect: Error, no slot of name ${slot}`)
|
||||
return null
|
||||
}
|
||||
} else if (!outputs || slot >= outputs.length) {
|
||||
if (LiteGraph.debug)
|
||||
console.error('Connect: Error, slot number not found')
|
||||
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2691,7 +2696,7 @@ export class LGraphNode
|
||||
targetIndex = target_node.findInputSlot(target_slot)
|
||||
if (targetIndex == -1) {
|
||||
if (LiteGraph.debug)
|
||||
console.error(`Connect: Error, no slot of name ${targetIndex}`)
|
||||
console.log(`Connect: Error, no slot of name ${targetIndex}`)
|
||||
return null
|
||||
}
|
||||
} else if (target_slot === LiteGraph.EVENT) {
|
||||
@@ -2723,8 +2728,7 @@ export class LGraphNode
|
||||
!target_node.inputs ||
|
||||
targetIndex >= target_node.inputs.length
|
||||
) {
|
||||
if (LiteGraph.debug)
|
||||
console.error('Connect: Error, slot number not found')
|
||||
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2910,7 +2914,7 @@ export class LGraphNode
|
||||
const fromLastFloatingReroute =
|
||||
parentReroute?.floating?.slotType === 'output'
|
||||
|
||||
// Adding from an output, or a floating reroute that is NOT the tip of an existing floating chain
|
||||
// Adding from an ouput, or a floating reroute that is NOT the tip of an existing floating chain
|
||||
if (afterRerouteId == null || !fromLastFloatingReroute) {
|
||||
const link = new LLink(
|
||||
-1,
|
||||
@@ -2951,12 +2955,11 @@ export class LGraphNode
|
||||
slot = this.findOutputSlot(slot)
|
||||
if (slot == -1) {
|
||||
if (LiteGraph.debug)
|
||||
console.error(`Connect: Error, no slot of name ${slot}`)
|
||||
console.log(`Connect: Error, no slot of name ${slot}`)
|
||||
return false
|
||||
}
|
||||
} else if (!this.outputs || slot >= this.outputs.length) {
|
||||
if (LiteGraph.debug)
|
||||
console.error('Connect: Error, slot number not found')
|
||||
if (LiteGraph.debug) console.log('Connect: Error, slot number not found')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3072,19 +3075,19 @@ export class LGraphNode
|
||||
slot = this.findInputSlot(slot)
|
||||
if (slot == -1) {
|
||||
if (LiteGraph.debug)
|
||||
console.error(`Connect: Error, no slot of name ${slot}`)
|
||||
console.log(`Connect: Error, no slot of name ${slot}`)
|
||||
return false
|
||||
}
|
||||
} else if (!this.inputs || slot >= this.inputs.length) {
|
||||
if (LiteGraph.debug) {
|
||||
console.error('Connect: Error, slot number not found')
|
||||
console.log('Connect: Error, slot number not found')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const input = this.inputs[slot]
|
||||
if (!input) {
|
||||
console.error('disconnectInput: input not found', slot, this.inputs)
|
||||
console.debug('disconnectInput: input not found', slot, this.inputs)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3113,16 +3116,19 @@ export class LGraphNode
|
||||
|
||||
const target_node = graph.getNodeById(link_info.origin_id)
|
||||
if (!target_node) {
|
||||
console.error(
|
||||
'disconnectInput: output not found',
|
||||
link_info.origin_slot
|
||||
console.debug(
|
||||
'disconnectInput: target node not found',
|
||||
link_info.origin_id
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const output = target_node.outputs[link_info.origin_slot]
|
||||
if (!output?.links?.length) {
|
||||
// Output not found - may have been removed
|
||||
console.debug(
|
||||
'disconnectInput: output not found',
|
||||
link_info.origin_slot
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3748,13 +3754,6 @@ export class LGraphNode
|
||||
return !isHidden
|
||||
}
|
||||
|
||||
updateComputedDisabled() {
|
||||
if (!this.widgets) return
|
||||
for (const widget of this.widgets)
|
||||
widget.computedDisabled =
|
||||
widget.disabled || this.getSlotFromWidget(widget)?.link != null
|
||||
}
|
||||
|
||||
drawWidgets(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ lowQuality = false, editorAlpha = 1 }: DrawWidgetsOptions
|
||||
@@ -3768,7 +3767,6 @@ export class LGraphNode
|
||||
ctx.save()
|
||||
ctx.globalAlpha = editorAlpha
|
||||
|
||||
this.updateComputedDisabled()
|
||||
for (const widget of widgets) {
|
||||
if (!this.isWidgetVisible(widget)) continue
|
||||
|
||||
@@ -3778,6 +3776,9 @@ export class LGraphNode
|
||||
: LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
|
||||
widget.last_y = y
|
||||
// Disable widget if it is disabled or if the value is passed from socket connection.
|
||||
widget.computedDisabled =
|
||||
widget.disabled || this.getSlotFromWidget(widget)?.link != null
|
||||
|
||||
ctx.strokeStyle = outlineColour
|
||||
ctx.fillStyle = '#222'
|
||||
@@ -3838,7 +3839,7 @@ export class LGraphNode
|
||||
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
#measureSlots(): ReadOnlyRect | null {
|
||||
#measureSlots(): Rect | null {
|
||||
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
|
||||
|
||||
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
ISlotType,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
Point,
|
||||
ReadonlyLinkNetwork
|
||||
} from './interfaces'
|
||||
import type {
|
||||
@@ -110,7 +109,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
data?: number | string | boolean | { toToolTip?(): string }
|
||||
_data?: unknown
|
||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||
_pos: Point
|
||||
_pos: [number, number]
|
||||
/** @todo Clean up - never implemented in comfy. */
|
||||
_last_time?: number
|
||||
/** The last canvas 2D path that was used to render this link */
|
||||
|
||||
@@ -70,7 +70,6 @@ export class LiteGraphGlobal {
|
||||
|
||||
WIDGET_BGCOLOR = '#222'
|
||||
WIDGET_OUTLINE_COLOR = '#666'
|
||||
WIDGET_PROMOTED_OUTLINE_COLOR = '#BF00FF'
|
||||
WIDGET_ADVANCED_OUTLINE_COLOR = 'rgba(56, 139, 253, 0.8)'
|
||||
WIDGET_TEXT_COLOR = '#DDD'
|
||||
WIDGET_SECONDARY_TEXT_COLOR = '#999'
|
||||
@@ -242,10 +241,10 @@ export class LiteGraphGlobal {
|
||||
*/
|
||||
do_add_triggers_slots = false
|
||||
|
||||
/** [false!] being events, it is strongly recommended to use them sequentially, one by one */
|
||||
/** [false!] being events, it is strongly reccomended to use them sequentially, one by one */
|
||||
allow_multi_output_for_events = true
|
||||
|
||||
/** [true!] allows to create and connect a node clicking with the third button (wheel) */
|
||||
/** [true!] allows to create and connect a ndoe clicking with the third button (wheel) */
|
||||
middle_click_slot_add_default_node = false
|
||||
|
||||
/** [true!] dragging a link to empty space will open a menu, add from list, search or defaults */
|
||||
@@ -399,6 +398,8 @@ export class LiteGraphGlobal {
|
||||
throw 'Cannot register a simple object, it must be a class with a prototype'
|
||||
base_class.type = type
|
||||
|
||||
if (this.debug) console.log('Node registered:', type)
|
||||
|
||||
const classname = base_class.name
|
||||
|
||||
const pos = type.lastIndexOf('/')
|
||||
@@ -414,7 +415,7 @@ export class LiteGraphGlobal {
|
||||
|
||||
const prev = this.registered_node_types[type]
|
||||
if (prev && this.debug) {
|
||||
console.warn('replacing node type:', type)
|
||||
console.log('replacing node type:', type)
|
||||
}
|
||||
|
||||
this.registered_node_types[type] = base_class
|
||||
@@ -429,7 +430,7 @@ export class LiteGraphGlobal {
|
||||
`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`
|
||||
)
|
||||
|
||||
// TODO one would want to know input and output :: this would allow through registerNodeAndSlotType to get all the slots types
|
||||
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
|
||||
if (this.auto_load_slot_types) new base_class(base_class.title || 'tmpnode')
|
||||
}
|
||||
|
||||
@@ -523,7 +524,7 @@ export class LiteGraphGlobal {
|
||||
): LGraphNode | null {
|
||||
const base_class = this.registered_node_types[type]
|
||||
if (!base_class) {
|
||||
if (this.debug) console.warn(`GraphNode type "${type}" not registered.`)
|
||||
if (this.debug) console.log(`GraphNode type "${type}" not registered.`)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -636,6 +637,7 @@ export class LiteGraphGlobal {
|
||||
continue
|
||||
|
||||
try {
|
||||
if (this.debug) console.log('Reloading:', src)
|
||||
const dynamicScript = document.createElement('script')
|
||||
dynamicScript.type = 'text/javascript'
|
||||
dynamicScript.src = src
|
||||
@@ -643,9 +645,11 @@ export class LiteGraphGlobal {
|
||||
script_file.remove()
|
||||
} catch (error) {
|
||||
if (this.throw_errors) throw error
|
||||
if (this.debug) console.error('Error while reloading', src)
|
||||
if (this.debug) console.log('Error while reloading', src)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.debug) console.log('Nodes reloaded')
|
||||
}
|
||||
|
||||
// separated just to improve if it doesn't work
|
||||
@@ -745,7 +749,7 @@ export class LiteGraphGlobal {
|
||||
// convert pointerevents to touch event when not available
|
||||
if (sMethod == 'pointer' && !window.PointerEvent) {
|
||||
console.warn("sMethod=='pointer' && !window.PointerEvent")
|
||||
console.warn(
|
||||
console.log(
|
||||
`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`
|
||||
)
|
||||
switch (sEvent) {
|
||||
@@ -770,7 +774,7 @@ export class LiteGraphGlobal {
|
||||
break
|
||||
}
|
||||
case 'enter': {
|
||||
// TODO: Determine if a move event should be sent
|
||||
console.log('debug: Should I send a move event?') // ???
|
||||
break
|
||||
}
|
||||
// case "over": case "out": not used at now
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
|
||||
@@ -12,8 +13,8 @@ import type {
|
||||
LinkSegment,
|
||||
Point,
|
||||
Positionable,
|
||||
ReadOnlyRect,
|
||||
ReadonlyLinkNetwork
|
||||
ReadonlyLinkNetwork,
|
||||
Rect
|
||||
} from './interfaces'
|
||||
import { distance, isPointInRect } from './measure'
|
||||
import type { Serialisable, SerialisableReroute } from './types/serialisation'
|
||||
@@ -71,7 +72,7 @@ export class Reroute
|
||||
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
|
||||
floating?: FloatingRerouteSlot
|
||||
|
||||
#pos: Point = [0, 0]
|
||||
#pos: [number, number] = [0, 0]
|
||||
/** @inheritdoc */
|
||||
get pos(): Point {
|
||||
return this.#pos
|
||||
@@ -87,17 +88,17 @@ export class Reroute
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get boundingRect(): ReadOnlyRect {
|
||||
get boundingRect(): Rectangle {
|
||||
const { radius } = Reroute
|
||||
const [x, y] = this.#pos
|
||||
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
||||
return Rectangle.from([x - radius, y - radius, 2 * radius, 2 * radius])
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
|
||||
* Eliminates most hover positions using an extremely cheap check.
|
||||
*/
|
||||
get #hoverArea(): ReadOnlyRect {
|
||||
get #hoverArea(): Rect {
|
||||
const xOffset = 2 * Reroute.slotOffset
|
||||
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
|
||||
|
||||
@@ -124,14 +125,14 @@ export class Reroute
|
||||
sin: number = 0
|
||||
|
||||
/** Bezier curve control point for the "target" (input) side of the link */
|
||||
controlPoint: Point = [0, 0]
|
||||
controlPoint: [number, number] = [0, 0]
|
||||
|
||||
/** @inheritdoc */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
/** @inheritdoc */
|
||||
_pos: Point = [0, 0]
|
||||
_pos: [number, number] = [0, 0]
|
||||
|
||||
/** @inheritdoc */
|
||||
_dragging?: boolean
|
||||
|
||||