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

Xevion / Pac-Man / 17449905682

04 Sep 2025 12:45AM UTC coverage: 37.219% (-1.0%) from 38.266%
17449905682

push

github

Xevion
feat: add batching & merging of lines in debug rendering

0 of 94 new or added lines in 2 files covered. (0.0%)

2 existing lines in 1 file now uncovered.

1143 of 3071 relevant lines covered (37.22%)

885.18 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::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::{NonSendMut, Query, Res};
10
use glam::{IVec2, UVec2, 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
NEW
43
    pub fn new(map: &Map, scale: f32) -> Self {
×
NEW
44
        let mut horizontal_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
×
NEW
45
        let mut vertical_segments: HashMap<i32, Vec<(i32, i32)>> = HashMap::new();
×
NEW
46
        let mut processed_edges: HashSet<(u16, u16)> = HashSet::new();
×
47

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

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

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

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

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

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

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

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

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

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

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

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

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

138
        // Render vertical lines
NEW
139
        for &(x, y_start, y_end) in &self.vertical_lines {
×
NEW
140
            let points = [Point::new(x, y_start), Point::new(x, y_end)];
×
NEW
141
            let _ = canvas.draw_lines(&points[..]);
×
NEW
142
        }
×
NEW
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
    mut canvas: NonSendMut<&mut Canvas<Window>>,
×
207
    mut debug_texture: NonSendMut<DebugTextureResource>,
×
208
    mut ttf_atlas: NonSendMut<TtfAtlasResource>,
×
NEW
209
    batched_lines: Res<BatchedLinesResource>,
×
210
    debug_state: Res<DebugState>,
×
211
    timings: Res<SystemTimings>,
×
212
    map: Res<Map>,
×
213
    colliders: Query<(&Collider, &Position)>,
×
214
    cursor: Res<CursorPosition>,
×
215
) {
×
216
    if !debug_state.enabled {
×
217
        return;
×
218
    }
×
219
    let scale =
×
220
        (UVec2::from(canvas.output_size().unwrap()).as_vec2() / UVec2::from(canvas.logical_size()).as_vec2()).min_element();
×
221

×
222
    // Create debug text renderer
×
223
    let text_renderer = TtfRenderer::new(1.0);
×
224

225
    let cursor_world_pos = match *cursor {
×
226
        CursorPosition::None => None,
×
227
        CursorPosition::Some { position, .. } => Some(position - BOARD_PIXEL_OFFSET.as_vec2()),
×
228
    };
229

230
    // Draw debug info on the high-resolution debug texture
231
    canvas
×
232
        .with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
×
233
            // Clear the debug canvas
×
234
            debug_canvas.set_draw_color(Color::RGBA(0, 0, 0, 0));
×
235
            debug_canvas.clear();
×
236

237
            // Find the closest node to the cursor
238
            let closest_node = if let Some(cursor_world_pos) = cursor_world_pos {
×
239
                map.graph
×
240
                    .nodes()
×
241
                    .map(|node| node.position.distance(cursor_world_pos))
×
242
                    .enumerate()
×
243
                    .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
×
244
                    .map(|(id, _)| id)
×
245
            } else {
246
                None
×
247
            };
248

249
            debug_canvas.set_draw_color(Color::GREEN);
×
250
            {
×
251
                let rects = colliders
×
252
                    .iter()
×
253
                    .map(|(collider, position)| {
×
254
                        let pos = position.get_pixel_position(&map.graph).unwrap();
×
255

×
256
                        // Transform position and size using common methods
×
257
                        let pos = (pos * scale).as_ivec2();
×
258
                        let size = (collider.size * scale) as u32;
×
259

×
260
                        Rect::from_center(Point::from((pos.x, pos.y)), size, size)
×
261
                    })
×
262
                    .collect::<SmallVec<[Rect; 100]>>();
×
263
                if rects.len() > rects.capacity() {
×
264
                    warn!(
×
265
                        capacity = rects.capacity(),
×
266
                        count = rects.len(),
×
267
                        "Collider rects capacity exceeded"
×
268
                    );
269
                }
×
270
                debug_canvas.draw_rects(&rects).unwrap();
×
271
            }
×
272

×
273
            debug_canvas.set_draw_color(Color {
×
NEW
274
                a: f32_to_u8(0.6),
×
275
                ..Color::RED
×
276
            });
×
277
            debug_canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
×
278

×
NEW
279
            // Use cached batched line segments
×
NEW
280
            batched_lines.render(debug_canvas);
×
UNCOV
281

×
UNCOV
282
            {
×
283
                let rects: Vec<_> = map
×
284
                    .graph
×
285
                    .nodes()
×
286
                    .enumerate()
×
287
                    .filter_map(|(id, node)| {
×
288
                        let pos = transform_position_with_offset(node.position, scale);
×
289
                        let size = (2.0 * scale) as u32;
×
290
                        let rect = Rect::new(pos.x - (size as i32 / 2), pos.y - (size as i32 / 2), size, size);
×
291

×
292
                        // If the node is the one closest to the cursor, draw it immediately
×
293
                        if closest_node == Some(id) {
×
294
                            debug_canvas.set_draw_color(Color::YELLOW);
×
295
                            debug_canvas.fill_rect(rect).unwrap();
×
296
                            return None;
×
297
                        }
×
298

×
299
                        Some(rect)
×
300
                    })
×
301
                    .collect();
×
302

×
303
                if rects.len() > rects.capacity() {
×
304
                    warn!(
×
305
                        capacity = rects.capacity(),
×
306
                        count = rects.len(),
×
307
                        "Node rects capacity exceeded"
×
308
                    );
309
                }
×
310

311
                // Draw the non-closest nodes all at once in blue
312
                debug_canvas.set_draw_color(Color::BLUE);
×
313
                debug_canvas.fill_rects(&rects).unwrap();
×
314
            }
315

316
            // Render node ID if a node is highlighted
317
            if let Some(closest_node_id) = closest_node {
×
318
                let node = map.graph.get_node(closest_node_id as NodeId).unwrap();
×
319
                let pos = transform_position_with_offset(node.position, scale);
×
320

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

×
324
                text_renderer
×
325
                    .render_text(
×
326
                        debug_canvas,
×
327
                        &mut ttf_atlas.0,
×
328
                        &node_id_text,
×
329
                        text_pos,
×
330
                        Color {
×
331
                            a: f32_to_u8(0.4),
×
332
                            ..Color::WHITE
×
333
                        },
×
334
                    )
×
335
                    .unwrap();
×
336
            }
×
337

338
            // Render timing information in the top-left corner
339
            render_timing_display(debug_canvas, &timings, &text_renderer, &mut ttf_atlas.0);
×
340
        })
×
341
        .unwrap();
×
342
}
×
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