23 KiB
ImGui Node Editor - Architecture Overview
Table of Contents
- Introduction
- Core Architecture
- Editor Context
- Rendering System
- Pin Rendering
- Block System
- Link System
- Helper Functions
Introduction
The imgui-node-editor is a visual node graph editor built on top of Dear ImGui. This document describes the current architecture after recent refactoring that focused on modularity and maintainability.
Key Characteristics
- Immediate Mode: No retained scene graph - everything rebuilt each frame
- Type-Safe IDs:
NodeId,PinId,LinkIdprevent mixing different entity types - Modular Design: Functionality split into focused files (_render, _links, _store, _selection, _tools, _animation)
- Precise Pin Positioning: Pin renderers track exact pivot positions and bounds for accurate link connections
Core Architecture
File Structure
The library is organized into specialized modules:
imgui-node-editor/
├── imgui_node_editor.h # Public API
├── imgui_node_editor_internal.h # Internal structures and declarations
├── imgui_node_editor.cpp # Core editor context implementation
├── imgui_node_editor_render.cpp # Rendering logic
├── imgui_node_editor_links.cpp # Link interaction and control points
├── imgui_node_editor_store.cpp # Object storage and queries
├── imgui_node_editor_selection.cpp # Selection management
├── imgui_node_editor_tools.cpp # Utility functions
├── imgui_node_editor_animation.cpp # Animation controllers
└── imgui_node_editor_api.cpp # Public API implementation
Namespace Structure
namespace ax::NodeEditor::Detail {
// Internal implementation
class EditorContext { /* ... */ };
// Common alias used throughout
namespace ed = ax::NodeEditor::Detail;
}
Editor Context
EditorContext Class
The heart of the editor is EditorContext (defined in imgui_node_editor_internal.h), which manages the entire editor state.
Key Responsibilities:
- Canvas navigation (pan, zoom)
- Object management (nodes, pins, links)
- User interactions (create, delete, select)
- Drawing coordination
- Animation playback
The End() Method - Main Frame Coordinator
void ed::EditorContext::End() in imgui_node_editor_render.cpp is the main orchestrator called at the end of each frame. It coordinates all rendering and interaction logic.
void ed::EditorContext::End()
{
auto& io = ImGui::GetIO();
const auto control = BuildControl(IsFocused());
// 1. Update hover/interaction state
UpdateControlState(control);
// 2. Handle control point dragging for guided links
HandleControlPointDragging();
// 3. Draw all visual elements
DrawNodes(); // Node visuals
DrawLinks(); // Link curves
DrawSelectionHighlights(control); // Selected objects
DrawHoverHighlight(control, IsSelecting); // Hovered objects
DrawAnimations(); // Flow animations
// 4. Process user actions
ProcessCurrentAction(control); // Active action (dragging, etc.)
SelectNextAction(control); // Determine next action
// 5. Manage Z-order and channels
BringActiveNodeToFront(control, isDragging);
SortNodesByGroupAndZOrder(sortGroups);
ArrangeNodeChannels();
// 6. Finalize rendering
DrawGrid(); // Background grid
FinalizeDrawChannels(); // Transform clip rects
MergeChannelsAndFinishCanvas(); // Merge and draw border
// 7. Cleanup
PostFrameCleanup(); // Reset state, save settings
}
Location: imgui_node_editor_render.cpp:394-445
Extracted Helper Methods
The End() method was refactored from ~620 lines into focused helper methods:
Rendering Helpers (imgui_node_editor_render.cpp)
UpdateControlState()- Updates hover and double-click statesDrawNodes()- Renders all visible nodesDrawLinks()- Renders all visible linksDrawSelectionHighlights()- Highlights selected objectsDrawHoverHighlight()- Highlights hovered objectDrawAnimations()- Renders animation effectsDrawGrid()- Draws background gridArrangeNodeChannels()- Manages ImDrawList channels for proper Z-orderFinalizeDrawChannels()- Transforms clip rects for channelsMergeChannelsAndFinishCanvas()- Merges channels and draws borderPostFrameCleanup()- Post-frame cleanup and state reset
Link Interaction Helpers (imgui_node_editor_links.cpp)
HandleControlPointDragging()- Manages dragging of link control pointsHandleGuidedLinkInteractions()- Double-click to add/remove control pointsAddControlPointToGuidedLink()- Adds a waypoint to a guided linkConvertLinkToGuidedMode()- Converts auto link to guided modeShowControlPointHoverCursor()- Sets cursor when hovering control points
Action Helpers (imgui_node_editor_render.cpp)
ProcessCurrentAction()- Processes the currently active actionProcessNavigateAction()- Handles canvas navigationSelectNextAction()- Determines next action based on priority
Z-Order Helpers (imgui_node_editor_render.cpp)
BringActiveNodeToFront()- Brings active node/group to frontSortNodesByGroupAndZOrder()- Sorts nodes for correct rendering order
Rendering System
Drawing Channels
The editor uses ImGui's channel system to control draw order:
// Channel allocation (from imgui_node_editor.cpp)
const int c_BackgroundChannel_SelectionRect = 0; // Selection rectangle
const int c_UserChannel_Content = 1; // User canvas content
const int c_UserChannel_Grid = 2; // Background grid
const int c_UserChannel_HintsBackground = 3; // Hint backgrounds
const int c_UserChannel_Hints = 4; // Hint content
const int c_LinkChannel_Selection = 5; // Selected links
const int c_LinkChannel_Links = 6; // Regular links
const int c_LinkChannel_Flow = 7; // Flow animations
const int c_LinkChannel_NewLink = 8; // Link being created
const int c_NodeStartChannel = 9; // First node channel
// Each node gets 2 channels (background + foreground)
const int c_ChannelsPerNode = 2;
Node Rendering
Nodes are drawn by the application using the builder pattern. The editor provides the infrastructure:
ed::Begin("Node Editor");
// Application draws nodes
for (auto& node : nodes) {
ed::BeginNode(node.ID);
// ... custom rendering ...
ed::EndNode();
}
// Application draws links
for (auto& link : links) {
ed::Link(link.ID, link.StartPin, link.EndPin);
}
ed::End(); // Coordinates everything via EditorContext::End()
Pin Rendering
PinRenderer Architecture
The blueprints example uses a specialized pin rendering system for precise positioning.
Location: examples/blueprints-example/utilities/pin_renderer.h/cpp
Class Hierarchy
// Base class for all pin renderers
class PinRendererBase
{
public:
virtual void BeginPin(ed::PinId id, ed::PinKind kind) = 0;
virtual void EndPin() = 0;
virtual ImVec2 GetPivotPosition() const = 0; // Link connection point
virtual ImRect GetRenderBounds() const = 0; // Hit-test area
virtual ImVec2 GetRelativeOffset() const = 0; // Offset from node origin
protected:
float m_Alpha = 1.0f;
ImVec2 m_LastPivotPosition; // Cached pivot (screen space)
ImRect m_LastRenderBounds; // Cached bounds (screen space)
};
ParameterPinRenderer
Renders data parameter pins (Int, Float, String, etc.) with icons and labels.
class ParameterPinRenderer : public PinRendererBase
{
public:
struct Config {
float iconSize = 24.0f;
float iconInnerScale = 0.75f;
float spacing = 4.0f;
bool showLabels = true;
ImVec2 padding = ImVec2(8, 4);
};
void Render(ed::PinId pinId, ed::PinKind kind, const Pin& pin,
bool isLinked, App* app, const Config* overrideConfig = nullptr);
// Returns exact pivot point where links should connect
ImVec2 GetPivotPosition() const override;
// Returns full visual bounds for hit-testing
ImRect GetRenderBounds() const override;
};
Key Features:
- Renders icon + label for parameter pins
- Automatically positions icon based on pin direction (left/right)
- Tracks exact pivot position for link connection
- Supports alpha blending for inactive pins
- Configurable appearance via
Configstruct
Usage:
ParameterPinRenderer paramRenderer;
// Render input pin
paramRenderer.Render(pin.ID, ed::PinKind::Input, pin, isLinked, app);
// Store position data for link routing
pin.LastPivotPosition = paramRenderer.GetPivotPosition();
pin.LastRenderBounds = paramRenderer.GetRenderBounds();
pin.HasPositionData = true;
FlowPinRenderer
Renders execution flow pins with square markers at node edges.
class FlowPinRenderer : public PinRendererBase
{
public:
struct Config {
float pinSize = 12.0f;
float edgeOffset = 4.0f;
float rounding = 2.0f;
float borderWidth = 2.0f;
ImU32 fillColor = IM_COL32(255, 255, 255, 255);
ImU32 borderColor = IM_COL32(32, 32, 32, 255);
};
void Render(ed::PinId pinId, ed::PinKind kind, const Pin& pin,
App* app, const Config* overrideConfig = nullptr);
};
Key Features:
- Renders small square at node edge
- Positioned precisely at block boundary
- Distinct visual from parameter pins
- Minimal screen space usage
Pin Position Tracking
The Pin struct stores rendered position data:
struct Pin
{
ed::PinId ID;
Node* Node;
std::string Name;
PinType Type;
PinKind Kind;
// Position tracking (updated during rendering)
ImVec2 LastPivotPosition; // Where links connect (screen space)
ImRect LastRenderBounds; // Full pin area for hit testing
bool HasPositionData; // True after first render
// Get position relative to node
ImVec2 GetRelativePivotPosition() const;
};
Why This Matters: Accurate pin positions are critical for:
- Link waypoint generation
- Link end-point alignment
- Hit-testing during link creation
- Visual polish (no gaps or misalignments)
Block System
ParameterizedBlock
Represents a standard blueprint-style block with parameters and flow pins.
Location: examples/blueprints-example/blocks/block.cpp
Structure:
class ParameterizedBlock : public BlockBase
{
std::vector<Pin> InputParameters; // Data inputs (top area)
std::vector<Pin> OutputParameters; // Data outputs (top area)
Pin FlowInputPin; // Execution input (left edge)
Pin FlowOutputPin; // Execution output (right edge)
};
Rendering Pattern (lines 25-220):
void ParameterizedBlock::Render()
{
ed::BeginNode(ID);
ImGui::BeginVertical("node");
// Header
ImGui::BeginHorizontal("header");
// ... render title ...
ImGui::EndHorizontal();
// Input Parameters (left side)
ImGui::BeginHorizontal("inputs");
ImGui::BeginVertical("input_params");
for (auto& pin : InputParameters) {
ParameterPinRenderer renderer;
renderer.Render(pin.ID, ed::PinKind::Input, pin, isLinked, app);
// Store position for link routing
pin.LastPivotPosition = renderer.GetPivotPosition();
pin.LastRenderBounds = renderer.GetRenderBounds();
pin.HasPositionData = true;
}
ImGui::EndVertical();
// Middle spacer
ImGui::Spring(1);
// Output Parameters (right side)
ImGui::BeginVertical("output_params");
for (auto& pin : OutputParameters) {
ParameterPinRenderer renderer;
renderer.Render(pin.ID, ed::PinKind::Output, pin, isLinked, app);
pin.LastPivotPosition = renderer.GetPivotPosition();
pin.LastRenderBounds = renderer.GetRenderBounds();
pin.HasPositionData = true;
}
ImGui::EndVertical();
ImGui::EndHorizontal();
// Flow Pins (edges)
FlowPinRenderer flowRenderer;
// Flow input (left edge)
flowRenderer.Render(FlowInputPin.ID, ed::PinKind::Input, FlowInputPin, app);
FlowInputPin.LastPivotPosition = flowRenderer.GetPivotPosition();
FlowInputPin.HasPositionData = true;
// Flow output (right edge)
flowRenderer.Render(FlowOutputPin.ID, ed::PinKind::Output, FlowOutputPin, app);
FlowOutputPin.LastPivotPosition = flowRenderer.GetPivotPosition();
FlowOutputPin.HasPositionData = true;
ImGui::EndVertical();
ed::EndNode();
}
ParameterNode
Compact nodes representing variables or constants (no flow pins).
Location: examples/blueprints-example/blocks/parameter_node.cpp
Three Display Modes:
- Name Only (lines 96-154) - Just the variable name
- Name + Value (lines 156-252) - Name with editable value
- Small Box (lines 254-348) - Minimal representation
Rendering Example (Name Only mode):
void ParameterNode::RenderNameOnly()
{
ed::BeginNode(ID);
// Just the name and output pin
ImGui::BeginHorizontal("content");
ImGui::TextUnformatted(Name.c_str());
ImGui::Spring(0);
// Output pin
ParameterPinRenderer paramRenderer;
paramRenderer.Render(output.ID, ed::PinKind::Output, output, isLinked, app);
// Store position
output.LastPivotPosition = paramRenderer.GetPivotPosition();
output.LastRenderBounds = paramRenderer.GetRenderBounds();
output.HasPositionData = true;
ImGui::EndHorizontal();
ed::EndNode();
}
Link System
Link Structure
struct Link
{
ed::LinkId ID;
ed::PinId StartPinID; // Output pin
ed::PinId EndPinID; // Input pin
ImColor Color;
// Guided link waypoints
std::vector<ImVec2> ControlPoints; // User-defined waypoints
bool IsGuided = false; // Guided vs auto-routed
};
Link Modes
Auto Mode (default):
- Editor automatically routes link curve
- Uses bezier or spline curves
- No user-defined waypoints
Guided Mode:
- User controls link path via waypoints
- Double-click link to add control point
- Drag control points to adjust path
- Double-click control point to remove
Link Rendering
Links are drawn in DrawLinks() (imgui_node_editor_render.cpp:129-170):
void EditorContext::DrawLinks()
{
for (auto& link : m_Links)
{
if (!link.IsVisible())
continue;
if (link.IsGuided()) {
// Draw guided link with control points
ed::Link(link.ID, link.StartPinID, link.EndPinID,
link.Color, 2.0f, link.ControlPoints);
} else {
// Draw auto-routed link
ed::Link(link.ID, link.StartPinID, link.EndPinID,
link.Color, 2.0f);
}
}
}
Control Point Interaction
Dragging (HandleControlPointDragging() in imgui_node_editor_links.cpp:25-50):
void EditorContext::HandleControlPointDragging()
{
if (!m_DraggedControlPoint.IsValid())
return;
// Update position while dragging
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
m_DraggedControlPoint.Position = ImGui::GetMousePos();
}
// Finish drag
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
m_DraggedControlPoint.Clear();
}
}
Adding/Removing (HandleGuidedLinkInteractions() in imgui_node_editor_links.cpp:52-85):
void EditorContext::HandleGuidedLinkInteractions(const Control& control)
{
if (control.HotLink && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
auto link = FindLink(control.HotLink);
if (!link) return;
if (control.HoveredControlPoint >= 0) {
// Remove control point
link->ControlPoints.erase(
link->ControlPoints.begin() + control.HoveredControlPoint);
} else {
// Add control point at mouse position
AddControlPointToGuidedLink(link, ImGui::GetMousePos());
}
}
}
Waypoint Generation
When creating a link, waypoints are generated to connect pins accurately.
Critical Function: GetPinPosition() in app-render.cpp:48-112
static ImVec2 GetPinPosition(Pin* pin, const ImVec2& nodePos, const ImVec2& nodeSize)
{
// Use stored position data from PinRenderer if available
if (pin->HasPositionData)
{
// Get current node position
ImVec2 currentNodePos = ed::GetNodePosition(pin->Node->ID);
// Calculate relative offset
ImVec2 relativeOffset = pin->LastPivotPosition - currentNodePos;
// Apply offset to requested position
// (Handles cases where nodePos != currentNodePos during creation)
return nodePos + relativeOffset;
}
// Fallback to approximation for unrendered pins
// ... (approximation based on node size and pin type) ...
}
Why This Works:
- Pin renderers store exact pivot position during rendering
- Position is stored in screen space
- Convert to relative offset from node origin
- Apply offset to current/target node position
- Result: pixel-perfect link connection
Link Direction
Link direction is determined by pin type and position:
static ImVec2 GetPinDirection(Pin* pin)
{
// Use pin renderer data if available
if (pin->HasPositionData) {
// Calculate from pin position relative to node center
ImVec2 nodePos = ed::GetNodePosition(pin->Node->ID);
ImVec2 nodeSize = ed::GetNodeSize(pin->Node->ID);
ImVec2 nodeCenter = nodePos + nodeSize * 0.5f;
ImVec2 pinPos = pin->LastPivotPosition;
ImVec2 dir = pinPos - nodeCenter;
float len = sqrtf(dir.x * dir.x + dir.y * dir.y);
return len > 0.0f ? ImVec2(dir.x / len, dir.y / len) : ImVec2(1, 0);
}
// Fallback: standard left/right
return pin->Kind == PinKind::Input ? ImVec2(-1, 0) : ImVec2(1, 0);
}
Helper Functions
app-render.cpp Utilities
Location: examples/blueprints-example/app-render.cpp
Key helper functions used throughout the blueprints example:
Pin Queries (lines 48-145)
// Get exact pin position for link routing
static ImVec2 GetPinPosition(Pin* pin, const ImVec2& nodePos, const ImVec2& nodeSize);
// Get pin direction vector for link curves
static ImVec2 GetPinDirection(Pin* pin);
// Check if pin has an active connection
bool IsPinLinked(ed::PinId pinId);
Icon Rendering (lines 147-320)
// Draw pin icon (circle, square, flow, etc.)
void DrawPinIcon(const Pin& pin, bool connected, int alpha, const ImVec2& size);
// Get icon color based on pin type
ImColor GetIconColor(PinType type);
// Icon types
enum class IconType { Flow, Circle, Square, Grid, RoundSquare, Diamond };
Link Rendering (lines 550-890)
// Render a single link with proper curve
void DrawLink(const Link& link);
// Render flow animation on link
void DrawFlowAnimation(const Link& link, float time);
// Get link curve points for rendering
std::vector<ImVec2> GetLinkCurve(const Link& link);
Interaction Helpers (lines 1450-1610)
// Handle link creation
void HandleLinkCreation();
// Handle node/link deletion
void HandleDeletion();
// Handle node selection
void HandleSelection();
// Show context menu
void ShowContextMenu();
Quick Reference
Core Editor Flow
Frame Start
↓
ImGui::NewFrame()
↓
ed::Begin()
↓
App Renders Nodes/Links
↓
ed::End() → EditorContext::End()
├─ UpdateControlState()
├─ HandleControlPointDragging()
├─ DrawNodes()
├─ DrawLinks()
├─ DrawSelectionHighlights()
├─ DrawHoverHighlight()
├─ DrawAnimations()
├─ ProcessCurrentAction()
├─ SelectNextAction()
├─ BringActiveNodeToFront()
├─ SortNodesByGroupAndZOrder()
├─ ArrangeNodeChannels()
├─ DrawGrid()
├─ FinalizeDrawChannels()
├─ MergeChannelsAndFinishCanvas()
└─ PostFrameCleanup()
↓
ImGui::Render()
↓
Frame End
Key Files Reference
| File | Purpose | Key Functions |
|---|---|---|
imgui_node_editor.cpp |
Core editor implementation | Begin(), node/link creation |
imgui_node_editor_render.cpp |
Rendering coordination | End(), DrawNodes(), DrawLinks() |
imgui_node_editor_links.cpp |
Link interactions | HandleControlPointDragging(), waypoint management |
imgui_node_editor_internal.h |
Internal declarations | EditorContext class, all helper declarations |
utilities/pin_renderer.h/cpp |
Pin rendering | ParameterPinRenderer, FlowPinRenderer |
blocks/block.cpp |
Standard blocks | ParameterizedBlock::Render() |
blocks/parameter_node.cpp |
Parameter nodes | ParameterNode::Render() modes |
app-render.cpp |
Rendering helpers | GetPinPosition(), DrawPinIcon() |
Data Flow for Pin Positioning
1. App calls pin renderer
ParameterPinRenderer::Render()
↓
2. Renderer calculates layout
Layout layout = CalculateLayout(...)
↓
3. Renderer stores position
m_LastPivotPosition = layout.pivotPoint
m_LastRenderBounds = layout.combinedRect
↓
4. App stores in Pin struct
pin.LastPivotPosition = renderer.GetPivotPosition()
pin.LastRenderBounds = renderer.GetRenderBounds()
pin.HasPositionData = true
↓
5. Link system uses stored position
GetPinPosition(pin) → returns pin.LastPivotPosition
↓
6. Waypoints generated with exact positions
CreateLink() → GenerateWaypoints() → pixel-perfect alignment
Summary
The ImGui Node Editor architecture is built around:
- EditorContext - Central coordinator managing all state
- Modular Files - Focused modules for rendering, links, selection, etc.
- End() Method - Main frame orchestrator split into focused helpers
- Pin Renderers - Precise positioning system for perfect link alignment
- Block System - Flexible node types (blocks, parameters, comments)
- Link System - Auto and guided modes with waypoint editing
- Helper Functions - Utilities in app-render.cpp for common operations
This design provides a clean separation of concerns while maintaining the immediate-mode paradigm of Dear ImGui.