Skip to main content

oxihuman_export/
proto_text_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Export data as Protocol Buffers text format.
5
6#![allow(dead_code)]
7
8/// A proto text field.
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct ProtoField {
12    pub name: String,
13    pub value: String,
14}
15
16/// A proto text message.
17#[allow(dead_code)]
18#[derive(Debug, Clone, Default)]
19pub struct ProtoMessage {
20    pub name: String,
21    pub fields: Vec<ProtoField>,
22    pub nested: Vec<ProtoMessage>,
23}
24
25/// Create a new proto message.
26#[allow(dead_code)]
27pub fn new_proto_message(name: &str) -> ProtoMessage {
28    ProtoMessage {
29        name: name.to_string(),
30        fields: Vec::new(),
31        nested: Vec::new(),
32    }
33}
34
35/// Add a string field.
36#[allow(dead_code)]
37pub fn add_proto_string(msg: &mut ProtoMessage, name: &str, value: &str) {
38    msg.fields.push(ProtoField {
39        name: name.to_string(),
40        value: format!("\"{}\"", value),
41    });
42}
43
44/// Add an integer field.
45#[allow(dead_code)]
46pub fn add_proto_int(msg: &mut ProtoMessage, name: &str, value: i64) {
47    msg.fields.push(ProtoField {
48        name: name.to_string(),
49        value: value.to_string(),
50    });
51}
52
53/// Add a float field.
54#[allow(dead_code)]
55pub fn add_proto_float(msg: &mut ProtoMessage, name: &str, value: f64) {
56    msg.fields.push(ProtoField {
57        name: name.to_string(),
58        value: format!("{:.6}", value),
59    });
60}
61
62/// Add a boolean field.
63#[allow(dead_code)]
64pub fn add_proto_bool(msg: &mut ProtoMessage, name: &str, value: bool) {
65    msg.fields.push(ProtoField {
66        name: name.to_string(),
67        value: if value {
68            "true".to_string()
69        } else {
70            "false".to_string()
71        },
72    });
73}
74
75/// Add a nested message.
76#[allow(dead_code)]
77pub fn add_proto_nested(msg: &mut ProtoMessage, nested: ProtoMessage) {
78    msg.nested.push(nested);
79}
80
81/// Return the field count.
82#[allow(dead_code)]
83pub fn proto_field_count(msg: &ProtoMessage) -> usize {
84    msg.fields.len()
85}
86
87/// Find a field by name.
88#[allow(dead_code)]
89pub fn find_proto_field<'a>(msg: &'a ProtoMessage, name: &str) -> Option<&'a str> {
90    msg.fields
91        .iter()
92        .find(|f| f.name == name)
93        .map(|f| f.value.as_str())
94}
95
96/// Serialise as proto text format.
97#[allow(dead_code)]
98pub fn to_proto_text(msg: &ProtoMessage, indent: usize) -> String {
99    let pad = "  ".repeat(indent);
100    let mut out = if msg.name.is_empty() {
101        String::new()
102    } else {
103        format!("{}{} {{\n", pad, msg.name)
104    };
105    for f in &msg.fields {
106        out.push_str(&format!("{}  {}: {}\n", pad, f.name, f.value));
107    }
108    for n in &msg.nested {
109        out.push_str(&to_proto_text(n, indent + 1));
110    }
111    if !msg.name.is_empty() {
112        out.push_str(&format!("{}}}\n", pad));
113    }
114    out
115}
116
117/// Export mesh stats as proto text.
118#[allow(dead_code)]
119pub fn export_mesh_stats_proto(vertex_count: usize, index_count: usize, name: &str) -> String {
120    let mut msg = new_proto_message("Mesh");
121    add_proto_string(&mut msg, "name", name);
122    add_proto_int(&mut msg, "vertex_count", vertex_count as i64);
123    add_proto_int(&mut msg, "index_count", index_count as i64);
124    to_proto_text(&msg, 0)
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_new_proto_message_empty() {
133        let msg = new_proto_message("Foo");
134        assert_eq!(proto_field_count(&msg), 0);
135    }
136
137    #[test]
138    fn test_add_string() {
139        let mut msg = new_proto_message("M");
140        add_proto_string(&mut msg, "name", "test");
141        assert_eq!(proto_field_count(&msg), 1);
142    }
143
144    #[test]
145    fn test_add_int() {
146        let mut msg = new_proto_message("M");
147        add_proto_int(&mut msg, "count", 42);
148        assert_eq!(find_proto_field(&msg, "count"), Some("42"));
149    }
150
151    #[test]
152    fn test_add_float() {
153        let mut msg = new_proto_message("M");
154        add_proto_float(&mut msg, "scale", 1.0);
155        assert!(find_proto_field(&msg, "scale").is_some());
156    }
157
158    #[test]
159    fn test_add_bool() {
160        let mut msg = new_proto_message("M");
161        add_proto_bool(&mut msg, "enabled", true);
162        assert_eq!(find_proto_field(&msg, "enabled"), Some("true"));
163    }
164
165    #[test]
166    fn test_to_proto_text_contains_name() {
167        let msg = new_proto_message("Mesh");
168        let s = to_proto_text(&msg, 0);
169        assert!(s.contains("Mesh"));
170    }
171
172    #[test]
173    fn test_to_proto_text_contains_field() {
174        let mut msg = new_proto_message("M");
175        add_proto_int(&mut msg, "vertices", 100);
176        let s = to_proto_text(&msg, 0);
177        assert!(s.contains("vertices: 100"));
178    }
179
180    #[test]
181    fn test_nested_message() {
182        let mut outer = new_proto_message("Outer");
183        let mut inner = new_proto_message("inner");
184        add_proto_string(&mut inner, "k", "v");
185        add_proto_nested(&mut outer, inner);
186        let s = to_proto_text(&outer, 0);
187        assert!(s.contains("inner"));
188    }
189
190    #[test]
191    fn test_export_mesh_stats_proto() {
192        let s = export_mesh_stats_proto(100, 300, "body");
193        assert!(s.contains("vertex_count"));
194        assert!(s.contains("body"));
195    }
196
197    #[test]
198    fn test_find_missing_field() {
199        let msg = new_proto_message("M");
200        assert!(find_proto_field(&msg, "ghost").is_none());
201    }
202}