Skip to main content

oxihuman_physics/
proxy_types.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Proxy primitive types and JSON serialization / deserialization.
5
6/// A capsule collision primitive (line segment + radius).
7#[derive(Debug, Clone, PartialEq)]
8pub struct CapsuleProxy {
9    /// Bottom center of the capsule.
10    pub center_a: [f32; 3],
11    /// Top center of the capsule.
12    pub center_b: [f32; 3],
13    /// Radius of the capsule.
14    pub radius: f32,
15    /// Label (e.g. "torso", "head", "arm_l").
16    pub label: String,
17}
18
19impl CapsuleProxy {
20    pub fn new(center_a: [f32; 3], center_b: [f32; 3], radius: f32, label: &str) -> Self {
21        CapsuleProxy {
22            center_a,
23            center_b,
24            radius,
25            label: label.to_string(),
26        }
27    }
28}
29
30/// A sphere collision primitive.
31#[derive(Debug, Clone, PartialEq)]
32pub struct SphereProxy {
33    pub center: [f32; 3],
34    pub radius: f32,
35    pub label: String,
36}
37
38impl SphereProxy {
39    pub fn new(center: [f32; 3], radius: f32, label: &str) -> Self {
40        SphereProxy {
41            center,
42            radius,
43            label: label.to_string(),
44        }
45    }
46}
47
48/// A box (AABB) collision primitive.
49#[derive(Debug, Clone, PartialEq)]
50pub struct BoxProxy {
51    pub center: [f32; 3],
52    pub half_extents: [f32; 3],
53    pub label: String,
54}
55
56/// Complete set of collision proxies for a humanoid body.
57#[derive(Debug, Default, Clone)]
58pub struct BodyProxies {
59    pub capsules: Vec<CapsuleProxy>,
60    pub spheres: Vec<SphereProxy>,
61    pub boxes: Vec<BoxProxy>,
62}
63
64impl BodyProxies {
65    pub fn new() -> Self {
66        BodyProxies::default()
67    }
68
69    pub fn total_count(&self) -> usize {
70        self.capsules.len() + self.spheres.len() + self.boxes.len()
71    }
72}
73
74// ── JSON serialization ────────────────────────────────────────────────────────
75
76/// Serialize a `[f32; 3]` to a compact JSON array string.
77fn fmt_vec3(v: [f32; 3]) -> String {
78    format!("[{},{},{}]", v[0], v[1], v[2])
79}
80
81/// Escape a string for safe embedding in JSON (handles `"` and `\`).
82fn json_escape(s: &str) -> String {
83    let mut out = String::with_capacity(s.len() + 2);
84    for ch in s.chars() {
85        match ch {
86            '"' => out.push_str("\\\""),
87            '\\' => out.push_str("\\\\"),
88            '\n' => out.push_str("\\n"),
89            '\r' => out.push_str("\\r"),
90            '\t' => out.push_str("\\t"),
91            other => out.push(other),
92        }
93    }
94    out
95}
96
97/// Serialize [`BodyProxies`] to a JSON string.
98///
99/// Output format:
100/// ```json
101/// {
102///   "capsules": [...],
103///   "spheres": [...],
104///   "boxes": [...]
105/// }
106/// ```
107pub fn proxies_to_json(proxies: &BodyProxies) -> String {
108    let mut out = String::with_capacity(512);
109    out.push_str("{\n  \"capsules\": [\n");
110
111    for (i, c) in proxies.capsules.iter().enumerate() {
112        let comma = if i + 1 < proxies.capsules.len() {
113            ","
114        } else {
115            ""
116        };
117        out.push_str(&format!(
118            "    {{\"label\":\"{}\",\"center_a\":{},\"center_b\":{},\"radius\":{}}}{}\n",
119            json_escape(&c.label),
120            fmt_vec3(c.center_a),
121            fmt_vec3(c.center_b),
122            c.radius,
123            comma
124        ));
125    }
126
127    out.push_str("  ],\n  \"spheres\": [\n");
128
129    for (i, s) in proxies.spheres.iter().enumerate() {
130        let comma = if i + 1 < proxies.spheres.len() {
131            ","
132        } else {
133            ""
134        };
135        out.push_str(&format!(
136            "    {{\"label\":\"{}\",\"center\":{},\"radius\":{}}}{}\n",
137            json_escape(&s.label),
138            fmt_vec3(s.center),
139            s.radius,
140            comma
141        ));
142    }
143
144    out.push_str("  ],\n  \"boxes\": [\n");
145
146    for (i, b) in proxies.boxes.iter().enumerate() {
147        let comma = if i + 1 < proxies.boxes.len() { "," } else { "" };
148        out.push_str(&format!(
149            "    {{\"label\":\"{}\",\"center\":{},\"half_extents\":{}}}{}\n",
150            json_escape(&b.label),
151            fmt_vec3(b.center),
152            fmt_vec3(b.half_extents),
153            comma
154        ));
155    }
156
157    out.push_str("  ]\n}");
158    out
159}
160
161/// Deserialize [`BodyProxies`] from a JSON string produced by [`proxies_to_json`].
162///
163/// Uses `serde_json` for reliable parsing.
164pub fn proxies_from_json(s: &str) -> anyhow::Result<BodyProxies> {
165    let v: serde_json::Value = serde_json::from_str(s)?;
166
167    let parse_vec3 = |arr: &serde_json::Value| -> anyhow::Result<[f32; 3]> {
168        let a = arr
169            .as_array()
170            .ok_or_else(|| anyhow::anyhow!("expected array for vec3"))?;
171        if a.len() != 3 {
172            anyhow::bail!("vec3 must have 3 elements, got {}", a.len());
173        }
174        Ok([
175            a[0].as_f64()
176                .ok_or_else(|| anyhow::anyhow!("expected float"))? as f32,
177            a[1].as_f64()
178                .ok_or_else(|| anyhow::anyhow!("expected float"))? as f32,
179            a[2].as_f64()
180                .ok_or_else(|| anyhow::anyhow!("expected float"))? as f32,
181        ])
182    };
183
184    let get_str = |obj: &serde_json::Value, key: &str| -> anyhow::Result<String> {
185        obj[key]
186            .as_str()
187            .map(|s| s.to_string())
188            .ok_or_else(|| anyhow::anyhow!("missing string field '{key}'"))
189    };
190
191    let get_f32 = |obj: &serde_json::Value, key: &str| -> anyhow::Result<f32> {
192        obj[key]
193            .as_f64()
194            .map(|f| f as f32)
195            .ok_or_else(|| anyhow::anyhow!("missing float field '{key}'"))
196    };
197
198    let mut proxies = BodyProxies::new();
199
200    if let Some(caps) = v["capsules"].as_array() {
201        for c in caps {
202            proxies.capsules.push(CapsuleProxy {
203                label: get_str(c, "label")?,
204                center_a: parse_vec3(&c["center_a"])?,
205                center_b: parse_vec3(&c["center_b"])?,
206                radius: get_f32(c, "radius")?,
207            });
208        }
209    }
210
211    if let Some(spheres) = v["spheres"].as_array() {
212        for s in spheres {
213            proxies.spheres.push(SphereProxy {
214                label: get_str(s, "label")?,
215                center: parse_vec3(&s["center"])?,
216                radius: get_f32(s, "radius")?,
217            });
218        }
219    }
220
221    if let Some(boxes) = v["boxes"].as_array() {
222        for b in boxes {
223            proxies.boxes.push(BoxProxy {
224                label: get_str(b, "label")?,
225                center: parse_vec3(&b["center"])?,
226                half_extents: parse_vec3(&b["half_extents"])?,
227            });
228        }
229    }
230
231    Ok(proxies)
232}