From 006e6bd57c060bbb510059e89ca92376f6e6ff9f Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 4 Sep 2025 21:31:59 -0700 Subject: [PATCH] [feat] Vue-Based Rendering System for the ComfyUI Node Graph (#4263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 * [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 * [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 * [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 * [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 * [feat] Add Vue action widgets - WidgetButton: Action button with Button component and callback handling - WidgetFileUpload: File upload interface with FileUpload component * [feat] TransformPane - Viewport synchronization layer for Vue nodes (#4304) Co-authored-by: Claude Co-authored-by: Benjamin Lu Co-authored-by: github-actions * Update locales [skip ci] * Fix TransformPane pos/size (#4826) * Update locales [skip ci] * 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. * 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 d7ed1d36ed9ec55b4d6e2b94020ff4d50ed35910. * test(ci): skip transformPerformance suite on CI (#4843) * 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 * 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 * 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 * 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 * Add node slots to layout tree * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * Remove slots from layoutTypes * Totally not scuffed renderer and adapter * Revert "Totally not scuffed renderer and adapter" This reverts commit 2b9d83efb81d9b9800f868e8804601ce7983c0d8. * Revert "Remove slots from layoutTypes" This reverts commit 18f78ff786411f640bb22dd52d917501fad53b04. * Reapply "Add node slots to layout tree" This reverts commit 236fecb549c9ffcb642412d8a70df3d37629260d. * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 Co-authored-by: Benjamin Lu * [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 * 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 * 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 * 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 * Add node slots to layout tree * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * Remove slots from layoutTypes * Totally not scuffed renderer and adapter * Revert "Totally not scuffed renderer and adapter" This reverts commit 2b9d83efb81d9b9800f868e8804601ce7983c0d8. * Revert "Remove slots from layoutTypes" This reverts commit 18f78ff786411f640bb22dd52d917501fad53b04. * Reapply "Add node slots to layout tree" This reverts commit 236fecb549c9ffcb642412d8a70df3d37629260d. * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 * [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 Co-authored-by: Claude * 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 * [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. * 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 --------- Co-authored-by: Claude * [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 * 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 * remove logging from vue node layouting modules (#5111) * 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 * Revert "feat: Add slot registration and spatial indexing for hit detection" This reverts commit 70fbfd0f5e1659305070761d6a4a0c0f597651df. * [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 * [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 * 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 * 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: 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) * Fix lodash import (#5269) * Decouple link and slot hit-testing out of Litegraph (#5134) * [feat] TransformPane - Viewport synchronization layer for Vue nodes (#4304) Co-authored-by: Claude Co-authored-by: Benjamin Lu Co-authored-by: github-actions * 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 * 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 * 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 * 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 * Add node slots to layout tree * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * Remove slots from layoutTypes * Totally not scuffed renderer and adapter * Revert "Totally not scuffed renderer and adapter" This reverts commit 2b9d83efb81d9b9800f868e8804601ce7983c0d8. * Revert "Remove slots from layoutTypes" This reverts commit 18f78ff786411f640bb22dd52d917501fad53b04. * Reapply "Add node slots to layout tree" This reverts commit 236fecb549c9ffcb642412d8a70df3d37629260d. * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 Co-authored-by: Benjamin Lu * [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 * 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 * 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 * 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 * Add node slots to layout tree * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * Remove slots from layoutTypes * Totally not scuffed renderer and adapter * Revert "Totally not scuffed renderer and adapter" This reverts commit 2b9d83efb81d9b9800f868e8804601ce7983c0d8. * Revert "Remove slots from layoutTypes" This reverts commit 18f78ff786411f640bb22dd52d917501fad53b04. * Reapply "Add node slots to layout tree" This reverts commit 236fecb549c9ffcb642412d8a70df3d37629260d. * Revert "Add node slots to layout tree" This reverts commit 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 460493a6203d0d7c15422fb4fd2f021eb6809e37. * 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 * [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 Co-authored-by: Claude * 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 * Revert "feat: Add slot registration and spatial indexing for hit detection" This reverts commit 70fbfd0f5e1659305070761d6a4a0c0f597651df. * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 Co-authored-by: Claude Co-authored-by: github-actions * chore: Empty commit to trigger CI checks * [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. * feat: localization fields (#5318) * fix: remove clipping by removing unnecessary css contain (#5327) * [bugfix] Remove placeholder IMAGE widget to restore previous functionality (#5349) * Remove IMAGE widget * Remove IMAGE widget test expectations * - 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 * 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 * 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 * add explicit typing on component IDs (#5352) * Remove IMAGE widget cont. (#5355) * 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 * [fix] Disable link markers on dragged connections (#5358) Set linkMarkerShape to None for links being actively dragged by the mouse to prevent visual artifacts. * [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 * [Vue Nodes] Fix Node Header Tests (#5360) * Enable VueNodes * Use KSampler not save image * Update test expectations [skip ci] * remove crdt ADR (moved to separate PR) * update adr README * removed unused IMAGE widget enum value * remove all unused (knip pass) * remove debug overlay panel * simplify unit tests * change name "transformPaneEnabled" => "isVueNodesEnabled" * remove debug viewport visualizer * remove debug viewport visualizer prop * remove outdated README * skip all vue node operations if feature is turned off * remove debug logging and setting * remove event forwarding hack. todo: add link moving in vue * cleanup comments * cleanup comments * add missing translations * use camelCase for all non-component files * remove debug viewport test * - 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 * remove redundant comment * use camelcase for layoutStore filename * removed unused type guards * simplify widget registration * move back test that was mistakenly moved * remove unused typeguards * removed unused node def type guards --------- Co-authored-by: Claude Co-authored-by: Benjamin Lu Co-authored-by: github-actions Co-authored-by: Benjamin Lu Co-authored-by: Rizumu Ayaka Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com> --- CLAUDE.md | 3 + .../fixtures/utils/vueNodeFixtures.ts | 131 ++ .../template-grid-mobile-chromium-linux.png | Bin 90329 -> 89916 bytes .../template-grid-tablet-chromium-linux.png | Bin 181975 -> 181268 bytes .../tests/vueNodes/NodeHeader.spec.ts | 134 ++ package.json | 4 + pnpm-lock.yaml | 58 + src/assets/css/style.css | 333 +++- src/components/common/EditableText.spec.ts | 69 + src/components/common/EditableText.vue | 30 +- src/components/graph/GraphCanvas.vue | 378 ++++- src/components/graph/TransformPane.spec.ts | 350 +++++ src/components/graph/TransformPane.vue | 91 ++ src/composables/element/useTransformState.ts | 242 +++ .../graph/useCanvasTransformSync.ts | 115 ++ src/composables/graph/useGraphNodeManager.ts | 813 ++++++++++ src/composables/graph/useSpatialIndex.ts | 198 +++ src/composables/graph/useTransformSettling.ts | 151 ++ src/composables/graph/useWidgetValue.ts | 155 ++ .../node/useNodeCanvasImagePreview.ts | 2 +- src/composables/node/useNodeChatHistory.ts | 2 +- src/composables/node/useNodeProgressText.ts | 2 +- src/composables/useCoreCommands.ts | 12 + src/composables/useFeatureFlags.ts | 9 +- src/composables/useVueFeatureFlags.ts | 38 + src/constants/coreSettings.ts | 14 + src/constants/slotColors.ts | 30 + src/extensions/core/groupNodeManage.ts | 2 +- src/lib/litegraph/CLAUDE.md | 2 +- src/lib/litegraph/src/LGraph.ts | 23 + src/lib/litegraph/src/LGraphCanvas.ts | 558 +++---- src/lib/litegraph/src/LGraphNode.ts | 189 ++- src/lib/litegraph/src/LGraphNodeProperties.ts | 176 +++ src/lib/litegraph/src/LLink.ts | 21 +- src/lib/litegraph/src/LiteGraphGlobal.ts | 40 + src/lib/litegraph/src/Reroute.ts | 14 + src/lib/litegraph/src/interfaces.ts | 2 + src/lib/litegraph/src/litegraph.ts | 5 +- src/lib/litegraph/src/node/NodeSlot.ts | 2 +- src/lib/litegraph/src/types/widgets.ts | 86 ++ src/lib/litegraph/src/widgets/ChartWidget.ts | 50 + src/lib/litegraph/src/widgets/ColorWidget.ts | 50 + .../litegraph/src/widgets/FileUploadWidget.ts | 50 + .../litegraph/src/widgets/GalleriaWidget.ts | 50 + .../src/widgets/ImageCompareWidget.ts | 50 + .../litegraph/src/widgets/MarkdownWidget.ts | 50 + .../src/widgets/MultiSelectWidget.ts | 50 + .../src/widgets/SelectButtonWidget.ts | 50 + .../litegraph/src/widgets/TextareaWidget.ts | 50 + .../litegraph/src/widgets/TreeSelectWidget.ts | 50 + src/lib/litegraph/src/widgets/widgetMap.ts | 40 + .../test/LGraphNodeProperties.test.ts | 163 ++ .../__snapshots__/ConfigureGraph.test.ts.snap | 328 ++++ .../test/__snapshots__/LGraph.test.ts.snap | 3 + .../LGraph_constructor.test.ts.snap | 3 + src/locales/en/main.json | 26 +- src/locales/en/settings.json | 8 + src/locales/es/commands.json | 2 +- src/locales/es/main.json | 22 +- src/locales/es/settings.json | 8 + src/locales/fr/main.json | 23 +- src/locales/fr/settings.json | 8 + src/locales/ja/main.json | 25 +- src/locales/ja/settings.json | 8 + src/locales/ko/commands.json | 2 +- src/locales/ko/main.json | 26 +- src/locales/ko/settings.json | 8 + src/locales/ru/commands.json | 2 +- src/locales/ru/main.json | 25 +- src/locales/ru/settings.json | 8 + src/locales/zh-TW/main.json | 11 +- src/locales/zh-TW/settings.json | 8 + src/locales/zh/commands.json | 6 +- src/locales/zh/main.json | 15 +- src/locales/zh/settings.json | 8 + .../canvas/litegraph/litegraphLinkAdapter.ts | 589 +++++++ .../core/canvas/litegraph/slotCalculations.ts | 283 ++++ src/renderer/core/canvas/pathRenderer.ts | 820 ++++++++++ src/renderer/core/layout/constants.ts | 50 + .../core/layout/operations/layoutMutations.ts | 340 +++++ src/renderer/core/layout/slots/register.ts | 75 + .../core/layout/slots/slotIdentifier.ts | 40 + .../layout/slots/useDomSlotRegistration.ts | 229 +++ src/renderer/core/layout/store/layoutStore.ts | 1356 +++++++++++++++++ .../core/layout/sync/useLayoutSync.ts | 79 + .../core/layout/sync/useLinkLayoutSync.ts | 365 +++++ .../core/layout/sync/useSlotLayoutSync.ts | 163 ++ src/renderer/core/layout/types.ts | 322 ++++ src/renderer/core/spatial/SpatialIndex.ts | 169 ++ .../vueNodes/components/InputSlot.vue | 107 ++ .../vueNodes/components/LGraphNode.vue | 271 ++++ .../vueNodes/components/NodeContent.vue | 40 + .../vueNodes/components/NodeHeader.vue | 115 ++ .../vueNodes/components/NodeSlots.vue | 113 ++ .../vueNodes/components/NodeWidgets.vue | 155 ++ .../vueNodes/components/OutputSlot.vue | 106 ++ .../vueNodes/components/SlotConnectionDot.vue | 40 + .../widgets/LOD_IMPLEMENTATION_GUIDE.md | 295 ++++ .../vueNodes/layout/useNodeLayout.ts | 168 ++ .../extensions/vueNodes/lod/useLOD.ts | 186 +++ .../widgets/components/WidgetButton.vue | 43 + .../widgets/components/WidgetChart.vue | 78 + .../widgets/components/WidgetColorPicker.vue | 63 + .../widgets/components/WidgetFileUpload.vue | 318 ++++ .../widgets/components/WidgetGalleria.vue | 123 ++ .../widgets/components/WidgetImageCompare.vue | 70 + .../widgets/components/WidgetInputText.vue | 49 + .../widgets/components/WidgetMarkdown.vue | 95 ++ .../widgets/components/WidgetMultiSelect.vue | 71 + .../widgets/components/WidgetSelect.vue | 65 + .../widgets/components/WidgetSelectButton.vue | 36 + .../widgets/components/WidgetSlider.vue | 170 +++ .../widgets/components/WidgetTextarea.vue | 49 + .../widgets/components/WidgetToggleSwitch.vue | 55 + .../widgets/components/WidgetTreeSelect.vue | 57 + .../components/form/FormSelectButton.vue | 108 ++ .../components/layout/WidgetLayoutField.vue | 28 + .../widgets/components/layout/index.ts | 14 + .../widgets/composables}/useBooleanWidget.ts | 0 .../widgets/composables/useChartWidget.ts | 28 + .../composables}/useChatHistoryWidget.ts | 0 .../widgets/composables/useColorWidget.ts | 20 + .../widgets/composables}/useComboWidget.ts | 0 .../composables/useFileUploadWidget.ts | 20 + .../widgets/composables}/useFloatWidget.ts | 4 +- .../widgets/composables/useGalleriaWidget.ts | 26 + .../composables/useImageCompareWidget.ts | 20 + .../composables}/useImagePreviewWidget.ts | 0 .../composables}/useImageUploadWidget.ts | 0 .../widgets/composables}/useIntWidget.ts | 0 .../widgets/composables}/useMarkdownWidget.ts | 0 .../composables/useMultiSelectWidget.ts | 21 + .../composables}/useProgressTextWidget.ts | 0 .../widgets/composables}/useRemoteWidget.ts | 0 .../composables/useSelectButtonWidget.ts | 28 + .../widgets/composables}/useStringWidget.ts | 0 .../widgets/composables/useTextareaWidget.ts | 28 + .../composables/useTreeSelectWidget.ts | 24 + .../widgets/registry/widgetRegistry.ts | 150 ++ src/schemas/apiSchema.ts | 1 + src/schemas/nodeDef/nodeDefSchemaV2.ts | 153 +- src/scripts/errorNodeWidgets.ts | 6 +- src/scripts/widgets.ts | 34 +- src/stores/managerStateStore.ts | 8 - src/types/simplifiedWidget.ts | 41 + src/types/spatialIndex.ts | 23 + src/utils/spatial/QuadTree.ts | 302 ++++ src/utils/tailwindUtil.ts | 8 + src/utils/typeGuardUtil.ts | 19 +- src/utils/widgetPropFilter.ts | 71 + tailwind.config.ts | 1 + .../element/useTransformState.test.ts | 351 +++++ .../graph/useCanvasTransformSync.test.ts | 240 +++ .../composables/graph/useSpatialIndex.test.ts | 498 ++++++ .../graph/useTransformSettling.test.ts | 277 ++++ .../composables/graph/useWidgetValue.test.ts | 503 ++++++ .../{widgets => }/useManagerQueue.test.ts | 0 .../composables/useNodeChatHistory.test.ts | 31 +- .../tests/litegraph/core/LGraphNode.test.ts | 34 - .../core/__snapshots__/LGraph.test.ts.snap | 6 + .../core/__snapshots__/litegraph.test.ts.snap | 12 + .../spatialIndexPerformance.test.ts | 406 +++++ .../performance/transformPerformance.test.ts | 483 ++++++ .../renderer/core/layout/layoutStore.test.ts | 260 ++++ .../extensions/vueNodes/lod/useLOD.test.ts | 270 ++++ .../composables}/useComboWidget.test.ts | 2 +- .../composables}/useFloatWidget.test.ts | 2 +- .../widgets/composables}/useIntWidget.test.ts | 2 +- .../composables}/useRemoteWidget.test.ts | 2 +- .../composables/useWidgetRenderer.test.ts | 168 ++ tests-ui/tests/utils/spatial/QuadTree.test.ts | 269 ++++ 171 files changed, 18026 insertions(+), 564 deletions(-) create mode 100644 browser_tests/fixtures/utils/vueNodeFixtures.ts create mode 100644 browser_tests/tests/vueNodes/NodeHeader.spec.ts create mode 100644 src/components/graph/TransformPane.spec.ts create mode 100644 src/components/graph/TransformPane.vue create mode 100644 src/composables/element/useTransformState.ts create mode 100644 src/composables/graph/useCanvasTransformSync.ts create mode 100644 src/composables/graph/useGraphNodeManager.ts create mode 100644 src/composables/graph/useSpatialIndex.ts create mode 100644 src/composables/graph/useTransformSettling.ts create mode 100644 src/composables/graph/useWidgetValue.ts create mode 100644 src/composables/useVueFeatureFlags.ts create mode 100644 src/constants/slotColors.ts create mode 100644 src/lib/litegraph/src/LGraphNodeProperties.ts create mode 100644 src/lib/litegraph/src/widgets/ChartWidget.ts create mode 100644 src/lib/litegraph/src/widgets/ColorWidget.ts create mode 100644 src/lib/litegraph/src/widgets/FileUploadWidget.ts create mode 100644 src/lib/litegraph/src/widgets/GalleriaWidget.ts create mode 100644 src/lib/litegraph/src/widgets/ImageCompareWidget.ts create mode 100644 src/lib/litegraph/src/widgets/MarkdownWidget.ts create mode 100644 src/lib/litegraph/src/widgets/MultiSelectWidget.ts create mode 100644 src/lib/litegraph/src/widgets/SelectButtonWidget.ts create mode 100644 src/lib/litegraph/src/widgets/TextareaWidget.ts create mode 100644 src/lib/litegraph/src/widgets/TreeSelectWidget.ts create mode 100644 src/lib/litegraph/test/LGraphNodeProperties.test.ts create mode 100644 src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts create mode 100644 src/renderer/core/canvas/litegraph/slotCalculations.ts create mode 100644 src/renderer/core/canvas/pathRenderer.ts create mode 100644 src/renderer/core/layout/constants.ts create mode 100644 src/renderer/core/layout/operations/layoutMutations.ts create mode 100644 src/renderer/core/layout/slots/register.ts create mode 100644 src/renderer/core/layout/slots/slotIdentifier.ts create mode 100644 src/renderer/core/layout/slots/useDomSlotRegistration.ts create mode 100644 src/renderer/core/layout/store/layoutStore.ts create mode 100644 src/renderer/core/layout/sync/useLayoutSync.ts create mode 100644 src/renderer/core/layout/sync/useLinkLayoutSync.ts create mode 100644 src/renderer/core/layout/sync/useSlotLayoutSync.ts create mode 100644 src/renderer/core/layout/types.ts create mode 100644 src/renderer/core/spatial/SpatialIndex.ts create mode 100644 src/renderer/extensions/vueNodes/components/InputSlot.vue create mode 100644 src/renderer/extensions/vueNodes/components/LGraphNode.vue create mode 100644 src/renderer/extensions/vueNodes/components/NodeContent.vue create mode 100644 src/renderer/extensions/vueNodes/components/NodeHeader.vue create mode 100644 src/renderer/extensions/vueNodes/components/NodeSlots.vue create mode 100644 src/renderer/extensions/vueNodes/components/NodeWidgets.vue create mode 100644 src/renderer/extensions/vueNodes/components/OutputSlot.vue create mode 100644 src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue create mode 100644 src/renderer/extensions/vueNodes/components/widgets/LOD_IMPLEMENTATION_GUIDE.md create mode 100644 src/renderer/extensions/vueNodes/layout/useNodeLayout.ts create mode 100644 src/renderer/extensions/vueNodes/lod/useLOD.ts create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetSlider.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue create mode 100644 src/renderer/extensions/vueNodes/widgets/components/layout/index.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useBooleanWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useChartWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useChatHistoryWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useColorWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useComboWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useFileUploadWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useFloatWidget.ts (97%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useGalleriaWidget.ts create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useImageCompareWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useImagePreviewWidget.ts (100%) rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useImageUploadWidget.ts (100%) rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useIntWidget.ts (100%) rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useMarkdownWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useMultiSelectWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useProgressTextWidget.ts (100%) rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useRemoteWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useSelectButtonWidget.ts rename src/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useStringWidget.ts (100%) create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget.ts create mode 100644 src/renderer/extensions/vueNodes/widgets/composables/useTreeSelectWidget.ts create mode 100644 src/renderer/extensions/vueNodes/widgets/registry/widgetRegistry.ts create mode 100644 src/types/simplifiedWidget.ts create mode 100644 src/types/spatialIndex.ts create mode 100644 src/utils/spatial/QuadTree.ts create mode 100644 src/utils/tailwindUtil.ts create mode 100644 src/utils/widgetPropFilter.ts create mode 100644 tests-ui/tests/composables/element/useTransformState.test.ts create mode 100644 tests-ui/tests/composables/graph/useCanvasTransformSync.test.ts create mode 100644 tests-ui/tests/composables/graph/useSpatialIndex.test.ts create mode 100644 tests-ui/tests/composables/graph/useTransformSettling.test.ts create mode 100644 tests-ui/tests/composables/graph/useWidgetValue.test.ts rename tests-ui/tests/composables/{widgets => }/useManagerQueue.test.ts (100%) create mode 100644 tests-ui/tests/performance/spatialIndexPerformance.test.ts create mode 100644 tests-ui/tests/performance/transformPerformance.test.ts create mode 100644 tests-ui/tests/renderer/core/layout/layoutStore.test.ts create mode 100644 tests-ui/tests/renderer/extensions/vueNodes/lod/useLOD.test.ts rename tests-ui/tests/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useComboWidget.test.ts (90%) rename tests-ui/tests/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useFloatWidget.test.ts (95%) rename tests-ui/tests/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useIntWidget.test.ts (94%) rename tests-ui/tests/{composables/widgets => renderer/extensions/vueNodes/widgets/composables}/useRemoteWidget.test.ts (99%) create mode 100644 tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useWidgetRenderer.test.ts create mode 100644 tests-ui/tests/utils/spatial/QuadTree.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 2ac2ab06e..68be11a12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,3 +127,6 @@ const value = api.getServerFeature('config_name', defaultValue) // Get config - NEVER use `--no-verify` flag when committing - NEVER delete or disable tests to make them pass - NEVER circumvent quality checks +- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black` +- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `
` + diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts new file mode 100644 index 000000000..5c4541b92 --- /dev/null +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -0,0 +1,131 @@ +import type { Locator, Page } from '@playwright/test' + +import type { NodeReference } from './litegraphUtils' + +/** + * VueNodeFixture provides Vue-specific testing utilities for interacting with + * Vue node components. It bridges the gap between litegraph node references + * and Vue UI components. + */ +export class VueNodeFixture { + constructor( + private readonly nodeRef: NodeReference, + private readonly page: Page + ) {} + + /** + * Get the node's header element using data-testid + */ + async getHeader(): Promise { + const nodeId = this.nodeRef.id + return this.page.locator(`[data-testid="node-header-${nodeId}"]`) + } + + /** + * Get the node's title element + */ + async getTitleElement(): Promise { + const header = await this.getHeader() + return header.locator('[data-testid="node-title"]') + } + + /** + * Get the current title text + */ + async getTitle(): Promise { + const titleElement = await this.getTitleElement() + return (await titleElement.textContent()) || '' + } + + /** + * Set a new title by double-clicking and entering text + */ + async setTitle(newTitle: string): Promise { + const titleElement = await this.getTitleElement() + await titleElement.dblclick() + + const input = (await this.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.fill(newTitle) + await input.press('Enter') + } + + /** + * Cancel title editing + */ + async cancelTitleEdit(): Promise { + const titleElement = await this.getTitleElement() + await titleElement.dblclick() + + const input = (await this.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.press('Escape') + } + + /** + * Check if the title is currently being edited + */ + async isEditingTitle(): Promise { + const header = await this.getHeader() + const input = header.locator('[data-testid="node-title-input"]') + return await input.isVisible() + } + + /** + * Get the collapse/expand button + */ + async getCollapseButton(): Promise { + const header = await this.getHeader() + return header.locator('[data-testid="node-collapse-button"]') + } + + /** + * Toggle the node's collapsed state + */ + async toggleCollapse(): Promise { + const button = await this.getCollapseButton() + await button.click() + } + + /** + * Get the collapse icon element + */ + async getCollapseIcon(): Promise { + const button = await this.getCollapseButton() + return button.locator('i') + } + + /** + * Get the collapse icon's CSS classes + */ + async getCollapseIconClass(): Promise { + const icon = await this.getCollapseIcon() + return (await icon.getAttribute('class')) || '' + } + + /** + * Check if the collapse button is visible + */ + async isCollapseButtonVisible(): Promise { + const button = await this.getCollapseButton() + return await button.isVisible() + } + + /** + * Get the node's body/content element + */ + async getBody(): Promise { + const nodeId = this.nodeRef.id + return this.page.locator(`[data-testid="node-body-${nodeId}"]`) + } + + /** + * Check if the node body is visible (not collapsed) + */ + async isBodyVisible(): Promise { + const body = await this.getBody() + return await body.isVisible() + } +} diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-mobile-chromium-linux.png index fadb023484b899fa3673f16785c9e7e8d683353b..604c23351d505955e705dad7b122aa00b96720b5 100644 GIT binary patch delta 73015 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

)A>fRo-s9}~tUGlKUf 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∋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

q8@eF&c}&d;Fz5t)9m5y+k~yo8Q_D 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 diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-tablet-chromium-linux.png index 350b01a2849687dafadbff1be77171dc9b6f7da9..1e2a1017cf4a8d788ee27f31fd278102687a3b95 100644 GIT binary patch delta 164881 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 z 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�w(UCW3~W73Gs43%4iApJRHEM8cCV3Vi99FwC8y=w|g_LZ$!!pYMBs3}VAiYQYxFZBtl?^c-VJU6nRZVCw$$ zJ)?}r^U?ifZ);OKuyai;|4pi8wM#i1?NdHWIA%^T##3gJfH;N z1dhj`MpA1PL4XdZes>j!wJNnrdj`biYR2E+Tbk{E- z9{c*OA5|`9u!YbNXn}mi?um(Qtn;I`bE4)wQHd!X{1|I*h1Tb{K}LiD?3=DaR_U8A zFSveKwTTH1G%4qr+1SLme8GK%w=nZpNHrCm9)xUo`*1kDA$|4!7BWjSW>Yhar(L&& zm^dTND~A3@wgyAQjxND>l$r#seS*`p~GD5JY>0Av4(J zZLP~rQrpLedgzr@Y`hhhe+)*Yx47P|2+hi3o7LxlhB&biT{o--;Qhyy*Z|QqTjt~P zV}VU-PVEL^pN`B=pF42KYZrRU^cXK0{(OA=bdte`3g8z=1`Ydgh@WvKf^PhBA;hh> zxkZE*09iUvp)Tf0tQdl?`{ncwK6dXOfyYg?MT{Bc zZ(5LJq-kNv=!_1)KE)-+U_b+0LkW!@nH?d&T&z>6axIE0hmhm5j11bR9 z*Cfj-K1m?ecTL>+4?-#qM940X!yXsiHBaI`kFReez7r$JCjZ&A5NuEoWpHH2gW~Xv z8b&!x+J*fykh-WFr|t@O*QUH&aRWPZ<7~pvQI9(LxR5@?bJx%UgZEmQ*%pZLK7YbZ zkcl(HCN z*O;zPO)`{>i|sRz#*`Tp4>1f6$HL+fP}Ne-;zl}pGWYtc?;*}#w^1%;w>UpIX!q@X z+zMayPxGP!@?DiKzTta)NT>GqvJz9-`kG1VJTp>u_*j{?jq$Xnjk|~F;|XbM!Vc0= zZH$CP#-2Z2>PdH;MLTg@ne|J`@>Aa??yRo!Em_Bt?&UiD(--`U=)(0xqD6`On-q** zPHc~7b}h>Ca2P(V2pwME@<0V&1R;jGd{ZQ+=Q~UCqE$8isL4YvNKGn{4%#>IzD6i> zl0kAYg7^M&M%+6*+Svc%{P2<$RNi!B1gY6iF4M>MC;dtBK@&?=Btn<4vB$lA-r>(p zR*vnmlMfvR-)wb9`=3$kw`Mm8IEBYwQD$V4-Q+QK5-$wW zg72cMb!9SIy>hk;A+A!j+rFC0Cxys)rKMQ-C=0R}dOH2=F+%lb22zBHc!8-_MH%fjRW)jQb zGqX(HKSzFwTSxh3iPvQvM_6|X5@2;YIlGTWToNhkZ>p^|#Dr$@4!;BIf1VetXsj6~ zQw5}T8reT#*L>^i7#)95*2M~2F|uzuHzy~RD-hOQ(GJ1asUW#P^Y1vYOLpN?JQf;G zK%pId6 z{cWndEV&8-E=6QpgpI`Ys~SSjj-8)xh7nUm@0;(7h~s zFa!-w`|t*@X;BU*Ocn!l59Id+w6Js$g93YZXwRnNIM{N=8I!63)5+0r-u)E+6Q>rB$dU zN7OGmge~&{2(0^M3l9WbhTm8jGD21n>T{ymQPMF<9*|J1cSDJ1l%d zCGHddn0v~f7-RwD;a&b3d~#CKzDOw3?*w_(2Xdvq(Hfvv&yMq;boW)w0Ix!)oLf@N z&lKRX^}`}T`<51l_WX*lq``GM7+nx1Oj)76u;uJL8jFS8H8ndfqy09*==yxrt(IfMkr%Lwl!~d$E=xlKZiU2u95t4?ouE1+60YfqjgA@Shb2 z@_oq ziK)qzb^qX{Z}~C*VA3Z7eJO>#YoB2uN}sob=f8F0O6TGZL{YtL1cOAfMnE zFL1#wzoS5pyOv4Cbvx9huRJ&8Hdzj^{6;E55o%EneiE&lWtIcIm!LGKlEvr?e+_el7? zNH@jG7Nv~5wXUuin2;G^l%W=^EvNrJC zEV(Co&@tlyZ>AFUg?iQ|eLMtrZO=u4Vqr{#l0ivyn`PyH6tlC6R}Ue#Sw8Ul_9_E> zXy(V?OYT!?lOUi=1>Y$MAf1fI7MkN=krocMu>mj<@kJD5MCcRRseD$}bLrg7SDVwD zlIHw?y~ICkc6J_Ic{mm={&J|UoBWltccSC2AoktzIPQun2m2uy0kfhqpCzdQ<37df zOV?87BqA6kf4Lp2#35Be9dm|L%WwgOTq9BMJ4t(GzNqx1$?8l zUnK|8uZ-j=Bk@J<^}&682LmdZjxUXPflRfZgepr_Cix3B?J4_l-UBChE?}osSz>CHn12GUX8!6RShbKUMqRdQwl zQlnnYmTS&hKrdcIiM9?3ZY8Y51=^;AAY>3~_D5&lE?qBo5D>+Fr7p?){}Qv3OxF;dl!D90Oonve(tr_Hy{-!i$p5QzcOiTnF+a`nz9 zobM%W(zx>g2V5=00~}BbVyJc-MoHZ$pQU z%EwbDoWz_|N@!}MBGVz-=CQ=@FXFUNdAx{db1>uEz$5o6oeX{_{*9EfyV%Y*qo0{~ zIbw_h+ijK4YJp$1hSVD}GE9o%mCX-t5-em+Ca_4%XubT|w!KhSk;GeJtfKXDbYZtF zs-+V*bX2LU*Ao&@NPKEoN|c&nW8p?rR!A&s>0U?tCwVy`+6Yd+yHx)P849lWvv`s+ z9~J-wqk|1>itp$&B6;0&jlLhD8X zWJBs_F@EYJz1J|i;ZyLevwXeqKt}9N27)qZtN#cMnwBg|$*g5b`X7R-_q!pA&}hyP zg9;DUm6lM%t|{wLEx*4dtkdONEY|}FD6`Dok{N$B0RjkwZ_&d z*be4+q_6|0*dfM-l%JBh(1B-11(phugirw9FHTerVfv)bN?n%_h2d@88BEevg%Hr; z;JMN)Z?sWl?$lpnXK~M~z5E#QtmJ?5YkA66H6SczYEVU!GCfe|k|qW0AdRp8{n)yJ zl^$ORaUjMAAZetn~sYE*9d;*{h}5r}Y^U01^|muE|@9>~dn$fN|lvIw(wq4jvXd zZ;V<*g_}Lx4}umQB0lhP;dcLXF(nyC>s+qxH6K z1`)d;N(4NSmn4B);UQaX$t*l$`wUPHp;>Q7Lm_+FX|VNWvBH3{KZur!?$M)cLtVZ0FAge4T}^Bi^=et zDkbQMEL_OB%gxf+B;q}e`rFd6W<;ogOSl_PsJH+GhBjV$4euY9b64#eH?+p+O$o+j5t`?*;vTQZ@`2gw5r%Q$I)V>UjVR_25;jU)@aawtn4 zf=jxeB{bB9uEdhR`r5o0XUDz7ZGO7`?{Gr4QwI0K{lnY5&l8efU$|S#;wsV2StoFQS2NN-qF08B)W>MY0sXGv;_X?@Y7`n z)+pYzc1IrKTW!7cGu`7Zee>F#Y?(@fB7xRTW#p*!okqoecfB~rpU2mi=hw%u)#qUF z;61I4EG8ik>P@BH>jhy1sxJ1Bx^FV{l^D=AC6Q+2Gpc8y@9aG} zKhHQn?85w7DiRBHCuV$o~mChHF#h#nKRP7c!S26L9$eNgzg7m1g~sl`he72D7z} zi?v3J4>dmh@XhBi8zCFi1tzqi zMOlYgBIkRVj}|{4_T~C1^&2lHI$?==+#P%)aavi|!+5Vpll9HuJF zps1S;KfT*jmoQq4*iC1oLhRmS_vJ7PS3)Md5}o~KT)YK-s3p%YuJGS0*HIpYiSZ1x zjcapF=-OiMQ=%t-2WU&;heFKGeI)L>YANi8^z`&lzkrO{i5IWycU-xmjTUn^+?At+ zlCZ3y)GTlp79v3F?cylxqqjBX=bNs44b9Oe;J3y4miW1KPVy)|Jx;p4{u&tU%$x#)E3y0N z%%0WfZJE)$Bt_0HZgm~kab8jLz^p@2h2MJQr7r2!2+`oe*Z46ai=Cv(lHfsZSlg>~)rAvL>G;n(t;nur;l$nRcH@3> z(kEhaaYEsG%~Df~Zi|s)Q}6A48B8GA-6dHzPboqO zZZG;D`d43HJYu3LrS#t;ZGLU!t3txxUeL#KwhNz&DNQ@5yC$STCWu3V9h8NcP6;Fm zB5CWKC#{%3Qj{z>KUW7K#WI(ke17%R0AZK{8Qh_$dwZS2&cqhLhrfsG6#lA+v-y=~ zZFtXxa^+FZXsV;uHE%iA(2U)8O?7sM{5+rp2D>dVVA%qucRXz=BpCTU9Tl%W-`s|d zTYZ|i1;J>6?sdQMy4_ol5?NXe*-utGvi}oQqYZ%6J82NP_5^H^E28m)v8%oh=;0%U%K%LinU>cs%MLp)&17ac#BCIiz3hjL++`Uxgj}Zm9#f!Yx3Mh zlTzWc;XUh0#S#n+S6RuGDs?Mt*1gW90dZOGFbL1~*fr3HF!asc1eUyU*%PMng`Q;l zKBhws%PtuNrPJK=8HtnjGTnTgT|n+TbLv89P6d+7^aI3xpJivW5>T07XkFy}q+%k? z*2GlY!F$QaX2MITPU&wOP}w{_->>SCvzW{sMqZeRLX4}+0`v%j-+9Zdx@!XX_+(Ic z@)H@jQb5Xbu+2?`r}Jfg!*ar1+*81$Uh#<&s4QKmRpxMkOH7`B-`0nRrRnXcQ?Og`+}~~7E5l_w|vW0 z^GZq|NmG*sxBt1#rRJP(c3IwDd2{9`v?!8^UMVdW3LTZK)76wU(VqW_mshfrm;t$U zTP<=^Q>AD^RN3j!scJ#Iyv-tdz|@xK1@kK@$O zCt}tN?pjq^T0o5&9>9}5uvT<=0l7|b*H+*KjoFPc(93B-VxgO5$&W{>t0n&)}QrSgE}y~$@=c`$2B?B z>2z}_5l&Y*7dn!n%P@rzZ#VF4gxe&7+3mBtFkGDXFf^l49!kqXo^ zs<|9BJ+V%Zp34r1C@@T5%uwcD zJRUMI`Y39w!VymQ&-?q|Zxe;pLAvg63}+oE^w&ZWV^-9X^|`1KB@RqO*}AtVYNaNy zWU8GE;F451=kf`Gv|Z+C?#~uKd7R_;?HK7L0D};Sz!!UPANbW;GTq=p64M9de{c5; zk3y&2QKlbqmsQ{UgzaG`ApN0r#$^fo4RZLxHoGZ^JpcYoa%FesgeNvje+iNG*AIeb8o1iYCfVh&+=m#-SE4+BjNn-1{H@bY^3d z(2AJ#N1B#O)u0lVw0Gy6@;xSUh9~^JEwkQql~jJ88{d?0xRn)g_}sMDc286IFE@U= z=$O`fpl%h+_|njd+u+)O#E1le5boaP+KiQ}_ zXgnp1#UoT#SMLHKy+J(dCR9e~hM72t7JrWO2usv58pX6dop(H%{X80vJTtE|UeJ`a z$?IJBF6HW!UQfelQ1*t0)&SeAW<^llZ&gnTrYzfX?EZy~u*b*XW?2Ekk>#N}viSY4 z=tT%+hVuR*P5f%~B;I3GiltrJ&efHtvQoZ!Vm3rcK$St< zKv-uNQ%%GlR=z&9`HUDk;h80VO_ee*rj(T5Xa5&-FfGqL@-oWRkjZV zI-ai+^21^zyj)&3<{Q-@h{CK>A<#MHQthOzBXeHDRHl@byt?I=TxEIAf+mmm88RB! zSuK_67?zYIlgYC1j!%9l6Ul7X4dL9#JsbSN752tT@AnyWHb-?AW%pSo;7s3db?A_2j6B2@8V#U~~N zypzFNs2#V~=7x(u9r7!d^V@*=$~`G9x3{+JM53sVCce&s$@BK?4@wf>Y3;-_k__To zvA9P#+AF3SD5pNtx?Sa{xL7xt;h7{%~FsNljz&)?+kW zMNw9^YS;ACeAENUV||Ch{|A&QjgpG$w);wSHdg`>`U#C70sI9Gg1&o!rBS%A=$4${yA>b>8*2{K4v{#2J6W;MeSz;ocb6h^B3d6^hpDFXjLN ztN(;ryxh_RkH&qyhC18mH%T@Z^1iKd5B1SU1R{}%%yK*?)YNRd%<|)w5TpY=Nm5V( zy77=CAu5{Fv2Qw+?4s416B34}PYwKqi~>*{>| z%?+$LmQ`5=&(6;N8W{yXSloG;-+$QHIM~>?p?XG)0j|%F7CT-SSD%gi9)$2{+$^t9 zLE+OZI@p+zu)KGM5zxRCpe7NKmRyG4%s>l7h|c>mT}N5JYIej>dHT}GB1M%A%%1PaVrX?hKumf(xC&aW%2M`c|Jze66$CY^+yJ*_~p(p ztXC|j3FMa+)fe9#Wq#WQe8#VQ|1r}N3sSH_~a;*j2rajJ=hUEo#X0bs8@8b2I`c>zvYn2m2(n4b7C!6(S2!+8o2+x z=JHxeIBpH0<&n#2DvfYromyExC2!!0xTaRz(yJvhdFWVD37Z{&cCf8_WSd!`1|TBu zA0#JOpyGet4pe%x)qL4qYJzCyuyAvV@X|4SgW4t%;S`4E*7h@YPe0rzT#nv1$**pR z?Jy05YiI}E^UF^SnP!n{`@WAJQ(GxBhl+>3z?K~+oifJxWc_{Wh6)1e>=q|`FSaJ~ z{~I6kzpzoTiHRPg{9f(i@S|lSUhfbGR;xOvT6mqAmSoVYfn-guq=C5p=okTxww-IC zK(Rxr+}k>3<-w7iQ1X5nRYYOI?4kaREkh`ePh9+ebJw3r;BVRt>`UurK1dT*uB|N;XyHx3W9=YmUI(s2t{NEx^s6?cuz-$65KxJ)o=eE z2grSjjUPbhQe|L@391w|^=ohn^G#G@EHq%?8_R&tjK&x+aH8cL6ZVmVPCHOX7u9V(U@xXCtT9fv710)pBMg0BOG&dh}{*Z=I5 z71nnb?Fx=V8W7LMqXNH&`GZS!aoR2fU@xry#Iv=$+B-Ge>NnSZPf8qW?guRI9Zl}) z_oa8rN#5eAwvp!(kEde|`2VROx8bwTOAwnP#%`aZP8|tWoqhI($z1#f(+TEQdzh|q zAp&@l>CHR2T{#q?#h#xH9@lP6>RtW`*;oR}#^fW=FC z!_>az|LbixDozZdDAe37c+L6w+k@u^zI;vsf56>;))+?m!p-@&YcuC0g{cA_QH1WUDT@AT8A)aBO2*pX>O+k2` z0|^O<^-h}tBJVXMR807^*j<`Yb4$3;7qIi6yN)Jb&X+P3q2v1Wc>d>no>MVf`n4v* zzwg#ogj=BkL9!twIoPevVSsTzH8*ESo!2D2&?^s-wTE`MpU2$i%fa-YAv>4s$Z-v6 z?frFyNN zG3WI849J6R4=-Q#?zBocg<8$l-j2O}eY(uHVkRn!3p~$$+0JfUdhUXpg_%LtI_{1X z5SZC)-gZA>{}=y99O(mJj2vKmJg$tM>kXXik~eC2a`V{za1lNQQjQcXRH93-$=`Mz z(yEDE);w)V0lTajo>3aEL>C3=OmjB3tH|MFlab<2AQADMQlP{(19kAD7u{Ks<{kUb z3ygFF=(vA_Mo8yUCR)IM&LB8kmV?!o@QL%bO+$uNzBQLK$34?87qcDLdwY$37yrA| zwg&%_opdyO>9O+G6uKY1wil7 z%*U%)UZ~3l#2641G2x6jC_m^KsnaErGN*&p?Zj&-5BExU1`K*N7+1N$d&upa>QO8B z3VR66bk!KsjM$L^Q;|4#RS-VmI^FN(0OW7&$`bbx+2a0?i+S+%hQ#x3M}uCaHkT!8 zt&r<32Tc0NC~#`8d53}8I&{|jFn|drFu7Nh>t>z#{sR(KhU7b2r6NF&EI4!om3lTY z|6}3p$6u;Ew9<5v(mtNLMU6bbT?uF1Q^v@qNnwP^4*cvLdqUJ5*{HoLVuP?hi%iMyFx^%68DGK;Q;%p*udxKHcOu(L0*j2xH8&8If;G z-6yVEN;2>5VUcX5yq~jc0AEbAWn(j@hW6${v>}6Yc*S`RbTFgY9nrs=YmI;TxsaR8 zf2)49z6rsmXsl5vhF_+5w7;1G*^2)0KR&dvc}Hx1f7!2|?SIqyHdys+WiWWU#^L=j zdq1@L1Pjy4vXsBFxRZTaWn==)av-CAGqh6S`;EO6U{|p3Yy~3*SAHNC)0qCv=x|m0 z768)99qpOkw!P@oAy-ROHHz)Lr#$OSbaH_Ud1s38pit$V^O{!K067K~YHRAyMLM2k zfivsVjrh;95JgBIst%+h;Mf!a@oxc^IXXCn&GRS|kBy`Y?aPLa7lO@pu5*GnHRt(- zz&xIgkCGELm*SozL3(IZzAY45u;==I8wdwx^Gvohg)Ygi<0ka%zJ~FmpD_9A>rF$u zxUs0$!vk}XbaK6RuNA8k7^V-#VmDJK& zvdwXBXd0cV-Y@)79z2uJS-r9>EJe1DN2SOmzdqnq!(w5jFlO%7f{$%W7yg0GR}}38 z2r0;f{!Vq@VzOyjwTpD{Y};c8Qs+EY<~YT1+EhXPx2mGOsNc>s4^z?>!1UlA_QtYV z4g4W>>H8yovjNO)eX-+wF+A%FK9Ww)RO}SXKliWVqk3byO(J6AQ8E~%!n*~q?k{q+ z%-QAse+6SqBpx5!yjuT8n2yr5uocpS2<3HY{>$NGr45sx68-Cr!cP%c3b6`k&5Sfh zjd?aVD;U5GERCSSIuV5#ZL-ic5RoN|z5RcK1@bgGe~ zrs0gVY^02w1|8r^1x^&W;C{aH>|@?5XJZj=M99AUU)$}TL$&Q?W1DB|ia3?sqJY1P z^S$ZyIb6PSQCgW_lL;RH0V!D}v#~OJx$bd#l1#@uS$-AgE4N)?rAY&NY0EqQ?(VMg z3ub~pMserO9ll>@xEBl~@Jt}k0C2Vz`dt1#5TOXkt;s9yDwlxFU3w(2`dE2*g-zbP zGD$XA3XsiJ+HZteW-oFtgiWgVYkFjn~=wD+>T4eT-Mc;SHJ^C3=C%vZ%8}8 zzrWO61E>2xT=p+&k8$vC9w<-tKHfgwo@UF3#ItY-t@%N|!pk=Om;EojOVe&La#DTN z1Y%Yc%B@)t_0&Rvb^$}Omf?*Otc&Hab7e)5us_zOp|F}9G2{`W)JR)Eo_URdC@WAH z2%y&1q?bhpYH3KzFDRsA%xl$p*|cqoxP*1kB9{Fr`4I^h62z*twEtfI`~9|b;B#|e zkunst4e54<%<(&_=}V~)&Q(?w2gs>Zx^IF#yjlhM%1cjTM`VF~EiMInS;k^7laOmh zl54NO_+8H?kPFD`4&;VPjs9Jw4t^N+G-fPz9Ttnh^_GyHm^k>tT7;acVn*b90cFW` zu1otHd0X5hr1{v!f&xl0g=Hk#bd1tG=^_vq!Z$v52jn9SFSRIwS~KaJlQe;KQ2r#+ zd+)~n{05W5C+^me`gOi_^>T4%+wwL2ZBx-^jaBLX)zx0x?I~?{@BPiw`CFFpx@817 z_hE7~f>AGut+LWRZ?7jX&QfbyxRek@L-s6s!75o!nTzV`zSjo*;;$BN5?(rkxaZv& z%(e<7+Eo7&GO^gpH;0<5Fs@@%Z5aa#J~1Xz`@7~%|(P$RWR2%+Th5k{*V zzqOjVxd3NJsMCO!ddEezpROR>FI}AwRuS2M89X4m#iGhk{NsWy9O9P$M4I z$;OZG#Mk^3B4lh3IgV5Z9L%GP@M^Ga+PtMA{!}fD9*M!Y!lQfQ*+t-Y;tAxFkB)^k zfPQNp35qF8h@IN_mE7;@ZqVfUX!CVf9g^`gJ^(qHC2q}@%lEXU!|(529UZS^4nj}0 ziN~WHw>Rdh7qg5ggB_%UGTfgPs@^d*cUZkzw(yfQ9d|Z09A;dyQ3b(Uw_S_ZTYQ zuh~;&EQ>52yBo-wSe5iJ>?EqjNmkBee?*EWHGrC>yTAMWuKhmBSxxVaSdt-+MB#zk%%I3X#c5i_B_1tHW|NJnio|EWr4~Grc}Wb^ELCf z+A)p4XGV_yCICb(MQv4YK=6%r2Q@N?j7Dho;R^^vT?U3hgwz2cM-~!|)4WMXZtJ$W zc5W5S)65kN*|l@$d2g~tWEPd!f&w~!{*og~0i@Scpzr>gbFH9Pnpa8hrGI`xY(kFY zXYZOt- z__CI`IZ!C194ipTT>VEm%j5CdB;!~^OQ{JP99B`x>qo$nCqpC8OQ z>QapHZ-k#bme*N(5My{;Gl(SbWBARMN&D)y0VfU;f;`f3xo?v#Eoc2Z{l*d1@+9mh z@u*NL(N7kGNNN%{!ZIc=alda54!*h-1c3lHwdgP(h3u!Rgj4@ZpHR$@5W?1XE9oCd zk6a&6#F6=GrHamUMyl@cdOGwfAFovgR{EW6G*=@}4Z#vvyJB_XJo*egc< zFW*}#u;2F0_yA*GmtQX=#Qa}sUf2Iu$^hAucy&&`rqO}d1qJ8~Xw5r*9^A-u&fD!& z2It8syX^a3fB$4}#aEa9P^3*3+8Z49E{^P~kNV`80^dY1I&3GwBTw#i&$NT7%Ij_0 z-oq>h*Yb{d=6!K0O8_j~;5Q6L9_$7uO;Aqy*HrsOirh9-Wr|G$KbY){PNNH(R0Y!8 zIi=EximibpbT?N&yqWJfGl#%M)F2^XdD!3D7W`lCiUY4~=dT%;&6@;;#Ozvhb3CcA zM})%IAI9(I@Ps}k4KvM?eG$2a@+B+jw#@W0SFmRrA#?4jUM<(CON>s3bWczJl#H@y zH84t@GUUz|xuQ~#j0=a3;7VDWkVABStb87nEC81hrnh9LXw^}@+^cuIKCdEvoi)ch zPwVrVGs2(Wj{diYKEz(~5AmXXy4tu$WDfjKJs@WouSeNY*Q?KuR4=i3q=yrE&pY`F zh!Rg{zq1vTAW;l6Ox_ly(RtN6ff$@SgGTps+ce{3BPU+{)h!GdS;@sm0*D;PB%8^_ zp6gwjcg3|GwXOF27r@B5k;5f?-AS=sc`L&iZWcC=tT~}Cn~5m$=uI&B*=8nq!vHm5 z^)Z%sk(7LQEB>xZZ{a?m&~p*FQ!-}UAut__t4$lLrTMlNJ@i*baPSC zAkDzg%^)BxtrF6Wl!SDn(*4Z+JC6VJ39fmuXPtYmbH&Tvd|G|WO9Ge`c{K+|BL&mr z(@~dkg3Y2o>m}cAZlLu@^JVi@;z4<35xYjI3bVVsMhT_Tbb{*F(FpuOwB8WP-jAY| zFR9OLzJ~DMTVkErsKJOV-wrT;e4$?q;Q_>%_9eno+$Us^F7_v$*18!KKK!hMb(CSeb+5?E|D%vvn|`B+(q~+v z4&(W6eEI*eaIIfaQ0uV(;_O&dJ!EppS2fuWsCh3PET8xSPk(Z$XXZuMjI0u+WV21w zAlln+C52Xwe(Ls8X4Ax$+RA6D-J+sK$;XB|$0(s?zMTQ1ESD;b!!Wp)N&Cprm{@vV zllLt0wb}&9jBAQW%*-`SY`lNZ z$|b}k=<#Fq9dKx}mPPh+Mj*@VyA6xUL1ujIJL={a>+9FNLIU+ybM4hq*ma!&^nY*z z1LuDKeeB;3sCS*$B8J+(4GtO4A^fIra}L*GG}pfR$eN#Y_56}E$Ecfom$7dKdi!T+ zyLV<`?wt*<46{V3A~0u&FY?X}fvE4`bWVBT!R%1}MRBZYKeOX?yznwXD{uVI{=2%a z7E@lM!}j;J+|})l!Z=KlKeQS@A&~v+f>Q`{F8MeVv)E9(p~jWKo0F^HlMC)zS*N4X zxBgl~+y9C*GsvIQ)NpSemXEd26oj!2=(KLDGgMSee1eGnSqH`(l*;u*RD>EwRT~s) z8ncA`?MTgHxpUKPbyToa+^oGbE&%ERzhC8WKEZ1V$F2GPar8e&?zeQ4AA#9pk{eZj zVN5T=Z+K6aZV!3?Wx8R8+IF!jp+Twj3EMqLM&%2y439EC6rS>a&V5ImmcZKV9t?iH z-16v)JniDE0I)wk^|D+LduGUMDeCi-qoAxpikOW`M-ohul?8LaM}CPT0LTcqwlOcf_lwc5P* z2yZ*#F6P~60y2Fre*b*q{`@}oek)_%Vy!1|0($rAg<-nW00Y(J*GZyAOrHVmnmT%^ z#yHRe)skAu!A@}v9BesQ%dPHxz@{$1TC+1_C0W)GbTKBzmiF)ZWC|IW+rM30z3I{R zrX~}R%pP#1?QnIglP~U-avQxWCsz8svf6~>qN?daewpQX`d%mG>2Gn>;sozFpLy`M z?IlZPY)n{N+z(qXrYO_K0dtDMQ7m;bExlA4db-kCQUa0YjELT@e`f{2-Q&xvwWsyq zp#4emXGD1g`Lsu4e0+R5+S(`d+7`jvC<1lB{JWUcWc!)LeybW#?#7{;^ffMUsWYL0 zf$OyRLCqJ-NE4B%CD?qmUW(tw^)WM1`ws@g9^6lbIeO5okNuy~SOKvJ)AuQ&+rJDZ z=U$(>Qyq<%1jj|je1^q0m271;@jl`4w1i>|#>BvIp65`+Ty0*MO|0*re_k?y;YE{#ylP8=S#dgSc@cRrXAi^iWZI7$r!?S{w&D``Zc5|g7!jUcK5d*8yV0tufsHpSn6Jj-q^@?x0?ND_-|#B!9eA(Qk|QLu zGu6bU#4#YM;?+1-d+k1LZ?tVtYkBNFw>Nsz;zh3~e{yIf3_HYxqMO*9Wbb!BxL-d3R^fc~^mNB#)$`a#dwZ4aakVbbKN3KZ`ngZ8IVB~V z`_4%0>ArZ3&TGg>Y&6~Zpgo#n=YHNOOTgR7;pk0+9$n%-1cvh zqri^D-H*#&XUYSO8$=TWS|ev)LLZ%P7b?ILg+y!Wtd6GVoxgcn_G_Eq&{35RdtssL z|KfHF9)>3#BcAq8Kfv_W5D`gKx*BogQ68eiF5*d2n%`Ej$w8p&s4_h*^6C$eF5jn& z)GPgH7M+w8r#bP{E?+>V7OC?YO|LoEC$>A35mH$Tn2}hfHzmdrOwD$|a*Ei)NZC!- zUlxf!dubPN^Vs!V_CE6s=rhG;`TZ=WovJv>GbJr5;>ce{eORILqqfjvEw;0pRh_4E zi$?>Cdyj4-HM}n7_pT>>M1!~PjT3)m?t5)&{h<^ESGS@fXz9T6;?oTA=8cW133z=h zIhp6j*nni77-JUwZs!6#B7(Az$!1^RbTJ@`w$dpTgu&tM;xOWb%375~gib5;Y8*Of zSNOt&D~^<^dT=tO3blbTnXfYWBAj2VXxjVzy0n3(ds|ZKpz+Z!uhaYce_Q+e->$ki zHiC^`q!DwxR=xwSE;V`jT58@ABGdSdqqe3T+(ebQiWiI_7UK<6OM&CbeusTWFo@PcybS|22)c2WRlM4Nc47I#La5cQTy5H z-7hq&@D76?%8%JDzXbiwospxg^{>0N zsS7Y(kp+M*sx9vLo)+bvDxR+CpGefd!`%Dw(E%!S3^SYSx<1BgAT;SZ7c59R&xv>~1QzK;(X zJ^xNo|0I8N)BP>SxgTuwcYU+?m5HG>dDE9igbg~?jMnkH*1xfR;{Cr|ZhQ0Q&kkIf zv71iK`5QTKY-Z!*8w_VAy^q)&LSxc~N-OaQ^O!+j#HF4O%&q=j{Z`~jaw=mPSMNzj z#Doek2(WHWW94!PG%!V6?cr9v$@Ox{(~ql`1l!0hc63Vbvh%F~pApN@l`*!!!**7q z%%s^=L?xIv7^+Eu0*Og>`de%HHU6xE?IS%jO?}TQ?hWY|v8v>`n}Y_zunx9e+-9uT z&fRq}pkaz=gEoEDZxsD_IH%Uj>Cf)pJskj9JR*(LMXMvR;-K}G#9w}WR&182V@zHQ zj&+|geF@;TJRd9EdTzcd><%_0N)eg)s|dAcD}0l_C^!EDm}t_w>HX{3S?rY%10_q9 zlSs*+#i3Vru(D22!X*wga$wbG9dWb8Sa|a;(S(Mg?E5Ztlko{il<6PPDtQWQ1_N$6 zkFxdhHvD8$`(C%Lk3ZcW&vl-*kz=#aQ4)-9-H0A)6|Po}9qLn_pjRs~Dh@Z_fA>m3 znQ4agQ+{q=N$;Jl`#KCS7Ef4xX}Us<9|e?5WRRc{sYDwE3Z-@Rs#3)Ar|094C^Pl9 zDo!u00248m+Dv;NBaHL%gAC{20aNIO{Cn=s?RR6PMj(C>&`?|Xv;ICC6+D|k>1?gb z-8;E+rKe%O`whNvI}-U;EwQ@=zYyo{S$4B)bCc#pHMvQjnK64za#a6S&~fY8LwusZ zaJLXi!T{dKHxxszf(B2$PpL8N<)FP>Uu~;|!;5y9p{JNV8iG@}>X3{Gc*(LbMe}9%o@(gSI>DQrcaUmj zYTpG*XuSs4ep%R5e`A(wR{L{RfCxqJ8&r?9>?+mjVBFw9QG;Igwy-)Y@Xi3@#*i7cK$<#MT-vT)nS2u$15_f2(TZYAxk1@oUx|d!FoS+a@_Q>7fTu~RnY^^QmF@Hm zz!ZUdRPswLx4V==5i>h#YktRb^Q(Uj+b^}R>{v{aNn{z@`1>Y5$A15;;m82S-qr!DJrlyHry|%RKU{V!)Xguw&mH9-}pbibFv#%b-wd zpFAQE+OA$>!))o#;`L`5(tz{|YCT#$FW<%+6CyhQq{Yu_5YIto>qrDjiJc84#Qgm( zi%Y6Ymmf7$otPN_p@Z=dL;4Pi^DT@~-bT?2J_0Zfgyout$O7T^JA=hHQmLGUJ5WrK zj$1|!65=x({gbh))!?f#Oi{%Rn4fql>q8c{QXSNgi-I6FX10gXiCC7 zW!3e`b}MrWSwTR)L9v%FMj#Onx@ea8mOH1JqVzh8E%)hupPre6IRant)L54@)Flz zAVT>{-u+%%Uy)4IcCs|Qk3*SBbm>nK(Wpz)$}|JLj@r0F>X+Bt2jvoyut2LKfjy!6yLxEA@eC>gWz`DYFO<5F+M4Oa4aNR(6*FpQ*QYl{w{=SLv$r5MSvG8fv$&o_wVkh4gGC%fq&08 zXydRR-IcT?k?`&3Mc=3?!;A(8W{TH0y>j>VJ(taOAs6p@^r5yIIA0TL3EqK(UV@C< zf`AHKk1vB$HerI(sK1&X3$t0#K~5};QEc(gjrOg8)AjcO^GabDh;~zM{Xk^BQWt8@B7$CKAGH*xO-U$M^_kNw(S5@*qvGn zjSPI7f8>>NEHHbb>rhZ#9SE)}IUQ{2Wm!?IlYTr7kl$EVGhZ9H;&pY7Lc{o4ZHOt{ zk>8&v2DNl8{NbX7i!UGcbvu0|;VV{qi?Ybe`j*g8rc8JhSm$epY$6DUl=M%!qwG2oYnHiV8%U7THTmm&DY;& zVA}?G0@d)RbuH!%`%j7IjSarVGQ%3%IOizk^+qzUsqs?LKLP^-F9JT4kKcID5C=}Z zhOROlja|OtF_ewNXf(XWNi^Wv1CnXW(;+34^?W5@J$(^YMtLD(?4+ZA9*i8Xa}h;n zkWdaB)HDG*IJP2#E|Y))6KK@@omI|PZ5_T>BNt9)YaPC)@1Jyw`2)~J!AeK=rJC$4 z)1N9ar0kd|g~6mCgETE8zY~;s(}F2;a3pPZPK;W6$TrpI(PO-?*)x)yK;82IhOeWN zpE%&b&LNFEHP0DA%IbSGB|@O^X?c{yAtDLrq3?O5-Pcf<9zME!P`+L4^7U#z5~dtu zr~W;;G0_+*~At zpfS!JdzkT!?~&q-Y09Gjc#~LIM7ZbA$ZZ8U;)ZMly=&i(6ZwmTVhMPeIpzt@kM zLP4A1Z9M59VeV#~PHg^s7(KAy<(1p5w!}ozH-BH-`uF=LYrbm%KwyWlS}E+$yt0cy zaDs25JNx@)&%2K(6nm6V9fB3_ZI+dR8A^c$dNCuT;1<)^WsTyt$2t?dRUGfVESi+O zKxq{xz``QzlaHm;_(J3oI^aZGG_ywn{$|sLx2*Q26sGSWZdmVPwOpGFzbN+D8nTM) zx=|2**o*k#(zR3#+y_aTnE5=XmqiRW7}w$K#x;DJua1a7Q}<{yC5uRvAR%%$&a|EK z7T9TFLp6XJDy?WZ!4L>ZLDWi(kcVcY@o_!^i;m^m7np$|8?;xYi=QamQXvYhV9bbB zW>rse&AOGhp{D7W(fzAIPnJN>`_VWCbrct0jG-(Xte8&#Vl$g_duPxTgZi*zHzj3v zK~G0Al+kY1b3NmbBKDW7@eg$u74_&V=Y9PfsR0VH7rSk+Bwn-^7?%D7KNAC`%!S5m zx>+eJFL+4=h1b^x;^Y4JhN>;S!5X6G!Cq6rdHm4l*VbN@?~`U}VEh81u?K7$g}5{$ zo*vhpFoE)){U_y~G80GdOZbB1j5&!h-VqUE<0~|sB7K7*d+1qjPgK8e?A`iA#8Cc9wW2IujQ3N909~T`R8QR4UDUG`>E0Nt8W*3olUeV9;2zLsU~7Lu)qumx1>eWxiS;M%xB1` ztz23tBemA-=7hB!wznKwC9D!Dq<{x(a3GQ6{!lJflEXLuHZ-{K1_$v(u!$Te4d=-L z9VF^YvUvNrOE8UHd-W$eCH>uk~E%|K3~QbJY{y zH8sx68nd0B{cu%|rKz?hlT~*0tm0=jScpWOqwG5wBRc;G)pvPqEu|yr<-?xVbsfm_6+|SX_b3^CDSu}G)JX^%5OTDzPwrYFVGK%~J$l8-YKsmLrH)*7 zHenLuZwd+7i(kP)UXNM3;wll?DY1UAp)=_!c=65G^0hfKIx7SAHBD9V1D&<4bh}J& zmD9I292L{BGc5A^Zj%O@RzxOmy7^h-T9An6>ZKxkXK zODG24b0J|6>S2T0=WJq1%NFTz-mC~$EE1-RIJli+f+TNDXr_Br21n~(mJv7SILQ*+Oh#yj4b$! zO!~_$O&cwTxmQd~8sybb!I1a6+l8bDx>{_!Ea(nD6LT1>G^*YK%e9WySdb5Fh-K}u z&|Da?BnwzfE+w}QuBzeUE>g`wl1#ZVY{XM zcF}?boL2VXsU+tg$rajfaQ_`p4h&H)T)AXxepL z3!7bKVi@z$vlF@6?wvil6PQ0kaW;XzFC3c|`(bCWgT2Fy!_d*Sl6Ix<@3i30(URXj zOD9re30RP1Ior<C__u-f#+Yk z4XB|ajxnkFF^nw9iX;$}9Yw2wrOMi6^j9ae=G;_{G;N4n8bXIp^JZPL;^Lq%LhIJr zWpA@g+kHgs2{;U)n*Rci5j&tx*>;0?_IpOVc2I@7rC?Y(O_kRa3-cc5tdV&;;Q4Pa zV&)^AMXfS+PKyi>;Bo5UROSDWVI+ID^Dw;Gzwh`hpQg@;3&UZ4Y~$#c|8rK`-%7-% zBUrB(1Q>{6q2XX6Lk&g_ViJiR8<1sEC?V^II%$7>;fo`Y4<$DVS~y1S(?mijOjZf% z%>~;3US#PJa^S#v z*j-u`;w%I$o+JlxPJ;QpTuwe}hY>Q$37=d>U9{V>5mlr7-drXJ{ye@Bl^yy5Xo*=kCzPMk z9z4*8kf=;BIIYQgL@n-SioEn!nIA&~FM*HulRHCK9w!Xm1%_?bFKWO8a{rJf#X z%e%bLQgZTLUJvirnx@L-WB>l$ACc0Q5w?8|=rZRej|>I5EOQp30E|LmI#g*^3eP^1 zqyHljWsKdE@OL4`2*GJYJDlU`Rs-t7A!$?U{q7{)zXN;8`?1WV=o&li7bbW)__{BeJn=I4O?*0V2%`tb9Tv>7*~^ZyMXYwmQViEFX* z(;FB~6?l_(hsRu`d^k}-lkQ<+B{lJcZYO76&gUfwEB|PX2!hand*px|_$?RwFg6i{ z<8#ME~1Io`=WdV8c9=^p&0hUseI%Athz z_67pSa=`UFi<{wzlT&)w_tIQg5o?kkOIhIMmK5Jf6Dpo4tYm!hw_wy;QFC9&fa&k4 zLu-P+;|pINFL=tG9(?ER$EQnZTlsr&4kc13{VpgE*5

Hab~R(Q+7sh?BU)mem7`pEdCr( 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 diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts new file mode 100644 index 000000000..7a8ae5dd2 --- /dev/null +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -0,0 +1,134 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../fixtures/ComfyPage' +import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures' + +test.describe('NodeHeader', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.setSetting('Comfy.EnableTooltips', true) + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + }) + + test('displays node title', async ({ comfyPage }) => { + // Get the KSampler node from the default workflow + const nodes = await comfyPage.getNodeRefsByType('KSampler') + expect(nodes.length).toBeGreaterThanOrEqual(1) + + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + const title = await vueNode.getTitle() + expect(title).toBe('KSampler') + + // Verify title is visible in the header + const header = await vueNode.getHeader() + await expect(header).toContainText('KSampler') + }) + + test('allows title renaming', async ({ comfyPage }) => { + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Test renaming with Enter + await vueNode.setTitle('My Custom Sampler') + const newTitle = await vueNode.getTitle() + expect(newTitle).toBe('My Custom Sampler') + + // Verify the title is displayed + const header = await vueNode.getHeader() + await expect(header).toContainText('My Custom Sampler') + + // Test cancel with Escape + const titleElement = await vueNode.getTitleElement() + await titleElement.dblclick() + await comfyPage.nextFrame() + + // Type a different value but cancel + const input = (await vueNode.getHeader()).locator( + '[data-testid="node-title-input"]' + ) + await input.fill('This Should Be Cancelled') + await input.press('Escape') + await comfyPage.nextFrame() + + // Title should remain as the previously saved value + const titleAfterCancel = await vueNode.getTitle() + expect(titleAfterCancel).toBe('My Custom Sampler') + }) + + test('handles node collapsing', async ({ comfyPage }) => { + // Get the KSampler node from the default workflow + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Initially should not be collapsed + expect(await node.isCollapsed()).toBe(false) + const body = await vueNode.getBody() + await expect(body).toBeVisible() + + // Collapse the node + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(true) + + // Verify node content is hidden + const collapsedSize = await node.getSize() + await expect(body).not.toBeVisible() + + // Expand again + await vueNode.toggleCollapse() + expect(await node.isCollapsed()).toBe(false) + await expect(body).toBeVisible() + + // Size should be restored + const expandedSize = await node.getSize() + expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height) + }) + + test('shows collapse/expand icon state', async ({ comfyPage }) => { + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Check initial expanded state icon + let iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + + // Collapse and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-right') + + // Expand and check icon + await vueNode.toggleCollapse() + iconClass = await vueNode.getCollapseIconClass() + expect(iconClass).toContain('pi-chevron-down') + }) + + test('preserves title when collapsing/expanding', async ({ comfyPage }) => { + const nodes = await comfyPage.getNodeRefsByType('KSampler') + const node = nodes[0] + const vueNode = new VueNodeFixture(node, comfyPage.page) + + // Set custom title + await vueNode.setTitle('Test Sampler') + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Collapse + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Expand + await vueNode.toggleCollapse() + expect(await vueNode.getTitle()).toBe('Test Sampler') + + // Verify title is still displayed + const header = await vueNode.getHeader() + await expect(header).toContainText('Test Sampler') + }) +}) diff --git a/package.json b/package.json index c7acbf645..f7482346c 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,8 @@ "@xterm/xterm": "^5.5.0", "algoliasearch": "^5.21.0", "axios": "^1.8.2", + "chart.js": "^4.5.0", + "clsx": "^2.1.1", "dompurify": "^3.2.5", "dotenv": "^16.4.5", "es-toolkit": "^1.39.9", @@ -140,12 +142,14 @@ "primeicons": "^7.0.0", "primevue": "^4.2.5", "semver": "^7.7.2", + "tailwind-merge": "^3.3.1", "three": "^0.170.0", "tiptap-markdown": "^0.8.10", "vue": "^3.5.13", "vue-i18n": "^9.14.3", "vue-router": "^4.4.3", "vuefire": "^3.2.1", + "yjs": "^13.6.27", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f0aeb501..c0f1f7af8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,12 @@ importers: axios: specifier: ^1.8.2 version: 1.11.0 + chart.js: + specifier: ^4.5.0 + version: 4.5.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 dompurify: specifier: ^3.2.5 version: 3.2.5 @@ -131,6 +137,9 @@ importers: semver: specifier: ^7.7.2 version: 7.7.2 + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 three: specifier: ^0.170.0 version: 0.170.0 @@ -149,6 +158,9 @@ importers: vuefire: specifier: ^3.2.1 version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.2)) + yjs: + specifier: ^13.6.27 + version: 13.6.27 zod: specifier: ^3.23.8 version: 3.24.1 @@ -1707,6 +1719,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@lobehub/cli-ui@1.13.0': resolution: {integrity: sha512-7kXm84dc6yiniEFb/KRZv5H4g43n+xKTSpKSczlv54DY3tHSuZjBARyI/UDxFVgn7ezWYAIFuphzs0hSdhs6hw==} engines: {node: '>=18'} @@ -3290,6 +3305,10 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chart.js@4.5.0: + resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + engines: {pnpm: '>=8'} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -3342,6 +3361,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + code-excerpt@4.0.0: resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4540,6 +4563,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4726,6 +4752,11 @@ packages: resolution: {integrity: sha512-vzaalVBmFLnMaedq0QAsBAaXsWahzRpvnIBdBjj7y+7EKTS6lnziU2y/PsU2c6rV5qYj2B5IDw0uNJ9peXD0vw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + lib0@0.2.114: + resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} + engines: {node: '>=16'} + hasBin: true + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -6150,6 +6181,9 @@ packages: resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==} engines: {node: ^14.18.0 || >=16.0.0} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss-primeui@0.6.1: resolution: {integrity: sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==} peerDependencies: @@ -6876,6 +6910,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yjs@13.6.27: + resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -8525,6 +8563,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kurkle/color@0.3.4': {} + '@lobehub/cli-ui@1.13.0(@types/react@19.1.9)': dependencies: arr-rotate: 1.0.0 @@ -10436,6 +10476,10 @@ snapshots: charenc@0.0.2: {} + chart.js@4.5.0: + dependencies: + '@kurkle/color': 0.3.4 + check-error@2.1.1: {} chokidar@3.6.0: @@ -10485,6 +10529,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + code-excerpt@4.0.0: dependencies: convert-to-spaces: 2.0.1 @@ -11738,6 +11784,8 @@ snapshots: isexe@2.0.0: {} + isomorphic.js@0.2.5: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -11951,6 +11999,10 @@ snapshots: lex@1.7.9: {} + lib0@0.2.114: + dependencies: + isomorphic.js: 0.2.5 + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -13734,6 +13786,8 @@ snapshots: '@pkgr/core': 0.1.2 tslib: 2.8.1 + tailwind-merge@3.3.1: {} + tailwindcss-primeui@0.6.1(tailwindcss@4.1.12): dependencies: tailwindcss: 4.1.12 @@ -14445,6 +14499,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yjs@13.6.27: + dependencies: + lib0: 0.2.114 + yocto-queue@0.1.0: {} yoctocolors@2.1.1: {} diff --git a/src/assets/css/style.css b/src/assets/css/style.css index dcf8e55fe..ee6e697f0 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -7,6 +7,66 @@ @config '../../../tailwind.config.ts'; +@layer tailwind-utilities { + /* Set default values to prevent some styles from not working properly. */ + *, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(66 153 225 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; + } + + @tailwind components; + @tailwind utilities; +} + :root { --fg-color: #000; --bg-color: #fff; @@ -29,7 +89,7 @@ --content-fg: #000; --content-hover-bg: #adadad; --content-hover-fg: #000; - + /* Code styling colors for help menu*/ --code-text-color: rgba(0, 122, 255, 1); --code-bg-color: rgba(96, 165, 250, 0.2); @@ -136,6 +196,188 @@ body { border: thin solid; } +/* Shared markdown content styling for consistent rendering across components */ +.comfy-markdown-content { + /* Typography */ + font-size: 0.875rem; /* text-sm */ + line-height: 1.6; + word-wrap: break-word; +} + +/* Headings */ +.comfy-markdown-content h1 { + font-size: 22px; /* text-[22px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h1:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h2 { + font-size: 18px; /* text-[18px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h2:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h3 { + font-size: 16px; /* text-[16px] */ + font-weight: 700; /* font-bold */ + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h3:first-child { + margin-top: 0; /* first:mt-0 */ +} + +.comfy-markdown-content h4, +.comfy-markdown-content h5, +.comfy-markdown-content h6 { + margin-top: 2rem; /* mt-8 */ + margin-bottom: 1rem; /* mb-4 */ +} + +.comfy-markdown-content h4:first-child, +.comfy-markdown-content h5:first-child, +.comfy-markdown-content h6:first-child { + margin-top: 0; /* first:mt-0 */ +} + +/* Paragraphs */ +.comfy-markdown-content p { + margin: 0 0 0.5em; +} + +.comfy-markdown-content p:last-child { + margin-bottom: 0; +} + +/* First child reset */ +.comfy-markdown-content *:first-child { + margin-top: 0; /* mt-0 */ +} + +/* Lists */ +.comfy-markdown-content ul, +.comfy-markdown-content ol { + padding-left: 2rem; /* pl-8 */ + margin: 0.5rem 0; /* my-2 */ +} + +/* Nested lists */ +.comfy-markdown-content ul ul, +.comfy-markdown-content ol ol, +.comfy-markdown-content ul ol, +.comfy-markdown-content ol ul { + padding-left: 1.5rem; /* pl-6 */ + margin: 0.5rem 0; /* my-2 */ +} + +.comfy-markdown-content li { + margin: 0.5rem 0; /* my-2 */ +} + +/* Code */ +.comfy-markdown-content code { + color: var(--code-text-color); + background-color: var(--code-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 0.125rem 0.375rem; /* px-1.5 py-0.5 */ + font-family: monospace; +} + +.comfy-markdown-content pre { + background-color: var(--code-block-bg-color); + border-radius: 0.25rem; /* rounded */ + padding: 1rem; /* p-4 */ + margin: 1rem 0; /* my-4 */ + overflow-x: auto; /* overflow-x-auto */ +} + +.comfy-markdown-content pre code { + background-color: transparent; /* bg-transparent */ + padding: 0; /* p-0 */ + color: var(--p-text-color); +} + +/* Tables */ +.comfy-markdown-content table { + width: 100%; /* w-full */ + border-collapse: collapse; /* border-collapse */ +} + +.comfy-markdown-content th, +.comfy-markdown-content td { + padding: 0.5rem; /* px-2 py-2 */ +} + +.comfy-markdown-content th { + color: var(--fg-color); +} + +.comfy-markdown-content td { + color: var(--drag-text); +} + +.comfy-markdown-content tr { + border-bottom: 1px solid var(--content-bg); +} + +.comfy-markdown-content tr:last-child { + border-bottom: none; +} + +.comfy-markdown-content thead { + border-bottom: 1px solid var(--p-text-color); +} + +/* Links */ +.comfy-markdown-content a { + color: var(--drag-text); + text-decoration: underline; +} + +/* Media */ +.comfy-markdown-content img, +.comfy-markdown-content video { + max-width: 100%; /* max-w-full */ + height: auto; /* h-auto */ + display: block; /* block */ + margin-bottom: 1rem; /* mb-4 */ +} + +/* Blockquotes */ +.comfy-markdown-content blockquote { + border-left: 3px solid var(--p-primary-color, var(--primary-bg)); + padding-left: 0.75em; + margin: 0.5em 0; + opacity: 0.8; +} + +/* Horizontal rule */ +.comfy-markdown-content hr { + border: none; + border-top: 1px solid var(--p-border-color, var(--border-color)); + margin: 1em 0; +} + +/* Strong and emphasis */ +.comfy-markdown-content strong { + font-weight: bold; +} + +.comfy-markdown-content em { + font-style: italic; +} + .comfy-modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ @@ -641,3 +883,92 @@ audio.comfy-audio.empty-audio-widget { width: calc(100vw - env(titlebar-area-width, 100vw)); } /* End of [Desktop] Electron window specific styles */ + +/* Vue Node LOD (Level of Detail) System */ +/* These classes control rendering detail based on zoom level */ + +/* Minimal LOD (zoom <= 0.4) - Title only for performance */ +.lg-node--lod-minimal { + min-height: 32px; + transition: min-height 0.2s ease; + /* Performance optimizations */ + text-shadow: none; + backdrop-filter: none; +} + +.lg-node--lod-minimal .lg-node-body { + display: none !important; +} + +/* Reduced LOD (0.4 < zoom <= 0.8) - Essential widgets, simplified styling */ +.lg-node--lod-reduced { + transition: opacity 0.1s ease; + /* Performance optimizations */ + text-shadow: none; +} + +.lg-node--lod-reduced .lg-widget-label, +.lg-node--lod-reduced .lg-slot-label { + display: none; +} + +.lg-node--lod-reduced .lg-slot { + opacity: 0.6; + font-size: 0.75rem; +} + +.lg-node--lod-reduced .lg-widget { + margin: 2px 0; + font-size: 0.875rem; +} + +/* Full LOD (zoom > 0.8) - Complete detail rendering */ +.lg-node--lod-full { + /* Uses default styling - no overrides needed */ +} + +/* Smooth transitions between LOD levels */ +.lg-node { + transition: min-height 0.2s ease; + /* Disable text selection on all nodes */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.lg-node .lg-slot, +.lg-node .lg-widget { + transition: opacity 0.1s ease, font-size 0.1s ease; +} + +/* Performance optimization during canvas interaction */ +.transform-pane--interacting .lg-node * { + transition: none !important; +} + +.transform-pane--interacting .lg-node { + will-change: transform; +} + +/* Global performance optimizations for LOD */ +.lg-node--lod-minimal, +.lg-node--lod-reduced { + /* Remove ALL expensive paint effects */ + box-shadow: none !important; + filter: none !important; + backdrop-filter: none !important; + text-shadow: none !important; + -webkit-mask-image: none !important; + mask-image: none !important; + clip-path: none !important; +} + +/* Reduce paint complexity for minimal LOD */ +.lg-node--lod-minimal { + /* Skip complex borders */ + border-radius: 0 !important; + /* Use solid colors only */ + background-image: none !important; +} + diff --git a/src/components/common/EditableText.spec.ts b/src/components/common/EditableText.spec.ts index 2e7b036b5..2d31123b9 100644 --- a/src/components/common/EditableText.spec.ts +++ b/src/components/common/EditableText.spec.ts @@ -68,4 +68,73 @@ describe('EditableText', () => { // @ts-expect-error fixme ts strict error expect(wrapper.emitted('edit')[0]).toEqual(['Test Text']) }) + + it('cancels editing on escape key', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Should emit cancel event + expect(wrapper.emitted('cancel')).toBeTruthy() + + // Should NOT emit edit event + expect(wrapper.emitted('edit')).toBeFalsy() + + // Input value should be reset to original + expect(wrapper.findComponent(InputText).props()['modelValue']).toBe( + 'Original Text' + ) + }) + + it('does not save changes when escape is pressed and blur occurs', async () => { + const wrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + + // Change the input value + await wrapper.findComponent(InputText).setValue('Modified Text') + + // Press escape (which triggers blur internally) + await wrapper.findComponent(InputText).trigger('keyup.escape') + + // Manually trigger blur to simulate the blur that happens after escape + await wrapper.findComponent(InputText).trigger('blur') + + // Should emit cancel but not edit + expect(wrapper.emitted('cancel')).toBeTruthy() + expect(wrapper.emitted('edit')).toBeFalsy() + }) + + it('saves changes on enter but not on escape', async () => { + // Test Enter key saves changes + const enterWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await enterWrapper.findComponent(InputText).setValue('Saved Text') + await enterWrapper.findComponent(InputText).trigger('keyup.enter') + // Trigger blur that happens after enter + await enterWrapper.findComponent(InputText).trigger('blur') + expect(enterWrapper.emitted('edit')).toBeTruthy() + // @ts-expect-error fixme ts strict error + expect(enterWrapper.emitted('edit')[0]).toEqual(['Saved Text']) + + // Test Escape key cancels changes with a fresh wrapper + const escapeWrapper = mountComponent({ + modelValue: 'Original Text', + isEditing: true + }) + await escapeWrapper.findComponent(InputText).setValue('Cancelled Text') + await escapeWrapper.findComponent(InputText).trigger('keyup.escape') + expect(escapeWrapper.emitted('cancel')).toBeTruthy() + expect(escapeWrapper.emitted('edit')).toBeFalsy() + }) }) diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue index 16510d3fd..c6fa18a8d 100644 --- a/src/components/common/EditableText.vue +++ b/src/components/common/EditableText.vue @@ -14,10 +14,12 @@ fluid :pt="{ root: { - onBlur: finishEditing + onBlur: finishEditing, + ...inputAttrs } }" @keyup.enter="blurInputElement" + @keyup.escape="cancelEditing" @click.stop />

@@ -27,21 +29,41 @@ import InputText from 'primevue/inputtext' import { nextTick, ref, watch } from 'vue' -const { modelValue, isEditing = false } = defineProps<{ +const { + modelValue, + isEditing = false, + inputAttrs = {} +} = defineProps<{ modelValue: string isEditing?: boolean + inputAttrs?: Record }>() -const emit = defineEmits(['update:modelValue', 'edit']) +const emit = defineEmits(['update:modelValue', 'edit', 'cancel']) const inputValue = ref(modelValue) const inputRef = ref | undefined>() +const isCanceling = ref(false) const blurInputElement = () => { // @ts-expect-error - $el is an internal property of the InputText component inputRef.value?.$el.blur() } const finishEditing = () => { - emit('edit', inputValue.value) + // Don't save if we're canceling + if (!isCanceling.value) { + emit('edit', inputValue.value) + } + isCanceling.value = false +} +const cancelEditing = () => { + // Set canceling flag to prevent blur from saving + isCanceling.value = true + // Reset to original value + inputValue.value = modelValue + // Emit cancel event + emit('cancel') + // Blur the input to exit edit mode + blurInputElement() } watch( () => isEditing, diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index b8b8baa4f..467da7e5d 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -31,6 +31,35 @@ class="w-full h-full touch-none" /> + + + + + + @@ -39,13 +68,22 @@ diff --git a/src/components/graph/TransformPane.spec.ts b/src/components/graph/TransformPane.spec.ts new file mode 100644 index 000000000..acfa172ee --- /dev/null +++ b/src/components/graph/TransformPane.spec.ts @@ -0,0 +1,350 @@ +import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick, ref } from 'vue' + +import TransformPane from './TransformPane.vue' + +// Mock the transform state composable +const mockTransformState = { + camera: ref({ x: 0, y: 0, z: 1 }), + transformStyle: ref({ + transform: 'scale(1) translate(0px, 0px)', + transformOrigin: '0 0' + }), + syncWithCanvas: vi.fn(), + canvasToScreen: vi.fn(), + screenToCanvas: vi.fn(), + isNodeInViewport: vi.fn() +} + +vi.mock('@/composables/element/useTransformState', () => ({ + useTransformState: () => mockTransformState +})) + +// Mock requestAnimationFrame/cancelAnimationFrame +global.requestAnimationFrame = vi.fn((cb) => { + setTimeout(cb, 16) + return 1 +}) +global.cancelAnimationFrame = vi.fn() + +describe('TransformPane', () => { + let wrapper: ReturnType + let mockCanvas: any + + beforeEach(() => { + vi.clearAllMocks() + + // Create mock canvas with LiteGraph interface + mockCanvas = { + canvas: { + addEventListener: vi.fn(), + removeEventListener: vi.fn() + }, + ds: { + offset: [0, 0], + scale: 1 + } + } + + // Reset mock transform state + mockTransformState.camera.value = { x: 0, y: 0, z: 1 } + mockTransformState.transformStyle.value = { + transform: 'scale(1) translate(0px, 0px)', + transformOrigin: '0 0' + } + }) + + afterEach(() => { + if (wrapper) { + wrapper.unmount() + } + }) + + describe('component mounting', () => { + it('should mount successfully with minimal props', () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('.transform-pane').exists()).toBe(true) + }) + + it('should apply transform style from composable', () => { + mockTransformState.transformStyle.value = { + transform: 'scale(2) translate(100px, 50px)', + transformOrigin: '0 0' + } + + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + const transformPane = wrapper.find('.transform-pane') + const style = transformPane.attributes('style') + expect(style).toContain('transform: scale(2) translate(100px, 50px)') + }) + + it('should render slot content', () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + }, + slots: { + default: '
Test Node
' + } + }) + + expect(wrapper.find('.test-content').exists()).toBe(true) + expect(wrapper.find('.test-content').text()).toBe('Test Node') + }) + }) + + describe('RAF synchronization', () => { + it('should start RAF sync on mount', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + + // Should emit RAF status change to true + expect(wrapper.emitted('rafStatusChange')).toBeTruthy() + expect(wrapper.emitted('rafStatusChange')?.[0]).toEqual([true]) + }) + + it('should call syncWithCanvas during RAF updates', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + + // Allow RAF to execute + await new Promise((resolve) => setTimeout(resolve, 20)) + + expect(mockTransformState.syncWithCanvas).toHaveBeenCalledWith(mockCanvas) + }) + + it('should emit transform update timing', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + + // Allow RAF to execute + await new Promise((resolve) => setTimeout(resolve, 20)) + + expect(wrapper.emitted('transformUpdate')).toBeTruthy() + const updateEvent = wrapper.emitted('transformUpdate')?.[0] + expect(typeof updateEvent?.[0]).toBe('number') + expect(updateEvent?.[0]).toBeGreaterThanOrEqual(0) + }) + + it('should stop RAF sync on unmount', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + wrapper.unmount() + + expect(wrapper.emitted('rafStatusChange')).toBeTruthy() + const events = wrapper.emitted('rafStatusChange') as any[] + expect(events[events.length - 1]).toEqual([false]) + expect(global.cancelAnimationFrame).toHaveBeenCalled() + }) + }) + + describe('canvas event listeners', () => { + it('should add event listeners to canvas on mount', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + + expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( + 'pointerdown', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( + 'pointerup', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( + 'pointercancel', + expect.any(Function), + expect.any(Object) + ) + }) + + it('should remove event listeners on unmount', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + await nextTick() + wrapper.unmount() + + expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( + 'wheel', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( + 'pointerdown', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( + 'pointerup', + expect.any(Function), + expect.any(Object) + ) + expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( + 'pointercancel', + expect.any(Function), + expect.any(Object) + ) + }) + }) + + describe('interaction state management', () => { + it('should apply interacting class during interactions', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + // Simulate interaction start by checking internal state + // Note: This tests the CSS class application logic + const transformPane = wrapper.find('.transform-pane') + + // Initially should not have interacting class + expect(transformPane.classes()).not.toContain( + 'transform-pane--interacting' + ) + }) + + it('should handle pointer events for node delegation', async () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + const transformPane = wrapper.find('.transform-pane') + + // Simulate pointer down - we can't test the exact delegation logic + // in unit tests due to vue-test-utils limitations, but we can verify + // the event handler is set up correctly + await transformPane.trigger('pointerdown') + + // The test passes if no errors are thrown during event handling + expect(transformPane.exists()).toBe(true) + }) + }) + + describe('transform state integration', () => { + it('should provide transform utilities to child components', () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + // The component should provide transform state via Vue's provide/inject + // This is tested indirectly through the composable integration + expect(mockTransformState.syncWithCanvas).toBeDefined() + expect(mockTransformState.canvasToScreen).toBeDefined() + expect(mockTransformState.screenToCanvas).toBeDefined() + }) + }) + + describe('error handling', () => { + it('should handle null canvas gracefully', () => { + wrapper = mount(TransformPane, { + props: { + canvas: undefined + } + }) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('.transform-pane').exists()).toBe(true) + }) + + it('should handle missing canvas properties', () => { + const incompleteCanvas = {} as any + + wrapper = mount(TransformPane, { + props: { + canvas: incompleteCanvas + } + }) + + expect(wrapper.exists()).toBe(true) + // Should not throw errors during mount + }) + }) + + describe('performance optimizations', () => { + it('should use contain CSS property for layout optimization', () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + } + }) + + const transformPane = wrapper.find('.transform-pane') + + // This test verifies the CSS contains the performance optimization + // Note: In JSDOM, computed styles might not reflect all CSS properties + expect(transformPane.element.className).toContain('transform-pane') + }) + + it('should disable pointer events on container but allow on children', () => { + wrapper = mount(TransformPane, { + props: { + canvas: mockCanvas + }, + slots: { + default: '
Test Node
' + } + }) + + const transformPane = wrapper.find('.transform-pane') + + // The CSS should handle pointer events optimization + // This is primarily a CSS concern, but we verify the structure + expect(transformPane.exists()).toBe(true) + expect(wrapper.find('[data-node-id="test"]').exists()).toBe(true) + }) + }) +}) diff --git a/src/components/graph/TransformPane.vue b/src/components/graph/TransformPane.vue new file mode 100644 index 000000000..266dd0569 --- /dev/null +++ b/src/components/graph/TransformPane.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/composables/element/useTransformState.ts b/src/composables/element/useTransformState.ts new file mode 100644 index 000000000..5192bd6df --- /dev/null +++ b/src/composables/element/useTransformState.ts @@ -0,0 +1,242 @@ +/** + * Composable for managing transform state synchronized with LiteGraph canvas + * + * This composable is a critical part of the hybrid rendering architecture that + * allows Vue components to render in perfect alignment with LiteGraph's canvas. + * + * ## Core Concept + * + * LiteGraph uses a canvas for rendering connections, grid, and handling interactions. + * Vue components need to render nodes on top of this canvas. The challenge is + * synchronizing the coordinate systems: + * + * - LiteGraph: Uses canvas coordinates with its own transform matrix + * - Vue/DOM: Uses screen coordinates with CSS transforms + * + * ## Solution: Transform Container Pattern + * + * Instead of transforming individual nodes (O(n) complexity), we: + * 1. Mirror LiteGraph's transform matrix to a single CSS container + * 2. Place all Vue nodes as children with simple absolute positioning + * 3. Achieve O(1) transform updates regardless of node count + * + * ## Coordinate Systems + * + * - **Canvas coordinates**: LiteGraph's internal coordinate system + * - **Screen coordinates**: Browser's viewport coordinate system + * - **Transform sync**: camera.x/y/z mirrors canvas.ds.offset/scale + * + * ## Performance Benefits + * + * - GPU acceleration via CSS transforms + * - No layout thrashing (only transform changes) + * - Efficient viewport culling calculations + * - Scales to 1000+ nodes while maintaining 60 FPS + * + * @example + * ```typescript + * const { camera, transformStyle, canvasToScreen } = useTransformState() + * + * // In template + *
+ * + *
+ * + * // Convert coordinates + * const screenPos = canvasToScreen({ x: nodeX, y: nodeY }) + * ``` + */ +import { computed, reactive, readonly } from 'vue' + +import type { LGraphCanvas } from '../../lib/litegraph/src/litegraph' + +export interface Point { + x: number + y: number +} + +export interface Camera { + x: number + y: number + z: number // scale/zoom +} + +export const useTransformState = () => { + // Reactive state mirroring LiteGraph's canvas transform + const camera = reactive({ + x: 0, + y: 0, + z: 1 + }) + + // Computed transform string for CSS + const transformStyle = computed(() => ({ + transform: `scale(${camera.z}) translate(${camera.x}px, ${camera.y}px)`, + transformOrigin: '0 0' + })) + + /** + * Synchronizes Vue's reactive camera state with LiteGraph's canvas transform + * + * Called every frame via RAF to ensure Vue components stay aligned with canvas. + * This is the heart of the hybrid rendering system - it bridges the gap between + * LiteGraph's canvas transforms and Vue's reactive system. + * + * @param canvas - LiteGraph canvas instance with DragAndScale (ds) transform state + */ + const syncWithCanvas = (canvas: LGraphCanvas) => { + if (!canvas || !canvas.ds) return + + // Mirror LiteGraph's transform state to Vue's reactive state + // ds.offset = pan offset, ds.scale = zoom level + camera.x = canvas.ds.offset[0] + camera.y = canvas.ds.offset[1] + camera.z = canvas.ds.scale || 1 + } + + /** + * Converts canvas coordinates to screen coordinates + * + * Applies the same transform that LiteGraph uses for rendering. + * Essential for positioning Vue components to align with canvas elements. + * + * Formula: screen = canvas * scale + offset + * + * @param point - Point in canvas coordinate system + * @returns Point in screen coordinate system + */ + const canvasToScreen = (point: Point): Point => { + return { + x: point.x * camera.z + camera.x, + y: point.y * camera.z + camera.y + } + } + + /** + * Converts screen coordinates to canvas coordinates + * + * Inverse of canvasToScreen. Useful for hit testing and converting + * mouse events back to canvas space. + * + * Formula: canvas = (screen - offset) / scale + * + * @param point - Point in screen coordinate system + * @returns Point in canvas coordinate system + */ + const screenToCanvas = (point: Point): Point => { + return { + x: (point.x - camera.x) / camera.z, + y: (point.y - camera.y) / camera.z + } + } + + // Get node's screen bounds for culling + const getNodeScreenBounds = ( + pos: ArrayLike, + size: ArrayLike + ): DOMRect => { + const topLeft = canvasToScreen({ x: pos[0], y: pos[1] }) + const width = size[0] * camera.z + const height = size[1] * camera.z + + return new DOMRect(topLeft.x, topLeft.y, width, height) + } + + // Helper: Calculate zoom-adjusted margin for viewport culling + const calculateAdjustedMargin = (baseMargin: number): number => { + if (camera.z < 0.1) return Math.min(baseMargin * 5, 2.0) + if (camera.z > 3.0) return Math.max(baseMargin * 0.5, 0.05) + return baseMargin + } + + // Helper: Check if node is too small to be visible at current zoom + const isNodeTooSmall = (nodeSize: ArrayLike): boolean => { + const nodeScreenSize = Math.max(nodeSize[0], nodeSize[1]) * camera.z + return nodeScreenSize < 4 + } + + // Helper: Calculate expanded viewport bounds with margin + const getExpandedViewportBounds = ( + viewport: { width: number; height: number }, + margin: number + ) => { + const marginX = viewport.width * margin + const marginY = viewport.height * margin + return { + left: -marginX, + right: viewport.width + marginX, + top: -marginY, + bottom: viewport.height + marginY + } + } + + // Helper: Test if node intersects with viewport bounds + const testViewportIntersection = ( + screenPos: { x: number; y: number }, + nodeSize: ArrayLike, + bounds: { left: number; right: number; top: number; bottom: number } + ): boolean => { + const nodeRight = screenPos.x + nodeSize[0] * camera.z + const nodeBottom = screenPos.y + nodeSize[1] * camera.z + + return !( + nodeRight < bounds.left || + screenPos.x > bounds.right || + nodeBottom < bounds.top || + screenPos.y > bounds.bottom + ) + } + + // Check if node is within viewport with frustum and size-based culling + const isNodeInViewport = ( + nodePos: ArrayLike, + nodeSize: ArrayLike, + viewport: { width: number; height: number }, + margin: number = 0.2 + ): boolean => { + // Early exit for tiny nodes + if (isNodeTooSmall(nodeSize)) return false + + const screenPos = canvasToScreen({ x: nodePos[0], y: nodePos[1] }) + const adjustedMargin = calculateAdjustedMargin(margin) + const bounds = getExpandedViewportBounds(viewport, adjustedMargin) + + return testViewportIntersection(screenPos, nodeSize, bounds) + } + + // Get viewport bounds in canvas coordinates (for spatial index queries) + const getViewportBounds = ( + viewport: { width: number; height: number }, + margin: number = 0.2 + ) => { + const marginX = viewport.width * margin + const marginY = viewport.height * margin + + const topLeft = screenToCanvas({ x: -marginX, y: -marginY }) + const bottomRight = screenToCanvas({ + x: viewport.width + marginX, + y: viewport.height + marginY + }) + + return { + x: topLeft.x, + y: topLeft.y, + width: bottomRight.x - topLeft.x, + height: bottomRight.y - topLeft.y + } + } + + return { + camera: readonly(camera), + transformStyle, + syncWithCanvas, + canvasToScreen, + screenToCanvas, + getNodeScreenBounds, + isNodeInViewport, + getViewportBounds + } +} diff --git a/src/composables/graph/useCanvasTransformSync.ts b/src/composables/graph/useCanvasTransformSync.ts new file mode 100644 index 000000000..3e382492b --- /dev/null +++ b/src/composables/graph/useCanvasTransformSync.ts @@ -0,0 +1,115 @@ +import { onUnmounted, ref } from 'vue' + +import type { LGraphCanvas } from '../../lib/litegraph/src/litegraph' + +export interface CanvasTransformSyncOptions { + /** + * Whether to automatically start syncing when canvas is available + * @default true + */ + autoStart?: boolean +} + +export interface CanvasTransformSyncCallbacks { + /** + * Called when sync starts + */ + onStart?: () => void + /** + * Called after each sync update with timing information + */ + onUpdate?: (duration: number) => void + /** + * Called when sync stops + */ + onStop?: () => void +} + +/** + * Manages requestAnimationFrame-based synchronization with LiteGraph canvas transforms. + * + * This composable provides a clean way to sync Vue transform state with LiteGraph canvas + * on every frame. It handles RAF lifecycle management, provides performance timing, + * and ensures proper cleanup. + * + * The sync function typically reads canvas.ds (draw state) properties like offset and scale + * to keep Vue components aligned with the canvas coordinate system. + * + * @example + * ```ts + * const { isActive, startSync, stopSync } = useCanvasTransformSync( + * canvas, + * (canvas) => syncWithCanvas(canvas), + * { + * onStart: () => emit('rafStatusChange', true), + * onUpdate: (time) => emit('transformUpdate', time), + * onStop: () => emit('rafStatusChange', false) + * } + * ) + * ``` + */ +export function useCanvasTransformSync( + canvas: LGraphCanvas | undefined | null, + syncFn: (canvas: LGraphCanvas) => void, + callbacks: CanvasTransformSyncCallbacks = {}, + options: CanvasTransformSyncOptions = {} +) { + const { autoStart = true } = options + const { onStart, onUpdate, onStop } = callbacks + + const isActive = ref(false) + let rafId: number | null = null + + const startSync = () => { + if (isActive.value || !canvas) return + + isActive.value = true + onStart?.() + + const sync = () => { + if (!isActive.value || !canvas) return + + try { + const startTime = performance.now() + syncFn(canvas) + const endTime = performance.now() + + onUpdate?.(endTime - startTime) + } catch (error) { + console.warn('Canvas transform sync error:', error) + } + + rafId = requestAnimationFrame(sync) + } + + sync() + } + + const stopSync = () => { + if (!isActive.value) return + + if (rafId !== null) { + cancelAnimationFrame(rafId) + rafId = null + } + + isActive.value = false + onStop?.() + } + + // Auto-start if canvas is available and autoStart is enabled + if (autoStart && canvas) { + startSync() + } + + // Clean up on unmount + onUnmounted(() => { + stopSync() + }) + + return { + isActive, + startSync, + stopSync + } +} diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts new file mode 100644 index 000000000..a5079c453 --- /dev/null +++ b/src/composables/graph/useGraphNodeManager.ts @@ -0,0 +1,813 @@ +/** + * Vue node lifecycle management for LiteGraph integration + * Provides event-driven reactivity with performance optimizations + */ +import { nextTick, reactive, readonly } from 'vue' + +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' +import type { WidgetValue } from '@/types/simplifiedWidget' +import type { SpatialIndexDebugInfo } from '@/types/spatialIndex' + +import type { LGraph, LGraphNode } from '../../lib/litegraph/src/litegraph' +import { type Bounds, QuadTree } from '../../utils/spatial/QuadTree' + +export interface NodeState { + visible: boolean + dirty: boolean + lastUpdate: number + culled: boolean +} + +export interface NodeMetadata { + lastRenderTime: number + cachedBounds: DOMRect | null + lodLevel: 'high' | 'medium' | 'low' + spatialIndex?: QuadTree +} + +export interface PerformanceMetrics { + fps: number + frameTime: number + updateTime: number + nodeCount: number + culledCount: number + callbackUpdateCount: number + rafUpdateCount: number + adaptiveQuality: boolean +} + +export interface SafeWidgetData { + name: string + type: string + value: WidgetValue + options?: Record + callback?: ((value: unknown) => void) | undefined +} + +export interface VueNodeData { + id: string + title: string + type: string + mode: number + selected: boolean + executing: boolean + widgets?: SafeWidgetData[] + inputs?: unknown[] + outputs?: unknown[] + flags?: { + collapsed?: boolean + } +} + +export interface SpatialMetrics { + queryTime: number + nodesInIndex: number +} + +export interface GraphNodeManager { + // Reactive state - safe data extracted from LiteGraph nodes + vueNodeData: ReadonlyMap + nodeState: ReadonlyMap + nodePositions: ReadonlyMap + nodeSizes: ReadonlyMap + + // Access to original LiteGraph nodes (non-reactive) + getNode(id: string): LGraphNode | undefined + + // Lifecycle methods + setupEventListeners(): () => void + cleanup(): void + + // Update methods + scheduleUpdate( + nodeId?: string, + priority?: 'critical' | 'normal' | 'low' + ): void + forceSync(): void + detectChangesInRAF(): void + + // Spatial queries + getVisibleNodeIds(viewportBounds: Bounds): Set + + // Performance + performanceMetrics: PerformanceMetrics + spatialMetrics: SpatialMetrics + + // Debug + getSpatialIndexDebugInfo(): SpatialIndexDebugInfo | null +} + +export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => { + // Get layout mutations composable + const { moveNode, resizeNode, createNode, deleteNode, setSource } = + useLayoutMutations() + // Safe reactive data extracted from LiteGraph nodes + const vueNodeData = reactive(new Map()) + const nodeState = reactive(new Map()) + const nodePositions = reactive(new Map()) + const nodeSizes = reactive( + new Map() + ) + + // Non-reactive storage for original LiteGraph nodes + const nodeRefs = new Map() + + // WeakMap for heavy data that auto-GCs when nodes are removed + const nodeMetadata = new WeakMap() + + // Performance tracking + const performanceMetrics = reactive({ + fps: 0, + frameTime: 0, + updateTime: 0, + nodeCount: 0, + culledCount: 0, + callbackUpdateCount: 0, + rafUpdateCount: 0, + adaptiveQuality: false + }) + + // Spatial indexing using QuadTree + const spatialIndex = new QuadTree( + { x: -10000, y: -10000, width: 20000, height: 20000 }, + { maxDepth: 6, maxItemsPerNode: 4 } + ) + let lastSpatialQueryTime = 0 + + // Spatial metrics + const spatialMetrics = reactive({ + queryTime: 0, + nodesInIndex: 0 + }) + + // Update batching + const pendingUpdates = new Set() + const criticalUpdates = new Set() + const lowPriorityUpdates = new Set() + let updateScheduled = false + let batchTimeoutId: number | null = null + + // Change detection state + const lastNodesSnapshot = new Map< + string, + { pos: [number, number]; size: [number, number] } + >() + + const attachMetadata = (node: LGraphNode) => { + nodeMetadata.set(node, { + lastRenderTime: performance.now(), + cachedBounds: null, + lodLevel: 'high', + spatialIndex: undefined + }) + } + + // Extract safe data from LiteGraph node for Vue consumption + const extractVueNodeData = (node: LGraphNode): VueNodeData => { + // Extract safe widget data + const safeWidgets = node.widgets?.map((widget) => { + try { + // TODO: Use widget.getReactiveData() once TypeScript types are updated + let value = widget.value + + // For combo widgets, if value is undefined, use the first option as default + if ( + value === undefined && + widget.type === 'combo' && + widget.options?.values && + Array.isArray(widget.options.values) && + widget.options.values.length > 0 + ) { + value = widget.options.values[0] + } + + return { + name: widget.name, + type: widget.type, + value: value, + options: widget.options ? { ...widget.options } : undefined, + callback: widget.callback + } + } catch (error) { + return { + name: widget.name || 'unknown', + type: widget.type || 'text', + value: undefined, // Already a valid WidgetValue + options: undefined, + callback: undefined + } + } + }) + + return { + id: String(node.id), + title: node.title || 'Untitled', + type: node.type || 'Unknown', + mode: node.mode || 0, + selected: node.selected || false, + executing: false, // Will be updated separately based on execution state + widgets: safeWidgets, + inputs: node.inputs ? [...node.inputs] : undefined, + outputs: node.outputs ? [...node.outputs] : undefined, + flags: node.flags ? { ...node.flags } : undefined + } + } + + // Get access to original LiteGraph node (non-reactive) + const getNode = (id: string): LGraphNode | undefined => { + return nodeRefs.get(id) + } + + /** + * Validates that a value is a valid WidgetValue type + */ + const validateWidgetValue = (value: unknown): WidgetValue => { + if (value === null || value === undefined || value === void 0) { + return undefined + } + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value + } + if (typeof value === 'object') { + // Check if it's a File array + if (Array.isArray(value) && value.every((item) => item instanceof File)) { + return value as File[] + } + // Otherwise it's a generic object + return value as object + } + // If none of the above, return undefined + console.warn(`Invalid widget value type: ${typeof value}`, value) + return undefined + } + + /** + * Updates Vue state when widget values change + */ + const updateVueWidgetState = ( + nodeId: string, + widgetName: string, + value: unknown + ): void => { + try { + const currentData = vueNodeData.get(nodeId) + if (!currentData?.widgets) return + + const updatedWidgets = currentData.widgets.map((w) => + w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w + ) + vueNodeData.set(nodeId, { + ...currentData, + widgets: updatedWidgets + }) + performanceMetrics.callbackUpdateCount++ + } catch (error) { + // Ignore widget update errors to prevent cascade failures + } + } + + /** + * Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync + */ + const createWrappedWidgetCallback = ( + widget: { value?: unknown; name: string }, // LiteGraph widget with minimal typing + originalCallback: ((value: unknown) => void) | undefined, + nodeId: string + ) => { + let updateInProgress = false + + return (value: unknown) => { + if (updateInProgress) return + updateInProgress = true + + try { + // 1. Update the widget value in LiteGraph (critical for LiteGraph state) + // Validate that the value is of an acceptable type + if ( + value !== null && + value !== undefined && + typeof value !== 'string' && + typeof value !== 'number' && + typeof value !== 'boolean' && + typeof value !== 'object' + ) { + console.warn(`Invalid widget value type: ${typeof value}`) + updateInProgress = false + return + } + + // Always update widget.value to ensure sync + widget.value = value + + // 2. Call the original callback if it exists + if (originalCallback) { + originalCallback.call(widget, value) + } + + // 3. Update Vue state to maintain synchronization + updateVueWidgetState(nodeId, widget.name, value) + } finally { + updateInProgress = false + } + } + } + + /** + * Sets up widget callbacks for a node - now with reduced nesting + */ + const setupNodeWidgetCallbacks = (node: LGraphNode) => { + if (!node.widgets) return + + const nodeId = String(node.id) + + node.widgets.forEach((widget) => { + const originalCallback = widget.callback + widget.callback = createWrappedWidgetCallback( + widget, + originalCallback, + nodeId + ) + }) + } + + // Uncomment when needed for future features + // const getNodeMetadata = (node: LGraphNode): NodeMetadata => { + // let metadata = nodeMetadata.get(node) + // if (!metadata) { + // attachMetadata(node) + // metadata = nodeMetadata.get(node)! + // } + // return metadata + // } + + const scheduleUpdate = ( + nodeId?: string, + priority: 'critical' | 'normal' | 'low' = 'normal' + ) => { + if (nodeId) { + const state = nodeState.get(nodeId) + if (state) state.dirty = true + + // Priority queuing + if (priority === 'critical') { + criticalUpdates.add(nodeId) + flush() // Immediate flush for critical updates + return + } else if (priority === 'low') { + lowPriorityUpdates.add(nodeId) + } else { + pendingUpdates.add(nodeId) + } + } + + if (!updateScheduled) { + updateScheduled = true + + // Adaptive batching strategy + if (pendingUpdates.size > 10) { + // Many updates - batch in nextTick + void nextTick(() => flush()) + } else { + // Few updates - small delay for more batching + batchTimeoutId = window.setTimeout(() => flush(), 4) + } + } + } + + const flush = () => { + const startTime = performance.now() + + if (batchTimeoutId !== null) { + clearTimeout(batchTimeoutId) + batchTimeoutId = null + } + + // Clear all pending updates + criticalUpdates.clear() + pendingUpdates.clear() + lowPriorityUpdates.clear() + updateScheduled = false + + // Sync with graph state + syncWithGraph() + + const endTime = performance.now() + performanceMetrics.updateTime = endTime - startTime + } + + const syncWithGraph = () => { + if (!graph?._nodes) return + + const currentNodes = new Set(graph._nodes.map((n) => String(n.id))) + + // Remove deleted nodes + for (const id of Array.from(vueNodeData.keys())) { + if (!currentNodes.has(id)) { + nodeRefs.delete(id) + vueNodeData.delete(id) + nodeState.delete(id) + nodePositions.delete(id) + nodeSizes.delete(id) + lastNodesSnapshot.delete(id) + spatialIndex.remove(id) + } + } + + // Add/update existing nodes + graph._nodes.forEach((node) => { + const id = String(node.id) + + // Store non-reactive reference + nodeRefs.set(id, node) + + // Set up widget callbacks BEFORE extracting data (critical order) + setupNodeWidgetCallbacks(node) + + // Extract and store safe data for Vue + vueNodeData.set(id, extractVueNodeData(node)) + + if (!nodeState.has(id)) { + nodeState.set(id, { + visible: true, + dirty: false, + lastUpdate: performance.now(), + culled: false + }) + nodePositions.set(id, { x: node.pos[0], y: node.pos[1] }) + nodeSizes.set(id, { width: node.size[0], height: node.size[1] }) + attachMetadata(node) + + // Add to spatial index + const bounds: Bounds = { + x: node.pos[0], + y: node.pos[1], + width: node.size[0], + height: node.size[1] + } + spatialIndex.insert(id, bounds, id) + } + }) + + // Update performance metrics + performanceMetrics.nodeCount = vueNodeData.size + performanceMetrics.culledCount = Array.from(nodeState.values()).filter( + (s) => s.culled + ).length + } + + // Most performant: Direct position sync without re-setting entire node + // Query visible nodes using QuadTree spatial index + const getVisibleNodeIds = (viewportBounds: Bounds): Set => { + const startTime = performance.now() + + // Use QuadTree for fast spatial query + const results: string[] = spatialIndex.query(viewportBounds) + const visibleIds = new Set(results) + + lastSpatialQueryTime = performance.now() - startTime + spatialMetrics.queryTime = lastSpatialQueryTime + + return visibleIds + } + + /** + * Detects position changes for a single node and updates reactive state + */ + const detectPositionChanges = (node: LGraphNode, id: string): boolean => { + const currentPos = nodePositions.get(id) + + if ( + !currentPos || + currentPos.x !== node.pos[0] || + currentPos.y !== node.pos[1] + ) { + nodePositions.set(id, { x: node.pos[0], y: node.pos[1] }) + + // Push position change to layout store + // Source is already set to 'canvas' in detectChangesInRAF + void moveNode(id, { x: node.pos[0], y: node.pos[1] }) + + return true + } + return false + } + + /** + * Detects size changes for a single node and updates reactive state + */ + const detectSizeChanges = (node: LGraphNode, id: string): boolean => { + const currentSize = nodeSizes.get(id) + + if ( + !currentSize || + currentSize.width !== node.size[0] || + currentSize.height !== node.size[1] + ) { + nodeSizes.set(id, { width: node.size[0], height: node.size[1] }) + + // Push size change to layout store + // Source is already set to 'canvas' in detectChangesInRAF + void resizeNode(id, { + width: node.size[0], + height: node.size[1] + }) + + return true + } + return false + } + + /** + * Updates spatial index for a node if bounds changed + */ + const updateSpatialIndex = (node: LGraphNode, id: string): void => { + const bounds: Bounds = { + x: node.pos[0], + y: node.pos[1], + width: node.size[0], + height: node.size[1] + } + spatialIndex.update(id, bounds) + } + + /** + * Updates performance metrics after change detection + */ + const updatePerformanceMetrics = ( + startTime: number, + positionUpdates: number, + sizeUpdates: number + ): void => { + const endTime = performance.now() + performanceMetrics.updateTime = endTime - startTime + performanceMetrics.nodeCount = vueNodeData.size + performanceMetrics.culledCount = Array.from(nodeState.values()).filter( + (state) => state.culled + ).length + spatialMetrics.nodesInIndex = spatialIndex.size + + if (positionUpdates > 0 || sizeUpdates > 0) { + performanceMetrics.rafUpdateCount++ + } + } + + /** + * Main RAF change detection function + */ + const detectChangesInRAF = () => { + const startTime = performance.now() + + if (!graph?._nodes) return + + let positionUpdates = 0 + let sizeUpdates = 0 + + // Set source for all canvas-driven updates + setSource(LayoutSource.Canvas) + + // Process each node for changes + for (const node of graph._nodes) { + const id = String(node.id) + + const posChanged = detectPositionChanges(node, id) + const sizeChanged = detectSizeChanges(node, id) + + if (posChanged) positionUpdates++ + if (sizeChanged) sizeUpdates++ + + // Update spatial index if geometry changed + if (posChanged || sizeChanged) { + updateSpatialIndex(node, id) + } + } + + updatePerformanceMetrics(startTime, positionUpdates, sizeUpdates) + } + + /** + * Handles node addition to the graph - sets up Vue state and spatial indexing + */ + const handleNodeAdded = ( + node: LGraphNode, + originalCallback?: (node: LGraphNode) => void + ) => { + const id = String(node.id) + + // Store non-reactive reference to original node + nodeRefs.set(id, node) + + // Set up widget callbacks BEFORE extracting data (critical order) + setupNodeWidgetCallbacks(node) + + // Extract safe data for Vue (now with proper callbacks) + vueNodeData.set(id, extractVueNodeData(node)) + + // Set up reactive tracking state + nodeState.set(id, { + visible: true, + dirty: false, + lastUpdate: performance.now(), + culled: false + }) + nodePositions.set(id, { x: node.pos[0], y: node.pos[1] }) + nodeSizes.set(id, { width: node.size[0], height: node.size[1] }) + attachMetadata(node) + + // Add to spatial index for viewport culling + const bounds: Bounds = { + x: node.pos[0], + y: node.pos[1], + width: node.size[0], + height: node.size[1] + } + spatialIndex.insert(id, bounds, id) + + // Add node to layout store + setSource(LayoutSource.Canvas) + void createNode(id, { + position: { x: node.pos[0], y: node.pos[1] }, + size: { width: node.size[0], height: node.size[1] }, + zIndex: node.order || 0, + visible: true + }) + + // Call original callback if provided + if (originalCallback) { + void originalCallback(node) + } + } + + /** + * Handles node removal from the graph - cleans up all references + */ + const handleNodeRemoved = ( + node: LGraphNode, + originalCallback?: (node: LGraphNode) => void + ) => { + const id = String(node.id) + + // Remove from spatial index + spatialIndex.remove(id) + + // Remove node from layout store + setSource(LayoutSource.Canvas) + void deleteNode(id) + + // Clean up all tracking references + nodeRefs.delete(id) + vueNodeData.delete(id) + nodeState.delete(id) + nodePositions.delete(id) + nodeSizes.delete(id) + lastNodesSnapshot.delete(id) + + // Call original callback if provided + if (originalCallback) { + originalCallback(node) + } + } + + /** + * Creates cleanup function for event listeners and state + */ + const createCleanupFunction = ( + originalOnNodeAdded: ((node: LGraphNode) => void) | undefined, + originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined, + originalOnTrigger: ((action: string, param: unknown) => void) | undefined + ) => { + return () => { + // Restore original callbacks + graph.onNodeAdded = originalOnNodeAdded || undefined + graph.onNodeRemoved = originalOnNodeRemoved || undefined + graph.onTrigger = originalOnTrigger || undefined + + // Clear pending updates + if (batchTimeoutId !== null) { + clearTimeout(batchTimeoutId) + batchTimeoutId = null + } + + // Clear all state maps + nodeRefs.clear() + vueNodeData.clear() + nodeState.clear() + nodePositions.clear() + nodeSizes.clear() + lastNodesSnapshot.clear() + pendingUpdates.clear() + criticalUpdates.clear() + lowPriorityUpdates.clear() + spatialIndex.clear() + } + } + + /** + * Sets up event listeners - now simplified with extracted handlers + */ + const setupEventListeners = (): (() => void) => { + // Store original callbacks + const originalOnNodeAdded = graph.onNodeAdded + const originalOnNodeRemoved = graph.onNodeRemoved + const originalOnTrigger = graph.onTrigger + + // Set up graph event handlers + graph.onNodeAdded = (node: LGraphNode) => { + handleNodeAdded(node, originalOnNodeAdded) + } + + graph.onNodeRemoved = (node: LGraphNode) => { + handleNodeRemoved(node, originalOnNodeRemoved) + } + + // Listen for property change events from instrumented nodes + graph.onTrigger = (action: string, param: unknown) => { + if ( + action === 'node:property:changed' && + param && + typeof param === 'object' + ) { + const event = param as { + nodeId: string | number + property: string + oldValue: unknown + newValue: unknown + } + + const nodeId = String(event.nodeId) + const currentData = vueNodeData.get(nodeId) + + if (currentData) { + if (event.property === 'title') { + vueNodeData.set(nodeId, { + ...currentData, + title: String(event.newValue) + }) + } else if (event.property === 'flags.collapsed') { + vueNodeData.set(nodeId, { + ...currentData, + flags: { + ...currentData.flags, + collapsed: Boolean(event.newValue) + } + }) + } + } + } + + // Call original trigger handler if it exists + if (originalOnTrigger) { + originalOnTrigger(action, param) + } + } + + // Initialize state + syncWithGraph() + + // Return cleanup function + return createCleanupFunction( + originalOnNodeAdded || undefined, + originalOnNodeRemoved || undefined, + originalOnTrigger || undefined + ) + } + + // Set up event listeners immediately + const cleanup = setupEventListeners() + + // Process any existing nodes after event listeners are set up + if (graph._nodes && graph._nodes.length > 0) { + graph._nodes.forEach((node: LGraphNode) => { + if (graph.onNodeAdded) { + graph.onNodeAdded(node) + } + }) + } + + return { + vueNodeData: readonly(vueNodeData) as ReadonlyMap, + nodeState: readonly(nodeState) as ReadonlyMap, + nodePositions: readonly(nodePositions) as ReadonlyMap< + string, + { x: number; y: number } + >, + nodeSizes: readonly(nodeSizes) as ReadonlyMap< + string, + { width: number; height: number } + >, + getNode, + setupEventListeners, + cleanup, + scheduleUpdate, + forceSync: syncWithGraph, + detectChangesInRAF, + getVisibleNodeIds, + performanceMetrics, + spatialMetrics: readonly(spatialMetrics), + getSpatialIndexDebugInfo: () => spatialIndex.getDebugInfo() + } +} diff --git a/src/composables/graph/useSpatialIndex.ts b/src/composables/graph/useSpatialIndex.ts new file mode 100644 index 000000000..997e331f7 --- /dev/null +++ b/src/composables/graph/useSpatialIndex.ts @@ -0,0 +1,198 @@ +/** + * Composable for spatial indexing of nodes using QuadTree + * Integrates with useGraphNodeManager for efficient viewport culling + */ +import { useDebounceFn } from '@vueuse/core' +import { computed, reactive, ref } from 'vue' + +import { type Bounds, QuadTree } from '@/utils/spatial/QuadTree' + +export interface SpatialIndexOptions { + worldBounds?: Bounds + maxDepth?: number + maxItemsPerNode?: number + updateDebounceMs?: number +} + +interface SpatialMetrics { + queryTime: number + totalNodes: number + visibleNodes: number + treeDepth: number + rebuildCount: number +} + +export const useSpatialIndex = (options: SpatialIndexOptions = {}) => { + // Default world bounds (can be expanded dynamically) + const defaultBounds: Bounds = { + x: -10000, + y: -10000, + width: 20000, + height: 20000 + } + + // QuadTree instance + const quadTree = ref | null>(null) + + // Performance metrics + const metrics = reactive({ + queryTime: 0, + totalNodes: 0, + visibleNodes: 0, + treeDepth: 0, + rebuildCount: 0 + }) + + // Initialize QuadTree + const initialize = (bounds: Bounds = defaultBounds) => { + quadTree.value = new QuadTree(bounds, { + maxDepth: options.maxDepth ?? 6, + maxItemsPerNode: options.maxItemsPerNode ?? 4 + }) + metrics.rebuildCount++ + } + + // Add or update node in spatial index + const updateNode = ( + nodeId: string, + position: { x: number; y: number }, + size: { width: number; height: number } + ) => { + if (!quadTree.value) { + initialize() + } + + const bounds: Bounds = { + x: position.x, + y: position.y, + width: size.width, + height: size.height + } + + // Use insert instead of update - insert handles both new and existing nodes + quadTree.value!.insert(nodeId, bounds, nodeId) + metrics.totalNodes = quadTree.value!.size + } + + // Batch update for multiple nodes + const batchUpdate = ( + updates: Array<{ + id: string + position: { x: number; y: number } + size: { width: number; height: number } + }> + ) => { + if (!quadTree.value) { + initialize() + } + + for (const update of updates) { + const bounds: Bounds = { + x: update.position.x, + y: update.position.y, + width: update.size.width, + height: update.size.height + } + // Use insert instead of update - insert handles both new and existing nodes + quadTree.value!.insert(update.id, bounds, update.id) + } + + metrics.totalNodes = quadTree.value!.size + } + + // Remove node from spatial index + const removeNode = (nodeId: string) => { + if (!quadTree.value) return + + quadTree.value.remove(nodeId) + metrics.totalNodes = quadTree.value.size + } + + // Query nodes within viewport bounds + const queryViewport = (viewportBounds: Bounds): string[] => { + if (!quadTree.value) return [] + + const startTime = performance.now() + const nodeIds = quadTree.value.query(viewportBounds) + const queryTime = performance.now() - startTime + + metrics.queryTime = queryTime + metrics.visibleNodes = nodeIds.length + + return nodeIds + } + + // Get nodes within a radius (for proximity queries) + const queryRadius = ( + center: { x: number; y: number }, + radius: number + ): string[] => { + if (!quadTree.value) return [] + + const bounds: Bounds = { + x: center.x - radius, + y: center.y - radius, + width: radius * 2, + height: radius * 2 + } + + return quadTree.value.query(bounds) + } + + // Clear all nodes + const clear = () => { + if (!quadTree.value) return + + quadTree.value.clear() + metrics.totalNodes = 0 + metrics.visibleNodes = 0 + } + + // Rebuild tree (useful after major layout changes) + const rebuild = ( + nodes: Map< + string, + { + position: { x: number; y: number } + size: { width: number; height: number } + } + > + ) => { + initialize() + + const updates = Array.from(nodes.entries()).map(([id, data]) => ({ + id, + position: data.position, + size: data.size + })) + + batchUpdate(updates) + } + + // Debounced update for performance + const debouncedUpdateNode = useDebounceFn( + updateNode, + options.updateDebounceMs ?? 16 + ) + + return { + // Core functions + initialize, + updateNode, + batchUpdate, + removeNode, + queryViewport, + queryRadius, + clear, + rebuild, + + // Debounced version for high-frequency updates + debouncedUpdateNode, + + // Metrics + metrics: computed(() => metrics), + + // Direct access to QuadTree (for advanced usage) + quadTree: computed(() => quadTree.value) + } +} diff --git a/src/composables/graph/useTransformSettling.ts b/src/composables/graph/useTransformSettling.ts new file mode 100644 index 000000000..669cfceaa --- /dev/null +++ b/src/composables/graph/useTransformSettling.ts @@ -0,0 +1,151 @@ +import { useDebounceFn, useEventListener, useThrottleFn } from '@vueuse/core' +import { ref } from 'vue' +import type { MaybeRefOrGetter } from 'vue' + +export interface TransformSettlingOptions { + /** + * Delay in ms before transform is considered "settled" after last interaction + * @default 200 + */ + settleDelay?: number + /** + * Whether to track both zoom (wheel) and pan (pointer drag) interactions + * @default false + */ + trackPan?: boolean + /** + * Throttle delay for high-frequency pointermove events (only used when trackPan is true) + * @default 16 (~60fps) + */ + pointerMoveThrottle?: number + /** + * Whether to use passive event listeners (better performance but can't preventDefault) + * @default true + */ + passive?: boolean +} + +/** + * Tracks when canvas transforms (zoom/pan) are actively changing vs settled. + * + * This composable helps optimize rendering quality during transformations. + * When the user is actively zooming or panning, we can reduce rendering quality + * for better performance. Once the transform "settles" (stops changing), we can + * trigger high-quality re-rasterization. + * + * The settling concept prevents constant quality switching during interactions + * by waiting for a period of inactivity before considering the transform complete. + * + * Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for + * efficient settle detection. + * + * @example + * ```ts + * const { isTransforming } = useTransformSettling(canvasRef, { + * settleDelay: 200, + * trackPan: true + * }) + * + * // Use in CSS classes or rendering logic + * const cssClass = computed(() => ({ + * 'low-quality': isTransforming.value, + * 'high-quality': !isTransforming.value + * })) + * ``` + */ +export function useTransformSettling( + target: MaybeRefOrGetter, + options: TransformSettlingOptions = {} +) { + const { + settleDelay = 200, + trackPan = false, + pointerMoveThrottle = 16, + passive = true + } = options + + const isTransforming = ref(false) + let isPanning = false + + /** + * Mark transform as active + */ + const markTransformActive = () => { + isTransforming.value = true + } + + /** + * Mark transform as settled (debounced) + */ + const markTransformSettled = useDebounceFn(() => { + isTransforming.value = false + }, settleDelay) + + /** + * Handle any transform event - mark active then queue settle + */ + const handleTransformEvent = () => { + markTransformActive() + void markTransformSettled() + } + + // Wheel handler + const handleWheel = () => { + handleTransformEvent() + } + + // Pointer handlers for panning + const handlePointerDown = () => { + if (trackPan) { + isPanning = true + handleTransformEvent() + } + } + + // Throttled pointer move handler for performance + const handlePointerMove = trackPan + ? useThrottleFn(() => { + if (isPanning) { + handleTransformEvent() + } + }, pointerMoveThrottle) + : undefined + + const handlePointerEnd = () => { + if (trackPan) { + isPanning = false + // Don't immediately stop - let the debounced settle handle it + } + } + + // Register event listeners with auto-cleanup + useEventListener(target, 'wheel', handleWheel, { + capture: true, + passive + }) + + if (trackPan) { + useEventListener(target, 'pointerdown', handlePointerDown, { + capture: true + }) + + if (handlePointerMove) { + useEventListener(target, 'pointermove', handlePointerMove, { + capture: true, + passive + }) + } + + useEventListener(target, 'pointerup', handlePointerEnd, { + capture: true + }) + + useEventListener(target, 'pointercancel', handlePointerEnd, { + capture: true + }) + } + + return { + isTransforming + } +} diff --git a/src/composables/graph/useWidgetValue.ts b/src/composables/graph/useWidgetValue.ts new file mode 100644 index 000000000..1cf2fb353 --- /dev/null +++ b/src/composables/graph/useWidgetValue.ts @@ -0,0 +1,155 @@ +/** + * Composable for managing widget value synchronization between Vue and LiteGraph + * Provides consistent pattern for immediate UI updates and LiteGraph callbacks + */ +import { type Ref, ref, watch } from 'vue' + +import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' + +export interface UseWidgetValueOptions< + T extends WidgetValue = WidgetValue, + U = T +> { + /** The widget configuration from LiteGraph */ + widget: SimplifiedWidget + /** The current value from parent component */ + modelValue: T + /** Default value if modelValue is null/undefined */ + defaultValue: T + /** Emit function from component setup */ + emit: (event: 'update:modelValue', value: T) => void + /** Optional value transformer before sending to LiteGraph */ + transform?: (value: U) => T +} + +export interface UseWidgetValueReturn< + T extends WidgetValue = WidgetValue, + U = T +> { + /** Local value for immediate UI updates */ + localValue: Ref + /** Handler for user interactions */ + onChange: (newValue: U) => void +} + +/** + * Manages widget value synchronization with LiteGraph + * + * @example + * ```vue + * const { localValue, onChange } = useWidgetValue({ + * widget: props.widget, + * modelValue: props.modelValue, + * defaultValue: '' + * }) + * ``` + */ +export function useWidgetValue({ + widget, + modelValue, + defaultValue, + emit, + transform +}: UseWidgetValueOptions): UseWidgetValueReturn { + // Local value for immediate UI updates + const localValue = ref(modelValue ?? defaultValue) + + // Handle user changes + const onChange = (newValue: U) => { + // Handle different PrimeVue component signatures + let processedValue: T + if (transform) { + processedValue = transform(newValue) + } else { + // Ensure type safety - only cast when types are compatible + if ( + typeof newValue === typeof defaultValue || + newValue === null || + newValue === undefined + ) { + processedValue = (newValue ?? defaultValue) as T + } else { + console.warn( + `useWidgetValue: Type mismatch for widget ${widget.name}. Expected ${typeof defaultValue}, got ${typeof newValue}` + ) + processedValue = defaultValue + } + } + + // 1. Update local state for immediate UI feedback + localValue.value = processedValue + + // 2. Emit to parent component + emit('update:modelValue', processedValue) + } + + // Watch for external updates from LiteGraph + watch( + () => modelValue, + (newValue) => { + localValue.value = newValue ?? defaultValue + } + ) + + return { + localValue: localValue as Ref, + onChange + } +} + +/** + * Type-specific helper for string widgets + */ +export function useStringWidgetValue( + widget: SimplifiedWidget, + modelValue: string, + emit: (event: 'update:modelValue', value: string) => void +) { + return useWidgetValue({ + widget, + modelValue, + defaultValue: '', + emit, + transform: (value: string | undefined) => String(value || '') // Handle undefined from PrimeVue + }) +} + +/** + * Type-specific helper for number widgets + */ +export function useNumberWidgetValue( + widget: SimplifiedWidget, + modelValue: number, + emit: (event: 'update:modelValue', value: number) => void +) { + return useWidgetValue({ + widget, + modelValue, + defaultValue: 0, + emit, + transform: (value: number | number[]) => { + // Handle PrimeVue Slider which can emit number | number[] + if (Array.isArray(value)) { + return value.length > 0 ? value[0] ?? 0 : 0 + } + return Number(value) || 0 + } + }) +} + +/** + * Type-specific helper for boolean widgets + */ +export function useBooleanWidgetValue( + widget: SimplifiedWidget, + modelValue: boolean, + emit: (event: 'update:modelValue', value: boolean) => void +) { + return useWidgetValue({ + widget, + modelValue, + defaultValue: false, + emit, + transform: (value: boolean) => Boolean(value) + }) +} diff --git a/src/composables/node/useNodeCanvasImagePreview.ts b/src/composables/node/useNodeCanvasImagePreview.ts index 98d49b485..008119407 100644 --- a/src/composables/node/useNodeCanvasImagePreview.ts +++ b/src/composables/node/useNodeCanvasImagePreview.ts @@ -1,5 +1,5 @@ -import { useImagePreviewWidget } from '@/composables/widgets/useImagePreviewWidget' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useImagePreviewWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget' const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview' diff --git a/src/composables/node/useNodeChatHistory.ts b/src/composables/node/useNodeChatHistory.ts index 8fbe78895..a1fa3ad3a 100644 --- a/src/composables/node/useNodeChatHistory.ts +++ b/src/composables/node/useNodeChatHistory.ts @@ -1,6 +1,6 @@ import type ChatHistoryWidget from '@/components/graph/widgets/ChatHistoryWidget.vue' -import { useChatHistoryWidget } from '@/composables/widgets/useChatHistoryWidget' import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useChatHistoryWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChatHistoryWidget' const CHAT_HISTORY_WIDGET_NAME = '$$node-chat-history' diff --git a/src/composables/node/useNodeProgressText.ts b/src/composables/node/useNodeProgressText.ts index 12e09bd5e..07e7488ea 100644 --- a/src/composables/node/useNodeProgressText.ts +++ b/src/composables/node/useNodeProgressText.ts @@ -1,5 +1,5 @@ -import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget' import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useTextPreviewWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useProgressTextWidget' const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview' diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 8fe83eb36..61ed889bb 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -282,6 +282,18 @@ export function useCoreCommands(): ComfyCommand[] { app.canvas.setDirty(true, true) } }, + { + id: 'Experimental.ToggleVueNodes', + label: () => + `Experimental: ${ + useSettingStore().get('Comfy.VueNodes.Enabled') ? 'Disable' : 'Enable' + } Vue Nodes`, + function: async () => { + const settingStore = useSettingStore() + const current = settingStore.get('Comfy.VueNodes.Enabled') ?? false + await settingStore.set('Comfy.VueNodes.Enabled', !current) + } + }, { id: 'Comfy.Canvas.FitView', icon: 'pi pi-expand', diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index a578eb8bf..9a0bcd03d 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -12,10 +12,9 @@ export enum ServerFeatureFlag { } /** - * Composable for reactive access to feature flags + * Composable for reactive access to server-side feature flags */ export function useFeatureFlags() { - // Create reactive state that tracks server feature flags const flags = reactive({ get supportsPreviewMetadata() { return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) @@ -28,10 +27,8 @@ export function useFeatureFlags() { } }) - // Create a reactive computed for any feature flag - const featureFlag = (featurePath: string, defaultValue?: T) => { - return computed(() => api.getServerFeature(featurePath, defaultValue)) - } + const featureFlag = (featurePath: string, defaultValue?: T) => + computed(() => api.getServerFeature(featurePath, defaultValue)) return { flags: readonly(flags), diff --git a/src/composables/useVueFeatureFlags.ts b/src/composables/useVueFeatureFlags.ts new file mode 100644 index 000000000..8816555d1 --- /dev/null +++ b/src/composables/useVueFeatureFlags.ts @@ -0,0 +1,38 @@ +/** + * Vue-related feature flags composable + * Manages local settings-driven flags and LiteGraph integration + */ +import { computed, watch } from 'vue' + +import { useSettingStore } from '@/stores/settingStore' + +import { LiteGraph } from '../lib/litegraph/src/litegraph' + +export const useVueFeatureFlags = () => { + const settingStore = useSettingStore() + + const isVueNodesEnabled = computed(() => { + try { + return settingStore.get('Comfy.VueNodes.Enabled') ?? false + } catch { + return false + } + }) + + // Whether Vue nodes should render + const shouldRenderVueNodes = computed(() => isVueNodesEnabled.value) + + // Sync the Vue nodes flag with LiteGraph global settings + const syncVueNodesFlag = () => { + LiteGraph.vueNodesMode = isVueNodesEnabled.value + } + + // Watch for changes and update LiteGraph immediately + watch(isVueNodesEnabled, syncVueNodesFlag, { immediate: true }) + + return { + isVueNodesEnabled, + shouldRenderVueNodes, + syncVueNodesFlag + } +} diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index 45093da5d..9b6a2a02c 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -952,5 +952,19 @@ export const CORE_SETTINGS: SettingParams[] = [ name: 'Release seen timestamp', type: 'hidden', defaultValue: 0 + }, + + /** + * Vue Node System Settings + */ + { + id: 'Comfy.VueNodes.Enabled', + name: 'Enable Vue node rendering (hidden)', + type: 'hidden', + tooltip: + 'Render nodes as Vue components instead of canvas. Hidden; toggle via Experimental keybinding.', + defaultValue: false, + experimental: true, + versionAdded: '1.27.1' } ] diff --git a/src/constants/slotColors.ts b/src/constants/slotColors.ts new file mode 100644 index 000000000..797bd94f5 --- /dev/null +++ b/src/constants/slotColors.ts @@ -0,0 +1,30 @@ +/** + * Default colors for node slot types + * Mirrors LiteGraph's slot_default_color_by_type + */ +export const SLOT_TYPE_COLORS: Record = { + number: '#AAD', + string: '#DCA', + boolean: '#DAA', + vec2: '#ADA', + vec3: '#ADA', + vec4: '#ADA', + color: '#DDA', + image: '#353', + latent: '#858', + conditioning: '#FFA', + control_net: '#F8F', + clip: '#FFD', + vae: '#F82', + model: '#B98', + '*': '#AAA' // Default color +} + +/** + * Get the color for a slot type + */ +export function getSlotColor(type?: string | number | null): string { + if (!type) return SLOT_TYPE_COLORS['*'] + const typeStr = String(type).toLowerCase() + return SLOT_TYPE_COLORS[typeStr] || SLOT_TYPE_COLORS['*'] +} diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 15b9c95bc..7cc7fb220 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -121,7 +121,7 @@ export class ManageGroupDialog extends ComfyDialog { getGroupData() { this.groupNodeType = LiteGraph.registered_node_types[ `${PREFIX}${SEPARATOR}` + this.selectedGroup - ] as LGraphNodeConstructor + ] as unknown as LGraphNodeConstructor this.groupNodeDef = this.groupNodeType.nodeData this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType) } diff --git a/src/lib/litegraph/CLAUDE.md b/src/lib/litegraph/CLAUDE.md index d0326b505..68f8bea95 100644 --- a/src/lib/litegraph/CLAUDE.md +++ b/src/lib/litegraph/CLAUDE.md @@ -22,7 +22,7 @@ # Workflow -- Be sure to typecheck when you’re done making a series of code changes +- Be sure to typecheck when you're done making a series of code changes - Prefer running single tests, and not the whole test suite, for performance # Testing Guidelines diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 94eee60de..cdd387d89 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -6,6 +6,8 @@ import { } from '@/lib/litegraph/src/constants' import type { UUID } from '@/lib/litegraph/src/utils/uuid' import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid' +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' import type { DragAndScaleState } from './DragAndScale' import { LGraphCanvas } from './LGraphCanvas' @@ -1336,6 +1338,7 @@ export class LGraph * @returns The newly created reroute - typically ignored. */ createReroute(pos: Point, before: LinkSegment): Reroute { + const layoutMutations = useLayoutMutations() const rerouteId = ++this.state.lastRerouteId const linkIds = before instanceof Reroute ? before.linkIds : [before.id] const floatingLinkIds = @@ -1349,6 +1352,16 @@ export class LGraph floatingLinkIds ) this.reroutes.set(rerouteId, reroute) + + // Register reroute in Layout Store for spatial tracking + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.createReroute( + rerouteId, + { x: pos[0], y: pos[1] }, + before.parentId, + Array.from(linkIds) + ) + for (const linkId of linkIds) { const link = this._links.get(linkId) if (!link) continue @@ -1379,6 +1392,7 @@ export class LGraph * @param id ID of reroute to remove */ removeReroute(id: RerouteId): void { + const layoutMutations = useLayoutMutations() const { reroutes } = this const reroute = reroutes.get(id) if (!reroute) return @@ -1422,6 +1436,11 @@ export class LGraph } reroutes.delete(id) + + // Delete reroute from Layout Store + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.deleteReroute(id) + // This does not belong here; it should be handled by the caller, or run by a remove-many API. // https://github.com/Comfy-Org/litegraph.js/issues/898 this.setDirtyCanvas(false, true) @@ -2105,6 +2124,7 @@ export class LGraph data: ISerialisedGraph | SerialisableGraph, keep_old?: boolean ): boolean | undefined { + const layoutMutations = useLayoutMutations() const options: LGraphEventMap['configuring'] = { data, clearGraph: !keep_old @@ -2245,6 +2265,9 @@ export class LGraph // Drop broken links, and ignore reroutes with no valid links if (!reroute.validateLinks(this._links, this.floatingLinks)) { this.reroutes.delete(reroute.id) + // Clean up layout store + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.deleteReroute(reroute.id) } } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index a32b8c354..9dacd426e 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -2,6 +2,11 @@ import { toString } from 'es-toolkit/compat' import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector' +import { + type LinkRenderContext, + LitegraphLinkAdapter +} from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { CanvasPointer } from './CanvasPointer' import type { ContextMenu } from './ContextMenu' @@ -51,7 +56,6 @@ import { containsRect, createBounds, distance, - findPointOnCurve, isInRect, isInRectangle, isPointInRect, @@ -235,9 +239,6 @@ export class LGraphCanvas static #tmp_area = new Float32Array(4) static #margin_area = new Float32Array(4) static #link_bounding = new Float32Array(4) - static #lTempA: Point = new Float32Array(2) - static #lTempB: Point = new Float32Array(2) - static #lTempC: Point = new Float32Array(2) static DEFAULT_BACKGROUND_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=' @@ -679,6 +680,9 @@ export class LGraphCanvas /** Set on keydown, keyup. @todo */ #shiftDown: boolean = false + /** Link rendering adapter for litegraph-to-canvas integration */ + linkRenderer: LitegraphLinkAdapter | null = null + /** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */ dragZoomEnabled: boolean = false /** The start position of the drag zoom. */ @@ -748,6 +752,13 @@ export class LGraphCanvas } } + // Initialize link renderer if graph is available + if (graph) { + this.linkRenderer = new LitegraphLinkAdapter(graph) + // Disable layout writes during render + this.linkRenderer.enableLayoutStoreWrites = false + } + this.linkConnector.events.addEventListener('link-created', () => this.#dirty() ) @@ -1843,6 +1854,11 @@ export class LGraphCanvas this.clear() newGraph.attachCanvas(this) + // Re-initialize link renderer with new graph + this.linkRenderer = new LitegraphLinkAdapter(newGraph) + // Disable layout writes during render + this.linkRenderer.enableLayoutStoreWrites = false + this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph }) this.#dirty() } @@ -2236,11 +2252,22 @@ export class LGraphCanvas this.processSelect(node, e, true) } else if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) { // Reroutes - const reroute = graph.getRerouteOnPos( - e.canvasX, - e.canvasY, - this.#visibleReroutes - ) + // Try layout store first, fallback to old method + const rerouteLayout = layoutStore.queryRerouteAtPoint({ + x: e.canvasX, + y: e.canvasY + }) + + let reroute: Reroute | undefined + if (rerouteLayout) { + reroute = graph.getReroute(rerouteLayout.id) + } else { + reroute = graph.getRerouteOnPos( + e.canvasX, + e.canvasY, + this.#visibleReroutes + ) + } if (reroute) { if (e.altKey) { pointer.onClick = (upEvent) => { @@ -2406,8 +2433,18 @@ export class LGraphCanvas // Reroutes if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) { + // Try layout store first for hit detection + const rerouteLayout = layoutStore.queryRerouteAtPoint({ x, y }) + let foundReroute: Reroute | undefined + + if (rerouteLayout) { + foundReroute = graph.getReroute(rerouteLayout.id) + } + + // Fallback to checking visible reroutes directly for (const reroute of this.#visibleReroutes) { - const overReroute = reroute.containsPoint([x, y]) + const overReroute = + foundReroute === reroute || reroute.containsPoint([x, y]) if (!reroute.isSlotHovered && !overReroute) continue if (overReroute) { @@ -2441,16 +2478,32 @@ export class LGraphCanvas this.ctx.lineWidth = this.connections_width + 7 const dpi = Math.max(window?.devicePixelRatio ?? 1, 1) + // Try layout store for segment hit testing first (more precise) + const hitSegment = layoutStore.queryLinkSegmentAtPoint({ x, y }, this.ctx) + for (const linkSegment of this.renderedPaths) { const centre = linkSegment._pos if (!centre) continue + // Check if this link segment was hit + let isLinkHit = + hitSegment && + linkSegment.id === + (linkSegment instanceof Reroute + ? hitSegment.rerouteId + : hitSegment.linkId) + + if (!isLinkHit && linkSegment.path) { + // Fallback to direct path hit testing if not found in layout store + isLinkHit = this.ctx.isPointInStroke( + linkSegment.path, + x * dpi, + y * dpi + ) + } + // If we shift click on a link then start a link from that input - if ( - (e.shiftKey || e.altKey) && - linkSegment.path && - this.ctx.isPointInStroke(linkSegment.path, x * dpi, y * dpi) - ) { + if ((e.shiftKey || e.altKey) && isLinkHit) { this.ctx.lineWidth = lineWidth if (e.shiftKey && !e.altKey) { @@ -2465,7 +2518,10 @@ export class LGraphCanvas pointer.onDragEnd = (e) => this.#processDraggedItems(e) return } - } else if (isInRectangle(x, y, centre[0] - 4, centre[1] - 4, 8, 8)) { + } else if ( + this.linkMarkerShape !== LinkMarkerShape.None && + isInRectangle(x, y, centre[0] - 4, centre[1] - 4, 8, 8) + ) { this.ctx.lineWidth = lineWidth pointer.onClick = () => this.showLinkMenu(linkSegment, e) @@ -3178,8 +3234,27 @@ export class LGraphCanvas // For input/output hovering // to store the output of isOverNodeInput const pos: Point = [0, 0] - const inputId = isOverNodeInput(node, x, y, pos) - const outputId = isOverNodeOutput(node, x, y, pos) + + // Try to use layout store for hit testing first, fallback to old method + let inputId: number = -1 + let outputId: number = -1 + + const slotLayout = layoutStore.querySlotAtPoint({ x, y }) + if (slotLayout && slotLayout.nodeId === String(node.id)) { + if (slotLayout.type === 'input') { + inputId = slotLayout.index + pos[0] = slotLayout.position.x + pos[1] = slotLayout.position.y + } else { + outputId = slotLayout.index + pos[0] = slotLayout.position.x + pos[1] = slotLayout.position.y + } + } else { + // Fallback to old method + inputId = isOverNodeInput(node, x, y, pos) + outputId = isOverNodeOutput(node, x, y, pos) + } const overWidget = node.getWidgetOnPos(x, y, true) ?? undefined if (!node.mouseOver) { @@ -4640,18 +4715,28 @@ export class LGraphCanvas : LiteGraph.CONNECTING_LINK_COLOR // the connection being dragged by the mouse - this.renderLink( - ctx, - pos, - highlightPos, - null, - false, - null, - colour, - fromDirection, - dragDirection - ) + if (this.linkRenderer) { + this.linkRenderer.renderLinkDirect( + ctx, + pos, + highlightPos, + null, + false, + null, + colour, + fromDirection, + dragDirection, + { + ...this.buildLinkRenderContext(), + linkMarkerShape: LinkMarkerShape.None + }, + { + disabled: false + } + ) + } + ctx.fillStyle = colour ctx.beginPath() if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) { ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10) @@ -4724,6 +4809,11 @@ export class LGraphCanvas /** @returns If the pointer is over a link centre marker, the link segment it belongs to. Otherwise, `undefined`. */ #getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined { + // Skip hit detection if center markers are disabled + if (this.linkMarkerShape === LinkMarkerShape.None) { + return undefined + } + for (const linkSegment of this.renderedPaths) { const centre = linkSegment._pos if (!centre) continue @@ -5049,6 +5139,19 @@ export class LGraphCanvas drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void { this.current_node = node + // When Vue nodes mode is enabled, LiteGraph should not draw node chrome or widgets. + // We still need to keep slot metrics and layout in sync for hit-testing and links. + // Interaction system changes coming later, chances are vue nodes mode will be mostly broken on land + if (LiteGraph.vueNodesMode) { + // Prepare concrete slots and compute layout measures without rendering visuals. + node._setConcreteSlots() + if (!node.collapsed) { + node.arrange() + } + // Skip all node body/widget/title rendering. Vue overlay handles visuals. + return + } + const color = node.renderingColor const bgcolor = node.renderingBgColor @@ -5762,6 +5865,34 @@ export class LGraphCanvas } } + /** + * Build LinkRenderContext from canvas properties + * Helper method for using LitegraphLinkAdapter + */ + private buildLinkRenderContext(): LinkRenderContext { + return { + // Canvas settings + renderMode: this.links_render_mode, + connectionWidth: this.connections_width, + renderBorder: this.render_connections_border, + lowQuality: this.low_quality, + highQualityRender: this.highquality_render, + scale: this.ds.scale, + linkMarkerShape: this.linkMarkerShape, + renderConnectionArrows: this.render_connection_arrows, + + // State + highlightedLinks: new Set(Object.keys(this.highlighted_links)), + + // Colors + defaultLinkColor: this.default_link_color, + linkTypeColors: LGraphCanvas.link_type_colors, + + // Pattern for disabled links + disabledPattern: this._pattern + } + } + /** * draws a link between two points * @param ctx Canvas 2D rendering context @@ -5803,333 +5934,27 @@ export class LGraphCanvas disabled?: boolean } = {} ): void { - const linkColour = - link != null && this.highlighted_links[link.id] - ? '#FFF' - : color || - link?.color || - (link?.type != null && LGraphCanvas.link_type_colors[link.type]) || - this.default_link_color - const startDir = start_dir || LinkDirection.RIGHT - const endDir = end_dir || LinkDirection.LEFT - - const dist = - this.links_render_mode == LinkRenderType.SPLINE_LINK && - (!endControl || !startControl) - ? distance(a, b) - : 0 - - // TODO: Subline code below was inserted in the wrong place - should be before this statement - if (this.render_connections_border && !this.low_quality) { - ctx.lineWidth = this.connections_width + 4 - } - ctx.lineJoin = 'round' - num_sublines ||= 1 - if (num_sublines > 1) ctx.lineWidth = 0.5 - - // begin line shape - const path = new Path2D() - - /** The link or reroute we're currently rendering */ - const linkSegment = reroute ?? link - if (linkSegment) linkSegment.path = path - - const innerA = LGraphCanvas.#lTempA - const innerB = LGraphCanvas.#lTempB - - /** Reference to {@link reroute._pos} if present, or {@link link._pos} if present. Caches the centre point of the link. */ - const pos: Point = linkSegment?._pos ?? [0, 0] - - for (let i = 0; i < num_sublines; i++) { - const offsety = (i - (num_sublines - 1) * 0.5) * 5 - innerA[0] = a[0] - innerA[1] = a[1] - innerB[0] = b[0] - innerB[1] = b[1] - - if (this.links_render_mode == LinkRenderType.SPLINE_LINK) { - if (endControl) { - innerB[0] = b[0] + endControl[0] - innerB[1] = b[1] + endControl[1] - } else { - this.#addSplineOffset(innerB, endDir, dist) + if (this.linkRenderer) { + const context = this.buildLinkRenderContext() + this.linkRenderer.renderLinkDirect( + ctx, + a, + b, + link, + skip_border, + flow, + color, + start_dir, + end_dir, + context, + { + reroute, + startControl, + endControl, + num_sublines, + disabled } - if (startControl) { - innerA[0] = a[0] + startControl[0] - innerA[1] = a[1] + startControl[1] - } else { - this.#addSplineOffset(innerA, startDir, dist) - } - path.moveTo(a[0], a[1] + offsety) - path.bezierCurveTo( - innerA[0], - innerA[1] + offsety, - innerB[0], - innerB[1] + offsety, - b[0], - b[1] + offsety - ) - - // Calculate centre point - findPointOnCurve(pos, a, b, innerA, innerB, 0.5) - - if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) { - const justPastCentre = LGraphCanvas.#lTempC - findPointOnCurve(justPastCentre, a, b, innerA, innerB, 0.51) - - linkSegment._centreAngle = Math.atan2( - justPastCentre[1] - pos[1], - justPastCentre[0] - pos[0] - ) - } - } else { - const l = this.links_render_mode == LinkRenderType.LINEAR_LINK ? 15 : 10 - switch (startDir) { - case LinkDirection.LEFT: - innerA[0] += -l - break - case LinkDirection.RIGHT: - innerA[0] += l - break - case LinkDirection.UP: - innerA[1] += -l - break - case LinkDirection.DOWN: - innerA[1] += l - break - } - switch (endDir) { - case LinkDirection.LEFT: - innerB[0] += -l - break - case LinkDirection.RIGHT: - innerB[0] += l - break - case LinkDirection.UP: - innerB[1] += -l - break - case LinkDirection.DOWN: - innerB[1] += l - break - } - if (this.links_render_mode == LinkRenderType.LINEAR_LINK) { - path.moveTo(a[0], a[1] + offsety) - path.lineTo(innerA[0], innerA[1] + offsety) - path.lineTo(innerB[0], innerB[1] + offsety) - path.lineTo(b[0], b[1] + offsety) - - // Calculate centre point - pos[0] = (innerA[0] + innerB[0]) * 0.5 - pos[1] = (innerA[1] + innerB[1]) * 0.5 - - if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) { - linkSegment._centreAngle = Math.atan2( - innerB[1] - innerA[1], - innerB[0] - innerA[0] - ) - } - } else if (this.links_render_mode == LinkRenderType.STRAIGHT_LINK) { - const midX = (innerA[0] + innerB[0]) * 0.5 - - path.moveTo(a[0], a[1]) - path.lineTo(innerA[0], innerA[1]) - path.lineTo(midX, innerA[1]) - path.lineTo(midX, innerB[1]) - path.lineTo(innerB[0], innerB[1]) - path.lineTo(b[0], b[1]) - - // Calculate centre point - pos[0] = midX - pos[1] = (innerA[1] + innerB[1]) * 0.5 - - if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) { - const diff = innerB[1] - innerA[1] - if (Math.abs(diff) < 4) linkSegment._centreAngle = 0 - else if (diff > 0) linkSegment._centreAngle = Math.PI * 0.5 - else linkSegment._centreAngle = -(Math.PI * 0.5) - } - } else { - return - } - } - } - - // rendering the outline of the connection can be a little bit slow - if (this.render_connections_border && !this.low_quality && !skip_border) { - ctx.strokeStyle = 'rgba(0,0,0,0.5)' - ctx.stroke(path) - } - - ctx.lineWidth = this.connections_width - ctx.fillStyle = ctx.strokeStyle = linkColour - ctx.stroke(path) - - // render arrow in the middle - if (this.ds.scale >= 0.6 && this.highquality_render && linkSegment) { - // render arrow - if (this.render_connection_arrows) { - // compute two points in the connection - const posA = this.computeConnectionPoint(a, b, 0.25, startDir, endDir) - const posB = this.computeConnectionPoint(a, b, 0.26, startDir, endDir) - const posC = this.computeConnectionPoint(a, b, 0.75, startDir, endDir) - const posD = this.computeConnectionPoint(a, b, 0.76, startDir, endDir) - - // compute the angle between them so the arrow points in the right direction - let angleA = 0 - let angleB = 0 - if (this.render_curved_connections) { - angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]) - angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]) - } else { - angleB = angleA = b[1] > a[1] ? 0 : Math.PI - } - - // render arrow - const transform = ctx.getTransform() - ctx.translate(posA[0], posA[1]) - ctx.rotate(angleA) - ctx.beginPath() - ctx.moveTo(-5, -3) - ctx.lineTo(0, +7) - ctx.lineTo(+5, -3) - ctx.fill() - ctx.setTransform(transform) - - ctx.translate(posC[0], posC[1]) - ctx.rotate(angleB) - ctx.beginPath() - ctx.moveTo(-5, -3) - ctx.lineTo(0, +7) - ctx.lineTo(+5, -3) - ctx.fill() - ctx.setTransform(transform) - } - - // Draw link centre marker - ctx.beginPath() - if (this.linkMarkerShape === LinkMarkerShape.Arrow) { - const transform = ctx.getTransform() - ctx.translate(pos[0], pos[1]) - if (linkSegment._centreAngle) ctx.rotate(linkSegment._centreAngle) - // The math is off, but it currently looks better in chromium - ctx.moveTo(-3.2, -5) - ctx.lineTo(+7, 0) - ctx.lineTo(-3.2, +5) - ctx.setTransform(transform) - } else if ( - this.linkMarkerShape == null || - this.linkMarkerShape === LinkMarkerShape.Circle - ) { - ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2) - } - if (disabled) { - const { fillStyle, globalAlpha } = ctx - ctx.fillStyle = this._pattern ?? '#797979' - ctx.globalAlpha = 0.75 - ctx.fill() - ctx.globalAlpha = globalAlpha - ctx.fillStyle = fillStyle - } - ctx.fill() - - if (LLink._drawDebug) { - const { fillStyle, font, globalAlpha, lineWidth, strokeStyle } = ctx - ctx.globalAlpha = 1 - ctx.lineWidth = 4 - ctx.fillStyle = 'white' - ctx.strokeStyle = 'black' - ctx.font = '16px Arial' - - const text = String(linkSegment.id) - const { width, actualBoundingBoxAscent } = ctx.measureText(text) - const x = pos[0] - width * 0.5 - const y = pos[1] + actualBoundingBoxAscent * 0.5 - ctx.strokeText(text, x, y) - ctx.fillText(text, x, y) - - ctx.font = font - ctx.globalAlpha = globalAlpha - ctx.lineWidth = lineWidth - ctx.fillStyle = fillStyle - ctx.strokeStyle = strokeStyle - } - } - - // render flowing points - if (flow) { - ctx.fillStyle = linkColour - for (let i = 0; i < 5; ++i) { - const f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1 - const flowPos = this.computeConnectionPoint(a, b, f, startDir, endDir) - ctx.beginPath() - ctx.arc(flowPos[0], flowPos[1], 5, 0, 2 * Math.PI) - ctx.fill() - } - } - } - - /** - * Finds a point along a spline represented by a to b, with spline endpoint directions dictacted by start_dir and end_dir. - * @param a Start point - * @param b End point - * @param t Time: distance between points (e.g 0.25 is 25% along the line) - * @param start_dir Spline start direction - * @param end_dir Spline end direction - * @returns The point at {@link t} distance along the spline a-b. - */ - computeConnectionPoint( - a: ReadOnlyPoint, - b: ReadOnlyPoint, - t: number, - start_dir: LinkDirection, - end_dir: LinkDirection - ): Point { - start_dir ||= LinkDirection.RIGHT - end_dir ||= LinkDirection.LEFT - - const dist = distance(a, b) - const pa: Point = [a[0], a[1]] - const pb: Point = [b[0], b[1]] - - this.#addSplineOffset(pa, start_dir, dist) - this.#addSplineOffset(pb, end_dir, dist) - - const c1 = (1 - t) * (1 - t) * (1 - t) - const c2 = 3 * ((1 - t) * (1 - t)) * t - const c3 = 3 * (1 - t) * (t * t) - const c4 = t * t * t - - const x = c1 * a[0] + c2 * pa[0] + c3 * pb[0] + c4 * b[0] - const y = c1 * a[1] + c2 * pa[1] + c3 * pb[1] + c4 * b[1] - return [x, y] - } - - /** - * Modifies an existing point, adding a single-axis offset. - * @param point The point to add the offset to - * @param direction The direction to add the offset in - * @param dist Distance to offset - * @param factor Distance is mulitplied by this value. Default: 0.25 - */ - #addSplineOffset( - point: Point, - direction: LinkDirection, - dist: number, - factor = 0.25 - ): void { - switch (direction) { - case LinkDirection.LEFT: - point[0] += dist * -factor - break - case LinkDirection.RIGHT: - point[0] += dist * factor - break - case LinkDirection.UP: - point[1] += dist * -factor - break - case LinkDirection.DOWN: - point[1] += dist * factor - break + ) } } @@ -6336,6 +6161,8 @@ export class LGraphCanvas : segment.id if (linkId !== undefined) { graph.removeLink(linkId) + // Clean up layout store + layoutStore.deleteLinkLayout(linkId) } break } @@ -8413,11 +8240,22 @@ export class LGraphCanvas // Check for reroutes if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) { - const reroute = this.graph.getRerouteOnPos( - event.canvasX, - event.canvasY, - this.#visibleReroutes - ) + // Try layout store first, fallback to old method + const rerouteLayout = layoutStore.queryRerouteAtPoint({ + x: event.canvasX, + y: event.canvasY + }) + + let reroute: Reroute | undefined + if (rerouteLayout) { + reroute = this.graph.getReroute(rerouteLayout.id) + } else { + reroute = this.graph.getRerouteOnPos( + event.canvasX, + event.canvasY, + this.#visibleReroutes + ) + } if (reroute) { menu_info.unshift( { diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 607c4e55a..a68f2a82d 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,3 +1,13 @@ +import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' +import { + type SlotPositionContext, + calculateInputSlotPos, + calculateInputSlotPosFromSlot, + calculateOutputSlotPos +} from '@/renderer/core/canvas/litegraph/slotCalculations' +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' + import type { DragAndScale } from './DragAndScale' import type { LGraph } from './LGraph' import { BadgePosition, LGraphBadge } from './LGraphBadge' @@ -258,6 +268,10 @@ export class LGraphNode properties_info: INodePropertyInfo[] = [] flags: INodeFlags = {} widgets?: IBaseWidget[] + + /** Property manager for this node */ + changeTracker: LGraphNodeProperties + /** * The amount of space available for widgets to grow into. * @see {@link layoutWidgets} @@ -729,6 +743,37 @@ export class LGraphNode error: this.#getErrorStrokeStyle, selected: this.#getSelectedStrokeStyle } + + // Assign onMouseDown implementation + this.onMouseDown = ( + // @ts-expect-error - CanvasPointerEvent type needs fixing + e: CanvasPointerEvent, + pos: Point, + canvas: LGraphCanvas + ): boolean => { + // Check for title button clicks (only if not collapsed) + if (this.title_buttons?.length && !this.flags.collapsed) { + // pos contains the offset from the node's position, so we need to use node-relative coordinates + const nodeRelativeX = pos[0] + const nodeRelativeY = pos[1] + + for (let i = 0; i < this.title_buttons.length; i++) { + const button = this.title_buttons[i] + if ( + button.visible && + button.isPointInside(nodeRelativeX, nodeRelativeY) + ) { + this.onTitleButtonClick(button, canvas) + return true // Prevent default behavior + } + } + } + + return false // Allow default behavior + } + + // Initialize property manager with tracked properties + this.changeTracker = new LGraphNodeProperties(this) } /** Internal callback for subgraph nodes. Do not implement externally. */ @@ -1941,6 +1986,14 @@ export class LGraphNode move(deltaX: number, deltaY: number): void { if (this.pinned) return + // If Vue nodes mode is enabled, skip LiteGraph's direct position update + // The layout store will handle the movement and sync back to LiteGraph + if (LiteGraph.vueNodesMode) { + // Vue nodes handle their own dragging through the layout store + // This prevents the snap-back issue from conflicting position updates + return + } + this.pos[0] += deltaX this.pos[1] += deltaY } @@ -2745,6 +2798,8 @@ export class LGraphNode const { graph } = this if (!graph) throw new NullGraphError() + const layoutMutations = useLayoutMutations() + const outputIndex = this.outputs.indexOf(output) if (outputIndex === -1) { console.warn('connectSlots: output not found') @@ -2803,6 +2858,16 @@ export class LGraphNode // add to graph links list graph._links.set(link.id, link) + // Register link in Layout Store for spatial tracking + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.createLink( + link.id, + this.id, + outputIndex, + inputNode.id, + inputIndex + ) + // connect in output output.links ??= [] output.links.push(link.id) @@ -3204,6 +3269,25 @@ export class LGraphNode return this.outputs.filter((slot: INodeOutputSlot) => !slot.pos) } + /** + * Get the context needed for slot position calculations + * @internal + */ + #getSlotPositionContext(): SlotPositionContext { + return { + nodeX: this.pos[0], + nodeY: this.pos[1], + nodeWidth: this.size[0], + nodeHeight: this.size[1], + collapsed: this.flags.collapsed ?? false, + collapsedWidth: this._collapsed_width, + slotStartY: this.constructor.slot_start_y, + inputs: this.inputs, + outputs: this.outputs, + widgets: this.widgets + } + } + /** * Gets the position of an input slot, in graph co-ordinates. * @@ -3212,7 +3296,7 @@ export class LGraphNode * @returns Position of the input slot */ getInputPos(slot: number): Point { - return this.getInputSlotPos(this.inputs[slot]) + return calculateInputSlotPos(this.#getSlotPositionContext(), slot) } /** @@ -3221,25 +3305,7 @@ export class LGraphNode * @returns Position of the centre of the input slot in graph co-ordinates. */ getInputSlotPos(input: INodeInputSlot): Point { - const { - pos: [nodeX, nodeY] - } = this - - if (this.flags.collapsed) { - const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 - return [nodeX, nodeY - halfTitle] - } - - const { pos } = input - if (pos) return [nodeX + pos[0], nodeY + pos[1]] - - // default vertical slots - const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 - const nodeOffsetY = this.constructor.slot_start_y || 0 - const slotIndex = this.#defaultVerticalInputs.indexOf(input) - const slotY = (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT - - return [nodeX + offsetX, nodeY + slotY + nodeOffsetY] + return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input) } /** @@ -3250,29 +3316,7 @@ export class LGraphNode * @returns Position of the output slot */ getOutputPos(slot: number): Point { - const { - pos: [nodeX, nodeY], - outputs, - size: [width] - } = this - - if (this.flags.collapsed) { - const width = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH - const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 - return [nodeX + width, nodeY - halfTitle] - } - - const outputPos = outputs?.[slot]?.pos - if (outputPos) return [nodeX + outputPos[0], nodeY + outputPos[1]] - - // default vertical slots - const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 - const nodeOffsetY = this.constructor.slot_start_y || 0 - const slotIndex = this.#defaultVerticalOutputs.indexOf(this.outputs[slot]) - const slotY = (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT - - // TODO: Why +1? - return [nodeX + width + 1 - offsetX, nodeY + slotY + nodeOffsetY] + return calculateOutputSlotPos(this.#getSlotPositionContext(), slot) } /** @inheritdoc */ @@ -3818,12 +3862,33 @@ export class LGraphNode ? this.getInputPos(slotIndex) : this.getOutputPos(slotIndex) - slot.boundingRect[0] = pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 - slot.boundingRect[1] = pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 - slot.boundingRect[2] = slot.isWidgetInputSlot - ? BaseWidget.margin - : LiteGraph.NODE_SLOT_HEIGHT - slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT + if (LiteGraph.vueNodesMode) { + // Vue-based slot dimensions + const dimensions = LiteGraph.COMFY_VUE_NODE_DIMENSIONS.components + + if (slot.isWidgetInputSlot) { + // Widget slots have a 20x20 clickable area centered at the position + slot.boundingRect[0] = pos[0] - 10 + slot.boundingRect[1] = pos[1] - 10 + slot.boundingRect[2] = 20 + slot.boundingRect[3] = 20 + } else { + // Regular slots have a 20x20 clickable area for the connector + // but the full slot height for vertical spacing + slot.boundingRect[0] = pos[0] - 10 + slot.boundingRect[1] = pos[1] - dimensions.SLOT_HEIGHT / 2 + slot.boundingRect[2] = 20 + slot.boundingRect[3] = dimensions.SLOT_HEIGHT + } + } else { + // Traditional LiteGraph dimensions + slot.boundingRect[0] = pos[0] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 + slot.boundingRect[1] = pos[1] - LiteGraph.NODE_SLOT_HEIGHT * 0.5 + slot.boundingRect[2] = slot.isWidgetInputSlot + ? BaseWidget.margin + : LiteGraph.NODE_SLOT_HEIGHT + slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT + } } #measureSlots(): ReadOnlyRect | null { @@ -4019,14 +4084,26 @@ export class LGraphNode } if (!slotByWidgetName.size) return - for (const widget of this.widgets) { - const slot = slotByWidgetName.get(widget.name) - if (!slot) continue + // Only set custom pos if not using Vue positioning + // Vue positioning calculates widget slot positions dynamically + if (!LiteGraph.vueNodesMode) { + for (const widget of this.widgets) { + const slot = slotByWidgetName.get(widget.name) + if (!slot) continue - const actualSlot = this.#concreteInputs[slot.index] - const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 - actualSlot.pos = [offset, widget.y + offset] - this.#measureSlot(actualSlot, slot.index, true) + const actualSlot = this.#concreteInputs[slot.index] + const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + actualSlot.pos = [offset, widget.y + offset] + this.#measureSlot(actualSlot, slot.index, true) + } + } else { + // For Vue positioning, just measure the slots without setting pos + for (const widget of this.widgets) { + const slot = slotByWidgetName.get(widget.name) + if (!slot) continue + + this.#measureSlot(this.#concreteInputs[slot.index], slot.index, true) + } } } diff --git a/src/lib/litegraph/src/LGraphNodeProperties.ts b/src/lib/litegraph/src/LGraphNodeProperties.ts new file mode 100644 index 000000000..33eafa02f --- /dev/null +++ b/src/lib/litegraph/src/LGraphNodeProperties.ts @@ -0,0 +1,176 @@ +import type { LGraphNode } from './LGraphNode' + +/** + * Default properties to track + */ +const DEFAULT_TRACKED_PROPERTIES: string[] = ['title', 'flags.collapsed'] + +/** + * Manages node properties with optional change tracking and instrumentation. + */ +export class LGraphNodeProperties { + /** The node this property manager belongs to */ + node: LGraphNode + + /** Set of property paths that have been instrumented */ + #instrumentedPaths = new Set() + + constructor(node: LGraphNode) { + this.node = node + + this.#setupInstrumentation() + } + + /** + * Sets up property instrumentation for all tracked properties + */ + #setupInstrumentation(): void { + for (const path of DEFAULT_TRACKED_PROPERTIES) { + this.#instrumentProperty(path) + } + } + + /** + * Instruments a single property to track changes + */ + #instrumentProperty(path: string): void { + const parts = path.split('.') + + if (parts.length > 1) { + this.#ensureNestedPath(path) + } + + let targetObject: any = this.node + let propertyName = parts[0] + + if (parts.length > 1) { + for (let i = 0; i < parts.length - 1; i++) { + targetObject = targetObject[parts[i]] + } + propertyName = parts.at(-1)! + } + + const hasProperty = Object.prototype.hasOwnProperty.call( + targetObject, + propertyName + ) + const currentValue = targetObject[propertyName] + + if (!hasProperty) { + let value: any = undefined + + Object.defineProperty(targetObject, propertyName, { + get: () => value, + set: (newValue: any) => { + const oldValue = value + value = newValue + this.#emitPropertyChange(path, oldValue, newValue) + + // Update enumerable: true for non-undefined values, false for undefined + const shouldBeEnumerable = newValue !== undefined + const currentDescriptor = Object.getOwnPropertyDescriptor( + targetObject, + propertyName + ) + if ( + currentDescriptor && + currentDescriptor.enumerable !== shouldBeEnumerable + ) { + Object.defineProperty(targetObject, propertyName, { + ...currentDescriptor, + enumerable: shouldBeEnumerable + }) + } + }, + enumerable: false, + configurable: true + }) + } else { + Object.defineProperty( + targetObject, + propertyName, + this.#createInstrumentedDescriptor(path, currentValue) + ) + } + + this.#instrumentedPaths.add(path) + } + + /** + * Creates a property descriptor that emits change events + */ + #createInstrumentedDescriptor( + propertyPath: string, + initialValue: any + ): PropertyDescriptor { + let value = initialValue + + return { + get: () => value, + set: (newValue: any) => { + const oldValue = value + value = newValue + this.#emitPropertyChange(propertyPath, oldValue, newValue) + }, + enumerable: true, + configurable: true + } + } + + /** + * Emits a property change event if the node is connected to a graph + */ + #emitPropertyChange( + propertyPath: string, + oldValue: any, + newValue: any + ): void { + if (oldValue !== newValue && this.node.graph) { + this.node.graph.trigger('node:property:changed', { + nodeId: this.node.id, + property: propertyPath, + oldValue, + newValue + }) + } + } + + /** + * Ensures parent objects exist for nested properties + */ + #ensureNestedPath(path: string): void { + const parts = path.split('.') + let current: any = this.node + + // Create all parent objects except the last property + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i] + if (!current[part]) { + current[part] = {} + } + current = current[part] + } + } + + /** + * Checks if a property is being tracked + */ + isTracked(path: string): boolean { + return this.#instrumentedPaths.has(path) + } + + /** + * Gets the list of tracked properties + */ + getTrackedProperties(): string[] { + return [...DEFAULT_TRACKED_PROPERTIES] + } + + /** + * Custom toJSON method for JSON.stringify + * Returns undefined to exclude from serialization since we only use defaults + */ + toJSON(): any { + return undefined + } +} diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts index bd36719ed..58ae4e090 100644 --- a/src/lib/litegraph/src/LLink.ts +++ b/src/lib/litegraph/src/LLink.ts @@ -2,6 +2,8 @@ import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants' +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' import type { LGraphNode, NodeId } from './LGraphNode' import type { Reroute, RerouteId } from './Reroute' @@ -14,13 +16,14 @@ import type { LinkSegment, ReadonlyLinkNetwork } from './interfaces' -import { Subgraph } from './litegraph' import type { Serialisable, SerialisableLLink, SubgraphIO } from './types/serialisation' +const layoutMutations = useLayoutMutations() + export type LinkId = number export type SerialisedLLinkArray = [ @@ -460,19 +463,15 @@ export class LLink implements LinkSegment, Serialisable { reroute.linkIds.delete(this.id) if (!keepReroutes && !reroute.totalLinks) { network.reroutes.delete(reroute.id) + // Delete reroute from Layout Store + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.deleteReroute(reroute.id) } } network.links.delete(this.id) - - if (this.originIsIoNode && network instanceof Subgraph) { - const subgraphInput = network.inputs.at(this.origin_slot) - if (!subgraphInput) - throw new Error('Invalid link - subgraph input not found') - - subgraphInput.events.dispatch('input-disconnected', { - input: subgraphInput - }) - } + // Delete link from Layout Store + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.deleteLink(this.id) } /** diff --git a/src/lib/litegraph/src/LiteGraphGlobal.ts b/src/lib/litegraph/src/LiteGraphGlobal.ts index 42ad95877..a7c1140ae 100644 --- a/src/lib/litegraph/src/LiteGraphGlobal.ts +++ b/src/lib/litegraph/src/LiteGraphGlobal.ts @@ -24,6 +24,26 @@ import { } from './types/globalEnums' import { createUuidv4 } from './utils/uuid' +/** + * Vue node dimensions configuration for the contract between LiteGraph and Vue components. + * These values ensure both systems can independently calculate node, slot, and widget positions + * to place them in identical locations. + * + * IMPORTANT: These values must match the actual rendered dimensions of Vue components + * for the positioning contract to work correctly. + */ +export const COMFY_VUE_NODE_DIMENSIONS = { + spacing: { + BETWEEN_SLOTS_AND_BODY: 8, + BETWEEN_WIDGETS: 8 + }, + components: { + HEADER_HEIGHT: 34, // 18 header + 16 padding + SLOT_HEIGHT: 24, + STANDARD_WIDGET_HEIGHT: 30 + } +} as const + /** * The Global Scope. It contains all the registered node classes. */ @@ -75,6 +95,14 @@ export class LiteGraphGlobal { WIDGET_SECONDARY_TEXT_COLOR = '#999' WIDGET_DISABLED_TEXT_COLOR = '#666' + /** + * Vue node dimensions configuration for the contract between LiteGraph and Vue components. + * These values ensure both systems can independently calculate node, slot, and widget positions + * to place them in identical locations. + */ + // WARNING THIS WILL BE REMOVED IN FAVOR OF THE SLOTS LAYOUT TREE useDomSlotRegistration + COMFY_VUE_NODE_DIMENSIONS = COMFY_VUE_NODE_DIMENSIONS + LINK_COLOR = '#9A9' EVENT_LINK_COLOR = '#A86' CONNECTING_LINK_COLOR = '#AFA' @@ -330,6 +358,18 @@ export class LiteGraphGlobal { */ saveViewportWithGraph: boolean = true + /** + * Enable Vue nodes mode for rendering and positioning. + * When true: + * - Nodes will calculate slot positions using Vue component dimensions + * - LiteGraph will skip rendering node bodies entirely + * - Vue components will handle all node rendering + * - LiteGraph continues to render connections, links, and graph background + * This should be set by the frontend when the Vue nodes feature is enabled. + * @default false + */ + vueNodesMode: boolean = false + // TODO: Remove legacy accessors LGraph = LGraph LLink = LLink diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index 886930227..4ac682599 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -1,3 +1,6 @@ +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { LayoutSource } from '@/renderer/core/layout/types' + import { LGraphBadge } from './LGraphBadge' import type { LGraphNode, NodeId } from './LGraphNode' import { LLink, type LinkId } from './LLink' @@ -15,6 +18,8 @@ import type { import { distance, isPointInRect } from './measure' import type { Serialisable, SerialisableReroute } from './types/serialisation' +const layoutMutations = useLayoutMutations() + export type RerouteId = number /** The input or output slot that an incomplete reroute link is connected to. */ @@ -407,8 +412,17 @@ export class Reroute /** @inheritdoc */ move(deltaX: number, deltaY: number) { + const previousPos = { x: this.#pos[0], y: this.#pos[1] } this.#pos[0] += deltaX this.#pos[1] += deltaY + + // Update Layout Store with new position + layoutMutations.setSource(LayoutSource.Canvas) + layoutMutations.moveReroute( + this.id, + { x: this.#pos[0], y: this.#pos[1] }, + previousPos + ) } /** @inheritdoc */ diff --git a/src/lib/litegraph/src/interfaces.ts b/src/lib/litegraph/src/interfaces.ts index 67870b390..0a34133f9 100644 --- a/src/lib/litegraph/src/interfaces.ts +++ b/src/lib/litegraph/src/interfaces.ts @@ -5,6 +5,7 @@ import type { ContextMenu } from './ContextMenu' import type { LGraphNode, NodeId } from './LGraphNode' import type { LLink, LinkId } from './LLink' import type { Reroute, RerouteId } from './Reroute' +import { SubgraphInput } from './subgraph/SubgraphInput' import type { SubgraphInputNode } from './subgraph/SubgraphInputNode' import type { SubgraphOutputNode } from './subgraph/SubgraphOutputNode' import type { LinkDirection, RenderShape } from './types/globalEnums' @@ -471,6 +472,7 @@ export interface DefaultConnectionColors { export interface ISubgraphInput extends INodeInputSlot { _listenerController?: AbortController + _subgraphSlot: SubgraphInput } /** diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index a4987b645..5be28a413 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -8,7 +8,7 @@ import type { CanvasEventDetail } from './types/events' import type { RenderShape, TitleMode } from './types/globalEnums' // Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in `configure`) -export { Subgraph } from './subgraph/Subgraph' +export { Subgraph, type GraphOrSubgraph } from './subgraph/Subgraph' export const LiteGraph = new LiteGraphGlobal() @@ -134,7 +134,8 @@ export { } from './LGraphBadge' export { LGraphCanvas, type LGraphCanvasState } from './LGraphCanvas' export { LGraphGroup } from './LGraphGroup' -export { LGraphNode, type NodeId } from './LGraphNode' +export { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode' +export { COMFY_VUE_NODE_DIMENSIONS } from './LiteGraphGlobal' export { type LinkId, LLink } from './LLink' export { createBounds } from './measure' export { Reroute, type RerouteId } from './Reroute' diff --git a/src/lib/litegraph/src/node/NodeSlot.ts b/src/lib/litegraph/src/node/NodeSlot.ts index 527c1dfac..48f0a443c 100644 --- a/src/lib/litegraph/src/node/NodeSlot.ts +++ b/src/lib/litegraph/src/node/NodeSlot.ts @@ -73,7 +73,7 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot { slot: OptionalProps, node: LGraphNode ) { - // Workaround: Ensure internal properties are not copied to the slot (_listenerController + // @ts-expect-error Workaround: Ensure internal properties are not copied to the slot (_listenerController // https://github.com/Comfy-Org/litegraph.js/issues/1138 const maybeSubgraphSlot: OptionalProps< ISubgraphInput, diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index e61a8360a..4dde6949b 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -65,6 +65,17 @@ export type IWidget = | ISliderWidget | IButtonWidget | IKnobWidget + | IFileUploadWidget + | IColorWidget + | IMarkdownWidget + | IImageWidget + | ITreeSelectWidget + | IMultiSelectWidget + | IChartWidget + | IGalleriaWidget + | IImageCompareWidget + | ISelectButtonWidget + | ITextareaWidget export interface IBooleanWidget extends IBaseWidget { type: 'toggle' @@ -138,6 +149,81 @@ export interface ICustomWidget extends IBaseWidget { value: string | object } +/** File upload widget for selecting and uploading files */ +export interface IFileUploadWidget extends IBaseWidget { + type: 'fileupload' + value: string + label?: string +} + +/** Color picker widget for selecting colors */ +export interface IColorWidget extends IBaseWidget { + type: 'color' + value: string +} + +/** Markdown widget for displaying formatted text */ +export interface IMarkdownWidget extends IBaseWidget { + type: 'markdown' + value: string +} + +/** Image display widget */ +export interface IImageWidget extends IBaseWidget { + type: 'image' + value: string +} + +/** Tree select widget for hierarchical selection */ +export interface ITreeSelectWidget + extends IBaseWidget { + type: 'treeselect' + value: string | string[] +} + +/** Multi-select widget for selecting multiple options */ +export interface IMultiSelectWidget + extends IBaseWidget { + type: 'multiselect' + value: string[] +} + +/** Chart widget for displaying data visualizations */ +export interface IChartWidget extends IBaseWidget { + type: 'chart' + value: object +} + +/** Gallery widget for displaying multiple images */ +export interface IGalleriaWidget extends IBaseWidget { + type: 'galleria' + value: string[] +} + +/** Image comparison widget for comparing two images side by side */ +export interface IImageCompareWidget + extends IBaseWidget { + type: 'imagecompare' + value: string[] +} + +/** Select button widget for selecting from a group of buttons */ +export interface ISelectButtonWidget + extends IBaseWidget< + string, + 'selectbutton', + RequiredProps, 'values'> + > { + type: 'selectbutton' + value: string +} + +/** Textarea widget for multi-line text input */ +export interface ITextareaWidget extends IBaseWidget { + type: 'textarea' + value: string +} + /** * Valid widget types. TS cannot provide easily extensible type safety for this at present. * Override linkedWidgets[] diff --git a/src/lib/litegraph/src/widgets/ChartWidget.ts b/src/lib/litegraph/src/widgets/ChartWidget.ts new file mode 100644 index 000000000..3dfbb069b --- /dev/null +++ b/src/lib/litegraph/src/widgets/ChartWidget.ts @@ -0,0 +1,50 @@ +import type { IChartWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for displaying charts and data visualizations + * This is a widget that only has a Vue widgets implementation + */ +export class ChartWidget + extends BaseWidget + implements IChartWidget +{ + override type = 'chart' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Chart: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/ColorWidget.ts b/src/lib/litegraph/src/widgets/ColorWidget.ts new file mode 100644 index 000000000..dfd1e5afb --- /dev/null +++ b/src/lib/litegraph/src/widgets/ColorWidget.ts @@ -0,0 +1,50 @@ +import type { IColorWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for displaying a color picker + * This is a widget that only has a Vue widgets implementation + */ +export class ColorWidget + extends BaseWidget + implements IColorWidget +{ + override type = 'color' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Color: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/FileUploadWidget.ts b/src/lib/litegraph/src/widgets/FileUploadWidget.ts new file mode 100644 index 000000000..5025017ed --- /dev/null +++ b/src/lib/litegraph/src/widgets/FileUploadWidget.ts @@ -0,0 +1,50 @@ +import type { IFileUploadWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for handling file uploads + * This is a widget that only has a Vue widgets implementation + */ +export class FileUploadWidget + extends BaseWidget + implements IFileUploadWidget +{ + override type = 'fileupload' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Fileupload: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/GalleriaWidget.ts b/src/lib/litegraph/src/widgets/GalleriaWidget.ts new file mode 100644 index 000000000..963e517d2 --- /dev/null +++ b/src/lib/litegraph/src/widgets/GalleriaWidget.ts @@ -0,0 +1,50 @@ +import type { IGalleriaWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for displaying image galleries + * This is a widget that only has a Vue widgets implementation + */ +export class GalleriaWidget + extends BaseWidget + implements IGalleriaWidget +{ + override type = 'galleria' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Galleria: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/ImageCompareWidget.ts b/src/lib/litegraph/src/widgets/ImageCompareWidget.ts new file mode 100644 index 000000000..d24fc5d85 --- /dev/null +++ b/src/lib/litegraph/src/widgets/ImageCompareWidget.ts @@ -0,0 +1,50 @@ +import type { IImageCompareWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for comparing two images side by side + * This is a widget that only has a Vue widgets implementation + */ +export class ImageCompareWidget + extends BaseWidget + implements IImageCompareWidget +{ + override type = 'imagecompare' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'ImageCompare: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/MarkdownWidget.ts b/src/lib/litegraph/src/widgets/MarkdownWidget.ts new file mode 100644 index 000000000..aac8f2cc1 --- /dev/null +++ b/src/lib/litegraph/src/widgets/MarkdownWidget.ts @@ -0,0 +1,50 @@ +import type { IMarkdownWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for displaying markdown formatted text + * This is a widget that only has a Vue widgets implementation + */ +export class MarkdownWidget + extends BaseWidget + implements IMarkdownWidget +{ + override type = 'markdown' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Markdown: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/MultiSelectWidget.ts b/src/lib/litegraph/src/widgets/MultiSelectWidget.ts new file mode 100644 index 000000000..8201bd77d --- /dev/null +++ b/src/lib/litegraph/src/widgets/MultiSelectWidget.ts @@ -0,0 +1,50 @@ +import type { IMultiSelectWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for selecting multiple options + * This is a widget that only has a Vue widgets implementation + */ +export class MultiSelectWidget + extends BaseWidget + implements IMultiSelectWidget +{ + override type = 'multiselect' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'MultiSelect: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/SelectButtonWidget.ts b/src/lib/litegraph/src/widgets/SelectButtonWidget.ts new file mode 100644 index 000000000..65218e12e --- /dev/null +++ b/src/lib/litegraph/src/widgets/SelectButtonWidget.ts @@ -0,0 +1,50 @@ +import type { ISelectButtonWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for selecting from a group of buttons + * This is a widget that only has a Vue widgets implementation + */ +export class SelectButtonWidget + extends BaseWidget + implements ISelectButtonWidget +{ + override type = 'selectbutton' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'SelectButton: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/TextareaWidget.ts b/src/lib/litegraph/src/widgets/TextareaWidget.ts new file mode 100644 index 000000000..efd14dfe8 --- /dev/null +++ b/src/lib/litegraph/src/widgets/TextareaWidget.ts @@ -0,0 +1,50 @@ +import type { ITextareaWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for multi-line text input + * This is a widget that only has a Vue widgets implementation + */ +export class TextareaWidget + extends BaseWidget + implements ITextareaWidget +{ + override type = 'textarea' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'Textarea: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/TreeSelectWidget.ts b/src/lib/litegraph/src/widgets/TreeSelectWidget.ts new file mode 100644 index 000000000..afe3dfee7 --- /dev/null +++ b/src/lib/litegraph/src/widgets/TreeSelectWidget.ts @@ -0,0 +1,50 @@ +import type { ITreeSelectWidget } from '../types/widgets' +import { + BaseWidget, + type DrawWidgetOptions, + type WidgetEventOptions +} from './BaseWidget' + +/** + * Widget for hierarchical tree selection + * This is a widget that only has a Vue widgets implementation + */ +export class TreeSelectWidget + extends BaseWidget + implements ITreeSelectWidget +{ + override type = 'treeselect' as const + + drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { width } = options + const { y, height } = this + + const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + + ctx.fillStyle = this.background_color + ctx.fillRect(15, y, width - 30, height) + + ctx.strokeStyle = this.outline_color + ctx.strokeRect(15, y, width - 30, height) + + ctx.fillStyle = this.text_color + ctx.font = '11px monospace' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + const text = 'TreeSelect: Vue-only' + ctx.fillText(text, width / 2, y + height / 2) + + Object.assign(ctx, { + fillStyle, + strokeStyle, + textAlign, + textBaseline, + font + }) + } + + onClick(_options: WidgetEventOptions): void { + // This is a widget that only has a Vue widgets implementation + } +} diff --git a/src/lib/litegraph/src/widgets/widgetMap.ts b/src/lib/litegraph/src/widgets/widgetMap.ts index ffbb7ae69..87b8614e7 100644 --- a/src/lib/litegraph/src/widgets/widgetMap.ts +++ b/src/lib/litegraph/src/widgets/widgetMap.ts @@ -17,12 +17,22 @@ import { toClass } from '@/lib/litegraph/src/utils/type' import { BaseWidget } from './BaseWidget' import { BooleanWidget } from './BooleanWidget' import { ButtonWidget } from './ButtonWidget' +import { ChartWidget } from './ChartWidget' +import { ColorWidget } from './ColorWidget' import { ComboWidget } from './ComboWidget' +import { FileUploadWidget } from './FileUploadWidget' +import { GalleriaWidget } from './GalleriaWidget' +import { ImageCompareWidget } from './ImageCompareWidget' import { KnobWidget } from './KnobWidget' import { LegacyWidget } from './LegacyWidget' +import { MarkdownWidget } from './MarkdownWidget' +import { MultiSelectWidget } from './MultiSelectWidget' import { NumberWidget } from './NumberWidget' +import { SelectButtonWidget } from './SelectButtonWidget' import { SliderWidget } from './SliderWidget' import { TextWidget } from './TextWidget' +import { TextareaWidget } from './TextareaWidget' +import { TreeSelectWidget } from './TreeSelectWidget' export type WidgetTypeMap = { button: ButtonWidget @@ -34,6 +44,16 @@ export type WidgetTypeMap = { string: TextWidget text: TextWidget custom: LegacyWidget + fileupload: FileUploadWidget + color: ColorWidget + markdown: MarkdownWidget + treeselect: TreeSelectWidget + multiselect: MultiSelectWidget + chart: ChartWidget + galleria: GalleriaWidget + imagecompare: ImageCompareWidget + selectbutton: SelectButtonWidget + textarea: TextareaWidget [key: string]: BaseWidget } @@ -82,6 +102,26 @@ export function toConcreteWidget( return toClass(TextWidget, narrowedWidget, node) case 'text': return toClass(TextWidget, narrowedWidget, node) + case 'fileupload': + return toClass(FileUploadWidget, narrowedWidget, node) + case 'color': + return toClass(ColorWidget, narrowedWidget, node) + case 'markdown': + return toClass(MarkdownWidget, narrowedWidget, node) + case 'treeselect': + return toClass(TreeSelectWidget, narrowedWidget, node) + case 'multiselect': + return toClass(MultiSelectWidget, narrowedWidget, node) + case 'chart': + return toClass(ChartWidget, narrowedWidget, node) + case 'galleria': + return toClass(GalleriaWidget, narrowedWidget, node) + case 'imagecompare': + return toClass(ImageCompareWidget, narrowedWidget, node) + case 'selectbutton': + return toClass(SelectButtonWidget, narrowedWidget, node) + case 'textarea': + return toClass(TextareaWidget, narrowedWidget, node) default: { if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node) } diff --git a/src/lib/litegraph/test/LGraphNodeProperties.test.ts b/src/lib/litegraph/test/LGraphNodeProperties.test.ts new file mode 100644 index 000000000..512b43158 --- /dev/null +++ b/src/lib/litegraph/test/LGraphNodeProperties.test.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { LGraphNodeProperties } from '../src/LGraphNodeProperties' + +describe('LGraphNodeProperties', () => { + let mockNode: any + let mockGraph: any + + beforeEach(() => { + mockGraph = { + trigger: vi.fn() + } + + mockNode = { + id: 123, + title: 'Test Node', + flags: {}, + graph: mockGraph + } + }) + + describe('constructor', () => { + it('should initialize with default tracked properties', () => { + const propManager = new LGraphNodeProperties(mockNode) + const tracked = propManager.getTrackedProperties() + + expect(tracked).toHaveLength(2) + expect(tracked).toContain('title') + expect(tracked).toContain('flags.collapsed') + }) + }) + + describe('property tracking', () => { + it('should track changes to existing properties', () => { + new LGraphNodeProperties(mockNode) + + mockNode.title = 'New Title' + + expect(mockGraph.trigger).toHaveBeenCalledWith('node:property:changed', { + nodeId: mockNode.id, + property: 'title', + oldValue: 'Test Node', + newValue: 'New Title' + }) + }) + + it('should track changes to nested properties', () => { + new LGraphNodeProperties(mockNode) + + mockNode.flags.collapsed = true + + expect(mockGraph.trigger).toHaveBeenCalledWith('node:property:changed', { + nodeId: mockNode.id, + property: 'flags.collapsed', + oldValue: undefined, + newValue: true + }) + }) + + it("should not emit events when value doesn't change", () => { + new LGraphNodeProperties(mockNode) + + mockNode.title = 'Test Node' // Same value as original + + expect(mockGraph.trigger).toHaveBeenCalledTimes(0) + }) + + it('should not emit events when node has no graph', () => { + mockNode.graph = null + new LGraphNodeProperties(mockNode) + + // Should not throw + expect(() => { + mockNode.title = 'New Title' + }).not.toThrow() + }) + }) + + describe('isTracked', () => { + it('should correctly identify tracked properties', () => { + const propManager = new LGraphNodeProperties(mockNode) + + expect(propManager.isTracked('title')).toBe(true) + expect(propManager.isTracked('flags.collapsed')).toBe(true) + expect(propManager.isTracked('untracked')).toBe(false) + }) + }) + + describe('serialization behavior', () => { + it('should not make non-existent properties enumerable', () => { + new LGraphNodeProperties(mockNode) + + // flags.collapsed doesn't exist initially + const descriptor = Object.getOwnPropertyDescriptor( + mockNode.flags, + 'collapsed' + ) + expect(descriptor?.enumerable).toBe(false) + }) + + it('should make properties enumerable when set to non-default values', () => { + new LGraphNodeProperties(mockNode) + + mockNode.flags.collapsed = true + + const descriptor = Object.getOwnPropertyDescriptor( + mockNode.flags, + 'collapsed' + ) + expect(descriptor?.enumerable).toBe(true) + }) + + it('should make properties non-enumerable when set back to undefined', () => { + new LGraphNodeProperties(mockNode) + + mockNode.flags.collapsed = true + mockNode.flags.collapsed = undefined + + const descriptor = Object.getOwnPropertyDescriptor( + mockNode.flags, + 'collapsed' + ) + expect(descriptor?.enumerable).toBe(false) + }) + + it('should keep existing properties enumerable', () => { + // title exists initially + const initialDescriptor = Object.getOwnPropertyDescriptor( + mockNode, + 'title' + ) + expect(initialDescriptor?.enumerable).toBe(true) + + new LGraphNodeProperties(mockNode) + + const afterDescriptor = Object.getOwnPropertyDescriptor(mockNode, 'title') + expect(afterDescriptor?.enumerable).toBe(true) + }) + + it('should only include non-undefined values in JSON.stringify', () => { + new LGraphNodeProperties(mockNode) + + // Initially, flags.collapsed shouldn't appear + let json = JSON.parse(JSON.stringify(mockNode)) + expect(json.flags.collapsed).toBeUndefined() + + // After setting to true, it should appear + mockNode.flags.collapsed = true + json = JSON.parse(JSON.stringify(mockNode)) + expect(json.flags.collapsed).toBe(true) + + // After setting to false, it should still appear (false is not undefined) + mockNode.flags.collapsed = false + json = JSON.parse(JSON.stringify(mockNode)) + expect(json.flags.collapsed).toBe(false) + + // After setting back to undefined, it should disappear + mockNode.flags.collapsed = undefined + json = JSON.parse(JSON.stringify(mockNode)) + expect(json.flags.collapsed).toBeUndefined() + }) + }) +}) diff --git a/src/lib/litegraph/test/__snapshots__/ConfigureGraph.test.ts.snap b/src/lib/litegraph/test/__snapshots__/ConfigureGraph.test.ts.snap index e5adac480..80e344d5b 100644 --- a/src/lib/litegraph/test/__snapshots__/ConfigureGraph.test.ts.snap +++ b/src/lib/litegraph/test/__snapshots__/ConfigureGraph.test.ts.snap @@ -326,3 +326,331 @@ LGraph { "version": 1, } `; +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LGraph configure() > LGraph matches previous snapshot (normal configure() usage) > configuredBasicGraph 1`] = ` +LGraph { + "_groups": [ + LGraphGroup { + "_bounding": Float32Array [ + 20, + 20, + 1, + 3, + ], + "_children": Set {}, + "_nodes": [], + "_pos": Float32Array [ + 20, + 20, + ], + "_size": Float32Array [ + 1, + 3, + ], + "color": "#6029aa", + "flags": {}, + "font": undefined, + "font_size": 14, + "graph": [Circular], + "id": 123, + "isPointInside": [Function], + "selected": undefined, + "setDirtyCanvas": [Function], + "title": "A group to test with", + }, + ], + "_input_nodes": undefined, + "_last_trigger_time": undefined, + "_links": Map {}, + "_nodes": [ + LGraphNode { + "_collapsed_width": undefined, + "_level": undefined, + "_pos": Float32Array [ + 10, + 10, + ], + "_posSize": Float32Array [ + 10, + 10, + 140, + 60, + ], + "_relative_id": undefined, + "_shape": undefined, + "_size": Float32Array [ + 140, + 60, + ], + "action_call": undefined, + "action_triggered": undefined, + "badgePosition": "top-left", + "badges": [], + "bgcolor": undefined, + "block_delete": undefined, + "boxcolor": undefined, + "changeTracker": undefined, + "clip_area": undefined, + "clonable": undefined, + "color": undefined, + "console": undefined, + "exec_version": undefined, + "execute_triggered": undefined, + "flags": {}, + "freeWidgetSpace": undefined, + "gotFocusAt": undefined, + "graph": [Circular], + "has_errors": undefined, + "id": 1, + "ignore_remove": undefined, + "inputs": [], + "last_serialization": undefined, + "locked": undefined, + "lostFocusAt": undefined, + "mode": 0, + "mouseOver": undefined, + "order": 0, + "outputs": [], + "progress": undefined, + "properties": {}, + "properties_info": [], + "redraw_on_mouse": undefined, + "removable": undefined, + "resizable": undefined, + "selected": undefined, + "serialize_widgets": undefined, + "showAdvanced": undefined, + "strokeStyles": { + "error": [Function], + "selected": [Function], + }, + "title": "LGraphNode", + "type": "mustBeSet", + "widgets": undefined, + "widgets_start_y": undefined, + "widgets_up": undefined, + }, + ], + "_nodes_by_id": { + "1": LGraphNode { + "_collapsed_width": undefined, + "_level": undefined, + "_pos": Float32Array [ + 10, + 10, + ], + "_posSize": Float32Array [ + 10, + 10, + 140, + 60, + ], + "_relative_id": undefined, + "_shape": undefined, + "_size": Float32Array [ + 140, + 60, + ], + "action_call": undefined, + "action_triggered": undefined, + "badgePosition": "top-left", + "badges": [], + "bgcolor": undefined, + "block_delete": undefined, + "boxcolor": undefined, + "changeTracker": undefined, + "clip_area": undefined, + "clonable": undefined, + "color": undefined, + "console": undefined, + "exec_version": undefined, + "execute_triggered": undefined, + "flags": {}, + "freeWidgetSpace": undefined, + "gotFocusAt": undefined, + "graph": [Circular], + "has_errors": undefined, + "id": 1, + "ignore_remove": undefined, + "inputs": [], + "last_serialization": undefined, + "locked": undefined, + "lostFocusAt": undefined, + "mode": 0, + "mouseOver": undefined, + "order": 0, + "outputs": [], + "progress": undefined, + "properties": {}, + "properties_info": [], + "redraw_on_mouse": undefined, + "removable": undefined, + "resizable": undefined, + "selected": undefined, + "serialize_widgets": undefined, + "showAdvanced": undefined, + "strokeStyles": { + "error": [Function], + "selected": [Function], + }, + "title": "LGraphNode", + "type": "mustBeSet", + "widgets": undefined, + "widgets_start_y": undefined, + "widgets_up": undefined, + }, + }, + "_nodes_executable": [], + "_nodes_in_order": [ + LGraphNode { + "_collapsed_width": undefined, + "_level": undefined, + "_pos": Float32Array [ + 10, + 10, + ], + "_posSize": Float32Array [ + 10, + 10, + 140, + 60, + ], + "_relative_id": undefined, + "_shape": undefined, + "_size": Float32Array [ + 140, + 60, + ], + "action_call": undefined, + "action_triggered": undefined, + "badgePosition": "top-left", + "badges": [], + "bgcolor": undefined, + "block_delete": undefined, + "boxcolor": undefined, + "changeTracker": undefined, + "clip_area": undefined, + "clonable": undefined, + "color": undefined, + "console": undefined, + "exec_version": undefined, + "execute_triggered": undefined, + "flags": {}, + "freeWidgetSpace": undefined, + "gotFocusAt": undefined, + "graph": [Circular], + "has_errors": undefined, + "id": 1, + "ignore_remove": undefined, + "inputs": [], + "last_serialization": undefined, + "locked": undefined, + "lostFocusAt": undefined, + "mode": 0, + "mouseOver": undefined, + "order": 0, + "outputs": [], + "progress": undefined, + "properties": {}, + "properties_info": [], + "redraw_on_mouse": undefined, + "removable": undefined, + "resizable": undefined, + "selected": undefined, + "serialize_widgets": undefined, + "showAdvanced": undefined, + "strokeStyles": { + "error": [Function], + "selected": [Function], + }, + "title": "LGraphNode", + "type": "mustBeSet", + "widgets": undefined, + "widgets_start_y": undefined, + "widgets_up": undefined, + }, + ], + "_subgraphs": Map {}, + "_version": 3, + "catch_errors": true, + "config": {}, + "elapsed_time": 0.01, + "errors_in_execution": undefined, + "events": CustomEventTarget {}, + "execution_time": undefined, + "execution_timer_id": undefined, + "extra": {}, + "filter": undefined, + "fixedtime": 0, + "fixedtime_lapse": 0.01, + "globaltime": 0, + "id": "ca9da7d8-fddd-4707-ad32-67be9be13140", + "iteration": 0, + "last_update_time": 0, + "links": Map {}, + "list_of_graphcanvas": null, + "nodes_actioning": [], + "nodes_executedAction": [], + "nodes_executing": [], + "revision": 0, + "runningtime": 0, + "starttime": 0, + "state": { + "lastGroupId": 123, + "lastLinkId": 0, + "lastNodeId": 1, + "lastRerouteId": 0, + }, + "status": 1, + "vars": {}, + "version": 1, +} +`; + +exports[`LGraph configure() > LGraph matches previous snapshot (normal configure() usage) > configuredMinGraph 1`] = ` +LGraph { + "_groups": [], + "_input_nodes": undefined, + "_last_trigger_time": undefined, + "_links": Map {}, + "_nodes": [], + "_nodes_by_id": {}, + "_nodes_executable": [], + "_nodes_in_order": [], + "_subgraphs": Map {}, + "_version": 0, + "catch_errors": true, + "config": {}, + "elapsed_time": 0.01, + "errors_in_execution": undefined, + "events": CustomEventTarget {}, + "execution_time": undefined, + "execution_timer_id": undefined, + "extra": {}, + "filter": undefined, + "fixedtime": 0, + "fixedtime_lapse": 0.01, + "globaltime": 0, + "id": "d175890f-716a-4ece-ba33-1d17a513b7be", + "iteration": 0, + "last_update_time": 0, + "links": Map {}, + "list_of_graphcanvas": null, + "nodes_actioning": [], + "nodes_executedAction": [], + "nodes_executing": [], + "revision": 0, + "runningtime": 0, + "starttime": 0, + "state": { + "lastGroupId": 0, + "lastLinkId": 0, + "lastNodeId": 0, + "lastRerouteId": 0, + }, + "status": 1, + "vars": {}, + "version": 1, +} +`; diff --git a/src/lib/litegraph/test/__snapshots__/LGraph.test.ts.snap b/src/lib/litegraph/test/__snapshots__/LGraph.test.ts.snap index 17253de25..628c8e3a4 100644 --- a/src/lib/litegraph/test/__snapshots__/LGraph.test.ts.snap +++ b/src/lib/litegraph/test/__snapshots__/LGraph.test.ts.snap @@ -62,6 +62,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, @@ -133,6 +134,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, @@ -205,6 +207,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, diff --git a/src/lib/litegraph/test/__snapshots__/LGraph_constructor.test.ts.snap b/src/lib/litegraph/test/__snapshots__/LGraph_constructor.test.ts.snap index 92afb55e0..106f0d221 100644 --- a/src/lib/litegraph/test/__snapshots__/LGraph_constructor.test.ts.snap +++ b/src/lib/litegraph/test/__snapshots__/LGraph_constructor.test.ts.snap @@ -62,6 +62,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, @@ -131,6 +132,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, @@ -201,6 +203,7 @@ LGraph { "bgcolor": undefined, "block_delete": undefined, "boxcolor": undefined, + "changeTracker": undefined, "clip_area": undefined, "clonable": undefined, "color": undefined, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 8000d087c..761a8bace 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -56,6 +56,7 @@ "no": "No", "cancel": "Cancel", "close": "Close", + "dropYourFileOr": "Drop your file or", "back": "Back", "next": "Next", "install": "Install", @@ -151,7 +152,12 @@ "noAudioRecorded": "No audio recorded", "nodesRunning": "nodes running", "duplicate": "Duplicate", - "moreWorkflows": "More workflows" + "moreWorkflows": "More workflows", + "nodeRenderError": "Node Render Error", + "nodeContentError": "Node Content Error", + "nodeHeaderError": "Node Header Error", + "nodeSlotsError": "Node Slots Error", + "nodeWidgetsError": "Node Widgets Error" }, "manager": { "title": "Custom Nodes Manager", @@ -1089,14 +1095,27 @@ "Next Opened Workflow": "Next Opened Workflow", "Previous Opened Workflow": "Previous Opened Workflow", "Toggle Search Box": "Toggle Search Box", + "Bottom Panel": "Bottom Panel", "Toggle Bottom Panel": "Toggle Bottom Panel", + "Show Keybindings Dialog": "Show Keybindings Dialog", "Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel", "Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel", + "Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel", + "Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel", "Toggle Focus Mode": "Toggle Focus Mode", + "Focus Mode": "Focus Mode", + "Model Library": "Model Library", + "Node Library": "Node Library", + "Queue Panel": "Queue Panel", + "Workflows": "Workflows", "Toggle Model Library Sidebar": "Toggle Model Library Sidebar", "Toggle Node Library Sidebar": "Toggle Node Library Sidebar", "Toggle Queue Sidebar": "Toggle Queue Sidebar", - "Toggle Workflows Sidebar": "Toggle Workflows Sidebar" + "Toggle Workflows Sidebar": "Toggle Workflows Sidebar", + "sideToolbar_modelLibrary": "sideToolbar.modelLibrary", + "sideToolbar_nodeLibrary": "sideToolbar.nodeLibrary", + "sideToolbar_queue": "sideToolbar.queue", + "sideToolbar_workflows": "sideToolbar.workflows" }, "desktopMenu": { "reinstall": "Reinstall", @@ -1156,7 +1175,8 @@ "Credits": "Credits", "API Nodes": "API Nodes", "Notification Preferences": "Notification Preferences", - "3DViewer": "3DViewer" + "3DViewer": "3DViewer", + "Vue Nodes": "Vue Nodes" }, "serverConfigItems": { "listen": { diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 3f8d206db..7c7cec305 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "Validate workflows" }, + "Comfy_VueNodes_Enabled": { + "name": "Enable Vue node rendering", + "tooltip": "Render nodes as Vue components instead of canvas elements. Experimental feature." + }, + "Comfy_VueNodes_Widgets": { + "name": "Enable Vue widgets", + "tooltip": "Render widgets as Vue components within Vue nodes." + }, "Comfy_WidgetControlMode": { "name": "Widget control mode", "tooltip": "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.", diff --git a/src/locales/es/commands.json b/src/locales/es/commands.json index 9f2ff9e05..7d2fef949 100644 --- a/src/locales/es/commands.json +++ b/src/locales/es/commands.json @@ -285,7 +285,7 @@ "label": "Alternar panel inferior de controles de vista" }, "Workspace_ToggleBottomPanel_Shortcuts": { - "label": "Mostrar diálogo de atajos de teclado" + "label": "Mostrar diálogo de combinaciones de teclas" }, "Workspace_ToggleFocusMode": { "label": "Alternar Modo de Enfoque" diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 773cf5bca..31acf70ba 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -310,6 +310,8 @@ "disabling": "Deshabilitando", "dismiss": "Descartar", "download": "Descargar", + "dropYourFileOr": "Suelta tu archivo o", + "duplicate": "Duplicar", "edit": "Editar", "empty": "Vacío", "enableAll": "Habilitar todo", @@ -805,6 +807,7 @@ "Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)", "Show Settings Dialog": "Mostrar diálogo de configuración", "Sign Out": "Cerrar sesión", + "Toggle Essential Bottom Panel": "Alternar panel inferior esencial", "Toggle Bottom Panel": "Alternar panel inferior", "Toggle Focus Mode": "Alternar modo de enfoque", "Toggle Logs Bottom Panel": "Alternar panel inferior de registros", @@ -814,7 +817,9 @@ "Toggle Search Box": "Alternar caja de búsqueda", "Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal", "Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)", + "Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista", "Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo", + "Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados", "Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados", "Undo": "Deshacer", "Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados", @@ -822,7 +827,19 @@ "Unload Models and Execution Cache": "Descargar modelos y caché de ejecución", "Workflow": "Flujo de trabajo", "Zoom In": "Acercar", - "Zoom Out": "Alejar" + "Zoom Out": "Alejar", + "Zoom to fit": "Ajustar al tamaño" + }, + "minimap": { + "nodeColors": "Colores de nodos", + "renderBypassState": "Mostrar estado de omisión", + "renderErrorState": "Mostrar estado de error", + "showGroups": "Mostrar marcos/grupos", + "showLinks": "Mostrar enlaces", + "sideToolbar_modelLibrary": "sideToolbar.bibliotecaDeModelos", + "sideToolbar_nodeLibrary": "sideToolbar.bibliotecaDeNodos", + "sideToolbar_queue": "sideToolbar.cola", + "sideToolbar_workflows": "sideToolbar.flujosDeTrabajo" }, "missingModelsDialog": { "doNotAskAgain": "No mostrar esto de nuevo", @@ -1138,6 +1155,7 @@ "UV": "UV", "User": "Usuario", "Validation": "Validación", + "Vue Nodes": "Nodos Vue", "Window": "Ventana", "Workflow": "Flujo de Trabajo" }, @@ -1625,4 +1643,4 @@ "exportWorkflow": "Exportar flujo de trabajo", "saveWorkflow": "Guardar flujo de trabajo" } -} \ No newline at end of file +} diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index 3444877ed..b70416af7 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "Validar flujos de trabajo" }, + "Comfy_VueNodes_Enabled": { + "name": "Habilitar renderizado de nodos Vue", + "tooltip": "Renderiza los nodos como componentes Vue en lugar de elementos canvas. Función experimental." + }, + "Comfy_VueNodes_Widgets": { + "name": "Habilitar widgets de Vue", + "tooltip": "Renderiza los widgets como componentes de Vue dentro de los nodos de Vue." + }, "Comfy_WidgetControlMode": { "name": "Modo de control del widget", "options": { diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 3e45f7e0f..f7201f187 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -310,6 +310,8 @@ "disabling": "Désactivation", "dismiss": "Fermer", "download": "Télécharger", + "dropYourFileOr": "Déposez votre fichier ou", + "duplicate": "Dupliquer", "edit": "Modifier", "empty": "Vide", "enableAll": "Activer tout", @@ -806,6 +808,8 @@ "Show Settings Dialog": "Afficher la boîte de dialogue des paramètres", "Sign Out": "Se déconnecter", "Toggle Essential Bottom Panel": "Basculer le panneau inférieur essentiel", + "Toggle Bottom Panel": "Basculer le panneau inférieur", + "Toggle Focus Mode": "Basculer le mode focus", "Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux", "Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles", "Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds", @@ -814,6 +818,7 @@ "Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal", "Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)", "Toggle View Controls Bottom Panel": "Basculer le panneau inférieur des contrôles d’affichage", + "Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows", "Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés", "Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés", "Undo": "Annuler", @@ -822,7 +827,20 @@ "Unload Models and Execution Cache": "Décharger les modèles et le cache d'exécution", "Workflow": "Flux de travail", "Zoom In": "Zoom avant", - "Zoom Out": "Zoom arrière" + "Zoom Out": "Zoom arrière", + "Zoom to fit": "Ajuster à l'écran" + }, + "minimap": { + "nodeColors": "Couleurs des nœuds", + "renderBypassState": "Afficher l'état de contournement", + "renderErrorState": "Afficher l'état d'erreur", + "showGroups": "Afficher les cadres/groupes", + "showLinks": "Afficher les liens", + "Zoom Out": "Zoom arrière", + "sideToolbar_modelLibrary": "Bibliothèque de modèles", + "sideToolbar_nodeLibrary": "Bibliothèque de nœuds", + "sideToolbar_queue": "File d'attente", + "sideToolbar_workflows": "Flux de travail" }, "missingModelsDialog": { "doNotAskAgain": "Ne plus afficher ce message", @@ -1138,6 +1156,7 @@ "UV": "UV", "User": "Utilisateur", "Validation": "Validation", + "Vue Nodes": "Nœuds Vue", "Window": "Fenêtre", "Workflow": "Flux de Travail" }, @@ -1625,4 +1644,4 @@ "exportWorkflow": "Exporter le flux de travail", "saveWorkflow": "Enregistrer le flux de travail" } -} \ No newline at end of file +} diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index b551b5267..5031782ca 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "Valider les flux de travail" }, + "Comfy_VueNodes_Enabled": { + "name": "Activer le rendu des nœuds Vue", + "tooltip": "Rendre les nœuds comme composants Vue au lieu d’éléments canvas. Fonctionnalité expérimentale." + }, + "Comfy_VueNodes_Widgets": { + "name": "Activer les widgets Vue", + "tooltip": "Rendre les widgets comme composants Vue à l'intérieur des nœuds Vue." + }, "Comfy_WidgetControlMode": { "name": "Mode de contrôle du widget", "options": { diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index a3ee74360..8b91c2c95 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -310,6 +310,8 @@ "disabling": "無効化", "dismiss": "閉じる", "download": "ダウンロード", + "dropYourFileOr": "ファイルをドロップするか", + "duplicate": "複製", "edit": "編集", "empty": "空", "enableAll": "すべて有効にする", @@ -807,10 +809,16 @@ "Sign Out": "サインアウト", "Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え", "Toggle Logs Bottom Panel": "ログ下部パネルの切り替え", + "Toggle Bottom Panel": "下部パネルの切り替え", + "Toggle Focus Mode": "フォーカスモードの切り替え", + "Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え", + "Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え", + "Toggle Queue Sidebar": "キューサイドバーを切り替え", "Toggle Search Box": "検索ボックスの切り替え", "Toggle Terminal Bottom Panel": "ターミナル下部パネルの切り替え", "Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)", "Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え", + "Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え", "Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え", "Undo": "元に戻す", "Ungroup selected group nodes": "選択したグループノードのグループ解除", @@ -818,7 +826,19 @@ "Unload Models and Execution Cache": "モデルと実行キャッシュのアンロード", "Workflow": "ワークフロー", "Zoom In": "ズームイン", - "Zoom Out": "ズームアウト" + "Zoom Out": "ズームアウト", + "Zoom to fit": "全体表示にズーム" + }, + "minimap": { + "nodeColors": "ノードの色", + "renderBypassState": "バイパス状態を表示", + "renderErrorState": "エラー状態を表示", + "showGroups": "フレーム/グループを表示", + "showLinks": "リンクを表示", + "sideToolbar_modelLibrary": "モデルライブラリ", + "sideToolbar_nodeLibrary": "ノードライブラリ", + "sideToolbar_queue": "キュー", + "sideToolbar_workflows": "ワークフロー" }, "missingModelsDialog": { "doNotAskAgain": "再度表示しない", @@ -1134,6 +1154,7 @@ "UV": "UV", "User": "ユーザー", "Validation": "検証", + "Vue Nodes": "Vueノード", "Window": "ウィンドウ", "Workflow": "ワークフロー" }, @@ -1621,4 +1642,4 @@ "exportWorkflow": "ワークフローをエクスポート", "saveWorkflow": "ワークフローを保存" } -} \ No newline at end of file +} diff --git a/src/locales/ja/settings.json b/src/locales/ja/settings.json index c31c323c8..beb3ff20f 100644 --- a/src/locales/ja/settings.json +++ b/src/locales/ja/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "ワークフローを検証" }, + "Comfy_VueNodes_Enabled": { + "name": "Vueノードレンダリングを有効化", + "tooltip": "ノードをキャンバス要素の代わりにVueコンポーネントとしてレンダリングします。実験的な機能です。" + }, + "Comfy_VueNodes_Widgets": { + "name": "Vueウィジェットを有効化", + "tooltip": "ウィジェットをVueノード内のVueコンポーネントとしてレンダリングします。" + }, "Comfy_WidgetControlMode": { "name": "ウィジェット制御モード", "options": { diff --git a/src/locales/ko/commands.json b/src/locales/ko/commands.json index c1b9e1a30..a389d7701 100644 --- a/src/locales/ko/commands.json +++ b/src/locales/ko/commands.json @@ -282,7 +282,7 @@ "label": "필수 하단 패널 전환" }, "Workspace_ToggleBottomPanelTab_shortcuts-view-controls": { - "label": "보기 컨트롤 하단 패널 전환" + "label": "뷰 컨트롤 하단 패널 전환" }, "Workspace_ToggleBottomPanel_Shortcuts": { "label": "키 바인딩 대화상자 표시" diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 48e48ad96..dd573dd06 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -310,6 +310,8 @@ "disabling": "비활성화 중", "dismiss": "닫기", "download": "다운로드", + "dropYourFileOr": "파일을 드롭하거나", + "duplicate": "복제", "edit": "편집", "empty": "비어 있음", "enableAll": "모두 활성화", @@ -805,16 +807,21 @@ "Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)", "Show Settings Dialog": "설정 대화상자 표시", "Sign Out": "로그아웃", + "Toggle Essential Bottom Panel": "필수 하단 패널 전환", "Toggle Bottom Panel": "하단 패널 전환", "Toggle Focus Mode": "포커스 모드 전환", "Toggle Logs Bottom Panel": "로그 하단 패널 전환", "Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환", "Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환", - "Toggle Queue Sidebar": "실행 대기열 사이드바 전환", + "Toggle Queue Sidebar": "대기열 사이드바 전환", + "Toggle Workflows Sidebar": "워크플로우 사이드바 전환", + "Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환", "Toggle Search Box": "검색 상자 전환", "Toggle Terminal Bottom Panel": "터미널 하단 패널 전환", "Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)", + "Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환", "Toggle Workflows Sidebar": "워크플로우 사이드바 전환", + "Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환", "Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환", "Undo": "실행 취소", "Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제", @@ -822,7 +829,19 @@ "Unload Models and Execution Cache": "모델 및 실행 캐시 언로드", "Workflow": "워크플로", "Zoom In": "확대", - "Zoom Out": "축소" + "Zoom Out": "축소", + "Zoom to fit": "화면에 맞추기" + }, + "minimap": { + "nodeColors": "노드 색상", + "renderBypassState": "바이패스 상태 렌더링", + "renderErrorState": "에러 상태 렌더링", + "showGroups": "프레임/그룹 표시", + "showLinks": "링크 표시", + "sideToolbar_modelLibrary": "sideToolbar.모델 라이브러리", + "sideToolbar_nodeLibrary": "sideToolbar.노드 라이브러리", + "sideToolbar_queue": "sideToolbar.대기열", + "sideToolbar_workflows": "sideToolbar.워크플로우" }, "missingModelsDialog": { "doNotAskAgain": "다시 보지 않기", @@ -1138,6 +1157,7 @@ "UV": "UV", "User": "사용자", "Validation": "검증", + "Vue Nodes": "Vue 노드", "Window": "창", "Workflow": "워크플로" }, @@ -1625,4 +1645,4 @@ "exportWorkflow": "워크플로 내보내기", "saveWorkflow": "워크플로 저장" } -} \ No newline at end of file +} diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 45117dcb6..1a57c7841 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "워크플로 유효성 검사" }, + "Comfy_VueNodes_Enabled": { + "name": "Vue 노드 렌더링 활성화", + "tooltip": "노드를 캔버스 요소 대신 Vue 컴포넌트로 렌더링합니다. 실험적인 기능입니다." + }, + "Comfy_VueNodes_Widgets": { + "name": "Vue 위젯 활성화", + "tooltip": "Vue 노드 내에서 위젯을 Vue 컴포넌트로 렌더링합니다." + }, "Comfy_WidgetControlMode": { "name": "위젯 제어 모드", "options": { diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json index ef5c89310..a1e9609de 100644 --- a/src/locales/ru/commands.json +++ b/src/locales/ru/commands.json @@ -282,7 +282,7 @@ "label": "Показать/скрыть основную нижнюю панель" }, "Workspace_ToggleBottomPanelTab_shortcuts-view-controls": { - "label": "Показать или скрыть нижнюю панель управления просмотром" + "label": "Показать/скрыть нижнюю панель управления просмотром" }, "Workspace_ToggleBottomPanel_Shortcuts": { "label": "Показать диалог клавиш" diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 6f67c4ba5..99cb25217 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -310,6 +310,8 @@ "disabling": "Отключение", "dismiss": "Закрыть", "download": "Скачать", + "dropYourFileOr": "Перетащите ваш файл или", + "duplicate": "Дублировать", "edit": "Редактировать", "empty": "Пусто", "enableAll": "Включить все", @@ -807,10 +809,16 @@ "Sign Out": "Выйти", "Toggle Essential Bottom Panel": "Показать/скрыть нижнюю панель основных элементов", "Toggle Logs Bottom Panel": "Показать/скрыть нижнюю панель логов", + "Toggle Bottom Panel": "Переключить нижнюю панель", + "Toggle Focus Mode": "Переключить режим фокуса", + "Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей", + "Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов", + "Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди", "Toggle Search Box": "Переключить поисковую панель", "Toggle Terminal Bottom Panel": "Показать/скрыть нижнюю панель терминала", "Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)", "Toggle View Controls Bottom Panel": "Показать/скрыть нижнюю панель элементов управления", + "Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов", "Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов", "Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов", "Undo": "Отменить", @@ -819,7 +827,19 @@ "Unload Models and Execution Cache": "Выгрузить модели и кэш выполнения", "Workflow": "Рабочий процесс", "Zoom In": "Увеличить", - "Zoom Out": "Уменьшить" + "Zoom Out": "Уменьшить", + "Zoom to fit": "Масштабировать по размеру" + }, + "minimap": { + "nodeColors": "Цвета узлов", + "renderBypassState": "Отображать состояние обхода", + "renderErrorState": "Отображать состояние ошибки", + "showGroups": "Показать фреймы/группы", + "showLinks": "Показать связи", + "sideToolbar_modelLibrary": "sideToolbar.каталогМоделей", + "sideToolbar_nodeLibrary": "sideToolbar.каталогУзлов", + "sideToolbar_queue": "sideToolbar.очередь", + "sideToolbar_workflows": "sideToolbar.рабочиеПроцессы" }, "missingModelsDialog": { "doNotAskAgain": "Больше не показывать это", @@ -1135,6 +1155,7 @@ "UV": "UV", "User": "Пользователь", "Validation": "Валидация", + "Vue Nodes": "Vue Nodes", "Window": "Окно", "Workflow": "Рабочий процесс" }, @@ -1622,4 +1643,4 @@ "exportWorkflow": "Экспорт рабочего процесса", "saveWorkflow": "Сохранить рабочий процесс" } -} \ No newline at end of file +} diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 76e38da04..48459266b 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "Проверка рабочих процессов" }, + "Comfy_VueNodes_Enabled": { + "name": "Включить рендеринг узлов через Vue", + "tooltip": "Отображать узлы как компоненты Vue вместо элементов canvas. Экспериментальная функция." + }, + "Comfy_VueNodes_Widgets": { + "name": "Включить виджеты Vue", + "tooltip": "Отображать виджеты как компоненты Vue внутри узлов Vue." + }, "Comfy_WidgetControlMode": { "name": "Режим управления виджетом", "options": { diff --git a/src/locales/zh-TW/main.json b/src/locales/zh-TW/main.json index c03e4bd4b..b29b7deaf 100644 --- a/src/locales/zh-TW/main.json +++ b/src/locales/zh-TW/main.json @@ -310,6 +310,8 @@ "disabling": "停用中", "dismiss": "關閉", "download": "下載", + "dropYourFileOr": "拖放您的檔案或", + "duplicate": "複製", "edit": "編輯", "empty": "空", "enableAll": "全部啟用", @@ -811,6 +813,12 @@ "Toggle Terminal Bottom Panel": "切換終端機底部面板", "Toggle Theme (Dark/Light)": "切換主題(深色/淺色)", "Toggle View Controls Bottom Panel": "切換檢視控制底部面板", + "Toggle Bottom Panel": "切換下方面板", + "Toggle Focus Mode": "切換專注模式", + "Toggle Model Library Sidebar": "切換模型庫側邊欄", + "Toggle Node Library Sidebar": "切換節點庫側邊欄", + "Toggle Queue Sidebar": "切換佇列側邊欄", + "Toggle Workflows Sidebar": "切換工作流程側邊欄", "Toggle the Custom Nodes Manager": "切換自訂節點管理器", "Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條", "Undo": "復原", @@ -1135,6 +1143,7 @@ "UV": "UV", "User": "使用者", "Validation": "驗證", + "Vue Nodes": "Vue 節點", "Window": "視窗", "Workflow": "工作流程" }, @@ -1622,4 +1631,4 @@ "exportWorkflow": "匯出工作流程", "saveWorkflow": "儲存工作流程" } -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/settings.json b/src/locales/zh-TW/settings.json index 375f751e6..06b0aefd3 100644 --- a/src/locales/zh-TW/settings.json +++ b/src/locales/zh-TW/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "驗證工作流程" }, + "Comfy_VueNodes_Enabled": { + "name": "啟用 Vue 節點渲染", + "tooltip": "將節點以 Vue 元件而非畫布元素方式渲染。實驗性功能。" + }, + "Comfy_VueNodes_Widgets": { + "name": "啟用 Vue 小工具", + "tooltip": "在 Vue 節點中以 Vue 元件渲染小工具。" + }, "Comfy_WidgetControlMode": { "name": "元件控制模式", "options": { diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json index e7b51b6eb..fb0578cf5 100644 --- a/src/locales/zh/commands.json +++ b/src/locales/zh/commands.json @@ -279,13 +279,13 @@ "label": "切换日志底部面板" }, "Workspace_ToggleBottomPanelTab_shortcuts-essentials": { - "label": "切换基础底部面板" + "label": "切換基本下方面板" }, "Workspace_ToggleBottomPanelTab_shortcuts-view-controls": { - "label": "切换视图控制底部面板" + "label": "切換檢視控制底部面板" }, "Workspace_ToggleBottomPanel_Shortcuts": { - "label": "显示快捷键对话框" + "label": "顯示快捷鍵對話框" }, "Workspace_ToggleFocusMode": { "label": "切换焦点模式" diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index d98dbc008..626c53c70 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -309,6 +309,7 @@ "disabling": "禁用中", "dismiss": "关闭", "download": "下载", + "dropYourFileOr": "拖放您的文件或", "duplicate": "复制", "edit": "编辑", "empty": "空", @@ -733,7 +734,7 @@ "instantTooltip": "工作流将会在生成完成后立即执行", "interrupt": "取消当前任务", "light": "淺色", - "manageExtensions": "管理擴充功能", + "manageExtensions": "管理扩展功能", "onChange": "更改时", "onChangeTooltip": "一旦进行更改,工作流将添加到执行队列", "queue": "队列面板", @@ -831,11 +832,14 @@ "Show Settings Dialog": "显示设置对话框", "Sign Out": "退出登录", "Toggle Essential Bottom Panel": "切换基础底部面板", + "Toggle Bottom Panel": "切换底部面板", + "Toggle Focus Mode": "切换专注模式", "Toggle Logs Bottom Panel": "切换日志底部面板", "Toggle Search Box": "切换搜索框", "Toggle Terminal Bottom Panel": "切换终端底部面板", "Toggle Theme (Dark/Light)": "切换主题(暗/亮)", "Toggle View Controls Bottom Panel": "切换视图控制底部面板", + "Toggle Workflows Sidebar": "切换工作流侧边栏", "Toggle the Custom Nodes Manager": "切换自定义节点管理器", "Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条", "Undo": "撤销", @@ -852,7 +856,11 @@ "renderBypassState": "渲染绕过状态", "renderErrorState": "渲染错误状态", "showGroups": "显示框架/分组", - "showLinks": "显示连接" + "showLinks": "显示连接", + "sideToolbar_modelLibrary": "侧边工具栏.模型库", + "sideToolbar_nodeLibrary": "侧边工具栏.节点库", + "sideToolbar_queue": "侧边工具栏.队列", + "sideToolbar_workflows": "侧边工具栏.工作流" }, "missingModelsDialog": { "doNotAskAgain": "不再显示此消息", @@ -1169,6 +1177,7 @@ "UV": "UV", "User": "用户", "Validation": "验证", + "Vue Nodes": "Vue 节点", "Window": "窗口", "Workflow": "工作流" }, @@ -1687,4 +1696,4 @@ "showMinimap": "显示小地图", "zoomToFit": "适合画面" } -} \ No newline at end of file +} diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 343454ce9..965a3cb42 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -343,6 +343,14 @@ "Comfy_Validation_Workflows": { "name": "校验工作流" }, + "Comfy_VueNodes_Enabled": { + "name": "启用 Vue 节点渲染", + "tooltip": "将节点渲染为 Vue 组件,而不是画布元素。实验性功能。" + }, + "Comfy_VueNodes_Widgets": { + "name": "启用Vue小部件", + "tooltip": "在Vue节点中将小部件渲染为Vue组件。" + }, "Comfy_WidgetControlMode": { "name": "组件控制模式", "options": { diff --git a/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts new file mode 100644 index 000000000..301996585 --- /dev/null +++ b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts @@ -0,0 +1,589 @@ +/** + * Litegraph Link Adapter + * + * Bridges the gap between litegraph's data model and the pure canvas renderer. + * Converts litegraph-specific types (LLink, LGraphNode, slots) into generic + * rendering data that can be consumed by the PathRenderer. + * Maintains backward compatibility with existing litegraph integration. + */ +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { LLink } from '@/lib/litegraph/src/LLink' +import type { Reroute } from '@/lib/litegraph/src/Reroute' +import type { + CanvasColour, + INodeInputSlot, + INodeOutputSlot, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { + LinkDirection, + LinkMarkerShape, + LinkRenderType +} from '@/lib/litegraph/src/types/globalEnums' +import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' +import { + type ArrowShape, + CanvasPathRenderer, + type Direction, + type DragLinkData, + type LinkRenderData, + type RenderContext as PathRenderContext, + type Point, + type RenderMode +} from '@/renderer/core/canvas/pathRenderer' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { Bounds } from '@/renderer/core/layout/types' + +export interface LinkRenderContext { + // Canvas settings + renderMode: LinkRenderType + connectionWidth: number + renderBorder: boolean + lowQuality: boolean + highQualityRender: boolean + scale: number + linkMarkerShape: LinkMarkerShape + renderConnectionArrows: boolean + + // State + highlightedLinks: Set + + // Colors + defaultLinkColor: CanvasColour + linkTypeColors: Record + + // Pattern for disabled links (optional) + disabledPattern?: CanvasPattern | null +} + +export interface LinkRenderOptions { + color?: CanvasColour + flow?: boolean + skipBorder?: boolean + disabled?: boolean +} + +export class LitegraphLinkAdapter { + private graph: LGraph + private pathRenderer: CanvasPathRenderer + public enableLayoutStoreWrites = true + + constructor(graph: LGraph) { + this.graph = graph + this.pathRenderer = new CanvasPathRenderer() + } + + /** + * Render a single link with all necessary data properly fetched + * Populates link.path for hit detection + */ + renderLink( + ctx: CanvasRenderingContext2D, + link: LLink, + context: LinkRenderContext, + options: LinkRenderOptions = {} + ): void { + // Get nodes from graph + const sourceNode = this.graph.getNodeById(link.origin_id) + const targetNode = this.graph.getNodeById(link.target_id) + + if (!sourceNode || !targetNode) { + console.warn(`Cannot render link ${link.id}: missing nodes`) + return + } + + // Get slots from nodes + const sourceSlot = sourceNode.outputs?.[link.origin_slot] + const targetSlot = targetNode.inputs?.[link.target_slot] + + if (!sourceSlot || !targetSlot) { + console.warn(`Cannot render link ${link.id}: missing slots`) + return + } + + // Get positions using layout tree data if available + const startPos = getSlotPosition( + sourceNode, + link.origin_slot, + false // output + ) + const endPos = getSlotPosition( + targetNode, + link.target_slot, + true // input + ) + + // Get directions from slots + const startDir = sourceSlot.dir || LinkDirection.RIGHT + const endDir = targetSlot.dir || LinkDirection.LEFT + + // Convert to pure render data + const linkData = this.convertToLinkRenderData( + link, + { x: startPos[0], y: startPos[1] }, + { x: endPos[0], y: endPos[1] }, + startDir, + endDir, + options + ) + + // Convert context + const pathContext = this.convertToPathRenderContext(context) + + // Render using pure renderer + const path = this.pathRenderer.drawLink(ctx, linkData, pathContext) + + // Store path for hit detection + link.path = path + + // Update layout store when writes are enabled (event-driven path) + if (this.enableLayoutStoreWrites && link.id !== -1) { + // Calculate bounds and center only when writing + const bounds = this.calculateLinkBounds(startPos, endPos, linkData) + const centerPos = linkData.centerPos || { + x: (startPos[0] + endPos[0]) / 2, + y: (startPos[1] + endPos[1]) / 2 + } + + layoutStore.updateLinkLayout(link.id, { + id: link.id, + path: path, + bounds: bounds, + centerPos: centerPos, + sourceNodeId: String(link.origin_id), + targetNodeId: String(link.target_id), + sourceSlot: link.origin_slot, + targetSlot: link.target_slot + }) + + // Also update segment layout for the whole link (null rerouteId means final segment) + layoutStore.updateLinkSegmentLayout(link.id, null, { + path: path, + bounds: bounds, + centerPos: centerPos + }) + } + } + + /** + * Convert litegraph link data to pure render format + */ + private convertToLinkRenderData( + link: LLink, + startPoint: Point, + endPoint: Point, + startDir: LinkDirection, + endDir: LinkDirection, + options: LinkRenderOptions + ): LinkRenderData { + return { + id: String(link.id), + startPoint, + endPoint, + startDirection: this.convertDirection(startDir), + endDirection: this.convertDirection(endDir), + color: options.color + ? String(options.color) + : link.color + ? String(link.color) + : undefined, + type: link.type !== undefined ? String(link.type) : undefined, + flow: options.flow || false, + disabled: options.disabled || false + } + } + + /** + * Convert LinkDirection enum to Direction string + */ + private convertDirection(dir: LinkDirection): Direction { + switch (dir) { + case LinkDirection.LEFT: + return 'left' + case LinkDirection.RIGHT: + return 'right' + case LinkDirection.UP: + return 'up' + case LinkDirection.DOWN: + return 'down' + default: + return 'right' + } + } + + /** + * Convert LinkRenderContext to PathRenderContext + */ + private convertToPathRenderContext( + context: LinkRenderContext + ): PathRenderContext { + // Match original arrow rendering conditions: + // Arrows only render when scale >= 0.6 AND highquality_render AND render_connection_arrows + const shouldShowArrows = + context.scale >= 0.6 && + context.highQualityRender && + context.renderConnectionArrows + + // Only show center marker when not set to None + const shouldShowCenterMarker = + context.linkMarkerShape !== LinkMarkerShape.None + + return { + style: { + mode: this.convertRenderMode(context.renderMode), + connectionWidth: context.connectionWidth, + borderWidth: context.renderBorder ? 4 : undefined, + arrowShape: this.convertArrowShape(context.linkMarkerShape), + showArrows: shouldShowArrows, + lowQuality: context.lowQuality, + // Center marker settings (matches original litegraph behavior) + showCenterMarker: shouldShowCenterMarker, + centerMarkerShape: + context.linkMarkerShape === LinkMarkerShape.Arrow + ? 'arrow' + : 'circle', + highQuality: context.highQualityRender + }, + colors: { + default: String(context.defaultLinkColor), + byType: this.convertColorMap(context.linkTypeColors), + highlighted: '#FFF' + }, + patterns: { + disabled: context.disabledPattern + }, + animation: { + time: LiteGraph.getTime() * 0.001 + }, + scale: context.scale, + highlightedIds: new Set(Array.from(context.highlightedLinks).map(String)) + } + } + + /** + * Convert LinkRenderType to RenderMode + */ + private convertRenderMode(mode: LinkRenderType): RenderMode { + switch (mode) { + case LinkRenderType.LINEAR_LINK: + return 'linear' + case LinkRenderType.STRAIGHT_LINK: + return 'straight' + case LinkRenderType.SPLINE_LINK: + default: + return 'spline' + } + } + + /** + * Convert LinkMarkerShape to ArrowShape + */ + private convertArrowShape(shape: LinkMarkerShape): ArrowShape { + switch (shape) { + case LinkMarkerShape.Circle: + return 'circle' + case LinkMarkerShape.Arrow: + default: + return 'triangle' + } + } + + /** + * Convert color map to ensure all values are strings + */ + private convertColorMap( + colors: Record + ): Record { + const result: Record = {} + for (const [key, value] of Object.entries(colors)) { + result[key] = String(value) + } + return result + } + + /** + * Apply spline offset to a point, mimicking original #addSplineOffset behavior + * Critically: does nothing for CENTER/NONE directions (no case for them) + */ + private applySplineOffset( + point: Point, + direction: LinkDirection, + distance: number + ): void { + switch (direction) { + case LinkDirection.LEFT: + point.x -= distance + break + case LinkDirection.RIGHT: + point.x += distance + break + case LinkDirection.UP: + point.y -= distance + break + case LinkDirection.DOWN: + point.y += distance + break + // CENTER and NONE: no offset applied (original behavior) + } + } + + /** + * Direct rendering method compatible with LGraphCanvas + * Converts data and delegates to pure renderer + */ + renderLinkDirect( + ctx: CanvasRenderingContext2D, + a: ReadOnlyPoint, + b: ReadOnlyPoint, + link: LLink | null, + skip_border: boolean, + flow: number | boolean | null, + color: CanvasColour | null, + start_dir: LinkDirection, + end_dir: LinkDirection, + context: LinkRenderContext, + extras: { + reroute?: Reroute + startControl?: ReadOnlyPoint + endControl?: ReadOnlyPoint + num_sublines?: number + disabled?: boolean + } = {} + ): void { + // Apply same defaults as original renderLink + const startDir = start_dir || LinkDirection.RIGHT + const endDir = end_dir || LinkDirection.LEFT + + // Convert flow to boolean + const flowBool = flow === true || (typeof flow === 'number' && flow > 0) + + // Create LinkRenderData from direct parameters + const linkData: LinkRenderData = { + id: link ? String(link.id) : 'temp', + startPoint: { x: a[0], y: a[1] }, + endPoint: { x: b[0], y: b[1] }, + startDirection: this.convertDirection(startDir), + endDirection: this.convertDirection(endDir), + color: color !== null && color !== undefined ? String(color) : undefined, + type: link?.type !== undefined ? String(link.type) : undefined, + flow: flowBool, + disabled: extras.disabled || false + } + + // Control points handling (spline mode): + // - Pre-refactor, the old renderLink honored a single provided control and + // derived the missing side via #addSplineOffset (CENTER => no offset). + // - Restore that behavior here so reroute segments render identically. + if (context.renderMode === LinkRenderType.SPLINE_LINK) { + const hasStartCtrl = !!extras.startControl + const hasEndCtrl = !!extras.endControl + + // Compute distance once for offsets + const dist = Math.sqrt( + (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) + ) + const factor = 0.25 + + const cps: Point[] = [] + + if (hasStartCtrl && hasEndCtrl) { + // Both provided explicitly + cps.push( + { + x: a[0] + (extras.startControl![0] || 0), + y: a[1] + (extras.startControl![1] || 0) + }, + { + x: b[0] + (extras.endControl![0] || 0), + y: b[1] + (extras.endControl![1] || 0) + } + ) + linkData.controlPoints = cps + } else if (hasStartCtrl && !hasEndCtrl) { + // Start provided, derive end via direction offset (CENTER => no offset) + const start = { + x: a[0] + (extras.startControl![0] || 0), + y: a[1] + (extras.startControl![1] || 0) + } + const end = { x: b[0], y: b[1] } + this.applySplineOffset(end, endDir, dist * factor) + cps.push(start, end) + linkData.controlPoints = cps + } else if (!hasStartCtrl && hasEndCtrl) { + // End provided, derive start via direction offset (CENTER => no offset) + const start = { x: a[0], y: a[1] } + this.applySplineOffset(start, startDir, dist * factor) + const end = { + x: b[0] + (extras.endControl![0] || 0), + y: b[1] + (extras.endControl![1] || 0) + } + cps.push(start, end) + linkData.controlPoints = cps + } else { + // Neither provided: derive both from directions (CENTER => no offset) + const start = { x: a[0], y: a[1] } + const end = { x: b[0], y: b[1] } + this.applySplineOffset(start, startDir, dist * factor) + this.applySplineOffset(end, endDir, dist * factor) + cps.push(start, end) + linkData.controlPoints = cps + } + } + + // Convert context + const pathContext = this.convertToPathRenderContext(context) + + // Override skip_border if needed + if (skip_border) { + pathContext.style.borderWidth = undefined + } + + // Render using pure renderer + const path = this.pathRenderer.drawLink(ctx, linkData, pathContext) + + // Store path for hit detection + const linkSegment = extras.reroute ?? link + if (linkSegment) { + linkSegment.path = path + + // Copy calculated center position back to litegraph object + // This is needed for hit detection and menu interaction + if (linkData.centerPos) { + linkSegment._pos = linkSegment._pos || new Float32Array(2) + linkSegment._pos[0] = linkData.centerPos.x + linkSegment._pos[1] = linkData.centerPos.y + + // Store center angle if calculated (for arrow markers) + if (linkData.centerAngle !== undefined) { + linkSegment._centreAngle = linkData.centerAngle + } + } + + // Update layout store when writes are enabled (event-driven path) + if (this.enableLayoutStoreWrites && link && link.id !== -1) { + // Calculate bounds and center only when writing + const bounds = this.calculateLinkBounds( + [linkData.startPoint.x, linkData.startPoint.y] as ReadOnlyPoint, + [linkData.endPoint.x, linkData.endPoint.y] as ReadOnlyPoint, + linkData + ) + const centerPos = linkData.centerPos || { + x: (linkData.startPoint.x + linkData.endPoint.x) / 2, + y: (linkData.startPoint.y + linkData.endPoint.y) / 2 + } + + // Update whole link layout (only if not a reroute segment) + if (!extras.reroute) { + layoutStore.updateLinkLayout(link.id, { + id: link.id, + path: path, + bounds: bounds, + centerPos: centerPos, + sourceNodeId: String(link.origin_id), + targetNodeId: String(link.target_id), + sourceSlot: link.origin_slot, + targetSlot: link.target_slot + }) + } + + // Always update segment layout (for both regular links and reroute segments) + const rerouteId = extras.reroute ? extras.reroute.id : null + layoutStore.updateLinkSegmentLayout(link.id, rerouteId, { + path: path, + bounds: bounds, + centerPos: centerPos + }) + } + } + } + + /** + * Render a link being dragged from a slot to mouse position + * Used during link creation/reconnection + */ + renderDraggingLink( + ctx: CanvasRenderingContext2D, + fromNode: LGraphNode | null, + fromSlot: INodeOutputSlot | INodeInputSlot, + fromSlotIndex: number, + toPosition: ReadOnlyPoint, + context: LinkRenderContext, + options: { + fromInput?: boolean + color?: CanvasColour + disabled?: boolean + } = {} + ): void { + if (!fromNode) return + + // Get slot position using layout tree if available + const slotPos = getSlotPosition( + fromNode, + fromSlotIndex, + options.fromInput || false + ) + if (!slotPos) return + + // Get slot direction + const slotDir = + fromSlot.dir || + (options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT) + + // Create drag data + const dragData: DragLinkData = { + fixedPoint: { x: slotPos[0], y: slotPos[1] }, + fixedDirection: this.convertDirection(slotDir), + dragPoint: { x: toPosition[0], y: toPosition[1] }, + color: options.color ? String(options.color) : undefined, + type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined, + disabled: options.disabled || false, + fromInput: options.fromInput || false + } + + // Convert context + const pathContext = this.convertToPathRenderContext(context) + + // Hide center marker when dragging links + pathContext.style.showCenterMarker = false + + // Render using pure renderer + this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext) + } + + /** + * Calculate bounding box for a link + * Includes padding for line width and control points + */ + private calculateLinkBounds( + startPos: ReadOnlyPoint, + endPos: ReadOnlyPoint, + linkData: LinkRenderData + ): Bounds { + let minX = Math.min(startPos[0], endPos[0]) + let maxX = Math.max(startPos[0], endPos[0]) + let minY = Math.min(startPos[1], endPos[1]) + let maxY = Math.max(startPos[1], endPos[1]) + + // Include control points if they exist (for spline links) + if (linkData.controlPoints) { + for (const cp of linkData.controlPoints) { + minX = Math.min(minX, cp.x) + maxX = Math.max(maxX, cp.x) + minY = Math.min(minY, cp.y) + maxY = Math.max(maxY, cp.y) + } + } + + // Add padding for line width and hit tolerance + const padding = 20 + + return { + x: minX - padding, + y: minY - padding, + width: maxX - minX + 2 * padding, + height: maxY - minY + 2 * padding + } + } +} diff --git a/src/renderer/core/canvas/litegraph/slotCalculations.ts b/src/renderer/core/canvas/litegraph/slotCalculations.ts new file mode 100644 index 000000000..b56d95b97 --- /dev/null +++ b/src/renderer/core/canvas/litegraph/slotCalculations.ts @@ -0,0 +1,283 @@ +/** + * Slot Position Calculations + * + * Centralized utility for calculating input/output slot positions on nodes. + * This allows both litegraph nodes and the layout system to use the same + * calculation logic while providing their own position data. + */ +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { + INodeInputSlot, + INodeOutputSlot, + INodeSlot, + Point, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { isWidgetInputSlot } from '@/lib/litegraph/src/node/slotUtils' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' + +export interface SlotPositionContext { + /** Node's X position in graph coordinates */ + nodeX: number + /** Node's Y position in graph coordinates */ + nodeY: number + /** Node's width */ + nodeWidth: number + /** Node's height */ + nodeHeight: number + /** Whether the node is collapsed */ + collapsed: boolean + /** Collapsed width (if applicable) */ + collapsedWidth?: number + /** Node constructor's slot_start_y offset */ + slotStartY?: number + /** Node's input slots */ + inputs: INodeInputSlot[] + /** Node's output slots */ + outputs: INodeOutputSlot[] + /** Node's widgets (for widget slot detection) */ + widgets?: Array<{ name?: string }> +} + +/** + * Calculate the position of an input slot in graph coordinates + * @param context Node context containing position and slot data + * @param slot The input slot index + * @returns Position of the input slot center in graph coordinates + */ +export function calculateInputSlotPos( + context: SlotPositionContext, + slot: number +): Point { + const input = context.inputs[slot] + if (!input) return [context.nodeX, context.nodeY] + + return calculateInputSlotPosFromSlot(context, input) +} + +/** + * Calculate the position of an input slot in graph coordinates + * @param context Node context containing position and slot data + * @param input The input slot object + * @returns Position of the input slot center in graph coordinates + */ +export function calculateInputSlotPosFromSlot( + context: SlotPositionContext, + input: INodeInputSlot +): Point { + const { nodeX, nodeY, collapsed } = context + + // Handle collapsed nodes + if (collapsed) { + const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 + return [nodeX, nodeY - halfTitle] + } + + // Handle hard-coded positions + const { pos } = input + if (pos) return [nodeX + pos[0], nodeY + pos[1]] + + // Check if we should use Vue positioning + if (LiteGraph.vueNodesMode) { + if (isWidgetInputSlot(input)) { + // Widget slot - pass the slot object + return calculateVueSlotPosition(context, true, input, -1) + } else { + // Regular slot - find its index in default vertical inputs + const defaultVerticalInputs = getDefaultVerticalInputs(context) + const slotIndex = defaultVerticalInputs.indexOf(input) + if (slotIndex !== -1) { + return calculateVueSlotPosition(context, true, input, slotIndex) + } + } + } + + // Default vertical slots + const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + const nodeOffsetY = context.slotStartY || 0 + const defaultVerticalInputs = getDefaultVerticalInputs(context) + const slotIndex = defaultVerticalInputs.indexOf(input) + const slotY = (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + return [nodeX + offsetX, nodeY + slotY + nodeOffsetY] +} + +/** + * Calculate the position of an output slot in graph coordinates + * @param context Node context containing position and slot data + * @param slot The output slot index + * @returns Position of the output slot center in graph coordinates + */ +export function calculateOutputSlotPos( + context: SlotPositionContext, + slot: number +): Point { + const { nodeX, nodeY, nodeWidth, collapsed, collapsedWidth, outputs } = + context + + // Handle collapsed nodes + if (collapsed) { + const width = collapsedWidth || LiteGraph.NODE_COLLAPSED_WIDTH + const halfTitle = LiteGraph.NODE_TITLE_HEIGHT * 0.5 + return [nodeX + width, nodeY - halfTitle] + } + + const outputSlot = outputs[slot] + if (!outputSlot) return [nodeX + nodeWidth, nodeY] + + // Handle hard-coded positions + const outputPos = outputSlot.pos + if (outputPos) return [nodeX + outputPos[0], nodeY + outputPos[1]] + + // Check if we should use Vue positioning + if (LiteGraph.vueNodesMode) { + const defaultVerticalOutputs = getDefaultVerticalOutputs(context) + const slotIndex = defaultVerticalOutputs.indexOf(outputSlot) + if (slotIndex !== -1) { + return calculateVueSlotPosition(context, false, outputSlot, slotIndex) + } + } + + // Default vertical slots + const offsetX = LiteGraph.NODE_SLOT_HEIGHT * 0.5 + const nodeOffsetY = context.slotStartY || 0 + const defaultVerticalOutputs = getDefaultVerticalOutputs(context) + const slotIndex = defaultVerticalOutputs.indexOf(outputSlot) + const slotY = (slotIndex + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + + // TODO: Why +1? + return [nodeX + nodeWidth + 1 - offsetX, nodeY + slotY + nodeOffsetY] +} + +/** + * Get slot position using layout tree if available, fallback to node's position + * Unified implementation used by both LitegraphLinkAdapter and useLinkLayoutSync + * @param node The LGraphNode + * @param slotIndex The slot index + * @param isInput Whether this is an input slot + * @returns Position of the slot center in graph coordinates + */ +export function getSlotPosition( + node: LGraphNode, + slotIndex: number, + isInput: boolean +): ReadOnlyPoint { + // Try to get precise position from slot layout (DOM-registered) + const slotKey = getSlotKey(String(node.id), slotIndex, isInput) + const slotLayout = layoutStore.getSlotLayout(slotKey) + if (slotLayout) { + return [slotLayout.position.x, slotLayout.position.y] + } + + // Fallback: derive position from node layout tree and slot model + const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value + + if (nodeLayout) { + // Create context from layout tree data + const context: SlotPositionContext = { + nodeX: nodeLayout.position.x, + nodeY: nodeLayout.position.y, + nodeWidth: nodeLayout.size.width, + nodeHeight: nodeLayout.size.height, + collapsed: node.flags.collapsed || false, + collapsedWidth: node._collapsed_width, + slotStartY: node.constructor.slot_start_y, + inputs: node.inputs, + outputs: node.outputs, + widgets: node.widgets + } + + // Use helper to calculate position + return isInput + ? calculateInputSlotPos(context, slotIndex) + : calculateOutputSlotPos(context, slotIndex) + } + + // Fallback to node's own methods if layout not available + return isInput ? node.getInputPos(slotIndex) : node.getOutputPos(slotIndex) +} + +/** + * Get the inputs that are not positioned with absolute coordinates + */ +function getDefaultVerticalInputs( + context: SlotPositionContext +): INodeInputSlot[] { + return context.inputs.filter( + (slot) => !slot.pos && !(context.widgets?.length && isWidgetInputSlot(slot)) + ) +} + +/** + * Get the outputs that are not positioned with absolute coordinates + */ +function getDefaultVerticalOutputs( + context: SlotPositionContext +): INodeOutputSlot[] { + return context.outputs.filter((slot) => !slot.pos) +} + +/** + * Calculate slot position using Vue node dimensions. + * This method uses the COMFY_VUE_NODE_DIMENSIONS constants to match Vue component rendering. + * @param context Node context + * @param isInput Whether this is an input slot (true) or output slot (false) + * @param slot The slot object (for widget detection) + * @param slotIndex The index of the slot in the appropriate array + * @returns The [x, y] position of the slot center in graph coordinates + */ +function calculateVueSlotPosition( + context: SlotPositionContext, + isInput: boolean, + slot: INodeSlot, + slotIndex: number +): Point { + const { nodeX, nodeY, nodeWidth, widgets } = context + const dimensions = LiteGraph.COMFY_VUE_NODE_DIMENSIONS.components + const spacing = LiteGraph.COMFY_VUE_NODE_DIMENSIONS.spacing + + let slotCenterY: number + + // IMPORTANT: LiteGraph's node position (nodeY) is at the TOP of the body (below the header) + // The header is rendered ABOVE this position at negative Y coordinates + // So we need to adjust for the difference between LiteGraph's header (30px) and Vue's header (34px) + const headerDifference = + dimensions.HEADER_HEIGHT - LiteGraph.NODE_TITLE_HEIGHT + + if (isInput && isWidgetInputSlot(slot as INodeInputSlot)) { + // Widget input slot - calculate based on widget position + // Count regular (non-widget) input slots + const regularInputCount = getDefaultVerticalInputs(context).length + + // Find widget index + const widgetIndex = + widgets?.findIndex( + (w) => w.name === (slot as INodeInputSlot).widget?.name + ) ?? 0 + + // Y position relative to the node body top (not the header) + slotCenterY = + headerDifference + + regularInputCount * dimensions.SLOT_HEIGHT + + (regularInputCount > 0 ? spacing.BETWEEN_SLOTS_AND_BODY : 0) + + widgetIndex * + (dimensions.STANDARD_WIDGET_HEIGHT + spacing.BETWEEN_WIDGETS) + + dimensions.STANDARD_WIDGET_HEIGHT / 2 + } else { + // Regular slot (input or output) + // Slots start at the top of the body, but we need to account for Vue's larger header + slotCenterY = + headerDifference + + slotIndex * dimensions.SLOT_HEIGHT + + dimensions.SLOT_HEIGHT / 2 + } + + // Calculate X position + // Input slots: 10px from left edge (center of 20x20 connector) + // Output slots: 10px from right edge (center of 20x20 connector) + const slotCenterX = isInput ? 10 : nodeWidth - 10 + + return [nodeX + slotCenterX, nodeY + slotCenterY] +} diff --git a/src/renderer/core/canvas/pathRenderer.ts b/src/renderer/core/canvas/pathRenderer.ts new file mode 100644 index 000000000..a2ee8817c --- /dev/null +++ b/src/renderer/core/canvas/pathRenderer.ts @@ -0,0 +1,820 @@ +/** + * Path Renderer + * + * Pure canvas2D rendering utility with no framework dependencies. + * Renders bezier curves, straight lines, and linear connections between points. + * Supports arrows, flow animations, and returns Path2D objects for hit detection. + * Can be reused in any canvas-based project without modification. + */ + +export interface Point { + x: number + y: number +} + +export type Direction = 'left' | 'right' | 'up' | 'down' +export type RenderMode = 'spline' | 'straight' | 'linear' +export type ArrowShape = 'triangle' | 'circle' | 'square' + +export interface LinkRenderData { + id: string + startPoint: Point + endPoint: Point + startDirection: Direction + endDirection: Direction + color?: string + type?: string + controlPoints?: Point[] + flow?: boolean + disabled?: boolean + // Optional multi-segment support + segments?: Array<{ + start: Point + end: Point + controlPoints?: Point[] + }> + // Center point storage (for hit detection and menu) + centerPos?: Point + centerAngle?: number +} + +export interface RenderStyle { + mode: RenderMode + connectionWidth: number + borderWidth?: number + arrowShape?: ArrowShape + showArrows?: boolean + lowQuality?: boolean + // Center marker properties + showCenterMarker?: boolean + centerMarkerShape?: 'circle' | 'arrow' + highQuality?: boolean +} + +export interface RenderColors { + default: string + byType: Record + highlighted: string +} + +export interface RenderContext { + style: RenderStyle + colors: RenderColors + patterns?: { + disabled?: CanvasPattern | null + } + animation?: { + time: number // Seconds for flow animation + } + scale?: number // Canvas scale for quality adjustments + highlightedIds?: Set +} + +export interface DragLinkData { + /** Fixed end - the slot being dragged from */ + fixedPoint: Point + fixedDirection: Direction + /** Moving end - follows mouse */ + dragPoint: Point + dragDirection?: Direction + /** Visual properties */ + color?: string + type?: string + disabled?: boolean + /** Whether dragging from input (reverse direction) */ + fromInput?: boolean +} + +export class CanvasPathRenderer { + /** + * Draw a link between two points + * Returns a Path2D object for hit detection + */ + drawLink( + ctx: CanvasRenderingContext2D, + link: LinkRenderData, + context: RenderContext + ): Path2D { + const path = new Path2D() + + // Determine final color + const isHighlighted = context.highlightedIds?.has(link.id) ?? false + const color = this.determineLinkColor(link, context, isHighlighted) + + // Save context state + ctx.save() + + // Apply disabled pattern if needed + if (link.disabled && context.patterns?.disabled) { + ctx.strokeStyle = context.patterns.disabled + } else { + ctx.strokeStyle = color + } + + // Set line properties + ctx.lineWidth = context.style.connectionWidth + ctx.lineJoin = 'round' + + // Draw border if needed + if (context.style.borderWidth && !context.style.lowQuality) { + this.drawLinkPath( + ctx, + path, + link, + context, + context.style.connectionWidth + context.style.borderWidth, + 'rgba(0,0,0,0.5)' + ) + } + + // Draw main link + this.drawLinkPath( + ctx, + path, + link, + context, + context.style.connectionWidth, + color + ) + + // Calculate and store center position + this.calculateCenterPoint(link, context) + + // Draw arrows if needed + if (context.style.showArrows) { + this.drawArrows(ctx, link, context, color) + } + + // Draw center marker if needed (for link menu interaction) + if ( + context.style.showCenterMarker && + context.scale && + context.scale >= 0.6 && + context.style.highQuality + ) { + this.drawCenterMarker(ctx, link, context, color) + } + + // Draw flow animation if needed + if (link.flow && context.animation) { + this.drawFlowAnimation(ctx, path, link, context) + } + + ctx.restore() + + return path + } + + private determineLinkColor( + link: LinkRenderData, + context: RenderContext, + isHighlighted: boolean + ): string { + if (isHighlighted) { + return context.colors.highlighted + } + if (link.color) { + return link.color + } + if (link.type && context.colors.byType[link.type]) { + return context.colors.byType[link.type] + } + return context.colors.default + } + + private drawLinkPath( + ctx: CanvasRenderingContext2D, + path: Path2D, + link: LinkRenderData, + context: RenderContext, + lineWidth: number, + color: string + ): void { + ctx.strokeStyle = color + ctx.lineWidth = lineWidth + + const start = link.startPoint + const end = link.endPoint + + // Build the path based on render mode + if (context.style.mode === 'linear') { + this.buildLinearPath( + path, + start, + end, + link.startDirection, + link.endDirection + ) + } else if (context.style.mode === 'straight') { + this.buildStraightPath( + path, + start, + end, + link.startDirection, + link.endDirection + ) + } else { + // Spline mode (default) + this.buildSplinePath( + path, + start, + end, + link.startDirection, + link.endDirection, + link.controlPoints + ) + } + + ctx.stroke(path) + } + + private buildLinearPath( + path: Path2D, + start: Point, + end: Point, + startDir: Direction, + endDir: Direction + ): void { + // Match original litegraph LINEAR_LINK mode with 4-point path + const l = 15 // offset distance for control points + + const innerA = { x: start.x, y: start.y } + const innerB = { x: end.x, y: end.y } + + // Apply directional offsets to create control points + switch (startDir) { + case 'left': + innerA.x -= l + break + case 'right': + innerA.x += l + break + case 'up': + innerA.y -= l + break + case 'down': + innerA.y += l + break + } + + switch (endDir) { + case 'left': + innerB.x -= l + break + case 'right': + innerB.x += l + break + case 'up': + innerB.y -= l + break + case 'down': + innerB.y += l + break + } + + // Draw 4-point path: start -> innerA -> innerB -> end + path.moveTo(start.x, start.y) + path.lineTo(innerA.x, innerA.y) + path.lineTo(innerB.x, innerB.y) + path.lineTo(end.x, end.y) + } + + private buildStraightPath( + path: Path2D, + start: Point, + end: Point, + startDir: Direction, + endDir: Direction + ): void { + // Match original STRAIGHT_LINK implementation with l=10 offset + const l = 10 // offset distance matching original + + const innerA = { x: start.x, y: start.y } + const innerB = { x: end.x, y: end.y } + + // Apply directional offsets to match original behavior + switch (startDir) { + case 'left': + innerA.x -= l + break + case 'right': + innerA.x += l + break + case 'up': + innerA.y -= l + break + case 'down': + innerA.y += l + break + } + + switch (endDir) { + case 'left': + innerB.x -= l + break + case 'right': + innerB.x += l + break + case 'up': + innerB.y -= l + break + case 'down': + innerB.y += l + break + } + + // Calculate midpoint using innerA/innerB positions (matching original) + const midX = (innerA.x + innerB.x) * 0.5 + + // Build path: start -> innerA -> (midX, innerA.y) -> (midX, innerB.y) -> innerB -> end + path.moveTo(start.x, start.y) + path.lineTo(innerA.x, innerA.y) + path.lineTo(midX, innerA.y) + path.lineTo(midX, innerB.y) + path.lineTo(innerB.x, innerB.y) + path.lineTo(end.x, end.y) + } + + private buildSplinePath( + path: Path2D, + start: Point, + end: Point, + startDir: Direction, + endDir: Direction, + controlPoints?: Point[] + ): void { + path.moveTo(start.x, start.y) + + // Calculate control points if not provided + const controls = + controlPoints || this.calculateControlPoints(start, end, startDir, endDir) + + if (controls.length >= 2) { + // Cubic bezier + path.bezierCurveTo( + controls[0].x, + controls[0].y, + controls[1].x, + controls[1].y, + end.x, + end.y + ) + } else if (controls.length === 1) { + // Quadratic bezier + path.quadraticCurveTo(controls[0].x, controls[0].y, end.x, end.y) + } else { + // Fallback to linear + path.lineTo(end.x, end.y) + } + } + + private calculateControlPoints( + start: Point, + end: Point, + startDir: Direction, + endDir: Direction + ): Point[] { + const dist = Math.sqrt( + Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2) + ) + const controlDist = Math.max(30, dist * 0.25) + + // Calculate control point offsets based on direction + const startControl = this.getDirectionOffset(startDir, controlDist) + const endControl = this.getDirectionOffset(endDir, controlDist) + + return [ + { x: start.x + startControl.x, y: start.y + startControl.y }, + { x: end.x + endControl.x, y: end.y + endControl.y } + ] + } + + private getDirectionOffset(direction: Direction, distance: number): Point { + switch (direction) { + case 'left': + return { x: -distance, y: 0 } + case 'right': + return { x: distance, y: 0 } + case 'up': + return { x: 0, y: -distance } + case 'down': + return { x: 0, y: distance } + } + } + + private drawArrows( + ctx: CanvasRenderingContext2D, + link: LinkRenderData, + context: RenderContext, + color: string + ): void { + if (!context.style.showArrows) return + + // Render arrows at 0.25 and 0.75 positions along the path (matching original) + const positions = [0.25, 0.75] + + for (const t of positions) { + // Compute arrow position and angle + const posA = this.computeConnectionPoint(link, t, context) + const posB = this.computeConnectionPoint(link, t + 0.01, context) // slightly ahead for angle + + const angle = Math.atan2(posB.y - posA.y, posB.x - posA.x) + + // Draw arrow triangle (matching original shape) + const transform = ctx.getTransform() + ctx.translate(posA.x, posA.y) + ctx.rotate(angle) + ctx.fillStyle = color + ctx.beginPath() + ctx.moveTo(-5, -3) + ctx.lineTo(0, +7) + ctx.lineTo(+5, -3) + ctx.fill() + ctx.setTransform(transform) + } + } + + /** + * Compute a point along the link path at position t (0 to 1) + * For backward compatibility with original litegraph, this always uses + * bezier calculation with spline offsets, regardless of render mode. + * This ensures arrow positions match the original implementation. + */ + private computeConnectionPoint( + link: LinkRenderData, + t: number, + _context: RenderContext + ): Point { + const { startPoint, endPoint, startDirection, endDirection } = link + + // Match original behavior: always use bezier math with spline offsets + // regardless of render mode (for arrow position compatibility) + const dist = Math.sqrt( + Math.pow(endPoint.x - startPoint.x, 2) + + Math.pow(endPoint.y - startPoint.y, 2) + ) + const factor = 0.25 + + // Create control points with spline offsets (matching original #addSplineOffset) + const pa = { x: startPoint.x, y: startPoint.y } + const pb = { x: endPoint.x, y: endPoint.y } + + // Apply spline offsets based on direction + switch (startDirection) { + case 'left': + pa.x -= dist * factor + break + case 'right': + pa.x += dist * factor + break + case 'up': + pa.y -= dist * factor + break + case 'down': + pa.y += dist * factor + break + } + + switch (endDirection) { + case 'left': + pb.x -= dist * factor + break + case 'right': + pb.x += dist * factor + break + case 'up': + pb.y -= dist * factor + break + case 'down': + pb.y += dist * factor + break + } + + // Calculate bezier point (matching original computeConnectionPoint) + const c1 = (1 - t) * (1 - t) * (1 - t) + const c2 = 3 * ((1 - t) * (1 - t)) * t + const c3 = 3 * (1 - t) * (t * t) + const c4 = t * t * t + + return { + x: c1 * startPoint.x + c2 * pa.x + c3 * pb.x + c4 * endPoint.x, + y: c1 * startPoint.y + c2 * pa.y + c3 * pb.y + c4 * endPoint.y + } + } + + private drawFlowAnimation( + ctx: CanvasRenderingContext2D, + _path: Path2D, + link: LinkRenderData, + context: RenderContext + ): void { + if (!context.animation) return + + // Match original implementation: render 5 moving circles along the path + const time = context.animation.time + const linkColor = this.determineLinkColor(link, context, false) + + ctx.save() + ctx.fillStyle = linkColor + + // Draw 5 circles at different positions along the path + for (let i = 0; i < 5; ++i) { + // Calculate position along path (0 to 1), with time-based animation + const f = (time + i * 0.2) % 1 + const flowPos = this.computeConnectionPoint(link, f, context) + + // Draw circle at this position + ctx.beginPath() + ctx.arc(flowPos.x, flowPos.y, 5, 0, 2 * Math.PI) + ctx.fill() + } + + ctx.restore() + } + + /** + * Utility to find a point on a bezier curve (for hit detection) + */ + findPointOnBezier( + t: number, + p0: Point, + p1: Point, + p2: Point, + p3: Point + ): Point { + const mt = 1 - t + const mt2 = mt * mt + const mt3 = mt2 * mt + const t2 = t * t + const t3 = t2 * t + + return { + x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x, + y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y + } + } + + /** + * Draw a link being dragged from a slot to the mouse position + * Returns a Path2D object for potential hit detection + */ + drawDraggingLink( + ctx: CanvasRenderingContext2D, + dragData: DragLinkData, + context: RenderContext + ): Path2D { + // Create LinkRenderData from drag data + // When dragging from input, swap the points/directions + const linkData: LinkRenderData = dragData.fromInput + ? { + id: 'dragging', + startPoint: dragData.dragPoint, + endPoint: dragData.fixedPoint, + startDirection: + dragData.dragDirection || + this.getOppositeDirection(dragData.fixedDirection), + endDirection: dragData.fixedDirection, + color: dragData.color, + type: dragData.type, + disabled: dragData.disabled + } + : { + id: 'dragging', + startPoint: dragData.fixedPoint, + endPoint: dragData.dragPoint, + startDirection: dragData.fixedDirection, + endDirection: + dragData.dragDirection || + this.getOppositeDirection(dragData.fixedDirection), + color: dragData.color, + type: dragData.type, + disabled: dragData.disabled + } + + // Use standard link drawing + return this.drawLink(ctx, linkData, context) + } + + /** + * Get the opposite direction (for drag preview) + */ + private getOppositeDirection(direction: Direction): Direction { + switch (direction) { + case 'left': + return 'right' + case 'right': + return 'left' + case 'up': + return 'down' + case 'down': + return 'up' + } + } + + /** + * Get the center point of a link (useful for labels, debugging) + */ + getLinkCenter(link: LinkRenderData): Point { + // For now, simple midpoint + // Could be enhanced to find actual curve midpoint + return { + x: (link.startPoint.x + link.endPoint.x) / 2, + y: (link.startPoint.y + link.endPoint.y) / 2 + } + } + + /** + * Calculate and store the center point and angle of a link + * Mimics the original litegraph center point calculation + */ + private calculateCenterPoint( + link: LinkRenderData, + context: RenderContext + ): void { + const { startPoint, endPoint, controlPoints } = link + + if ( + context.style.mode === 'spline' && + controlPoints && + controlPoints.length >= 2 + ) { + // For spline mode, find point at t=0.5 on the bezier curve + const centerPos = this.findPointOnBezier( + 0.5, + startPoint, + controlPoints[0], + controlPoints[1], + endPoint + ) + link.centerPos = centerPos + + // Calculate angle for arrow marker (point slightly past center) + if (context.style.centerMarkerShape === 'arrow') { + const justPastCenter = this.findPointOnBezier( + 0.51, + startPoint, + controlPoints[0], + controlPoints[1], + endPoint + ) + link.centerAngle = Math.atan2( + justPastCenter.y - centerPos.y, + justPastCenter.x - centerPos.x + ) + } + } else if (context.style.mode === 'linear') { + // For linear mode, calculate midpoint between control points (matching original) + const l = 15 // Same offset as buildLinearPath + const innerA = { x: startPoint.x, y: startPoint.y } + const innerB = { x: endPoint.x, y: endPoint.y } + + // Apply same directional offsets as buildLinearPath + switch (link.startDirection) { + case 'left': + innerA.x -= l + break + case 'right': + innerA.x += l + break + case 'up': + innerA.y -= l + break + case 'down': + innerA.y += l + break + } + + switch (link.endDirection) { + case 'left': + innerB.x -= l + break + case 'right': + innerB.x += l + break + case 'up': + innerB.y -= l + break + case 'down': + innerB.y += l + break + } + + link.centerPos = { + x: (innerA.x + innerB.x) * 0.5, + y: (innerA.y + innerB.y) * 0.5 + } + + if (context.style.centerMarkerShape === 'arrow') { + link.centerAngle = Math.atan2(innerB.y - innerA.y, innerB.x - innerA.x) + } + } else if (context.style.mode === 'straight') { + // For straight mode, match original STRAIGHT_LINK center calculation + const l = 10 // Same offset as buildStraightPath + const innerA = { x: startPoint.x, y: startPoint.y } + const innerB = { x: endPoint.x, y: endPoint.y } + + // Apply same directional offsets as buildStraightPath + switch (link.startDirection) { + case 'left': + innerA.x -= l + break + case 'right': + innerA.x += l + break + case 'up': + innerA.y -= l + break + case 'down': + innerA.y += l + break + } + + switch (link.endDirection) { + case 'left': + innerB.x -= l + break + case 'right': + innerB.x += l + break + case 'up': + innerB.y -= l + break + case 'down': + innerB.y += l + break + } + + // Calculate center using midX and average of innerA/innerB y positions + const midX = (innerA.x + innerB.x) * 0.5 + link.centerPos = { + x: midX, + y: (innerA.y + innerB.y) * 0.5 + } + + if (context.style.centerMarkerShape === 'arrow') { + const diff = innerB.y - innerA.y + if (Math.abs(diff) < 4) { + link.centerAngle = 0 + } else if (diff > 0) { + link.centerAngle = Math.PI * 0.5 + } else { + link.centerAngle = -(Math.PI * 0.5) + } + } + } else { + // Fallback to simple midpoint + link.centerPos = this.getLinkCenter(link) + if (context.style.centerMarkerShape === 'arrow') { + link.centerAngle = Math.atan2( + endPoint.y - startPoint.y, + endPoint.x - startPoint.x + ) + } + } + } + + /** + * Draw the center marker on a link (for menu interaction) + * Matches the original litegraph center marker rendering + */ + private drawCenterMarker( + ctx: CanvasRenderingContext2D, + link: LinkRenderData, + context: RenderContext, + color: string + ): void { + if (!link.centerPos) return + + ctx.beginPath() + + if ( + context.style.centerMarkerShape === 'arrow' && + link.centerAngle !== undefined + ) { + const transform = ctx.getTransform() + ctx.translate(link.centerPos.x, link.centerPos.y) + ctx.rotate(link.centerAngle) + // The math is off, but it currently looks better in chromium (from original) + ctx.moveTo(-3.2, -5) + ctx.lineTo(7, 0) + ctx.lineTo(-3.2, 5) + ctx.setTransform(transform) + } else { + // Default to circle + ctx.arc(link.centerPos.x, link.centerPos.y, 5, 0, Math.PI * 2) + } + + // Apply disabled pattern or color + if (link.disabled && context.patterns?.disabled) { + const { fillStyle, globalAlpha } = ctx + ctx.fillStyle = context.patterns.disabled + ctx.globalAlpha = 0.75 + ctx.fill() + ctx.globalAlpha = globalAlpha + ctx.fillStyle = fillStyle + } else { + ctx.fillStyle = color + ctx.fill() + } + } +} diff --git a/src/renderer/core/layout/constants.ts b/src/renderer/core/layout/constants.ts new file mode 100644 index 000000000..cc1de914e --- /dev/null +++ b/src/renderer/core/layout/constants.ts @@ -0,0 +1,50 @@ +/** + * Layout System Constants + * + * Centralized configuration values for the layout system. + * These values control spatial indexing, performance, and behavior. + */ +import { LayoutSource } from '@/renderer/core/layout/types' + +/** + * QuadTree configuration for spatial indexing + */ +export const QUADTREE_CONFIG = { + /** Default bounds for the QuadTree - covers a large canvas area */ + DEFAULT_BOUNDS: { + x: -10000, + y: -10000, + width: 20000, + height: 20000 + }, + /** Maximum tree depth to prevent excessive subdivision */ + MAX_DEPTH: 6, + /** Maximum items per node before subdivision */ + MAX_ITEMS_PER_NODE: 4 +} as const + +/** + * Performance and optimization settings + */ +export const PERFORMANCE_CONFIG = { + /** RAF-based change detection interval (roughly 60fps) */ + CHANGE_DETECTION_INTERVAL: 16, + /** Spatial query cache TTL in milliseconds */ + SPATIAL_CACHE_TTL: 1000, + /** Maximum cache size for spatial queries */ + SPATIAL_CACHE_MAX_SIZE: 100, + /** Batch update delay in milliseconds */ + BATCH_UPDATE_DELAY: 4 +} as const + +/** + * Actor and source identifiers + */ +export const ACTOR_CONFIG = { + /** Prefix for auto-generated actor IDs */ + USER_PREFIX: 'user-', + /** Length of random suffix for actor IDs */ + ID_LENGTH: 9, + /** Default source when not specified */ + DEFAULT_SOURCE: LayoutSource.External +} as const diff --git a/src/renderer/core/layout/operations/layoutMutations.ts b/src/renderer/core/layout/operations/layoutMutations.ts new file mode 100644 index 000000000..42ce2677c --- /dev/null +++ b/src/renderer/core/layout/operations/layoutMutations.ts @@ -0,0 +1,340 @@ +/** + * Layout Mutations - Simplified Direct Operations + * + * Provides a clean API for layout operations that are CRDT-ready. + * Operations are synchronous and applied directly to the store. + */ +import log from 'loglevel' + +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { + LayoutSource, + type LinkId, + type NodeLayout, + type Point, + type RerouteId, + type Size +} from '@/renderer/core/layout/types' + +const logger = log.getLogger('LayoutMutations') + +export interface LayoutMutations { + // Single node operations (synchronous, CRDT-ready) + moveNode(nodeId: NodeId, position: Point): void + resizeNode(nodeId: NodeId, size: Size): void + setNodeZIndex(nodeId: NodeId, zIndex: number): void + + // Node lifecycle operations + createNode(nodeId: NodeId, layout: Partial): void + deleteNode(nodeId: NodeId): void + + // Link operations + createLink( + linkId: LinkId, + sourceNodeId: NodeId, + sourceSlot: number, + targetNodeId: NodeId, + targetSlot: number + ): void + deleteLink(linkId: LinkId): void + + // Reroute operations + createReroute( + rerouteId: RerouteId, + position: Point, + parentId?: LinkId, + linkIds?: LinkId[] + ): void + deleteReroute(rerouteId: RerouteId): void + moveReroute( + rerouteId: RerouteId, + position: Point, + previousPosition: Point + ): void + + // Stacking operations + bringNodeToFront(nodeId: NodeId): void + + // Source tracking + setSource(source: LayoutSource): void + setActor(actor: string): void +} + +/** + * Composable for accessing layout mutations with clean destructuring API + */ +export function useLayoutMutations(): LayoutMutations { + /** + * Set the current mutation source + */ + const setSource = (source: LayoutSource): void => { + layoutStore.setSource(source) + } + + /** + * Set the current actor (for CRDT) + */ + const setActor = (actor: string): void => { + layoutStore.setActor(actor) + } + + /** + * Move a node to a new position + */ + const moveNode = (nodeId: NodeId, position: Point): void => { + const normalizedNodeId = String(nodeId) + const existing = layoutStore.getNodeLayoutRef(normalizedNodeId).value + if (!existing) return + + layoutStore.applyOperation({ + type: 'moveNode', + entity: 'node', + nodeId: normalizedNodeId, + position, + previousPosition: existing.position, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Resize a node + */ + const resizeNode = (nodeId: NodeId, size: Size): void => { + const normalizedNodeId = String(nodeId) + const existing = layoutStore.getNodeLayoutRef(normalizedNodeId).value + if (!existing) return + + layoutStore.applyOperation({ + type: 'resizeNode', + entity: 'node', + nodeId: normalizedNodeId, + size, + previousSize: existing.size, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Set node z-index + */ + const setNodeZIndex = (nodeId: NodeId, zIndex: number): void => { + const normalizedNodeId = String(nodeId) + const existing = layoutStore.getNodeLayoutRef(normalizedNodeId).value + if (!existing) return + + layoutStore.applyOperation({ + type: 'setNodeZIndex', + entity: 'node', + nodeId: normalizedNodeId, + zIndex, + previousZIndex: existing.zIndex, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Create a new node + */ + const createNode = (nodeId: NodeId, layout: Partial): void => { + const normalizedNodeId = String(nodeId) + const fullLayout: NodeLayout = { + id: normalizedNodeId, + position: layout.position ?? { x: 0, y: 0 }, + size: layout.size ?? { width: 200, height: 100 }, + zIndex: layout.zIndex ?? 0, + visible: layout.visible ?? true, + bounds: { + x: layout.position?.x ?? 0, + y: layout.position?.y ?? 0, + width: layout.size?.width ?? 200, + height: layout.size?.height ?? 100 + } + } + + layoutStore.applyOperation({ + type: 'createNode', + entity: 'node', + nodeId: normalizedNodeId, + layout: fullLayout, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Delete a node + */ + const deleteNode = (nodeId: NodeId): void => { + const normalizedNodeId = String(nodeId) + const existing = layoutStore.getNodeLayoutRef(normalizedNodeId).value + if (!existing) return + + layoutStore.applyOperation({ + type: 'deleteNode', + entity: 'node', + nodeId: normalizedNodeId, + previousLayout: existing, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Bring a node to the front (highest z-index) + */ + const bringNodeToFront = (nodeId: NodeId): void => { + // Get all nodes to find the highest z-index + const allNodes = layoutStore.getAllNodes().value + let maxZIndex = 0 + + for (const [, layout] of allNodes) { + if (layout.zIndex > maxZIndex) { + maxZIndex = layout.zIndex + } + } + + // Set this node's z-index to be one higher than the current max + setNodeZIndex(nodeId, maxZIndex + 1) + } + + /** + * Create a new link + */ + const createLink = ( + linkId: LinkId, + sourceNodeId: NodeId, + sourceSlot: number, + targetNodeId: NodeId, + targetSlot: number + ): void => { + // Normalize node IDs to strings for layout store consistency + const normalizedSourceNodeId = String(sourceNodeId) + const normalizedTargetNodeId = String(targetNodeId) + + logger.debug('Creating link:', { + linkId, + from: `${normalizedSourceNodeId}[${sourceSlot}]`, + to: `${normalizedTargetNodeId}[${targetSlot}]` + }) + layoutStore.applyOperation({ + type: 'createLink', + entity: 'link', + linkId, + sourceNodeId: normalizedSourceNodeId, + sourceSlot, + targetNodeId: normalizedTargetNodeId, + targetSlot, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Delete a link + */ + const deleteLink = (linkId: LinkId): void => { + logger.debug('Deleting link:', linkId) + layoutStore.applyOperation({ + type: 'deleteLink', + entity: 'link', + linkId, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Create a new reroute + */ + const createReroute = ( + rerouteId: RerouteId, + position: Point, + parentId?: LinkId, + linkIds: LinkId[] = [] + ): void => { + logger.debug('Creating reroute:', { + rerouteId, + position, + parentId, + linkCount: linkIds.length + }) + layoutStore.applyOperation({ + type: 'createReroute', + entity: 'reroute', + rerouteId, + position, + parentId, + linkIds, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Delete a reroute + */ + const deleteReroute = (rerouteId: RerouteId): void => { + logger.debug('Deleting reroute:', rerouteId) + layoutStore.applyOperation({ + type: 'deleteReroute', + entity: 'reroute', + rerouteId, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + /** + * Move a reroute + */ + const moveReroute = ( + rerouteId: RerouteId, + position: Point, + previousPosition: Point + ): void => { + logger.debug('Moving reroute:', { + rerouteId, + from: previousPosition, + to: position + }) + layoutStore.applyOperation({ + type: 'moveReroute', + entity: 'reroute', + rerouteId, + position, + previousPosition, + timestamp: Date.now(), + source: layoutStore.getCurrentSource(), + actor: layoutStore.getCurrentActor() + }) + } + + return { + setSource, + setActor, + moveNode, + resizeNode, + setNodeZIndex, + createNode, + deleteNode, + bringNodeToFront, + createLink, + deleteLink, + createReroute, + deleteReroute, + moveReroute + } +} diff --git a/src/renderer/core/layout/slots/register.ts b/src/renderer/core/layout/slots/register.ts new file mode 100644 index 000000000..5965b885b --- /dev/null +++ b/src/renderer/core/layout/slots/register.ts @@ -0,0 +1,75 @@ +/** + * Slot Registration + * + * Handles registration of slot layouts with the layout store for hit testing. + * This module manages the state mutation side of slot layout management, + * while pure calculations are handled separately in SlotCalculations.ts. + */ +import type { Point } from '@/lib/litegraph/src/interfaces' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { + type SlotPositionContext, + calculateInputSlotPos, + calculateOutputSlotPos +} from '@/renderer/core/canvas/litegraph/slotCalculations' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { SlotLayout } from '@/renderer/core/layout/types' + +import { getSlotKey } from './slotIdentifier' + +/** + * Register slot layout with the layout store for hit testing + * @param nodeId The node ID + * @param slotIndex The slot index + * @param isInput Whether this is an input slot + * @param position The slot position in graph coordinates + */ +export function registerSlotLayout( + nodeId: string, + slotIndex: number, + isInput: boolean, + position: Point +): void { + const slotKey = getSlotKey(nodeId, slotIndex, isInput) + + // Calculate bounds for the slot using LiteGraph's standard slot height + const slotSize = LiteGraph.NODE_SLOT_HEIGHT + const halfSize = slotSize / 2 + + const slotLayout: SlotLayout = { + nodeId, + index: slotIndex, + type: isInput ? 'input' : 'output', + position: { x: position[0], y: position[1] }, + bounds: { + x: position[0] - halfSize, + y: position[1] - halfSize, + width: slotSize, + height: slotSize + } + } + + layoutStore.updateSlotLayout(slotKey, slotLayout) +} + +/** + * Register all slots for a node + * @param nodeId The node ID + * @param context The slot position context + */ +export function registerNodeSlots( + nodeId: string, + context: SlotPositionContext +): void { + // Register input slots + context.inputs.forEach((_, index) => { + const position = calculateInputSlotPos(context, index) + registerSlotLayout(nodeId, index, true, position) + }) + + // Register output slots + context.outputs.forEach((_, index) => { + const position = calculateOutputSlotPos(context, index) + registerSlotLayout(nodeId, index, false, position) + }) +} diff --git a/src/renderer/core/layout/slots/slotIdentifier.ts b/src/renderer/core/layout/slots/slotIdentifier.ts new file mode 100644 index 000000000..df1f64fc2 --- /dev/null +++ b/src/renderer/core/layout/slots/slotIdentifier.ts @@ -0,0 +1,40 @@ +/** + * Slot identifier utilities for consistent slot key generation and parsing + * + * Provides a centralized interface for slot identification across the layout system + * + * @TODO Replace this concatenated string with root cause fix + */ + +export interface SlotIdentifier { + nodeId: string + index: number + isInput: boolean +} + +/** + * Generate a unique key for a slot + * Format: "{nodeId}-{in|out}-{index}" + */ +export function getSlotKey(identifier: SlotIdentifier): string +export function getSlotKey( + nodeId: string, + index: number, + isInput: boolean +): string +export function getSlotKey( + nodeIdOrIdentifier: string | SlotIdentifier, + index?: number, + isInput?: boolean +): string { + if (typeof nodeIdOrIdentifier === 'object') { + const { nodeId, index, isInput } = nodeIdOrIdentifier + return `${nodeId}-${isInput ? 'in' : 'out'}-${index}` + } + + if (index === undefined || isInput === undefined) { + throw new Error('Missing required parameters for slot key generation') + } + + return `${nodeIdOrIdentifier}-${isInput ? 'in' : 'out'}-${index}` +} diff --git a/src/renderer/core/layout/slots/useDomSlotRegistration.ts b/src/renderer/core/layout/slots/useDomSlotRegistration.ts new file mode 100644 index 000000000..94a1f09e5 --- /dev/null +++ b/src/renderer/core/layout/slots/useDomSlotRegistration.ts @@ -0,0 +1,229 @@ +/** + * DOM-based slot registration with performance optimization + * + * Measures the actual DOM position of a Vue slot connector and registers it + * into the LayoutStore so hit-testing and link rendering use the true position. + * + * Performance strategy: + * - Cache slot offset relative to node (avoids DOM reads during drag) + * - No measurements during pan/zoom (camera transforms don't change canvas coords) + * - Batch DOM reads via requestAnimationFrame + * - Only remeasure on structural changes (resize, collapse, LOD) + */ +import { + type Ref, + type WatchStopHandle, + nextTick, + onMounted, + onUnmounted, + ref, + watch +} from 'vue' + +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { Point as LayoutPoint } from '@/renderer/core/layout/types' + +import { getSlotKey } from './slotIdentifier' + +export type TransformState = { + screenToCanvas: (p: LayoutPoint) => LayoutPoint +} + +// Shared RAF queue for batching measurements +const measureQueue = new Set<() => void>() +let rafId: number | null = null +// Track mounted components to prevent execution on unmounted ones +const mountedComponents = new WeakSet() + +function scheduleMeasurement(fn: () => void) { + measureQueue.add(fn) + if (rafId === null) { + rafId = requestAnimationFrame(() => { + rafId = null + const batch = Array.from(measureQueue) + measureQueue.clear() + batch.forEach((measure) => measure()) + }) + } +} + +const cleanupFunctions = new WeakMap< + Ref, + { + stopWatcher?: WatchStopHandle + handleResize?: () => void + } +>() + +interface SlotRegistrationOptions { + nodeId: string + slotIndex: number + isInput: boolean + element: Ref + transform?: TransformState +} + +export function useDomSlotRegistration(options: SlotRegistrationOptions) { + const { nodeId, slotIndex, isInput, element: elRef, transform } = options + + // Early return if no nodeId + if (!nodeId || nodeId === '') { + return { + remeasure: () => {} + } + } + const slotKey = getSlotKey(nodeId, slotIndex, isInput) + // Track if this component is mounted + const componentToken = {} + + // Cached offset from node position (avoids DOM reads during drag) + const cachedOffset = ref(null) + const lastMeasuredBounds = ref(null) + + // Measure DOM and cache offset (expensive, minimize calls) + const measureAndCacheOffset = () => { + // Skip if component was unmounted + if (!mountedComponents.has(componentToken)) return + + const el = elRef.value + if (!el || !transform?.screenToCanvas) return + + const rect = el.getBoundingClientRect() + + // Skip if bounds haven't changed significantly (within 0.5px) + if (lastMeasuredBounds.value) { + const prev = lastMeasuredBounds.value + if ( + Math.abs(rect.left - prev.left) < 0.5 && + Math.abs(rect.top - prev.top) < 0.5 && + Math.abs(rect.width - prev.width) < 0.5 && + Math.abs(rect.height - prev.height) < 0.5 + ) { + return // No significant change - skip update + } + } + + lastMeasuredBounds.value = rect + + // Center of the visual connector (dot) in screen coords + const centerScreen = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2 + } + const centerCanvas = transform.screenToCanvas(centerScreen) + + // Cache offset from node position for fast updates during drag + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (nodeLayout) { + cachedOffset.value = { + x: centerCanvas.x - nodeLayout.position.x, + y: centerCanvas.y - nodeLayout.position.y + } + } + + updateSlotPosition(centerCanvas) + } + + // Fast update using cached offset (no DOM read) + const updateFromCachedOffset = () => { + if (!cachedOffset.value) { + // No cached offset yet, need to measure + scheduleMeasurement(measureAndCacheOffset) + return + } + + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (!nodeLayout) { + return + } + + // Calculate absolute position from node position + cached offset + const centerCanvas = { + x: nodeLayout.position.x + cachedOffset.value.x, + y: nodeLayout.position.y + cachedOffset.value.y + } + + updateSlotPosition(centerCanvas) + } + + // Update slot position in layout store + const updateSlotPosition = (centerCanvas: LayoutPoint) => { + const size = LiteGraph.NODE_SLOT_HEIGHT + const half = size / 2 + + layoutStore.updateSlotLayout(slotKey, { + nodeId, + index: slotIndex, + type: isInput ? 'input' : 'output', + position: { x: centerCanvas.x, y: centerCanvas.y }, + bounds: { + x: centerCanvas.x - half, + y: centerCanvas.y - half, + width: size, + height: size + } + }) + } + + onMounted(async () => { + // Mark component as mounted + mountedComponents.add(componentToken) + + // Initial measure after mount + await nextTick() + measureAndCacheOffset() + + // Subscribe to node position changes for fast cached updates + const nodeRef = layoutStore.getNodeLayoutRef(nodeId) + + const stopWatcher = watch( + nodeRef, + (newLayout) => { + if (newLayout) { + // Node moved/resized - update using cached offset + updateFromCachedOffset() + } + }, + { immediate: false } + ) + + // Store cleanup functions without type assertions + const cleanup = cleanupFunctions.get(elRef) || {} + cleanup.stopWatcher = stopWatcher + + // Window resize - remeasure as viewport changed + const handleResize = () => { + scheduleMeasurement(measureAndCacheOffset) + } + window.addEventListener('resize', handleResize, { passive: true }) + cleanup.handleResize = handleResize + cleanupFunctions.set(elRef, cleanup) + }) + + onUnmounted(() => { + // Mark component as unmounted + mountedComponents.delete(componentToken) + + // Clean up watchers and listeners + const cleanup = cleanupFunctions.get(elRef) + if (cleanup) { + if (cleanup.stopWatcher) cleanup.stopWatcher() + if (cleanup.handleResize) { + window.removeEventListener('resize', cleanup.handleResize) + } + cleanupFunctions.delete(elRef) + } + + // Remove from layout store + layoutStore.deleteSlotLayout(slotKey) + + // Remove from measurement queue if pending + measureQueue.delete(measureAndCacheOffset) + }) + + return { + // Expose for forced remeasure on structural changes + remeasure: () => scheduleMeasurement(measureAndCacheOffset) + } +} diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts new file mode 100644 index 000000000..5be50702b --- /dev/null +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -0,0 +1,1356 @@ +/** + * Layout Store - Single Source of Truth + * + * Uses Yjs for efficient local state management and future collaboration. + * CRDT ensures conflict-free operations for both single and multi-user scenarios. + */ +import log from 'loglevel' +import { type ComputedRef, type Ref, computed, customRef } from 'vue' +import * as Y from 'yjs' + +import { ACTOR_CONFIG } from '@/renderer/core/layout/constants' +import type { + CreateLinkOperation, + CreateNodeOperation, + CreateRerouteOperation, + DeleteLinkOperation, + DeleteNodeOperation, + DeleteRerouteOperation, + LayoutOperation, + MoveNodeOperation, + MoveRerouteOperation, + ResizeNodeOperation, + SetNodeZIndexOperation +} from '@/renderer/core/layout/types' +import { + type Bounds, + type LayoutChange, + LayoutSource, + type LayoutStore, + type LinkId, + type LinkLayout, + type LinkSegmentLayout, + type NodeId, + type NodeLayout, + type Point, + type RerouteId, + type RerouteLayout, + type SlotLayout +} from '@/renderer/core/layout/types' +import { SpatialIndexManager } from '@/renderer/core/spatial/SpatialIndex' + +type YEventChange = { + action: 'add' | 'update' | 'delete' + oldValue: unknown +} + +const logger = log.getLogger('LayoutStore') + +// Constants +const REROUTE_RADIUS = 8 + +class LayoutStoreImpl implements LayoutStore { + // Yjs document and shared data structures + private ydoc = new Y.Doc() + private ynodes: Y.Map> // Maps nodeId -> Y.Map containing NodeLayout data + private ylinks: Y.Map> // Maps linkId -> Y.Map containing link data + private yreroutes: Y.Map> // Maps rerouteId -> Y.Map containing reroute data + private yoperations: Y.Array // Operation log + + // Vue reactivity layer + private version = 0 + private currentSource: LayoutSource = + ACTOR_CONFIG.DEFAULT_SOURCE as LayoutSource + private currentActor = `${ACTOR_CONFIG.USER_PREFIX}${Math.random() + .toString(36) + .substring(2, 2 + ACTOR_CONFIG.ID_LENGTH)}` + + // Change listeners + private changeListeners = new Set<(change: LayoutChange) => void>() + + // CustomRef cache and trigger functions + private nodeRefs = new Map>() + private nodeTriggers = new Map void>() + + // New data structures for hit testing + private linkLayouts = new Map() + private linkSegmentLayouts = new Map() // Internal string key: ${linkId}:${rerouteId ?? 'final'} + private slotLayouts = new Map() + private rerouteLayouts = new Map() + + // Spatial index managers + private spatialIndex: SpatialIndexManager // For nodes + private linkSegmentSpatialIndex: SpatialIndexManager // For link segments (single index for all link geometry) + private slotSpatialIndex: SpatialIndexManager // For slots + private rerouteSpatialIndex: SpatialIndexManager // For reroutes + + constructor() { + // Initialize Yjs data structures + this.ynodes = this.ydoc.getMap('nodes') + this.ylinks = this.ydoc.getMap('links') + this.yreroutes = this.ydoc.getMap('reroutes') + this.yoperations = this.ydoc.getArray('operations') + + // Initialize spatial index managers + this.spatialIndex = new SpatialIndexManager() + this.linkSegmentSpatialIndex = new SpatialIndexManager() // Single index for all link geometry + this.slotSpatialIndex = new SpatialIndexManager() + this.rerouteSpatialIndex = new SpatialIndexManager() + + // Listen for Yjs changes and trigger Vue reactivity + this.ynodes.observe((event: Y.YMapEvent>) => { + this.version++ + + // Trigger all affected node refs + event.changes.keys.forEach((_change: YEventChange, key: string) => { + const trigger = this.nodeTriggers.get(key) + if (trigger) { + trigger() + } + }) + }) + + // Listen for link changes and update spatial indexes + this.ylinks.observe((event: Y.YMapEvent>) => { + this.version++ + event.changes.keys.forEach((change, linkIdStr) => { + this.handleLinkChange(change, linkIdStr) + }) + }) + + // Listen for reroute changes and update spatial indexes + this.yreroutes.observe((event: Y.YMapEvent>) => { + this.version++ + event.changes.keys.forEach((change, rerouteIdStr) => { + this.handleRerouteChange(change, rerouteIdStr) + }) + }) + } + + /** + * Get or create a customRef for a node layout + */ + getNodeLayoutRef(nodeId: NodeId): Ref { + let nodeRef = this.nodeRefs.get(nodeId) + + if (!nodeRef) { + nodeRef = customRef((track, trigger) => { + // Store the trigger so we can call it when Yjs changes + this.nodeTriggers.set(nodeId, trigger) + + return { + get: () => { + track() + const ynode = this.ynodes.get(nodeId) + const layout = ynode ? this.yNodeToLayout(ynode) : null + return layout + }, + set: (newLayout: NodeLayout | null) => { + if (newLayout === null) { + // Delete operation + const existing = this.ynodes.get(nodeId) + if (existing) { + this.applyOperation({ + type: 'deleteNode', + entity: 'node', + nodeId, + timestamp: Date.now(), + source: this.currentSource, + actor: this.currentActor, + previousLayout: this.yNodeToLayout(existing) + }) + } + } else { + // Update operation - detect what changed + const existing = this.ynodes.get(nodeId) + if (!existing) { + // Create operation + this.applyOperation({ + type: 'createNode', + entity: 'node', + nodeId, + layout: newLayout, + timestamp: Date.now(), + source: this.currentSource, + actor: this.currentActor + }) + } else { + const existingLayout = this.yNodeToLayout(existing) + + // Check what properties changed + if ( + existingLayout.position.x !== newLayout.position.x || + existingLayout.position.y !== newLayout.position.y + ) { + this.applyOperation({ + type: 'moveNode', + entity: 'node', + nodeId, + position: newLayout.position, + previousPosition: existingLayout.position, + timestamp: Date.now(), + source: this.currentSource, + actor: this.currentActor + }) + } + if ( + existingLayout.size.width !== newLayout.size.width || + existingLayout.size.height !== newLayout.size.height + ) { + this.applyOperation({ + type: 'resizeNode', + entity: 'node', + nodeId, + size: newLayout.size, + previousSize: existingLayout.size, + timestamp: Date.now(), + source: this.currentSource, + actor: this.currentActor + }) + } + if (existingLayout.zIndex !== newLayout.zIndex) { + this.applyOperation({ + type: 'setNodeZIndex', + entity: 'node', + nodeId, + zIndex: newLayout.zIndex, + previousZIndex: existingLayout.zIndex, + timestamp: Date.now(), + source: this.currentSource, + actor: this.currentActor + }) + } + } + } + trigger() + } + } + }) + + this.nodeRefs.set(nodeId, nodeRef) + } + + return nodeRef + } + + /** + * Get nodes within bounds (reactive) + */ + getNodesInBounds(bounds: Bounds): ComputedRef { + return computed(() => { + // Touch version for reactivity + void this.version + + const result: NodeId[] = [] + for (const [nodeId] of this.ynodes) { + const ynode = this.ynodes.get(nodeId) + if (ynode) { + const layout = this.yNodeToLayout(ynode) + if (layout && this.boundsIntersect(layout.bounds, bounds)) { + result.push(nodeId) + } + } + } + return result + }) + } + + /** + * Get all nodes as a reactive map + */ + getAllNodes(): ComputedRef> { + return computed(() => { + // Touch version for reactivity + void this.version + + const result = new Map() + for (const [nodeId] of this.ynodes) { + const ynode = this.ynodes.get(nodeId) + if (ynode) { + const layout = this.yNodeToLayout(ynode) + if (layout) { + result.set(nodeId, layout) + } + } + } + return result + }) + } + + /** + * Get current version for change detection + */ + getVersion(): ComputedRef { + return computed(() => this.version) + } + + /** + * Query node at point (non-reactive for performance) + */ + queryNodeAtPoint(point: Point): NodeId | null { + const nodes: Array<[NodeId, NodeLayout]> = [] + + for (const [nodeId] of this.ynodes) { + const ynode = this.ynodes.get(nodeId) + if (ynode) { + const layout = this.yNodeToLayout(ynode) + if (layout) { + nodes.push([nodeId, layout]) + } + } + } + + // Sort by zIndex (top to bottom) + nodes.sort(([, a], [, b]) => b.zIndex - a.zIndex) + + for (const [nodeId, layout] of nodes) { + if (this.pointInBounds(point, layout.bounds)) { + return nodeId + } + } + + return null + } + + /** + * Query nodes in bounds (non-reactive for performance) + */ + queryNodesInBounds(bounds: Bounds): NodeId[] { + return this.spatialIndex.query(bounds) + } + + /** + * Update link layout data (for geometry/debug, no separate spatial index) + */ + updateLinkLayout(linkId: LinkId, layout: LinkLayout): void { + const existing = this.linkLayouts.get(linkId) + + // Short-circuit if bounds and centerPos unchanged + if ( + existing && + 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.centerPos.x === layout.centerPos.x && + existing.centerPos.y === layout.centerPos.y + ) { + // Only update path if provided (for hit detection) + if (layout.path) { + existing.path = layout.path + } + return + } + + this.linkLayouts.set(linkId, layout) + } + + /** + * Delete link layout data + */ + deleteLinkLayout(linkId: LinkId): void { + const deleted = this.linkLayouts.delete(linkId) + if (deleted) { + // Clean up any segment layouts for this link + const keysToDelete: string[] = [] + for (const [key] of this.linkSegmentLayouts) { + if (key.startsWith(`${linkId}:`)) { + keysToDelete.push(key) + } + } + for (const key of keysToDelete) { + this.linkSegmentLayouts.delete(key) + this.linkSegmentSpatialIndex.remove(key) + } + } + } + + /** + * Update slot layout data + */ + updateSlotLayout(key: string, layout: SlotLayout): void { + const existing = this.slotLayouts.get(key) + + if (!existing) { + logger.debug('Adding slot:', { + nodeId: layout.nodeId, + type: layout.type, + index: layout.index, + bounds: layout.bounds + }) + } + + if (existing) { + // Update spatial index + this.slotSpatialIndex.update(key, layout.bounds) + } else { + // Insert into spatial index + this.slotSpatialIndex.insert(key, layout.bounds) + } + + this.slotLayouts.set(key, layout) + } + + /** + * Delete slot layout data + */ + deleteSlotLayout(key: string): void { + const deleted = this.slotLayouts.delete(key) + if (deleted) { + // Remove from spatial index + this.slotSpatialIndex.remove(key) + } + } + + /** + * Delete all slot layouts for a node + */ + deleteNodeSlotLayouts(nodeId: NodeId): void { + const keysToDelete: string[] = [] + for (const [key, layout] of this.slotLayouts) { + if (layout.nodeId === nodeId) { + keysToDelete.push(key) + } + } + for (const key of keysToDelete) { + this.slotLayouts.delete(key) + // Remove from spatial index + this.slotSpatialIndex.remove(key) + } + } + + /** + * Update reroute layout data + */ + updateRerouteLayout(rerouteId: RerouteId, layout: RerouteLayout): void { + const existing = this.rerouteLayouts.get(rerouteId) + + if (!existing) { + logger.debug('Adding reroute layout:', { + rerouteId, + position: layout.position, + bounds: layout.bounds + }) + } + + if (existing) { + // Update spatial index + this.rerouteSpatialIndex.update(String(rerouteId), layout.bounds) // Spatial index uses strings + } else { + // Insert into spatial index + this.rerouteSpatialIndex.insert(String(rerouteId), layout.bounds) // Spatial index uses strings + } + + this.rerouteLayouts.set(rerouteId, layout) + } + + /** + * Delete reroute layout data + */ + deleteRerouteLayout(rerouteId: RerouteId): void { + const deleted = this.rerouteLayouts.delete(rerouteId) + if (deleted) { + // Remove from spatial index + this.rerouteSpatialIndex.remove(String(rerouteId)) // Spatial index uses strings + } + } + + /** + * Get link layout data + */ + getLinkLayout(linkId: LinkId): LinkLayout | null { + return this.linkLayouts.get(linkId) || null + } + + /** + * Get slot layout data + */ + getSlotLayout(key: string): SlotLayout | null { + return this.slotLayouts.get(key) || null + } + + /** + * Get reroute layout data + */ + getRerouteLayout(rerouteId: RerouteId): RerouteLayout | null { + return this.rerouteLayouts.get(rerouteId) || null + } + + /** + * Helper to create internal key for link segment + */ + private makeLinkSegmentKey( + linkId: LinkId, + rerouteId: RerouteId | null + ): string { + return `${linkId}:${rerouteId ?? 'final'}` + } + + /** + * Update link segment layout data + */ + updateLinkSegmentLayout( + linkId: LinkId, + rerouteId: RerouteId | null, + layout: Omit + ): void { + const key = this.makeLinkSegmentKey(linkId, rerouteId) + const existing = this.linkSegmentLayouts.get(key) + + // Short-circuit if bounds and centerPos unchanged (prevents spatial index churn) + if ( + existing && + 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.centerPos.x === layout.centerPos.x && + existing.centerPos.y === layout.centerPos.y + ) { + // Only update path if provided (for hit detection) + if (layout.path) { + existing.path = layout.path + } + return + } + + const fullLayout: LinkSegmentLayout = { + ...layout, + linkId, + rerouteId + } + + if (!existing) { + logger.debug('Adding link segment:', { + linkId, + rerouteId, + bounds: layout.bounds, + hasPath: !!layout.path + }) + } + + if (existing) { + // Update spatial index + this.linkSegmentSpatialIndex.update(key, layout.bounds) + } else { + // Insert into spatial index + this.linkSegmentSpatialIndex.insert(key, layout.bounds) + } + + this.linkSegmentLayouts.set(key, fullLayout) + } + + /** + * Delete link segment layout data + */ + deleteLinkSegmentLayout(linkId: LinkId, rerouteId: RerouteId | null): void { + const key = this.makeLinkSegmentKey(linkId, rerouteId) + const deleted = this.linkSegmentLayouts.delete(key) + if (deleted) { + // Remove from spatial index + this.linkSegmentSpatialIndex.remove(key) + } + } + + /** + * Query link segment at point (returns structured data) + */ + queryLinkSegmentAtPoint( + point: Point, + ctx?: CanvasRenderingContext2D + ): { linkId: LinkId; rerouteId: RerouteId | null } | null { + // Determine tolerance from current canvas state (if available) + // - Use the caller-provided ctx.lineWidth (LGraphCanvas sets this to connections_width + padding) + // - Fall back to a sensible default when ctx is not provided + const hitWidth = ctx?.lineWidth ?? 10 + const halfSize = Math.max(10, hitWidth) // keep a minimum window for spatial index + + // Use spatial index to get candidate segments + const searchArea = { + x: point.x - halfSize, + y: point.y - halfSize, + width: halfSize * 2, + height: halfSize * 2 + } + const candidateKeys = this.linkSegmentSpatialIndex.query(searchArea) + + if (candidateKeys.length > 0) { + logger.debug('Checking link segments at point:', { + point, + candidateCount: candidateKeys.length, + tolerance: hitWidth + }) + } + + // Precise hit test only on candidates + for (const key of candidateKeys) { + const segmentLayout = this.linkSegmentLayouts.get(key) + if (!segmentLayout) continue + + if (ctx && segmentLayout.path) { + // Match LiteGraph behavior: hit test uses device pixel ratio for coordinates + const dpi = + (typeof window !== 'undefined' && window?.devicePixelRatio) || 1 + const hit = ctx.isPointInStroke( + segmentLayout.path, + point.x * dpi, + point.y * dpi + ) + + if (hit) { + logger.debug('Link segment hit:', { + linkId: segmentLayout.linkId, + rerouteId: segmentLayout.rerouteId, + point + }) + return { + linkId: segmentLayout.linkId, + rerouteId: segmentLayout.rerouteId + } + } + } else if (this.pointInBounds(point, segmentLayout.bounds)) { + // Fallback to bounding box test + return { + linkId: segmentLayout.linkId, + rerouteId: segmentLayout.rerouteId + } + } + } + + return null + } + + /** + * Query link at point (derived from segment query) + */ + queryLinkAtPoint( + point: Point, + ctx?: CanvasRenderingContext2D + ): LinkId | null { + // Invoke segment query and return just the linkId + const segment = this.queryLinkSegmentAtPoint(point, ctx) + return segment ? segment.linkId : null + } + + /** + * Query slot at point + */ + querySlotAtPoint(point: Point): SlotLayout | null { + // Use spatial index to get candidate slots + const searchArea = { + x: point.x - 10, // Tolerance for slot size + y: point.y - 10, + width: 20, + height: 20 + } + const candidateSlotKeys = this.slotSpatialIndex.query(searchArea) + + // Check precise bounds for candidates + for (const key of candidateSlotKeys) { + const slotLayout = this.slotLayouts.get(key) + if (slotLayout && this.pointInBounds(point, slotLayout.bounds)) { + return slotLayout + } + } + return null + } + + /** + * Query reroute at point + */ + queryRerouteAtPoint(point: Point): RerouteLayout | null { + // Use spatial index to get candidate reroutes + const maxRadius = 20 // Maximum expected reroute radius + const searchArea = { + x: point.x - maxRadius, + y: point.y - maxRadius, + width: maxRadius * 2, + height: maxRadius * 2 + } + const candidateRerouteKeys = this.rerouteSpatialIndex.query(searchArea) + + if (candidateRerouteKeys.length > 0) { + logger.debug('Checking reroutes at point:', { + point, + candidateCount: candidateRerouteKeys.length + }) + } + + // Check precise distance for candidates + for (const rerouteKey of candidateRerouteKeys) { + const rerouteId = Number(rerouteKey) as RerouteId // Convert string key back to numeric + const rerouteLayout = this.rerouteLayouts.get(rerouteId) + if (rerouteLayout) { + const dx = point.x - rerouteLayout.position.x + const dy = point.y - rerouteLayout.position.y + const distance = Math.sqrt(dx * dx + dy * dy) + + if (distance <= rerouteLayout.radius) { + logger.debug('Reroute hit:', { + rerouteId: rerouteLayout.id, + position: rerouteLayout.position, + distance + }) + return rerouteLayout + } + } + } + return null + } + + /** + * Query all items in bounds + */ + queryItemsInBounds(bounds: Bounds): { + nodes: NodeId[] + links: LinkId[] + slots: string[] + reroutes: RerouteId[] + } { + // Query segments and union their linkIds + const segmentKeys = this.linkSegmentSpatialIndex.query(bounds) + const linkIds = new Set() + for (const key of segmentKeys) { + const segment = this.linkSegmentLayouts.get(key) + if (segment) { + linkIds.add(segment.linkId) + } + } + + return { + nodes: this.queryNodesInBounds(bounds), + links: Array.from(linkIds), + slots: this.slotSpatialIndex.query(bounds), + reroutes: this.rerouteSpatialIndex + .query(bounds) + .map((key) => Number(key) as RerouteId) // Convert string keys to numeric + } + } + + /** + * Apply a layout operation using Yjs transactions + */ + applyOperation(operation: LayoutOperation): void { + // Create change object outside transaction so we can use it after + const change: LayoutChange = { + type: 'update', + nodeIds: [], + timestamp: operation.timestamp, + source: operation.source, + operation + } + + // Use Yjs transaction for atomic updates + this.ydoc.transact(() => { + // Add operation to log + this.yoperations.push([operation]) + + // Apply the operation + this.applyOperationInTransaction(operation, change) + }, this.currentActor) + + // Post-transaction updates + this.finalizeOperation(change) + } + + /** + * Apply operation within a transaction + */ + private applyOperationInTransaction( + operation: LayoutOperation, + change: LayoutChange + ): void { + switch (operation.type) { + case 'moveNode': + this.handleMoveNode(operation as MoveNodeOperation, change) + break + case 'resizeNode': + this.handleResizeNode(operation as ResizeNodeOperation, change) + break + case 'setNodeZIndex': + this.handleSetNodeZIndex(operation as SetNodeZIndexOperation, change) + break + case 'createNode': + this.handleCreateNode(operation as CreateNodeOperation, change) + break + case 'deleteNode': + this.handleDeleteNode(operation as DeleteNodeOperation, change) + break + case 'createLink': + this.handleCreateLink(operation as CreateLinkOperation, change) + break + case 'deleteLink': + this.handleDeleteLink(operation as DeleteLinkOperation, change) + break + case 'createReroute': + this.handleCreateReroute(operation as CreateRerouteOperation, change) + break + case 'deleteReroute': + this.handleDeleteReroute(operation as DeleteRerouteOperation, change) + break + case 'moveReroute': + this.handleMoveReroute(operation as MoveRerouteOperation, change) + break + } + } + + /** + * Finalize operation after transaction + */ + private finalizeOperation(change: LayoutChange): void { + // Update version + this.version++ + + // Manually trigger affected node refs after transaction + // This is needed because Yjs observers don't fire for property changes + change.nodeIds.forEach((nodeId) => { + const trigger = this.nodeTriggers.get(nodeId) + if (trigger) { + trigger() + } + }) + + // Notify listeners (after transaction completes) + setTimeout(() => this.notifyChange(change), 0) + } + + /** + * Subscribe to layout changes + */ + onChange(callback: (change: LayoutChange) => void): () => void { + this.changeListeners.add(callback) + return () => this.changeListeners.delete(callback) + } + + /** + * Set the current operation source + */ + setSource(source: LayoutSource): void { + this.currentSource = source + } + + /** + * Set the current actor (for CRDT) + */ + setActor(actor: string): void { + this.currentActor = actor + } + + /** + * Get the current operation source + */ + getCurrentSource(): LayoutSource { + return this.currentSource + } + + /** + * Get the current actor + */ + getCurrentActor(): string { + return this.currentActor + } + + /** + * Initialize store with existing nodes + */ + initializeFromLiteGraph( + nodes: Array<{ id: string; pos: [number, number]; size: [number, number] }> + ): void { + this.ydoc.transact(() => { + this.ynodes.clear() + this.nodeRefs.clear() + this.nodeTriggers.clear() + this.spatialIndex.clear() + this.linkSegmentSpatialIndex.clear() + this.slotSpatialIndex.clear() + this.rerouteSpatialIndex.clear() + this.linkLayouts.clear() + this.linkSegmentLayouts.clear() + this.slotLayouts.clear() + this.rerouteLayouts.clear() + + nodes.forEach((node, index) => { + const layout: NodeLayout = { + id: node.id.toString(), + position: { x: node.pos[0], y: node.pos[1] }, + size: { width: node.size[0], height: node.size[1] }, + zIndex: index, + visible: true, + bounds: { + x: node.pos[0], + y: node.pos[1], + width: node.size[0], + height: node.size[1] + } + } + + this.ynodes.set(layout.id, this.layoutToYNode(layout)) + + // Add to spatial index + this.spatialIndex.insert(layout.id, layout.bounds) + }) + }, 'initialization') + } + + // Operation handlers + private handleMoveNode( + operation: MoveNodeOperation, + change: LayoutChange + ): void { + const ynode = this.ynodes.get(operation.nodeId) + if (!ynode) { + return + } + + const size = ynode.get('size') as { width: number; height: number } + const newBounds = { + x: operation.position.x, + y: operation.position.y, + width: size.width, + height: size.height + } + + // Update spatial index FIRST, synchronously to prevent race conditions + // Hit detection queries can run before CRDT updates complete + this.spatialIndex.update(operation.nodeId, newBounds) + + // Update associated slot positions synchronously + this.updateNodeSlotPositions(operation.nodeId, operation.position) + + // Then update CRDT + ynode.set('position', operation.position) + this.updateNodeBounds(ynode, operation.position, size) + + change.nodeIds.push(operation.nodeId) + } + + private handleResizeNode( + operation: ResizeNodeOperation, + change: LayoutChange + ): void { + const ynode = this.ynodes.get(operation.nodeId) + if (!ynode) return + + const position = ynode.get('position') as Point + const newBounds = { + x: position.x, + y: position.y, + width: operation.size.width, + height: operation.size.height + } + + // Update spatial index FIRST, synchronously to prevent race conditions + // Hit detection queries can run before CRDT updates complete + this.spatialIndex.update(operation.nodeId, newBounds) + + // Update associated slot positions synchronously (size changes may affect slot positions) + this.updateNodeSlotPositions(operation.nodeId, position) + + // Then update CRDT + ynode.set('size', operation.size) + this.updateNodeBounds(ynode, position, operation.size) + + change.nodeIds.push(operation.nodeId) + } + + private handleSetNodeZIndex( + operation: SetNodeZIndexOperation, + change: LayoutChange + ): void { + const ynode = this.ynodes.get(operation.nodeId) + if (!ynode) return + + ynode.set('zIndex', operation.zIndex) + change.nodeIds.push(operation.nodeId) + } + + private handleCreateNode( + operation: CreateNodeOperation, + change: LayoutChange + ): void { + const ynode = this.layoutToYNode(operation.layout) + this.ynodes.set(operation.nodeId, ynode) + + // Add to spatial index + this.spatialIndex.insert(operation.nodeId, operation.layout.bounds) + + change.type = 'create' + change.nodeIds.push(operation.nodeId) + } + + private handleDeleteNode( + operation: DeleteNodeOperation, + change: LayoutChange + ): void { + if (!this.ynodes.has(operation.nodeId)) return + + this.ynodes.delete(operation.nodeId) + this.nodeRefs.delete(operation.nodeId) + this.nodeTriggers.delete(operation.nodeId) + + // Remove from spatial index + this.spatialIndex.remove(operation.nodeId) + + // Clean up associated slot layouts + this.deleteNodeSlotLayouts(operation.nodeId) + + // Clean up associated links + const linksToDelete = this.findLinksConnectedToNode(operation.nodeId) + + // Delete the associated links + for (const linkId of linksToDelete) { + this.ylinks.delete(String(linkId)) + this.linkLayouts.delete(linkId) + + // Clean up link segment layouts + this.cleanupLinkSegments(linkId) + } + + change.type = 'delete' + change.nodeIds.push(operation.nodeId) + } + + private handleCreateLink( + operation: CreateLinkOperation, + change: LayoutChange + ): void { + const linkData = new Y.Map() + linkData.set('id', operation.linkId) + linkData.set('sourceNodeId', operation.sourceNodeId) + linkData.set('sourceSlot', operation.sourceSlot) + linkData.set('targetNodeId', operation.targetNodeId) + linkData.set('targetSlot', operation.targetSlot) + + this.ylinks.set(String(operation.linkId), linkData) + + // Link geometry will be computed separately when nodes move + // This just tracks that the link exists + change.type = 'create' + } + + private handleDeleteLink( + operation: DeleteLinkOperation, + change: LayoutChange + ): void { + if (!this.ylinks.has(String(operation.linkId))) return + + this.ylinks.delete(String(operation.linkId)) + this.linkLayouts.delete(operation.linkId) + // Clean up any segment layouts for this link + this.cleanupLinkSegments(operation.linkId) + + change.type = 'delete' + } + + private handleCreateReroute( + operation: CreateRerouteOperation, + change: LayoutChange + ): void { + const rerouteData = new Y.Map() + rerouteData.set('id', operation.rerouteId) + rerouteData.set('position', operation.position) + rerouteData.set('parentId', operation.parentId) + rerouteData.set('linkIds', operation.linkIds) + + this.yreroutes.set(String(operation.rerouteId), rerouteData) // Yjs Map keys must be strings + + // The observer will automatically update the spatial index + change.type = 'create' + } + + private handleDeleteReroute( + operation: DeleteRerouteOperation, + change: LayoutChange + ): void { + if (!this.yreroutes.has(String(operation.rerouteId))) return // Yjs Map keys are strings + + this.yreroutes.delete(String(operation.rerouteId)) // Yjs Map keys are strings + this.rerouteLayouts.delete(operation.rerouteId) // Layout map uses numeric ID + this.rerouteSpatialIndex.remove(String(operation.rerouteId)) // Spatial index uses strings + + change.type = 'delete' + } + + private handleMoveReroute( + operation: MoveRerouteOperation, + change: LayoutChange + ): void { + const yreroute = this.yreroutes.get(String(operation.rerouteId)) // Yjs Map keys are strings + if (!yreroute) return + + yreroute.set('position', operation.position) + + const pos = operation.position + const layout: RerouteLayout = { + id: operation.rerouteId, + position: pos, + radius: 8, + bounds: { + x: pos.x - 8, + y: pos.y - 8, + width: 16, + height: 16 + } + } + this.updateRerouteLayout(operation.rerouteId, layout) + + // Mark as update for listeners + change.type = 'update' + } + + /** + * Update node bounds helper + */ + private updateNodeBounds( + ynode: Y.Map, + position: Point, + size: { width: number; height: number } + ): void { + ynode.set('bounds', { + x: position.x, + y: position.y, + width: size.width, + height: size.height + }) + } + + /** + * Find all links connected to a specific node + */ + private findLinksConnectedToNode(nodeId: NodeId): LinkId[] { + const connectedLinks: LinkId[] = [] + this.ylinks.forEach((linkData: Y.Map, linkIdStr: string) => { + const linkId = Number(linkIdStr) as LinkId + const sourceNodeId = linkData.get('sourceNodeId') as NodeId + const targetNodeId = linkData.get('targetNodeId') as NodeId + + if (sourceNodeId === nodeId || targetNodeId === nodeId) { + connectedLinks.push(linkId) + } + }) + return connectedLinks + } + + /** + * Handle link change events + */ + private handleLinkChange(change: YEventChange, linkIdStr: string): void { + if (change.action === 'delete') { + const linkId = Number(linkIdStr) as LinkId + this.cleanupLinkData(linkId) + } + // Link was added or updated - geometry will be computed separately + // This just tracks that the link exists in CRDT + } + + /** + * Clean up all data associated with a link + */ + private cleanupLinkData(linkId: LinkId): void { + this.linkLayouts.delete(linkId) + this.cleanupLinkSegments(linkId) + } + + /** + * Clean up all segment layouts for a link + */ + private cleanupLinkSegments(linkId: LinkId): void { + const keysToDelete: string[] = [] + for (const [key] of this.linkSegmentLayouts) { + if (key.startsWith(`${linkId}:`)) { + keysToDelete.push(key) + } + } + + for (const key of keysToDelete) { + this.linkSegmentLayouts.delete(key) + this.linkSegmentSpatialIndex.remove(key) + } + } + + /** + * Handle reroute change events + */ + private handleRerouteChange( + change: YEventChange, + rerouteIdStr: string + ): void { + const rerouteId = Number(rerouteIdStr) as RerouteId + + if (change.action === 'delete') { + this.handleRerouteDelete(rerouteId, rerouteIdStr) + } else if (change.action === 'update' || change.action === 'add') { + this.handleRerouteAddOrUpdate(rerouteId, rerouteIdStr) + } + } + + /** + * Handle reroute deletion + */ + private handleRerouteDelete( + rerouteId: RerouteId, + rerouteIdStr: string + ): void { + this.rerouteLayouts.delete(rerouteId) + this.rerouteSpatialIndex.remove(rerouteIdStr) + } + + /** + * Handle reroute add or update + */ + private handleRerouteAddOrUpdate( + rerouteId: RerouteId, + rerouteIdStr: string + ): void { + const rerouteData = this.yreroutes.get(rerouteIdStr) + if (!rerouteData) return + + const position = rerouteData.get('position') as Point + if (!position) return + + const layout = this.createRerouteLayout(rerouteId, position) + this.updateRerouteLayout(rerouteId, layout) + } + + /** + * Create reroute layout from position + */ + private createRerouteLayout( + rerouteId: RerouteId, + position: Point + ): RerouteLayout { + return { + id: rerouteId, + position, + radius: REROUTE_RADIUS, + bounds: { + x: position.x - REROUTE_RADIUS, + y: position.y - REROUTE_RADIUS, + width: REROUTE_RADIUS * 2, + height: REROUTE_RADIUS * 2 + } + } + } + + /** + * Update slot positions when a node moves + * TODO: This should be handled by the layout sync system (useSlotLayoutSync) + * rather than manually here. For now, we'll mark affected slots as needing recalculation. + */ + private updateNodeSlotPositions(nodeId: NodeId, _nodePosition: Point): void { + // Mark all slots for this node as potentially stale + // The layout sync system will recalculate positions on the next frame + const slotsToRemove: string[] = [] + + for (const [key, slotLayout] of this.slotLayouts) { + if (slotLayout.nodeId === nodeId) { + slotsToRemove.push(key) + } + } + + // Remove from spatial index so they'll be recalculated + for (const key of slotsToRemove) { + this.slotSpatialIndex.remove(key) + this.slotLayouts.delete(key) + } + } + + // Helper methods + private layoutToYNode(layout: NodeLayout): Y.Map { + const ynode = new Y.Map() + ynode.set('id', layout.id) + ynode.set('position', layout.position) + ynode.set('size', layout.size) + ynode.set('zIndex', layout.zIndex) + ynode.set('visible', layout.visible) + ynode.set('bounds', layout.bounds) + return ynode + } + + private yNodeToLayout(ynode: Y.Map): NodeLayout { + return { + id: ynode.get('id') as string, + position: ynode.get('position') as Point, + size: ynode.get('size') as { width: number; height: number }, + zIndex: ynode.get('zIndex') as number, + visible: ynode.get('visible') as boolean, + bounds: ynode.get('bounds') as Bounds + } + } + + private notifyChange(change: LayoutChange): void { + this.changeListeners.forEach((listener) => { + try { + listener(change) + } catch (error) { + console.error('Error in layout change listener:', error) + } + }) + } + + private pointInBounds(point: Point, bounds: Bounds): boolean { + return ( + point.x >= bounds.x && + point.x <= bounds.x + bounds.width && + point.y >= bounds.y && + point.y <= bounds.y + bounds.height + ) + } + + private boundsIntersect(a: Bounds, b: Bounds): boolean { + return !( + a.x + a.width < b.x || + b.x + b.width < a.x || + a.y + a.height < b.y || + b.y + b.height < a.y + ) + } + + // CRDT-specific methods + getOperationsSince(timestamp: number): LayoutOperation[] { + const operations: LayoutOperation[] = [] + this.yoperations.forEach((op: LayoutOperation) => { + if (op && op.timestamp > timestamp) { + operations.push(op) + } + }) + return operations + } + + getOperationsByActor(actor: string): LayoutOperation[] { + const operations: LayoutOperation[] = [] + this.yoperations.forEach((op: LayoutOperation) => { + if (op && op.actor === actor) { + operations.push(op) + } + }) + return operations + } + + /** + * Get the Yjs document for network sync (future feature) + */ + getYDoc(): Y.Doc { + return this.ydoc + } + + /** + * Apply updates from remote peers (future feature) + */ + applyUpdate(update: Uint8Array): void { + Y.applyUpdate(this.ydoc, update) + } + + /** + * Get state as update for sending to peers (future feature) + */ + getStateAsUpdate(): Uint8Array { + return Y.encodeStateAsUpdate(this.ydoc) + } +} + +// Create singleton instance +export const layoutStore = new LayoutStoreImpl() + +// Export types for convenience +export type { LayoutStore } from '@/renderer/core/layout/types' diff --git a/src/renderer/core/layout/sync/useLayoutSync.ts b/src/renderer/core/layout/sync/useLayoutSync.ts new file mode 100644 index 000000000..2aee7974c --- /dev/null +++ b/src/renderer/core/layout/sync/useLayoutSync.ts @@ -0,0 +1,79 @@ +/** + * Composable for syncing LiteGraph with the Layout system + * + * Implements one-way sync from Layout Store to LiteGraph. + * The layout store is the single source of truth. + */ +import { onUnmounted } from 'vue' + +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' + +/** + * Composable for syncing LiteGraph with the Layout system + * This replaces the bidirectional sync with a one-way sync + */ +export function useLayoutSync() { + let unsubscribe: (() => void) | null = null + + /** + * Start syncing from Layout system to LiteGraph + * This is one-way: Layout → LiteGraph only + */ + function startSync(canvas: any) { + if (!canvas?.graph) return + + // Subscribe to layout changes + unsubscribe = layoutStore.onChange((change) => { + // Apply changes to LiteGraph regardless of source + // The layout store is the single source of truth + for (const nodeId of change.nodeIds) { + const layout = layoutStore.getNodeLayoutRef(nodeId).value + if (!layout) continue + + const liteNode = canvas.graph.getNodeById(parseInt(nodeId)) + if (!liteNode) continue + + // Update position if changed + if ( + liteNode.pos[0] !== layout.position.x || + liteNode.pos[1] !== layout.position.y + ) { + liteNode.pos[0] = layout.position.x + liteNode.pos[1] = layout.position.y + } + + // Update size if changed + if ( + liteNode.size[0] !== layout.size.width || + liteNode.size[1] !== layout.size.height + ) { + liteNode.size[0] = layout.size.width + liteNode.size[1] = layout.size.height + } + } + + // Trigger single redraw for all changes + canvas.setDirty(true, true) + }) + } + + /** + * Stop syncing + */ + function stopSync() { + if (unsubscribe) { + unsubscribe() + unsubscribe = null + } + } + + // Auto-cleanup on unmount + onUnmounted(() => { + stopSync() + }) + + return { + startSync, + stopSync + } +} diff --git a/src/renderer/core/layout/sync/useLinkLayoutSync.ts b/src/renderer/core/layout/sync/useLinkLayoutSync.ts new file mode 100644 index 000000000..fd6b3b19c --- /dev/null +++ b/src/renderer/core/layout/sync/useLinkLayoutSync.ts @@ -0,0 +1,365 @@ +/** + * Composable for event-driven link layout synchronization + * + * Implements event-driven link layout updates decoupled from the render cycle. + * Updates link geometry only when it actually changes (node move/resize, link create/delete, + * reroute create/delete/move, collapse toggles). + */ +import log from 'loglevel' +import { onUnmounted } from 'vue' + +import type { LGraph } from '@/lib/litegraph/src/LGraph' +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import { LLink } from '@/lib/litegraph/src/LLink' +import { Reroute } from '@/lib/litegraph/src/Reroute' +import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' +import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' +import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { LayoutChange } from '@/renderer/core/layout/types' + +const logger = log.getLogger('useLinkLayoutSync') + +/** + * Composable for managing link layout synchronization + */ +export function useLinkLayoutSync() { + let canvas: LGraphCanvas | null = null + let graph: LGraph | null = null + let offscreenCtx: CanvasRenderingContext2D | null = null + let adapter: LitegraphLinkAdapter | null = null + let unsubscribeLayoutChange: (() => void) | null = null + let restoreHandlers: (() => void) | null = null + + /** + * Build link render context from canvas properties + */ + function buildLinkRenderContext(): LinkRenderContext { + if (!canvas) { + throw new Error('Canvas not initialized') + } + + return { + // Canvas settings + renderMode: canvas.links_render_mode, + connectionWidth: canvas.connections_width, + renderBorder: canvas.render_connections_border, + lowQuality: canvas.low_quality, + highQualityRender: canvas.highquality_render, + scale: canvas.ds.scale, + linkMarkerShape: canvas.linkMarkerShape, + renderConnectionArrows: canvas.render_connection_arrows, + + // State + highlightedLinks: new Set(Object.keys(canvas.highlighted_links)), + + // Colors + defaultLinkColor: canvas.default_link_color, + linkTypeColors: (canvas.constructor as any).link_type_colors || {}, + + // Pattern for disabled links + disabledPattern: canvas._pattern + } + } + + /** + * Recompute a single link and all its segments + * + * Note: This logic mirrors LGraphCanvas#renderAllLinkSegments but: + * - Works with offscreen context for event-driven updates + * - No visibility checks (always computes full geometry) + * - No dragging state handling (pure geometry computation) + */ + function recomputeLinkById(linkId: number): void { + if (!graph || !adapter || !offscreenCtx || !canvas) return + + const link = graph.links.get(linkId) + if (!link || link.id === -1) return // Skip floating/temp links + + // Get source and target nodes + const sourceNode = graph.getNodeById(link.origin_id) + const targetNode = graph.getNodeById(link.target_id) + if (!sourceNode || !targetNode) return + + // Get slots + const sourceSlot = sourceNode.outputs?.[link.origin_slot] + const targetSlot = targetNode.inputs?.[link.target_slot] + if (!sourceSlot || !targetSlot) return + + // Get positions + const startPos = getSlotPosition(sourceNode, link.origin_slot, false) + const endPos = getSlotPosition(targetNode, link.target_slot, true) + + // Get directions + const startDir = sourceSlot.dir || LinkDirection.RIGHT + const endDir = targetSlot.dir || LinkDirection.LEFT + + // Get reroutes for this link + const reroutes = LLink.getReroutes(graph, link) + + // Build render context + const context = buildLinkRenderContext() + + if (reroutes.length > 0) { + // Render segmented link with reroutes + let segmentStartPos = startPos + let segmentStartDir = startDir + + for (let i = 0; i < reroutes.length; i++) { + const reroute = reroutes[i] + + // Calculate reroute angle + reroute.calculateAngle(Date.now(), graph, [ + segmentStartPos[0], + segmentStartPos[1] + ]) + + // Calculate control points + const distance = Math.sqrt( + (reroute.pos[0] - segmentStartPos[0]) ** 2 + + (reroute.pos[1] - segmentStartPos[1]) ** 2 + ) + const dist = Math.min(Reroute.maxSplineOffset, distance * 0.25) + + // Special handling for floating input chain + const isFloatingInputChain = !sourceNode && targetNode + const startControl: ReadOnlyPoint = isFloatingInputChain + ? [0, 0] + : [dist * reroute.cos, dist * reroute.sin] + + // Render segment to this reroute + adapter.renderLinkDirect( + offscreenCtx, + segmentStartPos, + reroute.pos, + link, + true, // skip_border + 0, // flow + null, // color + segmentStartDir, + LinkDirection.CENTER, + context, + { + startControl, + endControl: reroute.controlPoint, + reroute, + disabled: false + } + ) + + // Prepare for next segment + segmentStartPos = reroute.pos + segmentStartDir = LinkDirection.CENTER + } + + // Render final segment from last reroute to target + const lastReroute = reroutes[reroutes.length - 1] + const finalDistance = Math.sqrt( + (endPos[0] - lastReroute.pos[0]) ** 2 + + (endPos[1] - lastReroute.pos[1]) ** 2 + ) + const finalDist = Math.min(Reroute.maxSplineOffset, finalDistance * 0.25) + const finalStartControl: ReadOnlyPoint = [ + finalDist * lastReroute.cos, + finalDist * lastReroute.sin + ] + + adapter.renderLinkDirect( + offscreenCtx, + lastReroute.pos, + endPos, + link, + true, // skip_border + 0, // flow + null, // color + LinkDirection.CENTER, + endDir, + context, + { + startControl: finalStartControl, + disabled: false + } + ) + } else { + // No reroutes - render direct link + adapter.renderLinkDirect( + offscreenCtx, + startPos, + endPos, + link, + true, // skip_border + 0, // flow + null, // color + startDir, + endDir, + context, + { + disabled: false + } + ) + } + } + + /** + * Recompute all links connected to a node + */ + function recomputeLinksForNode(nodeId: number): void { + if (!graph) return + + const node = graph.getNodeById(nodeId) + if (!node) return + + const linkIds = new Set() + + // Collect output links + if (node.outputs) { + for (const output of node.outputs) { + if (output.links) { + for (const linkId of output.links) { + linkIds.add(linkId) + } + } + } + } + + // Collect input links + if (node.inputs) { + for (const input of node.inputs) { + if (input.link !== null && input.link !== undefined) { + linkIds.add(input.link) + } + } + } + + // Recompute each link + for (const linkId of linkIds) { + recomputeLinkById(linkId) + } + } + + /** + * Recompute all links associated with a reroute + */ + function recomputeLinksForReroute(rerouteId: number): void { + if (!graph) return + + const reroute = graph.reroutes.get(rerouteId) + if (!reroute) return + + // Recompute all links that pass through this reroute + for (const linkId of reroute.linkIds) { + recomputeLinkById(linkId) + } + } + + /** + * Start link layout sync with event-driven functionality + */ + function start(canvasInstance: LGraphCanvas): void { + canvas = canvasInstance + graph = canvas.graph + if (!graph) return + + // Create offscreen canvas context + const offscreenCanvas = document.createElement('canvas') + offscreenCtx = offscreenCanvas.getContext('2d') + if (!offscreenCtx) { + logger.error('Failed to create offscreen canvas context') + return + } + + // Create dedicated adapter with layout writes enabled + adapter = new LitegraphLinkAdapter(graph) + adapter.enableLayoutStoreWrites = true + + // Initial computation for all existing links + for (const link of graph._links.values()) { + if (link.id !== -1) { + recomputeLinkById(link.id) + } + } + + // Subscribe to layout store changes + unsubscribeLayoutChange = layoutStore.onChange((change: LayoutChange) => { + switch (change.operation.type) { + case 'moveNode': + case 'resizeNode': + recomputeLinksForNode(parseInt(change.operation.nodeId)) + break + case 'createLink': + recomputeLinkById(change.operation.linkId) + break + case 'deleteLink': + // No-op - store already cleaned by existing code + break + case 'createReroute': + case 'deleteReroute': + // Recompute all affected links + if ('linkIds' in change.operation) { + for (const linkId of change.operation.linkIds) { + recomputeLinkById(linkId) + } + } + break + case 'moveReroute': + recomputeLinksForReroute(change.operation.rerouteId) + break + } + }) + + // Hook collapse events + const origTrigger = graph.onTrigger + + graph.onTrigger = (action: string, param: any) => { + if ( + action === 'node:property:changed' && + param?.property === 'flags.collapsed' + ) { + const nodeId = parseInt(String(param.nodeId)) + if (!isNaN(nodeId)) { + recomputeLinksForNode(nodeId) + } + } + if (origTrigger) { + origTrigger.call(graph, action, param) + } + } + + // Store cleanup function + restoreHandlers = () => { + if (graph) { + graph.onTrigger = origTrigger || undefined + } + } + } + + /** + * Stop link layout sync and cleanup all resources + */ + function stop(): void { + if (unsubscribeLayoutChange) { + unsubscribeLayoutChange() + unsubscribeLayoutChange = null + } + if (restoreHandlers) { + restoreHandlers() + restoreHandlers = null + } + canvas = null + graph = null + offscreenCtx = null + adapter = null + } + + // Auto-cleanup on unmount + onUnmounted(() => { + stop() + }) + + return { + start, + stop + } +} diff --git a/src/renderer/core/layout/sync/useSlotLayoutSync.ts b/src/renderer/core/layout/sync/useSlotLayoutSync.ts new file mode 100644 index 000000000..2bc7d53f3 --- /dev/null +++ b/src/renderer/core/layout/sync/useSlotLayoutSync.ts @@ -0,0 +1,163 @@ +/** + * Composable for managing slot layout registration + * + * Implements event-driven slot registration decoupled from the draw cycle. + * Registers slots once on initial load and keeps them updated when necessary. + */ +import { onUnmounted } from 'vue' + +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { type SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' +import { registerNodeSlots } from '@/renderer/core/layout/slots/register' +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 { + const nodeId = String(node.id) + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + + // Fallback to live node values if layout not ready + const nodeX = nodeLayout?.position.x ?? node.pos[0] + const nodeY = nodeLayout?.position.y ?? node.pos[1] + const nodeWidth = nodeLayout?.size.width ?? node.size[0] + const nodeHeight = nodeLayout?.size.height ?? node.size[1] + + // Ensure concrete slots & arrange when needed for accurate positions + node._setConcreteSlots() + const collapsed = node.flags.collapsed ?? false + if (!collapsed) { + node.arrange() + } + + const context: SlotPositionContext = { + nodeX, + nodeY, + nodeWidth, + nodeHeight, + collapsed, + collapsedWidth: node._collapsed_width, + slotStartY: node.constructor.slot_start_y, + inputs: node.inputs, + outputs: node.outputs, + widgets: node.widgets + } + + registerNodeSlots(nodeId, context) +} + +/** + * Composable for managing slot layout registration + */ +export function useSlotLayoutSync() { + let unsubscribeLayoutChange: (() => void) | null = null + let restoreHandlers: (() => void) | null = null + + /** + * Start slot layout sync with full event-driven functionality + * @param canvas LiteGraph canvas instance + */ + function start(canvas: LGraphCanvas): void { + // When Vue nodes are enabled, slot DOM registers exact positions. + // Skip calculated registration to avoid conflicts. + if (LiteGraph.vueNodesMode) { + return + } + const graph = canvas?.graph + if (!graph) return + + // Initial registration for all nodes in the current graph + for (const node of graph._nodes) { + computeAndRegisterSlots(node) + } + + // Layout changes → recompute slots for changed nodes + unsubscribeLayoutChange = layoutStore.onChange((change) => { + for (const nodeId of change.nodeIds) { + const node = graph.getNodeById(parseInt(nodeId)) + if (node) { + computeAndRegisterSlots(node) + } + } + }) + + // LiteGraph event hooks + const origNodeAdded = graph.onNodeAdded + const origNodeRemoved = graph.onNodeRemoved + const origTrigger = graph.onTrigger + const origAfterChange = graph.onAfterChange + + graph.onNodeAdded = (node: LGraphNode) => { + computeAndRegisterSlots(node) + if (origNodeAdded) { + origNodeAdded.call(graph, node) + } + } + + graph.onNodeRemoved = (node: LGraphNode) => { + layoutStore.deleteNodeSlotLayouts(String(node.id)) + if (origNodeRemoved) { + origNodeRemoved.call(graph, node) + } + } + + graph.onTrigger = (action: string, param: any) => { + if ( + action === 'node:property:changed' && + param?.property === 'flags.collapsed' + ) { + const node = graph.getNodeById(parseInt(String(param.nodeId))) + if (node) { + computeAndRegisterSlots(node) + } + } + if (origTrigger) { + origTrigger.call(graph, action, param) + } + } + + graph.onAfterChange = (graph: any, node?: any) => { + if (node && node.id) { + computeAndRegisterSlots(node) + } + if (origAfterChange) { + origAfterChange.call(graph, graph, node) + } + } + + // Store cleanup function + restoreHandlers = () => { + graph.onNodeAdded = origNodeAdded || undefined + graph.onNodeRemoved = origNodeRemoved || undefined + graph.onTrigger = origTrigger || undefined + graph.onAfterChange = origAfterChange || undefined + } + } + + /** + * Stop slot layout sync and cleanup all subscriptions + */ + function stop(): void { + if (unsubscribeLayoutChange) { + unsubscribeLayoutChange() + unsubscribeLayoutChange = null + } + if (restoreHandlers) { + restoreHandlers() + restoreHandlers = null + } + } + + // Auto-cleanup on unmount + onUnmounted(() => { + stop() + }) + + return { + start, + stop + } +} diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts new file mode 100644 index 000000000..8ff25ea25 --- /dev/null +++ b/src/renderer/core/layout/types.ts @@ -0,0 +1,322 @@ +/** + * Layout System - Type Definitions + * + * This file contains all type definitions for the layout system + * that manages node positions, bounds, spatial data, and operations. + */ +import type { ComputedRef, Ref } from 'vue' + +// Enum for layout source types +export enum LayoutSource { + Canvas = 'canvas', + Vue = 'vue', + External = 'external' +} + +// Basic geometric types +export interface Point { + x: number + y: number +} + +export interface Size { + width: number + height: number +} + +export interface Bounds { + x: number + y: number + width: number + height: number +} + +export type NodeId = string +export type LinkId = number +export type RerouteId = number + +// Layout data structures +export interface NodeLayout { + id: NodeId + position: Point + size: Size + zIndex: number + visible: boolean + // Computed bounds for hit testing + bounds: Bounds +} + +export interface SlotLayout { + nodeId: NodeId + index: number + type: 'input' | 'output' + position: Point + bounds: Bounds +} + +export interface LinkLayout { + id: LinkId + path: Path2D + bounds: Bounds + centerPos: Point + sourceNodeId: NodeId + targetNodeId: NodeId + sourceSlot: number + targetSlot: number +} + +// Layout for individual link segments (for precise hit-testing) +export interface LinkSegmentLayout { + linkId: LinkId + rerouteId: RerouteId | null // null for final segment to target + path: Path2D + bounds: Bounds + centerPos: Point +} + +export interface RerouteLayout { + id: RerouteId + position: Point + radius: number + bounds: Bounds +} + +/** + * Meta-only base for all operations - contains common fields + */ +export interface OperationMeta { + /** Unique operation ID for deduplication */ + id?: string + /** Timestamp for ordering operations */ + timestamp: number + /** Actor who performed the operation (for CRDT) */ + actor: string + /** Source system that initiated the operation */ + source: LayoutSource + /** Operation type discriminator */ + type: OperationType +} + +/** + * Entity-specific base types for proper type discrimination + */ +export type NodeOpBase = OperationMeta & { entity: 'node'; nodeId: NodeId } +export type LinkOpBase = OperationMeta & { entity: 'link'; linkId: LinkId } +export type RerouteOpBase = OperationMeta & { + entity: 'reroute' + rerouteId: RerouteId +} + +/** + * Operation type discriminator for type narrowing + */ +export type OperationType = + | 'moveNode' + | 'resizeNode' + | 'setNodeZIndex' + | 'createNode' + | 'deleteNode' + | 'setNodeVisibility' + | 'batchUpdate' + | 'createLink' + | 'deleteLink' + | 'createReroute' + | 'deleteReroute' + | 'moveReroute' + +/** + * Move node operation + */ +export interface MoveNodeOperation extends NodeOpBase { + type: 'moveNode' + position: Point + previousPosition: Point +} + +/** + * Resize node operation + */ +export interface ResizeNodeOperation extends NodeOpBase { + type: 'resizeNode' + size: { width: number; height: number } + previousSize: { width: number; height: number } +} + +/** + * Set node z-index operation + */ +export interface SetNodeZIndexOperation extends NodeOpBase { + type: 'setNodeZIndex' + zIndex: number + previousZIndex: number +} + +/** + * Create node operation + */ +export interface CreateNodeOperation extends NodeOpBase { + type: 'createNode' + layout: NodeLayout +} + +/** + * Delete node operation + */ +export interface DeleteNodeOperation extends NodeOpBase { + type: 'deleteNode' + previousLayout: NodeLayout +} + +/** + * Set node visibility operation + */ +export interface SetNodeVisibilityOperation extends NodeOpBase { + type: 'setNodeVisibility' + visible: boolean + previousVisible: boolean +} + +/** + * Batch update operation for atomic multi-property changes + */ +export interface BatchUpdateOperation extends NodeOpBase { + type: 'batchUpdate' + updates: Partial + previousValues: Partial +} + +/** + * Create link operation + */ +export interface CreateLinkOperation extends LinkOpBase { + type: 'createLink' + sourceNodeId: NodeId + sourceSlot: number + targetNodeId: NodeId + targetSlot: number +} + +/** + * Delete link operation + */ +export interface DeleteLinkOperation extends LinkOpBase { + type: 'deleteLink' +} + +/** + * Create reroute operation + */ +export interface CreateRerouteOperation extends RerouteOpBase { + type: 'createReroute' + position: Point + parentId?: RerouteId + linkIds: LinkId[] +} + +/** + * Delete reroute operation + */ +export interface DeleteRerouteOperation extends RerouteOpBase { + type: 'deleteReroute' +} + +/** + * Move reroute operation + */ +export interface MoveRerouteOperation extends RerouteOpBase { + type: 'moveReroute' + position: Point + previousPosition: Point +} + +/** + * Union of all operation types + */ +export type LayoutOperation = + | MoveNodeOperation + | ResizeNodeOperation + | SetNodeZIndexOperation + | CreateNodeOperation + | DeleteNodeOperation + | SetNodeVisibilityOperation + | BatchUpdateOperation + | CreateLinkOperation + | DeleteLinkOperation + | CreateRerouteOperation + | DeleteRerouteOperation + | MoveRerouteOperation + +export interface LayoutChange { + type: 'create' | 'update' | 'delete' + nodeIds: NodeId[] + timestamp: number + source: LayoutSource + operation: LayoutOperation +} + +// Store interfaces +export interface LayoutStore { + // CustomRef accessors for shared write access + getNodeLayoutRef(nodeId: NodeId): Ref + getNodesInBounds(bounds: Bounds): ComputedRef + getAllNodes(): ComputedRef> + getVersion(): ComputedRef + + // Spatial queries (non-reactive) + queryNodeAtPoint(point: Point): NodeId | null + queryNodesInBounds(bounds: Bounds): NodeId[] + + // Hit testing queries for links, slots, and reroutes + queryLinkAtPoint(point: Point, ctx?: CanvasRenderingContext2D): LinkId | null + queryLinkSegmentAtPoint( + point: Point, + ctx?: CanvasRenderingContext2D + ): { linkId: LinkId; rerouteId: RerouteId | null } | null + querySlotAtPoint(point: Point): SlotLayout | null + queryRerouteAtPoint(point: Point): RerouteLayout | null + queryItemsInBounds(bounds: Bounds): { + nodes: NodeId[] + links: LinkId[] + slots: string[] + reroutes: RerouteId[] + } + + // Update methods for link, slot, and reroute layouts + updateLinkLayout(linkId: LinkId, layout: LinkLayout): void + updateLinkSegmentLayout( + linkId: LinkId, + rerouteId: RerouteId | null, + layout: Omit + ): void + updateSlotLayout(key: string, layout: SlotLayout): void + updateRerouteLayout(rerouteId: RerouteId, layout: RerouteLayout): void + + // Delete methods for cleanup + deleteLinkLayout(linkId: LinkId): void + deleteLinkSegmentLayout(linkId: LinkId, rerouteId: RerouteId | null): void + deleteSlotLayout(key: string): void + deleteNodeSlotLayouts(nodeId: NodeId): void + deleteRerouteLayout(rerouteId: RerouteId): void + + // Get layout data + getLinkLayout(linkId: LinkId): LinkLayout | null + getSlotLayout(key: string): SlotLayout | null + getRerouteLayout(rerouteId: RerouteId): RerouteLayout | null + + // Direct mutation API (CRDT-ready) + applyOperation(operation: LayoutOperation): void + + // Change subscription + onChange(callback: (change: LayoutChange) => void): () => void + + // Initialization + initializeFromLiteGraph( + nodes: Array<{ id: string; pos: [number, number]; size: [number, number] }> + ): void + + // Source and actor management + setSource(source: LayoutSource): void + setActor(actor: string): void + getCurrentSource(): LayoutSource + getCurrentActor(): string +} diff --git a/src/renderer/core/spatial/SpatialIndex.ts b/src/renderer/core/spatial/SpatialIndex.ts new file mode 100644 index 000000000..5b6fb269c --- /dev/null +++ b/src/renderer/core/spatial/SpatialIndex.ts @@ -0,0 +1,169 @@ +/** + * Spatial Index Manager + * + * Manages spatial indexing for efficient node queries based on bounds. + * Uses QuadTree for fast spatial lookups with caching for performance. + */ +import { + PERFORMANCE_CONFIG, + QUADTREE_CONFIG +} from '@/renderer/core/layout/constants' +import type { Bounds, NodeId } from '@/renderer/core/layout/types' +import { QuadTree } from '@/utils/spatial/QuadTree' + +/** + * Cache entry for spatial queries + */ +interface CacheEntry { + result: NodeId[] + timestamp: number +} + +/** + * Spatial index manager using QuadTree + */ +export class SpatialIndexManager { + private quadTree: QuadTree + private queryCache: Map + private cacheSize = 0 + + constructor(bounds?: Bounds) { + this.quadTree = new QuadTree( + bounds ?? QUADTREE_CONFIG.DEFAULT_BOUNDS, + { + maxDepth: QUADTREE_CONFIG.MAX_DEPTH, + maxItemsPerNode: QUADTREE_CONFIG.MAX_ITEMS_PER_NODE + } + ) + this.queryCache = new Map() + } + + /** + * Insert a node into the spatial index + */ + insert(nodeId: NodeId, bounds: Bounds): void { + this.quadTree.insert(nodeId, bounds, nodeId) + this.invalidateCache() + } + + /** + * Update a node's bounds in the spatial index + */ + update(nodeId: NodeId, bounds: Bounds): void { + this.quadTree.update(nodeId, bounds) + this.invalidateCache() + } + + /** + * Remove a node from the spatial index + */ + remove(nodeId: NodeId): void { + this.quadTree.remove(nodeId) + this.invalidateCache() + } + + /** + * Query nodes within the given bounds + */ + query(bounds: Bounds): NodeId[] { + const cacheKey = this.getCacheKey(bounds) + const cached = this.queryCache.get(cacheKey) + + // Check cache validity + if (cached) { + const age = Date.now() - cached.timestamp + if (age < PERFORMANCE_CONFIG.SPATIAL_CACHE_TTL) { + return cached.result + } + // Remove stale entry + this.queryCache.delete(cacheKey) + this.cacheSize-- + } + + // Perform query + const result = this.quadTree.query(bounds) + + // Cache result + this.addToCache(cacheKey, result) + + return result + } + + /** + * Clear all nodes from the spatial index + */ + clear(): void { + this.quadTree.clear() + this.invalidateCache() + } + + /** + * Get the current size of the index + */ + get size(): number { + return this.quadTree.size + } + + /** + * Get debug information about the spatial index + */ + getDebugInfo() { + return { + quadTreeInfo: this.quadTree.getDebugInfo(), + cacheSize: this.cacheSize, + cacheEntries: this.queryCache.size + } + } + + /** + * Generate cache key for bounds + */ + private getCacheKey(bounds: Bounds): string { + return `${bounds.x},${bounds.y},${bounds.width},${bounds.height}` + } + + /** + * Add result to cache with LRU eviction + */ + private addToCache(key: string, result: NodeId[]): void { + // Evict oldest entries if cache is full + if (this.cacheSize >= PERFORMANCE_CONFIG.SPATIAL_CACHE_MAX_SIZE) { + const oldestKey = this.findOldestCacheEntry() + if (oldestKey) { + this.queryCache.delete(oldestKey) + this.cacheSize-- + } + } + + this.queryCache.set(key, { + result, + timestamp: Date.now() + }) + this.cacheSize++ + } + + /** + * Find oldest cache entry for LRU eviction + */ + private findOldestCacheEntry(): string | null { + let oldestKey: string | null = null + let oldestTime = Infinity + + for (const [key, entry] of this.queryCache) { + if (entry.timestamp < oldestTime) { + oldestTime = entry.timestamp + oldestKey = key + } + } + + return oldestKey + } + + /** + * Invalidate all cached queries + */ + private invalidateCache(): void { + this.queryCache.clear() + this.cacheSize = 0 + } +} diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue new file mode 100644 index 000000000..52dbacac0 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -0,0 +1,107 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue new file mode 100644 index 000000000..4ce4e2100 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -0,0 +1,271 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/NodeContent.vue b/src/renderer/extensions/vueNodes/components/NodeContent.vue new file mode 100644 index 000000000..f99e10917 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeContent.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue new file mode 100644 index 000000000..3b3da3101 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -0,0 +1,115 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.vue b/src/renderer/extensions/vueNodes/components/NodeSlots.vue new file mode 100644 index 000000000..68f247932 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.vue @@ -0,0 +1,113 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue new file mode 100644 index 000000000..0cd7a59cc --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -0,0 +1,155 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/OutputSlot.vue b/src/renderer/extensions/vueNodes/components/OutputSlot.vue new file mode 100644 index 000000000..144d5978d --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/OutputSlot.vue @@ -0,0 +1,106 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue b/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue new file mode 100644 index 000000000..d5b8b1ad8 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/SlotConnectionDot.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/extensions/vueNodes/components/widgets/LOD_IMPLEMENTATION_GUIDE.md b/src/renderer/extensions/vueNodes/components/widgets/LOD_IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000..95c8b40a3 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/widgets/LOD_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,295 @@ +# Level of Detail (LOD) Implementation Guide for Widgets + +## What is Level of Detail (LOD)? + +Level of Detail is a technique used to optimize performance by showing different amounts of detail based on how zoomed in the user is. Think of it like Google Maps - when you're zoomed out looking at the whole country, you only see major cities and highways. When you zoom in close, you see street names, building details, and restaurants. + +For ComfyUI nodes, this means: +- **Zoomed out** (viewing many nodes): Show only essential controls, hide labels and descriptions +- **Zoomed in** (focusing on specific nodes): Show all details, labels, help text, and visual polish + +## Why LOD Matters + +Without LOD optimization: +- 1000+ nodes with full detail = browser lag and poor performance +- Text that's too small to read still gets rendered (wasted work) +- Visual effects that are invisible at distance still consume GPU + +With LOD optimization: +- Smooth performance even with large node graphs +- Battery life improvement on laptops +- Better user experience across different zoom levels + +## How to Implement LOD in Your Widget + +### Step 1: Get the LOD Context + +Every widget component gets a `zoomLevel` prop. Use this to determine how much detail to show: + +```vue + +``` + +**Primary API:** Use `lodScore` (0-1) for granular control and smooth transitions +**Convenience API:** Use `lodLevel` ('minimal'|'reduced'|'full') for simple on/off decisions + +### Step 2: Choose What to Show at Different Zoom Levels + +#### Understanding the LOD Score +- `lodScore` is a number from 0 to 1 +- 0 = completely zoomed out (show minimal detail) +- 1 = fully zoomed in (show everything) +- 0.5 = medium zoom (show some details) + +#### Understanding LOD Levels +- `'minimal'` = zoom level 0.4 or below (very zoomed out) +- `'reduced'` = zoom level 0.4 to 0.8 (medium zoom) +- `'full'` = zoom level 0.8 or above (zoomed in close) + +### Step 3: Implement Your Widget's LOD Strategy + +Here's a complete example of a slider widget with LOD: + +```vue + + + + + +``` + +## Common LOD Patterns + +### Pattern 1: Essential vs. Nice-to-Have +```typescript +// Always show the main functionality +const showMainControl = computed(() => true) + +// Granular control with lodScore +const showLabels = computed(() => lodScore.value > 0.4) +const labelOpacity = computed(() => Math.max(0.3, lodScore.value)) + +// Simple control with lodLevel +const showExtras = computed(() => lodLevel.value === 'full') +``` + +### Pattern 2: Smooth Opacity Transitions +```typescript +// Gradually fade elements based on zoom +const labelOpacity = computed(() => { + // Fade in from zoom 0.3 to 0.6 + return Math.max(0, Math.min(1, (lodScore.value - 0.3) / 0.3)) +}) +``` + +### Pattern 3: Progressive Detail +```typescript +const detailLevel = computed(() => { + if (lodScore.value < 0.3) return 'none' + if (lodScore.value < 0.6) return 'basic' + if (lodScore.value < 0.8) return 'standard' + return 'full' +}) +``` + +## LOD Guidelines by Widget Type + +### Text Input Widgets +- **Always show**: The input field itself +- **Medium zoom**: Show label +- **High zoom**: Show placeholder text, validation messages +- **Full zoom**: Show character count, format hints + +### Button Widgets +- **Always show**: The button +- **Medium zoom**: Show button text +- **High zoom**: Show button description +- **Full zoom**: Show keyboard shortcuts, tooltips + +### Selection Widgets (Dropdown, Radio) +- **Always show**: The current selection +- **Medium zoom**: Show option labels +- **High zoom**: Show all options when expanded +- **Full zoom**: Show option descriptions, icons + +### Complex Widgets (Color Picker, File Browser) +- **Always show**: Simplified representation (color swatch, filename) +- **Medium zoom**: Show basic controls +- **High zoom**: Show full interface +- **Full zoom**: Show advanced options, previews + +## Design Collaboration Guidelines + +### For Designers +When designing widgets, consider creating variants for different zoom levels: + +1. **Minimal Design** (far away view) + - Essential elements only + - Higher contrast for visibility + - Simplified shapes and fewer details + +2. **Standard Design** (normal view) + - Balanced detail and simplicity + - Clear labels and readable text + - Good for most use cases + +3. **Full Detail Design** (close-up view) + - All labels, descriptions, and help text + - Rich visual effects and polish + - Maximum information density + +### Design Handoff Checklist +- [ ] Specify which elements are essential vs. nice-to-have +- [ ] Define minimum readable sizes for text elements +- [ ] Provide simplified versions for distant viewing +- [ ] Consider color contrast at different opacity levels +- [ ] Test designs at multiple zoom levels + +## Testing Your LOD Implementation + +### Manual Testing +1. Create a workflow with your widget +2. Zoom out until nodes are very small +3. Verify essential functionality still works +4. Zoom in gradually and check that details appear smoothly +5. Test performance with 50+ nodes containing your widget + +### Performance Considerations +- Avoid complex calculations in LOD computed properties +- Use `v-if` instead of `v-show` for elements that won't render +- Consider using `v-memo` for expensive widget content +- Test on lower-end devices + +### Common Mistakes +❌ **Don't**: Hide the main widget functionality at any zoom level +❌ **Don't**: Use complex animations that trigger at every zoom change +❌ **Don't**: Make LOD thresholds too sensitive (causes flickering) +❌ **Don't**: Forget to test with real content and edge cases + +✅ **Do**: Keep essential functionality always visible +✅ **Do**: Use smooth transitions between LOD levels +✅ **Do**: Test with varying content lengths and types +✅ **Do**: Consider accessibility at all zoom levels + +## Getting Help + +- Check existing widgets in `src/components/graph/vueNodes/widgets/` for examples +- Ask in the ComfyUI frontend Discord for LOD implementation questions +- Test your changes with the LOD debug panel (top-right in GraphCanvas) +- Profile performance impact using browser dev tools \ No newline at end of file diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts new file mode 100644 index 000000000..407a14243 --- /dev/null +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -0,0 +1,168 @@ +/** + * Composable for individual Vue node components + * + * Uses customRef for shared write access with Canvas renderer. + * Provides dragging functionality and reactive layout state. + */ +import { computed, inject } from 'vue' + +import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { LayoutSource, type Point } from '@/renderer/core/layout/types' + +/** + * Composable for individual Vue node components + * Uses customRef for shared write access with Canvas renderer + */ +export function useNodeLayout(nodeId: string) { + const store = layoutStore + const mutations = useLayoutMutations() + + // Get transform utilities from TransformPane if available + const transformState = inject('transformState') as + | { + canvasToScreen: (point: Point) => Point + screenToCanvas: (point: Point) => Point + } + | undefined + + // Get the customRef for this node (shared write access) + const layoutRef = store.getNodeLayoutRef(nodeId) + + // Computed properties for easy access + const position = computed(() => { + const layout = layoutRef.value + const pos = layout?.position ?? { x: 0, y: 0 } + return pos + }) + const size = computed( + () => layoutRef.value?.size ?? { width: 200, height: 100 } + ) + const bounds = computed( + () => + layoutRef.value?.bounds ?? { + x: position.value.x, + y: position.value.y, + width: size.value.width, + height: size.value.height + } + ) + const isVisible = computed(() => layoutRef.value?.visible ?? true) + const zIndex = computed(() => layoutRef.value?.zIndex ?? 0) + + // Drag state + let isDragging = false + let dragStartPos: Point | null = null + let dragStartMouse: Point | null = null + + /** + * Start dragging the node + */ + function startDrag(event: PointerEvent) { + if (!layoutRef.value) return + + isDragging = true + dragStartPos = { ...position.value } + dragStartMouse = { x: event.clientX, y: event.clientY } + + // Set mutation source + mutations.setSource(LayoutSource.Vue) + + // Capture pointer + const target = event.target as HTMLElement + target.setPointerCapture(event.pointerId) + } + + /** + * Handle drag movement + */ + const handleDrag = (event: PointerEvent) => { + if (!isDragging || !dragStartPos || !dragStartMouse || !transformState) { + return + } + + // Calculate mouse delta in screen coordinates + const mouseDelta = { + x: event.clientX - dragStartMouse.x, + y: event.clientY - dragStartMouse.y + } + + // Convert to canvas coordinates + const canvasOrigin = transformState.screenToCanvas({ x: 0, y: 0 }) + const canvasWithDelta = transformState.screenToCanvas(mouseDelta) + const canvasDelta = { + x: canvasWithDelta.x - canvasOrigin.x, + y: canvasWithDelta.y - canvasOrigin.y + } + + // Calculate new position + const newPosition = { + x: dragStartPos.x + canvasDelta.x, + y: dragStartPos.y + canvasDelta.y + } + + // Apply mutation through the layout system + mutations.moveNode(nodeId, newPosition) + } + + /** + * End dragging + */ + function endDrag(event: PointerEvent) { + if (!isDragging) return + + isDragging = false + dragStartPos = null + dragStartMouse = null + + // Release pointer + const target = event.target as HTMLElement + target.releasePointerCapture(event.pointerId) + } + + /** + * Update node position directly (without drag) + */ + function moveTo(position: Point) { + mutations.setSource(LayoutSource.Vue) + mutations.moveNode(nodeId, position) + } + + /** + * Update node size + */ + function resize(newSize: { width: number; height: number }) { + mutations.setSource(LayoutSource.Vue) + mutations.resizeNode(nodeId, newSize) + } + + return { + // Reactive state (via customRef) + layoutRef, + position, + size, + bounds, + isVisible, + zIndex, + + // Mutations + moveTo, + resize, + + // Drag handlers + startDrag, + handleDrag, + endDrag, + + // Computed styles for Vue templates + nodeStyle: computed(() => ({ + position: 'absolute' as const, + left: `${position.value.x}px`, + top: `${position.value.y}px`, + width: `${size.value.width}px`, + height: `${size.value.height}px`, + zIndex: zIndex.value, + cursor: isDragging ? 'grabbing' : 'grab' + })) + } +} diff --git a/src/renderer/extensions/vueNodes/lod/useLOD.ts b/src/renderer/extensions/vueNodes/lod/useLOD.ts new file mode 100644 index 000000000..9d07e37ca --- /dev/null +++ b/src/renderer/extensions/vueNodes/lod/useLOD.ts @@ -0,0 +1,186 @@ +/** + * Level of Detail (LOD) composable for Vue-based node rendering + * + * Provides dynamic quality adjustment based on zoom level to maintain + * performance with large node graphs. Uses zoom thresholds to determine + * how much detail to render for each node component. + * + * ## LOD Levels + * + * - **FULL** (zoom > 0.8): Complete rendering with all widgets, slots, and content + * - **REDUCED** (0.4 < zoom <= 0.8): Essential widgets only, simplified slots + * - **MINIMAL** (zoom <= 0.4): Title only, no widgets or slots + * + * ## Performance Benefits + * + * - Reduces DOM element count by up to 80% at low zoom levels + * - Minimizes layout calculations and paint operations + * - Enables smooth performance with 1000+ nodes + * - Maintains visual fidelity when detail is actually visible + * + * @example + * ```typescript + * const { lodLevel, shouldRenderWidgets, shouldRenderSlots } = useLOD(zoomRef) + * + * // In template + * + * + * ``` + */ +import { type Ref, computed, readonly } from 'vue' + +export enum LODLevel { + MINIMAL = 'minimal', // zoom <= 0.4 + REDUCED = 'reduced', // 0.4 < zoom <= 0.8 + FULL = 'full' // zoom > 0.8 +} + +export interface LODConfig { + renderWidgets: boolean + renderSlots: boolean + renderContent: boolean + renderSlotLabels: boolean + renderWidgetLabels: boolean + cssClass: string +} + +// LOD configuration for each level +const LOD_CONFIGS: Record = { + [LODLevel.FULL]: { + renderWidgets: true, + renderSlots: true, + renderContent: true, + renderSlotLabels: true, + renderWidgetLabels: true, + cssClass: 'lg-node--lod-full' + }, + [LODLevel.REDUCED]: { + renderWidgets: true, + renderSlots: true, + renderContent: false, + renderSlotLabels: false, + renderWidgetLabels: false, + cssClass: 'lg-node--lod-reduced' + }, + [LODLevel.MINIMAL]: { + renderWidgets: false, + renderSlots: false, + renderContent: false, + renderSlotLabels: false, + renderWidgetLabels: false, + cssClass: 'lg-node--lod-minimal' + } +} + +/** + * Create LOD (Level of Detail) state based on zoom level + * + * @param zoomRef - Reactive reference to current zoom level (camera.z) + * @returns LOD state and configuration + */ +export function useLOD(zoomRef: Ref) { + // Continuous LOD score (0-1) for smooth transitions + const lodScore = computed(() => { + const zoom = zoomRef.value + return Math.max(0, Math.min(1, zoom)) + }) + + // Determine current LOD level based on zoom + const lodLevel = computed(() => { + const zoom = zoomRef.value + + if (zoom > 0.8) return LODLevel.FULL + if (zoom > 0.4) return LODLevel.REDUCED + return LODLevel.MINIMAL + }) + + // Get configuration for current LOD level + const lodConfig = computed(() => LOD_CONFIGS[lodLevel.value]) + + // Convenience computed properties for common rendering decisions + const shouldRenderWidgets = computed(() => lodConfig.value.renderWidgets) + const shouldRenderSlots = computed(() => lodConfig.value.renderSlots) + const shouldRenderContent = computed(() => lodConfig.value.renderContent) + const shouldRenderSlotLabels = computed( + () => lodConfig.value.renderSlotLabels + ) + const shouldRenderWidgetLabels = computed( + () => lodConfig.value.renderWidgetLabels + ) + + // CSS class for styling based on LOD level + const lodCssClass = computed(() => lodConfig.value.cssClass) + + // Get essential widgets for reduced LOD (only interactive controls) + const getEssentialWidgets = (widgets: unknown[]): unknown[] => { + if (lodLevel.value === LODLevel.FULL) return widgets + if (lodLevel.value === LODLevel.MINIMAL) return [] + + // For reduced LOD, filter to essential widget types only + return widgets.filter((widget: any) => { + const type = widget?.type?.toLowerCase() + return [ + 'combo', + 'select', + 'toggle', + 'boolean', + 'slider', + 'number' + ].includes(type) + }) + } + + // Performance metrics for debugging + const lodMetrics = computed(() => ({ + level: lodLevel.value, + zoom: zoomRef.value, + widgetCount: shouldRenderWidgets.value ? 'full' : 'none', + slotCount: shouldRenderSlots.value ? 'full' : 'none' + })) + + return { + // Core LOD state + lodLevel: readonly(lodLevel), + lodConfig: readonly(lodConfig), + lodScore: readonly(lodScore), + + // Rendering decisions + shouldRenderWidgets, + shouldRenderSlots, + shouldRenderContent, + shouldRenderSlotLabels, + shouldRenderWidgetLabels, + + // Styling + lodCssClass, + + // Utilities + getEssentialWidgets, + lodMetrics + } +} + +/** + * Get LOD level thresholds for configuration or debugging + */ +export const LOD_THRESHOLDS = { + FULL_THRESHOLD: 0.8, + REDUCED_THRESHOLD: 0.4, + MINIMAL_THRESHOLD: 0.0 +} as const + +/** + * Check if zoom level supports a specific feature + */ +export function supportsFeatureAtZoom( + zoom: number, + feature: keyof LODConfig +): boolean { + const level = + zoom > 0.8 + ? LODLevel.FULL + : zoom > 0.4 + ? LODLevel.REDUCED + : LODLevel.MINIMAL + return LOD_CONFIGS[level][feature] as boolean +} diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue new file mode 100644 index 000000000..ae8fb7567 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue new file mode 100644 index 000000000..1c40d35a7 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue new file mode 100644 index 000000000..ed5f2b0ec --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue @@ -0,0 +1,63 @@ + + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue new file mode 100644 index 000000000..bdbc42739 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue @@ -0,0 +1,318 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue new file mode 100644 index 000000000..3603b7ab6 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue new file mode 100644 index 000000000..e51413a30 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue new file mode 100644 index 000000000..a515ec7cd --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue new file mode 100644 index 000000000..4749e561c --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue @@ -0,0 +1,95 @@ +