• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Xevion / Pac-Man / 17476832061

04 Sep 2025 09:12PM UTC coverage: 31.982% (-0.2%) from 32.211%
17476832061

push

github

Xevion
feat: enumerate and display render driver info, increase node id text opacity

0 of 34 new or added lines in 3 files covered. (0.0%)

17 existing lines in 2 files now uncovered.

1081 of 3380 relevant lines covered (31.98%)

804.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/systems/debug.rs
1
//! Debug rendering system
2
use std::cmp::Ordering;
3

4
use crate::constants::{self, BOARD_PIXEL_OFFSET};
5
use crate::map::builder::Map;
6
use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};
7
use crate::texture::ttf::{TtfAtlas, TtfRenderer};
8
use bevy_ecs::resource::Resource;
9
use bevy_ecs::system::{Query, Res};
10
use glam::{IVec2, Vec2};
11
use sdl2::pixels::Color;
12
use sdl2::rect::{Point, Rect};
13
use sdl2::render::{Canvas, Texture};
14
use sdl2::video::Window;
15
use smallvec::SmallVec;
16
use std::collections::{HashMap, HashSet};
17
use tracing::warn;
18

19
#[derive(Resource, Default, Debug, Copy, Clone)]
20
pub struct DebugState {
21
    pub enabled: bool,
22
}
23

24
fn f32_to_u8(value: f32) -> u8 {
×
25
    (value * 255.0) as u8
×
26
}
×
27

28
/// Resource to hold the debug texture for persistent rendering
29
pub struct DebugTextureResource(pub Texture);
30

31
/// Resource to hold the TTF text atlas
32
pub struct TtfAtlasResource(pub TtfAtlas);
33

34
/// Resource to hold pre-computed batched line segments
35
#[derive(Resource, Default, Debug, Clone)]
36
pub struct BatchedLinesResource {
37
    horizontal_lines: Vec<(i32, i32, i32)>, // (y, x_start, x_end)
38
    vertical_lines: Vec<(i32, i32, i32)>,   // (x, y_start, y_end)
39
}
40

41
impl BatchedLinesResource {
42
    /// Computes and caches batched line segments for the map graph
43
    pub fn new(map: &Map, scale: f32) -> Self {
×
44
        let mut horizontal_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
×
45
        let mut vertical_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
×
46
        let mut processed_edges: HashSet<(u16, u16)> = HashSet::new();
×
47

48
        // Process all edges and group them by axis
49
        for (start_node_id, edge) in map.graph.edges() {
×
50
            // Acquire a stable key for the edge (from < to)
51
            let edge_key = (start_node_id.min(edge.target), start_node_id.max(edge.target));
×
52

×
53
            // Skip if we've already processed this edge in the reverse direction
×
54
            if processed_edges.contains(&edge_key) {
×
55
                continue;
×
56
            }
×
57
            processed_edges.insert(edge_key);
×
58

×
59
            let start_pos = map.graph.get_node(start_node_id).unwrap().position;
×
60
            let end_pos = map.graph.get_node(edge.target).unwrap().position;
×
61

×
62
            let start = transform_position_with_offset(start_pos, scale);
×
63
            let end = transform_position_with_offset(end_pos, scale);
×
64

×
65
            // Determine if this is a horizontal or vertical line
×
66
            if (start.y - end.y).abs() < 2 {
×
67
                // Horizontal line (allowing for slight vertical variance)
×
68
                let y = start.y;
×
69
                let x_min = start.x.min(end.x);
×
70
                let x_max = start.x.max(end.x);
×
71
                horizontal_segments.entry(y).or_default().push((x_min, x_max));
×
72
            } else if (start.x - end.x).abs() < 2 {
×
73
                // Vertical line (allowing for slight horizontal variance)
×
74
                let x = start.x;
×
75
                let y_min = start.y.min(end.y);
×
76
                let y_max = start.y.max(end.y);
×
77
                vertical_segments.entry(x).or_default().push((y_min, y_max));
×
78
            }
×
79
        }
80

81
        /// Merges overlapping or adjacent segments into continuous lines
82
        fn merge_segments(segments: Vec<(i32, i32)>) -> Vec<(i32, i32)> {
×
83
            if segments.is_empty() {
×
84
                return Vec::new();
×
85
            }
×
86

×
87
            let mut merged = Vec::new();
×
88
            let mut current_start = segments[0].0;
×
89
            let mut current_end = segments[0].1;
×
90

91
            for &(start, end) in segments.iter().skip(1) {
×
92
                if start <= current_end + 1 {
×
93
                    // Adjacent or overlapping
×
94
                    current_end = current_end.max(end);
×
95
                } else {
×
96
                    merged.push((current_start, current_end));
×
97
                    current_start = start;
×
98
                    current_end = end;
×
99
                }
×
100
            }
101

102
            merged.push((current_start, current_end));
×
103
            merged
×
104
        }
×
105

106
        // Convert to flat vectors for fast iteration during rendering
107
        let horizontal_lines = horizontal_segments
×
108
            .into_iter()
×
109
            .flat_map(|(y, mut segments)| {
×
110
                segments.sort_unstable_by_key(|(start, _)| *start);
×
111
                let merged = merge_segments(segments);
×
112
                merged.into_iter().map(move |(x_start, x_end)| (y, x_start, x_end))
×
113
            })
×
114
            .collect::<Vec<_>>();
×
115

×
116
        let vertical_lines = vertical_segments
×
117
            .into_iter()
×
118
            .flat_map(|(x, mut segments)| {
×
119
                segments.sort_unstable_by_key(|(start, _)| *start);
×
120
                let merged = merge_segments(segments);
×
121
                merged.into_iter().map(move |(y_start, y_end)| (x, y_start, y_end))
×
122
            })
×
123
            .collect::<Vec<_>>();
×
124

×
125
        Self {
×
126
            horizontal_lines,
×
127
            vertical_lines,
×
128
        }
×
129
    }
×
130

131
    pub fn render(&self, canvas: &mut Canvas<Window>) {
×
132
        // Render horizontal lines
133
        for &(y, x_start, x_end) in &self.horizontal_lines {
×
134
            let points = [Point::new(x_start, y), Point::new(x_end, y)];
×
135
            let _ = canvas.draw_lines(&points[..]);
×
136
        }
×
137

138
        // Render vertical lines
139
        for &(x, y_start, y_end) in &self.vertical_lines {
×
140
            let points = [Point::new(x, y_start), Point::new(x, y_end)];
×
141
            let _ = canvas.draw_lines(&points[..]);
×
142
        }
×
143
    }
×
144
}
145

