Skip to main content

oxihuman_viewer/
debug_draw.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Debug visualization primitives: wireframe overlays, normals, bounding boxes, joints.
5
6use oxihuman_physics::BodyProxies;
7
8// ── Data structures ───────────────────────────────────────────────────────────
9
10/// A line segment drawn for debug purposes.
11#[allow(dead_code)]
12#[derive(Debug, Clone, PartialEq)]
13pub struct DebugLine {
14    pub start: [f32; 3],
15    pub end: [f32; 3],
16    /// RGBA colour.
17    pub color: [f32; 4],
18    pub thickness: f32,
19}
20
21/// A sphere (wire or solid) drawn for debug purposes.
22#[allow(dead_code)]
23#[derive(Debug, Clone, PartialEq)]
24pub struct DebugSphere {
25    pub center: [f32; 3],
26    pub radius: f32,
27    /// RGBA colour.
28    pub color: [f32; 4],
29    pub filled: bool,
30}
31
32/// An arrow (line + head) drawn for debug purposes.
33#[allow(dead_code)]
34#[derive(Debug, Clone, PartialEq)]
35pub struct DebugArrow {
36    pub origin: [f32; 3],
37    /// Direction pre-scaled by desired length.
38    pub direction: [f32; 3],
39    /// RGBA colour.
40    pub color: [f32; 4],
41}
42
43/// A collection of debug primitives to be rendered in a single frame.
44#[allow(dead_code)]
45#[derive(Debug, Clone, Default)]
46pub struct DebugDrawList {
47    pub lines: Vec<DebugLine>,
48    pub spheres: Vec<DebugSphere>,
49    pub arrows: Vec<DebugArrow>,
50    /// World-space text labels: (text, position).
51    pub text_labels: Vec<(String, [f32; 3])>,
52}
53
54impl DebugDrawList {
55    /// Create a new, empty draw list.
56    #[allow(dead_code)]
57    pub fn new() -> Self {
58        DebugDrawList::default()
59    }
60
61    /// Add a line segment with default thickness 1.0.
62    #[allow(dead_code)]
63    pub fn add_line(&mut self, start: [f32; 3], end: [f32; 3], color: [f32; 4]) {
64        self.lines.push(DebugLine {
65            start,
66            end,
67            color,
68            thickness: 1.0,
69        });
70    }
71
72    /// Add a wireframe sphere.
73    #[allow(dead_code)]
74    pub fn add_sphere(&mut self, center: [f32; 3], radius: f32, color: [f32; 4]) {
75        self.spheres.push(DebugSphere {
76            center,
77            radius,
78            color,
79            filled: false,
80        });
81    }
82
83    /// Add a directional arrow.
84    #[allow(dead_code)]
85    pub fn add_arrow(&mut self, origin: [f32; 3], dir: [f32; 3], color: [f32; 4]) {
86        self.arrows.push(DebugArrow {
87            origin,
88            direction: dir,
89            color,
90        });
91    }
92
93    /// Add a world-space text label.
94    #[allow(dead_code)]
95    pub fn add_label(&mut self, text: &str, pos: [f32; 3]) {
96        self.text_labels.push((text.to_string(), pos));
97    }
98
99    /// Remove all primitives from the list.
100    #[allow(dead_code)]
101    pub fn clear(&mut self) {
102        self.lines.clear();
103        self.spheres.clear();
104        self.arrows.clear();
105        self.text_labels.clear();
106    }
107
108    /// Total count of all primitives (lines + spheres + arrows + labels).
109    #[allow(dead_code)]
110    pub fn total_primitives(&self) -> usize {
111        self.lines.len() + self.spheres.len() + self.arrows.len() + self.text_labels.len()
112    }
113}
114
115// ── Free functions ────────────────────────────────────────────────────────────
116
117/// Add an arrow for each vertex normal, scaled by `scale`.
118#[allow(dead_code)]
119pub fn draw_mesh_normals(
120    list: &mut DebugDrawList,
121    positions: &[[f32; 3]],
122    normals: &[[f32; 3]],
123    scale: f32,
124    color: [f32; 4],
125) {
126    let n = positions.len().min(normals.len());
127    for i in 0..n {
128        let dir = [
129            normals[i][0] * scale,
130            normals[i][1] * scale,
131            normals[i][2] * scale,
132        ];
133        list.add_arrow(positions[i], dir, color);
134    }
135}
136
137/// Add 12 line segments forming the edges of an axis-aligned bounding box.
138#[allow(dead_code)]
139pub fn draw_aabb(list: &mut DebugDrawList, min: [f32; 3], max: [f32; 3], color: [f32; 4]) {
140    // 8 corners
141    let c = [
142        [min[0], min[1], min[2]], // 0
143        [max[0], min[1], min[2]], // 1
144        [max[0], max[1], min[2]], // 2
145        [min[0], max[1], min[2]], // 3
146        [min[0], min[1], max[2]], // 4
147        [max[0], min[1], max[2]], // 5
148        [max[0], max[1], max[2]], // 6
149        [min[0], max[1], max[2]], // 7
150    ];
151    // 12 edges
152    let edges = [
153        (0, 1),
154        (1, 2),
155        (2, 3),
156        (3, 0), // bottom face
157        (4, 5),
158        (5, 6),
159        (6, 7),
160        (7, 4), // top face
161        (0, 4),
162        (1, 5),
163        (2, 6),
164        (3, 7), // verticals
165    ];
166    for (a, b) in edges {
167        list.add_line(c[a], c[b], color);
168    }
169}
170
171/// Add one line per joint to its parent (skipping root joints with no parent).
172#[allow(dead_code)]
173pub fn draw_skeleton(
174    list: &mut DebugDrawList,
175    joint_positions: &[[f32; 3]],
176    parent_indices: &[Option<usize>],
177    color: [f32; 4],
178) {
179    let n = joint_positions.len().min(parent_indices.len());
180    for i in 0..n {
181        if let Some(parent) = parent_indices[i] {
182            if parent < joint_positions.len() {
183                list.add_line(joint_positions[i], joint_positions[parent], color);
184            }
185        }
186    }
187}
188
189/// Add wireframe debug spheres for every sphere and capsule proxy in `proxies`.
190///
191/// For capsules, spheres are placed at both endpoints.
192#[allow(dead_code)]
193pub fn draw_physics_proxies_debug(
194    list: &mut DebugDrawList,
195    proxies: &BodyProxies,
196    color: [f32; 4],
197) {
198    for sphere in &proxies.spheres {
199        list.add_sphere(sphere.center, sphere.radius, color);
200    }
201    for capsule in &proxies.capsules {
202        list.add_sphere(capsule.center_a, capsule.radius, color);
203        list.add_sphere(capsule.center_b, capsule.radius, color);
204        list.add_line(capsule.center_a, capsule.center_b, color);
205    }
206}
207
208/// Serialize the draw list to a compact JSON string.
209#[allow(dead_code)]
210pub fn debug_draw_to_json(list: &DebugDrawList) -> String {
211    let lines: Vec<String> = list
212        .lines
213        .iter()
214        .map(|l| {
215            format!(
216                r#"{{"start":[{},{},{}],"end":[{},{},{}],"color":[{},{},{},{}],"thickness":{}}}"#,
217                l.start[0],
218                l.start[1],
219                l.start[2],
220                l.end[0],
221                l.end[1],
222                l.end[2],
223                l.color[0],
224                l.color[1],
225                l.color[2],
226                l.color[3],
227                l.thickness
228            )
229        })
230        .collect();
231    let spheres: Vec<String> = list
232        .spheres
233        .iter()
234        .map(|s| {
235            format!(
236                r#"{{"center":[{},{},{}],"radius":{},"color":[{},{},{},{}],"filled":{}}}"#,
237                s.center[0],
238                s.center[1],
239                s.center[2],
240                s.radius,
241                s.color[0],
242                s.color[1],
243                s.color[2],
244                s.color[3],
245                s.filled
246            )
247        })
248        .collect();
249    let arrows: Vec<String> = list
250        .arrows
251        .iter()
252        .map(|a| {
253            format!(
254                r#"{{"origin":[{},{},{}],"direction":[{},{},{}],"color":[{},{},{},{}]}}"#,
255                a.origin[0],
256                a.origin[1],
257                a.origin[2],
258                a.direction[0],
259                a.direction[1],
260                a.direction[2],
261                a.color[0],
262                a.color[1],
263                a.color[2],
264                a.color[3]
265            )
266        })
267        .collect();
268    let labels: Vec<String> = list
269        .text_labels
270        .iter()
271        .map(|(text, pos)| {
272            format!(
273                r#"{{"text":"{}","pos":[{},{},{}]}}"#,
274                json_escape(text),
275                pos[0],
276                pos[1],
277                pos[2]
278            )
279        })
280        .collect();
281
282    format!(
283        r#"{{"lines":[{}],"spheres":[{}],"arrows":[{}],"labels":[{}]}}"#,
284        lines.join(","),
285        spheres.join(","),
286        arrows.join(","),
287        labels.join(",")
288    )
289}
290
291// ── Private helpers ───────────────────────────────────────────────────────────
292
293fn json_escape(s: &str) -> String {
294    let mut out = String::with_capacity(s.len());
295    for ch in s.chars() {
296        match ch {
297            '"' => out.push_str("\\\""),
298            '\\' => out.push_str("\\\\"),
299            '\n' => out.push_str("\\n"),
300            '\r' => out.push_str("\\r"),
301            '\t' => out.push_str("\\t"),
302            other => out.push(other),
303        }
304    }
305    out
306}
307
308// ── Tests ─────────────────────────────────────────────────────────────────────
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use oxihuman_physics::{BodyProxies, CapsuleProxy, SphereProxy};
314
315    // 1. new() is empty
316    #[test]
317    fn new_is_empty() {
318        let list = DebugDrawList::new();
319        assert_eq!(list.total_primitives(), 0);
320    }
321
322    // 2. add_line increments line count
323    #[test]
324    fn add_line_increments_count() {
325        let mut list = DebugDrawList::new();
326        list.add_line([0.0; 3], [1.0; 3], [1.0, 0.0, 0.0, 1.0]);
327        assert_eq!(list.lines.len(), 1);
328        assert_eq!(list.total_primitives(), 1);
329    }
330
331    // 3. add_sphere increments sphere count
332    #[test]
333    fn add_sphere_increments_count() {
334        let mut list = DebugDrawList::new();
335        list.add_sphere([0.0; 3], 1.0, [0.0, 1.0, 0.0, 1.0]);
336        assert_eq!(list.spheres.len(), 1);
337    }
338
339    // 4. add_arrow increments arrow count
340    #[test]
341    fn add_arrow_increments_count() {
342        let mut list = DebugDrawList::new();
343        list.add_arrow([0.0; 3], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0, 1.0]);
344        assert_eq!(list.arrows.len(), 1);
345    }
346
347    // 5. add_label increments label count
348    #[test]
349    fn add_label_increments_count() {
350        let mut list = DebugDrawList::new();
351        list.add_label("test", [0.0, 1.0, 0.0]);
352        assert_eq!(list.text_labels.len(), 1);
353        assert_eq!(list.text_labels[0].0, "test");
354    }
355
356    // 6. clear() resets everything
357    #[test]
358    fn clear_resets_all() {
359        let mut list = DebugDrawList::new();
360        list.add_line([0.0; 3], [1.0; 3], [1.0; 4]);
361        list.add_sphere([0.0; 3], 1.0, [1.0; 4]);
362        list.add_arrow([0.0; 3], [1.0, 0.0, 0.0], [1.0; 4]);
363        list.add_label("lbl", [0.0; 3]);
364        list.clear();
365        assert_eq!(list.total_primitives(), 0);
366    }
367
368    // 7. total_primitives sums all types
369    #[test]
370    fn total_primitives_sum() {
371        let mut list = DebugDrawList::new();
372        list.add_line([0.0; 3], [1.0; 3], [1.0; 4]);
373        list.add_line([0.0; 3], [2.0; 3], [1.0; 4]);
374        list.add_sphere([0.0; 3], 0.5, [1.0; 4]);
375        list.add_arrow([0.0; 3], [0.0, 1.0, 0.0], [1.0; 4]);
376        list.add_label("a", [0.0; 3]);
377        assert_eq!(list.total_primitives(), 5);
378    }
379
380    // 8. draw_aabb adds exactly 12 lines
381    #[test]
382    fn draw_aabb_adds_12_lines() {
383        let mut list = DebugDrawList::new();
384        draw_aabb(&mut list, [-1.0; 3], [1.0; 3], [1.0, 1.0, 0.0, 1.0]);
385        assert_eq!(list.lines.len(), 12);
386    }
387
388    // 9. draw_mesh_normals adds n arrows for n vertices
389    #[test]
390    fn draw_mesh_normals_adds_n_arrows() {
391        let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
392        let normals = vec![[0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 0.0]];
393        let mut list = DebugDrawList::new();
394        draw_mesh_normals(&mut list, &positions, &normals, 0.1, [0.0, 0.0, 1.0, 1.0]);
395        assert_eq!(list.arrows.len(), 3);
396    }
397
398    // 10. draw_skeleton adds n-1 lines when all joints have parents except root
399    #[test]
400    fn draw_skeleton_adds_n_minus_1_lines() {
401        let positions = vec![
402            [0.0, 0.0, 0.0],
403            [0.0, 1.0, 0.0],
404            [0.0, 2.0, 0.0],
405            [0.0, 3.0, 0.0],
406        ];
407        let parents: Vec<Option<usize>> = vec![None, Some(0), Some(1), Some(2)];
408        let mut list = DebugDrawList::new();
409        draw_skeleton(&mut list, &positions, &parents, [1.0, 1.0, 1.0, 1.0]);
410        assert_eq!(list.lines.len(), 3); // 4 joints - 1 (root has no parent)
411    }
412
413    // 11. debug_draw_to_json produces non-empty JSON
414    #[test]
415    fn debug_draw_to_json_non_empty() {
416        let mut list = DebugDrawList::new();
417        list.add_line([0.0; 3], [1.0; 3], [1.0; 4]);
418        let json = debug_draw_to_json(&list);
419        assert!(!json.is_empty());
420        assert!(json.contains("lines"));
421    }
422
423    // 12. debug_draw_to_json empty list produces valid structure
424    #[test]
425    fn debug_draw_to_json_empty() {
426        let list = DebugDrawList::new();
427        let json = debug_draw_to_json(&list);
428        assert!(json.contains("lines"));
429        assert!(json.contains("spheres"));
430    }
431
432    // 13. draw_physics_proxies_debug runs without panic
433    #[test]
434    fn draw_physics_proxies_debug_no_panic() {
435        let mut proxies = BodyProxies::new();
436        proxies
437            .spheres
438            .push(SphereProxy::new([0.0, 1.0, 0.0], 0.1, "head"));
439        proxies.capsules.push(CapsuleProxy::new(
440            [0.0, 0.5, 0.0],
441            [0.0, 1.0, 0.0],
442            0.15,
443            "torso",
444        ));
445        let mut list = DebugDrawList::new();
446        draw_physics_proxies_debug(&mut list, &proxies, [0.0, 1.0, 0.0, 1.0]);
447        // 1 sphere proxy + 2 endpoint spheres + 1 capsule line = 4 total primitives
448        assert_eq!(list.spheres.len(), 3);
449        assert_eq!(list.lines.len(), 1);
450    }
451
452    // 14. draw_aabb covers correct corners — check one diagonal
453    #[test]
454    fn draw_aabb_min_max_in_lines() {
455        let min = [0.0f32; 3];
456        let max = [2.0f32; 3];
457        let mut list = DebugDrawList::new();
458        draw_aabb(&mut list, min, max, [1.0; 4]);
459        // At least one line must start or end at the min corner
460        let has_min = list.lines.iter().any(|l| l.start == min || l.end == min);
461        assert!(has_min);
462    }
463}