Compare commits

..

57 Commits

Author SHA1 Message Date
Benjamin Lu
f338480e8b Add drop on canvas functionality 2025-09-29 14:35:28 -07:00
github-actions
a71b99d6fc Update test expectations [skip ci] 2025-09-29 17:27:57 +00:00
Benjamin Lu
5e3c91faac Merge remote-tracking branch 'origin/bl-more-slots' into bl-snap 2025-09-28 04:14:10 -07:00
github-actions
066a755a5b Update test expectations [skip ci] 2025-09-28 11:12:19 +00:00
Benjamin Lu
21cc208e4b Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-28 00:06:55 -07:00
Benjamin Lu
f13a45c781 Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-27 14:05:20 -07:00
Benjamin Lu
1c11dcc5af Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-27 14:04:50 -07:00
Benjamin Lu
247e3950eb knip 2025-09-26 14:43:51 -07:00
Benjamin Lu
8da5ae3af6 Add tests 2025-09-26 14:43:16 -07:00
Benjamin Lu
4b95ef94df Implement caching and rAF 2025-09-26 14:18:56 -07:00
Benjamin Lu
18b4f56158 refactor candidatefromnodetarget 2025-09-25 21:09:00 -07:00
Benjamin Lu
9de27adfca fix stale 2025-09-25 20:41:20 -07:00
Benjamin Lu
0e33672443 Merge remote-tracking branch 'origin/bl-more-slots' into bl-snap 2025-09-25 20:22:04 -07:00
Benjamin Lu
ecc5bed87d type 2025-09-25 19:17:19 -07:00
Benjamin Lu
1ca3d75aaf nit 2025-09-25 19:13:24 -07:00
Benjamin Lu
0627a71fb6 those who know cont 2025-09-25 18:02:26 -07:00
Benjamin Lu
23f3e17d52 Try connecting to snapped first 2025-09-25 17:17:45 -07:00
Benjamin Lu
0f46452b70 Remove debug logging 2025-09-25 17:07:20 -07:00
Benjamin Lu
76c718e2ee Visually snap to node 2025-09-25 16:58:55 -07:00
Benjamin Lu
4f6eaea257 get nodeid and slotkey 2025-09-25 11:45:09 -07:00
Benjamin Lu
839d8a5f47 sure 2025-09-24 20:37:46 -07:00
Benjamin Lu
c05011594d those who know 2025-09-23 23:33:29 -07:00
Benjamin Lu
9b39835cd1 refactor linkInteraction.spec.ts 2025-09-23 19:57:25 -07:00
Benjamin Lu
57810b9350 nit 2025-09-23 19:32:05 -07:00
Benjamin Lu
99aaa4e4cb Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-23 15:37:41 -07:00
Benjamin Lu
e9ffce468d nit 2025-09-23 15:01:58 -07:00
Benjamin Lu
381d97a982 nit 2025-09-23 14:10:40 -07:00
Benjamin Lu
88cd60f0c5 nit 2025-09-23 13:35:24 -07:00
Benjamin Lu
a2be36a0bc fix bad fallback and remove logging 2025-09-23 13:05:16 -07:00
Benjamin Lu
65ec322100 Those who type 2025-09-23 12:45:00 -07:00
Benjamin Lu
f99d8c1a92 I am the one who knocks 2025-09-23 11:56:30 -07:00
Benjamin Lu
8eec7fb80e huh? 2025-09-23 11:55:11 -07:00
Benjamin Lu
d78029697c cleanup unused 2025-09-23 11:52:56 -07:00
Benjamin Lu
f34890296b nit 2025-09-23 11:45:43 -07:00
Benjamin Lu
e879bd5290 improve typing 2025-09-23 11:37:09 -07:00
Benjamin Lu
9d32b4cf06 temp screenshots 2025-09-23 10:24:09 -07:00
Benjamin Lu
0aa971bed9 Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-23 10:20:10 -07:00
Benjamin Lu
6685e004c0 Fix double links 2025-09-23 10:13:09 -07:00
Benjamin Lu
e136b89fae Add reroute anchor tests 2025-09-22 23:32:23 -07:00
Benjamin Lu
3f4a8060de clean up onPointerDown 2025-09-22 22:53:21 -07:00
Benjamin Lu
20d136dff3 Switch to adapter approach 2025-09-22 16:56:40 -07:00
Benjamin Lu
e7f0ee40e4 Update test expectation 2025-09-22 16:54:26 -07:00
Benjamin Lu
bef712ed4f o-o shift test 2025-09-22 15:36:38 -07:00
Benjamin Lu
263b28097d Add snapshot 2025-09-22 12:16:04 -07:00
Benjamin Lu
19c538c36c Support dragging from output to output 2025-09-22 11:43:38 -07:00
Benjamin Lu
b99d70d0f0 nit 2025-09-20 18:26:21 -07:00
Benjamin Lu
9d668a154e Merge remote-tracking branch 'origin/main' into bl-more-slots 2025-09-20 18:24:02 -07:00
Benjamin Lu
c227d60626 Add dragging input to input drags existing link test 2025-09-20 18:20:11 -07:00
Benjamin Lu
70651dcde0 Allow moving links and support reroutes 2025-09-20 18:08:44 -07:00
Benjamin Lu
22a1c61208 Merge remote-tracking branch 'origin/bl-tests' into bl-more-slots 2025-09-19 13:05:54 -07:00
Benjamin Lu
369da53743 review comments 2025-09-19 13:02:44 -07:00
Benjamin Lu
48f5087116 More test cases v1 2025-09-19 12:41:05 -07:00
Benjamin Lu
f624940e16 Merge branch 'bl-tests' into bl-more-slots 2025-09-19 07:03:27 -07:00
github-actions
5c6c21cdf2 Update test expectations [skip ci] 2025-09-19 12:43:33 +00:00
Benjamin Lu
e3e1d2e8e6 Add more test cases 2025-09-19 05:06:16 -07:00
Benjamin Lu
939cbe0899 Litegraph? Never heard if it 2025-09-19 04:47:10 -07:00
Benjamin Lu
9a18d37019 Add dragging test 2025-09-19 03:08:33 -07:00
146 changed files with 20765 additions and 21501 deletions

2
.gitattributes vendored
View File

@@ -12,5 +12,5 @@
*.yaml text eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true
src/types/comfyRegistryTypes.ts linguist-generated=true
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true

View File

@@ -1,67 +0,0 @@
name: Setup Frontend
description: 'Setup ComfyUI frontend development environment'
inputs:
extra_server_params:
description: 'Additional parameters to pass to ComfyUI server'
required: false
default: ''
runs:
using: 'composite'
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
- name: Copy ComfyUI_devtools from frontend repo
shell: bash
run: |
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
shell: bash
working-directory: ComfyUI
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
- name: Build & Install ComfyUI_frontend
shell: bash
working-directory: ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Start ComfyUI server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../ComfyUI_frontend/dist ${{ inputs.extra_server_params }} &
wait-for-it --service 127.0.0.1:8188 -t 600

View File

@@ -60,7 +60,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -1,4 +1,4 @@
name: Storybook and Chromatic CI
name: 'Chromatic'
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
@@ -8,100 +8,13 @@ on:
branches: [main]
jobs:
# Post starting comment for non-forked PRs
comment-on-pr-start:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting" \
"$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
# Build Storybook for all PRs (free Cloudflare deployment)
storybook-build:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
outputs:
conclusion: ${{ steps.job-status.outputs.conclusion }}
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Storybook
run: pnpm build-storybook
- name: Set job status
id: job-status
if: always()
run: |
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
- name: Get workflow URL
id: workflow-url
if: always()
run: |
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: storybook-static/
retention-days: 7
# Chromatic deployment only for version-bump-* branches or manual triggers
chromatic-deployment:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'version-bump-'))
outputs:
conclusion: ${{ steps.job-status.outputs.conclusion }}
workflow-url: ${{ steps.workflow-url.outputs.url }}
chromatic-build-url: ${{ steps.chromatic.outputs.buildUrl }}
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
# Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic baseline
@@ -116,6 +29,7 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
@@ -140,92 +54,4 @@ jobs:
buildScriptName: build-storybook
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
- name: Set job status
id: job-status
if: always()
run: |
echo "conclusion=${{ job.status }}" >> $GITHUB_OUTPUT
- name: Get workflow URL
id: workflow-url
if: always()
run: |
echo "url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
# Deploy and comment for non-forked PRs only
deploy-and-comment:
needs: [storybook-build]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && always()
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
- name: Make deployment script executable
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
- name: Deploy Storybook and comment on PR
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW_CONCLUSION: ${{ needs.storybook-build.outputs.conclusion }}
WORKFLOW_URL: ${{ needs.storybook-build.outputs.workflow-url }}
run: |
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"completed"
# Update comment with Chromatic URLs for version-bump branches
update-comment-with-chromatic:
needs: [chromatic-deployment, deploy-and-comment]
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && startsWith(github.head_ref, 'version-bump-') && needs.chromatic-deployment.outputs.chromatic-build-url != ''
permissions:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v7
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
// Find the existing Storybook comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }}
});
const storybookComment = comments.find(comment =>
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
);
if (storybookComment && buildUrl && storybookUrl) {
// Append Chromatic info to existing comment
const updatedBody = storybookComment.body.replace(
/---\n(.*)$/s,
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: storybookComment.id,
body: updatedBody
});
}

View File

@@ -50,7 +50,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}

View File

@@ -15,7 +15,7 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Download dist artifact
uses: actions/download-artifact@v4
with:

View File

