Skip to main content

oxihuman_export/
dot_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Export a dependency/scene graph as Graphviz DOT format.
5
6#![allow(dead_code)]
7
8/// A node in the DOT graph.
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct DotNode {
12    pub id: String,
13    pub label: String,
14    pub shape: String,
15}
16
17/// An edge in the DOT graph.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct DotEdge {
21    pub from: String,
22    pub to: String,
23    pub label: String,
24}
25
26/// A DOT graph export document.
27#[allow(dead_code)]
28#[derive(Debug, Clone, Default)]
29pub struct DotExport {
30    pub name: String,
31    pub directed: bool,
32    pub nodes: Vec<DotNode>,
33    pub edges: Vec<DotEdge>,
34}
35
36/// Create a new DOT export.
37#[allow(dead_code)]
38pub fn new_dot_export(name: &str, directed: bool) -> DotExport {
39    DotExport {
40        name: name.to_string(),
41        directed,
42        nodes: Vec::new(),
43        edges: Vec::new(),
44    }
45}
46
47/// Add a node.
48#[allow(dead_code)]
49pub fn add_dot_node(graph: &mut DotExport, id: &str, label: &str, shape: &str) {
50    graph.nodes.push(DotNode {
51        id: id.to_string(),
52        label: label.to_string(),
53        shape: shape.to_string(),
54    });
55}
56
57/// Add an edge.
58#[allow(dead_code)]
59pub fn add_dot_edge(graph: &mut DotExport, from: &str, to: &str, label: &str) {
60    graph.edges.push(DotEdge {
61        from: from.to_string(),
62        to: to.to_string(),
63        label: label.to_string(),
64    });
65}
66
67/// Return number of nodes.
68#[allow(dead_code)]
69pub fn dot_node_count(graph: &DotExport) -> usize {
70    graph.nodes.len()
71}
72
73/// Return number of edges.
74#[allow(dead_code)]
75pub fn dot_edge_count(graph: &DotExport) -> usize {
76    graph.edges.len()
77}
78
79/// Serialise as DOT string.
80#[allow(dead_code)]
81pub fn to_dot_string(graph: &DotExport) -> String {
82    let kw = if graph.directed { "digraph" } else { "graph" };
83    let arrow = if graph.directed { " -> " } else { " -- " };
84    let mut out = format!("{} {} {{\n", kw, graph.name);
85    for node in &graph.nodes {
86        out.push_str(&format!(
87            "  {} [label=\"{}\", shape={}];\n",
88            node.id, node.label, node.shape
89        ));
90    }
91    for edge in &graph.edges {
92        let lbl = if edge.label.is_empty() {
93            String::new()
94        } else {
95            format!(" [label=\"{}\"]", edge.label)
96        };
97        out.push_str(&format!("  {}{}{}{};\n", edge.from, arrow, edge.to, lbl));
98    }
99    out.push('}');
100    out
101}
102
103/// Find a node by id.
104#[allow(dead_code)]
105pub fn find_dot_node<'a>(graph: &'a DotExport, id: &str) -> Option<&'a DotNode> {
106    graph.nodes.iter().find(|n| n.id == id)
107}
108
109/// Export a simple skeleton as a DOT graph.
110#[allow(dead_code)]
111pub fn export_skeleton_dot(bones: &[(&str, Option<&str>)]) -> String {
112    let mut graph = new_dot_export("skeleton", true);
113    for &(bone, parent) in bones {
114        add_dot_node(&mut graph, bone, bone, "ellipse");
115        if let Some(p) = parent {
116            add_dot_edge(&mut graph, p, bone, "");
117        }
118    }
119    to_dot_string(&graph)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_new_dot_export_empty() {
128        let g = new_dot_export("g", true);
129        assert_eq!(dot_node_count(&g), 0);
130    }
131
132    #[test]
133    fn test_add_node() {
134        let mut g = new_dot_export("g", true);
135        add_dot_node(&mut g, "n1", "Node 1", "box");
136        assert_eq!(dot_node_count(&g), 1);
137    }
138
139    #[test]
140    fn test_add_edge() {
141        let mut g = new_dot_export("g", true);
142        add_dot_edge(&mut g, "a", "b", "dep");
143        assert_eq!(dot_edge_count(&g), 1);
144    }
145
146    #[test]
147    fn test_to_dot_directed() {
148        let g = new_dot_export("mygraph", true);
149        let s = to_dot_string(&g);
150        assert!(s.contains("digraph"));
151    }
152
153    #[test]
154    fn test_to_dot_undirected() {
155        let g = new_dot_export("g", false);
156        let s = to_dot_string(&g);
157        assert!(s.contains("graph"));
158    }
159
160    #[test]
161    fn test_to_dot_contains_node() {
162        let mut g = new_dot_export("g", true);
163        add_dot_node(&mut g, "n1", "MyNode", "box");
164        let s = to_dot_string(&g);
165        assert!(s.contains("n1"));
166    }
167
168    #[test]
169    fn test_to_dot_contains_edge() {
170        let mut g = new_dot_export("g", true);
171        add_dot_edge(&mut g, "a", "b", "");
172        let s = to_dot_string(&g);
173        assert!(s.contains("->"));
174    }
175
176    #[test]
177    fn test_find_dot_node() {
178        let mut g = new_dot_export("g", true);
179        add_dot_node(&mut g, "hip", "Hip", "ellipse");
180        let n = find_dot_node(&g, "hip");
181        assert!(n.is_some());
182    }
183
184    #[test]
185    fn test_export_skeleton_dot() {
186        let bones = vec![("root", None), ("hip", Some("root")), ("knee", Some("hip"))];
187        let s = export_skeleton_dot(&bones);
188        assert!(s.contains("digraph"));
189        assert!(s.contains("root"));
190    }
191
192    #[test]
193    fn test_find_missing_node() {
194        let g = new_dot_export("g", true);
195        assert!(find_dot_node(&g, "missing").is_none());
196    }
197}