21 KiB
Project: Node Canvas Editor Modernization
Overview
Goal: Transform the custom node canvas editor from an opaque, monolithic legacy system into a well-documented, modular, and testable architecture that the team can confidently extend and maintain.
Why this matters:
- The canvas is the core developer UX - every user interaction flows through it
- Current ~2000+ line monolith (
nodegrapheditor.ts) is intimidating for contributors - AI-assisted coding works dramatically better with smaller, focused files
- Enables future features (minimap, connection tracing, better comments) without fear
- Establishes patterns for modernizing other legacy parts of the codebase
Out of scope (for now):
- Migration to React Flow or other library
- Runtime/execution changes
- New feature implementation (those come after this foundation)
Current Architecture Analysis
Core Files
| File | Lines (est.) | Responsibility | Coupling Level |
|---|---|---|---|
nodegrapheditor.ts |
~2000+ | Everything: rendering, interaction, selection, pan/zoom, connections, undo, clipboard | Extreme - God object |
NodeGraphEditorNode.ts |
~600 | Node rendering, layout, port drawing | High - tied to parent |
NodeGraphEditorConnection.ts |
~300 | Connection/noodle rendering, hit testing | Medium |
commentlayer.ts |
~400 | Comment system orchestration | Medium - React bridge |
CommentLayer/*.tsx |
~500 total | Comment React components | Lower - mostly isolated |
Key Integration Points
The canvas talks to these systems (will need interface boundaries):
ProjectModel.instance- Project state singletonNodeLibrary.instance- Node type definitions, color schemesDebugInspector.InspectorsModel- Data inspection/pinningWarningsModel.instance- Node warning statesUndoQueue.instance- Undo/redo managementEventDispatcher.instance- Global event busPopupLayer.instance- Context menus, tooltipsToastLayer- User notifications
Current Rendering Pipeline
paint() called
→ clearRect()
→ scale & translate context
→ paintHierarchy() - parent/child lines
→ paint connections (normal)
→ paint connections (highlighted - second pass for z-order)
→ paint nodes
→ paint drag indicators
→ paint multiselect box
→ paint dragging connection preview
Current Interaction Handling
All mouse events funnel through single mouse(type, pos, evt) method with massive switch/if chains handling:
- Node selection (single, multi, add-to)
- Node dragging
- Connection creation
- Pan (right-click, middle-click, space+left)
- Zoom (wheel)
- Context menus
- Insert location indicators
Target Architecture
Module Structure
views/
└── NodeGraphEditor/
├── index.ts # Public API export
├── NodeGraphEditor.ts # Main orchestrator (slim)
├── ARCHITECTURE.md # Living documentation
│
├── core/
│ ├── CanvasRenderer.ts # Canvas 2D rendering pipeline
│ ├── ViewportManager.ts # Pan, zoom, scale, bounds
│ ├── GraphLayout.ts # Node positioning, AABB calculations
│ └── types.ts # Shared interfaces and types
│
├── interaction/
│ ├── InteractionManager.ts # Mouse/keyboard event routing
│ ├── SelectionManager.ts # Single/multi select, highlight state
│ ├── DragManager.ts # Node dragging, drop targets
│ ├── ConnectionDragManager.ts # Creating new connections
│ └── PanZoomHandler.ts # Viewport manipulation
│
├── rendering/
│ ├── NodeRenderer.ts # Individual node painting
│ ├── ConnectionRenderer.ts # Connection/noodle painting
│ ├── HierarchyRenderer.ts # Parent-child relationship lines
│ └── OverlayRenderer.ts # Selection boxes, drag previews
│
├── features/
│ ├── ClipboardManager.ts # Cut, copy, paste
│ ├── UndoIntegration.ts # UndoQueue bridge
│ ├── ContextMenus.ts # Right-click menus
│ └── ConnectionTracer.ts # NEW: Connection chain navigation
│
├── comments/ # Existing React layer (enhance)
│ ├── CommentLayer.ts
│ ├── CommentLayerView.tsx
│ ├── CommentForeground.tsx
│ ├── CommentBackground.tsx
│ └── CommentStyles.ts # NEW: Extended styling options
│
└── __tests__/
├── CanvasRenderer.test.ts
├── ViewportManager.test.ts
├── SelectionManager.test.ts
├── ConnectionRenderer.test.ts
└── integration/
└── NodeGraphEditor.integration.test.ts
Key Interfaces
// core/types.ts
export interface IViewport {
readonly pan: { x: number; y: number };
readonly scale: number;
readonly bounds: AABB;
setPan(x: number, y: number): void;
setScale(scale: number, focalPoint?: Point): void;
screenToCanvas(screenPoint: Point): Point;
canvasToScreen(canvasPoint: Point): Point;
fitToContent(padding?: number): void;
}
export interface ISelectionManager {
readonly selectedNodes: ReadonlyArray<NodeGraphEditorNode>;
readonly highlightedNode: NodeGraphEditorNode | null;
readonly highlightedConnection: NodeGraphEditorConnection | null;
select(nodes: NodeGraphEditorNode[]): void;
addToSelection(node: NodeGraphEditorNode): void;
removeFromSelection(node: NodeGraphEditorNode): void;
clearSelection(): void;
setHighlight(node: NodeGraphEditorNode | null): void;
isSelected(node: NodeGraphEditorNode): boolean;
// Events
on(event: 'selectionChanged', handler: (nodes: NodeGraphEditorNode[]) => void): void;
}
export interface IConnectionTracer {
// Start tracing from a connection
startTrace(connection: NodeGraphEditorConnection): void;
// Navigate along the trace
nextConnection(): NodeGraphEditorConnection | null;
previousConnection(): NodeGraphEditorConnection | null;
// Get all connections in current trace
getTraceChain(): ReadonlyArray<NodeGraphEditorConnection>;
// Clear trace state
clearTrace(): void;
// Visual state
readonly activeTrace: ReadonlyArray<NodeGraphEditorConnection>;
}
export interface IRenderContext {
ctx: CanvasRenderingContext2D;
viewport: IViewport;
paintRect: AABB;
theme: ColorScheme;
}
Implementation Phases
Phase 1: Documentation & Analysis (3-4 days)
Goal: Fully understand and document current system before changing anything.
Tasks:
-
Create
ARCHITECTURE.mddocumenting:- Current file responsibilities
- Data flow diagrams
- Event flow diagrams
- Integration point catalog
- Known quirks and gotchas
-
Add inline documentation to existing code:
- JSDoc for all public methods
- Explain non-obvious logic
- Mark technical debt with
// TODO(canvas-refactor):
-
Create dependency graph visualization
Deliverables:
NodeGraphEditor/ARCHITECTURE.md- Fully documented
nodegrapheditor.ts(comments only, no code changes) - Mermaid diagram of component interactions
Confidence checkpoint: Can explain any part of the canvas system to a new developer.
Phase 2: Testing Foundation (4-5 days)
Goal: Establish testing infrastructure before refactoring.
Tasks:
-
Set up testing environment for canvas code:
- Jest configuration for canvas mocking
- Helper utilities for creating test nodes/connections
- Snapshot testing for render output (optional)
-
Write characterization tests for current behavior:
- Selection behavior (single click, shift+click, ctrl+click, marquee)
- Pan/zoom behavior
- Connection creation
- Clipboard operations
- Undo/redo integration
-
Create test fixtures:
- Sample graph configurations
- Mock ProjectModel, NodeLibrary, etc.
Deliverables:
__tests__/directory structure- Test utilities and fixtures
- 70%+ characterization test coverage for interaction logic
- CI integration for canvas tests
Confidence checkpoint: Tests catch regressions when code is modified.
Phase 3: Extract Core Modules (5-6 days)
Goal: Pull out clearly separable concerns without changing behavior.
Order of extraction (lowest risk first):
-
ViewportManager (~1 day)
- Extract:
getPanAndScale,setPanAndScale,clampPanAndScale,updateZoomLevel,centerToFit - Pure calculations, minimal dependencies
- Easy to test independently
- Extract:
-
GraphLayout (~1 day)
- Extract:
calculateNodesAABB,getCenterPanAndScale,getCenterRootPanAndScale, AABB utilities - Pure geometry calculations
- Easy to test
- Extract:
-
SelectionManager (~1.5 days)
- Extract:
selectorobject, highlight state, multi-select logic - Currently scattered across mouse handlers
- Introduce event emitter for state changes
- Extract:
-
ClipboardManager (~1 day)
- Extract:
copySelected,paste,getNodeSetFromClipboard,insertNodeSet - Relatively self-contained
- Extract:
-
Types & Interfaces (~0.5 days)
- Create
types.tswith all shared interfaces - Migrate inline types
- Create
Approach for each extraction:
1. Create new file with extracted code
2. Import into nodegrapheditor.ts
3. Delegate calls to new module
4. Run tests - verify no behavior change
5. Commit
Deliverables:
core/ViewportManager.tswith testscore/GraphLayout.tswith testsinteraction/SelectionManager.tswith testsfeatures/ClipboardManager.tswith testscore/types.ts
Confidence checkpoint: nodegrapheditor.ts reduced by ~400-500 lines, all tests pass.
Phase 4: Extract Rendering Pipeline (4-5 days)
Goal: Separate what we draw from when/why we draw it.
Tasks:
-
CanvasRenderer (~1.5 days)
- Extract:
paint()method orchestration - Introduce
IRenderContextfor dependency injection - Make rendering stateless (receives state, outputs pixels)
- Extract:
-
NodeRenderer (~1 day)
- Extract from
NodeGraphEditorNode.paint() - Parameterize colors, sizes for future customization
- Document the rendering anatomy of a node
- Extract from
-
ConnectionRenderer (~1 day)
- Extract from
NodeGraphEditorConnection.paint() - Prepare for future routing algorithms
- Add support for trace highlighting (prep for Phase 6)
- Extract from
-
OverlayRenderer (~0.5 days)
- Extract: multiselect box, drag preview, insert indicators
- These are temporary visual states
Deliverables:
rendering/module with all renderers- Renderer unit tests
- Clear separation: state management ≠ rendering
Confidence checkpoint: Can modify node appearance without touching interaction code.
Phase 5: Extract Interaction Handling (4-5 days)
Goal: Untangle the mouse event spaghetti.
Tasks:
-
InteractionManager (~1 day)
- Central event router
- Delegates to specialized handlers based on state
- Manages interaction modes (normal, panning, dragging, connecting)
-
DragManager (~1 day)
- Node drag start/move/end
- Drop target detection
- Insert location indicators
-
ConnectionDragManager (~1 day)
- New connection creation flow
- Port detection and highlighting
- Connection preview rendering
-
PanZoomHandler (~0.5 days)
- Mouse wheel zoom
- Right/middle click pan
- Space+drag pan
-
Refactor main mouse() method (~0.5 days)
- Reduce to simple routing logic
- Each handler owns its interaction mode
Deliverables:
interaction/module complete- Interaction tests (simulate mouse events)
nodegrapheditor.tsmouse handling reduced to ~50 lines
Confidence checkpoint: Can add new interaction modes without touching existing handlers.
Phase 6: Feature Enablement - Connection Tracer (3-4 days)
Goal: Implement connection tracing as proof that the new architecture works.
Feature spec:
- Click a connection to start tracing
- Highlighted connection chain shows the data flow path
- Keyboard navigation (Tab/Shift+Tab) to walk the chain
- Visual distinction for traced connections (glow, thicker line, different color)
- Click elsewhere or Escape to clear trace
Tasks:
-
ConnectionTracer module (~1.5 days)
- Graph traversal logic
- Find upstream/downstream connections from a node's port
- Handle cycles gracefully
-
Visual integration (~1 day)
- Extend
ConnectionRendererfor trace state - Add trace highlight color to theme
- Subtle animation for active trace (optional)
- Extend
-
Interaction integration (~1 day)
- Add to
InteractionManager - Keyboard handler for navigation
- Context menu option: "Trace connection"
- Add to
Deliverables:
features/ConnectionTracer.tswith full tests- Working connection tracing feature
- Documentation for how to add similar features
Confidence checkpoint: Feature works, and implementation was straightforward given new architecture.
Phase 7: Feature Enablement - Comment Enhancements (2-3 days)
Goal: Improve comment system as second proof point.
Feature spec:
- More color options
- Border style options (solid, dashed, none)
- Font size options (small, medium, large, extra-large)
- Opacity control for filled comments
- Corner radius options
- Z-index control (send to back, bring to front)
Tasks:
-
Extend comment model (~0.5 days)
- Add new properties: borderStyle, fontSize, opacity, cornerRadius, zIndex
- Migration for existing comments (defaults)
-
Update CommentForeground controls (~1 day)
- Extended toolbar UI
- New control components
-
Update rendering (~0.5 days)
- Apply new styles in CommentBackground
- CSS updates
-
Tests (~0.5 days)
- Comment styling tests
- Backward compatibility tests
Deliverables:
- Enhanced comment styling options
- Updated
CommentStyles.ts - Tests for new functionality
File Change Summary
Files to Create
views/NodeGraphEditor/
├── ARCHITECTURE.md
├── core/
│ ├── CanvasRenderer.ts
│ ├── ViewportManager.ts
│ ├── GraphLayout.ts
│ └── types.ts
├── interaction/
│ ├── InteractionManager.ts
│ ├── SelectionManager.ts
│ ├── DragManager.ts
│ ├── ConnectionDragManager.ts
│ └── PanZoomHandler.ts
├── rendering/
│ ├── NodeRenderer.ts
│ ├── ConnectionRenderer.ts
│ ├── HierarchyRenderer.ts
│ └── OverlayRenderer.ts
├── features/
│ ├── ClipboardManager.ts
│ ├── UndoIntegration.ts
│ ├── ContextMenus.ts
│ └── ConnectionTracer.ts
├── comments/
│ └── CommentStyles.ts
└── __tests__/
└── [comprehensive test suite]
Files to Modify
nodegrapheditor.ts→ Slim orchestrator importing modulesNodeGraphEditorNode.ts→ Delegate rendering to NodeRendererNodeGraphEditorConnection.ts→ Delegate rendering to ConnectionRendererCommentLayerView.tsx→ Extended styling UICommentForeground.tsx→ New controlsCommentBackground.tsx→ New style application
Files Unchanged
commentlayer.ts→ Keep as bridge layer (minor updates)- Model files (ProjectModel, NodeLibrary, etc.) → Interface boundaries only
Testing Strategy
Unit Tests
Each extracted module gets comprehensive unit tests:
// Example: ViewportManager.test.ts
describe('ViewportManager', () => {
describe('screenToCanvas', () => {
it('converts screen coordinates at scale 1', () => {
const viewport = new ViewportManager({ width: 800, height: 600 });
viewport.setPan(100, 50);
const result = viewport.screenToCanvas({ x: 200, y: 150 });
expect(result).toEqual({ x: 100, y: 100 });
});
it('accounts for scale when converting', () => {
const viewport = new ViewportManager({ width: 800, height: 600 });
viewport.setScale(0.5);
viewport.setPan(100, 50);
const result = viewport.screenToCanvas({ x: 200, y: 150 });
expect(result).toEqual({ x: 300, y: 250 });
});
});
describe('fitToContent', () => {
it('adjusts pan and scale to show all nodes', () => {
// ...
});
});
});
Integration Tests
Test module interactions:
// Example: Selection + Rendering integration
describe('Selection rendering integration', () => {
it('renders selection box around selected nodes', () => {
const graph = createTestGraph([
{ id: 'node1', x: 0, y: 0 },
{ id: 'node2', x: 200, y: 0 }
]);
const selection = new SelectionManager();
const renderer = new CanvasRenderer();
selection.select([graph.nodes[0], graph.nodes[1]]);
renderer.render(graph, selection);
expect(renderer.getLastRenderCall()).toContainOverlay('multiselect-box');
});
});
Characterization Tests
Capture current behavior before refactoring:
// Example: Existing pan behavior
describe('Pan behavior (characterization)', () => {
it('right-click drag pans the viewport', async () => {
const editor = await createTestEditor();
const initialPan = editor.getPanAndScale();
await editor.simulateMouseEvent('down', { x: 100, y: 100, button: 2 });
await editor.simulateMouseEvent('move', { x: 150, y: 120 });
await editor.simulateMouseEvent('up', { x: 150, y: 120, button: 2 });
const finalPan = editor.getPanAndScale();
expect(finalPan.x - initialPan.x).toBe(50);
expect(finalPan.y - initialPan.y).toBe(20);
});
});
Success Criteria
Quantitative
nodegrapheditor.tsreduced from ~2000 to <500 lines- No single file >400 lines in new structure
- Test coverage >80% for new modules
- All existing functionality preserved (zero regressions)
Qualitative
- New developer can understand canvas architecture in <30 minutes
- Adding a new interaction mode takes <2 hours
- Adding a new visual effect takes <1 hour
- AI coding assistants can work effectively with individual modules
ARCHITECTURE.mdaccurately describes the system
Feature Validation
- Connection tracing works as specified
- Comment enhancements work as specified
- Both features implemented using new architecture patterns
Risks & Mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Hidden dependencies break during extraction | Medium | High | Extensive characterization tests before any changes |
| Performance regression from module overhead | Low | Medium | Benchmark critical paths, keep hot loops tight |
| Over-engineering abstractions | Medium | Medium | Extract only what exists, don't pre-build for imagined needs |
| Scope creep into features | Medium | Medium | Strict phase gates, no features until Phase 6 |
| Breaking existing user workflows | Low | High | Full test coverage, careful rollout |
Estimated Timeline
| Phase | Duration | Dependencies |
|---|---|---|
| Phase 1: Documentation | 3-4 days | None |
| Phase 2: Testing Foundation | 4-5 days | Phase 1 |
| Phase 3: Core Modules | 5-6 days | Phase 2 |
| Phase 4: Rendering | 4-5 days | Phase 3 |
| Phase 5: Interaction | 4-5 days | Phase 3, 4 |
| Phase 6: Connection Tracer | 3-4 days | Phase 5 |
| Phase 7: Comment Enhancements | 2-3 days | Phase 4 |
Total: 26-32 days (5-7 weeks at sustainable pace)
Phases 6 and 7 can be done in parallel or interleaved with other work.
Getting Started
- Create feature branch:
feature/canvas-editor-modernization - Start with Phase 1 - no code changes, just documentation
- Review
ARCHITECTURE.mdwith team before proceeding - Set up CI for canvas tests before Phase 3
- Small, frequent commits with clear messages
Appendix: Current Code Locations
packages/noodl-editor/src/editor/src/views/
├── nodegrapheditor.ts # Main canvas (THE MONOLITH)
├── nodegrapheditor/
│ ├── NodeGraphEditorNode.ts # Node rendering
│ └── NodeGraphEditorConnection.ts # Connection rendering
├── commentlayer.ts # Comment orchestration
├── CommentLayer/
│ ├── CommentLayer.css
│ ├── CommentLayerView.tsx
│ ├── CommentForeground.tsx
│ └── CommentBackground.tsx
└── documents/EditorDocument/
└── hooks/
├── UseCanvasView.ts
└── UseImportNodeset.ts
Notes for AI-Assisted Development
When working with Cline or similar tools on this refactoring:
- Single module focus: Work on one module at a time, complete with tests
- Confidence checks: After each extraction, verify tests pass before continuing
- Small commits: Each extraction should be a single, reviewable commit
- Documentation first: Update
ARCHITECTURE.mdas you go - No premature optimization: Extract what exists, optimize later if needed
Example prompt structure for Phase 3 extractions:
"Extract ViewportManager from nodegrapheditor.ts:
1. Identify all pan/zoom/scale related code
2. Create core/ViewportManager.ts with those methods
3. Create interface IViewport in types.ts
4. Add comprehensive unit tests
5. Update nodegrapheditor.ts to use ViewportManager
6. Verify all existing tests still pass
7. Confidence score before committing?"