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 {
273                start,
274                end,
275                colour,
276                layer,
277            });
278        }
279        self
280    }
281
282    /// Submit a point marker for this frame.
283    ///
284    /// `radius` is in screen-space pixels.
285    pub fn point(&mut self, position: glam::Vec3, radius: f32, colour: [f32; 4]) -> &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 {
309                position,
310                radius,
311                colour,
312                layer,
313            });
314        }
315        self
316    }
317
318    /// Submit an axis-aligned box wireframe for this frame.
319    pub fn aabb(&mut self, min: glam::Vec3, max: glam::Vec3, colour: [f32; 4]) -> &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 {
343                min,
344                max,
345                colour,
346                layer,
347            });
348        }
349        self
350    }
351
352    /// Submit a sphere wireframe for this frame.
353    ///
354    /// The sphere is drawn as three great circles (XY, XZ, and YZ planes).
355    pub fn sphere(&mut self, center: glam::Vec3, radius: f32, colour: [f32; 4]) -> &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 {
379                center,
380                radius,
381                colour,
382                layer,
383            });
384        }
385        self
386    }
387
388    /// Submit a text label anchored to a world-space position for this frame.
389    pub fn label(
390        &mut self,
391        position: glam::Vec3,
392        text: impl Into<String>,
393        colour: [f32; 4],
394    ) -> &mut Self {
395        self.label_layer(position, text, colour, DebugLayer::Dev)
396    }
397
398    /// Submit a label on the [`DebugLayer::Overlay`] layer.
399    pub fn label_overlay(
400        &mut self,
401        position: glam::Vec3,
402        text: impl Into<String>,
403        colour: [f32; 4],
404    ) -> &mut Self {
405        self.label_layer(position, text, colour, DebugLayer::Overlay)
406    }
407
408    /// Submit a label on a specific layer.
409    pub fn label_layer(
410        &mut self,
411        position: glam::Vec3,
412        text: impl Into<String>,
413        colour: [f32; 4],
414        layer: DebugLayer,
415    ) -> &mut Self {
416        if self.accept(layer) {
417            self.transient.push(DebugPrim::Label {
418                position,
419                text: text.into(),
420                colour,
421                layer,
422            });
423        }
424        self
425    }
426
427    // ---- Persistent primitives --------------------------------------------
428
429    /// Insert or replace a persistent primitive keyed by `id`.
430    ///
431    /// Persistent primitives survive [`begin_frame`](Self::begin_frame) and stay
432    /// until removed with [`remove_persistent`](Self::remove_persistent). The `id`
433    /// is caller-assigned; any `u64` value works as a stable identifier.
434    ///
435    /// Insertion respects the layer flags: if the layer would be suppressed by
436    /// the current `enabled`/`dev_enabled` state, the primitive is still stored
437    /// (so it becomes visible when the flag is re-enabled) but it will be skipped
438    /// during rendering conversion.
439    pub fn add_persistent(&mut self, id: u64, prim: DebugPrim) {
440        self.persistent.insert(id, prim);
441    }
442
443    /// Remove a persistent primitive by `id`.
444    ///
445    /// Returns the removed primitive, or `None` if `id` was not found.
446    pub fn remove_persistent(&mut self, id: u64) -> Option<DebugPrim> {
447        self.persistent.remove(&id)
448    }
449
450    /// True if a persistent primitive with the given `id` exists.
451    pub fn has_persistent(&self, id: u64) -> bool {
452        self.persistent.contains_key(&id)
453    }
454
455    /// Remove all persistent primitives.
456    pub fn clear_persistent(&mut self) {
457        self.persistent.clear();
458    }
459
460    // ---- Iteration --------------------------------------------------------
461
462    /// Iterate over all active primitives (both transient and persistent).
463    ///
464    /// Primitives on the [`DebugLayer::Dev`] layer are included regardless of
465    /// `dev_enabled`; the caller decides whether to filter. Use
466    /// [`to_polylines`](Self::to_polylines) and friends to get renderer-ready
467    /// items with layer filtering already applied.
468    pub fn prims(&self) -> impl Iterator<Item = &DebugPrim> {
469        self.transient.iter().chain(self.persistent.values())
470    }
471
472    /// Number of transient primitives currently accumulated.
473    pub fn transient_count(&self) -> usize {
474        self.transient.len()
475    }
476
477    /// Number of persistent primitives currently stored.
478    pub fn persistent_count(&self) -> usize {
479        self.persistent.len()
480    }
481
482    /// True when no primitives (transient or persistent) are present.
483    pub fn is_empty(&self) -> bool {
484        self.transient.is_empty() && self.persistent.is_empty()
485    }
486
487    // ---- Rendering conversion ---------------------------------------------
488
489    /// Convert accumulated line, AABB, and sphere primitives to polyline render items.
490    ///
491    /// Primitives are grouped by colour to minimize the number of render items.
492    /// Dev-layer primitives are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
493    /// Returns an empty `Vec` when no line-type primitives are present or when `enabled` is `false`.
494    pub fn to_polylines(&self) -> Vec<PolylineItem> {
495        if !self.enabled {
496            return Vec::new();
497        }
498
499        // Bucket positions and strip_lengths by colour key.
500        let mut groups: HashMap<u32, (Vec<[f32; 3]>, Vec<u32>, Vec<[f32; 4]>)> = HashMap::new();
501
502        for prim in self.prims() {
503            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
504                continue;
505            }
506            match prim {
507                DebugPrim::Line {
508                    start, end, colour, ..
509                } => {
510                    let key = colour_key(*colour);
511                    let entry = groups.entry(key).or_default();
512                    entry.0.push((*start).into());
513                    entry.0.push((*end).into());
514                    entry.1.push(2);
515                    entry.2.push(*colour);
516                    entry.2.push(*colour);
517                }
518                DebugPrim::Aabb {
519                    min, max, colour, ..
520                } => {
521                    let key = colour_key(*colour);
522                    let entry = groups.entry(key).or_default();
523                    let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
524                    // 6 strips: bottom loop (5), top loop (5), 4 vertical edges (2 each)
525                    let mn = *min;
526                    let mx = *max;
527                    let aabb_positions: &[[f32; 3]] = &[
528                        // Bottom face loop
529                        [mn.x, mn.y, mn.z],
530                        [mx.x, mn.y, mn.z],
531                        [mx.x, mx.y, mn.z],
532                        [mn.x, mx.y, mn.z],
533                        [mn.x, mn.y, mn.z],
534                        // Top face loop
535                        [mn.x, mn.y, mx.z],
536                        [mx.x, mn.y, mx.z],
537                        [mx.x, mx.y, mx.z],
538                        [mn.x, mx.y, mx.z],
539                        [mn.x, mn.y, mx.z],
540                        // Vertical edges
541                        [mn.x, mn.y, mn.z],
542                        [mn.x, mn.y, mx.z],
543                        [mx.x, mn.y, mn.z],
544                        [mx.x, mn.y, mx.z],
545                        [mx.x, mx.y, mn.z],
546                        [mx.x, mx.y, mx.z],
547                        [mn.x, mx.y, mn.z],
548                        [mn.x, mx.y, mx.z],
549                    ];
550                    let strip_lens: &[u32] = &[5, 5, 2, 2, 2, 2];
551                    for &p in aabb_positions {
552                        positions.push(p);
553                        colours.push(*colour);
554                    }
555                    strips.extend_from_slice(strip_lens);
556                }
557                DebugPrim::Sphere {
558                    center,
559                    radius,
560                    colour,
561                    ..
562                } => {
563                    let key = colour_key(*colour);
564                    let entry = groups.entry(key).or_default();
565                    let (positions, strips, colours) = (&mut entry.0, &mut entry.1, &mut entry.2);
566                    const SEGS: usize = 32;
567                    // Three great circles: XY, XZ, YZ planes.
568                    for circle_axis in 0..3_usize {
569                        for i in 0..=SEGS {
570                            let t = i as f32 / SEGS as f32 * std::f32::consts::TAU;
571                            let (s, c) = (t.sin(), t.cos());
572                            let p = match circle_axis {
573                                0 => glam::Vec3::new(
574                                    c * *radius + center.x,
575                                    s * *radius + center.y,
576                                    center.z,
577                                ),
578                                1 => glam::Vec3::new(
579                                    c * *radius + center.x,
580                                    center.y,
581                                    s * *radius + center.z,
582                                ),
583                                _ => glam::Vec3::new(
584                                    center.x,
585                                    c * *radius + center.y,
586                                    s * *radius + center.z,
587                                ),
588                            };
589                            positions.push(p.into());
590                            colours.push(*colour);
591                        }
592                        strips.push((SEGS + 1) as u32);
593                    }
594                }
595                _ => {} // Points and Labels handled separately.
596            }
597        }
598
599        groups
600            .into_values()
601            .filter(|(positions, _, _)| !positions.is_empty())
602            .map(|(positions, strip_lengths, node_colours)| PolylineItem {
603                positions,
604                strip_lengths,
605                node_colours,
606                line_width: 1.5,
607                ..PolylineItem::default()
608            })
609            .collect()
610    }
611
612    /// Convert accumulated point primitives to a single point cloud render item.
613    ///
614    /// Returns `None` when no point primitives are present or when `enabled` is `false`.
615    /// Dev-layer points are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
616    pub fn to_point_cloud(&self) -> Option<PointCloudItem> {
617        if !self.enabled {
618            return None;
619        }
620
621        let mut positions = Vec::new();
622        let mut colours = Vec::new();
623        let mut radii = Vec::new();
624
625        for prim in self.prims() {
626            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
627                continue;
628            }
629            if let DebugPrim::Point {
630                position,
631                radius,
632                colour,
633                ..
634            } = prim
635            {
636                positions.push((*position).into());
637                colours.push(*colour);
638                radii.push(*radius);
639            }
640        }
641
642        if positions.is_empty() {
643            return None;
644        }
645
646        Some(PointCloudItem {
647            positions,
648            colours,
649            radii,
650            ..PointCloudItem::default()
651        })
652    }
653
654    /// Convert accumulated label primitives to label render items.
655    ///
656    /// Returns an empty `Vec` when no label primitives are present or when `enabled` is `false`.
657    /// Dev-layer labels are skipped when [`dev_enabled`](Self::dev_enabled) is `false`.
658    pub fn to_labels(&self) -> Vec<LabelItem> {
659        if !self.enabled {
660            return Vec::new();
661        }
662
663        let mut out = Vec::new();
664        for prim in self.prims() {
665            if prim.layer() == DebugLayer::Dev && !self.dev_enabled {
666                continue;
667            }
668            if let DebugPrim::Label {
669                position,
670                text,
671                colour,
672                ..
673            } = prim
674            {
675                out.push(LabelItem {
676                    world_anchor: Some((*position).into()),
677                    text: text.clone(),
678                    colour: *colour,
679                    leader_line: true,
680                    font_size: 12.0,
681                    ..LabelItem::default()
682                });
683            }
684        }
685        out
686    }
687}
688
689// ---------------------------------------------------------------------------
690// Internal helpers
691// ---------------------------------------------------------------------------
692
693/// Convert a float RGBA colour to a u32 key for grouping.
694fn colour_key(c: [f32; 4]) -> u32 {
695    let r = (c[0].clamp(0.0, 1.0) * 255.0).round() as u32;
696    let g = (c[1].clamp(0.0, 1.0) * 255.0).round() as u32;
697    let b = (c[2].clamp(0.0, 1.0) * 255.0).round() as u32;
698    let a = (c[3].clamp(0.0, 1.0) * 255.0).round() as u32;
699    (r << 24) | (g << 16) | (b << 8) | a
700}
701
702// ---------------------------------------------------------------------------
703// Tests
704// ---------------------------------------------------------------------------
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    fn red() -> [f32; 4] {
711        [1.0, 0.0, 0.0, 1.0]
712    }
713    fn green() -> [f32; 4] {
714        [0.0, 1.0, 0.0, 1.0]
715    }
716
717    // ---- accumulation tests -----------------------------------------------
718
719    #[test]
720    fn test_line_accumulates() {
721        let mut dd = DebugDraw::new();
722        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
723        assert_eq!(dd.transient_count(), 1);
724        assert!(matches!(dd.prims().next(), Some(DebugPrim::Line { .. })));
725    }
726
727    #[test]
728    fn test_point_accumulates() {
729        let mut dd = DebugDraw::new();
730        dd.point(glam::Vec3::Y, 4.0, green());
731        assert_eq!(dd.transient_count(), 1);
732        assert!(matches!(dd.prims().next(), Some(DebugPrim::Point { .. })));
733    }
734
735    #[test]
736    fn test_aabb_accumulates() {
737        let mut dd = DebugDraw::new();
738        dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
739        assert_eq!(dd.transient_count(), 1);
740    }
741
742    #[test]
743    fn test_sphere_accumulates() {
744        let mut dd = DebugDraw::new();
745        dd.sphere(glam::Vec3::ZERO, 1.0, green());
746        assert_eq!(dd.transient_count(), 1);
747    }
748
749    #[test]
750    fn test_label_accumulates() {
751        let mut dd = DebugDraw::new();
752        dd.label(glam::Vec3::ZERO, "hello", red());
753        assert_eq!(dd.transient_count(), 1);
754        assert!(matches!(dd.prims().next(), Some(DebugPrim::Label { .. })));
755    }
756
757    #[test]
758    fn test_multiple_prims_accumulate() {
759        let mut dd = DebugDraw::new();
760        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
761        dd.line(glam::Vec3::ZERO, glam::Vec3::Y, green());
762        dd.point(glam::Vec3::Z, 5.0, red());
763        assert_eq!(dd.transient_count(), 3);
764    }
765
766    // ---- lifetime tests ---------------------------------------------------
767
768    #[test]
769    fn test_begin_frame_clears_transient() {
770        let mut dd = DebugDraw::new();
771        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
772        dd.sphere(glam::Vec3::ZERO, 1.0, green());
773        assert_eq!(dd.transient_count(), 2);
774
775        dd.begin_frame();
776        assert_eq!(dd.transient_count(), 0);
777        assert!(dd.is_empty());
778    }
779
780    #[test]
781    fn test_persistent_survives_begin_frame() {
782        let mut dd = DebugDraw::new();
783        dd.add_persistent(
784            1,
785            DebugPrim::Line {
786                start: glam::Vec3::ZERO,
787                end: glam::Vec3::X,
788                colour: red(),
789                layer: DebugLayer::Dev,
790            },
791        );
792        assert_eq!(dd.persistent_count(), 1);
793
794        dd.begin_frame();
795        assert_eq!(dd.transient_count(), 0);
796        assert_eq!(dd.persistent_count(), 1);
797        assert!(!dd.is_empty());
798    }
799
800    #[test]
801    fn test_persistent_removed_explicitly() {
802        let mut dd = DebugDraw::new();
803        dd.add_persistent(
804            42,
805            DebugPrim::Sphere {
806                center: glam::Vec3::ZERO,
807                radius: 1.0,
808                colour: green(),
809                layer: DebugLayer::Dev,
810            },
811        );
812        assert!(dd.has_persistent(42));
813
814        let removed = dd.remove_persistent(42);
815        assert!(removed.is_some());
816        assert!(!dd.has_persistent(42));
817        assert_eq!(dd.persistent_count(), 0);
818    }
819
820    #[test]
821    fn test_persistent_remove_missing_returns_none() {
822        let mut dd = DebugDraw::new();
823        assert!(dd.remove_persistent(999).is_none());
824    }
825
826    #[test]
827    fn test_persistent_and_transient_both_in_prims() {
828        let mut dd = DebugDraw::new();
829        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
830        dd.add_persistent(
831            1,
832            DebugPrim::Sphere {
833                center: glam::Vec3::ZERO,
834                radius: 1.0,
835                colour: green(),
836                layer: DebugLayer::Dev,
837            },
838        );
839        assert_eq!(dd.prims().count(), 2);
840    }
841
842    #[test]
843    fn test_clear_persistent() {
844        let mut dd = DebugDraw::new();
845        dd.add_persistent(
846            1,
847            DebugPrim::Point {
848                position: glam::Vec3::ZERO,
849                radius: 4.0,
850                colour: red(),
851                layer: DebugLayer::Dev,
852            },
853        );
854        dd.add_persistent(
855            2,
856            DebugPrim::Point {
857                position: glam::Vec3::X,
858                radius: 4.0,
859                colour: green(),
860                layer: DebugLayer::Dev,
861            },
862        );
863        dd.clear_persistent();
864        assert_eq!(dd.persistent_count(), 0);
865    }
866
867    // ---- disabled / no-op fallback tests ----------------------------------
868
869    #[test]
870    fn test_enabled_false_is_noop() {
871        let mut dd = DebugDraw::new();
872        dd.enabled = false;
873        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
874        dd.sphere(glam::Vec3::ZERO, 1.0, green());
875        dd.label(glam::Vec3::ZERO, "text", red());
876        assert_eq!(dd.transient_count(), 0);
877        assert!(dd.to_polylines().is_empty());
878        assert!(dd.to_point_cloud().is_none());
879        assert!(dd.to_labels().is_empty());
880    }
881
882    #[test]
883    fn test_dev_disabled_suppresses_dev_prims_in_output() {
884        let mut dd = DebugDraw::new();
885        dd.dev_enabled = false;
886
887        // Dev-layer line: should not appear in submission output.
888        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red()); // uses Dev layer by default
889        // The method is a no-op when dev_enabled is false.
890        assert_eq!(dd.transient_count(), 0);
891        assert!(dd.to_polylines().is_empty());
892    }
893
894    #[test]
895    fn test_overlay_layer_shown_when_dev_disabled() {
896        let mut dd = DebugDraw::new();
897        dd.dev_enabled = false;
898
899        // Overlay-layer line should still be submitted.
900        dd.line_overlay(glam::Vec3::ZERO, glam::Vec3::X, red());
901        assert_eq!(dd.transient_count(), 1);
902        assert!(!dd.to_polylines().is_empty());
903    }
904
905    #[test]
906    fn test_persistent_dev_skipped_in_rendering_when_dev_disabled() {
907        let mut dd = DebugDraw::new();
908        // Add a persistent dev-layer line when dev_enabled is true.
909        dd.add_persistent(
910            1,
911            DebugPrim::Line {
912                start: glam::Vec3::ZERO,
913                end: glam::Vec3::X,
914                colour: red(),
915                layer: DebugLayer::Dev,
916            },
917        );
918        // Now disable dev.
919        dd.dev_enabled = false;
920        // The persistent primitive is still stored but should not appear in to_polylines.
921        assert_eq!(dd.persistent_count(), 1);
922        assert!(dd.to_polylines().is_empty());
923    }
924
925    // ---- rendering conversion tests ---------------------------------------
926
927    #[test]
928    fn test_to_polylines_produces_output_for_lines() {
929        let mut dd = DebugDraw::new();
930        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
931        let polylines = dd.to_polylines();
932        assert!(!polylines.is_empty());
933        let total_positions: usize = polylines.iter().map(|p| p.positions.len()).sum();
934        assert_eq!(total_positions, 2);
935    }
936
937    #[test]
938    fn test_to_polylines_aabb_18_positions() {
939        let mut dd = DebugDraw::new();
940        dd.aabb(glam::Vec3::NEG_ONE, glam::Vec3::ONE, red());
941        let polylines = dd.to_polylines();
942        assert!(!polylines.is_empty());
943        // The AABB wireframe has 18 positions (5+5+2+2+2+2).
944        let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
945        assert_eq!(total, 18);
946    }
947
948    #[test]
949    fn test_to_polylines_sphere_three_circles() {
950        let mut dd = DebugDraw::new();
951        dd.sphere(glam::Vec3::ZERO, 1.0, green());
952        let polylines = dd.to_polylines();
953        assert!(!polylines.is_empty());
954        // 3 circles * 33 points each (32 segs + close).
955        let total: usize = polylines.iter().map(|p| p.positions.len()).sum();
956        assert_eq!(total, 3 * 33);
957    }
958
959    #[test]
960    fn test_to_point_cloud_produces_output() {
961        let mut dd = DebugDraw::new();
962        dd.point(glam::Vec3::ZERO, 5.0, red());
963        dd.point(glam::Vec3::X, 3.0, green());
964        let pc = dd.to_point_cloud();
965        assert!(pc.is_some());
966        let pc = pc.unwrap();
967        assert_eq!(pc.positions.len(), 2);
968        assert_eq!(pc.colours.len(), 2);
969        assert_eq!(pc.radii.len(), 2);
970    }
971
972    #[test]
973    fn test_to_labels_produces_output() {
974        let mut dd = DebugDraw::new();
975        dd.label(glam::Vec3::ZERO, "A", red());
976        dd.label(glam::Vec3::X, "B", green());
977        let labels = dd.to_labels();
978        assert_eq!(labels.len(), 2);
979        assert_eq!(labels[0].text, "A");
980        assert_eq!(labels[1].text, "B");
981    }
982
983    #[test]
984    fn test_same_colour_grouped_into_one_polyline() {
985        let mut dd = DebugDraw::new();
986        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
987        dd.line(glam::Vec3::Y, glam::Vec3::Z, red());
988        // Both lines share the same colour; they should be in a single PolylineItem.
989        let polylines = dd.to_polylines();
990        assert_eq!(polylines.len(), 1);
991        assert_eq!(polylines[0].positions.len(), 4);
992        assert_eq!(polylines[0].strip_lengths, vec![2, 2]);
993    }
994
995    #[test]
996    fn test_different_colours_produce_separate_polylines() {
997        let mut dd = DebugDraw::new();
998        dd.line(glam::Vec3::ZERO, glam::Vec3::X, red());
999        dd.line(glam::Vec3::Y, glam::Vec3::Z, green());
1000        let polylines = dd.to_polylines();
1001        assert_eq!(polylines.len(), 2);
1002    }
1003}