Skip to main content

oxihuman_export/
bone_envelope_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export bone envelope data (capsule-based influence volumes).
6
7/// A bone envelope represented as a capsule.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct BoneEnvelope {
11    pub bone_name: String,
12    pub head: [f32; 3],
13    pub tail: [f32; 3],
14    pub radius_head: f32,
15    pub radius_tail: f32,
16    pub weight: f32,
17}
18
19/// Collection of bone envelopes.
20#[allow(dead_code)]
21#[derive(Debug, Clone, Default)]
22pub struct EnvelopeSet {
23    pub envelopes: Vec<BoneEnvelope>,
24}
25
26/// Create a new envelope set.
27#[allow(dead_code)]
28pub fn new_envelope_set() -> EnvelopeSet {
29    EnvelopeSet::default()
30}
31
32/// Add an envelope to the set.
33#[allow(dead_code)]
34pub fn add_envelope(set: &mut EnvelopeSet, env: BoneEnvelope) {
35    set.envelopes.push(env);
36}
37
38/// Length (bone length) of an envelope.
39#[allow(dead_code)]
40pub fn envelope_length(env: &BoneEnvelope) -> f32 {
41    let d = [
42        env.tail[0] - env.head[0],
43        env.tail[1] - env.head[1],
44        env.tail[2] - env.head[2],
45    ];
46    (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
47}
48
49/// Check if a point lies inside the bone envelope capsule.
50#[allow(dead_code)]
51pub fn point_in_envelope(p: [f32; 3], env: &BoneEnvelope) -> bool {
52    // Project p onto the bone segment and check radius at that t.
53    let seg = [
54        env.tail[0] - env.head[0],
55        env.tail[1] - env.head[1],
56        env.tail[2] - env.head[2],
57    ];
58    let len2 = seg[0] * seg[0] + seg[1] * seg[1] + seg[2] * seg[2];
59    if len2 < 1e-10 {
60        let d2 = sq_dist(p, env.head);
61        return d2 <= env.radius_head * env.radius_head;
62    }
63    let hp = [p[0] - env.head[0], p[1] - env.head[1], p[2] - env.head[2]];
64    let t = (hp[0] * seg[0] + hp[1] * seg[1] + hp[2] * seg[2]) / len2;
65    let t = t.clamp(0.0, 1.0);
66    let closest = [
67        env.head[0] + t * seg[0],
68        env.head[1] + t * seg[1],
69        env.head[2] + t * seg[2],
70    ];
71    let d2 = sq_dist(p, closest);
72    let r = env.radius_head + t * (env.radius_tail - env.radius_head);
73    d2 <= r * r
74}
75
76fn sq_dist(a: [f32; 3], b: [f32; 3]) -> f32 {
77    let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
78    d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
79}
80
81/// Compute the volume of the capsule envelope (approximation).
82#[allow(dead_code)]
83pub fn envelope_volume(env: &BoneEnvelope) -> f32 {
84    use std::f32::consts::PI;
85    let l = envelope_length(env);
86    let r = (env.radius_head + env.radius_tail) * 0.5;
87    PI * r * r * l + (4.0 / 3.0) * PI * r * r * r
88}
89
90/// Find envelope by bone name.
91#[allow(dead_code)]
92pub fn find_envelope<'a>(set: &'a EnvelopeSet, name: &str) -> Option<&'a BoneEnvelope> {
93    set.envelopes.iter().find(|e| e.bone_name == name)
94}
95
96/// Serialise envelopes to a flat buffer: `[hx,hy,hz,tx,ty,tz,rh,rt,w]` per envelope.
97#[allow(dead_code)]
98pub fn serialise_envelopes(set: &EnvelopeSet) -> Vec<f32> {
99    set.envelopes
100        .iter()
101        .flat_map(|e| {
102            [
103                e.head[0],
104                e.head[1],
105                e.head[2],
106                e.tail[0],
107                e.tail[1],
108                e.tail[2],
109                e.radius_head,
110                e.radius_tail,
111                e.weight,
112            ]
113        })
114        .collect()
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    fn sample_env() -> BoneEnvelope {
122        BoneEnvelope {
123            bone_name: "arm".to_string(),
124            head: [0.0, 0.0, 0.0],
125            tail: [0.0, 1.0, 0.0],
126            radius_head: 0.1,
127            radius_tail: 0.05,
128            weight: 1.0,
129        }
130    }
131
132    #[test]
133    fn test_add_envelope() {
134        let mut s = new_envelope_set();
135        add_envelope(&mut s, sample_env());
136        assert_eq!(s.envelopes.len(), 1);
137    }
138
139    #[test]
140    fn test_envelope_length() {
141        let e = sample_env();
142        assert!((envelope_length(&e) - 1.0).abs() < 1e-6);
143    }
144
145    #[test]
146    fn test_point_in_envelope_inside() {
147        let e = sample_env();
148        assert!(point_in_envelope([0.0, 0.5, 0.0], &e));
149    }
150
151    #[test]
152    fn test_point_in_envelope_outside() {
153        let e = sample_env();
154        assert!(!point_in_envelope([1.0, 0.5, 0.0], &e));
155    }
156
157    #[test]
158    fn test_envelope_volume_positive() {
159        assert!(envelope_volume(&sample_env()) > 0.0);
160    }
161
162    #[test]
163    fn test_find_envelope_found() {
164        let mut s = new_envelope_set();
165        add_envelope(&mut s, sample_env());
166        assert!(find_envelope(&s, "arm").is_some());
167    }
168
169    #[test]
170    fn test_find_envelope_not_found() {
171        let s = new_envelope_set();
172        assert!(find_envelope(&s, "leg").is_none());
173    }
174
175    #[test]
176    fn test_serialise_envelopes_length() {
177        let mut s = new_envelope_set();
178        add_envelope(&mut s, sample_env());
179        assert_eq!(serialise_envelopes(&s).len(), 9);
180    }
181
182    #[test]
183    fn test_serialise_empty() {
184        let s = new_envelope_set();
185        assert!(serialise_envelopes(&s).is_empty());
186    }
187
188    #[test]
189    fn test_weight_stored() {
190        let e = sample_env();
191        assert!((e.weight - 1.0).abs() < 1e-6);
192    }
193}