use std::collections::HashMap;
use crate::renderer::{LabelItem, PointCloudItem, PolylineItem};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DebugLayer {
#[default]
Dev,
Overlay,
}
#[derive(Debug, Clone)]
pub enum DebugPrim {
Line {
start: glam::Vec3,
end: glam::Vec3,
colour: [f32; 4],
layer: DebugLayer,
},
Point {
position: glam::Vec3,
radius: f32,
colour: [f32; 4],
layer: DebugLayer,
},
Aabb {
min: glam::Vec3,
max: glam::Vec3,
colour: [f32; 4],
layer: DebugLayer,
},
Sphere {
center: glam::Vec3,
radius: f32,
colour: [f32; 4],
layer: DebugLayer,
},
Label {
position: glam::Vec3,
text: String,
colour: [f32; 4],
layer: DebugLayer,
},
}
impl DebugPrim {
pub fn layer(&self) -> DebugLayer {
match self {
DebugPrim::Line { layer, .. }
| DebugPrim::Point { layer, .. }
| DebugPrim::Aabb { layer, .. }
| DebugPrim::Sphere { layer, .. }
| DebugPrim::Label { layer, .. } => *layer,
}
}
}
pub struct DebugDraw {
pub enabled: bool,
pub dev_enabled: bool,
transient: Vec<DebugPrim>,
persistent: HashMap<u64, DebugPrim>,
}
impl Default for DebugDraw {
fn default() -> Self {
Self::new()
}
}
impl DebugDraw {
pub fn new() -> Self {
Self {
enabled: true,
dev_enabled: true,
transient: Vec::new(),
persistent: HashMap::new(),
}
}
pub fn overlay_only() -> Self {
Self {
enabled: true,
dev_enabled: false,
transient: Vec::new(),
persistent: HashMap::new(),
}
}
pub fn begin_frame(&mut self) {
self.transient.clear();
}
fn accept(&self, layer: DebugLayer) -> bool {
self.enabled && (layer == DebugLayer::Overlay || self.dev_enabled)
}
pub fn line(&mut self, start: glam::Vec3, end: glam::Vec3, colour: [f32; 4]) -> &mut Self {
self.line_layer(start, end, colour, DebugLayer::Dev)
}
pub fn line_overlay(
&mut self,
start: glam::Vec3,
end: glam::Vec3,
colour: [f32; 4],
) -> &mut Self {
self.line_layer(start, end, colour, DebugLayer::Overlay)
}
pub fn line_layer(
&mut self,
start: glam::Vec3,
end: glam::Vec3,
colour: [f32; 4],
layer: DebugLayer,
) -> &mut Self {
if self.accept(layer) {
self.transient.push(DebugPrim::Line {
start,
end,
colour,
layer,
});
}
self
}
pub fn point(&mut self, position: glam::Vec3, radius: f32, colour: [f32; 4]) -> &mut Self {
self.point_layer(position, radius, colour, DebugLayer::Dev)
}
pub fn point_overlay(
&mut self,
position: glam::Vec3,
radius: f32,
colour: [f32; 4],
) -> &mut Self {
self.point_layer(position, radius, colour, DebugLayer::Overlay)
}
pub fn point_layer(
&mut self,
position: glam::Vec3,
radius: f32,
colour: [f32; 4],
layer: DebugLayer,
) -> &mut Self {
if self.accept(layer) {
self.transient.push(DebugPrim::Point {
position,
radius,
colour,
layer,
});
}
self
}
pub fn aabb(&mut self, min: glam::Vec3, max: glam::Vec3, colour: [f32; 4]) -> &mut Self {
self.aabb_layer(min, max, colour, DebugLayer::Dev)
}
pub fn aabb_overlay(
&mut self,
min: glam::Vec3,
max: glam::Vec3,
colour: [f32; 4],
) -> &mut Self {
self.aabb_layer(min, max, colour, DebugLayer::Overlay)
}
pub fn aabb_layer(
&mut self,
min: glam::Vec3,
max: glam::Vec3,
colour: [f32; 4],
layer: DebugLayer,
) -> &mut Self {
if self.accept(layer) {
self.transient.push(DebugPrim::Aabb {
min,
max,
colour,
layer,
});
}
self
}
pub fn sphere(&mut self, center: glam::Vec3, radius: f32, colour: [f32; 4]) -> &mut Self {
self.sphere_layer(center, radius, colour, DebugLayer::Dev)
}
pub fn sphere_overlay(
&mut self,
center: glam::Vec3,
radius: f32,
colour: [f32; 4],
) -> &mut Self {
self.sphere_layer(center, radius, colour, DebugLayer::Overlay)
}
pub fn sphere_layer(
&mut self,
center: glam::Vec3,
radius: f32,
colour: [f32; 4],
layer: DebugLayer,
) -> &mut Self {
if self.accept(layer) {
self.transient.push(DebugPrim::Sphere {
center,
radius,
colour,
layer,
});
}
self
}
pub fn label(
&mut self,
position: glam::Vec3,
text: impl Into<String>,
colour: [f32; 4],
) -> &mut Self {
self.label_layer(position, text, colour, DebugLayer::Dev)
}
pub fn label_overlay(
&mut self,
position: glam::Vec3,
text: impl Into<String>,
colour: [f32; 4],
) -> &mut Self {
self.label_layer(position, text, colour, DebugLayer::Overlay)
}
pub fn label_layer(
&mut self,
position: glam::Vec3,
text: impl Into<String>,
colour: [f32; 4],
layer: DebugLayer,
) -> &mut Self {
if self.accept(layer) {
self.transient.push(DebugPrim::Label {
position,
text: text.into(),
colour,
layer,
});
}
self
}
pub fn add_persistent(&mut self, id: u64, prim: DebugPrim) {
self.persistent.insert(id, prim);
}
pub fn remove_persistent(&mut self, id: u64) -> Option<DebugPrim> {
self.persistent.remove(&id)
}
pub fn has_persistent(&self, id: u64) -> bool {
self.persistent.contains_key(&id)
}
pub fn clear_persistent(&mut self) {
self.persistent.clear();
}
pub fn prims(&self) -> impl Iterator<Item = &DebugPrim> {
self.transient.iter().chain(self.persistent.values())
}
pub fn transient_count(&self) -> usize {
self.transient.len()
}
pub fn persistent_count(&self) -> usize {
self.persistent.len()
}
pub fn is_empty(&self) -> bool {
self.transient.is_empty() && self.persistent.is_empty()
}
pub fn to_polylines(&self) -> Vec<PolylineItem> {
if !self.enabled {
return Vec::new();
}
let mut groups: HashMap<u32, (Vec<[f32; 3]>, Vec<u32>, Vec<[f32; 4]>)> = HashMap::new();
for prim in self.prims() {
if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
continue;
}
match prim {
DebugPrim::Line {
start, end, colour, ..
} => {
let key = colour_key(*colour);
let entry = groups.entry(key).or_default();
entry.0.push((*start).into());
entry.0.push((*end).into());
entry.1.push(2);
entry.2.push(*colour);
entry.2.push(*colour);
}
DebugPrim::Aabb {
min, max, colour, ..
} => {
let key = colour_key(*colour);
let entry = groups.entry(key).or_default();
let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
let mn = *min;
let mx = *max;
let aabb_positions: &[[f32; 3]] = &[
[mn.x, mn.y, mn.z],
[mx.x, mn.y, mn.z],
[mx.x, mx.y, mn.z],
[mn.x, mx.y, mn.z],
[mn.x, mn.y, mn.z],
[mn.x, mn.y, mx.z],
[mx.x, mn.y, mx.z],
[mx.x, mx.y, mx.z],
[mn.x, mx.y, mx.z],
[mn.x, mn.y, mx.z],
[mn.x, mn.y, mn.z],
[mn.x, mn.y, mx.z],
[mx.x, mn.y, mn.z],
[mx.x, mn.y, mx.z],
[mx.x, mx.y, mn.z],
[mx.x, mx.y, mx.z],
[mn.x, mx.y, mn.z],
[mn.x, mx.y, mx.z],
];
let strip_lens: &[u32] = &[5, 5, 2, 2, 2, 2];
for &p in aabb_positions {
positions.push(p);
colours.push(*colour);
}
strips.extend_from_slice(strip_lens);
}
DebugPrim::Sphere {
center,
radius,
colour,
..
} => {
let key = colour_key(*colour);
let entry = groups.entry(key).or_default();
let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
const SEGS: usize = 32;
for circle_axis in 0..3_usize {
for i in 0..=SEGS {
let t = i as f32 / SEGS as f32 * std::f32::consts::TAU;
let (s, c) = (t.sin(), t.cos());
let p = match circle_axis {
0 => glam::Vec3::new(
c * *radius + center.x,
s * *radius + center.y,
center.z,
),
1 => glam::Vec3::new(
c * *radius + center.x,
center.y,
s * *radius + center.z,
),
_ => glam::Vec3::new(
center.x,
c * *radius + center.y,
s * *radius + center.z,
),
};
positions.push(p.into());
colours.push(*colour);
}
strips.push((SEGS + 1) as u32);
}
}
_ => {} }
}
groups
.into_values()
.filter(|(positions, _, _)| !positions.is_empty())
.map(|(positions, strip_lengths, node_colours)| PolylineItem {
positions,
strip_lengths,
node_colours,
line_width: 1.5,
..PolylineItem::default()
})
.collect()
}
pub fn to_point_cloud(&self) -> Option<PointCloudItem> {
if !self.enabled {
return None;
}
let mut positions = Vec::new();
let mut colours = Vec::new();
let mut radii = Vec::new();
for prim in self.prims() {
if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
continue;
}
if let DebugPrim::Point {
position,
radius,
colour,
..
} = prim
{
positions.push((*position).into());
colours.push(*colour);
radii.push(*radius);
}
}
if positions.is_empty() {
return None;
}
Some(PointCloudItem {
positions,
colours,
radii,
..PointCloudItem::default()
})
}
pub fn to_labels(&self) -> Vec<LabelItem> {
if !self.enabled {
return Vec::new();
}
let mut out = Vec::new();
for prim in self.prims() {
if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
continue;
}
if let DebugPrim::Label {
position,
text,
colour,
..
} = prim
{
out.push(LabelItem {
world_anchor: Some((*position).into()),
text: text.clone(),
colour: *colour,
leader_line: true,
font_size: 12.0,
..LabelItem::default()
});
}
}
out
}
}
fn colour_key(c: [f32; 4]) -> u32 {
let r = (c[0].clamp(0.0, 1.0) * 255.0).round() as u32;
let g = (c[1].clamp(0.0, 1.0) * 255.0).round() as u32;
let b = (c[2].clamp(0.0, 1.0) * 255.0).round() as u32;
let a = (c[3].clamp(0.0, 1.0) * 255.0).round() as u32;
(r << 24) | (g << 16) | (b << 8) | a
}
#[cfg(test)]
mod tests {
use super::*;
fn red() -> [f32; 4] {
[1.0, 0.0, 0.0, 1.0]
}
fn green() -> [f32; 4] {
[0.0, 1.0, 0.0, 1.0]
}
#[test]
fn test_line_accumulates() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
assert_eq!(dd.transient_count(), 1);
assert!(matches!(dd.prims().next(), Some(DebugPrim::Line { .. })));
}
#[test]
fn test_point_accumulates() {
let mut dd = DebugDraw::new();
dd.point(glam::Vec3::Y, 4.0, green());
assert_eq!(dd.transient_count(), 1);
assert!(matches!(dd.prims().next(), Some(DebugPrim::Point { .. })));
}
#[test]
fn test_aabb_accumulates() {
let mut dd = DebugDraw::new();
dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
assert_eq!(dd.transient_count(), 1);
}
#[test]
fn test_sphere_accumulates() {
let mut dd = DebugDraw::new();
dd.sphere(glam::Vec3::ZERO, 1.0, green());
assert_eq!(dd.transient_count(), 1);
}
#[test]
fn test_label_accumulates() {
let mut dd = DebugDraw::new();
dd.label(glam::Vec3::ZERO, "hello", red());
assert_eq!(dd.transient_count(), 1);
assert!(matches!(dd.prims().next(), Some(DebugPrim::Label { .. })));
}
#[test]
fn test_multiple_prims_accumulate() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.line(glam::Vec3::ZERO, glam::Vec3::Y, green());
dd.point(glam::Vec3::Z, 5.0, red());
assert_eq!(dd.transient_count(), 3);
}
#[test]
fn test_begin_frame_clears_transient() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.sphere(glam::Vec3::ZERO, 1.0, green());
assert_eq!(dd.transient_count(), 2);
dd.begin_frame();
assert_eq!(dd.transient_count(), 0);
assert!(dd.is_empty());
}
#[test]
fn test_persistent_survives_begin_frame() {
let mut dd = DebugDraw::new();
dd.add_persistent(
1,
DebugPrim::Line {
start: glam::Vec3::ZERO,
end: glam::Vec3::X,
colour: red(),
layer: DebugLayer::Dev,
},
);
assert_eq!(dd.persistent_count(), 1);
dd.begin_frame();
assert_eq!(dd.transient_count(), 0);
assert_eq!(dd.persistent_count(), 1);
assert!(!dd.is_empty());
}
#[test]
fn test_persistent_removed_explicitly() {
let mut dd = DebugDraw::new();
dd.add_persistent(
42,
DebugPrim::Sphere {
center: glam::Vec3::ZERO,
radius: 1.0,
colour: green(),
layer: DebugLayer::Dev,
},
);
assert!(dd.has_persistent(42));
let removed = dd.remove_persistent(42);
assert!(removed.is_some());
assert!(!dd.has_persistent(42));
assert_eq!(dd.persistent_count(), 0);
}
#[test]
fn test_persistent_remove_missing_returns_none() {
let mut dd = DebugDraw::new();
assert!(dd.remove_persistent(999).is_none());
}
#[test]
fn test_persistent_and_transient_both_in_prims() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.add_persistent(
1,
DebugPrim::Sphere {
center: glam::Vec3::ZERO,
radius: 1.0,
colour: green(),
layer: DebugLayer::Dev,
},
);
assert_eq!(dd.prims().count(), 2);
}
#[test]
fn test_clear_persistent() {
let mut dd = DebugDraw::new();
dd.add_persistent(
1,
DebugPrim::Point {
position: glam::Vec3::ZERO,
radius: 4.0,
colour: red(),
layer: DebugLayer::Dev,
},
);
dd.add_persistent(
2,
DebugPrim::Point {
position: glam::Vec3::X,
radius: 4.0,
colour: green(),
layer: DebugLayer::Dev,
},
);
dd.clear_persistent();
assert_eq!(dd.persistent_count(), 0);
}
#[test]
fn test_enabled_false_is_noop() {
let mut dd = DebugDraw::new();
dd.enabled = false;
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.sphere(glam::Vec3::ZERO, 1.0, green());
dd.label(glam::Vec3::ZERO, "text", red());
assert_eq!(dd.transient_count(), 0);
assert!(dd.to_polylines().is_empty());
assert!(dd.to_point_cloud().is_none());
assert!(dd.to_labels().is_empty());
}
#[test]
fn test_dev_disabled_suppresses_dev_prims_in_output() {
let mut dd = DebugDraw::new();
dd.dev_enabled = false;
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red()); assert_eq!(dd.transient_count(), 0);
assert!(dd.to_polylines().is_empty());
}
#[test]
fn test_overlay_layer_shown_when_dev_disabled() {
let mut dd = DebugDraw::new();
dd.dev_enabled = false;
dd.line_overlay(glam::Vec3::ZERO, glam::Vec3::X, red());
assert_eq!(dd.transient_count(), 1);
assert!(!dd.to_polylines().is_empty());
}
#[test]
fn test_persistent_dev_skipped_in_rendering_when_dev_disabled() {
let mut dd = DebugDraw::new();
dd.add_persistent(
1,
DebugPrim::Line {
start: glam::Vec3::ZERO,
end: glam::Vec3::X,
colour: red(),
layer: DebugLayer::Dev,
},
);
dd.dev_enabled = false;
assert_eq!(dd.persistent_count(), 1);
assert!(dd.to_polylines().is_empty());
}
#[test]
fn test_to_polylines_produces_output_for_lines() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
let polylines = dd.to_polylines();
assert!(!polylines.is_empty());
let total_positions: usize = polylines.iter().map(|p| p.positions.len()).sum();
assert_eq!(total_positions, 2);
}
#[test]
fn test_to_polylines_aabb_18_positions() {
let mut dd = DebugDraw::new();
dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
let polylines = dd.to_polylines();
assert!(!polylines.is_empty());
let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
assert_eq!(total, 18);
}
#[test]
fn test_to_polylines_sphere_three_circles() {
let mut dd = DebugDraw::new();
dd.sphere(glam::Vec3::ZERO, 1.0, green());
let polylines = dd.to_polylines();
assert!(!polylines.is_empty());
let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
assert_eq!(total, 3 * 33);
}
#[test]
fn test_to_point_cloud_produces_output() {
let mut dd = DebugDraw::new();
dd.point(glam::Vec3::ZERO, 5.0, red());
dd.point(glam::Vec3::X, 3.0, green());
let pc = dd.to_point_cloud();
assert!(pc.is_some());
let pc = pc.unwrap();
assert_eq!(pc.positions.len(), 2);
assert_eq!(pc.colours.len(), 2);
assert_eq!(pc.radii.len(), 2);
}
#[test]
fn test_to_labels_produces_output() {
let mut dd = DebugDraw::new();
dd.label(glam::Vec3::ZERO, "A", red());
dd.label(glam::Vec3::X, "B", green());
let labels = dd.to_labels();
assert_eq!(labels.len(), 2);
assert_eq!(labels[0].text, "A");
assert_eq!(labels[1].text, "B");
}
#[test]
fn test_same_colour_grouped_into_one_polyline() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.line(glam::Vec3::Y, glam::Vec3::Z, red());
let polylines = dd.to_polylines();
assert_eq!(polylines.len(), 1);
assert_eq!(polylines[0].positions.len(), 4);
assert_eq!(polylines[0].strip_lengths, vec![2, 2]);
}
#[test]
fn test_different_colours_produce_separate_polylines() {
let mut dd = DebugDraw::new();
dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
dd.line(glam::Vec3::Y, glam::Vec3::Z, green());
let polylines = dd.to_polylines();
assert_eq!(polylines.len(), 2);
}
}