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

Xevion / Pac-Man / 17506959295

05 Sep 2025 11:49PM UTC coverage: 31.007% (-0.2%) from 31.246%
17506959295

push

github

Xevion
feat: re-implement CustomFormatter to clone Full formatterr

0 of 61 new or added lines in 1 file covered. (0.0%)

161 existing lines in 4 files now uncovered.

1093 of 3525 relevant lines covered (31.01%)

772.4 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
    current_tick: u64,
×
156
    text_renderer: &TtfRenderer,
×
157
    atlas: &mut TtfAtlas,
×
158
) {
×
159
    // Format timing information using the formatting module
×
160
    let lines = timings.format_timing_display(current_tick);
×
161
    let line_height = text_renderer.text_height(atlas) as i32 + 2; // Add 2px line spacing
×
162
    let padding = 10;
×
163

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
274
    // Use cached batched line segments
×
275
    batched_lines.render(canvas);
×
276

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

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

×
294
                Some(rect)
×
295
            })
×
296
            .collect();
×
297

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

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

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

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

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

333
    // Render timing information in the top-left corner
334
    // Use previous tick since current tick is incomplete (frame is still running)
UNCOV
335
    let current_tick = timing.get_current_tick();
×
UNCOV
336
    let previous_tick = current_tick.saturating_sub(1);
×
UNCOV
337
    render_timing_display(canvas, timings, previous_tick, &text_renderer, &mut ttf_atlas.0);
×
UNCOV
338
}
×
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