Files
OpenNoodl/dev-docs/future-projects/CANVAS-MODERNISATION-PROJECT.md
2025-12-15 11:58:55 +01:00

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 singleton
  • NodeLibrary.instance - Node type definitions, color schemes
  • DebugInspector.InspectorsModel - Data inspection/pinning
  • WarningsModel.instance - Node warning states
  • UndoQueue.instance - Undo/redo management
  • EventDispatcher.instance - Global event bus
  • PopupLayer.instance - Context menus, tooltips
  • ToastLayer - 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:

  1. Create ARCHITECTURE.md documenting:

    • Current file responsibilities
    • Data flow diagrams
    • Event flow diagrams
    • Integration point catalog
    • Known quirks and gotchas
  2. Add inline documentation to existing code:

    • JSDoc for all public methods
    • Explain non-obvious logic
    • Mark technical debt with // TODO(canvas-refactor):
  3. 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:

  1. 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)
  2. 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
  3. 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):

  1. ViewportManager (~1 day)

    • Extract: getPanAndScale, setPanAndScale, clampPanAndScale, updateZoomLevel, centerToFit
    • Pure calculations, minimal dependencies
    • Easy to test independently
  2. GraphLayout (~1 day)

    • Extract: calculateNodesAABB, getCenterPanAndScale, getCenterRootPanAndScale, AABB utilities
    • Pure geometry calculations
    • Easy to test
  3. SelectionManager (~1.5 days)

    • Extract: selector object, highlight state, multi-select logic
    • Currently scattered across mouse handlers
    • Introduce event emitter for state changes
  4. ClipboardManager (~1 day)

    • Extract: copySelected, paste, getNodeSetFromClipboard, insertNodeSet
    • Relatively self-contained
  5. Types & Interfaces (~0.5 days)

    • Create types.ts with all shared interfaces
    • Migrate inline types

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.ts with tests
  • core/GraphLayout.ts with tests
  • interaction/SelectionManager.ts with tests
  • features/ClipboardManager.ts with tests
  • core/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:

  1. CanvasRenderer (~1.5 days)

    • Extract: paint() method orchestration
    • Introduce IRenderContext for dependency injection
    • Make rendering stateless (receives state, outputs pixels)
  2. NodeRenderer (~1 day)

    • Extract from NodeGraphEditorNode.paint()
    • Parameterize colors, sizes for future customization
    • Document the rendering anatomy of a node
  3. ConnectionRenderer (~1 day)

    • Extract from NodeGraphEditorConnection.paint()
    • Prepare for future routing algorithms
    • Add support for trace highlighting (prep for Phase 6)
  4. 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:

  1. InteractionManager (~1 day)

    • Central event router
    • Delegates to specialized handlers based on state
    • Manages interaction modes (normal, panning, dragging, connecting)
  2. DragManager (~1 day)

    • Node drag start/move/end
    • Drop target detection
    • Insert location indicators
  3. ConnectionDragManager (~1 day)

    • New connection creation flow
    • Port detection and highlighting
    • Connection preview rendering
  4. PanZoomHandler (~0.5 days)

    • Mouse wheel zoom
    • Right/middle click pan
    • Space+drag pan
  5. 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.ts mouse 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:

  1. ConnectionTracer module (~1.5 days)

    • Graph traversal logic
    • Find upstream/downstream connections from a node's port
    • Handle cycles gracefully
  2. Visual integration (~1 day)

    • Extend ConnectionRenderer for trace state
    • Add trace highlight color to theme
    • Subtle animation for active trace (optional)
  3. Interaction integration (~1 day)

    • Add to InteractionManager
    • Keyboard handler for navigation
    • Context menu option: "Trace connection"

Deliverables:

  • features/ConnectionTracer.ts with 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:

  1. Extend comment model (~0.5 days)

    • Add new properties: borderStyle, fontSize, opacity, cornerRadius, zIndex
    • Migration for existing comments (defaults)
  2. Update CommentForeground controls (~1 day)

    • Extended toolbar UI
    • New control components
  3. Update rendering (~0.5 days)

    • Apply new styles in CommentBackground
    • CSS updates
  4. 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 modules
  • NodeGraphEditorNode.ts → Delegate rendering to NodeRenderer
  • NodeGraphEditorConnection.ts → Delegate rendering to ConnectionRenderer
  • CommentLayerView.tsx → Extended styling UI
  • CommentForeground.tsx → New controls
  • CommentBackground.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.ts reduced 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.md accurately 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

  1. Create feature branch: feature/canvas-editor-modernization
  2. Start with Phase 1 - no code changes, just documentation
  3. Review ARCHITECTURE.md with team before proceeding
  4. Set up CI for canvas tests before Phase 3
  5. 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:

  1. Single module focus: Work on one module at a time, complete with tests
  2. Confidence checks: After each extraction, verify tests pass before continuing
  3. Small commits: Each extraction should be a single, reviewable commit
  4. Documentation first: Update ARCHITECTURE.md as you go
  5. 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?"