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}