diff --git a/src/proto/visualization.proto b/src/proto/visualization.proto index 2dc1165ac2..d222060143 100644 --- a/src/proto/visualization.proto +++ b/src/proto/visualization.proto @@ -41,6 +41,11 @@ message AttackerVisualization optional Point chip_target = 4; } +message BallPlacementVisualization +{ + Point ball_placement_point = 1; +} + message CostVisualization { uint32 num_rows = 1; diff --git a/src/shared/constants.h b/src/shared/constants.h index f3b30c58e9..e72b54c490 100644 --- a/src/shared/constants.h +++ b/src/shared/constants.h @@ -79,6 +79,14 @@ static const double BALL_MAX_RADIUS_METERS = 0.0215; // cover more than 20% of the ball static const double MAX_FRACTION_OF_BALL_COVERED_BY_ROBOT = 0.2; +// The radius of a circle region where ball placement is acceptable (in meters). +constexpr double BALL_PLACEMENT_TOLERANCE_RADIUS_METERS = 0.15; +// The radius of the outer region where robots are not allowed to be during ball +// placement (in meters) +constexpr double BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS = 0.5; +// The time limit for ball placement in seconds +constexpr int BALL_PLACEMENT_TIME_LIMIT_S = 30; + // The mass of a standard golf ball, as defined by https://en.wikipedia.org/wiki/Golf_ball constexpr double BALL_MASS_KG = 0.004593; // The max allowed speed of the robot when the stop command is issued, in meters per diff --git a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play.cpp b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play.cpp index 0bc65ae98c..51443d36d3 100644 --- a/src/software/ai/hl/stp/play/ball_placement/ball_placement_play.cpp +++ b/src/software/ai/hl/stp/play/ball_placement/ball_placement_play.cpp @@ -1,5 +1,6 @@ #include "software/ai/hl/stp/play/ball_placement/ball_placement_play.h" +#include "proto/message_translation/tbots_geometry.h" #include "software/util/generic_factory/generic_factory.h" @@ -17,7 +18,19 @@ void BallPlacementPlay::getNextTactics(TacticCoroutine::push_type &yield, void BallPlacementPlay::updateTactics(const PlayUpdate &play_update) { - fsm.process_event(BallPlacementPlayFSM::Update(control_params, play_update)); + auto event = BallPlacementPlayFSM::Update(control_params, play_update); + fsm.process_event(event); + + std::optional placement_point = + event.common.world_ptr->gameState().getBallPlacementPoint(); + if (placement_point.has_value()) + { + TbotsProto::BallPlacementVisualization ball_placement_vis_msg; + *(ball_placement_vis_msg.mutable_ball_placement_point()) = + *createPointProto(placement_point.value()); + + LOG(VISUALIZE) << ball_placement_vis_msg; + } } std::vector BallPlacementPlay::getState() diff --git a/src/software/py_constants.cpp b/src/software/py_constants.cpp index 4a7d3dfa28..bda0addfc6 100644 --- a/src/software/py_constants.cpp +++ b/src/software/py_constants.cpp @@ -17,6 +17,11 @@ PYBIND11_MODULE(py_constants, m) m.attr("BALL_MAX_RADIUS_METERS") = BALL_MAX_RADIUS_METERS; m.attr("BALL_MAX_RADIUS_MILLIMETERS") = BALL_MAX_RADIUS_METERS * MILLIMETERS_PER_METER; + m.attr("BALL_PLACEMENT_TOLERANCE_RADIUS_METERS") = + BALL_PLACEMENT_TOLERANCE_RADIUS_METERS; + m.attr("BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS") = + BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS; + m.attr("BALL_PLACEMENT_TIME_LIMIT_S") = BALL_PLACEMENT_TIME_LIMIT_S; m.attr("MAX_FRACTION_OF_BALL_COVERED_BY_ROBOT") = MAX_FRACTION_OF_BALL_COVERED_BY_ROBOT; diff --git a/src/software/thunderscope/BUILD b/src/software/thunderscope/BUILD index 927fcba80e..e7b11cd453 100644 --- a/src/software/thunderscope/BUILD +++ b/src/software/thunderscope/BUILD @@ -91,6 +91,7 @@ py_library( "//software/thunderscope/gl/layers:gl_obstacle_layer", "//software/thunderscope/gl/layers:gl_passing_layer", "//software/thunderscope/gl/layers:gl_path_layer", + "//software/thunderscope/gl/layers:gl_referee_info_layer", "//software/thunderscope/gl/layers:gl_sandbox_world_layer", "//software/thunderscope/gl/layers:gl_simulator_layer", "//software/thunderscope/gl/layers:gl_tactic_layer", diff --git a/src/software/thunderscope/binary_context_managers/full_system.py b/src/software/thunderscope/binary_context_managers/full_system.py index 240a06354a..d6aa8b5f1d 100644 --- a/src/software/thunderscope/binary_context_managers/full_system.py +++ b/src/software/thunderscope/binary_context_managers/full_system.py @@ -184,6 +184,7 @@ def setup_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None: PlayInfo, ObstacleList, DebugShapes, + BallPlacementVisualization, ]: proto_unix_io.attach_unix_receiver( runtime_dir=self.full_system_runtime_dir, diff --git a/src/software/thunderscope/constants.py b/src/software/thunderscope/constants.py index 0314ca7b6c..e5b7ab1505 100644 --- a/src/software/thunderscope/constants.py +++ b/src/software/thunderscope/constants.py @@ -185,6 +185,8 @@ class EstopMode(IntEnum): """ ) +THUNDERSCOPE_UI_FONT_NAME = "Roboto" + def is_field_message_empty(field: Field) -> bool: """Checks if a field message is empty @@ -277,6 +279,7 @@ class Colors: ROBOT_MIDDLE_BLUE = QtGui.QColor(0, 0, 255, 255) PINK = QtGui.QColor(255, 0, 255) GREEN = QtGui.QColor(0, 255, 0) + RED = QtGui.QColor(255, 0, 0, 255) # Creates a default vision pattern lookup with the actual colors used on the robots VISION_PATTERN_LOOKUP = create_vision_pattern_lookup(PINK, GREEN) diff --git a/src/software/thunderscope/gl/graphics/BUILD b/src/software/thunderscope/gl/graphics/BUILD index 2b79290562..927612c58b 100644 --- a/src/software/thunderscope/gl/graphics/BUILD +++ b/src/software/thunderscope/gl/graphics/BUILD @@ -106,3 +106,12 @@ py_library( ":gl_shape", ], ) + +py_library( + name = "gl_label", + srcs = ["gl_label.py"], + deps = [ + requirement("pyqtgraph"), + ":gl_shape", + ], +) diff --git a/src/software/thunderscope/gl/graphics/gl_gradient_legend.py b/src/software/thunderscope/gl/graphics/gl_gradient_legend.py index 7654c7d6c0..96cc6e0045 100644 --- a/src/software/thunderscope/gl/graphics/gl_gradient_legend.py +++ b/src/software/thunderscope/gl/graphics/gl_gradient_legend.py @@ -4,7 +4,7 @@ from typing import Optional from software.thunderscope.gl.graphics.gl_painter import GLPainter -from software.thunderscope.constants import Colors +from software.thunderscope.constants import Colors, THUNDERSCOPE_UI_FONT_NAME class GLGradientLegend(GLPainter): @@ -47,8 +47,10 @@ def __init__( self.title = title self.text_pen = QtGui.QPen(Colors.PRIMARY_TEXT_COLOR) - self.labels_font = QtGui.QFont("Roboto", 8) - self.title_font = QtGui.QFont("Roboto", 9, QtGui.QFont.Weight.Bold) + self.labels_font = QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8) + self.title_font = QtGui.QFont( + THUNDERSCOPE_UI_FONT_NAME, 9, QtGui.QFont.Weight.Bold + ) self.add_draw_function(self.draw_gradient_legend) diff --git a/src/software/thunderscope/gl/graphics/gl_label.py b/src/software/thunderscope/gl/graphics/gl_label.py new file mode 100644 index 0000000000..74f55d3c01 --- /dev/null +++ b/src/software/thunderscope/gl/graphics/gl_label.py @@ -0,0 +1,77 @@ +from PyQt6.QtGui import QFont, QColor +from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtCore, QtGui + +from typing import Optional + +from software.thunderscope.gl.graphics.gl_painter import GLPainter +from software.thunderscope.constants import Colors, THUNDERSCOPE_UI_FONT_NAME + + +class GLLabel(GLPainter): + """Displays a 2D text label on the viewport""" + + def __init__( + self, + parent_item: Optional[GLGraphicsItem] = None, + font: QFont = QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + text_color: QColor = Colors.PRIMARY_TEXT_COLOR, + offset: tuple[int, int] = (0, 0), + text: str = "", + ) -> None: + """Initialize the GLLabel + + :param parent_item: The parent item of the graphic + :param font: The font using to render the text + :param text_color: The color for rendering the text. + :param offset: The offset (x, y) from the viewport left and top edge + to use when positioning the label. + If x is negative then the x offset is |x| pixels from + the viewport right edge. + If y is negative then the y offset is |y| pixels from + the viewport bottom edge. + :param text: The optional title to display above the legend + """ + super().__init__(parent_item=parent_item) + + self.text_pen = QtGui.QPen(text_color) + self.font = font + self.offset = offset + self.text = text + + self.add_draw_function(self.draw_label) + + def draw_label(self, painter: QtGui.QPainter, viewport_rect: QtCore.QRect) -> None: + """Draw the label + :param painter: The QPainter to perform drawing operations with + :param viewport_rect: The QRect indicating the viewport dimensions + """ + # calculate width and height of the label + painter.setFont(self.font) + bounds = painter.boundingRect( + QtCore.QRectF(0, 0, 0, 0), + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, + str(self.text), + ) + + width = round(bounds.width()) + height = round(bounds.height()) + + # Determine x and y coordinates of the label + if self.offset[0] < 0: + x = viewport_rect.right() + self.offset[0] - width + else: + x = viewport_rect.left() + self.offset[0] + if self.offset[1] < 0: + y = viewport_rect.bottom() + self.offset[1] - height + else: + y = viewport_rect.top() + self.offset[1] + + if self.text: + painter.drawText(QtCore.QPoint(x, y), self.text) + + def set_text(self, new_text: str) -> None: + """Update the text being displayed + :param new_text: new text being displayed + """ + self.text = new_text diff --git a/src/software/thunderscope/gl/layers/BUILD b/src/software/thunderscope/gl/layers/BUILD index 562eb840f3..fa6b76bb82 100644 --- a/src/software/thunderscope/gl/layers/BUILD +++ b/src/software/thunderscope/gl/layers/BUILD @@ -168,3 +168,13 @@ py_library( requirement("pyqtgraph"), ], ) + +py_library( + name = "gl_referee_info_layer", + srcs = ["gl_referee_info_layer.py"], + deps = [ + ":gl_layer", + "//software/thunderscope/gl/graphics:gl_label", + requirement("pyqtgraph"), + ], +) diff --git a/src/software/thunderscope/gl/layers/gl_attacker_layer.py b/src/software/thunderscope/gl/layers/gl_attacker_layer.py index f3b9c6cc06..8ce9f769eb 100644 --- a/src/software/thunderscope/gl/layers/gl_attacker_layer.py +++ b/src/software/thunderscope/gl/layers/gl_attacker_layer.py @@ -6,7 +6,11 @@ from proto.visualization_pb2 import AttackerVisualization -from software.thunderscope.constants import Colors, DepthValues +from software.thunderscope.constants import ( + Colors, + DepthValues, + THUNDERSCOPE_UI_FONT_NAME, +) from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer from software.thunderscope.gl.layers.gl_layer import GLLayer from software.thunderscope.gl.graphics.gl_circle import GLCircle @@ -97,7 +101,8 @@ def refresh_graphics(self) -> None: self.shot_open_angle_graphics.resize( 1, lambda: GLTextItem( - font=QtGui.QFont("Roboto", 8), color=Colors.SHOT_VISUALIZATION_COLOR + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + color=Colors.SHOT_VISUALIZATION_COLOR, ), ) diff --git a/src/software/thunderscope/gl/layers/gl_debug_shapes_layer.py b/src/software/thunderscope/gl/layers/gl_debug_shapes_layer.py index 9c22f232e1..be4742b5e2 100644 --- a/src/software/thunderscope/gl/layers/gl_debug_shapes_layer.py +++ b/src/software/thunderscope/gl/layers/gl_debug_shapes_layer.py @@ -6,7 +6,11 @@ from proto.visualization_pb2 import DebugShapes -from software.thunderscope.constants import Colors, DepthValues +from software.thunderscope.constants import ( + Colors, + DepthValues, + THUNDERSCOPE_UI_FONT_NAME, +) from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer from software.thunderscope.gl.layers.gl_layer import GLLayer from software.thunderscope.gl.graphics.gl_circle import GLCircle @@ -89,7 +93,8 @@ def refresh_graphics(self) -> None: self.poly_shape_name_graphics.resize( len(poly_named_shapes), lambda: GLTextItem( - font=QtGui.QFont("Roboto", 8), color=Colors.DEBUG_SHAPES_COLOR + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + color=Colors.DEBUG_SHAPES_COLOR, ), ) @@ -100,7 +105,8 @@ def refresh_graphics(self) -> None: self.circle_shape_name_graphics.resize( len(circle_named_shapes), lambda: GLTextItem( - font=QtGui.QFont("Roboto", 8), color=Colors.DEBUG_SHAPES_COLOR + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + color=Colors.DEBUG_SHAPES_COLOR, ), ) @@ -111,7 +117,8 @@ def refresh_graphics(self) -> None: self.stadium_shape_name_graphics.resize( len(stadium_named_shapes), lambda: GLTextItem( - font=QtGui.QFont("Roboto", 8), color=Colors.DEBUG_SHAPES_COLOR + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + color=Colors.DEBUG_SHAPES_COLOR, ), ) diff --git a/src/software/thunderscope/gl/layers/gl_measure_layer.py b/src/software/thunderscope/gl/layers/gl_measure_layer.py index d33623114a..152306ddc8 100644 --- a/src/software/thunderscope/gl/layers/gl_measure_layer.py +++ b/src/software/thunderscope/gl/layers/gl_measure_layer.py @@ -5,7 +5,11 @@ import numpy as np from software.py_constants import * -from software.thunderscope.constants import Colors, DepthValues +from software.thunderscope.constants import ( + Colors, + DepthValues, + THUNDERSCOPE_UI_FONT_NAME, +) from software.thunderscope.gl.layers.gl_layer import GLLayer from software.thunderscope.gl.graphics.gl_sphere import GLSphere @@ -80,7 +84,7 @@ def mouse_in_scene_pressed(self, event: MouseInSceneEvent) -> None: self.measurement_text_graphics.append( GLTextItem( - font=QtGui.QFont("Roboto", 8), + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), color=Colors.PRIMARY_TEXT_COLOR, text=f"{distance:.2f} m", pos=np.array([midpoint.x(), midpoint.y(), 0]), @@ -111,7 +115,7 @@ def mouse_in_scene_pressed(self, event: MouseInSceneEvent) -> None: self.measurement_text_graphics.append( GLTextItem( - font=QtGui.QFont("Roboto", 8), + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), color=Colors.PRIMARY_TEXT_COLOR, text=f"{angle:.1f}°", pos=np.array([placement_point.x(), placement_point.y(), 0]), @@ -142,7 +146,7 @@ def refresh_graphics(self) -> None: if not self.cursor_coords_graphic: self.cursor_coords_graphic = GLTextItem( parentItem=self, - font=QtGui.QFont("Roboto", 10), + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 10), color=Colors.PRIMARY_TEXT_COLOR, ) diff --git a/src/software/thunderscope/gl/layers/gl_referee_info_layer.py b/src/software/thunderscope/gl/layers/gl_referee_info_layer.py new file mode 100644 index 0000000000..cee57a1e51 --- /dev/null +++ b/src/software/thunderscope/gl/layers/gl_referee_info_layer.py @@ -0,0 +1,249 @@ +from typing import Optional + +from PyQt6 import QtGui +from google.protobuf.json_format import MessageToDict +from pyqtgraph.opengl.items.GLTextItem import GLTextItem + +from proto.import_all_protos import * +from software.py_constants import * +import software.python_bindings as tbots_cpp +from software.thunderscope.constants import ( + DepthValues, + Colors, + THUNDERSCOPE_UI_FONT_NAME, +) +from software.thunderscope.gl.graphics.gl_circle import GLCircle +from software.thunderscope.gl.graphics.gl_label import GLLabel +from software.thunderscope.gl.helpers.observable_list import ObservableList +from software.thunderscope.gl.layers.gl_layer import GLLayer +from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer + + +class GLRefereeInfoLayer(GLLayer): + """GLLayer that visualizes referee info""" + + REFEREE_COMMAND_PREFIX = "Command: " + GAMESTATE_PREFIX = "Game State: " + + # outline color of the avoid area for ball placement + BALL_PLACEMENT_ROBOT_AVOID_AREA_VISUALIZATION_COLOR = Colors.RED + # outline color of the tolerance region for ball placement (when the ball is inside) + BALL_PLACEMENT_TOLERANCE_VISUALIZATION_COLOR_ON = Colors.GREEN + # outline color of the tolerance region for ball placement (when the ball is outside) + BALL_PLACEMENT_TOLERANCE_VISUALIZATION_COLOR_OFF = Colors.RED + # outline color of the target mark + BALL_PLACEMENT_TARGET_VISUALIZATION_COLOR = Colors.FIELD_LINE_COLOR + # text color for count down + COUNT_DOWN_TEXT_COLOR = Colors.RED + + def __init__(self, name: str, buffer_size: int = 1) -> None: + """Initialize the GLRefereeInfoLayer + + :param name: The displayed name of the layer + :param buffer_size: the buffer size, set higher for smoother plots. + Set lower for more realtime plots. Default is arbitrary. + """ + super().__init__(name) + self.setDepthValue(DepthValues.OVERLAY_DEPTH) + self.referee_vis_buffer = ThreadSafeBuffer(buffer_size, Referee) + self.ball_placement_vis_buffer = ThreadSafeBuffer( + buffer_size, BallPlacementVisualization + ) + self.world_buffer = ThreadSafeBuffer(buffer_size, World) + + self.cached_world = None + self.cached_referee_info = None + + self.referee_text_graphics = ObservableList(self._graphics_changed) + + self.placement_tolerance_graphic = GLCircle( + parent_item=self, + radius=BALL_PLACEMENT_TOLERANCE_RADIUS_METERS, + outline_color=self.BALL_PLACEMENT_TOLERANCE_VISUALIZATION_COLOR_OFF, + ) + + self.placement_target_graphic = GLCircle( + parent_item=self, + radius=BALL_PLACEMENT_TOLERANCE_RADIUS_METERS, + outline_color=self.BALL_PLACEMENT_TARGET_VISUALIZATION_COLOR, + ) + + self.robot_avoid_circle_graphic = GLCircle( + parent_item=self, + radius=BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS, + outline_color=self.BALL_PLACEMENT_ROBOT_AVOID_AREA_VISUALIZATION_COLOR, + ) + + # initialize the two text items to display + self.gamestate_type_text: Optional[GLLabel] = None + self.command_type_text: Optional[GLLabel] = None + + self.ball_placement_countdown_graphic: Optional[GLTextItem] = None + + self.ball_placement_point = None + self.ball_placement_in_progress = False + self.ball_placement_tolerance_circle = None + self.shrink_target = True + + def __update_ball_placement(self) -> None: + """Update ball placement visuals""" + ball_placement_vis_proto = self.ball_placement_vis_buffer.get( + block=False, return_cached=False + ) + + if not self.ball_placement_countdown_graphic: + self.ball_placement_countdown_graphic = GLTextItem( + parentItem=self, + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 7, weight=700), + color=self.COUNT_DOWN_TEXT_COLOR, + ) + + # if ball placement is in progress, update all the visuals + if self.ball_placement_in_progress: + self.__update_ball_placement_status(self.cached_world.ball.current_state) + self.__update_target_visual() + + if ball_placement_vis_proto: + new_placement_point = ball_placement_vis_proto.ball_placement_point + if not self.ball_placement_in_progress: + # initialize the visuals + self.ball_placement_tolerance_circle = tbots_cpp.Circle( + tbots_cpp.createPoint(new_placement_point), + BALL_PLACEMENT_TOLERANCE_RADIUS_METERS, + ) + self.__display_ball_placement_visuals(new_placement_point) + self.ball_placement_point = new_placement_point + elif self.ball_placement_in_progress: + # finish ball placement visualization + self.__hide_ball_placement_visuals() + self.ball_placement_in_progress = False + + def __update_referee_info(self): + """Update gamestate and command info text displays""" + referee_proto = self.referee_vis_buffer.get(block=False, return_cached=False) + if not referee_proto: + return + + referee_msg_dict = MessageToDict(referee_proto) + if not referee_msg_dict: + return + self.cached_referee_info = referee_msg_dict + + if not self.gamestate_type_text: + self.gamestate_type_text = GLLabel( + parent_item=self, + offset=(-10, 50), + text=GLRefereeInfoLayer.GAMESTATE_PREFIX + referee_msg_dict["stage"], + ) + self.referee_text_graphics.append(self.gamestate_type_text) + else: + self.gamestate_type_text.set_text( + GLRefereeInfoLayer.GAMESTATE_PREFIX + referee_msg_dict["stage"] + ) + + if not self.command_type_text: + self.command_type_text = GLLabel( + parent_item=self, + offset=(-10, 70), + text=GLRefereeInfoLayer.GAMESTATE_PREFIX + referee_msg_dict["command"], + ) + self.referee_text_graphics.append(self.command_type_text) + else: + self.command_type_text.set_text( + GLRefereeInfoLayer.REFEREE_COMMAND_PREFIX + referee_msg_dict["command"] + ) + + def refresh_graphics(self) -> None: + """Refresh all visuals for both ball placement and referee info""" + self.cached_world = self.world_buffer.get(block=False, return_cached=True) + + self.__update_referee_info() + self.__update_ball_placement() + + def __update_ball_placement_status(self, ball_state: BallState) -> None: + """Update ball placement circle color corresponding to ball position. + If the ball lies inside the tolerance circle, the circle will be green, otherwise red. + :param ball_state: state of the ball + """ + if tbots_cpp.contains( + self.ball_placement_tolerance_circle, + tbots_cpp.createPoint(ball_state.global_position), + ): + self.placement_tolerance_graphic.set_outline_color( + self.BALL_PLACEMENT_TOLERANCE_VISUALIZATION_COLOR_ON + ) + else: + self.placement_tolerance_graphic.set_outline_color( + self.BALL_PLACEMENT_TOLERANCE_VISUALIZATION_COLOR_OFF + ) + + def __update_target_visual(self) -> None: + """Update the ball placement target graphic to shrink or expand""" + if self.shrink_target: + self.placement_target_graphic.set_radius( + self.placement_target_graphic.radius - 0.01 + ) + self.shrink_target = self.placement_target_graphic.radius >= 0 + else: + self.placement_target_graphic.set_radius( + self.placement_target_graphic.radius + 0.01 + ) + self.shrink_target = ( + self.placement_target_graphic.radius + >= BALL_PLACEMENT_TOLERANCE_RADIUS_METERS + ) + + # update the count-down graphics + if self.cached_referee_info: + time_left = max( + int(self.cached_referee_info["currentActionTimeRemaining"]) // 1000000, + 0, + ) + self.ball_placement_countdown_graphic.setData(text=f"{time_left}s") + + def __display_ball_placement_visuals(self, new_placement_point: Point) -> None: + """Display ball placement visuals + :param new_placement_point: ball placement point + """ + self.ball_placement_point = new_placement_point + + self.placement_tolerance_graphic.set_position( + self.ball_placement_point.x_meters, + self.ball_placement_point.y_meters, + ) + self.placement_tolerance_graphic.show() + + self.placement_target_graphic.set_position( + self.ball_placement_point.x_meters, + self.ball_placement_point.y_meters, + ) + self.placement_target_graphic.set_radius(BALL_PLACEMENT_TOLERANCE_RADIUS_METERS) + self.placement_target_graphic.show() + + self.robot_avoid_circle_graphic.set_position( + self.ball_placement_point.x_meters, + self.ball_placement_point.y_meters, + ) + self.robot_avoid_circle_graphic.show() + + self.ball_placement_countdown_graphic.setData( + text=f"{BALL_PLACEMENT_TIME_LIMIT_S}s", + pos=[ + self.ball_placement_point.x_meters + + BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS / 2, + self.ball_placement_point.y_meters + + BALL_PLACEMENT_ROBOT_AVOID_RADIUS_METERS + + 0.1, + 0, + ], + ) + self.ball_placement_countdown_graphic.show() + self.ball_placement_in_progress = True + + def __hide_ball_placement_visuals(self) -> None: + """Hide all the visuals for ball placement""" + self.placement_tolerance_graphic.hide() + self.placement_target_graphic.hide() + self.robot_avoid_circle_graphic.hide() + self.ball_placement_countdown_graphic.hide() + self.shrink_target = True diff --git a/src/software/thunderscope/gl/layers/gl_tactic_layer.py b/src/software/thunderscope/gl/layers/gl_tactic_layer.py index dca6981fec..eada4c813c 100644 --- a/src/software/thunderscope/gl/layers/gl_tactic_layer.py +++ b/src/software/thunderscope/gl/layers/gl_tactic_layer.py @@ -7,7 +7,11 @@ from proto.import_all_protos import * from software.py_constants import * -from software.thunderscope.constants import Colors, DepthValues +from software.thunderscope.constants import ( + Colors, + DepthValues, + THUNDERSCOPE_UI_FONT_NAME, +) from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer @@ -57,7 +61,8 @@ def __update_tactic_name_graphics(self, team: Team, play_info_dict) -> None: self.tactic_fsm_info_graphics.resize( len(team.team_robots), lambda: GLTextItem( - font=QtGui.QFont("Roboto", 8), color=Colors.SECONDARY_TEXT_COLOR + font=QtGui.QFont(THUNDERSCOPE_UI_FONT_NAME, 8), + color=Colors.SECONDARY_TEXT_COLOR, ), ) diff --git a/src/software/thunderscope/thunderscope_main.py b/src/software/thunderscope/thunderscope_main.py index 13cdd0276f..11bd6a4e8b 100644 --- a/src/software/thunderscope/thunderscope_main.py +++ b/src/software/thunderscope/thunderscope_main.py @@ -274,6 +274,7 @@ {"proto_class": PrimitiveSet}, {"proto_class": World}, {"proto_class": PlayInfo}, + {"proto_class": BallPlacementVisualization}, ]: proto_unix_io.attach_unix_receiver( runtime_dir, from_log_visualize=True, **arg diff --git a/src/software/thunderscope/widget_setup_functions.py b/src/software/thunderscope/widget_setup_functions.py index b3d9f2c6cd..82fc990252 100644 --- a/src/software/thunderscope/widget_setup_functions.py +++ b/src/software/thunderscope/widget_setup_functions.py @@ -28,6 +28,7 @@ gl_cost_vis_layer, gl_trail_layer, gl_max_dribble_layer, + gl_referee_info_layer, ) from software.thunderscope.common.proto_configuration_widget import ( @@ -127,6 +128,9 @@ def setup_gl_widget( max_dribble_layer = gl_max_dribble_layer.GLMaxDribbleLayer( "Dribble Tracking", visualization_buffer_size ) + referee_layer = gl_referee_info_layer.GLRefereeInfoLayer( + "Referee Info", visualization_buffer_size + ) gl_widget.add_layer(world_layer) gl_widget.add_layer(simulator_layer, False) @@ -140,6 +144,7 @@ def setup_gl_widget( gl_widget.add_layer(trail_layer, False) gl_widget.add_layer(debug_shapes_layer, True) gl_widget.add_layer(max_dribble_layer, True) + gl_widget.add_layer(referee_layer) simulation_control_toolbar = gl_widget.get_sim_control_toolbar() simulation_control_toolbar.set_speed_callback(world_layer.set_simulation_speed) @@ -189,6 +194,9 @@ def setup_gl_widget( (CostVisualization, cost_vis_layer.cost_visualization_buffer), (World, trail_layer.world_buffer), (DebugShapes, debug_shapes_layer.debug_shapes_buffer), + (Referee, referee_layer.referee_vis_buffer), + (BallPlacementVisualization, referee_layer.ball_placement_vis_buffer), + (World, referee_layer.world_buffer), ]: full_system_proto_unix_io.register_observer(*arg)