26 KiB
TASK-000J: Canvas Organization System
Overview
This task implements a comprehensive canvas organization system to address the chaos that emerges in complex node graphs. The primary problem: as users add nodes and connections, nodes expand vertically (due to new ports), groupings lose meaning, and the canvas becomes unmanageable.
The core philosophy: Work with lazy users, not against them. Rather than forcing component creation, provide organizational tools that are easier than the current chaos but don't require significant workflow changes.
Background
The Problem
Looking at a typical complex component canvas:
- Vertical expansion breaks layouts - When a node gains ports, it grows vertically, overlapping nodes below it
- No persistent groupings - Users mentally group related nodes, but nothing enforces or maintains these groupings
- Connection spaghetti - With many connections, it's impossible to trace data flow
- No navigation - In large canvases, users pan around aimlessly looking for specific logic
- Comments are passive - Current comment boxes are purely visual; nodes inside them don't behave as a group
Design Principles
- Backward compatible - Existing projects with comment boxes must work unchanged
- Opt-in complexity - Simple projects don't need these features; complex projects benefit
- User responsibility - Users create organization; system maintains it
- Minimal UI footprint - Features should feel native to the existing canvas
Feature Summary
| Feature | Purpose | Complexity | Impact |
|---|---|---|---|
| Smart Frames | Group nodes that move/resize together | Medium-High | ⭐⭐⭐⭐⭐ |
| Canvas Navigation | Minimap + jump-to-frame | Medium | ⭐⭐⭐⭐ |
| Vertical Snap + Push | Keep stacked nodes organized | Medium | ⭐⭐⭐ |
| Connection Labels | Annotate data flow on connections | Medium | ⭐⭐⭐⭐ |
Total Estimate: 45-65 hours (6-8 days)
Feature 1: Smart Frames
Description
Evolve the existing Comment system into "Smart Frames" - visual containers that actually contain their nodes. When a frame moves, nodes inside move with it. When nodes inside expand, the frame grows to accommodate them.
Backward Compatibility
Critical requirement: Existing comment boxes must continue to work as purely visual elements. Smart Frame behavior is opt-in:
- Legacy comment boxes render and behave exactly as before
- Dragging a node INTO a comment box converts it to a Smart Frame and adds the node to its group
- Dragging a node OUT of a Smart Frame removes it from the group
- Empty Smart Frames revert to passive comment boxes
This means:
- Old projects load with no changes
- Users gradually adopt Smart Frames by dragging nodes into existing comments
- No migration required
Capabilities
| Capability | Description |
|---|---|
| Visual container | Colored rectangle with title text (uses existing comment styling) |
| Opt-in containment | Drag node into frame to add; drag out to remove |
| Group movement | When frame is dragged, all contained nodes move together |
| Auto-resize | Frame grows/shrinks to fit contained nodes + padding |
| Collapse/Expand | Toggle to collapse frame to title bar only |
| Collapsed connections | When collapsed, connections to internal nodes render as dots on frame edge |
| Title as label | Frame title serves as the organizational label |
| Nav anchor | Each Smart Frame becomes a navigation waypoint (see Feature 2) |
Collapse Behavior
When collapsed:
┌─── Login Flow ──────────────────────────┐
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Email │───►│Validate│──►│ Login │ │
│ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────┘
│
▼ collapse
┌─── Login Flow ───●────────●─────────────►
▲ ▲
input dots output dots
- Frame height reduces to title bar only
- Internal nodes hidden but connections preserved
- Dots on frame edge show connection entry/exit points
- Clicking dots could highlight the connection path (nice-to-have)
Data Model Extension
Extend CommentsModel / Comment interface:
interface Comment {
// Existing fields
id: string;
text: string;
x: number;
y: number;
width: number;
height: number;
fill: CommentFillStyle;
color: string;
largeFont?: boolean;
// New Smart Frame fields
containedNodeIds?: string[]; // Empty = passive comment, populated = Smart Frame
isCollapsed?: boolean;
autoResize?: boolean; // Default true for Smart Frames
}
Implementation Approach
- Detection: Check if
containedNodeIdshas items to determine behavior mode - Adding nodes: On node drag-end, check if position is inside any comment bounds; if so, add to
containedNodeIds - Removing nodes: On node drag-start from inside a frame, if dragged outside bounds, remove from
containedNodeIds - Group movement: When frame is moved, apply delta to all contained node positions
- Auto-resize: After any contained node position/size change, recalculate frame bounds
- Collapse rendering: When
isCollapsed, render only title bar and calculate connection dots
Files to Modify
packages/noodl-editor/src/editor/src/models/commentsmodel.ts
- Add containedNodeIds, isCollapsed, autoResize to Comment interface
- Add methods: addNodeToFrame(), removeNodeFromFrame(), toggleCollapse()
packages/noodl-editor/src/editor/src/views/CommentLayer/CommentLayerView.tsx
- Handle collapsed rendering mode
- Render connection dots for collapsed frames
packages/noodl-editor/src/editor/src/views/CommentLayer/CommentForeground.tsx
- Add collapse/expand button to comment controls
- Update resize behavior for Smart Frames
packages/noodl-editor/src/editor/src/views/CommentLayer/CommentBackground.tsx
- Handle collapsed visual state
packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts
- On node drag-end: check for frame containment
- On node drag-start: handle removal from frame
- On frame drag: move contained nodes
- Subscribe to node size changes for auto-resize
packages/noodl-editor/src/editor/src/views/commentlayer.ts
- Coordinate between CommentLayer and NodeGraphEditor for containment logic
Files to Create
packages/noodl-editor/src/editor/src/views/CommentLayer/SmartFrameUtils.ts
- isPointInFrame(point, frame): boolean
- calculateFrameBounds(nodeIds, padding): Bounds
- getConnectionDotsForCollapsedFrame(frame, connections): ConnectionDot[]
Success Criteria
- Existing comment boxes work exactly as before (no behavioral change)
- Dragging a node into a comment box adds it to the frame
- Dragging a node out of a frame removes it
- Moving a Smart Frame moves all contained nodes
- Contained nodes expanding causes frame to grow
- Collapse button appears on Smart Frame controls
- Collapsed frame shows only title bar
- Connections to collapsed frame nodes render as dots on edge
- Empty Smart Frames revert to passive comments
Feature 2: Canvas Navigation
Description
A minimap overlay and jump-to navigation system for quickly moving around large canvases. Smart Frames automatically become navigation anchors.
Capabilities
| Capability | Description |
|---|---|
| Minimap toggle | Button in canvas toolbar to show/hide minimap |
| Minimap overlay | Small rectangle in corner showing frame locations |
| Viewport indicator | Rectangle showing current visible area |
| Click to navigate | Click anywhere on minimap to pan there |
| Frame list | Dropdown/list of all Smart Frames for quick jump |
| Keyboard shortcuts | Cmd+1..9 to jump to frames (in order of creation or position) |
Minimap Design
┌──────────────────────────────────────────────────┐
│ │
│ [Main Canvas] │
│ │
│ ┌─────┐│
│ │▪ A ││
│ │ ▪B ││ ← Minimap
│ │ ┌─┐ ││ ← Viewport
│ │ └─┘ ││
│ │▪ C ││
│ └─────┘│
└──────────────────────────────────────────────────┘
- Each
▪represents a Smart Frame (labeled with first letter or number) ┌─┐rectangle shows current viewport- Colors could match frame colors
Jump Menu
Accessible via:
- Toolbar button (dropdown)
- Keyboard shortcut (Cmd+J or Cmd+G for "go to")
- Right-click canvas → "Jump to..."
Shows list:
┌─────────────────────────┐
│ Jump to Frame │
├─────────────────────────┤
│ 1. Login Flow │
│ 2. Data Fetching │
│ 3. Authentication │
│ 4. Navigation Logic │
└─────────────────────────┘
Data Requirements
No new data model needed - reads from existing CommentsModel, filtering for Smart Frames (comments with containedNodeIds.length > 0).
Implementation Approach
- Minimap component: React component that subscribes to CommentsModel and NodeGraphEditor pan/scale
- Coordinate transformation: Convert canvas coordinates to minimap coordinates
- Frame detection: Filter comments to only show Smart Frames (have contained nodes)
- Click handling: Transform minimap click to canvas coordinates, animate pan
- Jump menu: Simple dropdown populated from Smart Frames list
Files to Create
packages/noodl-editor/src/editor/src/views/CanvasNavigation/
├── CanvasNavigation.tsx # Main container component
├── CanvasNavigation.module.scss
├── Minimap.tsx # Minimap rendering
├── Minimap.module.scss
├── JumpMenu.tsx # Frame list dropdown
└── index.ts
Files to Modify
packages/noodl-editor/src/editor/src/views/documents/EditorDocument/EditorDocument.tsx
- Add CanvasNavigation component to editor layout
- Pass nodeGraph and commentsModel refs
packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts
- Expose pan/scale state for minimap subscription
- Add method: animatePanTo(x, y)
packages/noodl-editor/src/editor/src/utils/editorsettings.ts
- Add setting: minimapVisible (boolean, default false)
Success Criteria
- Minimap toggle button in canvas toolbar
- Minimap shows frame positions as colored dots/rectangles
- Minimap shows current viewport as rectangle
- Clicking minimap pans canvas to that location
- Jump menu lists all Smart Frames
- Selecting from jump menu pans to that frame
- Keyboard shortcuts (Cmd+1..9) jump to frames
- Minimap visibility persists in editor settings
Feature 3: Vertical Snap + Push
Description
A system for vertically aligning and attaching nodes so that when one expands, nodes below it push down automatically, maintaining spacing.
Core Concept
Nodes can be vertically attached - think of it like a vertical stack. When the top node grows, everything below shifts down to maintain spacing.
Before expansion: After expansion:
┌────────────┐ ┌────────────┐
│ Node A │ │ Node A │
└────────────┘ │ (grew) │
│ │ │
│ attached └────────────┘
▼ │
┌────────────┐ │ attached
│ Node B │ ▼
└────────────┘ ┌────────────┐
│ │ Node B │ ← pushed down
│ attached └────────────┘
▼ │
┌────────────┐ │ attached
│ Node C │ ▼
└────────────┘ ┌────────────┐
│ Node C │ ← pushed down
└────────────┘
Attachment Mechanics
Creating attachments (proximity-based):
When dragging a node near another node's top or bottom edge:
- Visual indicator: Edge lights up (glow or highlight)
- On drop: If within threshold, attachment is created
- Attaching between existing attached nodes: New node slots into the chain
Dragging Node X near Node A's bottom:
┌────────────┐
│ Node A │
└────────────┘ ← bottom edge glows
[Node X] ← being dragged
┌────────────┐
│ Node B │
└────────────┘
Inserting between attached nodes:
If Node A → Node B are attached, and user drags Node X to the attachment point:
- Node X becomes: A → X → B
- All three remain attached
Breaking attachments:
- Context menu on node → "Detach from stack"
- Removes node from chain, remaining nodes close the gap
- Alternative: Drag node far enough away auto-detaches
Alignment Guides (Supporting Feature)
Even without attachment, show alignment guides when dragging:
- Horizontal line appears when node edge aligns with another node's edge
- Helps manual alignment
- Standard behavior in design tools (Figma, Sketch)
Data Model
Node attachments stored in NodeGraphModel or as a separate model:
interface VerticalAttachment {
topNodeId: string;
bottomNodeId: string;
spacing: number; // Gap between nodes
}
// Or simpler - store on node itself:
interface NodeGraphNode {
// ... existing fields
attachedAbove?: string; // ID of node this is attached below
attachedBelow?: string; // ID of node attached below this
}
Implementation Approach
- Drag feedback: During drag, check proximity to other node edges; show glow on nearby edges
- Drop handling: On drop, check if within attachment threshold; create attachment
- Insert detection: When dropping between attached nodes, insert into chain
- Push system: Subscribe to node size changes; when node grows, recalculate attached node positions
- Detachment: Context menu action; remove from chain and recalculate remaining chain positions
- Alignment guides: During drag, find aligned edges and render guide lines
Files to Modify
packages/noodl-editor/src/editor/src/models/nodegraphmodel.ts
- Add attachment storage (or create separate AttachmentsModel)
- Methods: createAttachment(), removeAttachment(), getAttachmentChain()
packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts
- Drag feedback: detect edge proximity, render glow
- Drop handling: create attachments
- Size change subscription: trigger push recalculation
- Paint alignment guides during drag
packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts
- Add visual state for edge highlight (top/bottom edge glowing)
- Expose edge positions for proximity detection
packages/noodl-editor/src/editor/src/views/NodePicker/NodePicker.utils.ts
- Update createNodeFunction to not auto-attach on creation
Files to Create
packages/noodl-editor/src/editor/src/models/attachmentsmodel.ts
- Manages vertical attachment relationships
- Methods for creating, breaking, querying attachments
- Push calculation logic
packages/noodl-editor/src/editor/src/views/nodegrapheditor/AlignmentGuides.ts
- Logic for detecting aligned edges
- Guide line rendering
Success Criteria
- Dragging node near another's top/bottom edge shows visual indicator
- Dropping on highlighted edge creates attachment
- Moving top node moves all attached nodes below
- Expanding node pushes attached nodes down
- Dropping between attached nodes inserts into chain
- Context menu "Detach from stack" removes node from chain
- Remaining chain nodes close gap after detachment
- Alignment guides appear when edges align (even without attachment)
Feature 4: Connection Labels
Description
Allow users to add text labels to connection lines to document data flow. Labels sit on the bezier curve and can be repositioned along the path.
Interaction Design
Adding a label:
- Hover over a connection line
- Small icon appears (similar to existing X delete icon)
- Click icon → inline text input appears on the connection
- Type label, press Enter or click away to confirm
Repositioning:
- Click and drag existing label along the connection path
- Label stays anchored to the bezier curve
Removing:
- Click label → small X button appears → click to delete
- Or: clear text and confirm
Visual Design
┌─────────┐
┌────────┐ │ user ID │
│ Source │─────────┴─────────┴──────────►│ Target │
└────────┘ └────────┘
▲
Connection label
(positioned on curve)
Label styling:
- Small text (10-11px)
- Subtle background matching connection color (with transparency)
- Rounded corners
- Positioned centered on curve at specified t-value (0-1 along bezier)
Data Model
Extend Connection model:
interface Connection {
// Existing fields
fromId: string;
fromProperty: string;
toId: string;
toProperty: string;
// New field
label?: {
text: string;
position: number; // 0-1 along bezier curve, default 0.5
};
}
Implementation Approach
- Hover detection: Use existing connection hit-testing; on hover, show add-label icon
- Icon positioning: Calculate midpoint of bezier curve for icon placement
- Add label UI: On icon click, render inline input at curve position
- Label rendering: Render labels as part of connection paint cycle
- Bezier math: Calculate point on curve at t-value for label positioning
- Drag repositioning: On label drag, calculate nearest t-value to mouse position
Bezier Curve Math
For a cubic bezier with control points P0, P1, P2, P3:
B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
Need functions:
getPointOnCurve(t): Returns {x, y} at position tgetNearestT(point): Returns t value for nearest point on curve to given point
Files to Modify
packages/noodl-editor/src/editor/src/models/nodegraphmodel.ts
- Extend Connection interface with label field
- Methods: setConnectionLabel(), removeConnectionLabel()
packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorConnection.ts
- Add label rendering in paint()
- Add hover state for showing add-label icon
- Handle label drag for repositioning
packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts
- Handle click on add-label icon
- Render inline input for label editing
- Handle label click for editing/deletion
Files to Create
packages/noodl-editor/src/editor/src/views/nodegrapheditor/ConnectionLabel.ts
- Label rendering logic
- Position calculation
- Edit mode handling
packages/noodl-editor/src/editor/src/utils/bezier.ts
- getPointOnCubicBezier(t, p0, p1, p2, p3): Point
- getNearestTOnCubicBezier(point, p0, p1, p2, p3): number
- getCubicBezierLength(p0, p1, p2, p3): number (for spacing)
Success Criteria
- Hovering connection shows add-label icon at midpoint
- Clicking icon opens inline text input
- Typing and confirming creates label on connection
- Label renders on the bezier curve path
- Label can be dragged along the curve
- Clicking label allows editing text
- Label can be deleted (clear text or X button)
- Labels persist when project is saved/loaded
Implementation Order
Phase 1: Smart Frames (16-24 hours)
Foundation for navigation; highest impact feature.
Sessions:
- Data model extension + basic containment logic
- Drag-in/drag-out behavior
- Group movement on frame drag
- Auto-resize on node changes
- Collapse UI and basic collapsed state
- Collapsed connection dots rendering
- Testing and edge cases
Phase 2: Canvas Navigation (8-12 hours)
Depends on Smart Frames for anchor points.
Sessions:
- Minimap component structure
- Coordinate transformation and frame rendering
- Click-to-navigate and viewport indicator
- Jump menu dropdown
- Keyboard shortcuts
- Settings persistence
Phase 3: Vertical Snap + Push (12-16 hours)
Independent; can be done in parallel after Phase 1 starts.
Sessions:
- Attachment data model
- Edge proximity detection and visual feedback
- Attachment creation on drop
- Push calculation on node resize
- Insert-between-attached logic
- Detachment via context menu
- Alignment guides (bonus)
Phase 4: Connection Labels (10-14 hours)
Most technically isolated; can be done anytime.
Sessions:
- Bezier utility functions
- Connection hover state and add-icon
- Inline label input
- Label rendering on curve
- Label drag repositioning
- Edit and delete functionality
Testing Checklist
Smart Frames
- Load legacy project with comments → comments work unchanged
- Drag node into empty comment → comment becomes Smart Frame
- Drag all nodes out → Smart Frame reverts to comment
- Move Smart Frame → contained nodes move
- Resize contained node → frame auto-resizes
- Collapse frame → only title visible, connections as dots
- Expand frame → contents visible again
- Create connection to collapsed frame node → dot visible
- Delete frame → contained nodes remain (orphaned)
- Undo/redo all operations
Canvas Navigation
- Toggle minimap visibility
- Minimap shows all Smart Frames
- Minimap shows viewport rectangle
- Click minimap → canvas pans
- Open jump menu → lists Smart Frames
- Select from jump menu → canvas pans to frame
- Keyboard shortcut → jumps to frame
- Close and reopen editor → minimap setting persists
Vertical Snap + Push
- Drag node near another's bottom edge → edge highlights
- Drop on highlighted edge → attachment created
- Move top node → attached nodes move
- Resize top node → attached nodes push down
- Drag node to attachment point between two attached → inserts
- Context menu detach → node removed, others close gap
- Alignment guides show when edges align
Connection Labels
- Hover connection → icon appears
- Click icon → input appears
- Type and confirm → label shows on curve
- Drag label → moves along curve
- Click label → can edit
- Clear text or delete → label removed
- Save and reload → labels persist
Risk Assessment
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Smart Frame collapse complex | Medium | High | Start with simple collapse (hide contents), add connection dots later |
| Bezier math for labels | Low | Medium | Well-documented algorithms; can use library if needed |
| Performance with many frames | Low | Medium | Lazy render off-screen frames; throttle minimap updates |
| Undo/redo complexity | Medium | High | Leverage existing UndoActionGroup pattern; test thoroughly |
| Backward compatibility breaks | Low | Critical | Extensive testing with legacy projects; containedNodeIds default undefined |
Open Questions
- Frame nesting: Should Smart Frames be nestable? (Recommendation: No, keep simple for v1)
- Frame-to-frame connections: If a collapsed frame has connections to another collapsed frame, how to render? (Recommendation: Just show frame edge dots on both)
- Attachment and frames: If an attached stack is inside a frame, should attachments be frame-local? (Recommendation: Yes, attachments are independent of frames)
- Label character limit: Should labels have max length? (Recommendation: Yes, ~50 chars to prevent visual clutter)
Success Metrics
Post-implementation, measure:
- Adoption rate: % of projects using Smart Frames after 30 days
- Navigation usage: How often minimap/jump menu is used per session
- Canvas cleanup: User feedback on organization improvements
- Performance: Frame rates with 50+ nodes and multiple Smart Frames
References
Existing Code
packages/noodl-editor/src/editor/src/views/CommentLayer/- Comment systempackages/noodl-editor/src/editor/src/views/nodegrapheditor.ts- Main canvaspackages/noodl-editor/src/editor/src/models/commentsmodel.ts- Comment data modelpackages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorConnection.ts- Connection rendering
Design Inspiration
- Figma: Frame containment, alignment guides, minimap
- Miro: Frames and navigation
- Unreal Blueprints: Comment boxes, reroute nodes
- TouchDesigner: Collapsed containers