Skip to content

Commit d2c8682

Browse files
committed
Introduced Export Dot File for GraphViz
1 parent 8c84969 commit d2c8682

File tree

6 files changed

+201
-9
lines changed

6 files changed

+201
-9
lines changed

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.16)
2-
project(CXXStateTree VERSION 0.3.0 LANGUAGES CXX)
2+
project(CXXStateTree VERSION 0.4.0 LANGUAGES CXX)
33

44
set(CMAKE_CXX_STANDARD 20)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -14,6 +14,11 @@ target_link_libraries(basic PRIVATE CXXStateTree)
1414
add_executable(nested examples/nested.cpp)
1515
target_link_libraries(nested PRIVATE CXXStateTree)
1616

17+
add_executable(export_dot_example examples/export_dot.cpp)
18+
target_include_directories(export_dot_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
19+
add_executable(export_dot_nested_example examples/export_dot_nested.cpp)
20+
target_include_directories(export_dot_nested_example PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
21+
1722
# GoogleTest setup
1823
include(FetchContent)
1924
FetchContent_Declare(

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ MPL2.0 License — see [LICENSE](LICENSE) for details.
9595
* DOT/Graphviz state diagram export
9696
* Transitions with context/parameters
9797

98-
| Milestone | Features |
99-
| --------- | ------------------------------------------------------------------ |
100-
| v0.1.0 | Basic state machine with `send()`, transitions, and state tracking |
101-
| v0.2.0 | Guards and actions |
102-
| v0.3.0 | Nested (hierarchical) states |
103-
| v0.4.0 | Graphviz export |
104-
| v0.5.0 | Coroutine/async support (optional) |
105-
| v1.0.0 | Full unit test coverage, benchmarks, and docs |
98+
| Completed | Milestone | Features |
99+
| :-: | :--------- | ------------------------------------------------------------------ |
100+
| :heavy_check_mark: | v0.1.0 | Basic state machine with `send()`, transitions, and state tracking |
101+
| :heavy_check_mark: | v0.2.0 | Guards and actions |
102+
| :heavy_check_mark: | v0.3.0 | Nested (hierarchical) states |
103+
| :heavy_check_mark: | v0.4.0 | Graphviz export |
104+
| :memo: | v0.5.0 | Coroutine/async support (optional) |
105+
| :memo: | v1.0.0 | Full unit test coverage, benchmarks, and docs |
106106

107107
---
108108

examples/export_dot.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#include "CXXStateTree/Builder.hpp"
2+
#include "CXXStateTree/StateTree.hpp"
3+
#include <iostream>
4+
#include <fstream>
5+
6+
using namespace CXXStateTree;
7+
8+
int main()
9+
{
10+
auto tree = Builder()
11+
.initial("App")
12+
.state("App", [](State &app)
13+
{ app.initial_substate("Idle")
14+
.substate("Idle", [](State &idle)
15+
{ idle.on("start", "Running"); })
16+
.substate("Running", [](State &running)
17+
{ running.on("stop", "Idle"); }); })
18+
.build();
19+
20+
// Export to DOT format and print
21+
std::string dot = tree.export_dot();
22+
std::cout << dot;
23+
24+
// Optionally write to file
25+
std::ofstream("example_state_tree.dot") << dot;
26+
27+
return 0;
28+
}

examples/export_dot_nested.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include "CXXStateTree/Builder.hpp"
2+
#include "CXXStateTree/StateTree.hpp"
3+
#include <iostream>
4+
#include <fstream>
5+
6+
using namespace CXXStateTree;
7+
8+
int main()
9+
{
10+
auto tree = Builder()
11+
.initial("Main")
12+
.state("Main", [](State &s)
13+
{ s.initial_substate("Idle")
14+
.substate("Idle", [](State &s)
15+
{ s.on("Start", "Running", nullptr, []()
16+
{ std::cout << "Transition: Idle -> Running" << std::endl; }); })
17+
18+
.substate("Running", [](State &s)
19+
{ s.on("Stop", "Idle", nullptr, []()
20+
{ std::cout << "Transition: Running -> idle" << std::endl; }); })
21+
22+
.on("Switch", "Alternate", nullptr, []()
23+
{ std::cout << "Transition: Main -> Alternate" << std::endl; }); })
24+
.state("Alternate", [](State &s)
25+
{ s.initial_substate("Idle")
26+
.substate("Idle", [](State &s)
27+
{ s.on("Start", "Running", nullptr, []()
28+
{ std::cout << "Transition: Idle -> Running" << std::endl; }); })
29+
30+
.substate("Running", [](State &s)
31+
{ s.on("Stop", "Idle", nullptr, []()
32+
{ std::cout << "Transition: Running -> idle" << std::endl; }); })
33+
.on("Switch", "Main", nullptr, []()
34+
{ std::cout << "Transition: Alternate -> Main" << std::endl; }); })
35+
.build();
36+
37+
// Export to DOT format and print
38+
std::string dot = tree.export_dot();
39+
std::cout << dot;
40+
41+
// Optionally write to file
42+
std::ofstream("example_state_tree_nested.dot") << dot;
43+
44+
return 0;
45+
}

include/CXXStateTree/State.hpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ namespace CXXStateTree
4545
return name_;
4646
}
4747
}
48+
49+
std::string baseName() const
50+
{
51+
if (parent_ != nullptr)
52+
{
53+
std::string result = parent_->baseName() + "." + name_;
54+
return result;
55+
}
56+
else
57+
{
58+
return "";
59+
}
60+
}
4861
const std::list<State> &substates() const { return substates_; }
4962
const std::unordered_map<std::string, Transition> &transitions() const { return transitions_; }
5063
const std::optional<std::string> &initial_substate() const { return initial_substate_; }
@@ -64,6 +77,45 @@ namespace CXXStateTree
6477
return nullptr;
6578
}
6679

