21 KiB
BehaviorGraph (Groups) Implementation Plan - Container Architecture
Overview
This document outlines the implementation plan for adding BehaviorGraph groups using a container-based architecture. Groups are implemented as nested containers within root containers.
Core Principle: All nodes, links, blocks exist within containers. Containers manage, run, and render their contents. The app supports multiple root containers (one per loaded file), and each root can contain groups (nested containers).
Architecture
Container Hierarchy
App
├── RootContainer1 (loaded from file1.json)
│ ├── Node A (top-level)
│ ├── Node B (top-level)
│ └── BehaviorGraph Container (Group 1)
│ ├── Node C (moved here, owned by Group 1)
│ ├── Node D (moved here, owned by Group 1)
│ └── BehaviorGraph Container (Nested Group)
│ └── Node E (owned by nested group)
│
├── RootContainer2 (loaded from file2.json)
│ └── Node F
│
└── RootContainer3 (loaded from file3.json)
└── BehaviorGraph Container (Group 2)
└── Node G
Key Concepts
- Container Base Class: Base class for all containers (root and groups)
- Root Container: One per file/graph, contains top-level nodes and groups
- BehaviorGraph Container: Groups that inherit from Container + ParameterizedBlock
- Single Ownership: Each node/link exists in exactly ONE container
- No Duplication: Nodes moved between containers, not copied
Data Structures
BehaviorGraph Container
File: examples/blueprints-example/containers/behavior_graph.h
class BehaviorGraph : public Container, public ParameterizedBlock
{
public:
BehaviorGraph(int id, const char* name);
// Container interface
void AddNode(Node* node) override;
void RemoveNode(Node* node) override;
bool CanAddLink(Link* link) const override; // Validate group boundaries
// Block interface (from ParameterizedBlock)
void Build(Node& node, App* app) override;
void Render(Node& node, App* app, Pin* newLinkPin) override;
int Run(Node& node, App* app) override; // Propagate to inner blocks
// Container execution (called from Run(Node&, App*))
void Run(App* app) override; // Only if active, sets Running flag during execution
// Group-specific
enum class GroupDisplayMode { Expanded, Collapsed };
GroupDisplayMode GetDisplayMode() const { return m_DisplayMode; }
void SetDisplayMode(GroupDisplayMode mode) { m_DisplayMode = mode; }
// Pin management
void AddInputFlowPin(App* app);
void AddOutputFlowPin(App* app);
void AddInputParamPin(App* app, PinType type);
void AddOutputParamPin(App* app, PinType type);
void RemovePin(ed::PinId pinId, App* app);
// Pin mapping (virtual connections)
void MapGroupPinToInnerPin(ed::PinId groupPin, ed::PinId innerPin);
void UnmapGroupPin(ed::PinId groupPin);
private:
GroupDisplayMode m_DisplayMode = GroupDisplayMode::Expanded;
ImVec2 m_GroupSize; // For ed::Group() size
};
GroupPin Structure
struct GroupPin
{
ed::PinId ID;
std::string Name;
PinType Type;
PinKind Kind; // Input or Output
bool IsFlow; // true = flow pin, false = parameter pin
};
Implementation Phases
Phase 2: BehaviorGraph Container (Groups)
Task 2.1: Create BehaviorGraph Container
Files: containers/behavior_graph.h, containers/behavior_graph.cpp
- Define
BehaviorGraphclass inheriting fromContainerandParameterizedBlock - Add
GroupDisplayModeenum - Implement basic
Build(),Render(),Run()stubs - Add pin management methods (AddInputFlowPin, etc.)
- Store pins in node's Inputs/Outputs vectors (for ParameterizedBlock compatibility)
Dependencies: Task 1.1, block.h
Task 2.2: Implement Variable Pin System
Files: containers/behavior_graph.cpp
- Implement
AddInputFlowPin(),AddOutputFlowPin() - Implement
AddInputParamPin(),AddOutputParamPin() - Store pins in container's pin vectors AND node's Inputs/Outputs vectors
- Pin editing similar to
parameter_node.h(edit name, type, value) - Update
Build()to register pins with node editor
Dependencies: Task 2.1
Task 2.3: Implement Pin Removal
Files: containers/behavior_graph.cpp
- Implement
RemovePin()method - Handle link cleanup when pin removed
- Remove from both container vectors and node vectors
- Update virtual connection mappings
Dependencies: Task 2.2
Task 2.4: Context Menu for Pin Management
Files: containers/behavior_graph.cpp
- Extend
OnMenu()(from ParameterizedBlock) to show:- "Add Input Flow Pin"
- "Add Output Flow Pin"
- "Add Input Parameter Pin" (with type selection)
- "Add Output Parameter Pin" (with type selection)
- "Remove Pin" submenu (list all pins)
- Handle pin creation/removal from menu
Dependencies: Task 2.2, 2.3
Phase 3: Display Modes
Task 3.1: Implement Expanded Mode Rendering
Files: containers/behavior_graph.cpp
- Render group container with title bar
- Use
ed::Group(m_GroupSize)to define group bounds - Render flow pins on left/right edges (using
FlowPinRenderer) - Render parameter pins on top/bottom edges (using
ParameterPinRenderer) - Render inner nodes (from
m_Nodes) inside group bounds - Allow user interaction inside group
Reference: ref/sample.cpp lines 1397-1421
Dependencies: Task 2.1
Task 3.2: Implement Collapsed Mode Rendering
Files: containers/behavior_graph.cpp
- Render as compact node (similar to block nodes)
- Show group name/title
- Render pins only (no inner nodes visible)
- Use
FlowPinRendererandParameterPinRendererfor compact display - Inner nodes not visible but remain in container (positions preserved)
Dependencies: Task 3.1
Task 3.3: Mode Toggle
Files: containers/behavior_graph.cpp
- Add mode toggle to context menu
- Add keyboard shortcut (optional: 'T' key)
- Store mode in container state
- Persist in serialization
Dependencies: Task 3.1, 3.2
Task 3.4: Implement Container Flags
Files: containers/container.cpp, containers/behavior_graph.cpp
- Implement flag management (hidden, active/disabled, running, error)
- Hidden: Container not rendered (but still exists)
- Active/Disabled: Disabled containers don't execute (skip Run())
- Running: Set during execution, cleared after (visual feedback)
- Error: Set when container encounters error (visual feedback)
- Update Render() to check IsHidden()
- Update Run() to check IsActive() / IsDisabled()
- Visual indicators for running/error states (e.g., red border for error, pulsing for running)
Dependencies: Task 1.1
Phase 4: Group Creation
Task 4.1: Implement 'g' Key Shortcut for Grouping
Files: app-render.cpp, app-logic.cpp
- In
HandleKeyboardShortcuts(), detect 'g' key press - Get selected nodes from active root container
- Calculate bounding box of selected nodes
- Create new
BehaviorGraphcontainer - Move selected nodes from active root container → group container:
- Remove nodes from root container's
m_Nodes - Add nodes to group container's
m_Nodes
- Remove nodes from root container's
- Break external links (links from/to selected nodes to/from external nodes)
- Move internal links (between selected nodes) to group container
- Add group container to root container's
m_Children - Set group size to encompass selected nodes (nodes keep absolute positions)
Dependencies: Task 2.1, 1.3
Task 4.2: Link Breaking Logic
Files: app-logic.cpp
- When creating group, scan all links
- For links connecting selected nodes to external nodes:
- Remove link from root container
- Store for potential group pin creation later
- For links between selected nodes:
- Move link to group container
- Clean up link references
Dependencies: Task 4.1
Phase 5: Pin Connections & Runtime Execution
Task 5.0: Implement Container Flags (if not done in Phase 3)
Files: containers/container.cpp
- Implement flag getters/setters
- Update Render() to respect IsHidden()
- Update Run() to respect IsActive() / IsDisabled()
- Visual feedback for Running and Error states
Dependencies: Task 1.1
Task 5.1: Implement Virtual Pin Connections
Files: containers/behavior_graph.cpp, app-logic.cpp
- Group pins act as proxies - users connect external nodes to group pins
- Virtual connections stored in maps (NOT actual Link objects):
m_GroupToInnerPins: Maps group input pin → inner node input pinm_InnerToGroupPins: Maps inner node output pin → group output pin
- Link validation: Prevent inner nodes from connecting directly to external nodes
- Override/extend
CanCreateLink()in App to reject: inner pin ↔ external pin - Only allow: group pin ↔ external pin, group pin ↔ inner pin (via mapping), inner pin ↔ inner pin
- Override/extend
- Pin mapping UI: When user connects group pin to inner pin (or vice versa), store in mapping
Dependencies: Task 2.2, 4.1
Task 5.2: Implement Run() Method for Flow Execution
Files: containers/behavior_graph.cpp
-
Implement
Run(Node&, App*)method (required for ParameterizedBlock):Step 1: Parameter propagation FIRST
- Copy parameter values from group input pins to connected inner pins
- Use existing
GetInputParamValue*helpers on group pins - Use existing
SetOutputParamValue*helpers (but apply to inner node pins) - Use
m_GroupToInnerPinsmapping to find target inner pins
Step 2: Flow activation
- When group flow input is activated (via
IsInputActive()):- Find inner node connected via
m_GroupToInnerPinsmapping - Activate the corresponding inner node's flow input (by index)
- Multiple activated inputs propagate independently
- Find inner node connected via
Step 3: Output collection
- After inner blocks execute (may be in next iteration), check inner node output states
- Iterate through
m_Nodes(inner nodes) - For each inner node output that's active, find mapped group output pin (via
m_InnerToGroupPins) - Activate corresponding group output pin
-
Call
Run(App*)fromRun(Node&, App*)to execute inner blocks
Execution Flow:
- Iteration N: Group input activated →
Run()copies params, activates inner inputs → returns - Iteration N+1: Runtime detects inner nodes have activated inputs → executes inner blocks
- Iteration N+2: Group
Run()called again → collects inner outputs → activates group outputs
Dependencies: Task 5.1
Task 5.3: Update Runtime to Support Containers
Files: app-runtime.cpp, containers/root_container.cpp
- Update
ExecuteRuntimeStep()to callm_ActiveRootContainer->Run(app)(only if active) - Update
RootContainer::Run()to:- Check
IsActive()/IsDisabled()- skip if disabled - Set
SetRunning(true)at start,SetRunning(false)at end - Iterate through its nodes, find blocks with activated inputs
- Execute blocks
- For BehaviorGraph containers (in
m_Children), call theirRun()recursively (only if active)
- Check
- Container execution propagates through hierarchy naturally
- Respect disabled flag - disabled containers don't execute
Dependencies: Task 5.2
Phase 6: Serialization
Task 6.1: Save Container Data
Files: app-logic.cpp, containers/container.cpp, containers/behavior_graph.cpp
- Extend
SaveGraph()to save active root container:- Container saves its nodes
- Container saves its links
- Container saves its child containers (recursively)
- BehaviorGraph saves pins, virtual mappings, display mode
- Each root container saves to its own file
Dependencies: Task 4.1, 5.1
Task 6.2: Load Container Data
Files: app-logic.cpp, containers/container.cpp, containers/behavior_graph.cpp
- Loading order: Nodes first, then links, then containers
- Extend
LoadGraph()to:- Load nodes first (into temporary storage)
- Load links (reference nodes by ID)
- Load containers (root container first, then nested):
- Create container
- Assign nodes to container (by ID lookup)
- Assign links to container
- Restore pins, mappings, display mode
- Establish virtual connections
Dependencies: Task 6.1
Phase 7: Edge Cases & Polish
Task 7.1: Handle Edge Cases
Files: containers/behavior_graph.cpp, app-logic.cpp
- Group nesting is allowed - groups can contain other groups
- Node deletion when inside group:
- When node is deleted, remove from its container's
m_Nodes - Clean up any virtual pin mappings involving this node
- When node is deleted, remove from its container's
- Group deletion:
- Remove group container from parent's
m_Children - Move inner nodes back to parent container (or delete)
- Break all links connected to group pins
- Clean up virtual pin mappings
- Remove group container from parent's
- Link deletion:
- When link involving group pin is deleted, remove from virtual mappings
- When link between inner nodes is deleted, no special handling needed
- Root container deletion:
- Remove from
m_RootContainers - Delete all nodes/links/containers it owns
- Remove from
Dependencies: All previous tasks
Task 7.2: Visual Polish
Files: containers/behavior_graph.cpp
- Improve styling for collapsed vs expanded modes
- Add hover effects
- Add selection highlighting
- Improve pin layout/spacing
- Handle link rendering across group boundaries
Dependencies: Task 3.1, 3.2
File Structure
examples/blueprints-example/
├── containers/
│ ├── container.h (NEW - base Container class)
│ ├── container.cpp (NEW)
│ ├── root_container.h (NEW)
│ ├── root_container.cpp (NEW)
│ ├── behavior_graph.h (NEW - BehaviorGraph container)
│ └── behavior_graph.cpp (NEW)
├── blocks/
│ └── (existing files...)
└── (existing files - updated)
Key Implementation Details
Node Movement (Not Duplication)
When creating a group:
// Before: Nodes in root
rootContainer->m_Nodes = [node1, node2, node3]
// After grouping node1, node2:
rootContainer->m_Nodes = [node3]
rootContainer->m_Children = [behaviorGraph1]
behaviorGraph1->m_Nodes = [node1, node2] // MOVED, not duplicated
Link Validation
bool App::CanCreateLink(Pin* a, Pin* b)
{
// ... existing validation ...
// Check if pins are in different root containers (reject)
Container* containerA = FindContainerForNode(a->Node->ID);
Container* containerB = FindContainerForNode(b->Node->ID);
if (GetRootContainer(containerA) != GetRootContainer(containerB))
return false; // Cross-root connections not allowed
// Check group boundary violations
if (IsInnerPin(a) && IsExternalPin(b))
return false; // Inner pins can't connect to external pins
if (IsExternalPin(a) && IsInnerPin(b))
return false;
// ... rest of validation ...
}
Container Rendering
void RootContainer::Render(App* app, Pin* newLinkPin)
{
// Only render if not hidden
if (IsHidden())
return;
// Render this container's nodes
for (auto* node : m_Nodes)
{
RenderNode(node, app, newLinkPin);
}
// Render links between nodes in this container
for (auto* link : m_Links)
{
RenderLink(link, app);
}
// Render child containers (groups) - only if not hidden
for (auto* child : m_Children)
{
if (child->IsHidden())
continue;
if (auto* group = dynamic_cast<BehaviorGraph*>(child))
{
if (group->GetDisplayMode() == GroupDisplayMode::Expanded)
{
// Render expanded group with ed::Group()
group->Render(app, newLinkPin);
}
else
{
// Render as collapsed node
RenderCollapsedGroup(group, app, newLinkPin);
}
}
}
}
Container Execution
void RootContainer::Run(App* app)
{
// Only execute if active (not disabled)
if (IsDisabled())
return;
// Set running flag
SetRunning(true);
// Execute blocks in this container
for (auto* node : m_Nodes)
{
if (node->IsBlockBased() && node->BlockInstance)
{
// Check if input activated, execute, etc. (existing logic)
}
}
// Execute child containers (groups) - only if active
for (auto* child : m_Children)
{
if (child->IsDisabled())
continue;
if (auto* group = dynamic_cast<BehaviorGraph*>(child))
{
// Group's Run() is called by runtime system when group input is activated
// But we can also call it here if needed for recursive execution
}
}
// Clear running flag
SetRunning(false);
}
Final TODO List
Phase 2: BehaviorGraph Container
-
Create BehaviorGraph container (
containers/behavior_graph.h/cpp)- Inherit from Container and ParameterizedBlock
- Add GroupDisplayMode
- Implement Build(), Render(), Run() stubs
-
Implement variable pin management (
behavior_graph.cpp)- AddInputFlowPin, AddOutputFlowPin
- AddInputParamPin, AddOutputParamPin
- RemovePin
- Pin editing (name, type, value)
-
Implement context menu for pins (
behavior_graph.cpp)- Add/remove pins via menu
- Similar to parameter_node.h patterns
Phase 3: Display Modes
-
Implement expanded mode rendering (
behavior_graph.cpp)- Render with ed::Group()
- Render pins and inner nodes
-
Implement collapsed mode rendering (
behavior_graph.cpp)- Compact node view with pins only
-
Implement mode toggle (
behavior_graph.cpp)- Context menu and keyboard shortcut
-
Implement container flags (
container.cpp)- Hidden: Container not rendered
- Active/Disabled: Disabled containers don't execute
- Running: Visual feedback during execution
- Error: Visual feedback for errors
- Update Render() and Run() to respect flags
Phase 4: Group Creation
- Implement 'g' key shortcut (
app-render.cpp)- Detect 'g' key press
- Move selected nodes from root → group container
- Break external links
- Move internal links
Phase 5: Pin Connections & Runtime
-
Implement virtual pin connections (
behavior_graph.cpp)- Store mappings (group pin → inner pin)
- Link validation (prevent inner ↔ external)
- Pin mapping UI
-
Implement Run() method (
behavior_graph.cpp)- Parameter propagation
- Flow activation
- Output collection
-
Update runtime system (
app-runtime.cpp,root_container.cpp)- Container-based execution
- Recursive container running
Phase 6: Serialization
- Implement container serialization (
container.cpp,behavior_graph.cpp)- Save container hierarchy
- Load in correct order (nodes → links → containers)
Phase 7: Polish
-
Handle edge cases (
behavior_graph.cpp,app-logic.cpp)- Node deletion
- Group deletion
- Link deletion
- Root container deletion
-
Visual polish (
behavior_graph.cpp)- Visual indicators for flags (running, error, disabled, hidden)
- Styling, hover effects, selection
Notes
- Container-based architecture eliminates duplication - nodes owned by exactly one container
- Multiple root containers - app supports multiple files, each is a root container
- BehaviorGraph is a container - inherits Container + ParameterizedBlock
- No m_ParentGroup field - container owns node, find via
FindContainerForNode() - Group pins act as proxies - virtual connections stored in maps
- Link validation prevents inner pins connecting directly to external pins
- Group nesting allowed - containers within containers
- Serialization per file - each root container saves to its own file
- Reference
app-runtime.cppfor execution patterns - Reference
parameter_node.hfor pin editing patterns - Reference
ref/sample.cppfor group rendering - Reference
block.cppfor pin rendering patterns
Migration Strategy
- Start with container foundation - Move existing code to use root container
- Then add BehaviorGraph - Groups as nested containers
- Incremental migration - Can maintain flattened view temporarily for compatibility
This architecture provides clean separation, no duplication, and natural multi-file support!