146
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
147
fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
×
148
    ((pos + BOARD_PIXEL_OFFSET.as_vec2()) * scale).as_ivec2()
×
149
}
×
150

151
/// Renders timing information in the top-left corner of the screen using the debug text atlas
152
fn render_timing_display(
×
153
    canvas: &mut Canvas<Window>,
×
154
    timings: &SystemTimings,
×
155
    text_renderer: &TtfRenderer,
×
156
    atlas: &mut TtfAtlas,
×
157
) {
×
158
    // Format timing information using the formatting module
×
159
    let lines = timings.format_timing_display();
×
160
    let line_height = text_renderer.text_height(atlas) as i32 + 2; // Add 2px line spacing
×
161
    let padding = 10;
×
162

×
163
    // Calculate background dimensions
×
164
    let max_width = lines
×
165
        .iter()
×
166
        .filter(|l| !l.is_empty()) // Don't consider empty lines for width
×
167
        .map(|line| text_renderer.text_width(atlas, line))
×
168
        .max()
×
169
        .unwrap_or(0);
×
170

×
171
    // Only draw background if there is text to display
×
172
    let total_height = (lines.len() as u32) * line_height as u32;
×
173
    if max_width > 0 && total_height > 0 {
×
174
        let bg_padding = 5;
×
175

×
176
        // Draw background
×
177
        let bg_rect = Rect::new(
×
178
            padding - bg_padding,
×
179
            padding - bg_padding,
×
180
            max_width + (bg_padding * 2) as u32,
×
181
            total_height + bg_padding as u32,
×
182
        );
×
183
        canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
×
184
        canvas.set_draw_color(Color::RGBA(40, 40, 40, 180));
×
185
        canvas.fill_rect(bg_rect).unwrap();
×
186
    }
×
187

188
    for (i, line) in lines.iter().enumerate() {
×
189
        if line.is_empty() {
×
190
            continue;
×
191
        }
×
192

×
193
        // Position each line below the previous one
×
194
        let y_pos = padding + (i as i32 * line_height);
×
195
        let position = Vec2::new(padding as f32, y_pos as f32);
×
196

×
197
        // Render the line using the debug text renderer
×
198
        text_renderer
×
199
            .render_text(canvas, atlas, line, position, Color::RGBA(255, 255, 255, 200))
×
200
            .unwrap();
×
201
    }
202
}
×
203