80+
void collect_transitions(std::vector<std::tuple<std::string, std::string, std::string>> &all_transitions, const std::string &full_name, const std::string &base_name) const
81+
{
82+
for (const auto &[event, t] : transitions_)
83+
{
84+
all_transitions.emplace_back(full_name, base_name != "" ? base_name + "." + t.target : t.target, event);
85+
}
86+
for (const auto &sub : substates_)
87+
{
88+
std::string sub_full_name = full_name + "." + sub.name();
89+
std::string sub_base_name = full_name;
90+
;
91+
sub.collect_transitions(all_transitions, sub_full_name, sub_base_name);
92+
}
93+
}
94+
95+
void collect_states(std::ostream &os, const std::string &prefix = "") const
96+
{
97+
std::string full_name = prefix.empty() ? name_ : prefix + "." + name_;
98+
99+
if (!substates_.empty())
100+
{
101+
os << "\tsubgraph cluster_" << full_name << " {\n";
102+
os << "\t\tlabel = \"" << full_name << "\";\n";
103+
for (const auto &sub : substates_)
104+
{
105+
sub.collect_states(os, full_name);
106+
}
107+
// Add virtual entry/exit nodes for clusters
108+
os << "\t\"" << full_name << "_entry\" [label=\"\", shape=point, style=invis];\n";
109+
os << "\t\"" << full_name << "_exit\" [label=\"\", shape=point, style=invis];\n";
110+
111+
os << "\t}\n";
112+
}
113+
else
114+
{
115+
os << "\t\"" << full_name << "\";\n";
116+
}
117+
}
118+
67119
private:
68120
std::string name_;
69121
State *parent_ = nullptr;

include/CXXStateTree/StateTree.hpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <functional>
77
#include <optional>
88
#include <stdexcept>
9+
#include <sstream>
10+
#include <set>
911
#include "State.hpp"
1012

1113
namespace CXXStateTree
@@ -86,6 +88,66 @@ namespace CXXStateTree
8688
return *current_;
8789
}
8890

91+
std::string export_dot() const
92+
{
93+
std::ostringstream os;
94+
os << "digraph StateTree {\n";
95+
os << "\trankdir=LR;\n";
96+
os << "\tnode [shape=box];\n";
97+
98+
// Collect cluster structure
99+
for (const auto &s : states_)
100+
{
101+
s.collect_states(os);
102+
}
103+
104+
std::vector<std::tuple<std::string, std::string, std::string>> transitions;
105+
for (const auto &s : states_)
106+
{
107+
std::string name = s.name();
108+
s.collect_transitions(transitions, name, s.baseName());
109+
}
110+
111+
std::set<std::string> cluster_roots;
112+
for (const auto &s : states_)
113+
{
114+
if (!s.substates().empty())
115+
{
116+
cluster_roots.insert(s.name());
117+
}
118+
}
119+
120+
for (const auto &[from, to, event] : transitions)
121+
{
122+
bool from_is_cluster = cluster_roots.count(from);
123+
bool to_is_cluster = cluster_roots.count(to);
124+
125+
std::string from_node = from_is_cluster ? from + "_exit" : "\"" + from + "\"";
126+
std::string to_node = to_is_cluster ? to + "_entry" : "\"" + to + "\"";
127+
128+
os << "\t" << from_node << " -> " << to_node;
129+
130+
if (from_is_cluster || to_is_cluster)
131+
{
132+
os << " [label=\"" << event << "\"";
133+
if (from_is_cluster)
134+
os << ", ltail=cluster_" << from;
135+
if (to_is_cluster)
136+
os << ", lhead=cluster_" << to;
137+
os << "]";
138+
}
139+
else
140+
{
141+
os << " [label=\"" << event << "\"]";
142+
}
143+
144+
os << ";\n";
145+
}
146+
147+
os << "}\n";
148+
return os.str();
149+
}
150+
89151
private:
90152
std::list<State> states_;
91153
const State *current_ = nullptr;

0 commit comments

Comments
 (0)