///|
/// Options for rendering behavior trees into graph descriptions.
pub(all) struct VisualOptions {
include_node_names : Bool
include_node_kinds : Bool
show_leaf_shape : Bool
} derive(Debug, Eq)
///|
pub fn default_visual_options() -> VisualOptions {
{ include_node_names: true, include_node_kinds: true, show_leaf_shape: true }
}
///|
pub fn compact_visual_options() -> VisualOptions {
{ include_node_names: false, include_node_kinds: false, show_leaf_shape: false }
}
///|
/// Export a behavior tree to Graphviz DOT.
pub fn tree_to_dot(tree : BehaviorTree, options? : VisualOptions) -> String {
let opts = options.unwrap_or(default_visual_options())
let lines = Array::new()
lines.push("digraph MoonBTKit {")
lines.push(" rankdir=TB;")
lines.push(" node [fontname=\"Arial\", fontsize=10];")
let mut i = 0
while i < tree.nodes.length() {
let n = tree.nodes[i]
lines.push(
" \"" +
dot_escape(n.id) +
"\" [label=\"" +
dot_escape(node_visual_label(n, opts)) +
"\", shape=" +
dot_shape(n, opts) +
"];",
)
i = i + 1
}
let mut p = 0
while p < tree.nodes.length() {
let parent = tree.nodes[p]
let mut c = 0
while c < parent.children.length() {
lines.push(
" \"" +
dot_escape(parent.id) +
"\" -> \"" +
dot_escape(parent.children[c]) +
"\";",
)
c = c + 1
}
p = p + 1
}
lines.push("}")
join_strings(lines, "\n")
}
///|
/// Export a behavior tree to Mermaid flowchart syntax.
pub fn tree_to_mermaid(tree : BehaviorTree, options? : VisualOptions) -> String {
let opts = options.unwrap_or(default_visual_options())
let lines = Array::new()
lines.push("flowchart TD")
let mut i = 0
while i < tree.nodes.length() {
let n = tree.nodes[i]
lines.push(
" " +
mermaid_id(n.id) +
mermaid_node_open(n, opts) +
mermaid_escape(node_visual_label(n, opts)) +
mermaid_node_close(n, opts),
)
i = i + 1
}
let mut p = 0
while p < tree.nodes.length() {
let parent = tree.nodes[p]
let mut c = 0
while c < parent.children.length() {
lines.push(
" " +
mermaid_id(parent.id) +
" --> " +
mermaid_id(parent.children[c]),
)
c = c + 1
}
p = p + 1
}
join_strings(lines, "\n")
}
///|
/// Render trace events as a Mermaid sequence diagram.
pub fn trace_to_mermaid_sequence(events : Array[TickEvent]) -> String {
let lines = Array::new()
lines.push("sequenceDiagram")
lines.push(" participant Engine")
let participants = trace_participants(events)
let mut p = 0
while p < participants.length() {
lines.push(" participant " + mermaid_id(participants[p]) + " as " + mermaid_escape(participants[p]))
p = p + 1
}
let mut i = 0
while i < events.length() {
let event = events[i]
lines.push(
" Engine->>" +
mermaid_id(event.node_id) +
": tick " +
event.tick.to_string() +
" " +
event.status.to_text(),
)
i = i + 1
}
join_strings(lines, "\n")
}
///|
/// Return a short visualization bundle suitable for docs and issue comments.
pub fn visualization_bundle(tree : BehaviorTree, events : Array[TickEvent]) -> Array[(String, String)] {
[
("tree.dot", tree_to_dot(tree)),
("tree.mmd", tree_to_mermaid(tree)),
("trace.mmd", trace_to_mermaid_sequence(events)),
]
}
///|
fn node_visual_label(n : BtNode, opts : VisualOptions) -> String {
let mut label = n.id
if opts.include_node_names && n.name != n.id {
label = label + "\\n" + n.name
}
if opts.include_node_kinds {
label = label + "\\n" + n.kind.to_text()
}
label
}
///|
fn dot_shape(n : BtNode, opts : VisualOptions) -> String {
if opts.show_leaf_shape && n.children.length() == 0 {
match n.kind {
Condition(_, _, _) => "diamond"
ActionPlan(_, _, _) => "box"
Wait(_) => "oval"
SetValue(_, _) => "note"
Emit(_) => "cds"
_ => "ellipse"
}
} else {
match n.kind {
Sequence | Selector | ParallelAll | ParallelAny => "folder"
Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "component"
_ => "ellipse"
}
}
}
///|
fn mermaid_node_open(n : BtNode, opts : VisualOptions) -> String {
if opts.show_leaf_shape && n.children.length() == 0 {
match n.kind {
Condition(_, _, _) => "{"
ActionPlan(_, _, _) => "["
Wait(_) => "(["
_ => "["
}
} else {
match n.kind {
Sequence | Selector | ParallelAll | ParallelAny => "[["
Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "[/"
_ => "["
}
}
}
///|
fn mermaid_node_close(n : BtNode, opts : VisualOptions) -> String {
if opts.show_leaf_shape && n.children.length() == 0 {
match n.kind {
Condition(_, _, _) => "}"
Wait(_) => "])"
_ => "]"
}
} else {
match n.kind {
Sequence | Selector | ParallelAll | ParallelAny => "]]"
Inverter | Succeeder | Failer | Repeat(_) | Retry(_) => "/]"
_ => "]"
}
}
}
///|
fn trace_participants(events : Array[TickEvent]) -> Array[String] {
let out = Array::new()
let mut i = 0
while i < events.length() {
if !string_array_contains(out, events[i].node_id) {
out.push(events[i].node_id)
}
i = i + 1
}
out
}
///|
fn mermaid_id(raw : String) -> String {
let chars = Array::new()
let mut i = 0
while i < raw.length() {
let code = raw[i]
if is_alpha_num_code(code) {
chars.push(code.unsafe_to_char())
} else {
chars.push('_')
}
i = i + 1
}
let id = String::from_array(chars)
if id == "" { "node" } else { "n_" + id }
}
///|
fn is_alpha_num_code(code : UInt16) -> Bool {
(code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57)
}
///|
fn dot_escape(raw : String) -> String {
raw.replace_all(old="\\", new="\\\\").replace_all(old="\"", new="\\\"")
}
///|
fn mermaid_escape(raw : String) -> String {
raw.replace_all(old="\"", new="'").replace_all(old="\n", new="
").replace_all(old="\\n", new="
")
}