Skip to main content

viewport_lib/runtime/
debug_draw.rs

1//! Debug draw accumulator for runtime plugins.
2//!
3//! [`DebugDraw`] is a shared resource that plugins use to submit transient and
4//! persistent debug visuals each frame. Store one instance in the runtime
5//! resource registry and call [`DebugDraw::begin_frame`] before each call to
6//! [`super::ViewportRuntime::step`] to clear one-frame primitives.
7//!
8//! # Usage
9//!
10//! ```rust,ignore
11//! use viewport_lib::runtime::debug_draw::{DebugDraw, DebugLayer};
12//!
13//! // At startup: insert a DebugDraw resource (dev_enabled controls dev-layer draws).
14//! runtime.resources_mut().insert(DebugDraw::new());
15//!
16//! // In each frame, before runtime.step():
17//! if let Some(dd) = runtime.resources_mut().get_mut::<DebugDraw>() {
18//!     dd.begin_frame();
19//! }
20//! let output = runtime.step(&mut scene, &mut selection, &frame_ctx);
21//!
22//! // After step: convert accumulated primitives to render items.
23//! if let Some(dd) = runtime.resources().get::<DebugDraw>() {
24//!     frame_data.scene.polylines.extend(dd.to_polylines());
25//!     if let Some(pc) = dd.to_point_cloud() {
26//!         frame_data.scene.point_clouds.push(pc);
27//!     }
28//!     frame_data.overlays.labels.extend(dd.to_labels());
29//! }
30//! ```
31//!
32//! # Layers
33//!
34//! Each primitive carries a [`DebugLayer`] tag:
35//!
36//! - [`DebugLayer::Dev`]: dev-mode visuals (contacts, AABBs, normals). Suppressed
37//!   when [`DebugDraw::dev_enabled`] is `false`. Set this to `false` in ship builds.
38//! - [`DebugLayer::Overlay`]: always shown regardless of `dev_enabled`. Use for
39//!   persistent status overlays that should appear in release builds.
40//!
41//! # Lifetime
42//!
43//! Primitives submitted via the `line`, `point`, `aabb`, `sphere`, and `label`
44//! methods are *transient*: cleared by the next [`begin_frame`](DebugDraw::begin_frame)
45//! call. Persistent primitives are added with [`add_persistent`](DebugDraw::add_persistent)
46//! (keyed by a caller-assigned `u64`) and stay until explicitly removed with
47//! [`remove_persistent`](DebugDraw::remove_persistent).
48//!
49//! # Rendering
50//!
51//! `DebugDraw` is a resource accumulator. It does not submit draw calls.
52//! After calling `runtime.step(...)`, convert accumulated primitives to render
53//! items and push them into `FrameData`:
54//!
55//! ```rust,ignore
56//! // After step():
57//! if let Some(dd) = runtime.resources().get::<DebugDraw>() {
58//!     frame_data.scene.polylines.extend(dd.to_polylines());
59//!     if let Some(pc) = dd.to_point_cloud() {
60//!         frame_data.scene.point_clouds.push(pc);
61//!     }
62//!     frame_data.overlays.labels.extend(dd.to_labels());
63//! }
64//! ```
65//!
66//! If this conversion step is omitted, nothing will appear on screen even if
67//! plugins submitted primitives during `step`.
68//!
69//! See `examples/eframe_showcase/showcase_46_debug_draw.rs` for a complete
70//! end-to-end example.
71
72use std::collections::HashMap;
73
74use crate::renderer::{LabelItem, PointCloudItem, PolylineItem};
75
76// ---------------------------------------------------------------------------
77// DebugLayer
78// ---------------------------------------------------------------------------
79
80/// Category of a debug primitive, controlling visibility in ship vs dev builds.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum DebugLayer {
83    /// Shown only in dev/debug mode. Contacts, AABBs, velocity arrows, and
84    /// similar development visuals should use this layer. Suppressed when
85    /// [`DebugDraw::dev_enabled`] is `false`.
86    #[default]
87    Dev,
88    /// Always shown regardless of [`DebugDraw::dev_enabled`]. Use for
89    /// ship-mode status overlays that need to be visible in release builds.
90    Overlay,
91}
92
93// ---------------------------------------------------------------------------
94// DebugPrim
95// ---------------------------------------------------------------------------
96
97/// A single debug draw primitive.
98#[derive(Debug, Clone)]
99pub enum DebugPrim {
100    /// A world-space line segment.
101    Line {
102        /// Start position in world space.
103        start: glam::Vec3,
104        /// End position in world space.
105        end: glam::Vec3,
106        /// RGBA colour in linear space.
107        colour: [f32; 4],
108        /// Layer controlling visibility in ship vs dev modes.
109        layer: DebugLayer,
110    },
111    /// A world-space point marker.
112    Point {
113        /// Position in world space.
114        position: glam::Vec3,
115        /// Screen-space radius in pixels.
116        radius: f32,
117        /// RGBA colour in linear space.
118        colour: [f32; 4],
119        /// Layer controlling visibility in ship vs dev modes.
120        layer: DebugLayer,
121    },
122    /// An axis-aligned bounding box wireframe.
123    Aabb {
124        /// Minimum corner in world space.
125        min: glam::Vec3,
126        /// Maximum corner in world space.
127        max: glam::Vec3,
128        /// RGBA colour in linear space.
129        colour: [f32; 4],
130        /// Layer controlling visibility in ship vs dev modes.
131        layer: DebugLayer,
132    },
133    /// A sphere wireframe drawn as three great circles.
134    Sphere {
135        /// Center position in world space.
136        center: glam::Vec3,
137        /// Sphere radius in world-space units.
138        radius: f32,
139        /// RGBA colour in linear space.
140        colour: [f32; 4],
141        /// Layer controlling visibility in ship vs dev modes.
142        layer: DebugLayer,
143    },
144    /// A text label anchored to a world-space position.
145    Label {
146        /// World-space position of the label anchor.
147        position: glam::Vec3,
148        /// Text content.
149        text: String,
150        /// RGBA colour in linear space.
151        colour: [f32; 4],
152        /// Layer controlling visibility in ship vs dev modes.
153        layer: DebugLayer,
154    },
155}
156
157impl DebugPrim {
158    /// The layer this primitive belongs to.
159    pub fn layer(&self) -> DebugLayer {
160        match self {
161            DebugPrim::Line { layer, .. }
162            | DebugPrim::Point { layer, .. }
163            | DebugPrim::Aabb { layer, .. }
164            | DebugPrim::Sphere { layer, .. }
165            | DebugPrim::Label { layer, .. } => *layer,
166        }
167    }
168}
169
170// ---------------------------------------------------------------------------
171// DebugDraw
172// ---------------------------------------------------------------------------
173
174/// Accumulated debug visuals for the current and future frames.
175///
176/// Store one instance in [`super::RuntimeResources`]. Plugins access it via
177/// `ctx.resources.get_mut::<DebugDraw>()`. Call [`begin_frame`](Self::begin_frame)
178/// before each [`super::ViewportRuntime::step`] call to clear transient primitives.
179///
180/// When both `enabled` and the relevant layer flag are true, submission methods
181/// record primitives into the internal buffers. When disabled they are no-ops,
182/// so no branches are needed in plugin code.
183pub struct DebugDraw {
184    /// Master switch. When `false`, all submission methods are no-ops.
185    pub enabled: bool,
186    /// Controls whether [`DebugLayer::Dev`] primitives are recorded.
187    ///
188    /// Set to `false` in ship builds to suppress development-only visuals
189    /// without changing plugin code. [`DebugLayer::Overlay`] primitives are
190    /// not affected by this flag.
191    pub dev_enabled: bool,
192
193    transient: Vec<DebugPrim>,
194    persistent: HashMap<u64, DebugPrim>,
195}
196
197impl Default for DebugDraw {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203impl DebugDraw {
204    /// Create a new `DebugDraw` with both `enabled` and `dev_enabled` set to `true`.
205    pub fn new() -> Self {
206        Self {
207            enabled: true,
208            dev_enabled: true,
209            transient: Vec::new(),
210            persistent: HashMap::new(),
211        }
212    }
213
214    /// Create a `DebugDraw` with `dev_enabled = false` (overlay-only mode).
215    ///
216    /// Use this in ship builds to suppress dev-layer visuals while keeping
217    /// any overlay-layer primitives active.
218    pub fn overlay_only() -> Self {
219        Self {
220            enabled: true,
221            dev_enabled: false,
222            transient: Vec::new(),
223            persistent: HashMap::new(),
224        }
225    }
226
227    // ---- Lifecycle --------------------------------------------------------
228
229    /// Clear all transient primitives.
230    ///
231    /// Call this before each frame (before [`super::ViewportRuntime::step`]) so
232    /// that one-frame debug draws do not accumulate across frames. Persistent
233    /// primitives (added with [`add_persistent`](Self::add_persistent)) are not
234    /// affected.
235    pub fn begin_frame(&mut self) {
236        self.transient.clear();
237    }
238
239    // ---- Transient submission ---------------------------------------------
240
241    fn accept(&self, layer: DebugLayer) -> bool {
242        self.enabled && (layer == DebugLayer::Overlay || self.dev_enabled)
243    }
244
245    /// Submit a line segment for this frame.
246    ///
247    /// The default layer is [`DebugLayer::Dev`]. Use [`line_overlay`](Self::line_overlay)
248    /// for a line that appears in ship builds.
249    pub fn line(&mut self, start: glam::Vec3, end: glam::Vec3, colour: [f32; 4]) -> &mut Self {
250        self.line_layer(start, end, colour, DebugLayer::Dev)
251    }
252
253    /// Submit a line segment on the [`DebugLayer::Overlay`] layer.
254    pub fn line_overlay(
255        &mut self,
256        start: glam::Vec3,
257        end: glam::Vec3,
258        colour: [f32; 4],
259    ) -> &mut Self {
260        self.line_layer(start, end, colour, DebugLayer::Overlay)
261    }
262
263    /// Submit a line segment on a specific layer.
264    pub fn line_layer(
265        &mut self,
266        start: glam::Vec3,
267        end: glam::Vec3,
268        colour: [f32; 4],
269        layer: DebugLayer,
270    ) -> &mut Self {
271        if self.accept(layer) {
272            self.transient.push(DebugPrim::Line { start, end, colour, layer });
273        }
274        self
275    }
276
277    /// Submit a point marker for this frame.
278    ///
279    /// `radius` is in screen-space pixels.
280    pub fn point(
281        &mut self,
282        position: glam::Vec3,
283        radius: f32,
284        colour: [f32; 4],
285    ) -> &mut Self {
286        self.point_layer(position, radius, colour, DebugLayer::Dev)
287    }
288
289    /// Submit a point marker on the [`DebugLayer::Overlay`] layer.
290    pub fn point_overlay(
291        &mut self,
292        position: glam::Vec3,
293        radius: f32,
294        colour: [f32; 4],
295    ) -> &mut Self {
296        self.point_layer(position, radius, colour, DebugLayer::Overlay)
297    }
298
299    /// Submit a point marker on a specific layer.
300    pub fn point_layer(
301        &mut self,
302        position: glam::Vec3,
303        radius: f32,
304        colour: [f32; 4],
305        layer: DebugLayer,
306    ) -> &mut Self {
307        if self.accept(layer) {
308            self.transient.push(DebugPrim::Point { position, radius, colour, layer });
309        }
310        self
311    }
312
313    /// Submit an axis-aligned box wireframe for this frame.
314    pub fn aabb(
315        &mut self,
316        min: glam::Vec3,
317        max: glam::Vec3,
318        colour: [f32; 4],
319    ) -> &mut Self {
320        self.aabb_layer(min, max, colour, DebugLayer::Dev)
321    }
322
323    /// Submit an AABB wireframe on the [`DebugLayer::Overlay`] layer.
324    pub fn aabb_overlay(
325        &mut self,
326        min: glam::Vec3,
327        max: glam::Vec3,
328        colour: [f32; 4],
329    ) -> &mut Self {
330        self.aabb_layer(min, max, colour, DebugLayer::Overlay)
331    }
332
333    /// Submit an AABB wireframe on a specific layer.
334    pub fn aabb_layer(
335        &mut self,
336        min: glam::Vec3,
337        max: glam::Vec3,
338        colour: [f32; 4],
339        layer: DebugLayer,
340    ) -> &mut Self {
341        if self.accept(layer) {
342            self.transient.push(DebugPrim::Aabb { min, max, colour, layer });
343        }
344        self
345    }
346
347    /// Submit a sphere wireframe for this frame.
348    ///
349    /// The sphere is drawn as three great circles (XY, XZ, and YZ planes).
350    pub fn sphere(
351        &mut self,
352        center: glam::Vec3,
353        radius: f32,
354        colour: [f32; 4],
355    ) -> &mut Self {
356        self.sphere_layer(center, radius, colour, DebugLayer::Dev)
357    }
358
359    /// Submit a sphere wireframe on the [`DebugLayer::Overlay`] layer.
360    pub fn sphere_overlay(
361        &mut self,
362        center: glam::Vec3,
363        radius: f32,
364        colour: [f32; 4],
365    ) -> &mut Self {
366        self.sphere_layer(center, radius, colour, DebugLayer::Overlay)
367    }
368
369    /// Submit a sphere wireframe on a specific layer.
370    pub fn sphere_layer(
371        &mut self,
372        center: glam::Vec3,
373        radius: f32,
374        colour: [f32; 4],
375        layer: DebugLayer,
376    ) -> &mut Self {
377        if self.accept(layer) {
378            self.transient.push(DebugPrim::Sphere { center, radius, colour, layer });
379        }
380        self
381    }
382
383    /// Submit a text label anchored to a world-space position for this frame.
384    pub fn label(
385        &mut self,
386        position: glam::Vec3,
387        text: impl Into<String>,
388        colour: [f32; 4],
389    ) -> &mut Self {
390        self.label_layer(position, text, colour, DebugLayer::Dev)
391    }
392
393    /// Submit a label on the [`DebugLayer::Overlay`] layer.
394    pub fn label_overlay(
395        &mut self,
396        position: glam::Vec3,
397        text: impl Into<String>,
398        colour: [f32; 4],
399    ) -> &mut Self {
400        self.label_layer(position, text, colour, DebugLayer::Overlay)
401    }
402
403    /// Submit a label on a specific layer.
404    pub fn label_layer(
405        &mut self,
406        position: glam::Vec3,
407        text: impl Into<String>,
408        colour: [f32; 4],
409        layer: DebugLayer,
410    ) -> &mut Self {
411        if self.accept(layer) {
412            self.transient.push(DebugPrim::Label {
413                position,
414                text: text.into(),
415                colour,
416                layer,
417            });
418        }
419        self
420    }
421
422    // ---- Persistent primitives --------------------------------------------
423
424    /// Insert or replace a persistent primitive keyed by `id`.
425    ///
426    /// Persistent primitives survive [`begin_frame`](Self::begin_frame) and stay
427    /// until removed with [`remove_persistent`](Self::remove_persistent). The `id`
428    /// is caller-assigned; any `u64` value works as a stable identifier.
429    ///
430    /// Insertion respects the layer flags: if the layer would be suppressed by
431    /// the current `enabled`/`dev_enabled` state, the primitive is still stored
432    /// (so it becomes visible when the flag is re-enabled) but it will be skipped
433    /// during rendering conversion.
434    pub fn add_persistent(&mut self, id: u64, prim: DebugPrim) {
435        self.persistent.insert(id, prim);
436    }
437
438    /// Remove a persistent primitive by `id`.
439    ///
440    /// Returns the removed primitive, or `None` if `id` was not found.
441    pub fn remove_persistent(&mut self, id: u64) -> Option<DebugPrim> {
442        self.persistent.remove(&id)
443    }
444
445    /// True if a persistent primitive with the given `id` exists.
446    pub fn has_persistent(&self, id: u64) -> bool {
447        self.persistent.contains_key(&id)
448    }
449
450    /// Remove all persistent primitives.
451    pub fn clear_persistent(&mut self) {
452        self.persistent.clear();
453    }
454
455    // ---- Iteration --------------------------------------------------------
456
457    /// Iterate over all active primitives (both transient and persistent).
458    ///
459    /// Primitives on the [`DebugLayer::Dev`] layer are included regardless of
460    /// `dev_enabled`; the caller decides whether to filter. Use
461    /// [`to_polylines`](Self::to_polylines) and friends to get renderer-ready
462    /// items with layer filtering already applied.
463    pub fn prims(&self) -> impl Iterator<Item = &DebugPrim> {
464        self.transient.iter().chain(self.persistent.values())
465    }
466
467    /// Number of transient primitives currently accumulated.
468    pub fn transient_count(&self) -> usize {
469        self.transient.len()
470    }
471
472    /// Number of persistent primitives currently stored.
473    pub fn persistent_count(&self) -> usize {
474        self.persistent.len()
475    }
476
477    /// True when no primitives (transient or persistent) are present.
478    pub fn is_empty(&self) -> bool {
479        self.transient.is_empty() && self.persistent.is_empty()
480    }
481
482    // ---- Rendering conversion ---------------------------------------------
483
484    /// Convert accumulated line, AABB, and sphere primitives to polyline render items.
485    ///
486    /// Primitives are grouped by colour to minimize the number of render items.
487    /// Dev-layer primitives are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
488    /// Returns an empty `Vec` when no line-type primitives are present or when `enabled` is `false`.
489    pub fn to_polylines(&self) -> Vec<PolylineItem> {
490        if !self.enabled {
491            return Vec::new();
492        }
493
494        // Bucket positions and strip_lengths by colour key.
495        let mut groups: HashMap<u32, (Vec<[f32; 3]>, Vec<u32>, Vec<[f32; 4]>)> = HashMap::new();
496
497        for prim in self.prims() {
498            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
499                continue;
500            }
501            match prim {
502                DebugPrim::Line { start, end, colour, .. } => {
503                    let key = colour_key(*colour);
504                    let entry = groups.entry(key).or_default();
505                    entry.0.push((*start).into());
506                    entry.0.push((*end).into());
507                    entry.1.push(2);
508                    entry.2.push(*colour);
509                    entry.2.push(*colour);
510                }
511                DebugPrim::Aabb { min, max, colour, .. } => {
512                    let key = colour_key(*colour);
513                    let entry = groups.entry(key).or_default();
514                    let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
515                    // 6 strips: bottom loop (5), top loop (5), 4 vertical edges (2 each)
516                    let mn = *min;
517                    let mx = *max;
518                    let aabb_positions: &[[f32; 3]] = &[
519                        // Bottom face loop
520                        [mn.x, mn.y, mn.z],
521                        [mx.x, mn.y, mn.z],
522                        [mx.x, mx.y, mn.z],
523                        [mn.x, mx.y, mn.z],
524                        [mn.x, mn.y, mn.z],
525                        // Top face loop
526                        [mn.x, mn.y, mx.z],
527                        [mx.x, mn.y, mx.z],
528                        [mx.x, mx.y, mx.z],
529                        [mn.x, mx.y, mx.z],
530                        [mn.x, mn.y, mx.z],
531                        // Vertical edges
532                        [mn.x, mn.y, mn.z],
533                        [mn.x, mn.y, mx.z],
534                        [mx.x, mn.y, mn.z],
535                        [mx.x, mn.y, mx.z],
536                        [mx.x, mx.y, mn.z],
537                        [mx.x, mx.y, mx.z],
538                        [mn.x, mx.y, mn.z],
539                        [mn.x, mx.y, mx.z],
540                    ];
541                    let strip_lens: &[u32] = &[5, 5, 2, 2, 2, 2];
542                    for &p in aabb_positions {
543                        positions.push(p);
544                        colours.push(*colour);
545                    }
546                    strips.extend_from_slice(strip_lens);
547                }
548                DebugPrim::Sphere { center, radius, colour, .. } => {
549                    let key = colour_key(*colour);
550                    let entry = groups.entry(key).or_default();
551                    let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
552                    const SEGS: usize = 32;
553                    // Three great circles: XY, XZ, YZ planes.
554                    for circle_axis in 0..3_usize {
555                        for i in 0..=SEGS {
556                            let t = i as f32 / SEGS as f32 * std::f32::consts::TAU;
557                            let (s, c) = (t.sin(), t.cos());
558                            let p = match circle_axis {
559                                0 => glam::Vec3::new(c * *radius + center.x, s * *radius + center.y, center.z),
560                                1 => glam::Vec3::new(c * *radius + center.x, center.y, s * *radius + center.z),
561                                _ => glam::Vec3::new(center.x, c * *radius + center.y, s * *radius + center.z),
562                            };
563                            positions.push(p.into());
564                            colours.push(*colour);
565                        }
566                        strips.push((SEGS + 1) as u32);
567                    }
568                }
569                _ => {} // Points and Labels handled separately.
570            }
571        }
572
573        groups
574            .into_values()
575            .filter(|(positions, _, _)| !positions.is_empty())
576            .map(|(positions, strip_lengths, node_colours)| PolylineItem {
577                positions,
578                strip_lengths,
579                node_colours,
580                line_width: 1.5,
581                ..PolylineItem::default()
582            })
583            .collect()
584    }
585
586    /// Convert accumulated point primitives to a single point cloud render item.
587    ///
588    /// Returns `None` when no point primitives are present or when `enabled` is `false`.
589    /// Dev-layer points are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
590    pub fn to_point_cloud(&self) -> Option<PointCloudItem> {
591        if !self.enabled {
592            return None;
593        }
594
595        let mut positions = Vec::new();
596        let mut colours = Vec::new();
597        let mut radii = Vec::new();
598
599        for prim in self.prims() {
600            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
601                continue;
602            }
603            if let DebugPrim::Point { position, radius, colour, .. } = prim {
604                positions.push((*position).into());
605                colours.push(*colour);
606                radii.push(*radius);
607            }
608        }
609
610        if positions.is_empty() {
611            return None;
612        }
613
614        Some(PointCloudItem {
615            positions,
616            colours,
617            radii,
618            ..PointCloudItem::default()
619        })
620    }
621
622    /// Convert accumulated label primitives to label render items.
623    ///
624    /// Returns an empty `Vec` when no label primitives are present or when `enabled` is `false`.
625    /// Dev-layer labels are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
626    pub fn to_labels(&self) -> Vec<LabelItem> {
627        if !self.enabled {
628            return Vec::new();
629        }
630
631        let mut out = Vec::new();
632        for prim in self.prims() {
633            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
634                continue;
635            }
636            if let DebugPrim::Label { position, text, colour, .. } = prim {
637                out.push(LabelItem {
638                    world_anchor: Some((*position).into()),
639                    text: text.clone(),
640                    colour: *colour,
641                    leader_line: true,
642                    font_size: 12.0,
643                    ..LabelItem::default()
644                });
645            }
646        }
647        out
648    }
649}
650
651// ---------------------------------------------------------------------------
652// Internal helpers
653// ---------------------------------------------------------------------------
654
655/// Convert a float RGBA colour to a u32 key for grouping.
656fn colour_key(c: [f32; 4]) -> u32 {
657    let r = (c[0].clamp(0.0, 1.0) * 255.0).round() as u32;
658    let g = (c[1].clamp(0.0, 1.0) * 255.0).round() as u32;
659    let b = (c[2].clamp(0.0, 1.0) * 255.0).round() as u32;
660    let a = (c[3].clamp(0.0, 1.0) * 255.0).round() as u32;
661    (r << 24) | (g << 16) | (b << 8) | a
662}
663
664// ---------------------------------------------------------------------------
665// Tests
666// ---------------------------------------------------------------------------
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    fn red() -> [f32; 4] { [1.0, 0.0, 0.0, 1.0] }
673    fn green() -> [f32; 4] { [0.0, 1.0, 0.0, 1.0] }
674
675    // ---- accumulation tests -----------------------------------------------
676
677    #[test]
678    fn test_line_accumulates() {
679        let mut dd = DebugDraw::new();
680        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
681        assert_eq!(dd.transient_count(), 1);
682        assert!(matches!(dd.prims().next(), Some(DebugPrim::Line { .. })));
683    }
684
685    #[test]
686    fn test_point_accumulates() {
687        let mut dd = DebugDraw::new();
688        dd.point(glam::Vec3::Y, 4.0, green());
689        assert_eq!(dd.transient_count(), 1);
690        assert!(matches!(dd.prims().next(), Some(DebugPrim::Point { .. })));
691    }
692
693    #[test]
694    fn test_aabb_accumulates() {
695        let mut dd = DebugDraw::new();
696        dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
697        assert_eq!(dd.transient_count(), 1);
698    }
699
700    #[test]
701    fn test_sphere_accumulates() {
702        let mut dd = DebugDraw::new();
703        dd.sphere(glam::Vec3::ZERO, 1.0, green());
704        assert_eq!(dd.transient_count(), 1);
705    }
706
707    #[test]
708    fn test_label_accumulates() {
709        let mut dd = DebugDraw::new();
710        dd.label(glam::Vec3::ZERO, "hello", red());
711        assert_eq!(dd.transient_count(), 1);
712        assert!(matches!(dd.prims().next(), Some(DebugPrim::Label { .. })));
713    }
714
715    #[test]
716    fn test_multiple_prims_accumulate() {
717        let mut dd = DebugDraw::new();
718        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
719        dd.line(glam::Vec3::ZERO, glam::Vec3::Y, green());
720        dd.point(glam::Vec3::Z, 5.0, red());
721        assert_eq!(dd.transient_count(), 3);
722    }
723
724    // ---- lifetime tests ---------------------------------------------------
725
726    #[test]
727    fn test_begin_frame_clears_transient() {
728        let mut dd = DebugDraw::new();
729        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
730        dd.sphere(glam::Vec3::ZERO, 1.0, green());
731        assert_eq!(dd.transient_count(), 2);
732
733        dd.begin_frame();
734        assert_eq!(dd.transient_count(), 0);
735        assert!(dd.is_empty());
736    }
737
738    #[test]
739    fn test_persistent_survives_begin_frame() {
740        let mut dd = DebugDraw::new();
741        dd.add_persistent(1, DebugPrim::Line {
742            start: glam::Vec3::ZERO,
743            end: glam::Vec3::X,
744            colour: red(),
745            layer: DebugLayer::Dev,
746        });
747        assert_eq!(dd.persistent_count(), 1);
748
749        dd.begin_frame();
750        assert_eq!(dd.transient_count(), 0);
751        assert_eq!(dd.persistent_count(), 1);
752        assert!(!dd.is_empty());
753    }
754
755    #[test]
756    fn test_persistent_removed_explicitly() {
757        let mut dd = DebugDraw::new();
758        dd.add_persistent(42, DebugPrim::Sphere {
759            center: glam::Vec3::ZERO,
760            radius: 1.0,
761            colour: green(),
762            layer: DebugLayer::Dev,
763        });
764        assert!(dd.has_persistent(42));
765
766        let removed = dd.remove_persistent(42);
767        assert!(removed.is_some());
768        assert!(!dd.has_persistent(42));
769        assert_eq!(dd.persistent_count(), 0);
770    }
771
772    #[test]
773    fn test_persistent_remove_missing_returns_none() {
774        let mut dd = DebugDraw::new();
775        assert!(dd.remove_persistent(999).is_none());
776    }
777
778    #[test]
779    fn test_persistent_and_transient_both_in_prims() {
780        let mut dd = DebugDraw::new();
781        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
782        dd.add_persistent(1, DebugPrim::Sphere {
783            center: glam::Vec3::ZERO,
784            radius: 1.0,
785            colour: green(),
786            layer: DebugLayer::Dev,
787        });
788        assert_eq!(dd.prims().count(), 2);
789    }
790
791    #[test]
792    fn test_clear_persistent() {
793        let mut dd = DebugDraw::new();
794        dd.add_persistent(1, DebugPrim::Point { position: glam::Vec3::ZERO, radius: 4.0, colour: red(), layer: DebugLayer::Dev });
795        dd.add_persistent(2, DebugPrim::Point { position: glam::Vec3::X, radius: 4.0, colour: green(), layer: DebugLayer::Dev });
796        dd.clear_persistent();
797        assert_eq!(dd.persistent_count(), 0);
798    }
799
800    // ---- disabled / no-op fallback tests ----------------------------------
801
802    #[test]
803    fn test_enabled_false_is_noop() {
804        let mut dd = DebugDraw::new();
805        dd.enabled = false;
806        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
807        dd.sphere(glam::Vec3::ZERO, 1.0, green());
808        dd.label(glam::Vec3::ZERO, "text", red());
809        assert_eq!(dd.transient_count(), 0);
810        assert!(dd.to_polylines().is_empty());
811        assert!(dd.to_point_cloud().is_none());
812        assert!(dd.to_labels().is_empty());
813    }
814
815    #[test]
816    fn test_dev_disabled_suppresses_dev_prims_in_output() {
817        let mut dd = DebugDraw::new();
818        dd.dev_enabled = false;
819
820        // Dev-layer line: should not appear in submission output.
821        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red()); // uses Dev layer by default
822        // The method is a no-op when dev_enabled is false.
823        assert_eq!(dd.transient_count(), 0);
824        assert!(dd.to_polylines().is_empty());
825    }
826
827    #[test]
828    fn test_overlay_layer_shown_when_dev_disabled() {
829        let mut dd = DebugDraw::new();
830        dd.dev_enabled = false;
831
832        // Overlay-layer line should still be submitted.
833        dd.line_overlay(glam::Vec3::ZERO, glam::Vec3::X, red());
834        assert_eq!(dd.transient_count(), 1);
835        assert!(!dd.to_polylines().is_empty());
836    }
837
838    #[test]
839    fn test_persistent_dev_skipped_in_rendering_when_dev_disabled() {
840        let mut dd = DebugDraw::new();
841        // Add a persistent dev-layer line when dev_enabled is true.
842        dd.add_persistent(1, DebugPrim::Line {
843            start: glam::Vec3::ZERO,
844            end: glam::Vec3::X,
845            colour: red(),
846            layer: DebugLayer::Dev,
847        });
848        // Now disable dev.
849        dd.dev_enabled = false;
850        // The persistent primitive is still stored but should not appear in to_polylines.
851        assert_eq!(dd.persistent_count(), 1);
852        assert!(dd.to_polylines().is_empty());
853    }
854
855    // ---- rendering conversion tests ---------------------------------------
856
857    #[test]
858    fn test_to_polylines_produces_output_for_lines() {
859        let mut dd = DebugDraw::new();
860        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
861        let polylines = dd.to_polylines();
862        assert!(!polylines.is_empty());
863        let total_positions: usize = polylines.iter().map(|p| p.positions.len()).sum();
864        assert_eq!(total_positions, 2);
865    }
866
867    #[test]
868    fn test_to_polylines_aabb_18_positions() {
869        let mut dd = DebugDraw::new();
870        dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
871        let polylines = dd.to_polylines();
872        assert!(!polylines.is_empty());
873        // The AABB wireframe has 18 positions (5+5+2+2+2+2).
874        let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
875        assert_eq!(total, 18);
876    }
877
878    #[test]
879    fn test_to_polylines_sphere_three_circles() {
880        let mut dd = DebugDraw::new();
881        dd.sphere(glam::Vec3::ZERO, 1.0, green());
882        let polylines = dd.to_polylines();
883        assert!(!polylines.is_empty());
884        // 3 circles * 33 points each (32 segs + close).
885        let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
886        assert_eq!(total, 3 * 33);
887    }
888
889    #[test]
890    fn test_to_point_cloud_produces_output() {
891        let mut dd = DebugDraw::new();
892        dd.point(glam::Vec3::ZERO, 5.0, red());
893        dd.point(glam::Vec3::X, 3.0, green());
894        let pc = dd.to_point_cloud();
895        assert!(pc.is_some());
896        let pc = pc.unwrap();
897        assert_eq!(pc.positions.len(), 2);
898        assert_eq!(pc.colours.len(), 2);
899        assert_eq!(pc.radii.len(), 2);
900    }
901
902    #[test]
903    fn test_to_labels_produces_output() {
904        let mut dd = DebugDraw::new();
905        dd.label(glam::Vec3::ZERO, "A", red());
906        dd.label(glam::Vec3::X, "B", green());
907        let labels = dd.to_labels();
908        assert_eq!(labels.len(), 2);
909        assert_eq!(labels[0].text, "A");
910        assert_eq!(labels[1].text, "B");
911    }
912
913    #[test]
914    fn test_same_colour_grouped_into_one_polyline() {
915        let mut dd = DebugDraw::new();
916        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
917        dd.line(glam::Vec3::Y, glam::Vec3::Z, red());
918        // Both lines share the same colour; they should be in a single PolylineItem.
919        let polylines = dd.to_polylines();
920        assert_eq!(polylines.len(), 1);
921        assert_eq!(polylines[0].positions.len(), 4);
922        assert_eq!(polylines[0].strip_lengths, vec![2, 2]);
923    }
924
925    #[test]
926    fn test_different_colours_produce_separate_polylines() {
927        let mut dd = DebugDraw::new();
928        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
929        dd.line(glam::Vec3::Y, glam::Vec3::Z, green());
930        let polylines = dd.to_polylines();
931        assert_eq!(polylines.len(), 2);
932    }
933}