@@ -1,26 +0,0 @@
name: Devtools Python Check
on:
pull_request:
paths:
- 'tools/devtools/**'
push:
branches: [ main ]
paths:
- 'tools/devtools/**'
jobs:
syntax:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Validate Python syntax
run: python3 -m compileall -q tools/devtools

View File

@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: comfyanonymous/ComfyUI
path: ComfyUI
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI_frontend
path: ComfyUI_frontend
@@ -37,7 +37,7 @@ jobs:
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
- name: Checkout custom node repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'

View File

@@ -13,8 +13,7 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -14,8 +14,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache tool outputs
uses: actions/cache@v4

View File

@@ -1,15 +0,0 @@
name: Validate JSON
on:
push:
branches:
- main
pull_request:
jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.ref }}

View File

@@ -30,7 +30,7 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Get PR Number
id: pr

View File

@@ -0,0 +1,126 @@
name: PR Storybook Comment
on:
workflow_run:
workflows: ['Chromatic']
types: [requested, completed]
jobs:
comment-storybook:
runs-on: ubuntu-latest
if: >-
github.repository == 'Comfy-Org/ComfyUI_frontend'
&& github.event.workflow_run.event == 'pull_request'
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Get workflow run details
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: workflow-run
uses: actions/github-script@v7
with:
script: |
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
return {
conclusion: run.data.conclusion,
html_url: run.data.html_url
};
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Storybook Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Build is starting...**
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
- name: Comment PR - Storybook Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|| '❌'
}} **${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|| 'Build failed!'
}}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 🔗 Links
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
---
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && ' Chromatic was skipped for this PR.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && ' The Chromatic run was cancelled.'
|| '⚠️ Please check the workflow logs for error details.'
}}

View File

@@ -1,90 +0,0 @@
name: PR Storybook Deploy (Forks)
on:
workflow_run:
workflows: ['Storybook and Chromatic CI']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
permissions:
pull-requests: write
actions: read
steps:
- name: Log workflow trigger info
run: |
echo "Repository: ${{ github.repository }}"
echo "Event: ${{ github.event.workflow_run.event }}"
echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}"
echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}"
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
- name: Get PR Number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
- name: Handle Storybook Start
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
- name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: storybook-static
path: storybook-static
- name: Handle Storybook Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
WORKFLOW_URL: ${{ github.event.workflow_run.html_url }}
run: |
chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"

View File

@@ -10,10 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- name: Checkout workflow repo
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache Playwright browsers
uses: actions/cache@v4
with:

View File

@@ -15,14 +15,14 @@ jobs:
playwright-version: ${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
@@ -250,7 +250,7 @@ jobs:
if: ${{ !cancelled() }}
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: 'Comfy-Org/ComfyUI_frontend'
path: 'ComfyUI_frontend'
@@ -306,7 +306,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Get start time
id: start-time
@@ -333,7 +333,7 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Download all playwright reports
uses: actions/download-artifact@v4

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -51,7 +51,7 @@ jobs:
comfyui-manager-repo-${{ runner.os }}-
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -50,7 +50,7 @@ jobs:
comfy-api-repo-${{ runner.os }}-
- name: Checkout comfy-api repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -68,18 +68,17 @@ jobs:
- name: Generate API types
run: |
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
mkdir -p ./packages/registry-types/src
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./packages/registry-types/src/comfyRegistryTypes.ts
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
- name: Validate generated types
run: |
if [ ! -f ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
if [ ! -f ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Types file was not generated."
exit 1
fi
# Check if file is not empty
if [ ! -s ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
if [ ! -s ./src/types/comfyRegistryTypes.ts ]; then
echo "Error: Generated types file is empty."
exit 1
fi
@@ -87,12 +86,12 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
pnpm lint:fix:no-cache -- ./packages/registry-types/src/comfyRegistryTypes.ts
pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes
run: |
if [[ -z $(git status --porcelain ./packages/registry-types/src/comfyRegistryTypes.ts) ]]; then
if [[ -z $(git status --porcelain ./src/types/comfyRegistryTypes.ts) ]]; then
echo "No changes to Comfy Registry API types detected."
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
@@ -122,4 +121,4 @@ jobs:
labels: CNR
delete-branch: true
add-paths: |
packages/registry-types/src/comfyRegistryTypes.ts
src/types/comfyRegistryTypes.ts

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -1,2 +1,2 @@
packages/registry-types/src/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts
src/types/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -31,9 +31,10 @@
- Playwright: place tests in `browser_tests/`; optional tags like `@mobile`, `@2x` are respected by config.
## Commit & Pull Request Guidelines
- Commits: Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`- Fixes #123`), and screenshots/GIFs for UI changes.
- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
## Security & Configuration Tips
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.
- Backend: Dev server expects ComfyUI backend at `localhost:8188` by default; configure via `.env`.

View File

@@ -84,7 +84,7 @@ UI mode features:
- **Console/Network Tabs**: View logs and API calls at each step
- **Attachments Tab**: View all snapshots with expected and actual images
![Playwright UI Mode](https://github.com/user-attachments/assets/9b9cb09f-6da7-4fa0-81df-2effceced755)
![Playwright UI Mode](https://github.com/user-attachments/assets/c158c93f-b39a-44c5-a1a1-e0cc975ee9f2)
For CI or headless testing:

View File

@@ -1,221 +0,0 @@
{
"id": "e74f5af9-b886-4a21-abbf-ed535d12e2fb",
"revision": 0,
"last_node_id": 8,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "LoadAudio",
"pos": [
41.52964782714844,
16.930862426757812
],
"size": [
444,
125
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "AUDIO",
"type": "AUDIO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadAudio"
},
"widgets_values": [
null,
null,
""
]
},
{
"id": 2,
"type": "LoadVideo",
"pos": [
502.28570556640625,
16.857147216796875
],
"size": [
444,
525
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadVideo"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 3,
"type": "DevToolsLoadAnimatedImageTest",
"pos": [
41.71427917480469,
188.0000457763672
],
"size": [
444,
553
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsLoadAnimatedImageTest"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 5,
"type": "LoadImage",
"pos": [
958.285888671875,
16.57145118713379
],
"size": [
444,
553
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
null,
"image"
]
},
{
"id": 6,
"type": "LoadImageMask",
"pos": [
503.4285888671875,
588
],
"size": [
444,
563
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImageMask"
},
"widgets_values": [
null,
"alpha",
"image"
]
},
{
"id": 7,
"type": "LoadImageOutput",
"pos": [
965.1429443359375,
612
],
"size": [
444,
553
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImageOutput"
},
"widgets_values": [
null,
false,
"refresh",
"image"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
},
"frontendVersion": "1.28.3"
},
"version": 0.4
}

View File

@@ -13,6 +13,13 @@ export class VueNodeHelpers {
return this.page.locator('[data-node-id]')
}
/**
* Get locator for a Vue node by its NodeId
*/
getNodeLocator(nodeId: string): Locator {
return this.page.locator(`[data-node-id="${nodeId}"]`)
}
/**
* Get locator for selected Vue node components (using visual selection indicators)
*/

View File

@@ -1,33 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should allow creating groups with hotkey', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-create-group.png'
)
})
test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-fit-to-contents.png'
)
})
})

View File

