deargui-vpl/docs/groups.md

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

  1. Container Base Class: Base class for all containers (root and groups)
  2. Root Container: One per file/graph, contains top-level nodes and groups
  3. BehaviorGraph Container: Groups that inherit from Container + ParameterizedBlock
  4. Single Ownership: Each node/link exists in exactly ONE container
  5. 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 BehaviorGraph class inheriting from Container and ParameterizedBlock
  • Add GroupDisplayMode enum
  • 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 FlowPinRenderer and ParameterPinRenderer for 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 BehaviorGraph container
  • 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
  • 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

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 pin
    • m_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
  • 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_GroupToInnerPins mapping to find target inner pins

    Step 2: Flow activation

    • When group flow input is activated (via IsInputActive()):
      • Find inner node connected via m_GroupToInnerPins mapping
      • Activate the corresponding inner node's flow input (by index)
      • Multiple activated inputs propagate independently

    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*) from Run(Node&, App*) to execute inner blocks

Execution Flow:

  1. Iteration N: Group input activated → Run() copies params, activates inner inputs → returns
  2. Iteration N+1: Runtime detects inner nodes have activated inputs → executes inner blocks
  3. 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 call m_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 their Run() recursively (only if active)
  • 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:
    1. Load nodes first (into temporary storage)
    2. Load links (reference nodes by ID)
    3. 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
    4. 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
  • 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
  • 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

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
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

  1. Create BehaviorGraph container (containers/behavior_graph.h/cpp)

    • Inherit from Container and ParameterizedBlock
    • Add GroupDisplayMode
    • Implement Build(), Render(), Run() stubs
  2. Implement variable pin management (behavior_graph.cpp)

    • AddInputFlowPin, AddOutputFlowPin
    • AddInputParamPin, AddOutputParamPin
    • RemovePin
    • Pin editing (name, type, value)
  3. Implement context menu for pins (behavior_graph.cpp)

    • Add/remove pins via menu
    • Similar to parameter_node.h patterns

Phase 3: Display Modes

  1. Implement expanded mode rendering (behavior_graph.cpp)

    • Render with ed::Group()
    • Render pins and inner nodes
  2. Implement collapsed mode rendering (behavior_graph.cpp)

    • Compact node view with pins only
  3. Implement mode toggle (behavior_graph.cpp)

    • Context menu and keyboard shortcut
  4. 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

  1. 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

  1. Implement virtual pin connections (behavior_graph.cpp)

    • Store mappings (group pin → inner pin)
    • Link validation (prevent inner ↔ external)
    • Pin mapping UI
  2. Implement Run() method (behavior_graph.cpp)

    • Parameter propagation
    • Flow activation
    • Output collection
  3. Update runtime system (app-runtime.cpp, root_container.cpp)

    • Container-based execution
    • Recursive container running

Phase 6: Serialization

  1. Implement container serialization (container.cpp, behavior_graph.cpp)
    • Save container hierarchy
    • Load in correct order (nodes → links → containers)

Phase 7: Polish

  1. Handle edge cases (behavior_graph.cpp, app-logic.cpp)

    • Node deletion
    • Group deletion
    • Link deletion
    • Root container deletion
  2. 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.cpp for execution patterns
  • Reference parameter_node.h for pin editing patterns
  • Reference ref/sample.cpp for group rendering
  • Reference block.cpp for pin rendering patterns

Migration Strategy

  1. Start with container foundation - Move existing code to use root container
  2. Then add BehaviorGraph - Groups as nested containers
  3. Incremental migration - Can maintain flattened view temporarily for compatibility

This architecture provides clean separation, no duplication, and natural multi-file support!