Compare commits

..

73 Commits

Author SHA1 Message Date
Benjamin Lu
b163d17d78 Fix slot layout validity on rendering system change 2025-09-04 23:26:23 -07:00
bymyself
c52c798817 removed unused node def type guards 2025-09-04 21:16:43 -07:00
bymyself
f6ae1b6417 remove unused typeguards 2025-09-04 21:15:20 -07:00
bymyself
896e44f66b move back test that was mistakenly moved 2025-09-04 21:14:05 -07:00
bymyself
8e098fc325 simplify widget registration 2025-09-04 21:11:30 -07:00
bymyself
85fa2f4559 removed unused type guards 2025-09-04 21:02:32 -07:00
bymyself
07b7ed9385 use camelcase for layoutStore filename 2025-09-04 19:59:49 -07:00
bymyself
817f4d036a remove redundant comment 2025-09-04 19:56:49 -07:00
Christian Byrne
6eeba70f10 - Fix memory leaks in node deletion (#5345)
- Fix TypeScript types in Yjs observers with proper YEventChange type
- Refactor nested observer logic into focused single-responsibility methods
- Consolidate duplicated link segment cleanup logic into reusable methods
- Extract findLinksConnectedToNode method for better readability
- Add explanatory comments for spatial index update ordering
- Extract REROUTE_RADIUS constant instead of magic numbers
- Maintain consistent parameter naming conventions
2025-09-04 19:56:19 -07:00
bymyself
7d8bdcb05a remove debug viewport test 2025-09-04 19:43:38 -07:00
bymyself
0aed837ff4 use camelCase for all non-component files 2025-09-04 19:38:51 -07:00
bymyself
b7fd1f476c add missing translations 2025-09-04 19:30:35 -07:00
bymyself
358d98e268 cleanup comments 2025-09-04 19:25:55 -07:00
bymyself
df36693ecb cleanup comments 2025-09-04 19:21:40 -07:00
bymyself
f6051f6634 remove event forwarding hack. todo: add link moving in vue 2025-09-04 19:19:13 -07:00
bymyself
e8dae57a6a remove debug logging and setting 2025-09-04 18:54:23 -07:00
bymyself
1e307564f0 skip all vue node operations if feature is turned off 2025-09-04 18:41:07 -07:00
bymyself
9ab075f11c remove outdated README 2025-09-04 18:33:24 -07:00
bymyself
3e5effeafc remove debug viewport visualizer prop 2025-09-04 18:32:22 -07:00
bymyself
0b941583d2 remove debug viewport visualizer 2025-09-04 18:31:52 -07:00
bymyself
7149af612a change name "transformPaneEnabled" => "isVueNodesEnabled" 2025-09-04 18:27:18 -07:00
bymyself
4fc89847ab simplify unit tests 2025-09-04 18:24:48 -07:00
bymyself
2f512b847d remove debug overlay panel 2025-09-04 18:24:07 -07:00
bymyself
b0f2a1d00a remove all unused (knip pass) 2025-09-04 18:02:13 -07:00
bymyself
73a1feea99 removed unused IMAGE widget enum value 2025-09-04 17:48:52 -07:00
bymyself
7130794cba update adr README 2025-09-04 17:37:37 -07:00
bymyself
7b7f9bbb17 remove crdt ADR (moved to separate PR) 2025-09-04 17:36:26 -07:00
github-actions
0f5315f24c Update test expectations [skip ci] 2025-09-04 23:24:48 +00:00
Benjamin Lu
2425f653e4 [Vue Nodes] Fix Node Header Tests (#5360)
* Enable VueNodes

* Use KSampler not save image
2025-09-04 15:46:40 -07:00
Benjamin Lu
1480dd748a [bugfix] Fix NodeHeader test workflow path (#5359)
The test was using an incorrect path for the workflow file. Updated to use the correct path under the nodes/ subdirectory.

Fixes test failure: ENOENT error for single_save_image_node.json
2025-09-04 14:47:53 -07:00
Benjamin Lu
f99c9de72f [fix] Disable link markers on dragged connections (#5358)
Set linkMarkerShape to None for links being actively dragged by the mouse to prevent visual artifacts.
2025-09-04 14:25:06 -07:00
Benjamin Lu
2a64f538f3 Merge remote-tracking branch 'origin/main' into vue-nodes-migration 2025-09-04 13:52:26 -07:00
Christian Byrne
6a3c075df1 Removes node's dependency on LGraph for access to layout mutations composable (#5356)
* remove DI

* remove layoutMutations property on LGraph

* remove layout mutations property from LGraph snapshot
2025-09-04 13:46:39 -07:00
Benjamin Lu
c30f5a41a9 Remove IMAGE widget cont. (#5355) 2025-09-04 13:45:49 -07:00
Christian Byrne
8a10387fdf add explicit typing on component IDs (#5352) 2025-09-04 13:21:24 -07:00
Rizumu Ayaka
32cffa6a83 refactor: v3 ui slots connection dots (#5316)
* refactor: v3 ui slots connection dots

* fix: use the new useTemplateRef

* fix: slot dark-theme border and hover styles

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-09-04 11:48:48 -07:00
Rizumu Ayaka
1dbbf20124 feat: widget styles for V3 UI (#5320)
* feat: widget input text style

* feat: widget select button style

* feat: the selection style of LGraphNode

* feat(V3 UI style): color picker + file upload + input text + multi select + select + select button + slider + textarea + tree select

* feat: placeholder

* fix: filter multi select options

* fix: direct binding, no transform for select button widget
2025-09-04 10:54:26 -07:00
Christian Byrne
f83801e998 - Convert class-based LayoutMutations to useLayoutMutations() composable (#5346)
- Remove unnecessary useLayout wrapper that added boilerplate
- Use LayoutMutations interface directly in LGraph instead of redefining types
- Update all components to use composable pattern consistently
2025-09-04 10:48:33 -07:00
Benjamin Lu
969c8e6325 [bugfix] Remove placeholder IMAGE widget to restore previous functionality (#5349)
* Remove IMAGE widget

* Remove IMAGE widget test expectations
2025-09-04 10:33:02 -07:00
Simula_r
c6fc8e6f0f fix: remove clipping by removing unnecessary css contain (#5327) 2025-09-03 18:57:59 -07:00
Rizumu Ayaka
3ce3b67155 feat: localization fields (#5318) 2025-09-03 13:18:30 -07:00
Christian Byrne
da042ae829 [refactor] Remove unused legacy mutation types from layout system (#5262)
- Remove LayoutMutationType, LayoutMutation, and related interfaces
- Remove AnyLayoutMutation union type and specific mutation interfaces
- Clean up duplicate legacy types from both layoutTypes.ts and layout/types.ts
- Fix JSON syntax error in Chinese locale file (missing comma)
- Replace lodash with es-toolkit in useFloatWidget (per project standards)
- Reduces codebase by ~120 lines of unused type definitions
- CRDT operations (LayoutOperation) remain unchanged and functional

The legacy mutation types were designed for backward compatibility
but have never been used since this code hasn't been merged to main.
Only the CRDT operation types are actually used in the implementation.
2025-09-01 19:36:45 -04:00
Benjamin Lu
62096d46c1 chore: Empty commit to trigger CI checks 2025-09-01 17:59:21 -04:00
Benjamin Lu
2a5e0d231e Decouple link and slot hit-testing out of Litegraph (#5134)
* [feat] TransformPane - Viewport synchronization layer for Vue nodes (#4304)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benceruleanlu@proton.me>
Co-authored-by: github-actions <github-actions@github.com>

* Update locales [skip ci]

* Update locales [skip ci]

* Add vue node feature flag (#4927)

* feat: Implement CRDT-based layout system for Vue nodes (#4959)

* feat: Implement CRDT-based layout system for Vue nodes

Major refactor to solve snap-back issues and create single source of truth for node positions:

- Add Yjs-based CRDT layout store for conflict-free position management
- Implement layout mutations service with clean API
- Create Vue composables for layout access and node dragging
- Add one-way sync from layout store to LiteGraph
- Disable LiteGraph dragging when Vue nodes mode is enabled
- Add z-index management with bring-to-front on node interaction
- Add comprehensive TypeScript types for layout system
- Include unit tests for layout store operations
- Update documentation to reflect CRDT architecture

This provides a solid foundation for both single-user performance and future real-time collaboration features.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: Apply linter fixes to layout system

* fix: Remove unnecessary README files and revert services README

- Remove unnecessary types/README.md file
- Revert unrelated changes to services/README.md
- Keep only relevant documentation for the layout system implementation

These were issues identified during PR review that needed to be addressed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Clean up layout store and implement proper CRDT operations

- Created dedicated layoutOperations.ts with production-grade CRDT interfaces
- Integrated existing QuadTree spatial index instead of simple cache
- Split composables into separate files (useLayout, useNodeLayout, useLayoutSync)
- Cleaned up operation handlers using specific types instead of Extract
- Added proper operation interfaces with type guards and extensibility
- Updated all type references to use new operation structure

The layout store now properly uses the existing QuadTree infrastructure for
efficient spatial queries and follows CRDT best practices with well-defined
operation interfaces.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract services and split composables for better organization

- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Revert "Totally not scuffed renderer and adapter"

This reverts commit 2b9d83efb8.

* Revert "Remove slots from layoutTypes"

This reverts commit 18f78ff786.

* Reapply "Add node slots to layout tree"

This reverts commit 236fecb549.

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* docs: Replace architecture docs with comprehensive ADR

- Add ADR-0002 for CRDT-based layout system decision
- Follow established ADR template with persuasive reasoning
- Include performance benefits, collaboration readiness, and architectural advantages
- Update ADR index

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>

* [chore] Extract link rendering out of LGraphCanvas (#4994)

* feat: Implement CRDT-based layout system for Vue nodes

Major refactor to solve snap-back issues and create single source of truth for node positions:

- Add Yjs-based CRDT layout store for conflict-free position management
- Implement layout mutations service with clean API
- Create Vue composables for layout access and node dragging
- Add one-way sync from layout store to LiteGraph
- Disable LiteGraph dragging when Vue nodes mode is enabled
- Add z-index management with bring-to-front on node interaction
- Add comprehensive TypeScript types for layout system
- Include unit tests for layout store operations
- Update documentation to reflect CRDT architecture

This provides a solid foundation for both single-user performance and future real-time collaboration features.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: Apply linter fixes to layout system

* fix: Remove unnecessary README files and revert services README

- Remove unnecessary types/README.md file
- Revert unrelated changes to services/README.md
- Keep only relevant documentation for the layout system implementation

These were issues identified during PR review that needed to be addressed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Clean up layout store and implement proper CRDT operations

- Created dedicated layoutOperations.ts with production-grade CRDT interfaces
- Integrated existing QuadTree spatial index instead of simple cache
- Split composables into separate files (useLayout, useNodeLayout, useLayoutSync)
- Cleaned up operation handlers using specific types instead of Extract
- Added proper operation interfaces with type guards and extensibility
- Updated all type references to use new operation structure

The layout store now properly uses the existing QuadTree infrastructure for
efficient spatial queries and follows CRDT best practices with well-defined
operation interfaces.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract services and split composables for better organization

- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Revert "Totally not scuffed renderer and adapter"

This reverts commit 2b9d83efb8.

* Revert "Remove slots from layoutTypes"

This reverts commit 18f78ff786.

* Reapply "Add node slots to layout tree"

This reverts commit 236fecb549.

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* docs: Replace architecture docs with comprehensive ADR

- Add ADR-0002 for CRDT-based layout system decision
- Follow established ADR template with persuasive reasoning
- Include performance benefits, collaboration readiness, and architectural advantages
- Update ADR index

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Remove unused methods in LGLA

* Extract slot position calculations to shared utility

- Create slotCalculations.ts utility for centralized slot position logic
- Update LGraphNode to delegate to helper while maintaining compatibility
- Modify LitegraphLinkAdapter to use layout tree positions when available
- Enable link rendering to use layout system coordinates instead of litegraph positions

This allows the layout tree to control link rendering positions, enabling proper
synchronization between Vue components and canvas rendering.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [fix] Restore original link rendering behavior after refactor

This commit fixes several rendering discrepancies introduced during the link rendering refactor to ensure exact parity with the original litegraph implementation:

Path Shape Fixes:
- STRAIGHT_LINK: Now correctly applies l=10 offset to create innerA/innerB points and uses midX=(innerA.x+innerB.x)*0.5 for elbow placement, matching the original 6-segment path
- LINEAR_LINK: Restored 4-point path with l=15 directional offsets (start → innerA → innerB → end)

Arrow Rendering:
- computeConnectionPoint: Now always uses bezier math with 0.25 factor spline offsets regardless of render mode, ensuring arrow positions match original
- Arrow positions: Fixed to render at 0.25 and 0.75 positions along the path
- Arrow gating: Moved scale>=0.6 and highQuality checks to adapter layer to maintain PathRenderer purity
- Arrow shape: Restored original triangle dimensions (-5,-3) to (0,+7) to (+5,-3)

Center Marker:
- Fixed 'None' option: Center marker now correctly hidden when LinkMarkerShape.None is selected
- Center point calculation: Updated for all render modes to match original positions
- STRAIGHT_LINK center: Uses midX and average of innerA/innerB y-coordinates
- LINEAR_LINK center: Uses midpoint between innerA and innerB control points

These fixes ensure backward compatibility while maintaining the clean separation between the pure PathRenderer and litegraph-specific LitegraphLinkAdapter.

Fixes #Issue-Number

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>

* feat: Add slot registration and spatial indexing for hit detection

- Implement slot registration for all nodes (Vue and LiteGraph)
- Add spatial indexes for slots and reroutes to improve hit detection performance
- Register slots when nodes are drawn via new registerSlots() method
- Update LayoutStore to use spatial indexing for O(log n) queries instead of O(n)

Resolves #5125

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "feat: Add slot registration and spatial indexing for hit detection"

This reverts commit 70fbfd0f5e.

* feat: Add slot registration and spatial indexing for hit detection

- Implement slot registration for all nodes (Vue and LiteGraph)
- Add spatial indexes for slots and reroutes to improve hit detection performance
- Register slots when nodes are drawn via new registerSlots() method
- Update LayoutStore to use spatial indexing for O(log n) queries instead of O(n)

Resolves #5125

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* relocate slot update to layoutstore

* Revert "relocate slot update to layoutstore"

This reverts commit 0b17ef148bdded35cb231bef25b8d5c77dc14c1f.

* add useSlotLayoutSync

* feat: Extend Layout Store with CRDT support for links and reroutes

Move links and reroutes to be first-class CRDT entities in the Layout Store,
eliminating per-frame registration during rendering. This provides a ~100x
reduction in spatial index operations by using event-driven updates instead
of polling.

Key changes:
- Add CRDT maps for links and reroutes with automatic observers
- Add mutation operations for link/reroute lifecycle management
- Update LiteGraph to use mutations instead of direct store calls
- Remove per-frame updateLinkLayout and updateRerouteLayout calls

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Scuffed diff, change to dirty later

* Fix reroute move desync

* Terrible reroute fixes

* Use LinkId for LinkLayout

* refactor: Remove unused duplicate layout type files

Deleted src/types/layoutTypes.ts and src/types/layoutOperations.ts
which were duplicates of src/renderer/core/layout/types.ts. These
files had zero imports and were creating confusion in the codebase.

The active types are in src/renderer/core/layout/types.ts which
is properly integrated with the current architecture.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract layout source strings into LayoutSource enum

Replace hardcoded 'canvas' | 'vue' | 'external' string literals with a proper TypeScript enum for better type safety and maintainability. This change provides a single source of truth for layout source types and makes future modifications easier.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Unify CRDT layout operations under type-safe entity bases

Replace node-centric BaseOperation with a clean hierarchy:
- Add OperationMeta base containing common fields (timestamp, actor, source, type)
- Introduce entity-specific bases (NodeOpBase, LinkOpBase, RerouteOpBase)
- Each operation now extends its appropriate entity base with proper typing
- Add entity discriminator field for runtime type narrowing

Benefits:
- Eliminates duplicate meta fields across link/reroute operations
- Provides type-safe discriminated unions for each entity type
- Enables clean extension path for future operation types
- Zero breaking changes - type-only refactor with no runtime impact

Also adds helper functions:
- getAffectedNodeIds() to extract node IDs affected by any operation
- Entity-specific helper checks for operation classification

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix initial link seeding

* fix: Fix reroute hit detection and type consistency issues

- Use instanceof Reroute type guard instead of structural 'linkIds' check
- Remove unnecessary Number() conversions for reroute IDs (already numeric)
- Fix parentId truthiness bug (0 is valid parent ID)
- Pass numeric IDs directly in GraphCanvas seeding
- Add missing link/reroute methods to LayoutMutations interface
- Make hit test tolerance scale-aware using ctx.lineWidth and DPI

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add debug logs

* Add missing reroute path

* cleanup

* feat: Implement event-driven link layout sync

Remove layout store writes from render loop and update link geometry only on
actual changes (node move/resize, link/reroute operations, collapse toggles).

Key improvements:
- No layout writes during canvas render (decoupled from draw cycle)
- Link layouts update only on causal events via useLinkLayoutSync
- Hit testing remains precise using stored Path2D objects
- Optimized adapter: calculations only when enableLayoutStoreWrites=true
- Store-level deduplication prevents spatial index churn

Performance impact:
- Render path: Zero layout work, no equality checks, no store writes
- Event path: Direct writes with cheap store-level dedup
- Significant CPU savings per frame on complex graphs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Implement DOM-based slot registration with unified position system

- Add centralized getSlotPosition() function in SlotCalculations
- Create SlotIdentifier utilities for consistent slot key generation
- Implement DOM-based slot registration composable with performance optimizations:
  - Cache slot offsets to avoid DOM reads during drag operations
  - Batch measurements via requestAnimationFrame
  - Skip redundant updates when bounds unchanged
- Update Vue slot components to register DOM positions
- Fix widget-to-input index mapping in NodeWidgets
- Prevent double registration when Vue nodes enabled

This improves slot hit-detection accuracy by using actual DOM positions
while maintaining performance through intelligent caching and batching.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove unused files

* Remove duplicated markdown file

* Remove duplicated files and address knip concerns

* Remove outdated test

* warning comment

* Update test snapshots

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-09-01 17:01:17 -04:00
Benjamin Lu
08309595e0 Fix lodash import (#5269) 2025-08-30 18:00:56 -04:00
Benjamin Lu
3982f29fda Update lockfile after rebase (#5254)
* chore: Update pnpm-lock.yaml after rebase

Add new dependencies from main branch:
- chart.js@^4.5.0
- clsx@^2.1.1
- tailwind-merge@^3.3.1
- yjs@^13.6.27

* Fix SelectionOverlay rebase issue (#5255)

* fix: Remove SelectionOverlay import accidentally re-added during rebase

During the rebase, the SelectionOverlay component import and usage was accidentally
re-introduced. This component was removed in commit 84e7102f (#5158) to fix
performance issues. The SelectionToolbox should be used directly without a wrapper.

The current main branch correctly uses:
<SelectionToolbox v-if="selectionToolboxEnabled" />

Ref: https://github.com/Comfy-Org/ComfyUI_frontend/pull/5158

* Deduplicate i18n keys from rebasing (#5257)

* fix: Add missing comma in zh locale JSON

Fixes JSON syntax error introduced during rebase.

* dedup i18n keys

* fix: Restore simplified Chinese translation for Toggle Workflows Sidebar

The previous dedup commit accidentally left a traditional Chinese
translation in the simplified Chinese locale file.

* fix: Replace remaining traditional Chinese characters in simplified Chinese locale

- Changed '檔案' to '文件' (file)
- Changed '擴充功能' to '扩展功能' (extensions)
2025-08-30 17:50:55 -04:00
Rizumu Ayaka
57db10f408 feat: v3 style of node body (#5169)
* feat: v3 style of node body

* Update src/renderer/extensions/vueNodes/components/LGraphNode.vue

* fix: review's issues

* fix: review's issue
2025-08-29 14:11:41 -04:00
Benjamin Lu
1447b15f4c [bugfix] Hide center dot when dragging links (#5133)
The center dot/marker on links should not be visible when the user is dragging links to connect nodes. This fix ensures the center marker is hidden during link dragging operations.

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 14:11:25 -04:00
Benjamin Lu
4a7c955a15 [bugfix] Fix link center dot hit detection when marker is disabled (#5135)
When linkMarkerShape is set to None, clicks were still being detected on the invisible center dot. This fix adds proper checks to skip hit detection when the center marker is disabled.

Fixes center dot hit detection issue

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 14:11:25 -04:00
Benjamin Lu
ba1fa1be25 Revert "feat: Add slot registration and spatial indexing for hit detection"
This reverts commit 70fbfd0f5e.
2025-08-29 14:11:25 -04:00
Benjamin Lu
5171decd8b feat: Add slot registration and spatial indexing for hit detection
- Implement slot registration for all nodes (Vue and LiteGraph)
- Add spatial indexes for slots and reroutes to improve hit detection performance
- Register slots when nodes are drawn via new registerSlots() method
- Update LayoutStore to use spatial indexing for O(log n) queries instead of O(n)

Resolves #5125

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 14:06:33 -04:00
Christian Byrne
5a74c019c7 remove logging from vue node layouting modules (#5111) 2025-08-29 13:59:48 -04:00
Christian Byrne
934c650790 Remove layout logging noise from console (#5101)
- Remove loglevel import and logger setup from LayoutStore
- Remove all logger.debug() calls throughout LayoutStore
- Remove localStorage debug check for layout operations
- Remove unused DEBUG_CONFIG from layout constants
- Clean up console noise while preserving error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 13:59:48 -04:00
bymyself
b2a828dda6 [bugfix] Fix Vue node import path after refactoring
Update LGraphNode import path from old location to new domain-driven architecture path.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 13:59:48 -04:00
Christian Byrne
d1ed5ecc0d fix: Initialize Vue node manager when first node is added to empty graph (#5086)
* fix: Initialize Vue node manager when first node is added to empty graph

When Vue nodes are enabled and the graph starts empty (0 nodes), the
node manager wasn't being initialized. This caused Vue nodes to not
render until the setting was toggled off and on again.

The fix adds a one-time event handler that listens for the first node
being added to an empty graph and initializes the node manager at that
point.

Fixes the issue where Vue nodes don't render on initial page load when
the setting is enabled.

* fix: Add TODO comment for reactive graph mutations observer

Added comment to indicate that the monkey-patching approach should be
replaced with a proper reactive graph mutations observer when available.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 13:59:48 -04:00
Christian Byrne
bfcbcf4873 [refactor] Reorganize Vue nodes to domain-driven design architecture (#5085)
* refactor: Reorganize Vue nodes system to domain-driven design architecture

Move Vue nodes code from scattered technical layers to domain-focused structure:

- Widget system → src/renderer/extensions/vueNodes/widgets/
- LOD optimization → src/renderer/extensions/vueNodes/lod/
- Layout logic → src/renderer/extensions/vueNodes/layout/
- Node components → src/renderer/extensions/vueNodes/components/
- Test structure mirrors source organization

Benefits:
- Clear domain boundaries instead of technical layers
- Everything Vue nodes related in renderer domain (not workbench)
- camelCase naming (vueNodes vs vue-nodes)
- Tests co-located with source domains
- All imports updated to new DDD structure

* fix: Skip spatial index performance test on CI to avoid flaky timing

Performance tests are inherently flaky on CI due to variable system
performance. This test should only run locally like the other
performance tests.
2025-08-29 13:59:48 -04:00
Christian Byrne
0dd4ff2087 refactor: Reorganize layout system into new renderer architecture (#5071)
- Move layout system to renderer/core/layout/
  - Store, operations, adapters, and sync modules organized clearly
  - Merged layoutTypes.ts and layoutOperations.ts into single types.ts
- Move canvas rendering to renderer/core/canvas/
  - LiteGraph-specific code in litegraph/ subdirectory
  - PathRenderer at canvas level
- Move spatial indexing to renderer/core/spatial/
- Move Vue node composables to renderer/extensions/vue-nodes/
- Update all import paths throughout codebase
- Apply consistent naming (renderer vs rendering)

This establishes clearer separation between core rendering concerns
and optional extensions, making the architecture more maintainable.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 13:59:48 -04:00
Benjamin Lu
889d136154 [chore] Extract link rendering out of LGraphCanvas (#4994)
* feat: Implement CRDT-based layout system for Vue nodes

Major refactor to solve snap-back issues and create single source of truth for node positions:

- Add Yjs-based CRDT layout store for conflict-free position management
- Implement layout mutations service with clean API
- Create Vue composables for layout access and node dragging
- Add one-way sync from layout store to LiteGraph
- Disable LiteGraph dragging when Vue nodes mode is enabled
- Add z-index management with bring-to-front on node interaction
- Add comprehensive TypeScript types for layout system
- Include unit tests for layout store operations
- Update documentation to reflect CRDT architecture

This provides a solid foundation for both single-user performance and future real-time collaboration features.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: Apply linter fixes to layout system

* fix: Remove unnecessary README files and revert services README

- Remove unnecessary types/README.md file
- Revert unrelated changes to services/README.md
- Keep only relevant documentation for the layout system implementation

These were issues identified during PR review that needed to be addressed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Clean up layout store and implement proper CRDT operations

- Created dedicated layoutOperations.ts with production-grade CRDT interfaces
- Integrated existing QuadTree spatial index instead of simple cache
- Split composables into separate files (useLayout, useNodeLayout, useLayoutSync)
- Cleaned up operation handlers using specific types instead of Extract
- Added proper operation interfaces with type guards and extensibility
- Updated all type references to use new operation structure

The layout store now properly uses the existing QuadTree infrastructure for
efficient spatial queries and follows CRDT best practices with well-defined
operation interfaces.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract services and split composables for better organization

- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Revert "Totally not scuffed renderer and adapter"

This reverts commit 2b9d83efb8.

* Revert "Remove slots from layoutTypes"

This reverts commit 18f78ff786.

* Reapply "Add node slots to layout tree"

This reverts commit 236fecb549.

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* docs: Replace architecture docs with comprehensive ADR

- Add ADR-0002 for CRDT-based layout system decision
- Follow established ADR template with persuasive reasoning
- Include performance benefits, collaboration readiness, and architectural advantages
- Update ADR index

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Remove unused methods in LGLA

* Extract slot position calculations to shared utility

- Create slotCalculations.ts utility for centralized slot position logic
- Update LGraphNode to delegate to helper while maintaining compatibility
- Modify LitegraphLinkAdapter to use layout tree positions when available
- Enable link rendering to use layout system coordinates instead of litegraph positions

This allows the layout tree to control link rendering positions, enabling proper
synchronization between Vue components and canvas rendering.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [fix] Restore original link rendering behavior after refactor

This commit fixes several rendering discrepancies introduced during the link rendering refactor to ensure exact parity with the original litegraph implementation:

Path Shape Fixes:
- STRAIGHT_LINK: Now correctly applies l=10 offset to create innerA/innerB points and uses midX=(innerA.x+innerB.x)*0.5 for elbow placement, matching the original 6-segment path
- LINEAR_LINK: Restored 4-point path with l=15 directional offsets (start → innerA → innerB → end)

Arrow Rendering:
- computeConnectionPoint: Now always uses bezier math with 0.25 factor spline offsets regardless of render mode, ensuring arrow positions match original
- Arrow positions: Fixed to render at 0.25 and 0.75 positions along the path
- Arrow gating: Moved scale>=0.6 and highQuality checks to adapter layer to maintain PathRenderer purity
- Arrow shape: Restored original triangle dimensions (-5,-3) to (0,+7) to (+5,-3)

Center Marker:
- Fixed 'None' option: Center marker now correctly hidden when LinkMarkerShape.None is selected
- Center point calculation: Updated for all render modes to match original positions
- STRAIGHT_LINK center: Uses midX and average of innerA/innerB y-coordinates
- LINEAR_LINK center: Uses midpoint between innerA and innerB control points

These fixes ensure backward compatibility while maintaining the clean separation between the pure PathRenderer and litegraph-specific LitegraphLinkAdapter.

Fixes #Issue-Number

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-29 13:59:48 -04:00
Christian Byrne
c773230b21 feat: Implement CRDT-based layout system for Vue nodes (#4959)
* feat: Implement CRDT-based layout system for Vue nodes

Major refactor to solve snap-back issues and create single source of truth for node positions:

- Add Yjs-based CRDT layout store for conflict-free position management
- Implement layout mutations service with clean API
- Create Vue composables for layout access and node dragging
- Add one-way sync from layout store to LiteGraph
- Disable LiteGraph dragging when Vue nodes mode is enabled
- Add z-index management with bring-to-front on node interaction
- Add comprehensive TypeScript types for layout system
- Include unit tests for layout store operations
- Update documentation to reflect CRDT architecture

This provides a solid foundation for both single-user performance and future real-time collaboration features.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: Apply linter fixes to layout system

* fix: Remove unnecessary README files and revert services README

- Remove unnecessary types/README.md file
- Revert unrelated changes to services/README.md
- Keep only relevant documentation for the layout system implementation

These were issues identified during PR review that needed to be addressed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Clean up layout store and implement proper CRDT operations

- Created dedicated layoutOperations.ts with production-grade CRDT interfaces
- Integrated existing QuadTree spatial index instead of simple cache
- Split composables into separate files (useLayout, useNodeLayout, useLayoutSync)
- Cleaned up operation handlers using specific types instead of Extract
- Added proper operation interfaces with type guards and extensibility
- Updated all type references to use new operation structure

The layout store now properly uses the existing QuadTree infrastructure for
efficient spatial queries and follows CRDT best practices with well-defined
operation interfaces.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Extract services and split composables for better organization

- Created SpatialIndexManager to handle QuadTree operations separately
- Added LayoutAdapter interface for CRDT abstraction (Yjs, mock implementations)
- Split GraphNodeManager into focused composables:
  - useNodeWidgets: Widget state and callback management
  - useNodeChangeDetection: RAF-based geometry change detection
  - useNodeState: Node visibility and reactive state management
- Extracted constants for magic numbers and configuration values
- Updated layout store to use SpatialIndexManager and constants

This improves code organization, testability, and makes it easier to swap
CRDT implementations or mock services for testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add node slots to layout tree

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* Remove slots from layoutTypes

* Totally not scuffed renderer and adapter

* Revert "Totally not scuffed renderer and adapter"

This reverts commit 2b9d83efb8.

* Revert "Remove slots from layoutTypes"

This reverts commit 18f78ff786.

* Reapply "Add node slots to layout tree"

This reverts commit 236fecb549.

* Revert "Add node slots to layout tree"

This reverts commit 460493a620.

* docs: Replace architecture docs with comprehensive ADR

- Add ADR-0002 for CRDT-based layout system decision
- Follow established ADR template with persuasive reasoning
- Include performance benefits, collaboration readiness, and architectural advantages
- Update ADR index

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
2025-08-29 13:59:48 -04:00
Benjamin Lu
8df41ab040 Add vue node feature flag (#4927) 2025-08-29 13:51:53 -04:00
Benjamin Lu
2b9a9e2371 test(ci): skip transformPerformance suite on CI (#4843) 2025-08-29 13:49:27 -04:00
Benjamin Lu
4171219402 Revert "refactor(litegraph): decouple render-time state from models for reroutes and links\n\nIntroduce RenderedLinkSegment; compute reroute render params without mutating model; render into ephemeral segments instead of writing to Reroute/LLink."
This reverts commit d7ed1d36ed.
2025-08-29 13:49:27 -04:00
Benjamin Lu
301355e018 refactor(litegraph): decouple render-time state from models for reroutes and links\n\nIntroduce RenderedLinkSegment; compute reroute render params without mutating model; render into ephemeral segments instead of writing to Reroute/LLink. 2025-08-29 13:49:27 -04:00
github-actions
ac17752c0b Update locales [skip ci] 2025-08-29 13:49:27 -04:00
Christian Byrne
fc6943cdd3 Fix TransformPane pos/size (#4826) 2025-08-29 13:37:44 -04:00
github-actions
9db96f2dcc Update locales [skip ci] 2025-08-29 13:37:44 -04:00
Christian Byrne
19084e2799 [feat] TransformPane - Viewport synchronization layer for Vue nodes (#4304)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benceruleanlu@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-29 13:17:40 -04:00
bymyself
6e04cb72b0 [feat] Add Vue action widgets
- WidgetButton: Action button with Button component and callback handling
- WidgetFileUpload: File upload interface with FileUpload component
2025-08-29 05:07:50 -04:00
bymyself
9c4d782b30 [feat] Add Vue visual widgets
- WidgetColorPicker: Color selection with ColorPicker component
- WidgetImage: Single image display with Image component
- WidgetImageCompare: Before/after comparison with ImageCompare component
- WidgetGalleria: Image gallery/carousel with Galleria component
- WidgetChart: Data visualization with Chart component
2025-08-29 05:07:50 -04:00
bymyself
ac60d1ceb4 [feat] Add Vue selection widgets
- WidgetSelect: Dropdown selection with Select component
- WidgetMultiSelect: Multiple selection with MultiSelect component
- WidgetSelectButton: Button group selection with SelectButton component
- WidgetTreeSelect: Hierarchical selection with TreeSelect component
2025-08-29 05:07:50 -04:00
bymyself
06d0a63a2f [feat] Add Vue input widgets
- WidgetInputText: Single-line text input with InputText component
- WidgetTextarea: Multi-line text input with Textarea component
- WidgetSlider: Numeric range input with Slider component
- WidgetToggleSwitch: Boolean toggle with ToggleSwitch component
2025-08-29 05:07:50 -04:00
bymyself
2dcfe84e99 [feat] Add Vue widget registry system
- Complete widget type enum with all 15 widget types
- Component mapping registry for dynamic widget rendering
- Helper function for type-safe widget component resolution
2025-08-29 05:07:50 -04:00
bymyself
f42a4dd6cc [feat] Add core Vue widget infrastructure
- SimplifiedWidget interface for Vue-based node widgets
- widgetPropFilter utility with component-specific exclusion lists
- Removes DOM manipulation and positioning concerns
- Provides clean API for value binding and prop filtering
2025-08-29 05:07:50 -04:00
46 changed files with 1801 additions and 269 deletions

View File

@@ -37,8 +37,6 @@ jobs:
}
const pr = pullRequests[0];
console.log(`✅ Found PR #${pr.number} for branch: ${context.payload.workflow_run.head_branch}`);
console.log(`PR number is: ${pr.number}`);
const branchName = context.payload.workflow_run.head_branch;
const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
@@ -80,38 +78,14 @@ jobs:
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
DEPLOYMENT_URL=""
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
# Capture wrangler output to extract deployment URL
OUTPUT=$(npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }} 2>&1)
EXIT_CODE=$?
echo "$OUTPUT"
if [ $EXIT_CODE -eq 0 ]; then
if npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
# Extract the deployment URL from wrangler output
# Look for the URL in various formats that wrangler might output
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[a-z0-9.-]+\.pages\.dev(/[^[:space:]]*)?$' | head -1)
if [ -z "$DEPLOYMENT_URL" ]; then
# Try another pattern if the first one fails
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[^[:space:]]+\.pages\.dev' | head -1)
fi
if [ -n "$DEPLOYMENT_URL" ]; then
echo "Deployment URL: $DEPLOYMENT_URL"
echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT
else
echo "Warning: Could not extract deployment URL from wrangler output"
# Construct expected URL as fallback
FALLBACK_URL="https://${{ steps.project-name.outputs.name }}-${{ steps.project-name.outputs.branch }}.pages.dev"
echo "Using fallback URL: $FALLBACK_URL"
echo "url=$FALLBACK_URL" >> $GITHUB_OUTPUT
fi
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
@@ -129,32 +103,6 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Save deployment info
if: fromJSON(steps.pr-info.outputs.result).number != null && always()
run: |
# Read test exit code from the artifact
TEST_EXIT_CODE="1"
if [ -f "playwright-report/test-exit-code.txt" ]; then
TEST_EXIT_CODE=$(cat playwright-report/test-exit-code.txt)
fi
# Use deployment URL if available, otherwise use a fallback
URL="${{ steps.cloudflare-deploy.outputs.url }}"
if [ -z "$URL" ] || [ "${{ steps.cloudflare-deploy.outcome }}" != "success" ]; then
URL="https://${{ steps.project-name.outputs.name }}-${{ steps.project-name.outputs.branch }}.pages.dev"
fi
echo "${{ matrix.browser }}|$TEST_EXIT_CODE|$URL" > deployment-info.txt
echo "Saved deployment info: ${{ matrix.browser }}|$TEST_EXIT_CODE|$URL"
- name: Upload deployment info
if: fromJSON(steps.pr-info.outputs.result).number != null && always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info.txt
retention-days: 1
comment-tests-starting:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
@@ -179,9 +127,7 @@ jobs:
return null;
}
const prNumber = pullRequests[0].number;
console.log(`✅ Found PR #${prNumber} for branch: ${context.payload.workflow_run.head_branch}`);
return prNumber;
return pullRequests[0].number;
- name: Get completion time
id: completion-time
@@ -242,9 +188,7 @@ jobs:
return null;
}
const prNumber = pullRequests[0].number;
console.log(`✅ Found PR #${prNumber} for branch: ${context.payload.workflow_run.head_branch}`);
return prNumber;
return pullRequests[0].number;
- name: Download all deployment info
if: steps.pr.outputs.result != 'null'

View File

@@ -231,21 +231,12 @@ jobs:
id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
working-directory: ComfyUI_frontend
continue-on-error: true
- name: Save test exit code
if: always()
run: |
echo "${{ steps.playwright.outcome == 'success' && '0' || '1' }}" > test-exit-code.txt
echo "Test outcome: ${{ steps.playwright.outcome }}"
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.browser }}
path: |
ComfyUI_frontend/playwright-report/
test-exit-code.txt
path: ComfyUI_frontend/playwright-report/
retention-days: 30
# Merge sharded test reports

12
.mcp.json Normal file
View File

@@ -0,0 +1,12 @@
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}

View File

@@ -1,156 +0,0 @@
# 4. Centralized Layout Management with CRDT
Date: 2025-08-27
## Status
Proposed
## Context
ComfyUI's node graph editor currently suffers from fundamental architectural limitations around spatial data management that prevent us from achieving key product goals.
### Current Architecture Problems
The existing system allows each node to directly mutate its position within LiteGraph's canvas renderer. This creates several critical issues:
1. **Performance Bottlenecks**: UI updates require full graph traversals to detect position changes. Large workflows (100+ nodes) can create bottlenecks during interactions due to this O(n) polling approach.
2. **Position Conflicts**: Multiple systems (LiteGraph canvas, DOMwidgets.ts overlays) currently compete to control node positions. Future Vue widget overlays will compound this maintenance burden.
3. **No Collaboration Foundation**: Direct position mutations make concurrent editing impossible—there's no mechanism to merge conflicting position updates from multiple users.
4. **Renderer Lock-in**: Spatial data is tightly coupled to LiteGraph's canvas implementation, preventing alternative rendering approaches (WebGL, DOM, other libraries, hybrid approaches).
5. **Inefficient Change Detection**: While LiteGraph provides some events, many operations require polling via changeTracker.ts. The current undo/redo system performs expensive diffs on every interaction rather than using reactive push/pull signals, creating performance bottlenecks and blocking efficient animations and viewport culling.
This represents a fundamental architectural limitation: diff-based systems scale O(n) with graph complexity (traverse entire structure to detect changes), while signal-based reactive systems scale O(1) with actual changes (data mutations automatically notify subscribers). Modern frameworks (Vue 3, Angular signals, SolidJS) have moved to reactive approaches for precisely this performance reason.
### Business Context
- Performance issues emerge with workflow complexity (100+ nodes)
- The AI workflow community increasingly expects collaborative features (similar to Figma, Miro)
- Accessibility requirements will necessitate DOM-based rendering options
- Technical debt compounds with each new spatial feature
This decision builds on [ADR-0001 (Merge LiteGraph)](0001-merge-litegraph-into-frontend.md), which enables the architectural restructuring proposed here.
## Decision
We will implement a centralized layout management system using CRDT (Conflict-free Replicated Data Types) with command pattern architecture to separate spatial data from rendering behavior.
### Centralized State Management Foundation
This solution applies proven centralized state management patterns:
- **Centralized Store**: All spatial data (position, size, bounds, transform) managed in a single CRDT-backed store
- **Command Interface**: All mutations flow through explicit commands rather than direct property access
- **Observer Pattern**: Independent systems (rendering, interaction, layout) subscribe to state changes
- **Domain Separation**: Layout logic completely separated from rendering and UI concerns
This provides single source of truth, predictable state updates, and natural system decoupling—solving our core architectural problems.
### Core Architecture
1. **Centralized Layout Store**: A Yjs CRDT maintains all spatial data in a single authoritative store:
```typescript
// Instead of: node.position = {x, y}
layoutStore.moveNode(nodeId, {x, y})
```
2. **Command Pattern**: All spatial mutations flow through explicit commands:
```
User Input → Commands → Layout Store → Observer Notifications → Renderers
```
3. **Observer-Based Systems**: Multiple independent systems subscribe to layout changes:
- **Rendering Systems**: LiteGraph canvas, WebGL, DOM accessibility renderers
- **Interaction Systems**: Drag handlers, selection, hover states
- **Layout Systems**: Auto-layout, alignment, distribution
- **Animation Systems**: Smooth transitions, physics simulations
4. **Reactive Updates**: Store changes propagate through observers, eliminating polling and enabling efficient system coordination.
### Implementation Strategy
**Phase 1: Parallel System**
- Build CRDT layout store alongside existing system
- Layout store initially mirrors LiteGraph changes via observers
- Gradually migrate user interactions to use command interface
- Maintain full backward compatibility
**Phase 2: Inversion of Control**
- CRDT store becomes single source of truth
- LiteGraph receives position updates via reactive subscriptions
- Enable alternative renderers and advanced features
### Why Centralized State + CRDT?
This combination provides both architectural and technical benefits:
**Centralized State Benefits:**
- **Single Source of Truth**: All layout data managed in one place, eliminating conflicts
- **System Decoupling**: Rendering, interaction, and layout systems operate independently
- **Predictable Updates**: Clear data flow makes debugging and testing easier
- **Extensibility**: Easy to add new layout behaviors without modifying existing systems
**CRDT Benefits:**
- **Conflict Resolution**: Automatic merging eliminates position conflicts between systems
- **Collaboration-Ready**: Built-in support for multi-user editing
- **Eventual Consistency**: Guaranteed convergence to same state across all clients
**Yjs-Specific Benefits:**
- **Event-Driven**: Native observer pattern removes need for polling
- **Selective Updates**: Only changed nodes trigger system updates
- **Fine-Grained Changes**: Efficient delta synchronization
## Consequences
### Positive
- **Eliminates Polling**: Observer pattern removes O(n) graph traversals, improving performance
- **System Modularity**: Independent systems can be developed, tested, and optimized separately
- **Renderer Flexibility**: Easy to add WebGL, DOM accessibility, or hybrid rendering systems
- **Rich Interactions**: Command pattern enables robust undo/redo, macros, and interaction history
- **Collaboration-Ready**: CRDT foundation enables real-time multi-user editing
- **Conflict Resolution**: Eliminates position "snap-back" behavior between competing systems
- **Better Developer Experience**: Clear separation of concerns and predictable data flow patterns
### Negative
- **Learning Curve**: Team must understand CRDT concepts and centralized state management
- **Migration Complexity**: Gradual migration of existing direct property access requires careful coordination
- **Memory Overhead**: Yjs library (~30KB) plus operation history storage
- **CRDT Performance**: CRDTs have computational overhead compared to direct property access
- **Increased Abstraction**: Additional layer between user interactions and visual updates
### Risk Mitigations
- Provide comprehensive migration documentation and examples
- Build compatibility layer for gradual, low-risk migration
- Implement operation history pruning for long-running sessions
- Phase implementation to validate approach before full migration
## Notes
This centralized state + CRDT architecture follows patterns from modern collaborative applications:
**Centralized State Management**: Similar to Redux/Vuex patterns in complex web applications, but with CRDT backing for collaboration. This provides predictable state updates while enabling real-time multi-user features.
**CRDT in Collaboration**: Tools like Figma, Linear, and Notion use similar approaches for real-time collaboration, demonstrating the effectiveness of separating authoritative data from presentation logic.
**Future Capabilities**: This foundation enables advanced features that would be difficult with the current architecture:
- Macro recording and workflow automation
- Programmatic layout optimization and constraints
- API-driven workflow construction
- Multiple simultaneous renderers (canvas + accessibility DOM)
- Real-time collaborative editing
- Advanced spatial features (physics, animations, auto-layout)
The architecture provides immediate single-user benefits while creating infrastructure for collaborative and advanced spatial features.
## References
- [Yjs Documentation](https://docs.yjs.dev/)
- [CRDTs: The Hard Parts](https://martin.kleppmann.com/2020/07/06/crdt-hard-parts-hydra.html) by Martin Kleppmann
- [Figma's Multiplayer Technology](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/)

View File

@@ -11,7 +11,7 @@ An Architecture Decision Record captures an important architectural decision mad
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Proposed | 2025-08-25 |
## Creating a New ADR
@@ -77,4 +77,4 @@ Optional section for additional information, references, or clarifications.
## Further Reading
- [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) by Michael Nygard
- [Architecture Decision Records](https://adr.github.io/) - Collection of ADR resources
- [Architecture Decision Records](https://adr.github.io/) - Collection of ADR resources

View File

@@ -16,7 +16,6 @@ const config: KnipConfig = {
'tests-ui/**/*.{js,ts,vue}',
'*.{js,ts,mts}'
],
ignoreBinaries: ['only-allow', 'openapi-typescript'],
ignoreDependencies: [
'@primeuix/forms',
'@primeuix/styled',
@@ -26,6 +25,7 @@ const config: KnipConfig = {
'tailwindcss',
'tailwindcss-primeui', // Need to figure out why tailwind plugin isn't applying
// Dev
'@executeautomation/playwright-mcp-server',
'@trivago/prettier-plugin-sort-imports'
],
ignore: [
@@ -59,11 +59,7 @@ const config: KnipConfig = {
'src/components/button/TextButton.vue',
'src/components/card/CardTitle.vue',
'src/components/card/CardDescription.vue',
'src/components/input/SingleSelect.vue',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts',
// Generated file: openapi
'src/types/comfyRegistryTypes.ts'
'src/components/input/SingleSelect.vue'
],
ignoreExportsUsedInFile: true,
// Vue-specific configuration
@@ -72,12 +68,15 @@ const config: KnipConfig = {
// Only check for unused files, disable all other rules
// TODO: Gradually enable other rules - see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4888
rules: {
classMembers: 'off'
binaries: 'off',
classMembers: 'off',
duplicates: 'off',
enumMembers: 'off',
exports: 'off',
nsExports: 'off',
nsTypes: 'off',
types: 'off'
},
tags: [
'-knipIgnoreUnusedButUsedByCustomNodes',
'-knipIgnoreUnusedButUsedByVueNodesBranch'
],
// Include dependencies analysis
includeEntryExports: true
}

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.27.1",
"version": "1.27.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -39,6 +39,7 @@
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.6",
"@iconify/tailwind": "^1.2.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@lobehub/i18n-cli": "^1.25.1",

801
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -183,6 +183,7 @@ let cleanupNodeManager: (() => void) | null = null
// Slot layout sync management
let slotSync: ReturnType<typeof useSlotLayoutSync> | null = null
let slotSyncStarted = false
let linkSync: ReturnType<typeof useLinkLayoutSync> | null = null
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
const nodeState = ref<ReadonlyMap<string, NodeState>>(new Map())
@@ -240,12 +241,6 @@ const initializeNodeManager = () => {
const { startSync } = useLayoutSync()
startSync(canvasStore.canvas)
// Initialize slot layout sync for hit detection
slotSync = useSlotLayoutSync()
if (canvasStore.canvas) {
slotSync.start(canvasStore.canvas as LGraphCanvas)
}
// Initialize link layout sync for event-driven updates
linkSync = useLinkLayoutSync()
if (canvasStore.canvas) {
@@ -266,12 +261,6 @@ const disposeNodeManagerAndSyncs = () => {
nodeManager = null
cleanupNodeManager = null
// Clean up slot layout sync
if (slotSync) {
slotSync.stop()
slotSync = null
}
// Clean up link layout sync
if (linkSync) {
linkSync.stop()
@@ -298,6 +287,68 @@ watch(
{ immediate: true }
)
// Ensure slot layout sync starts whenever a canvas is available (LiteGraph mode)
watch(
() => canvasStore.canvas,
(canvas, oldCanvas) => {
if (!canvas) {
// Canvas was removed - stop sync if active
if (slotSync && slotSyncStarted) {
slotSync.stop()
slotSyncStarted = false
}
// Clear any stale slot layouts when canvas is torn down
layoutStore.clearAllSlotLayouts()
return
}
// Canvas changed - restart sync
if (oldCanvas && oldCanvas !== canvas) {
if (slotSync && slotSyncStarted) {
slotSync.stop()
slotSyncStarted = false
}
}
// Start sync if not in Vue mode and not already started
if (!slotSync) slotSync = useSlotLayoutSync()
if (!slotSyncStarted && !isVueNodesEnabled.value) {
const started = slotSync.start(canvas as LGraphCanvas)
slotSyncStarted = started
}
},
{ immediate: true }
)
// On rendering mode change, clear slot layouts and manage slot sync
watch(
() => isVueNodesEnabled.value,
(enabled) => {
// Always clear invalid slot layouts from the prior mode
layoutStore.clearAllSlotLayouts()
if (enabled) {
// Switching TO Vue: Stop slot sync to avoid duplicate registration
if (slotSync && slotSyncStarted) {
slotSync.stop()
slotSyncStarted = false
}
// DOM will re-register via useDomSlotRegistration
} else {
// Switching TO LiteGraph
if (canvasStore.canvas && comfyApp.graph) {
// Ensure slot sync is active
if (!slotSync) slotSync = useSlotLayoutSync()
if (!slotSyncStarted) {
const started = slotSync.start(canvasStore.canvas as LGraphCanvas)
slotSyncStarted = started
}
}
}
},
{ immediate: false }
)
// Transform state for viewport culling
const { syncWithCanvas } = useTransformState()
@@ -726,6 +777,7 @@ onUnmounted(() => {
if (slotSync) {
slotSync.stop()
slotSync = null
slotSyncStarted = false
}
if (linkSync) {
linkSync.stop()

View File

@@ -2,8 +2,7 @@ import { type Ref, computed, ref } from 'vue'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
// @ts-expect-error unused (To be used later?)
interface TemplateFilterOptions {
export interface TemplateFilterOptions {
searchQuery?: string
}

View File

@@ -1,4 +1,5 @@
import {
AutoLaunch,
CrossAttentionMethod,
CudaMalloc,
FloatingPointPrecision,
@@ -19,6 +20,32 @@ export interface ServerConfig<T> extends FormItem {
getValue?: (value: T) => Record<string, ServerConfigValue>
}
export const WEB_ONLY_CONFIG_ITEMS: ServerConfig<any>[] = [
// Launch behavior
{
id: 'auto-launch',
name: 'Automatically opens in the browser on startup',
category: ['Launch'],
type: 'combo',
options: Object.values(AutoLaunch),
defaultValue: AutoLaunch.Auto,
getValue: (value: AutoLaunch) => {
switch (value) {
case AutoLaunch.Auto:
return {}
case AutoLaunch.Enable:
return {
['auto-launch']: true
}
case AutoLaunch.Disable:
return {
['disable-auto-launch']: true
}
}
}
}
]
export const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [
// Network settings
{

View File

@@ -185,3 +185,12 @@ export interface LoaderManagerInterface {
dispose(): void
loadModel(url: string, originalFileName?: string): Promise<void>
}
export interface RecordingManagerInterface extends BaseManager {
startRecording(): Promise<void>
stopRecording(): void
hasRecording(): boolean
getRecordingDuration(): number
exportRecording(filename?: string): void
clearRecording(): void
}

View File

@@ -422,7 +422,6 @@ function getConfig(this: LGraphNode, widgetName: string) {
* @param node The node to convert the widget to an input slot for.
* @param widget The widget to convert to an input slot.
* @returns The input slot that was converted from the widget or undefined if the widget is not found.
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export function convertToInput(
node: LGraphNode,
@@ -593,7 +592,7 @@ app.registerExtension({
const node = LiteGraph.createNode('PrimitiveNode')
if (!node) return r
this.graph?.add(node)
app.graph.add(node)
// Calculate a position that wont directly overlap another node
const pos: [number, number] = [

View File

@@ -1,7 +1,7 @@
import type { Rectangle } from './infrastructure/Rectangle'
import type { CanvasColour, Rect } from './interfaces'
import { LiteGraph } from './litegraph'
import { RenderShape, TitleMode } from './types/globalEnums'
import { LinkDirection, RenderShape, TitleMode } from './types/globalEnums'
const ELLIPSIS = '\u2026'
const TWO_DOT_LEADER = '\u2025'
@@ -22,7 +22,12 @@ export enum SlotShape {
}
/** @see LinkDirection */
export enum SlotDirection {}
export enum SlotDirection {
Up = LinkDirection.UP,
Right = LinkDirection.RIGHT,
Down = LinkDirection.DOWN,
Left = LinkDirection.LEFT
}
export enum LabelPosition {
Left = 'left',

View File

@@ -278,6 +278,9 @@ export type KeysOfType<T, Match> = Exclude<
undefined
>
/** A new type that contains only the properties of T that are of type Match */
export type PickByType<T, Match> = { [P in keyof T]: Extract<T[P], Match> }
/** The names of all (optional) methods and functions in T */
export type MethodNames<T> = KeysOfType<T, ((...args: any) => any) | undefined>

View File

@@ -2,7 +2,7 @@ import type { ContextMenu } from './ContextMenu'
import type { LGraphNode } from './LGraphNode'
import { LiteGraphGlobal } from './LiteGraphGlobal'
import type { ConnectingLink, Point } from './interfaces'
import type { IContextMenuOptions, Size } from './interfaces'
import type { IContextMenuOptions, INodeSlot, Size } from './interfaces'
import { loadPolyfills } from './polyfills'
import type { CanvasEventDetail } from './types/events'
import type { RenderShape, TitleMode } from './types/globalEnums'
@@ -22,6 +22,8 @@ loadPolyfills()
// Definitions by: NateScarlet <https://github.com/NateScarlet>
/** @deprecated Use {@link Point} instead. */
export type Vector2 = Point
/** @deprecated Use {@link Rect} instead. */
export type Vector4 = [number, number, number, number]
export interface IContextMenuItem {
content: string
@@ -44,6 +46,14 @@ export type ContextMenuEventListener = (
node: LGraphNode
) => boolean | void
export interface LinkReleaseContext {
node_to?: LGraphNode
node_from?: LGraphNode
slot_from: INodeSlot
type_filter_in?: string
type_filter_out?: string
}
export interface LinkReleaseContextExtended {
links: ConnectingLink[]
}
@@ -107,6 +117,7 @@ export type {
LinkNetwork,
LinkSegment,
MethodNames,
PickByType,
Point,
Positionable,
ReadonlyLinkNetwork,

View File

@@ -84,6 +84,10 @@ export function isINodeInputSlot(slot: INodeSlot): slot is INodeInputSlot {
return 'link' in slot
}
export function isINodeOutputSlot(slot: INodeSlot): slot is INodeOutputSlot {
return 'links' in slot
}
/**
* Type guard: Whether this input slot is attached to a widget.
* @param slot The slot to check.

View File

@@ -50,6 +50,9 @@ export interface CanvasMouseEvent
Readonly<CanvasPointerExtensions>,
LegacyMouseEvent {}
/** DragEvent with canvasX/Y and deltaX/Y properties */
export interface CanvasDragEvent extends DragEvent, CanvasPointerExtensions {}
export type CanvasEventDetail =
| GenericEventDetail
| GroupDoubleClickEventDetail

View File

@@ -89,6 +89,9 @@ export enum LGraphEventMode {
}
export enum EaseFunction {
LINEAR = 'linear',
EASE_IN_QUAD = 'easeInQuad',
EASE_OUT_QUAD = 'easeOutQuad',
EASE_IN_OUT_QUAD = 'easeInOutQuad'
}

View File

@@ -179,6 +179,14 @@ export interface ISerialisedGroup {
flags?: IGraphGroupFlags
}
export type TClipboardLink = [
targetRelativeIndex: number,
originSlot: number,
nodeRelativeIndex: number,
targetSlot: number,
targetNodeId: NodeId
]
/** Items copied from the canvas */
export interface ClipboardItems {
nodes?: ISerialisedNode[]
@@ -188,6 +196,12 @@ export interface ClipboardItems {
subgraphs?: ExportedSubgraph[]
}
/** @deprecated */
export interface IClipboardContents {
nodes?: ISerialisedNode[]
links?: TClipboardLink[]
}
export interface SerialisableReroute {
id: RerouteId
parentId?: RerouteId

View File

@@ -1,7 +1,8 @@
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LinkId } from '@/lib/litegraph/src/LLink'
import { parseSlotTypes } from '@/lib/litegraph/src/strings'
import type { ISlotType, Positionable } from '../interfaces'
import type { ConnectingLink, ISlotType, Positionable } from '../interfaces'
/**
* Creates a flat set of all positionable items by recursively iterating through all child items.
@@ -44,6 +45,19 @@ export function findFirstNode(
}
}
/** @returns `true` if the provided link ID is currently being dragged. */
export function isDraggingLink(
linkId: LinkId,
connectingLinks: ConnectingLink[] | null | undefined
): ConnectingLink | undefined {
if (connectingLinks == null) return
for (const connectingLink of connectingLinks) {
if (connectingLink.link == null) continue
if (linkId === connectingLink.link.id) return connectingLink
}
}
type FreeSlotResult<T extends { type: ISlotType }> =
| { index: number; slot: T }
| undefined

View File

@@ -1,7 +1,14 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type {
IBaseWidget,
IBooleanWidget,
IButtonWidget,
IComboWidget,
ICustomWidget,
IKnobWidget,
INumericWidget,
ISliderWidget,
IStringWidget,
IWidget,
TWidgetType
} from '@/lib/litegraph/src/types/widgets'
@@ -123,9 +130,49 @@ export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
// #region Type Guards
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IButtonWidget}. */
export function isButtonWidget(widget: IBaseWidget): widget is IButtonWidget {
return widget.type === 'button'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IBooleanWidget}. */
export function isBooleanWidget(widget: IBaseWidget): widget is IBooleanWidget {
return widget.type === 'toggle'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link ISliderWidget}. */
export function isSliderWidget(widget: IBaseWidget): widget is ISliderWidget {
return widget.type === 'slider'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IKnobWidget}. */
export function isKnobWidget(widget: IBaseWidget): widget is IKnobWidget {
return widget.type === 'knob'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IComboWidget}. */
export function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
return widget.type === 'combo'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link INumericWidget}. */
export function isNumberWidget(widget: IBaseWidget): widget is INumericWidget {
return widget.type === 'number'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IStringWidget}. */
export function isStringWidget(widget: IBaseWidget): widget is IStringWidget {
return widget.type === 'string'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link ITextWidget}. */
export function isTextWidget(widget: IBaseWidget): widget is IStringWidget {
return widget.type === 'text'
}
/** Type guard: Narrow **from {@link IBaseWidget}** to {@link ICustomWidget}. */
export function isCustomWidget(widget: IBaseWidget): widget is ICustomWidget {
return widget.type === 'custom'
}
// #endregion Type Guards

View File

@@ -169,3 +169,140 @@ export const subgraphTest = test.extend<SubgraphFixtures>({
capture.cleanup()
}
})
/**
* Fixtures that test edge cases and error conditions.
* These may leave the system in an invalid state and should be used carefully.
*/
export interface EdgeCaseFixtures {
/** Subgraph with circular references (for testing recursion detection) */
circularSubgraph: {
rootGraph: LGraph
subgraphA: Subgraph
subgraphB: Subgraph
nodeA: SubgraphNode
nodeB: SubgraphNode
}
/** Deeply nested subgraphs approaching the theoretical limit */
deeplyNestedSubgraph: ReturnType<typeof createNestedSubgraphs>
/** Subgraph with maximum inputs and outputs */
maxIOSubgraph: Subgraph
}
/**
* Test with edge case fixtures. Use sparingly and with caution.
* These tests may intentionally create invalid states.
*/
export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
circularSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
const rootGraph = new LGraph()
// Create two subgraphs that will reference each other
const subgraphA = createTestSubgraph({
name: 'Subgraph A',
inputs: [{ name: 'input', type: '*' }],
outputs: [{ name: 'output', type: '*' }]
})
const subgraphB = createTestSubgraph({
name: 'Subgraph B',
inputs: [{ name: 'input', type: '*' }],
outputs: [{ name: 'output', type: '*' }]
})
// Create instances (this doesn't create circular refs by itself)
const nodeA = createTestSubgraphNode(subgraphA, { pos: [100, 100] })
const nodeB = createTestSubgraphNode(subgraphB, { pos: [300, 100] })
// Add nodes to root graph
rootGraph.add(nodeA)
rootGraph.add(nodeB)
await use({
rootGraph,
subgraphA,
subgraphB,
nodeA,
nodeB
})
},
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
deeplyNestedSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
// Create a very deep nesting structure (but not exceeding MAX_NESTED_SUBGRAPHS)
const nested = createNestedSubgraphs({
depth: 50, // Deep but reasonable
nodesPerLevel: 1,
inputsPerSubgraph: 1,
outputsPerSubgraph: 1
})
await use(nested)
},
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
maxIOSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
// Create a subgraph with many inputs and outputs
const inputs = Array.from({ length: 20 }, (_, i) => ({
name: `input_${i}`,
type: i % 2 === 0 ? 'number' : ('string' as const)
}))
const outputs = Array.from({ length: 20 }, (_, i) => ({
name: `output_${i}`,
type: i % 2 === 0 ? 'number' : ('string' as const)
}))
const subgraph = createTestSubgraph({
name: 'Max IO Subgraph',
inputs,
outputs,
nodeCount: 10
})
await use(subgraph)
}
})
/**
* Helper to verify fixture integrity.
* Use this in tests to ensure fixtures are properly set up.
*/
export function verifyFixtureIntegrity<T extends Record<string, unknown>>(
fixture: T,
expectedProperties: (keyof T)[]
): void {
for (const prop of expectedProperties) {
if (!(prop in fixture)) {
throw new Error(`Fixture missing required property: ${String(prop)}`)
}
if (fixture[prop] === undefined || fixture[prop] === null) {
throw new Error(`Fixture property ${String(prop)} is null or undefined`)
}
}
}
/**
* Creates a snapshot-friendly representation of a subgraph for testing.
* Useful for serialization tests and regression detection.
*/
export function createSubgraphSnapshot(subgraph: Subgraph) {
return {
id: subgraph.id,
name: subgraph.name,
inputCount: subgraph.inputs.length,
outputCount: subgraph.outputs.length,
nodeCount: subgraph.nodes.length,
linkCount: subgraph.links.size,
inputs: subgraph.inputs.map((i) => ({ name: i.name, type: i.type })),
outputs: subgraph.outputs.map((o) => ({ name: o.name, type: o.type })),
hasInputNode: !!subgraph.inputNode,
hasOutputNode: !!subgraph.outputNode
}
}

View File

@@ -382,6 +382,76 @@ export function createTestSubgraphData(
}
}
/**
* Creates a complex subgraph with multiple nodes and connections.
* Useful for testing realistic scenarios.
* @param nodeCount Number of internal nodes to create
* @returns Complex subgraph data structure
*/
export function createComplexSubgraphData(
nodeCount: number = 5
): ExportedSubgraph {
const nodes = []
const links: Record<
string,
{
id: number
origin_id: number
origin_slot: number
target_id: number
target_slot: number
type: string
}
> = {}
// Create internal nodes
for (let i = 0; i < nodeCount; i++) {
nodes.push({
id: i + 1, // Start from 1 to avoid conflicts with IO nodes
type: 'basic/test',
pos: [100 + i * 150, 200],
size: [120, 60],
inputs: [{ name: 'in', type: '*', link: null }],
outputs: [{ name: 'out', type: '*', links: [] }],
properties: { value: i },
flags: {},
mode: 0
})
}
// Create some internal links
for (let i = 0; i < nodeCount - 1; i++) {
const linkId = i + 1
links[linkId] = {
id: linkId,
origin_id: i + 1,
origin_slot: 0,
target_id: i + 2,
target_slot: 0,
type: '*'
}
}
return createTestSubgraphData({
// @ts-expect-error TODO: Fix after merge - nodes parameter type
nodes,
// @ts-expect-error TODO: Fix after merge - links parameter type
links,
inputs: [
// @ts-expect-error TODO: Fix after merge - input object type
{ name: 'input1', type: 'number', pos: [0, 0] },
// @ts-expect-error TODO: Fix after merge - input object type
{ name: 'input2', type: 'string', pos: [0, 1] }
],
outputs: [
// @ts-expect-error TODO: Fix after merge - output object type
{ name: 'output1', type: 'number', pos: [0, 0] },
// @ts-expect-error TODO: Fix after merge - output object type
{ name: 'output2', type: 'string', pos: [0, 1] }
]
})
}
/**
* Creates an event capture system for testing event sequences.
* @param eventTarget The event target to monitor
@@ -423,5 +493,39 @@ export function createEventCapture<T = unknown>(
}
}
/**
* Utility to log subgraph structure for debugging tests.
* @param subgraph The subgraph to inspect
* @param label Optional label for the log output
*/
export function logSubgraphStructure(
subgraph: Subgraph,
label: string = 'Subgraph'
): void {
console.log(`\n=== ${label} Structure ===`)
console.log(`Name: ${subgraph.name}`)
console.log(`ID: ${subgraph.id}`)
console.log(`Inputs: ${subgraph.inputs.length}`)
console.log(`Outputs: ${subgraph.outputs.length}`)
console.log(`Nodes: ${subgraph.nodes.length}`)
console.log(`Links: ${subgraph.links.size}`)
if (subgraph.inputs.length > 0) {
console.log(
'Input details:',
subgraph.inputs.map((i) => ({ name: i.name, type: i.type }))
)
}
if (subgraph.outputs.length > 0) {
console.log(
'Output details:',
subgraph.outputs.map((o) => ({ name: o.name, type: o.type }))
)
}
console.log('========================\n')
}
// Re-export expect from vitest for convenience
export { expect } from 'vitest'

View File

@@ -371,7 +371,19 @@ class LayoutStoreImpl implements LayoutStore {
updateSlotLayout(key: string, layout: SlotLayout): void {
const existing = this.slotLayouts.get(key)
if (!existing) {
if (existing) {
// Short-circuit if bounds and position unchanged (prevents spatial index churn)
if (
existing.bounds.x === layout.bounds.x &&
existing.bounds.y === layout.bounds.y &&
existing.bounds.width === layout.bounds.width &&
existing.bounds.height === layout.bounds.height &&
existing.position.x === layout.position.x &&
existing.position.y === layout.position.y
) {
return
}
} else {
logger.debug('Adding slot:', {
nodeId: layout.nodeId,
type: layout.type,
@@ -419,6 +431,15 @@ class LayoutStoreImpl implements LayoutStore {
}
}
/**
* Clear all slot layouts and their spatial index (O(1) operations)
* Used when switching rendering modes (Vue ↔ LiteGraph)
*/
clearAllSlotLayouts(): void {
this.slotLayouts.clear()
this.slotSpatialIndex.clear()
}
/**
* Update reroute layout data
*/

View File

@@ -16,7 +16,7 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
* Compute and register slot layouts for a node
* @param node LiteGraph node to process
*/
function computeAndRegisterSlots(node: LGraphNode): void {
export function computeAndRegisterSlots(node: LGraphNode): void {
const nodeId = String(node.id)
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
@@ -59,18 +59,19 @@ export function useSlotLayoutSync() {
/**
* Start slot layout sync with full event-driven functionality
* @param canvas LiteGraph canvas instance
* @returns true if sync was actually started, false if early-returned
*/
function start(canvas: LGraphCanvas): void {
function start(canvas: LGraphCanvas): boolean {
// When Vue nodes are enabled, slot DOM registers exact positions.
// Skip calculated registration to avoid conflicts.
if (LiteGraph.vueNodesMode) {
return
return false
}
const graph = canvas?.graph
if (!graph) return
if (!graph) return false
// Initial registration for all nodes in the current graph
for (const node of graph.nodes) {
for (const node of graph._nodes) {
computeAndRegisterSlots(node)
}
@@ -135,6 +136,8 @@ export function useSlotLayoutSync() {
graph.onTrigger = origTrigger || undefined
graph.onAfterChange = origAfterChange || undefined
}
return true
}
/**

View File

@@ -297,6 +297,7 @@ export interface LayoutStore {
deleteSlotLayout(key: string): void
deleteNodeSlotLayouts(nodeId: NodeId): void
deleteRerouteLayout(rerouteId: RerouteId): void
clearAllSlotLayouts(): void
// Get layout data
getLinkLayout(linkId: LinkId): LinkLayout | null

View File

@@ -1,4 +1,5 @@
import { z } from 'zod'
import { fromZodError } from 'zod-validation-error'
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
@@ -279,6 +280,18 @@ export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
export type TaskItem = z.infer<typeof zTaskItem>
export function validateTaskItem(taskItem: unknown) {
const result = zTaskItem.safeParse(taskItem)
if (!result.success) {
const zodError = fromZodError(result.error)
// TODO accept a callback to report error.
console.warn(
`Invalid TaskItem: ${JSON.stringify(taskItem)}\n${zodError.message}`
)
}
return result
}
const zEmbeddingsResponse = z.array(z.string())
const zExtensionsResponse = z.array(z.string())
const zError = z.object({

View File

@@ -447,11 +447,14 @@ export const zSubgraphDefinition = zComfyWorkflow1
.passthrough()
export type ModelFile = z.infer<typeof zModelFile>
export type NodeInput = z.infer<typeof zNodeInput>
export type NodeOutput = z.infer<typeof zNodeOutput>
export type ComfyLink = z.infer<typeof zComfyLink>
export type ComfyLinkObject = z.infer<typeof zComfyLinkObject>
export type ComfyNode = z.infer<typeof zComfyNode>
export type Reroute = z.infer<typeof zReroute>
export type WorkflowJSON04 = z.infer<typeof zComfyWorkflow>
export type WorkflowJSON10 = z.infer<typeof zComfyWorkflow1>
export type ComfyWorkflowJSON = z.infer<
typeof zComfyWorkflow | typeof zComfyWorkflow1
>

View File

@@ -214,7 +214,9 @@ export type StringInputSpec = z.infer<typeof zStringInputSpec>
export type ComboInputSpec = z.infer<typeof zComboInputSpec>
export type ColorInputSpec = z.infer<typeof zColorInputSpec>
export type FileUploadInputSpec = z.infer<typeof zFileUploadInputSpec>
export type ImageInputSpec = z.infer<typeof zImageInputSpec>
export type ImageCompareInputSpec = z.infer<typeof zImageCompareInputSpec>
export type MarkdownInputSpec = z.infer<typeof zMarkdownInputSpec>
export type TreeSelectInputSpec = z.infer<typeof zTreeSelectInputSpec>
export type MultiSelectInputSpec = z.infer<typeof zMultiSelectInputSpec>
export type ChartInputSpec = z.infer<typeof zChartInputSpec>

View File

@@ -128,12 +128,30 @@ export function isFloatInputSpec(
return inputSpec[0] === 'FLOAT'
}
export function isBooleanInputSpec(
inputSpec: InputSpec
): inputSpec is BooleanInputSpec {
return inputSpec[0] === 'BOOLEAN'
}
export function isStringInputSpec(
inputSpec: InputSpec
): inputSpec is StringInputSpec {
return inputSpec[0] === 'STRING'
}
export function isComboInputSpecV2(
inputSpec: InputSpec
): inputSpec is ComboInputSpecV2 {
return inputSpec[0] === 'COMBO'
}
export function isCustomInputSpec(
inputSpec: InputSpec
): inputSpec is CustomInputSpec {
return typeof inputSpec[0] === 'string' && !excludedLiterals.has(inputSpec[0])
}
export function isComboInputSpec(
inputSpec: InputSpec
): inputSpec is ComboInputSpec | ComboInputSpecV2 {
@@ -229,13 +247,22 @@ export type ComfyOutputTypesSpec = z.infer<typeof zComfyOutputTypesSpec>
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>
export type RemoteWidgetConfig = z.infer<typeof zRemoteWidgetConfig>
// Input specs
export type IntInputOptions = z.infer<typeof zIntInputOptions>
export type FloatInputOptions = z.infer<typeof zFloatInputOptions>
export type BooleanInputOptions = z.infer<typeof zBooleanInputOptions>
export type StringInputOptions = z.infer<typeof zStringInputOptions>
export type ComboInputOptions = z.infer<typeof zComboInputOptions>
export type BaseInputOptions = z.infer<typeof zBaseInputOptions>
export type NumericInputOptions = z.infer<typeof zNumericInputOptions>
export type IntInputSpec = z.infer<typeof zIntInputSpec>
export type FloatInputSpec = z.infer<typeof zFloatInputSpec>
export type BooleanInputSpec = z.infer<typeof zBooleanInputSpec>
export type StringInputSpec = z.infer<typeof zStringInputSpec>
export type ComboInputSpec = z.infer<typeof zComboInputSpec>
export type ComboInputSpecV2 = z.infer<typeof zComboInputSpecV2>
export type CustomInputSpec = z.infer<typeof zCustomInputSpec>
export type InputSpec = z.infer<typeof zInputSpec>
export function validateComfyNodeDef(

View File

@@ -10,6 +10,8 @@ export const apiKeySchema = z.object({
.length(72, t('validation.length', { length: 72 }))
})
export type ApiKeyData = z.infer<typeof apiKeySchema>
export const signInSchema = z.object({
email: z
.string()
@@ -40,6 +42,8 @@ export const updatePasswordSchema = passwordSchema.refine(
}
)
export type UpdatePasswordData = z.infer<typeof updatePasswordSchema>
export const signUpSchema = passwordSchema
.extend({
email: z

View File

@@ -375,3 +375,17 @@ LGraphNode.prototype.addDOMWidget = function <
return widget
}
/**
* Prunes widgets that are no longer in the graph.
* @param nodes The nodes to prune widgets for.
*/
export const pruneWidgets = (nodes: LGraphNode[]) => {
const nodeSet = new Set(nodes)
const domWidgetStore = useDomWidgetStore()
for (const { widget } of domWidgetStore.widgetStates.values()) {
if (!nodeSet.has(widget.node)) {
domWidgetStore.unregisterWidget(widget.id)
}
}
}

View File

@@ -17,7 +17,6 @@ export function clone<T>(obj: T): T {
}
/**
* @knipIgnoreUnusedButUsedByCustomNodes
* @deprecated Use `applyTextReplacements` from `@/utils/searchAndReplace` instead
* There are external callers to this function, so we need to keep it for now
*/
@@ -25,7 +24,6 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
return _applyTextReplacements(app.graph, value)
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export async function addStylesheet(
urlOrFile: string,
relativeTo?: string

View File

@@ -1,4 +1,7 @@
import type { BaseSearchParamsWithoutQuery } from 'algoliasearch/dist/lite/browser'
import type {
BaseSearchParamsWithoutQuery,
Hit
} from 'algoliasearch/dist/lite/browser'
import type { components } from '@/types/comfyRegistryTypes'
@@ -10,6 +13,15 @@ type SafeNestedProperty<
type RegistryNodePack = components['schemas']['Node']
/**
* Result of searching the Algolia index.
* Represents the entire result of a search query.
*/
export type SearchPacksResult = {
nodePacks: Hit<AlgoliaNodePack>[]
querySuggestions: Hit<NodesIndexSuggestion>[]
}
/**
* Node pack record after it has been mapped to Algolia index format.
* @see https://github.com/Comfy-Org/comfy-api/blob/main/mapper/algolia.go

View File

@@ -2,6 +2,7 @@ import type { InjectionKey, Ref } from 'vue'
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { components as managerComponents } from '@/types/generatedManagerTypes'
export type RegistryPack = components['schemas']['Node']
export type MergedNodePack = RegistryPack & AlgoliaNodePack
@@ -9,9 +10,16 @@ export const isMergedNodePack = (
nodePack: RegistryPack | AlgoliaNodePack
): nodePack is MergedNodePack => 'comfy_nodes' in nodePack
export type PackField = keyof RegistryPack | null
export const IsInstallingKey: InjectionKey<Ref<boolean>> =
Symbol('isInstalling')
export enum ManagerWsQueueStatus {
DONE = 'all-done',
IN_PROGRESS = 'in_progress'
}
export enum ManagerTab {
All = 'all',
Installed = 'installed',
@@ -26,12 +34,31 @@ export interface TabItem {
icon: string
}
export enum ManagerSortField {
Author = 'author',
CreateDate = 'creation_date',
LastUpdateDate = 'last_update',
Name = 'name',
Stars = 'stars',
Size = 'size'
}
export enum PackEnableState {
Enabled,
Disabled,
NotInstalled
}
export type TaskLog = {
taskName: string
taskId: string
logs: string[]
}
export interface ManagerQueueOptions {
maxConcurrent?: number
}
export interface UseNodePacksOptions {
immediate?: boolean
maxConcurrent?: number
@@ -56,3 +83,13 @@ export interface ManagerState {
searchMode: 'nodes' | 'packs'
sortField: string
}
/**
* Types for import failure information API
*/
export type ImportFailInfoBulkRequest =
managerComponents['schemas']['ImportFailInfoBulkRequest']
export type ImportFailInfoBulkResponse =
managerComponents['schemas']['ImportFailInfoBulkResponse']
export type ImportFailInfoItem =
managerComponents['schemas']['ImportFailInfoItem']

View File

@@ -9,6 +9,9 @@ import type { components } from './comfyRegistryTypes'
// Re-export core types from Registry API
export type Node = components['schemas']['Node']
export type NodeVersion = components['schemas']['NodeVersion']
export type NodeStatus = components['schemas']['NodeStatus']
export type NodeVersionStatus = components['schemas']['NodeVersionStatus']
/**
* Conflict types that can be detected in the system
@@ -24,6 +27,22 @@ export type ConflictType =
| 'banned' // Banned package
| 'pending' // Security verification pending
/**
* Version comparison operators
* @enum {string}
*/
export type VersionOperator = '>=' | '>' | '<=' | '<' | '==' | '!='
/**
* Version requirement specification
*/
export interface VersionRequirement {
/** @description Comparison operator for version checking */
operator: VersionOperator
/** @description Target version string */
version: string
}
/**
* Node Pack requirements from Registry API
* Extends Node type with additional installation and compatibility metadata

View File

@@ -1,3 +1,9 @@
export enum LinkReleaseTriggerMode {
ALWAYS = 'always',
HOLD_SHIFT = 'hold shift',
NOT_HOLD_SHIFT = 'NOT hold shift'
}
export enum LinkReleaseTriggerAction {
CONTEXT_MENU = 'context menu',
SEARCH_BOX = 'search box',

View File

@@ -20,6 +20,15 @@ export enum HashFunction {
SHA512 = 'sha512'
}
export enum AutoLaunch {
// Let server decide whether to auto launch based on the current environment
Auto = 'auto',
// Disable auto launch
Disable = 'disable',
// Enable auto launch
Enable = 'enable'
}
export enum CudaMalloc {
// Let server decide whether to use CUDA malloc based on the current environment
Auto = 'auto',

View File

@@ -25,6 +25,13 @@ export interface SettingOption {
value?: any
}
export interface Setting {
id: keyof Settings
onChange?: (value: any, oldValue?: any) => void
name: string
render: () => HTMLElement
}
export interface SettingParams<TValue = unknown> extends FormItem {
id: keyof Settings
defaultValue: any | (() => any)

View File

@@ -33,6 +33,10 @@ export function appendJsonExt(path: string) {
return path
}
export function trimJsonExt(path?: string) {
return path?.replace(/\.json$/, '')
}
export function highlightQuery(text: string, query: string) {
if (!query) return text
@@ -76,6 +80,28 @@ export function formatSize(value?: number) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
/**
* Finds the common directory prefix between two paths
* @example
* findCommonPrefix('a/b/c', 'a/b/d') // returns 'a/b'
* findCommonPrefix('x/y/z', 'a/b/c') // returns ''
* findCommonPrefix('a/b/c', 'a/b/c/d') // returns 'a/b/c'
*/
export function findCommonPrefix(path1: string, path2: string): string {
const parts1 = path1.split('/')
const parts2 = path2.split('/')
const commonParts: string[] = []
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
if (parts1[i] === parts2[i]) {
commonParts.push(parts1[i])
} else {
break
}
}
return commonParts.join('/')
}
/**
* Returns various filename components.
* Example:
@@ -397,6 +423,26 @@ export function compareVersions(
return 0
}
/**
* Converts a currency amount to Metronome's integer representation.
* For USD, converts to cents (multiplied by 100).
* For all other currencies (including custom pricing units), returns the amount as is.
* This is specific to Metronome's API requirements.
*
* @param amount - The amount in currency to convert
* @param currency - The currency to convert
* @returns The amount in Metronome's integer format (cents for USD, base units for others)
* @example
* toMetronomeCurrency(1.23, 'usd') // returns 123 (cents)
* toMetronomeCurrency(1000, 'jpy') // returns 1000 (yen)
*/
export function toMetronomeCurrency(amount: number, currency: string): number {
if (currency === 'usd') {
return Math.round(amount * 100)
}
return amount
}
/**
* Converts Metronome's integer amount back to a formatted currency string.
* For USD, converts from cents to dollars.

View File

@@ -15,6 +15,7 @@ import {
isFloatInputSpec,
isIntInputSpec
} from '@/schemas/nodeDefSchema'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { lcm } from './mathUtil'
@@ -138,3 +139,11 @@ export const mergeInputSpec = (
return mergeCommonInputSpec(spec1, spec2)
}
/**
* Checks if a node definition represents a subgraph node.
* Subgraph nodes are created with category='subgraph' and python_module='nodes'.
*/
export const isSubgraphNode = (nodeDef: ComfyNodeDefImpl): boolean => {
return nodeDef.category === 'subgraph' && nodeDef.python_module === 'nodes'
}

View File

@@ -29,6 +29,34 @@ export function satisfiesVersion(version: string, range: string): boolean {
}
}
/**
* Compares two versions and returns the difference type
* @param version1 First version
* @param version2 Second version
* @returns Difference type or null if comparison fails
*/
export function getVersionDifference(
version1: string,
version2: string
): semver.ReleaseType | null {
try {
const clean1 = cleanVersion(version1)
const clean2 = cleanVersion(version2)
return semver.diff(clean1, clean2)
} catch {
return null
}
}
/**
* Checks if a version is valid according to semver
* @param version Version string to validate
* @returns true if version is valid
*/
export function isValidVersion(version: string): boolean {
return semver.valid(version) !== null
}
/**
* Checks version compatibility and returns conflict details.
* Supports all semver ranges including >=, <=, >, <, ~, ^ operators.

View File

@@ -113,7 +113,16 @@ LiteGraphGlobal {
"Reroute": [Function],
"SPLINE_LINK": 2,
"STRAIGHT_LINK": 0,
"SlotDirection": {},
"SlotDirection": {
"1": "Up",
"2": "Down",
"3": "Left",
"4": "Right",
"Down": 2,
"Left": 3,
"Right": 4,
"Up": 1,
},
"SlotShape": {
"1": "Box",
"3": "Circle",

View File

@@ -169,3 +169,140 @@ export const subgraphTest = test.extend<SubgraphFixtures>({
capture.cleanup()
}
})
/**
* Fixtures that test edge cases and error conditions.
* These may leave the system in an invalid state and should be used carefully.
*/
export interface EdgeCaseFixtures {
/** Subgraph with circular references (for testing recursion detection) */
circularSubgraph: {
rootGraph: LGraph
subgraphA: Subgraph
subgraphB: Subgraph
nodeA: SubgraphNode
nodeB: SubgraphNode
}
/** Deeply nested subgraphs approaching the theoretical limit */
deeplyNestedSubgraph: ReturnType<typeof createNestedSubgraphs>
/** Subgraph with maximum inputs and outputs */
maxIOSubgraph: Subgraph
}
/**
* Test with edge case fixtures. Use sparingly and with caution.
* These tests may intentionally create invalid states.
*/
export const edgeCaseTest = subgraphTest.extend<EdgeCaseFixtures>({
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
circularSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
const rootGraph = new LGraph()
// Create two subgraphs that will reference each other
const subgraphA = createTestSubgraph({
name: 'Subgraph A',
inputs: [{ name: 'input', type: '*' }],
outputs: [{ name: 'output', type: '*' }]
})
const subgraphB = createTestSubgraph({
name: 'Subgraph B',
inputs: [{ name: 'input', type: '*' }],
outputs: [{ name: 'output', type: '*' }]
})
// Create instances (this doesn't create circular refs by itself)
const nodeA = createTestSubgraphNode(subgraphA, { pos: [100, 100] })
const nodeB = createTestSubgraphNode(subgraphB, { pos: [300, 100] })
// Add nodes to root graph
rootGraph.add(nodeA)
rootGraph.add(nodeB)
await use({
rootGraph,
subgraphA,
subgraphB,
nodeA,
nodeB
})
},
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
deeplyNestedSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
// Create a very deep nesting structure (but not exceeding MAX_NESTED_SUBGRAPHS)
const nested = createNestedSubgraphs({
depth: 50, // Deep but reasonable
nodesPerLevel: 1,
inputsPerSubgraph: 1,
outputsPerSubgraph: 1
})
await use(nested)
},
// @ts-expect-error TODO: Fix after merge - fixture use parameter type
// eslint-disable-next-line no-empty-pattern
maxIOSubgraph: async ({}, use: (value: unknown) => Promise<void>) => {
// Create a subgraph with many inputs and outputs
const inputs = Array.from({ length: 20 }, (_, i) => ({
name: `input_${i}`,
type: i % 2 === 0 ? 'number' : ('string' as const)
}))
const outputs = Array.from({ length: 20 }, (_, i) => ({
name: `output_${i}`,
type: i % 2 === 0 ? 'number' : ('string' as const)
}))
const subgraph = createTestSubgraph({
name: 'Max IO Subgraph',
inputs,
outputs,
nodeCount: 10
})
await use(subgraph)
}
})
/**
* Helper to verify fixture integrity.
* Use this in tests to ensure fixtures are properly set up.
*/
export function verifyFixtureIntegrity<T extends Record<string, unknown>>(
fixture: T,
expectedProperties: (keyof T)[]
): void {
for (const prop of expectedProperties) {
if (!(prop in fixture)) {
throw new Error(`Fixture missing required property: ${String(prop)}`)
}
if (fixture[prop] === undefined || fixture[prop] === null) {
throw new Error(`Fixture property ${String(prop)} is null or undefined`)
}
}
}
/**
* Creates a snapshot-friendly representation of a subgraph for testing.
* Useful for serialization tests and regression detection.
*/
export function createSubgraphSnapshot(subgraph: Subgraph) {
return {
id: subgraph.id,
name: subgraph.name,
inputCount: subgraph.inputs.length,
outputCount: subgraph.outputs.length,
nodeCount: subgraph.nodes.length,
linkCount: subgraph.links.size,
inputs: subgraph.inputs.map((i) => ({ name: i.name, type: i.type })),
outputs: subgraph.outputs.map((o) => ({ name: o.name, type: o.type })),
hasInputNode: !!subgraph.inputNode,
hasOutputNode: !!subgraph.outputNode
}
}

View File

@@ -382,6 +382,76 @@ export function createTestSubgraphData(
}
}
/**
* Creates a complex subgraph with multiple nodes and connections.
* Useful for testing realistic scenarios.
* @param nodeCount Number of internal nodes to create
* @returns Complex subgraph data structure
*/
export function createComplexSubgraphData(
nodeCount: number = 5
): ExportedSubgraph {
const nodes = []
const links: Record<
string,
{
id: number
origin_id: number
origin_slot: number
target_id: number
target_slot: number
type: string
}
> = {}
// Create internal nodes
for (let i = 0; i < nodeCount; i++) {
nodes.push({
id: i + 1, // Start from 1 to avoid conflicts with IO nodes
type: 'basic/test',
pos: [100 + i * 150, 200],
size: [120, 60],
inputs: [{ name: 'in', type: '*', link: null }],
outputs: [{ name: 'out', type: '*', links: [] }],
properties: { value: i },
flags: {},
mode: 0
})
}
// Create some internal links
for (let i = 0; i < nodeCount - 1; i++) {
const linkId = i + 1
links[linkId] = {
id: linkId,
origin_id: i + 1,
origin_slot: 0,
target_id: i + 2,
target_slot: 0,
type: '*'
}
}
return createTestSubgraphData({
// @ts-expect-error TODO: Fix after merge - nodes parameter type
nodes,
// @ts-expect-error TODO: Fix after merge - links parameter type
links,
inputs: [
// @ts-expect-error TODO: Fix after merge - input object type
{ name: 'input1', type: 'number', pos: [0, 0] },
// @ts-expect-error TODO: Fix after merge - input object type
{ name: 'input2', type: 'string', pos: [0, 1] }
],
outputs: [
// @ts-expect-error TODO: Fix after merge - output object type
{ name: 'output1', type: 'number', pos: [0, 0] },
// @ts-expect-error TODO: Fix after merge - output object type
{ name: 'output2', type: 'string', pos: [0, 1] }
]
})
}
/**
* Creates an event capture system for testing event sequences.
* @param eventTarget The event target to monitor
@@ -423,5 +493,39 @@ export function createEventCapture<T = unknown>(
}
}
/**
* Utility to log subgraph structure for debugging tests.
* @param subgraph The subgraph to inspect
* @param label Optional label for the log output
*/
export function logSubgraphStructure(
subgraph: Subgraph,
label: string = 'Subgraph'
): void {
console.log(`\n=== ${label} Structure ===`)
console.log(`Name: ${subgraph.name}`)
console.log(`ID: ${subgraph.id}`)
console.log(`Inputs: ${subgraph.inputs.length}`)
console.log(`Outputs: ${subgraph.outputs.length}`)
console.log(`Nodes: ${subgraph.nodes.length}`)
console.log(`Links: ${subgraph.links.size}`)
if (subgraph.inputs.length > 0) {
console.log(
'Input details:',
subgraph.inputs.map((i) => ({ name: i.name, type: i.type }))
)
}
if (subgraph.outputs.length > 0) {
console.log(
'Output details:',
subgraph.outputs.map((o) => ({ name: o.name, type: o.type }))
)
}
console.log('========================\n')
}
// Re-export expect from vitest for convenience
export { expect } from 'vitest'