@@ -1,18 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Canvas Pan', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -693,4 +693,99 @@ test.describe('Vue Node Link Interaction', () => {
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
}
})
test('should snap to node center while dragging and link on drop', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
// Start drag from CLIP output[0]
const clipOutputCenter = await getSlotCenter(
comfyPage.page,
clipNode.id,
0,
false
)
// Drag to the visual center of the KSampler Vue node (not a slot)
const samplerVue = comfyPage.vueNodes.getNodeLocator(String(samplerNode.id))
await expect(samplerVue).toBeVisible()
const samplerCenter = await getCenter(samplerVue)
await comfyMouse.move(clipOutputCenter)
await comfyMouse.drag(samplerCenter)
// During drag, the preview should snap/highlight a compatible input on KSampler
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-node.png')
// Drop to create the link
await comfyMouse.drop()
await comfyPage.nextFrame()
// Validate a link was created to one of KSampler's compatible inputs (1 or 2)
const linkOnInput1 = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
1
)
const linkOnInput2 = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
2
)
const linked = linkOnInput1 ?? linkOnInput2
expect(linked).not.toBeNull()
expect(linked?.originId).toBe(clipNode.id)
expect(linked?.targetId).toBe(samplerNode.id)
})
test('should snap to a specific compatible slot when targeting it', async ({
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
// Drag from CLIP output[0] to KSampler input[2] (third slot) which is the
// second compatible input for CLIP
const clipOutputCenter = await getSlotCenter(
comfyPage.page,
clipNode.id,
0,
false
)
const samplerInput3Center = await getSlotCenter(
comfyPage.page,
samplerNode.id,
2,
true
)
await comfyMouse.move(clipOutputCenter)
await comfyMouse.drag(samplerInput3Center)
// Expect the preview to show snapping to the targeted slot
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-slot.png')
// Finish the connection
await comfyMouse.drop()
await comfyPage.nextFrame()
const linkDetails = await getInputLinkDetails(
comfyPage.page,
samplerNode.id,
2
)
expect(linkDetails).not.toBeNull()
expect(linkDetails).toMatchObject({
originId: clipNode.id,
targetId: samplerNode.id,
targetSlot: 2
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -1,21 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Upload Widgets', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
})
})

View File

@@ -18,7 +18,7 @@ export default defineConfig([
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*'

View File

@@ -18,13 +18,8 @@ const config: KnipConfig = {
'packages/design-system': {
entry: ['src/**/*.ts'],
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
},
'packages/registry-types': {
entry: ['src/comfyRegistryTypes.ts'],
project: ['src/**/*.{js,ts}']
}
},
ignoreBinaries: ['python3'],
ignoreDependencies: [
// Weird importmap things
'@iconify/json',
@@ -38,7 +33,7 @@ const config: KnipConfig = {
ignore: [
// Auto generated manager types
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
'src/types/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
],

View File

@@ -38,8 +38,7 @@
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"storybook": "nx storybook -p 6006",
"build-storybook": "storybook build",
"devtools:pycheck": "python3 -m compileall -q tools/devtools"
"build-storybook": "storybook build"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
@@ -107,7 +106,6 @@
"@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": "^2.2.380",
"@primeuix/forms": "0.0.2",

View File

@@ -1,16 +0,0 @@
{
"name": "@comfyorg/registry-types",
"version": "1.0.0",
"description": "Comfy Registry API TypeScript types",
"packageManager": "pnpm@10.17.1",
"type": "module",
"exports": {
".": "./src/comfyRegistryTypes.ts"
},
"nx": {
"tags": [
"scope:shared",
"type:types"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

View File

@@ -1,22 +0,0 @@
{
"name": "@comfyorg/shared-frontend-utils",
"private": true,
"version": "1.0.0",
"description": "Shared frontend utils for ComfyUI Frontend",
"scripts": {
"typecheck": "tsc --noEmit"
},
"keywords": [],
"packageManager": "pnpm@10.17.1",
"type": "module",
"exports": {
"./formatUtil": "./src/formatUtil.ts",
"./networkUtil": "./src/networkUtil.ts"
},
"dependencies": {
"axios": "^1.11.0"
},
"devDependencies": {
"typescript": "^5.9.2"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}

23
pnpm-lock.yaml generated
View File

@@ -20,9 +20,6 @@ importers:
'@comfyorg/design-system':
specifier: workspace:*
version: link:packages/design-system
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
'@comfyorg/tailwind-utils':
specifier: workspace:*
version: link:packages/tailwind-utils
@@ -371,18 +368,6 @@ importers:
specifier: ^5.4.5
version: 5.9.2
packages/registry-types: {}
packages/shared-frontend-utils:
dependencies:
axios:
specifier: ^1.11.0
version: 1.11.0
devDependencies:
typescript:
specifier: ^5.9.2
version: 5.9.2
packages/tailwind-utils:
dependencies:
clsx:
@@ -6522,8 +6507,8 @@ packages:
vue-component-type-helpers@3.0.7:
resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==}
vue-component-type-helpers@3.1.0:
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
vue-component-type-helpers@3.0.8:
resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -9081,7 +9066,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.0
vue-component-type-helpers: 3.0.8
'@swc/helpers@0.5.17':
dependencies:
@@ -13879,7 +13864,7 @@ snapshots:
vue-component-type-helpers@3.0.7: {}
vue-component-type-helpers@3.1.0: {}
vue-component-type-helpers@3.0.8: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

@@ -1,77 +0,0 @@
#!/bin/bash
set -euo pipefail
usage() {
echo "Usage: $0 [--debug]" >&2
}
debug=0
while [ "$#" -gt 0 ]; do
case "$1" in
--debug)
debug=1
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 2
;;
esac
shift
done
# Validate JSON syntax in tracked files using jq
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed" >&2
exit 127
fi
EXCLUDE_PATTERNS=(
'**/tsconfig*.json'
)
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then
# shellcheck disable=SC2206
EXCLUDE_PATTERNS+=( ${JSON_LINT_EXCLUDES} )
fi
pathspecs=(-- '*.json')
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
if [[ ${pattern:0:1} == ':' ]]; then
pathspecs+=("$pattern")
else
pathspecs+=(":(glob,exclude)${pattern}")
fi
done
mapfile -t json_files < <(git ls-files "${pathspecs[@]}")
if [ "${#json_files[@]}" -eq 0 ]; then
echo 'No JSON files found.'
exit 0
fi
if [ "$debug" -eq 1 ]; then
echo 'JSON files to validate:'
printf ' %s\n' "${json_files[@]}"
fi
failed=0
for file in "${json_files[@]}"; do
if ! jq -e . "$file" >/dev/null; then
echo "Invalid JSON syntax: $file" >&2
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo 'JSON validation failed.' >&2
exit 1
fi
echo 'All JSON files are valid.'

View File

@@ -1,247 +0,0 @@
#!/bin/bash
set -e
# Deploy Storybook to Cloudflare Pages and comment on PR
# Usage: ./pr-storybook-deploy-and-comment.sh <pr_number> <branch_name> <status> [start_time]
# Input validation
# Validate PR number is numeric
case "$1" in
''|*[!0-9]*)
echo "Error: PR_NUMBER must be numeric" >&2
exit 1
;;
esac
PR_NUMBER="$1"
# Sanitize and validate branch name (allow alphanumeric, dots, dashes, underscores, slashes)
BRANCH_NAME=$(echo "$2" | sed 's/[^a-zA-Z0-9._/-]//g')
if [ -z "$BRANCH_NAME" ]; then
echo "Error: Invalid or empty branch name" >&2
exit 1
fi
# Validate status parameter
STATUS="${3:-completed}"
case "$STATUS" in
starting|completed) ;;
*)
echo "Error: STATUS must be 'starting' or 'completed'" >&2
exit 1
;;
esac
START_TIME="${4:-$(date -u '+%m/%d/%Y, %I:%M:%S %p')}"
# Required environment variables
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
# Cloudflare variables only required for deployment
if [ "$STATUS" = "completed" ]; then
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN is required for deployment}"
: "${CLOUDFLARE_ACCOUNT_ID:?CLOUDFLARE_ACCOUNT_ID is required for deployment}"
fi
# Configuration
COMMENT_MARKER="<!-- STORYBOOK_BUILD_STATUS -->"
# Install wrangler if not available (output to stderr for debugging)
if ! command -v wrangler > /dev/null 2>&1; then
echo "Installing wrangler v4..." >&2
npm install -g wrangler@^4.0.0 >&2 || {
echo "Failed to install wrangler" >&2
echo "failed"
return
}
fi
# Deploy Storybook report, WARN: ensure inputs are sanitized before calling this function
deploy_storybook() {
dir="$1"
branch="$2"
[ ! -d "$dir" ] && echo "failed" && return
project="comfy-storybook"
echo "Deploying Storybook to project $project on branch $branch..." >&2
# Try deployment up to 3 times
i=1
while [ $i -le 3 ]; do
echo "Deployment attempt $i of 3..." >&2
# Branch is already sanitized, use it directly
if output=$(wrangler pages deploy "$dir" \
--project-name="$project" \
--branch="$branch" 2>&1); then
# Extract URL from output (improved regex for valid URL characters)
url=$(echo "$output" | grep -oE 'https://[a-zA-Z0-9.-]+\.pages\.dev\S*' | head -1)
result="${url:-https://${branch}.${project}.pages.dev}"
echo "Success! URL: $result" >&2
echo "$result" # Only this goes to stdout for capture
return
else
echo "Deployment failed on attempt $i: $output" >&2
fi
[ $i -lt 3 ] && sleep 10
i=$((i + 1))
done
echo "failed"
}
# Post or update GitHub comment
post_comment() {
body="$1"
temp_file=$(mktemp)
echo "$body" > "$temp_file"
if command -v gh > /dev/null 2>&1; then
# Find existing comment ID
existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
--jq ".[] | select(.body | contains(\"$COMMENT_MARKER\")) | .id" | head -1)
if [ -n "$existing" ]; then
# Update specific comment by ID
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing" \
--field body="$(cat "$temp_file")"
else
gh pr comment "$PR_NUMBER" --body-file "$temp_file"
fi
else
echo "GitHub CLI not available, outputting comment:"
cat "$temp_file"
fi
rm -f "$temp_file"
}
# Main execution
if [ "$STATUS" = "starting" ]; then
# Check if this is a version-bump branch
IS_VERSION_BUMP="false"
if echo "$BRANCH_NAME" | grep -q "^version-bump-"; then
IS_VERSION_BUMP="true"
fi
# Post starting comment with appropriate message
if [ "$IS_VERSION_BUMP" = "true" ]; then
comment=$(cat <<EOF
$COMMENT_MARKER
## 🎨 Storybook Build Status
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Build is starting...**
⏰ Started at: $START_TIME UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
EOF
)
else
comment=$(cat <<EOF
$COMMENT_MARKER
## 🎨 Storybook Build Status
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Build is starting...**
⏰ Started at: $START_TIME UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🌐 Preparing deployment to Cloudflare Pages...
---
⏱️ Please wait while the Storybook build is in progress...
EOF
)
fi
post_comment "$comment"
elif [ "$STATUS" = "completed" ]; then
# Deploy and post completion comment
# Convert branch name to Cloudflare-compatible format (lowercase, only alphanumeric and dashes)
cloudflare_branch=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | \
sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
echo "Looking for Storybook build in: $(pwd)/storybook-static"
# Deploy Storybook if build exists
deployment_url="Not deployed"
if [ -d "storybook-static" ]; then
echo "Found Storybook build, deploying..."
url=$(deploy_storybook "storybook-static" "$cloudflare_branch")
if [ "$url" != "failed" ] && [ -n "$url" ]; then
deployment_url="[View Storybook]($url)"
else
deployment_url="Deployment failed"
fi
else
echo "Storybook build not found at storybook-static"
fi
# Get workflow conclusion from environment or default to success
WORKFLOW_CONCLUSION="${WORKFLOW_CONCLUSION:-success}"
WORKFLOW_URL="${WORKFLOW_URL:-}"
# Generate completion comment based on conclusion
if [ "$WORKFLOW_CONCLUSION" = "success" ]; then
status_icon="✅"
status_text="Build completed successfully!"
footer_text="🎉 Your Storybook is ready for review!"
elif [ "$WORKFLOW_CONCLUSION" = "skipped" ]; then
status_icon="⏭️"
status_text="Build skipped."
footer_text=" Chromatic was skipped for this PR."
elif [ "$WORKFLOW_CONCLUSION" = "cancelled" ]; then
status_icon="🚫"
status_text="Build cancelled."
footer_text=" The Chromatic run was cancelled."
else
status_icon="❌"
status_text="Build failed!"
footer_text="⚠️ Please check the workflow logs for error details."
fi
comment="$COMMENT_MARKER
## 🎨 Storybook Build Status
$status_icon **$status_text**
⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC
### 🔗 Links
- [📊 View Workflow Run]($WORKFLOW_URL)"
# Add deployment status
if [ "$deployment_url" != "Not deployed" ]; then
if [ "$deployment_url" = "Deployment failed" ]; then
comment="$comment
- ❌ Storybook deployment failed"
elif [ "$WORKFLOW_CONCLUSION" = "success" ]; then
comment="$comment
- 🎨 $deployment_url"
else
comment="$comment
- ⚠️ Build failed - $deployment_url"
fi
elif [ "$WORKFLOW_CONCLUSION" != "success" ]; then
comment="$comment
- ⏭️ Storybook deployment skipped (build did not succeed)"
fi
comment="$comment
---
$footer_text"
post_comment "$comment"
fi

View File

@@ -1,15 +1,12 @@
import * as fs from 'fs'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import {
formatCamelCase,
normalizeI18nKey
} from '../packages/shared-frontend-utils/src/formatUtil'
import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands'
import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs'
import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig'
import type { FormItem, SettingParams } from '../src/platform/settings/types'
import type { ComfyCommandImpl } from '../src/stores/commandStore'
import { formatCamelCase, normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const commandsPath = './src/locales/en/commands.json'

View File

@@ -3,8 +3,8 @@ import * as fs from 'fs'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
import { normalizeI18nKey } from '../packages/shared-frontend-utils/src/formatUtil'
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
import { normalizeI18nKey } from '../src/utils/formatUtil'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'

View File

@@ -125,43 +125,50 @@ watch(
}
}
)
useEventListener(document, 'mousedown', (event) => {
if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {
return
}
if (!widget.element.contains(event.target as HTMLElement)) {
widget.element.blur()
}
})
onMounted(() => {
if (!isDOMWidget(widget)) {
return
// Set up event listeners only after the widget is mounted and visible
const setupDOMEventListeners = () => {
if (!isDOMWidget(widget) || !widgetState.visible) return
if (widget.element.blur) {
useEventListener(document, 'mousedown', (event) => {
if (!widget.element.contains(event.target as HTMLElement)) {
widget.element.blur()
}
})
}
useEventListener(
widget.element,
widget.options.selectOn ?? ['focus', 'click'],
() => {
for (const evt of widget.options.selectOn ?? ['focus', 'click']) {
useEventListener(widget.element, evt, () => {
const lgCanvas = canvasStore.canvas
lgCanvas?.selectNode(widget.node)
lgCanvas?.bringToFront(widget.node)
})
}
}
// Set up event listeners when widget becomes visible
watch(
() => widgetState.visible,
(visible) => {
if (visible) {
setupDOMEventListeners()
}
)
})
},
{ immediate: true }
)
const inputSpec = widget.node.constructor.nodeData
const tooltip = inputSpec?.inputs?.[widget.name]?.tooltip
// Mount DOM element when widget is or becomes visible
const mountElementIfVisible = () => {
if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {
return
if (widgetState.visible && isDOMWidget(widget) && widgetElement.value) {
// Only append if not already a child
if (!widgetElement.value.contains(widget.element)) {
widgetElement.value.appendChild(widget.element)
}
}
// Only append if not already a child
if (widgetElement.value.contains(widget.element)) {
return
}
widgetElement.value.appendChild(widget.element)
}
// Check on mount - but only after next tick to ensure visibility is calculated

View File

@@ -60,8 +60,8 @@ import { computed, onMounted, ref, watch } from 'vue'
import UrlInput from '@/components/common/UrlInput.vue'
import type { UVMirror } from '@/constants/uvMirrors'
import { st } from '@/i18n'
import { checkMirrorReachable } from '@/utils/electronMirrorCheck'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { checkMirrorReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const FILE_URL_SCHEME = 'file://'

View File

@@ -262,17 +262,26 @@ function cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {
}
function handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {
disconnectOnReset = true
const action = e.detail.shiftKey
const pendingAction = searchBoxStore.pendingLinkDropAction
searchBoxStore.setPendingLinkDropAction(null)
const fallbackAction = e.detail.shiftKey
? linkReleaseActionShift.value
: linkReleaseAction.value
const action =
pendingAction ?? fallbackAction ?? LinkReleaseTriggerAction.NO_ACTION
disconnectOnReset = action !== LinkReleaseTriggerAction.NO_ACTION
if (!disconnectOnReset) return
cancelNextReset(e)
switch (action) {
case LinkReleaseTriggerAction.SEARCH_BOX:
cancelNextReset(e)
showSearchBox(e.detail)
break
case LinkReleaseTriggerAction.CONTEXT_MENU:
cancelNextReset(e)
showContextMenu(e.detail)
break
case LinkReleaseTriggerAction.NO_ACTION:

View File

@@ -4,7 +4,7 @@ import type { Ref } from 'vue'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } 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'
@@ -71,7 +71,7 @@ export function useSelectionToolboxPosition(
visible.value = true
// Get bounds for all selected items
const allBounds: Rect[] = []
const allBounds: ReadOnlyRect[] = []
for (const item of selectableItems) {
// Skip items without valid IDs
if (item.id == null) continue

View File

@@ -241,7 +241,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
/**
* Sets up widget callbacks for a node
* Sets up widget callbacks for a node - now with reduced nesting
*/
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
if (!node.widgets) return

View File

@@ -1613,9 +1613,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
},
WanTextToImageApi: {
displayPrice: '$0.03/Run'
},
WanImageToImageApi: {
displayPrice: '$0.03/Run'
}
}

View File

@@ -6,8 +6,8 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { checkMirrorReachable } from '@/utils/electronMirrorCheck'
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
import { checkMirrorReachable } from '@/utils/networkUtil'
// Desktop documentation URLs
const DESKTOP_DOCS = {

View File

@@ -241,7 +241,7 @@ app.registerExtension({
inputName,
'',
openFileSelection,
{ serialize: false, canvasOnly: true }
{ serialize: false }
)
uploadWidget.label = t('g.choose_file_to_upload')
@@ -398,7 +398,7 @@ app.registerExtension({
mediaRecorder.stop()
}
},
{ serialize: false, canvasOnly: true }
{ serialize: false }
)
recordWidget.label = t('g.startRecording')

View File

@@ -106,8 +106,7 @@ app.registerExtension({
'button',
'waiting for camera...',
'capture',
capture,
{ canvasOnly: true }
capture
)
btn.disabled = true
btn.serializeValue = () => undefined

View File

@@ -1,4 +1,4 @@
import type { Point, Rect } from './interfaces'
import type { Point, ReadOnlyRect, Rect } from './interfaces'
import { EaseFunction, Rectangle } from './litegraph'
export interface DragAndScaleState {
@@ -188,7 +188,10 @@ 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: Rect, { zoom = 0.75 }: { zoom?: number } = {}): void {
fitToBounds(
bounds: ReadOnlyRect,
{ zoom = 0.75 }: { zoom?: number } = {}
): void {
const cw = this.element.width / window.devicePixelRatio
const ch = this.element.height / window.devicePixelRatio
let targetScale = this.scale
@@ -220,7 +223,7 @@ export class DragAndScale {
* @param bounds The bounds to animate the view to, defined by a rectangle.
*/
animateToBounds(
bounds: Readonly<Rect | Rectangle>,
bounds: ReadOnlyRect,
setDirty: () => void,
{
duration = 350,

View File

@@ -4,7 +4,6 @@ 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'
@@ -1708,12 +1707,7 @@ export class LGraph
...subgraphNode.subgraph.groups
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
return {
boundingRect: new Rectangle(
p.pos[0],
p.pos[1],
p.size?.[0] ?? 0,
p.size?.[1] ?? 0
)
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
}
})
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]

View File

@@ -47,6 +47,8 @@ import type {
NullableProperties,
Point,
Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
Size
} from './interfaces'
@@ -226,6 +228,16 @@ const cursors = {
NW: 'nwse-resize'
} as const
/** A lightweight converter for client<->canvas coordinate transforms. */
interface PositionConverter {
/** Convert a client/pointer position to canvas (graph) space. */
clientPosToCanvasPos(pos: Point): Point
/** Convert a canvas (graph) position to client space. */
canvasPosToClientPos(pos: Point): Point
/** Optional hook to refresh internal caches (e.g. bounding rect). */
update?(): void
}
/**
* 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
@@ -234,11 +246,11 @@ 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 #temp = new Float32Array(4)
static #temp_vec2 = new Float32Array(2)
static #tmp_area = new Float32Array(4)
static #margin_area = new Float32Array(4)
static #link_bounding = new Float32Array(4)
static DEFAULT_BACKGROUND_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
@@ -476,11 +488,19 @@ export class LGraphCanvas
return this._isLowQuality
}
/**
* Converts pointer/client positions to canvas positions without forcing layout reads.
* When present, {@link adjustMouseEvent} will use this instead of DOM queries.
*/
positionConverter?: PositionConverter
options: {
skip_events?: any
viewport?: any
skip_render?: any
autoresize?: any
/** Optional converter for client<->canvas position transforms. */
positionConverter?: PositionConverter
}
background_image: string
@@ -626,7 +646,7 @@ export class LGraphCanvas
dirty_area?: Rect | null
/** @deprecated Unused */
node_in_panel?: LGraphNode | null
last_mouse: Point = [0, 0]
last_mouse: ReadOnlyPoint = [0, 0]
last_mouseclick: number = 0
graph: LGraph | Subgraph | null
get _graph(): LGraph | Subgraph {
@@ -737,6 +757,8 @@ export class LGraphCanvas
) {
options ||= {}
this.options = options
if (options.positionConverter)
this.positionConverter = options.positionConverter
// if(graph === undefined)
// throw ("No graph assigned");
@@ -2632,7 +2654,7 @@ export class LGraphCanvas
pointer: CanvasPointer,
node?: LGraphNode | undefined
): void {
const dragRect: [number, number, number, number] = [0, 0, 0, 0]
const dragRect = new Float32Array(4)
dragRect[0] = e.canvasX
dragRect[1] = e.canvasY
@@ -3172,7 +3194,7 @@ export class LGraphCanvas
LGraphCanvas.active_canvas = this
this.adjustMouseEvent(e)
const mouse: Point = [e.clientX, e.clientY]
const mouse: ReadOnlyPoint = [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]]
@@ -4075,10 +4097,7 @@ export class LGraphCanvas
this.setDirty(true)
}
#handleMultiSelect(
e: CanvasPointerEvent,
dragRect: [number, number, number, number]
) {
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Float32Array) {
// Process drag
// Convert Point pair (pos, offset) to Rect
const { graph, selectedItems, subgraph } = this
@@ -4454,33 +4473,54 @@ export class LGraphCanvas
adjustMouseEvent<T extends MouseEvent>(
e: T & Partial<CanvasPointerExtensions>
): asserts e is T & CanvasPointerEvent {
let clientX_rel = e.clientX
let clientY_rel = e.clientY
const { ds, positionConverter } = this
if (this.canvas) {
const b = this.canvas.getBoundingClientRect()
clientX_rel -= b.left
clientY_rel -= b.top
if (positionConverter) {
const [canvasX, canvasY] = positionConverter.clientPosToCanvasPos([
e.clientX,
e.clientY
])
// safeOffset is relative to the canvas element (like offsetX/Y), not page
const safeX = (canvasX + ds.offset[0]) * ds.scale
const safeY = (canvasY + ds.offset[1]) * ds.scale
e.canvasX = canvasX
e.canvasY = canvasY
e.safeOffsetX = safeX
e.safeOffsetY = safeY
if (e.deltaX === undefined) e.deltaX = safeX - this.last_mouse_position[0]
if (e.deltaY === undefined) e.deltaY = safeY - this.last_mouse_position[1]
this.last_mouse_position[0] = safeX
this.last_mouse_position[1] = safeY
} else {
// Fallback to DOM rect (legacy path)
let clientX_rel = e.clientX
let clientY_rel = e.clientY
if (this.canvas) {
const b = this.canvas.getBoundingClientRect()
clientX_rel -= b.left
clientY_rel -= b.top
}
e.safeOffsetX = clientX_rel
e.safeOffsetY = clientY_rel
// Only set deltaX and deltaY if not already set.
if (e.deltaX === undefined)
e.deltaX = clientX_rel - this.last_mouse_position[0]
if (e.deltaY === undefined)
e.deltaY = clientY_rel - this.last_mouse_position[1]
this.last_mouse_position[0] = clientX_rel
this.last_mouse_position[1] = clientY_rel
e.canvasX = clientX_rel / ds.scale - ds.offset[0]
e.canvasY = clientY_rel / ds.scale - ds.offset[1]
}
e.safeOffsetX = clientX_rel
e.safeOffsetY = clientY_rel
// TODO: Find a less brittle way to do this
// Only set deltaX and deltaY if not already set.
// If deltaX and deltaY are already present, they are read-only.
// Setting them would result browser error => zoom in/out feature broken.
if (e.deltaX === undefined)
e.deltaX = clientX_rel - this.last_mouse_position[0]
if (e.deltaY === undefined)
e.deltaY = clientY_rel - this.last_mouse_position[1]
this.last_mouse_position[0] = clientX_rel
this.last_mouse_position[1] = clientY_rel
e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]
e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]
}
/**
@@ -4733,47 +4773,32 @@ 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 color = resolveConnectingLinkColor(connType)
const colour = resolveConnectingLinkColor(connType)
// the connection being dragged by the mouse
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
// }
)
}
if (this.linkRenderer) {
this.linkRenderer.renderDraggingLink(
ctx,
pos,
highlightPos,
colour,
fromDirection,
dragDirection,
{
...this.buildLinkRenderContext(),
linkMarkerShape: LinkMarkerShape.None
}
)
}
ctx.fillStyle = color
ctx.fillStyle = colour
ctx.beginPath()
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
@@ -4864,7 +4889,7 @@ export class LGraphCanvas
}
/** Get the target snap / highlight point in graph space */
#getHighlightPosition(): Point {
#getHighlightPosition(): ReadOnlyPoint {
return LiteGraph.snaps_for_comfy
? this.linkConnector.state.snapLinksPos ??
this._highlight_pos ??
@@ -4879,7 +4904,7 @@ export class LGraphCanvas
*/
#renderSnapHighlight(
ctx: CanvasRenderingContext2D,
highlightPos: Point
highlightPos: ReadOnlyPoint
): void {
const linkConnectorSnap = !!this.linkConnector.state.snapLinksPos
if (!this._highlight_pos && !linkConnectorSnap) return
@@ -5221,8 +5246,7 @@ export class LGraphCanvas
// clip if required (mask)
const shape = node._shape || RenderShape.BOX
const size = LGraphCanvas.#temp_vec2
size[0] = node.renderingSize[0]
size[1] = node.renderingSize[1]
size.set(node.renderingSize)
if (node.collapsed) {
ctx.font = this.inner_text_font
@@ -5417,10 +5441,7 @@ export class LGraphCanvas
// Normalised node dimensions
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.set(node.boundingRect)
area[0] -= node.pos[0]
area[1] -= node.pos[1]
@@ -5522,10 +5543,7 @@ export class LGraphCanvas
shape = RenderShape.ROUND
) {
const snapGuide = LGraphCanvas.#temp
snapGuide[0] = item.boundingRect[0]
snapGuide[1] = item.boundingRect[1]
snapGuide[2] = item.boundingRect[2]
snapGuide[3] = item.boundingRect[3]
snapGuide.set(item.boundingRect)
// Not all items have pos equal to top-left of bounds
const { pos } = item
@@ -5965,8 +5983,8 @@ export class LGraphCanvas
*/
renderLink(
ctx: CanvasRenderingContext2D,
a: Point,
b: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
link: LLink | null,
skip_border: boolean,
flow: number | null,
@@ -5983,9 +6001,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?: Point
startControl?: ReadOnlyPoint
/** Offset of the bezier curve control point from {@link b point b} (input side) */
endControl?: Point
endControl?: ReadOnlyPoint
/** 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 */
@@ -8456,7 +8474,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: Rect | Rectangle, options: AnimationOptions = {}) {
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) {
const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options)
}

View File

@@ -1,5 +1,4 @@
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'
@@ -41,15 +40,15 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
title: string
font?: string
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
_bounding: [number, number, number, number] = [
_bounding: Float32Array = new Float32Array([
10,
10,
LGraphGroup.minWidth,
LGraphGroup.minHeight
]
])
_pos: Point = [10, 10]
_size: Size = [LGraphGroup.minWidth, LGraphGroup.minHeight]
_pos: Point = this._bounding.subarray(0, 2)
_size: Size = this._bounding.subarray(2, 4)
/** @deprecated See {@link _children} */
_nodes: LGraphNode[] = []
_children: Set<Positionable> = new Set()
@@ -108,13 +107,8 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
}
get boundingRect(): Rectangle {
return Rectangle.from([
this._pos[0],
this._pos[1],
this._size[0],
this._size[1]
])
get boundingRect() {
return this._bounding
}
get nodes() {
@@ -151,17 +145,14 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
configure(o: ISerialisedGroup): void {
this.id = o.id
this.title = o.title
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._bounding.set(o.bounding)
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._pos[0], this._pos[1], this._size[0], this._size[1]]
const b = this._bounding
return {
id: this.id,
title: this.title,
@@ -219,7 +210,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
)
if (LiteGraph.highlight_selected_group && this.selected) {
strokeShape(ctx, this.boundingRect, {
strokeShape(ctx, this._bounding, {
title_height: this.titleHeight,
padding
})
@@ -260,7 +251,7 @@ export class LGraphGroup implements Positionable, IPinnable, IColorable {
// Move nodes we overlap the centre point of
for (const node of nodes) {
if (containsCentre(this.boundingRect, node.boundingRect)) {
if (containsCentre(this._bounding, node.boundingRect)) {
this._nodes.push(node)
children.add(node)
}
@@ -268,13 +259,12 @@ 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.boundingRect)) children.add(reroute)
if (isPointInRect(reroute.pos, this._bounding)) children.add(reroute)
}
// Move groups we wholly contain
for (const group of groups) {
if (containsRect(this.boundingRect, group.boundingRect))
children.add(group)
if (containsRect(this._bounding, group._bounding)) children.add(group)
}
groups.sort((a, b) => {

View File

@@ -18,6 +18,7 @@ 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,
@@ -36,6 +37,8 @@ import type {
ISlotType,
Point,
Positionable,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
Size
} from './interfaces'
@@ -384,7 +387,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: Rectangle): void
onBounding?(this: LGraphNode, out: Rect): void
console?: string[]
_level?: number
_shape?: RenderShape
@@ -410,12 +413,12 @@ export class LGraphNode
}
/** @inheritdoc {@link renderArea} */
#renderArea: [number, number, number, number] = [0, 0, 0, 0]
#renderArea: Float32Array = new Float32Array(4)
/**
* 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(): Rect {
get renderArea(): ReadOnlyRect {
return this.#renderArea
}
@@ -426,12 +429,12 @@ export class LGraphNode
*
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
*/
get boundingRect(): Rectangle {
get boundingRect(): ReadOnlyRectangle {
return this.#boundingRect
}
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
get boundingOffset(): Point {
get boundingOffset(): ReadOnlyPoint {
const {
pos: [posX, posY],
boundingRect: [bX, bY]
@@ -440,9 +443,9 @@ export class LGraphNode
}
/** {@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]
_posSize: Float32Array = new Float32Array(4)
_pos: Point = this._posSize.subarray(0, 2)
_size: Size = this._posSize.subarray(2, 4)
public get pos() {
return this._pos
@@ -1650,7 +1653,7 @@ export class LGraphNode
inputs ? inputs.filter((input) => !isWidgetInputSlot(input)).length : 1,
outputs ? outputs.length : 1
)
const size = out || [0, 0]
const size = out || new Float32Array([0, 0])
rows = Math.max(rows, 1)
// although it should be graphcanvas.inner_text_font size
const font_size = LiteGraph.NODE_TEXT_SIZE
@@ -1975,7 +1978,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: Rectangle, ctx: CanvasRenderingContext2D): void {
measure(out: Rect, ctx: CanvasRenderingContext2D): void {
const titleMode = this.title_mode
const renderTitle =
titleMode != TitleMode.TRANSPARENT_TITLE &&
@@ -2001,13 +2004,13 @@ export class LGraphNode
/**
* returns the bounding of the object, used for rendering purposes
* @param out {Rect?} [optional] a place to store the output, to free garbage
* @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage
* @param includeExternal {boolean?} [optional] set to true to
* include the shadow and connection points in the bounding calculation
* @returns the bounding box in format of [topleft_cornerx, topleft_cornery, width, height]
*/
getBounding(out?: Rect, includeExternal?: boolean): Rect {
out ||= [0, 0, 0, 0]
out ||= new Float32Array(4)
const rect = includeExternal ? this.renderArea : this.boundingRect
out[0] = rect[0]
@@ -2028,10 +2031,7 @@ export class LGraphNode
this.onBounding?.(bounds)
const renderArea = this.#renderArea
renderArea[0] = bounds[0]
renderArea[1] = bounds[1]
renderArea[2] = bounds[2]
renderArea[3] = bounds[3]
renderArea.set(bounds)
// 4 offset for collapsed node connection points
renderArea[0] -= 4
renderArea[1] -= 4
@@ -3174,7 +3174,7 @@ export class LGraphNode
* @returns the position
*/
getConnectionPos(is_input: boolean, slot_number: number, out?: Point): Point {
out ||= [0, 0]
out ||= new Float32Array(2)
const {
pos: [nodeX, nodeY],
@@ -3839,7 +3839,7 @@ export class LGraphNode
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
}
#measureSlots(): Rect | null {
#measureSlots(): ReadOnlyRect | null {
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {

View File

@@ -109,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: [number, number]
_pos: Float32Array
/** @todo Clean up - never implemented in comfy. */
_last_time?: number
/** The last canvas 2D path that was used to render this link */
@@ -171,7 +171,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
this._data = null
// center
this._pos = [0, 0]
this._pos = new Float32Array(2)
}
/** @deprecated Use {@link LLink.create} */

View File

@@ -1,4 +1,3 @@
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
@@ -13,8 +12,8 @@ import type {
LinkSegment,
Point,
Positionable,
ReadonlyLinkNetwork,
Rect
ReadOnlyRect,
ReadonlyLinkNetwork
} from './interfaces'
import { distance, isPointInRect } from './measure'
import type { Serialisable, SerialisableReroute } from './types/serialisation'
@@ -50,6 +49,8 @@ export class Reroute
return Reroute.radius + gap + Reroute.slotRadius
}
#malloc = new Float32Array(8)
/** The network this reroute belongs to. Contains all valid links and reroutes. */
#network: WeakRef<LinkNetwork>
@@ -72,7 +73,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: [number, number] = [0, 0]
#pos = this.#malloc.subarray(0, 2)
/** @inheritdoc */
get pos(): Point {
return this.#pos
@@ -88,17 +89,17 @@ export class Reroute
}
/** @inheritdoc */
get boundingRect(): Rectangle {
get boundingRect(): ReadOnlyRect {
const { radius } = Reroute
const [x, y] = this.#pos
return Rectangle.from([x - radius, y - radius, 2 * radius, 2 * radius])
return [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(): Rect {
get #hoverArea(): ReadOnlyRect {
const xOffset = 2 * Reroute.slotOffset
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
@@ -125,14 +126,14 @@ export class Reroute
sin: number = 0
/** Bezier curve control point for the "target" (input) side of the link */
controlPoint: [number, number] = [0, 0]
controlPoint: Point = this.#malloc.subarray(4, 6)
/** @inheritdoc */
path?: Path2D
/** @inheritdoc */
_centreAngle?: number
/** @inheritdoc */
_pos: [number, number] = [0, 0]
_pos: Float32Array = this.#malloc.subarray(6, 8)
/** @inheritdoc */
_dragging?: boolean

View File

@@ -67,7 +67,7 @@ interface IDrawTextInAreaOptions {
*/
export function strokeShape(
ctx: CanvasRenderingContext2D,
area: Rect | Rectangle,
area: Rect,
{
shape = RenderShape.BOX,
round_radius,

View File

@@ -1,6 +1,10 @@
import { clamp } from 'es-toolkit/compat'
import type { Rect, Size } from '@/lib/litegraph/src/interfaces'
import type {
ReadOnlyRect,
ReadOnlySize,
Size
} from '@/lib/litegraph/src/interfaces'
/**
* Basic width and height, with min/max constraints.
@@ -51,15 +55,15 @@ export class ConstrainedSize {
this.desiredHeight = height
}
static fromSize(size: Size): ConstrainedSize {
static fromSize(size: ReadOnlySize): ConstrainedSize {
return new ConstrainedSize(size[0], size[1])
}
static fromRect(rect: Rect): ConstrainedSize {
static fromRect(rect: ReadOnlyRect): ConstrainedSize {
return new ConstrainedSize(rect[2], rect[3])
}
setSize(size: Size): void {
setSize(size: ReadOnlySize): void {
this.desiredWidth = size[0]
this.desiredHeight = size[1]
}

View File

@@ -1,6 +1,6 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink'
import type { Rect } from '@/lib/litegraph/src/interfaces'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type {
ExportedSubgraph,
@@ -29,7 +29,7 @@ export interface LGraphEventMap {
/** The type of subgraph to create. */
subgraph: Subgraph
/** The boundary around every item that was moved into the subgraph. */
bounds: Rect
bounds: ReadOnlyRect
/** The raw data that was used to create the subgraph. */
exportedSubgraph: ExportedSubgraph
/** The links that were used to create the subgraph. */

View File

@@ -1,50 +1,47 @@
import type {
CompassCorners,
Point,
Rect,
ReadOnlyPoint,
ReadOnlyRect,
ReadOnlySize,
ReadOnlyTypedArray,
Size
} from '@/lib/litegraph/src/interfaces'
import { isInRectangle } from '@/lib/litegraph/src/measure'
/**
* A rectangle, represented as an array of 4 numbers: [x, y, width, height].
* A rectangle, represented as a float64 array of 4 numbers: [x, y, width, height].
*
* This class extends Array and provides both array access (rect[0], rect[1], etc.)
* and convenient property access (rect.x, rect.y, rect.width, rect.height).
* This class is a subclass of Float64Array, and so has all the methods of that class. Notably,
* {@link Rectangle.from} can be used to convert a {@link ReadOnlyRect}. Typing of this however,
* is broken due to the base TS lib returning Float64Array rather than `this`.
*
* Sub-array properties ({@link Float64Array.subarray}):
* - {@link pos}: The position of the top-left corner of the rectangle.
* - {@link size}: The size of the rectangle.
*/
export class Rectangle extends Array<number> {
export class Rectangle extends Float64Array {
#pos: Point | undefined
#size: Size | undefined
constructor(
x: number = 0,
y: number = 0,
width: number = 0,
height: number = 0
) {
super()
super(4)
this[0] = x
this[1] = y
this[2] = width
this[3] = height
this.length = 4
}
static override from([x, y, width, height]: Rect): Rectangle {
static override from([x, y, width, height]: ReadOnlyRect): Rectangle {
return new Rectangle(x, y, width, height)
}
/** Set all values from an array (for TypedArray compatibility) */
set(values: ArrayLike<number>): void {
this[0] = values[0] ?? 0
this[1] = values[1] ?? 0
this[2] = values[2] ?? 0
this[3] = values[3] ?? 0
}
/** Create a subarray (for TypedArray compatibility) */
subarray(begin: number = 0, end?: number): number[] {
const endIndex = end ?? this.length
return this.slice(begin, endIndex)
}
/**
* Creates a new rectangle positioned at the given centre, with the given width/height.
* @param centre The centre of the rectangle, as an `[x, y]` point
@@ -52,38 +49,57 @@ export class Rectangle extends Array<number> {
* @param height The height of the rectangle. Default: {@link width}
* @returns A new rectangle whose centre is at {@link x}
*/
static fromCentre([x, y]: Point, width: number, height = width): Rectangle {
static fromCentre(
[x, y]: ReadOnlyPoint,
width: number,
height = width
): Rectangle {
const left = x - width * 0.5
const top = y - height * 0.5
return new Rectangle(left, top, width, height)
}
static ensureRect(rect: Rect | Rectangle): Rectangle {
static ensureRect(rect: ReadOnlyRect): Rectangle {
return rect instanceof Rectangle
? rect
: new Rectangle(rect[0], rect[1], rect[2], rect[3])
}
/**
* The position of the top-left corner of this rectangle.
*/
get pos(): Point {
return [this[0], this[1]]
override subarray(
begin: number = 0,
end?: number
): Float64Array<ArrayBuffer> {
const byteOffset = begin << 3
const length = end === undefined ? end : end - begin
return new Float64Array(this.buffer, byteOffset, length)
}
set pos(value: Point) {
/**
* A reference to the position of the top-left corner of this rectangle.
*
* Updating the values of the returned object will update this rectangle.
*/
get pos(): Point {
this.#pos ??= this.subarray(0, 2)
return this.#pos!
}
set pos(value: ReadOnlyPoint) {
this[0] = value[0]
this[1] = value[1]
}
/**
* The size of this rectangle.
* A reference to the size of this rectangle.
*
* Updating the values of the returned object will update this rectangle.
*/
get size(): Size {
return [this[2], this[3]]
this.#size ??= this.subarray(2, 4)
return this.#size!
}
set size(value: Size) {
set size(value: ReadOnlySize) {
this[2] = value[0]
this[3] = value[1]
}
@@ -176,7 +192,7 @@ export class Rectangle extends Array<number> {
* Updates the rectangle to the values of {@link rect}.
* @param rect The rectangle to update to.
*/
updateTo(rect: Rect) {
updateTo(rect: ReadOnlyRect) {
this[0] = rect[0]
this[1] = rect[1]
this[2] = rect[2]
@@ -199,7 +215,7 @@ export class Rectangle extends Array<number> {
* @param point The point to check
* @returns `true` if {@link point} is inside this rectangle, otherwise `false`.
*/
containsPoint([x, y]: Point): boolean {
containsPoint([x, y]: ReadOnlyPoint): boolean {
const [left, top, width, height] = this
return x >= left && x < left + width && y >= top && y < top + height
}
@@ -210,7 +226,7 @@ export class Rectangle extends Array<number> {
* @param other The rectangle to check
* @returns `true` if {@link other} is inside this rectangle, otherwise `false`.
*/
containsRect(other: Rect | Rectangle): boolean {
containsRect(other: ReadOnlyRect): boolean {
const { right, bottom } = this
const otherRight = other[0] + other[2]
const otherBottom = other[1] + other[3]
@@ -235,7 +251,7 @@ export class Rectangle extends Array<number> {
* @param rect The rectangle to check
* @returns `true` if {@link rect} overlaps with this rectangle, otherwise `false`.
*/
overlaps(rect: Rect | Rectangle): boolean {
overlaps(rect: ReadOnlyRect): boolean {
return (
this.x < rect[0] + rect[2] &&
this.y < rect[1] + rect[3] &&
@@ -368,12 +384,12 @@ export class Rectangle extends Array<number> {
}
/** @returns The offset from the top-left of this rectangle to the point [{@link x}, {@link y}], as a new {@link Point}. */
getOffsetTo([x, y]: Point): Point {
getOffsetTo([x, y]: ReadOnlyPoint): Point {
return [x - this[0], y - this[1]]
}
/** @returns The offset from the point [{@link x}, {@link y}] to the top-left of this rectangle, as a new {@link Point}. */
getOffsetFrom([x, y]: Point): Point {
getOffsetFrom([x, y]: ReadOnlyPoint): Point {
return [this[0] - x, this[1] - y]
}
@@ -454,4 +470,14 @@ export class Rectangle extends Array<number> {
}
}
// ReadOnlyRectangle is now just Rectangle since we unified the types
export type ReadOnlyRectangle = Omit<
ReadOnlyTypedArray<Rectangle>,
| 'setHeightBottomAnchored'
| 'setWidthRightAnchored'
| 'resizeTopLeft'
| 'resizeBottomLeft'
| 'resizeTopRight'
| 'resizeBottomRight'
| 'resizeBottomRight'
| 'updateTo'
>

View File

@@ -60,7 +60,7 @@ export interface HasBoundingRect {
* @readonly
* @see {@link move}
*/
readonly boundingRect: Rectangle
readonly boundingRect: ReadOnlyRect
}
/** An object containing a set of child objects */
@@ -194,7 +194,7 @@ export interface LinkSegment {
/** The last canvas 2D path that was used to render this segment */
path?: Path2D
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
readonly _pos: [number, number]
readonly _pos: Float32Array
/**
* Y-forward along the {@link path} from its centre point, in radians.
* `undefined` if using circles for link centres.
@@ -226,13 +226,52 @@ export interface IFoundSlot extends IInputOrOutput {
}
/** A point represented as `[x, y]` co-ordinates */
export type Point = [x: number, y: number]
export type Point = [x: number, y: number] | Float32Array | Float64Array
/** A size represented as `[width, height]` */
export type Size = [width: number, height: number]
export type Size = [width: number, height: number] | Float32Array | Float64Array
/** A very firm array */
type ArRect = [x: number, y: number, width: number, height: number]
/** A rectangle starting at top-left coordinates `[x, y, width, height]` */
export type Rect = [number, number, number, number]
export type Rect = ArRect | Float32Array | Float64Array
/** A point represented as `[x, y]` co-ordinates that will not be modified */
export type ReadOnlyPoint =
| readonly [x: number, y: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
/** A size represented as `[width, height]` that will not be modified */
export type ReadOnlySize =
| readonly [width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
export type ReadOnlyRect =
| readonly [x: number, y: number, width: number, height: number]
| ReadOnlyTypedArray<Float32Array>
| ReadOnlyTypedArray<Float64Array>
type TypedArrays =
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
type TypedBigIntArrays = BigInt64Array | BigUint64Array
export type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> =
Omit<
Readonly<T>,
'fill' | 'copyWithin' | 'reverse' | 'set' | 'sort' | 'subarray'
>
/** Union of property names that are of type Match */
type KeysOfType<T, Match> = Exclude<
@@ -291,7 +330,7 @@ export interface INodeSlot extends HasBoundingRect {
nameLocked?: boolean
pos?: Point
/** @remarks Automatically calculated; not included in serialisation. */
boundingRect: Rectangle
boundingRect: Rect
/**
* A list of floating link IDs that are connected to this slot.
* This is calculated at runtime; it is **not** serialized.

View File

@@ -1,5 +1,10 @@
import type { Rectangle } from './infrastructure/Rectangle'
import type { HasBoundingRect, Point, Rect } from './interfaces'
import type {
HasBoundingRect,
Point,
ReadOnlyPoint,
ReadOnlyRect,
Rect
} from './interfaces'
import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
/**
@@ -8,7 +13,7 @@ import { Alignment, LinkDirection, hasFlag } from './types/globalEnums'
* @param b Point b as `x, y`
* @returns Distance between point {@link a} & {@link b}
*/
export function distance(a: Point, b: Point): number {
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
)
@@ -56,7 +61,10 @@ export function isInRectangle(
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isPointInRect(point: Point, rect: Rect | Rectangle): boolean {
export function isPointInRect(
point: ReadOnlyPoint,
rect: ReadOnlyRect
): boolean {
return (
point[0] >= rect[0] &&
point[0] < rect[0] + rect[2] &&
@@ -72,11 +80,7 @@ export function isPointInRect(point: Point, rect: Rect | Rectangle): boolean {
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInRect(
x: number,
y: number,
rect: Rect | Rectangle
): boolean {
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
return (
x >= rect[0] &&
x < rect[0] + rect[2] &&
@@ -117,10 +121,7 @@ export function isInsideRectangle(
* @param b Rectangle B as `x, y, width, height`
* @returns `true` if rectangles overlap, otherwise `false`
*/
export function overlapBounding(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
@@ -136,7 +137,7 @@ export function overlapBounding(
* @param rect The rectangle, as `x, y, width, height`
* @returns The centre of the rectangle, as `x, y`
*/
export function getCentre(rect: Rect | Rectangle): Point {
export function getCentre(rect: ReadOnlyRect): Point {
return [rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5]
}
@@ -146,10 +147,7 @@ export function getCentre(rect: Rect | Rectangle): Point {
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
*/
export function containsCentre(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const centreX = b[0] + b[2] * 0.5
const centreY = b[1] + b[3] * 0.5
return isInRect(centreX, centreY, a)
@@ -161,10 +159,7 @@ export function containsCentre(
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
*/
export function containsRect(
a: Rect | Rectangle,
b: Rect | Rectangle
): boolean {
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
@@ -294,8 +289,8 @@ export function rotateLink(
* the right
*/
export function getOrientation(
lineStart: Point,
lineEnd: Point,
lineStart: ReadOnlyPoint,
lineEnd: ReadOnlyPoint,
x: number,
y: number
): number {
@@ -315,10 +310,10 @@ export function getOrientation(
*/
export function findPointOnCurve(
out: Point,
a: Point,
b: Point,
controlA: Point,
controlB: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
controlA: ReadOnlyPoint,
controlB: ReadOnlyPoint,
t: number = 0.5
): void {
const iT = 1 - t
@@ -335,13 +330,8 @@ export function findPointOnCurve(
export function createBounds(
objects: Iterable<HasBoundingRect>,
padding: number = 10
): Rect | null {
const bounds: [number, number, number, number] = [
Infinity,
Infinity,
-Infinity,
-Infinity
]
): ReadOnlyRect | null {
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])
for (const obj of objects) {
const rect = obj.boundingRect
@@ -389,11 +379,11 @@ export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
* @returns The original {@link rect}, modified in place.
*/
export function alignToContainer(
rect: Rect | Rectangle,
rect: Rect,
anchors: Alignment,
[containerX, containerY, containerWidth, containerHeight]: Rect | Rectangle,
[insetX, insetY]: Point = [0, 0]
): Rect | Rectangle {
[containerX, containerY, containerWidth, containerHeight]: ReadOnlyRect,
[insetX, insetY]: ReadOnlyPoint = [0, 0]
): Rect {
if (hasFlag(anchors, Alignment.Left)) {
// Left
rect[0] = containerX + insetX
@@ -432,11 +422,11 @@ export function alignToContainer(
* @returns The original {@link rect}, modified in place.
*/
export function alignOutsideContainer(
rect: Rect | Rectangle,
rect: Rect,
anchors: Alignment,
[otherX, otherY, otherWidth, otherHeight]: Rect | Rectangle,
[outsetX, outsetY]: Point = [0, 0]
): Rect | Rectangle {
[otherX, otherY, otherWidth, otherHeight]: ReadOnlyRect,
[outsetX, outsetY]: ReadOnlyPoint = [0, 0]
): Rect {
if (hasFlag(anchors, Alignment.Left)) {
// Left
rect[0] = otherX - outsetX - rect[2]

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
OptionalProps,
Point
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -32,7 +32,7 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
this.#widget = widget ? new WeakRef(widget) : undefined
}
get collapsedPos(): Point {
get collapsedPos(): ReadOnlyPoint {
return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5]
}

View File

@@ -5,7 +5,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
OptionalProps,
Point
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
@@ -24,7 +24,7 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
return false
}
get collapsedPos(): Point {
get collapsedPos(): ReadOnlyPoint {
return [
this.#node._collapsed_width ?? LiteGraph.NODE_COLLAPSED_WIDTH,
LiteGraph.NODE_TITLE_HEIGHT * -0.5

View File

@@ -8,7 +8,8 @@ import type {
INodeSlot,
ISubgraphInput,
OptionalProps,
Point
Point,
ReadOnlyPoint
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph'
import { getCentre } from '@/lib/litegraph/src/measure'
@@ -35,7 +36,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
pos?: Point
/** The offset from the parent node to the centre point of this slot. */
get #centreOffset(): Point {
get #centreOffset(): ReadOnlyPoint {
const nodePos = this.node.pos
const { boundingRect } = this
@@ -51,7 +52,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
}
/** The center point of this slot when the node is collapsed. */
abstract get collapsedPos(): Point
abstract get collapsedPos(): ReadOnlyPoint
#node: LGraphNode
get node(): LGraphNode {

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -213,7 +213,7 @@ export class SubgraphInput extends SubgraphSlot {
}
/** For inputs, x is the right edge of the input node. */
override arrange(rect: Rect): void {
override arrange(rect: ReadOnlyRect): void {
const [right, top, width, height] = rect
const { boundingRect: b, pos } = this

View File

@@ -7,7 +7,7 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect
ReadOnlyRect
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
@@ -119,7 +119,7 @@ export class SubgraphOutput extends SubgraphSlot {
return [x + height, y + height * 0.5]
}
override arrange(rect: Rect): void {
override arrange(rect: ReadOnlyRect): void {
const [left, top, width, height] = rect
const { boundingRect: b, pos } = this

View File

@@ -11,8 +11,8 @@ import type {
INodeInputSlot,
INodeOutputSlot,
Point,
Rect,
Size
ReadOnlyRect,
ReadOnlySize
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SlotBase } from '@/lib/litegraph/src/node/SlotBase'
@@ -45,7 +45,7 @@ export abstract class SubgraphSlot
return LiteGraph.NODE_SLOT_HEIGHT
}
readonly #pos: Point = [0, 0]
readonly #pos: Point = new Float32Array(2)
readonly measurement: ConstrainedSize = new ConstrainedSize(
SubgraphSlot.defaultHeight,
@@ -133,7 +133,7 @@ export abstract class SubgraphSlot
}
}
measure(): Size {
measure(): ReadOnlySize {
const width = LGraphCanvas._measureText?.(this.displayName) ?? 0
const { defaultHeight } = SubgraphSlot
@@ -141,7 +141,7 @@ export abstract class SubgraphSlot
return this.measurement.toSize()
}
abstract arrange(rect: Rect): void
abstract arrange(rect: ReadOnlyRect): void
abstract connect(
slot: INodeInputSlot | INodeOutputSlot,

View File

@@ -25,8 +25,6 @@ export interface IWidgetOptions<TValues = unknown[]> {
property?: string
/** If `true`, an input socket will not be created for this widget. */
socketless?: boolean
/** If `true`, the widget will not be rendered by the Vue renderer. */
canvasOnly?: boolean
values?: TValues
callback?: IWidget['callback']

View File

@@ -1,6 +1,5 @@
import { afterEach, beforeEach, describe, expect, vi } from 'vitest'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { INodeInputSlot, Point } from '@/lib/litegraph/src/interfaces'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
@@ -85,8 +84,8 @@ describe('LGraphNode', () => {
}))
}
node.configure(configureData)
expect(node.pos).toEqual([50, 60])
expect(node.size).toEqual([70, 80])
expect(node.pos).toEqual(new Float32Array([50, 60]))
expect(node.size).toEqual(new Float32Array([70, 80]))
})
test('should configure inputs correctly', () => {
@@ -572,7 +571,7 @@ describe('LGraphNode', () => {
name: 'test_in',
type: 'string',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
})
test('should return position based on title height when collapsed', () => {
@@ -595,7 +594,7 @@ describe('LGraphNode', () => {
name: 'test_in_2',
type: 'number',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
node.inputs = [inputSlot, inputSlot2]
const slotIndex = 0
@@ -630,13 +629,13 @@ describe('LGraphNode', () => {
name: 'in0',
type: 'string',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0)
boundingRect: new Float32Array([0, 0, 0, 0])
}
const input1: INodeInputSlot = {
name: 'in1',
type: 'number',
link: null,
boundingRect: new Rectangle(0, 0, 0, 0),
boundingRect: new Float32Array([0, 0, 0, 0]),
pos: [5, 45]
}
node.inputs = [input0, input1]

View File

@@ -4,19 +4,19 @@ exports[`LGraph configure() > LGraph matches previous snapshot (normal configure
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -98,7 +98,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -109,19 +108,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -168,7 +167,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -180,19 +178,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -239,7 +237,6 @@ LGraph {
"selected": [Function],
},
"title": "LGraphNode",
"title_buttons": [],
"type": "mustBeSet",
"widgets": undefined,
"widgets_start_y": undefined,
@@ -252,16 +249,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
@@ -308,16 +296,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -4,19 +4,19 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -111,19 +111,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -184,19 +184,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -258,16 +258,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -4,19 +4,19 @@ exports[`LGraph (constructor only) > Matches previous snapshot > basicLGraph 1`]
LGraph {
"_groups": [
LGraphGroup {
"_bounding": [
10,
10,
140,
80,
"_bounding": Float32Array [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": [
"_pos": Float32Array [
20,
20,
],
"_size": [
"_size": Float32Array [
1,
3,
],
@@ -39,19 +39,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -109,19 +109,19 @@ LGraph {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -180,19 +180,19 @@ LGraph {
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": [
"_pos": Float32Array [
10,
10,
],
"_posSize": [
0,
0,
0,
0,
"_posSize": Float32Array [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": [
"_size": Float32Array [
140,
60,
],
@@ -252,16 +252,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
@@ -308,16 +299,7 @@ LGraph {
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"events": CustomEventTarget {},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},

View File

@@ -1,6 +1,5 @@
import { test as baseTest } from 'vitest'
import { Rectangle } from '../src/infrastructure/Rectangle'
import type { Point, Rect } from '../src/interfaces'
import {
addDirectionalOffset,
@@ -132,8 +131,8 @@ test('snapPoint correctly snaps points to grid', ({ expect }) => {
test('createBounds correctly creates bounding box', ({ expect }) => {
const objects = [
{ boundingRect: new Rectangle(0, 0, 10, 10) },
{ boundingRect: new Rectangle(5, 5, 10, 10) }
{ boundingRect: [0, 0, 10, 10] as Rect },
{ boundingRect: [5, 5, 10, 10] as Rect }
]
const defaultBounds = createBounds(objects)

Some files were not shown because too many files have changed in this diff Show More