Source code for simvx.core.debug.overlay

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


from __future__ import annotations

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 hit_target = None if self.show_hit_target: from ..input.state import Input hit_target = tree._find_control_at_point(Input._mouse_pos) def _walk(node): if not node.visible: return if isinstance(node, Control): 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._colour = colour renderer.draw_rect(x, y, w, h) for child in node.children: _walk(child) if tree.root: _walk(tree.root) def _draw_control_labels(self, renderer, tree, mouse_pos): """Draw control name+type near cursor (within 200px only).""" from ..ui import Control 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 def _walk(node): if not node.visible: return if isinstance(node, Control): 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__})" renderer._colour = (1.0, 1.0, 1.0, 0.9) # Background for readability tw = len(label) * 6 * 1 + 2 renderer._colour = (0.0, 0.0, 0.0, 0.7) renderer.fill_rect(x, y - 10, tw, 10) renderer._colour = (1.0, 1.0, 1.0, 0.9) renderer.draw_text(label, (x + 1, y - 9), scale=1) for child in node.children: _walk(child) if tree.root: _walk(tree.root) 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._colour = (0.0, 0.0, 0.0, 0.75) renderer.fill_rect(x0 - 5, y0 - 5, 225, 120) # FPS / frame time if profiler.fps >= 55: renderer._colour = (0.0, 1.0, 0.0, 1.0) elif profiler.fps >= 30: renderer._colour = (1.0, 1.0, 0.0, 1.0) else: renderer._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), 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) renderer._colour = (0.8, 0.8, 0.8, 1.0) label = f"{phase:>8s}: {ms:5.1f}ms" renderer.draw_text(label, (x0, y0), scale=scale) # Mini bar bar_x = x0 + 110 bar_w = min(100, (ms / max_ms) * 100) if phase == "total": renderer._colour = (0.3, 0.6, 1.0, 0.8) else: renderer._colour = (0.2, 0.8, 0.3, 0.8) renderer.fill_rect(bar_x, y0 + 1, bar_w, line_h - 2) y0 += line_h y0 += 4 renderer._colour = (0.7, 0.7, 0.7, 1.0) renderer.draw_text(f"Nodes: {profiler.node_count} Controls: {profiler.control_count}", (x0, y0), 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._colour = (0.0, 0.0, 0.0, 0.75) renderer.fill_rect(x0 - 5, y0 - 5, 500, len(entries) * line_h + 10) 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": renderer._colour = (0.3, 1.0, 0.3, 0.9) elif entry.outcome == "blocked" or entry.outcome == "miss": renderer._colour = (1.0, 0.3, 0.3, 0.9) else: renderer._colour = (0.8, 0.8, 0.8, 0.9) renderer.draw_text(text, (x0, y0), 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._colour = (0.0, 0.0, 0.0, 0.5) renderer.fill_rect(x, y, w, h) 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: renderer._colour = (0.2, 0.8, 0.2, 0.8) elif t <= 33.33: renderer._colour = (1.0, 1.0, 0.0, 0.8) else: renderer._colour = (1.0, 0.2, 0.2, 0.8) renderer.fill_rect(bx, by, max(bar_w - 0.5, 1), bar_h) # 16.67ms reference line ref_y = y + h - (16.67 / max_t) * h renderer._colour = (1.0, 1.0, 1.0, 0.3) renderer.draw_line(x, ref_y, x + w, ref_y)