Source code for qrobot_simulator.bug_world.rendering.liveview

"""Persistent Matplotlib view for the live bug ecosystem."""

from pathlib import Path

import matplotlib.pyplot as plt

from ..world import BugWorld, Chessboard
from .config import RENDERING_CONFIG
from .utils import (
    RobotGraphics,
    create_eye_field,
    create_proximity_wedge,
    create_robot_graphics,
    create_text_overlays,
    draw_checkerboard,
    update_eye_field,
    update_proximity_wedge,
    update_robot_graphics,
    update_text_overlays,
)


[docs] class BugWorldLiveView: """Display a persistent view of the world, sensors, and scores.""" def __init__(self, board: Chessboard, interactive: bool = True) -> None: """Create the figure and its static graphics. :param board: Checkerboard whose dimensions define the axes. :param interactive: Enable GUI updates when true. """ self._interactive = interactive if interactive: plt.ion() self.figure, self.axis = plt.subplots( num=RENDERING_CONFIG.figure_name, figsize=RENDERING_CONFIG.figure_size, ) self._setup(board) if interactive: plt.show(block=False) # Public view lifecycle @property def is_open(self) -> bool: """Return whether the Matplotlib figure still exists. :returns: True while the view's figure remains open. """ return plt.fignum_exists(self.figure.number)
[docs] def update(self, world: BugWorld) -> None: """Refresh all graphics and labels from a world snapshot. :param world: Current simulation state to display. """ for robot in world.robots: graphics = self._robots.get(robot.name) if graphics is None: graphics = create_robot_graphics(self.axis, robot) self._robots[robot.name] = graphics biting = (robot is world.bug and world.bug_biting) or ( robot is world.predator and world.predator_biting ) update_robot_graphics(graphics, robot, biting) update_eye_field(self._eye_field, world.bug) update_proximity_wedge( self._proximity, world.bug, active=bool(world.readings.get("proximity", 0.0)), ) update_text_overlays(self._text_overlays, world) self._flush_canvas()
[docs] def save(self, path: Path) -> Path: """Save the current frame to an image. Parent directories are created when needed. :param path: Destination image path. :returns: Destination path after the figure is saved. """ path.parent.mkdir(parents=True, exist_ok=True) self.figure.savefig(path, dpi=RENDERING_CONFIG.save_dpi, bbox_inches="tight") return path
[docs] def close(self) -> None: """Close the Matplotlib figure owned by this view.""" plt.close(self.figure)
# Internal figure management def _setup(self, board: Chessboard) -> None: """Draw static content and create the dynamic graphics.""" draw_checkerboard(self.axis, board.columns, board.rows, board.cell_size) width, height = board.bounds self.axis.set( xlim=(0, width), ylim=(0, height), aspect="equal", title=RENDERING_CONFIG.axis_title, xlabel=RENDERING_CONFIG.axis_x_label, ylabel=RENDERING_CONFIG.axis_y_label, ) self._robots: dict[str, RobotGraphics] = {} self._eye_field = create_eye_field(self.axis) self._proximity = create_proximity_wedge(self.axis) self._text_overlays = create_text_overlays(self.axis) def _flush_canvas(self) -> None: """Draw one interactive frame; do nothing for headless views.""" if self._interactive: self.figure.canvas.draw_idle() self.figure.canvas.flush_events() plt.pause(RENDERING_CONFIG.canvas_pause)