Source code for simvx.core.debug.overlay

"""Debug overlay renderer — draws via Draw2D interface."""

import logging

log = logging.getLogger(__name__)

__all__ = ["DebugOverlay"]

[docs] class DebugOverlay: """Renders debug visuals using any Draw2D-compatible renderer.""" def __init__(self): self.show_bounds: bool = False self.show_labels: bool = False self.show_hit_target: bool = False self.show_profiler: bool = True self.show_event_log: bool = False
[docs] def draw_overlay(self, renderer, tree, profiler, inspector): if tree is None: return ss = tree.screen_size w = float(ss[0]) if isinstance(ss, tuple | list) else float(ss.x) h = float(ss[1]) if isinstance(ss, tuple | list) else float(ss.y) # Count nodes once per frame when profiler visible if self.show_profiler: profiler.count_nodes(tree) inspector.snapshot_state(tree) if self.show_bounds: self._draw_control_bounds(renderer, tree) if self.show_labels: from ..input.state import Input mouse = Input._mouse_pos self._draw_control_labels(renderer, tree, mouse) if self.show_profiler: self._draw_profiler_hud(renderer, profiler, w, h) self._draw_frame_graph(renderer, profiler, w - 230, 130, 200, 50) if self.show_event_log: self._draw_event_log(renderer, inspector, w, h)
def _draw_control_bounds(self, renderer, tree): """Draw coloured rects for all visible Controls.""" from ..ui import Control if not tree.root: return hit_target = None if self.show_hit_target: from ..input.state import Input hit_target = tree._find_control_at_point(Input._mouse_pos) for node in tree.root.walk(): if not node._visible_in_hierarchy: continue if not isinstance(node, Control): continue x, y, w, h = node.get_rect() if node is hit_target: colour = (1.0, 1.0, 0.0, 0.6) # Yellow = hit target elif node.disabled: colour = (1.0, 0.2, 0.2, 0.4) # Red = disabled elif node.focused: colour = (0.2, 0.4, 1.0, 0.4) # Blue = focused elif node.mouse_over: colour = (0.2, 0.8, 0.2, 0.4) # Green = hovered else: colour = (0.5, 0.5, 0.5, 0.3) # Gray = normal renderer.draw_rect((x, y), (w, h), colour=colour) def _draw_control_labels(self, renderer, tree, mouse_pos): """Draw control name+type near cursor (within 200px only).""" from ..ui import Control if not tree.root: return mx = float(mouse_pos[0]) if not hasattr(mouse_pos, "x") else float(mouse_pos.x) my = float(mouse_pos[1]) if not hasattr(mouse_pos, "y") else float(mouse_pos.y) radius_sq = 200.0 * 200.0 for node in tree.root.walk(): if not node._visible_in_hierarchy: continue if not isinstance(node, Control): continue x, y, w, h = node.get_rect() cx, cy = x + w / 2, y + h / 2 dx, dy = cx - mx, cy - my if dx * dx + dy * dy <= radius_sq: label = f"{node.name} ({type(node).__name__})" # Background for readability tw = len(label) * 6 * 1 + 2 renderer.draw_rect((x, y - 10), (tw, 10), colour=(0.0, 0.0, 0.0, 0.7), filled=True) renderer.draw_text(label, (x + 1, y - 9), colour=(1.0, 1.0, 1.0, 0.9), scale=1) def _draw_profiler_hud(self, renderer, profiler, w, h): """Top-right corner: FPS, phase timings, node counts.""" x0 = w - 230 y0 = 10 line_h = 12 scale = 1 # Background renderer.draw_rect((x0 - 5, y0 - 5), (225, 120), colour=(0.0, 0.0, 0.0, 0.75), filled=True) # FPS / frame time if profiler.fps >= 55: fps_colour = (0.0, 1.0, 0.0, 1.0) elif profiler.fps >= 30: fps_colour = (1.0, 1.0, 0.0, 1.0) else: fps_colour = (1.0, 0.0, 0.0, 1.0) renderer.draw_text( f"FPS: {profiler.fps:.0f} Frame: {profiler.frame_time_ms:.1f}ms", (x0, y0), colour=fps_colour, scale=scale, ) y0 += line_h + 4 # Phase timings with mini bars phases = ["physics", "process", "draw", "submit", "total"] max_ms = max(profiler.phase_avg_ms(p) for p in phases) if any(profiler.phase_avg_ms(p) for p in phases) else 1.0 max_ms = max(max_ms, 1.0) for phase in phases: ms = profiler.phase_avg_ms(phase) label = f"{phase:>8s}: {ms:5.1f}ms" renderer.draw_text(label, (x0, y0), colour=(0.8, 0.8, 0.8, 1.0), scale=scale) # Mini bar bar_x = x0 + 110 bar_w = min(100, (ms / max_ms) * 100) bar_colour = (0.3, 0.6, 1.0, 0.8) if phase == "total" else (0.2, 0.8, 0.3, 0.8) renderer.draw_rect((bar_x, y0 + 1), (bar_w, line_h - 2), colour=bar_colour, filled=True) y0 += line_h y0 += 4 renderer.draw_text( f"Nodes: {profiler.node_count} Controls: {profiler.control_count}", (x0, y0), colour=(0.7, 0.7, 0.7, 1.0), scale=scale, ) def _draw_event_log(self, renderer, inspector, w, h): """Bottom-left: last 10 input events.""" entries = inspector.get_recent_log(10) if not entries: return x0 = 10 y0 = h - 10 - len(entries) * 12 line_h = 12 scale = 1 # Background renderer.draw_rect( (x0 - 5, y0 - 5), (500, len(entries) * line_h + 10), colour=(0.0, 0.0, 0.0, 0.75), filled=True, ) for entry in entries: px, py = entry.position text = f'{entry.event_type} ({px:.0f},{py:.0f}) -> {entry.target_path} "{entry.outcome}"' if len(text) > 80: text = text[:77] + "..." # Colour by outcome if entry.outcome == "delivered": colour = (0.3, 1.0, 0.3, 0.9) elif entry.outcome in ("blocked", "miss"): colour = (1.0, 0.3, 0.3, 0.9) else: colour = (0.8, 0.8, 0.8, 0.9) renderer.draw_text(text, (x0, y0), colour=colour, scale=scale) y0 += line_h def _draw_frame_graph(self, renderer, profiler, x, y, w, h): """Mini bar chart of recent frame times.""" times = profiler.frame_times(min(int(w), 120)) if not times: return # Background renderer.draw_rect((x, y), (w, h), colour=(0.0, 0.0, 0.0, 0.5), filled=True) max_t = max(max(times), 16.67) # At least 60fps baseline bar_w = w / len(times) for i, t in enumerate(times): bar_h = (t / max_t) * h bx = x + i * bar_w by = y + h - bar_h # Green < 16ms, yellow < 33ms, red > 33ms if t <= 16.67: colour = (0.2, 0.8, 0.2, 0.8) elif t <= 33.33: colour = (1.0, 1.0, 0.0, 0.8) else: colour = (1.0, 0.2, 0.2, 0.8) renderer.draw_rect((bx, by), (max(bar_w - 0.5, 1), bar_h), colour=colour, filled=True) # 16.67ms reference line ref_y = y + h - (16.67 / max_t) * h renderer.draw_line((x, ref_y), (x + w, ref_y), colour=(1.0, 1.0, 1.0, 0.3))