204
#[allow(clippy::too_many_arguments)]
205
pub fn debug_render_system(
×
206
    canvas: &mut Canvas<Window>,
×
207
    ttf_atlas: &mut TtfAtlasResource,
×
208
    batched_lines: &Res<BatchedLinesResource>,
×
209
    debug_state: &Res<DebugState>,
×
210
    timings: &Res<SystemTimings>,
×
211
    map: &Res<Map>,
×
212
    colliders: &Query<(&Collider, &Position)>,
×
213
    cursor: &Res<CursorPosition>,
×
214
) {
×
215
    if !debug_state.enabled {
×
216
        return;
×
217
    }
×
218
    // Create debug text renderer
×
219
    let text_renderer = TtfRenderer::new(1.0);
×
220

221
    let cursor_world_pos = match &**cursor {
×
UNCOV
222
        CursorPosition::None => None,
×
223
        CursorPosition::Some { position, .. } => Some(position - BOARD_PIXEL_OFFSET.as_vec2()),
×
224
    };
225

226
    // Clear the debug canvas
UNCOV
227
    canvas.set_draw_color(Color::RGBA(0, 0, 0, 0));
×
UNCOV
228
    canvas.clear();
×
229

230
    // Find the closest node to the cursor
UNCOV
231
    let closest_node = if let Some(cursor_world_pos) = cursor_world_pos {
×
UNCOV
232
        map.graph
×
233
            .nodes()
×
234
            .map(|node| node.position.distance(cursor_world_pos))
×
235
            .enumerate()
×
236
            .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
×
237
            .map(|(id, _)| id)
×
238
    } else {
239
        None
×
240
    };
241

UNCOV
242
    canvas.set_draw_color(Color::GREEN);
×
UNCOV
243
    {
×
244
        let rects = colliders
×
245
            .iter()
×
246
            .map(|(collider, position)| {
×
247
                let pos = position.get_pixel_position(&map.graph).unwrap();
×
248

×
249
                // Transform position and size using common methods
×
NEW
250
                let pos = (pos * constants::LARGE_SCALE).as_ivec2();
×
NEW
251
                let size = (collider.size * constants::LARGE_SCALE) as u32;
×
252

×
253
                Rect::from_center(Point::from((pos.x, pos.y)), size, size)
×
254
            })
×
255
            .collect::<SmallVec<[Rect; 100]>>();
×
256
        if rects.len() > rects.capacity() {
×
257
            warn!(
×
258
                capacity = rects.capacity(),
×
259
                count = rects.len(),
×
260
                "Collider rects capacity exceeded"
×
261
            );
262
        }
×
UNCOV
263
        canvas.draw_rects(&rects).unwrap();
×
264
    }
×
265

×
266
    canvas.set_draw_color(Color {
×
267
        a: f32_to_u8(0.6),
×
268
        ..Color::RED
×
269
    });
×
270
    canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
×
271

×
272
    // Use cached batched line segments
×
273
    batched_lines.render(canvas);
×
274

×
275
    {
×
276
        let rects: Vec<_> = map
×
277
            .graph
×
278
            .nodes()
×
279
            .enumerate()
×
280
            .filter_map(|(id, node)| {
×
NEW
281
                let pos = transform_position_with_offset(node.position, constants::LARGE_SCALE);
×
NEW
282
                let size = (2.0 * constants::LARGE_SCALE) as u32;
×
283
                let rect = Rect::new(pos.x - (size as i32 / 2), pos.y - (size as i32 / 2), size, size);
×
284

×
285
                // If the node is the one closest to the cursor, draw it immediately
×
286
                if closest_node == Some(id) {
×
287
                    canvas.set_draw_color(Color::YELLOW);
×
288
                    canvas.fill_rect(rect).unwrap();
×
289
                    return None;
×
290
                }
×
291

×
292
                Some(rect)
×
293
            })
×
294
            .collect();
×
295

×
296
        if rects.len() > rects.capacity() {
×
297
            warn!(
×
298
                capacity = rects.capacity(),
×
299
                count = rects.len(),
×
300
                "Node rects capacity exceeded"
×
301
            );
302
        }
×
303

304
        // Draw the non-closest nodes all at once in blue
UNCOV
305
        canvas.set_draw_color(Color::BLUE);
×
UNCOV
306
        canvas.fill_rects(&rects).unwrap();
×
307
    }
308

309
    // Render node ID if a node is highlighted
UNCOV
310
    if let Some(closest_node_id) = closest_node {
×
UNCOV
311
        let node = map.graph.get_node(closest_node_id as NodeId).unwrap();
×
NEW
312
        let pos = transform_position_with_offset(node.position, constants::LARGE_SCALE);
×
313

×
314
        let node_id_text = closest_node_id.to_string();
×
315
        let text_pos = Vec2::new((pos.x + 10) as f32, (pos.y - 5) as f32);
×
316

×
317
        text_renderer
×
318
            .render_text(
×
319
                canvas,
×
320
                &mut ttf_atlas.0,
×
321
                &node_id_text,
×
322
                text_pos,
×
323
                Color {
×
NEW
324
                    a: f32_to_u8(0.9),
×
325
                    ..Color::WHITE
×
326
                },
×
327
            )
×
328
            .unwrap();
×
329
    }
×
330

331
    // Render timing information in the top-left corner
UNCOV
332
    render_timing_display(canvas, timings, &text_renderer, &mut ttf_atlas.0);
×
UNCOV
333
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc