From 0b0d9788a24d5a1134f956b2b87113e3400bd468 Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Thu, 15 May 2025 22:50:04 -0700 Subject: [PATCH 1/7] Add render commands --- examples/base/game.py | 2 +- examples/custom_drawers/game.py | 8 +- gamms/VisualizationEngine/__init__.py | 2 - gamms/VisualizationEngine/artist.py | 12 ++- gamms/VisualizationEngine/default_drawers.py | 72 ++++++++++---- gamms/VisualizationEngine/no_engine.py | 8 +- gamms/VisualizationEngine/pygame_engine.py | 28 +++++- gamms/VisualizationEngine/render_command.py | 61 ++++++++++++ .../render_command_data.py | 96 +++++++++++++++++++ gamms/VisualizationEngine/render_manager.py | 2 + gamms/typing/__init__.py | 3 +- gamms/typing/artist.py | 14 ++- gamms/typing/render_command.py | 23 +++++ gamms/typing/visualization_engine.py | 12 ++- 14 files changed, 307 insertions(+), 36 deletions(-) create mode 100644 gamms/VisualizationEngine/render_command.py create mode 100644 gamms/VisualizationEngine/render_command_data.py create mode 100644 gamms/typing/render_command.py diff --git a/examples/base/game.py b/examples/base/game.py index c20be4d..0488426 100644 --- a/examples/base/game.py +++ b/examples/base/game.py @@ -18,7 +18,7 @@ ctx = gamms.create_context(vis_engine=vis_engine) -ctx.record.start(path="recording") +# ctx.record.start(path="recording") # Load the graph with open(graph_path, 'rb') as f: diff --git a/examples/custom_drawers/game.py b/examples/custom_drawers/game.py index 6ef3cfb..0655d90 100644 --- a/examples/custom_drawers/game.py +++ b/examples/custom_drawers/game.py @@ -14,6 +14,7 @@ from gamms.VisualizationEngine import Color, Shape from gamms.VisualizationEngine.artist import Artist from gamms.VisualizationEngine.default_drawers import render_rectangle +from gamms.VisualizationEngine.render_command import RenderCommand import pickle @@ -73,16 +74,17 @@ def custom_circle_drawer(ctx: IContext, data: dict): y = data.get('y') radius = data.get('radius') color = data.get('color') - ctx.visual.render_circle(x, y, radius, color) + return [RenderCommand.circle(x, y, radius, color)] + # ctx.visual.render_circle(x, y, radius, color) # Special nodes n1 = ctx.graph.graph.get_node(0) n2 = ctx.graph.graph.get_node(1) # You can create the artist directly -custom_artist1 = Artist(ctx, Shape.Circle, 5) +# custom_artist1 = Artist(ctx, Shape.Circle, 5) # Alternatively, you can use the custom drawer -# custom_artist1 = Artist(ctx, custom_circle_drawer, 5) +custom_artist1 = Artist(ctx, custom_circle_drawer, 5) custom_artist1.data['x'] = n1.x custom_artist1.data['y'] = n1.y custom_artist1.data['radius'] = 10 diff --git a/gamms/VisualizationEngine/__init__.py b/gamms/VisualizationEngine/__init__.py index e44f1d5..d338004 100644 --- a/gamms/VisualizationEngine/__init__.py +++ b/gamms/VisualizationEngine/__init__.py @@ -20,13 +20,11 @@ class Color: Brown = (210, 105, 30) Purple = (128, 0, 128) - class Space(IntEnum): World = 0 Screen = 1 Viewport = 2 - class Shape(Enum): Circle = auto() Rectangle = auto() diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index 36d7f1e..d86d3fe 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -1,10 +1,11 @@ from gamms.typing import IArtist, ArtistType, IContext from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle from gamms.VisualizationEngine import Shape -from typing import Callable, Union, Dict, Any +from gamms.VisualizationEngine.render_command import RenderCommand +from typing import Callable, Union, Dict, List, Any class Artist(IArtist): - def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, Any]], None], Shape], layer: int = 30): + def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, Any]], List[RenderCommand]], Shape], layer: int = 30): self.data = {} self._ctx = ctx @@ -13,6 +14,7 @@ def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, An self._visible = True self._will_draw = True self._artist_type = ArtistType.GENERAL + self._render_commands: List[RenderCommand] = [] if isinstance(drawer, Shape): if drawer == Shape.Circle: self._drawer = render_circle @@ -31,6 +33,10 @@ def layer_dirty(self) -> bool: def layer_dirty(self, value: bool): self._layer_dirty = value + @property + def render_commands(self) -> List[RenderCommand]: + return self._render_commands + def set_layer(self, layer: int): if self._layer == layer: return @@ -67,7 +73,7 @@ def set_artist_type(self, artist_type: ArtistType): def draw(self): try: - self._drawer(self._ctx, self.data) + self._render_commands = self._drawer(self._ctx, self.data) except Exception as e: self._ctx.logger.error(f"Error drawing artist: {e}") self._ctx.logger.debug(f"Artist data: {self.data}") \ No newline at end of file diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index 8c5608e..fdd1bae 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -1,5 +1,6 @@ from gamms.VisualizationEngine import Color from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData +from gamms.VisualizationEngine.render_command import RenderCommand from gamms.typing import IContext, OSMEdge, Node, ColorType from typing import Dict, Any, cast, List @@ -19,7 +20,9 @@ def render_circle(ctx: IContext, data: Dict[str, Any]): y = data.get('y') radius = data.get('radius') color = data.get('color', Color.Cyan) - ctx.visual.render_circle(x, y, radius, color) + render_command = RenderCommand.circle(x, y, radius, color) + return [render_command] + # ctx.visual.render_circle(x, y, radius, color) def render_rectangle(ctx: IContext, data: Dict[str, Any]): @@ -35,7 +38,9 @@ def render_rectangle(ctx: IContext, data: Dict[str, Any]): width = data.get('width') height = data.get('height') color = data.get('color', Color.Cyan) - ctx.visual.render_rectangle(x, y, width, height, color) + render_command = RenderCommand.rectangle(x, y, width, height, color) + return [render_command] + # ctx.visual.render_rectangle(x, y, width, height, color) def render_agent(ctx: IContext, data: Dict[str, Any]): """ @@ -84,7 +89,9 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): point2 = (position[0] + size * math.cos(angle + 2.5), position[1] + size * math.sin(angle + 2.5)) point3 = (position[0] + size * math.cos(angle - 2.5), position[1] + size * math.sin(angle - 2.5)) - ctx.visual.render_polygon([point1, point2, point3], color) + render_command = RenderCommand.polygon([point1, point2, point3], color) + return [render_command] + # ctx.visual.render_polygon([point1, point2, point3], color) def render_graph(ctx: IContext, data: Dict[str, Any]): @@ -95,6 +102,7 @@ def render_graph(ctx: IContext, data: Dict[str, Any]): ctx (Context): The current simulation context. data (dict): The data containing the graph's information. """ + command_list = [] graph_data = cast(GraphData, data.get('graph_data')) node_color = graph_data.node_color node_size = graph_data.node_size @@ -103,11 +111,13 @@ def render_graph(ctx: IContext, data: Dict[str, Any]): for edge_id in ctx.graph.graph.get_edges(): edge = ctx.graph.graph.get_edge(edge_id) - _render_graph_edge(ctx, graph_data, edge, edge_color) + _render_graph_edge(ctx, graph_data, edge, edge_color, command_list) for node_id in ctx.graph.graph.get_nodes(): node = ctx.graph.graph.get_node(node_id) - _render_graph_node(ctx, node, node_color, node_size, draw_id) + _render_graph_node(ctx, node, node_color, node_size, draw_id, command_list) + + return command_list def render_input_overlay(ctx: IContext, data: Dict[str, Any]): """ @@ -117,13 +127,14 @@ def render_input_overlay(ctx: IContext, data: Dict[str, Any]): ctx (Context): The current simulation context. data (dict): The data containing the graph's information. """ + command_list = [] graph_data = cast(GraphData, data.get('graph_data')) waiting_agent_name = data.get('_waiting_agent_name', None) input_options = data.get('_input_options', {}) waiting_user_input = data.get('_waiting_user_input', False) # Break checker - if waiting_agent_name == None or waiting_user_input == False or input_options == {}: + if waiting_agent_name is None or waiting_user_input == False or input_options == {}: return graph = ctx.graph.graph @@ -134,19 +145,22 @@ def render_input_overlay(ctx: IContext, data: Dict[str, Any]): target_node_id_set = set(input_options.values()) for node in target_node_id_set: - _render_graph_node(ctx, graph.get_node(node), node_color, node_size, draw_id) + _render_graph_node(ctx, graph.get_node(node), node_color, node_size, draw_id, command_list) active_edges: List[OSMEdge] = [] for edge_id in graph.get_edges(): edge = graph.get_edge(edge_id) current_waiting_agent = ctx.agent.get_agent(waiting_agent_name) - if (edge.source == current_waiting_agent.current_node_id and edge.target in target_node_id_set): + if edge.source == current_waiting_agent.current_node_id and edge.target in target_node_id_set: active_edges.append(edge) for edge in active_edges: - _render_graph_edge(ctx, graph_data, edge, edge_color) + _render_graph_edge(ctx, graph_data, edge, edge_color, command_list) + + return command_list + -def _render_graph_edge(ctx: IContext, graph_data: GraphData, edge: OSMEdge, color: ColorType): +def _render_graph_edge(ctx: IContext, graph_data: GraphData, edge: OSMEdge, color: ColorType, command_list: List[RenderCommand]): """Draw an edge as a curve or straight line based on the linestring.""" source = ctx.graph.graph.get_node(edge.source) target = ctx.graph.graph.get_node(edge.target) @@ -160,16 +174,20 @@ def _render_graph_edge(ctx: IContext, graph_data: GraphData, edge: OSMEdge, colo edge_line_points[edge.id] = linestring line_points = edge_line_points[edge.id] - ctx.visual.render_linestring(line_points, color, is_aa=True, perform_culling_test=False) + command_list.append(RenderCommand.linestring(line_points, color, True)) + # ctx.visual.render_linestring(line_points, color, is_aa=True, perform_culling_test=False) else: - ctx.visual.render_line(source.x, source.y, target.x, target.y, color, 2, perform_culling_test=False, is_aa=False) + command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, color)) + # ctx.visual.render_line(source.x, source.y, target.x, target.y, color, 2, perform_culling_test=False, is_aa=False) -def _render_graph_node(ctx: IContext, node: Node, color: ColorType, radius: float, draw_id: bool): - ctx.visual.render_circle(node.x, node.y, radius, color) +def _render_graph_node(ctx: IContext, node: Node, color: ColorType, radius: float, draw_id: bool, command_list: List[RenderCommand]): + command_list.append(RenderCommand.circle(node.x, node.y, radius, color)) + # ctx.visual.render_circle(node.x, node.y, radius, color) if draw_id: - ctx.visual.render_text(str(node.id), node.x, node.y + 10, (0, 0, 0)) + command_list.append(RenderCommand.text(node.x, node.y + 10, str(node.id), (0, 0, 0))) + # ctx.visual.render_text(str(node.id), node.x, node.y + 10, (0, 0, 0)) def render_neighbor_sensor(ctx: IContext, data: Dict[str, Any]): @@ -180,12 +198,16 @@ def render_neighbor_sensor(ctx: IContext, data: Dict[str, Any]): ctx (Context): The current simulation context. data (Dict[str, Any]): The data containing the sensor's information. """ + command_list = [] sensor = ctx.sensor.get_sensor(data.get('name')) color = data.get('color', Color.Cyan) sensor_data = cast(List[int], sensor.data) for neighbor_node_id in sensor_data: neighbor_node = ctx.graph.graph.get_node(neighbor_node_id) - ctx.visual.render_circle(neighbor_node.x, neighbor_node.y, 2, color) + command_list.append(RenderCommand.circle(neighbor_node.x, neighbor_node.y, 2, color)) + # ctx.visual.render_circle(neighbor_node.x, neighbor_node.y, 2, color) + + return command_list def render_map_sensor(ctx: IContext, data: Dict[str, Any]): @@ -196,6 +218,7 @@ def render_map_sensor(ctx: IContext, data: Dict[str, Any]): ctx (Context): The current simulation context. data (Dict[str, Any]): The data containing the sensor's information. """ + command_list = [] sensor = ctx.sensor.get_sensor(data.get('name')) node_color = data.get('node_color', Color.Cyan) sensor_data = cast(Dict[str, Any], sensor.data) @@ -204,7 +227,8 @@ def render_map_sensor(ctx: IContext, data: Dict[str, Any]): sensed_nodes = list(sensed_nodes.keys()) for node_id in sensed_nodes: node = ctx.graph.graph.get_node(node_id) - ctx.visual.render_circle(node.x, node.y, 1, node_color) + command_list.append(RenderCommand.circle(node.x, node.y, 1, node_color)) + # ctx.visual.render_circle(node.x, node.y, 1, node_color) edge_color = data.get('edge_color', Color.Cyan) sensed_edges = sensor_data.get('edges', []) @@ -218,9 +242,13 @@ def render_map_sensor(ctx: IContext, data: Dict[str, Any]): line_points = ([(source.x, source.y)] + [(x, y) for (x, y) in edge.linestring.coords] + [(target.x, target.y)]) - ctx.visual.render_linestring(line_points, edge_color, 4, is_aa=False, perform_culling_test=False) + command_list.append(RenderCommand.linestring(line_points, edge_color)) + # ctx.visual.render_linestring(line_points, edge_color, 4, is_aa=False, perform_culling_test=False) else: - ctx.visual.render_line(source.x, source.y, target.x, target.y, edge_color, 4, perform_culling_test=False) + command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, edge_color)) + # ctx.visual.render_line(source.x, source.y, target.x, target.y, edge_color, 4, perform_culling_test=False) + + return command_list def render_agent_sensor(ctx: IContext, data: Dict[str, Any]): @@ -231,6 +259,7 @@ def render_agent_sensor(ctx: IContext, data: Dict[str, Any]): ctx (Context): The current simulation context. data (Dict[str, Any]): The data containing the sensor's information. """ + command_list = [] sensor = ctx.sensor.get_sensor(data.get('name')) color = data.get('color', Color.Cyan) size = data.get('size', 8) @@ -244,4 +273,7 @@ def render_agent_sensor(ctx: IContext, data: Dict[str, Any]): point2 = (position[0] + size * math.cos(angle + 2.5), position[1] + size * math.sin(angle + 2.5)) point3 = (position[0] + size * math.cos(angle - 2.5), position[1] + size * math.sin(angle - 2.5)) - ctx.visual.render_polygon([point1, point2, point3], color) \ No newline at end of file + command_list.append(RenderCommand.polygon([point1, point2, point3], color)) + # ctx.visual.render_polygon([point1, point2, point3], color) + + return command_list \ No newline at end of file diff --git a/gamms/VisualizationEngine/no_engine.py b/gamms/VisualizationEngine/no_engine.py index dffbf62..24c40e3 100644 --- a/gamms/VisualizationEngine/no_engine.py +++ b/gamms/VisualizationEngine/no_engine.py @@ -2,7 +2,8 @@ IArtist, IContext, IVisualizationEngine, - ColorType + ColorType, + IRenderCommand ) from gamms.typing.opcodes import OpCodes from gamms.VisualizationEngine.artist import Artist @@ -36,6 +37,9 @@ def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArti def remove_artist(self, name: str): return + def execute_command_list(self, command_list: List[IRenderCommand]) -> None: + pass + def simulate(self): if self.ctx.record.record(): self.ctx.record.write(opCode=OpCodes.SIMULATE, data={}) @@ -59,7 +63,7 @@ def render_circle(self, x: float, y: float, radius: float, color: ColorType = Co return def render_line(self, start_x: float, start_y: float, end_x: float, end_y: float, color: ColorType = Color.Black, - width: int=1, is_aa: bool=False, perform_culling_test: bool=True, force_no_aa: bool = False): + width: int=1, is_aa: bool=False, perform_culling_test: bool=True): return def render_linestring(self, points: List[Tuple[float, float]], color: ColorType =Color.Black, width: int=1, closed: bool = False, diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index 0bf233d..a55dab5 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -1,6 +1,8 @@ from gamms.VisualizationEngine import Color, Space, Shape, Artist, lazy from gamms.VisualizationEngine.render_manager import RenderManager from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData +from gamms.VisualizationEngine.render_command import RenderCommand +from gamms.VisualizationEngine.render_command_data import * from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle, \ render_agent, render_graph, render_neighbor_sensor, render_map_sensor, render_agent_sensor, render_input_overlay from gamms.typing import ( @@ -10,7 +12,8 @@ IContext, SensorType, OpCodes, - ColorType + ColorType, + RenderOpCode ) from typing import Dict, Any, List, Tuple, Union, cast @@ -188,6 +191,27 @@ def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArti def remove_artist(self, name: str): self._render_manager.remove_artist(name) + def execute_command_list(self, command_list: List[RenderCommand]): + for command in command_list: + if command.opcode == RenderOpCode.RenderText: + data: TextRenderCommandData = command.data + self.render_text(data.text, data.x, data.y, data.color) + elif command.opcode == RenderOpCode.RenderRectangle: + data: RectangleRenderCommandData = command.data + self.render_rectangle(data.x, data.y, data.width, data.height, data.color) + elif command.opcode == RenderOpCode.RenderCircle: + data: CircleRenderCommandData = command.data + self.render_circle(data.x, data.y, data.radius, data.color) + elif command.opcode == RenderOpCode.RenderLine: + data: LineRenderCommandData = command.data + self.render_line(data.x1, data.y1, data.x2, data.y2, data.color) + elif command.opcode == RenderOpCode.RenderPolygon: + data: PolygonRenderCommandData = command.data + self.render_polygon(data.points, data.color) + elif command.opcode == RenderOpCode.RenderLineString: + data: LineStringRenderCommandData = command.data + self.render_linestring(data.points, data.color) + def handle_input(self): pressed_keys = self._pygame.key.get_pressed() scroll_speed = self._render_manager.camera_size / 2 @@ -373,7 +397,7 @@ def render_circle(self, x: float, y: float, radius: float, color: ColorType = Co self._pygame.draw.circle(surface, color, (x, y), radius) def render_line(self, start_x: float, start_y: float, end_x: float, end_y: float, color: ColorType = Color.Black, - width: int=1, is_aa: bool=False, perform_culling_test: bool=True, force_no_aa: bool = False): + width: int=1, is_aa: bool=False, perform_culling_test: bool=True): if perform_culling_test and self._render_manager.check_line_culled(start_x, start_y, end_x, end_y): return diff --git a/gamms/VisualizationEngine/render_command.py b/gamms/VisualizationEngine/render_command.py new file mode 100644 index 0000000..0f68aa9 --- /dev/null +++ b/gamms/VisualizationEngine/render_command.py @@ -0,0 +1,61 @@ +from gamms.VisualizationEngine.render_command_data import * +from gamms.typing import ColorType, RenderOpCode +from typing import List, Tuple + +class RenderCommand: + """ + This represents an instance of a render command + """ + + def __init__(self, op_code: RenderOpCode, data = None): + # the operation code for the command + self._opcode = op_code + + # contains all parameters for this command + self.data = data + + @property + def opcode(self) -> RenderOpCode: + return self._opcode + + def validate(self) -> int: + pass + + def __str__(self): + return f"RenderCommand({self.opcode}, {self.data})" + + @staticmethod + def circle(x: float, y: float, radius: float, color: ColorType) -> 'RenderCommand': + """Create a circle render command""" + cmd = RenderCommand(RenderOpCode.RenderCircle, CircleRenderCommandData(x, y, radius, color)) + return cmd + + @staticmethod + def rectangle(x: float, y: float, width: float, height: float, color: ColorType) -> 'RenderCommand': + """Create a rectangle render command""" + cmd = RenderCommand(RenderOpCode.RenderRectangle, RectangleRenderCommandData(x, y, width, height, color)) + return cmd + + @staticmethod + def polygon(points: List[Tuple[float, float]], color: ColorType) -> 'RenderCommand': + """Create a polygon render command""" + cmd = RenderCommand(RenderOpCode.RenderPolygon, PolygonRenderCommandData(points, color)) + return cmd + + @staticmethod + def line(x1: float, y1: float, x2: float, y2: float, color: ColorType) -> 'RenderCommand': + """Create a line render command""" + cmd = RenderCommand(RenderOpCode.RenderLine, LineRenderCommandData(x1, y1, x2, y2, color)) + return cmd + + @staticmethod + def linestring(points: List[Tuple[float, float]], color: ColorType, is_aa: bool = True) -> 'RenderCommand': + """Create a linestring render command""" + cmd = RenderCommand(RenderOpCode.RenderLineString, LineStringRenderCommandData(points, color, is_aa)) + return cmd + + @staticmethod + def text(x: float, y: float, text: str, color: ColorType) -> 'RenderCommand': + """Create a text render command""" + cmd = RenderCommand(RenderOpCode.RenderText, TextRenderCommandData(x, y, text, color)) + return cmd diff --git a/gamms/VisualizationEngine/render_command_data.py b/gamms/VisualizationEngine/render_command_data.py new file mode 100644 index 0000000..6165f36 --- /dev/null +++ b/gamms/VisualizationEngine/render_command_data.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass, field +from gamms.typing import ColorType +from typing import List, Tuple, Dict, Optional + +@dataclass() +class CircleRenderCommandData: + """ + Contains all necessary data for drawing a circle. + + Attributes: + x (float): The x-coordinate of the circle's center. + y (float): The y-coordinate of the circle's center. + radius (float): The radius of the circle. + color (ColorType): The color of the circle. + """ + x: float + y: float + radius: float + color: ColorType + +@dataclass() +class RectangleRenderCommandData: + """ + Contains all necessary data for drawing a rectangle. + + Attributes: + x (float): The x-coordinate of the rectangle's center. + y (float): The y-coordinate of the rectangle's center. + width (float): The width of the rectangle. + height (float): The height of the rectangle. + color (ColorType): The color of the rectangle. + """ + x: float + y: float + width: float + height: float + color: ColorType + +@dataclass() +class PolygonRenderCommandData: + """ + Contains all necessary data for drawing a polygon. + + Attributes: + points (List[Tuple[float, float]]): A list of points representing the vertices of the polygon. + color (ColorType): The color of the polygon. + """ + points: List[Tuple[float, float]] + color: ColorType + +@dataclass() +class LineRenderCommandData: + """ + Contains all necessary data for drawing a line. + + Attributes: + x1 (float): The x-coordinate of the start point of the line. + y1 (float): The y-coordinate of the start point of the line. + x2 (float): The x-coordinate of the end point of the line. + y2 (float): The y-coordinate of the end point of the line. + color (ColorType): The color of the line. + """ + x1: float + y1: float + x2: float + y2: float + color: ColorType + +@dataclass() +class LineStringRenderCommandData: + """ + Contains all necessary data for drawing a linestring. + + Attributes: + points (List[Tuple[float, float]]): A list of points representing the vertices of the linestring. + color (ColorType): The color of the linestring. + """ + points: List[Tuple[float, float]] + color: ColorType + is_aa: bool + +@dataclass() +class TextRenderCommandData: + """ + Contains all necessary data for drawing text. + + Attributes: + x (float): The x-coordinate of the text's position. + y (float): The y-coordinate of the text's position. + text (str): The text to be drawn. + color (ColorType): The color of the text. + """ + x: float + y: float + text: str + color: ColorType \ No newline at end of file diff --git a/gamms/VisualizationEngine/render_manager.py b/gamms/VisualizationEngine/render_manager.py index 494f063..6ae81ca 100644 --- a/gamms/VisualizationEngine/render_manager.py +++ b/gamms/VisualizationEngine/render_manager.py @@ -248,6 +248,7 @@ def render_single_artist(self, artist_name: str): self._current_drawing_artist = artist artist.draw() + self.ctx.visual.execute_command_list(artist.render_commands) self._current_drawing_artist = None def handle_render(self): @@ -277,4 +278,5 @@ def handle_render(self): self._current_drawing_artist = artist artist.draw() + self.ctx.visual.execute_command_list(artist.render_commands) self._current_drawing_artist = None \ No newline at end of file diff --git a/gamms/typing/__init__.py b/gamms/typing/__init__.py index 3bfa5e2..a05a58a 100644 --- a/gamms/typing/__init__.py +++ b/gamms/typing/__init__.py @@ -10,4 +10,5 @@ from gamms.typing.recorder import IRecorder from gamms.typing.logger import ILogger from gamms.typing.context import IContext -from gamms.typing.opcodes import OpCodes \ No newline at end of file +from gamms.typing.opcodes import OpCodes +from gamms.typing.render_command import IRenderCommand, RenderOpCode \ No newline at end of file diff --git a/gamms/typing/artist.py b/gamms/typing/artist.py index b0f9893..3f2a9a5 100644 --- a/gamms/typing/artist.py +++ b/gamms/typing/artist.py @@ -1,4 +1,5 @@ -from typing import Dict, Any, Callable, Union, Optional +from gamms.typing.render_command import IRenderCommand +from typing import Dict, Any, Callable, Union, Optional, List from abc import ABC, abstractmethod from enum import Enum, auto @@ -145,4 +146,15 @@ def layer_dirty(self, value: bool) -> None: Args: value (bool): The dirty state to set. """ + pass + + @property + @abstractmethod + def render_commands(self) -> List[IRenderCommand]: + """ + Get the render commands for the artist. + + Returns: + List[RenderCommand]: The list of render commands. + """ pass \ No newline at end of file diff --git a/gamms/typing/render_command.py b/gamms/typing/render_command.py new file mode 100644 index 0000000..06de89c --- /dev/null +++ b/gamms/typing/render_command.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +from enum import Enum, auto + +class RenderOpCode(Enum): + RenderCircle = auto() + RenderRectangle = auto() + RenderPolygon = auto() + RenderLine = auto() + RenderLineString = auto() + RenderText = auto() + +class IRenderCommand(ABC): + + @property + @abstractmethod + def opcode(self) -> RenderOpCode: + """ + Get the operation code for the command. + + Returns: + RenderOpCode: The operation code for the command. + """ + pass \ No newline at end of file diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index a413df8..b16d338 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -1,4 +1,4 @@ -from gamms.typing.artist import IArtist +from gamms.typing import IArtist, IRenderCommand from typing import Dict, Any, List, Tuple, Union from abc import ABC, abstractmethod @@ -109,6 +109,16 @@ def remove_artist(self, name: str) -> None: """ pass + @abstractmethod + def execute_command_list(self, command_list: List[IRenderCommand]) -> None: + """ + Execute a list of rendering commands. + + Args: + command_list (List[RenderCommand]): A list of rendering commands to execute. + """ + pass + @abstractmethod def simulate(self) -> None: """ From 74efbc42fdc27983c13e66cb64fd2d1085f344bc Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Sat, 17 May 2025 12:32:54 -0700 Subject: [PATCH 2/7] Update artist api --- gamms/VisualizationEngine/artist.py | 25 ++++++++++------ gamms/VisualizationEngine/pygame_engine.py | 6 ++-- gamms/VisualizationEngine/render_manager.py | 18 ++++++++---- gamms/typing/artist.py | 32 +++++++++++++++------ gamms/typing/visualization_engine.py | 3 +- 5 files changed, 57 insertions(+), 27 deletions(-) diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index d86d3fe..ca06633 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -12,7 +12,7 @@ def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, An self._layer = layer self._layer_dirty = False self._visible = True - self._will_draw = True + self._is_rendering = True self._artist_type = ArtistType.GENERAL self._render_commands: List[RenderCommand] = [] if isinstance(drawer, Shape): @@ -50,7 +50,7 @@ def get_layer(self) -> int: def set_visible(self, visible: bool): self._visible = visible - def get_visible(self) -> bool: + def is_visible(self) -> bool: return self._visible def set_drawer(self, drawer: Callable[[IContext, Dict[str, Any]], None]): @@ -59,11 +59,11 @@ def set_drawer(self, drawer: Callable[[IContext, Dict[str, Any]], None]): def get_drawer(self) -> Callable[[IContext, Dict[str, Any]], None]: return self._drawer - def get_will_draw(self) -> bool: - return self._will_draw + def is_rendering(self) -> bool: + return self._is_rendering - def set_will_draw(self, will_draw: bool): - self._will_draw = will_draw + def set_rendering(self, is_rendering: bool): + self._is_rendering = is_rendering def get_artist_type(self) -> ArtistType: return self._artist_type @@ -71,9 +71,18 @@ def get_artist_type(self) -> ArtistType: def set_artist_type(self, artist_type: ArtistType): self._artist_type = artist_type - def draw(self): + def draw(self, force = False): + if self._is_rendering and not force: + return + try: self._render_commands = self._drawer(self._ctx, self.data) except Exception as e: self._ctx.logger.error(f"Error drawing artist: {e}") - self._ctx.logger.debug(f"Artist data: {self.data}") \ No newline at end of file + self._ctx.logger.debug(f"Artist data: {self.data}") + + def clear(self): + if self._is_rendering: + return + + self._render_commands.clear() \ No newline at end of file diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index a55dab5..122d611 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -3,8 +3,8 @@ from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData from gamms.VisualizationEngine.render_command import RenderCommand from gamms.VisualizationEngine.render_command_data import * -from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle, \ - render_agent, render_graph, render_neighbor_sensor, render_map_sensor, render_agent_sensor, render_input_overlay +from gamms.VisualizationEngine.default_drawers import (render_agent, render_graph, render_neighbor_sensor, + render_map_sensor, render_agent_sensor, render_input_overlay) from gamms.typing import ( IVisualizationEngine, IArtist, @@ -84,7 +84,7 @@ def set_graph_visual(self, **kwargs: Dict[str, Any]) -> IArtist: artist = Artist(self.ctx, render_graph, 10) artist.data['graph_data'] = graph_data - artist.set_will_draw(False) + artist.set_rendering(False) artist.set_artist_type(ArtistType.GRAPH) #Add data for node ID and Color diff --git a/gamms/VisualizationEngine/render_manager.py b/gamms/VisualizationEngine/render_manager.py index 6ae81ca..27f177f 100644 --- a/gamms/VisualizationEngine/render_manager.py +++ b/gamms/VisualizationEngine/render_manager.py @@ -267,16 +267,22 @@ def handle_render(self): for layer, artist_name_list in self._layer_artists.items(): for artist_name in artist_name_list: artist = self._artists[artist_name] - if not artist.get_visible(): + if not artist.is_visible(): continue - if not artist.get_will_draw(): + self._current_drawing_artist = artist + + if artist.is_visible(): + # Update the render commands + if artist.is_rendering(): + artist.draw(True) + + # Execute the render commands + self.ctx.visual.execute_command_list(artist.render_commands) + + # Render the layer if it is a graph artist and not already rendered if artist.get_artist_type() == ArtistType.GRAPH and layer not in rendered_layers: self.ctx.visual.render_layer(layer) rendered_layers.add(layer) - continue - self._current_drawing_artist = artist - artist.draw() - self.ctx.visual.execute_command_list(artist.render_commands) self._current_drawing_artist = None \ No newline at end of file diff --git a/gamms/typing/artist.py b/gamms/typing/artist.py index 3f2a9a5..fb41e89 100644 --- a/gamms/typing/artist.py +++ b/gamms/typing/artist.py @@ -50,7 +50,7 @@ def set_visible(self, visible: bool) -> None: pass @abstractmethod - def get_visible(self) -> bool: + def is_visible(self) -> bool: """ Get the visibility of the artist. @@ -80,22 +80,23 @@ def get_drawer(self) -> Optional[Callable[["IContext", Dict[str, Any]], None]]: pass @abstractmethod - def get_will_draw(self) -> bool: + def is_rendering(self) -> bool: """ - Get whether the artist will draw. + Get whether the artist is rendering. If the artist is not rendering, its content will still be drawn but not updated. + To hide the artist, use the set_visible method. Returns: - bool: True if the artist will draw, False otherwise. + bool: True if the artist is rendering, False otherwise. """ pass @abstractmethod - def set_will_draw(self, will_draw: bool) -> None: + def set_rendering(self, is_rendering: bool) -> None: """ - Set whether the artist will draw. + Set whether the artist is rendering. Args: - will_draw (bool): The will_draw state to set. + is_rendering (bool): The is_rendering state to set. """ pass @@ -120,9 +121,22 @@ def set_artist_type(self, artist_type: ArtistType) -> None: pass @abstractmethod - def draw(self) -> None: + def draw(self, force = False) -> None: """ - Draw the artist immediately. + Draw the artist immediately. Note that if the artist is invisible, it will remain invisible. + Later when the artist is set to visible, its content will be the updated content. + This method has no effect if the is_rendering attribute is True because the artist is already updating every frame. + + Args: + force (bool): If True, force the artist to draw. + """ + pass + + @abstractmethod + def clear(self) -> None: + """ + Clear the artist's data and reset its state. + This method has no effect if the is_rendering attribute is True because the artist will update again on the next frame. """ pass diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index b16d338..7c38ebb 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -1,4 +1,5 @@ -from gamms.typing import IArtist, IRenderCommand +from gamms.typing import IArtist +from gamms.typing.render_command import IRenderCommand from typing import Dict, Any, List, Tuple, Union from abc import ABC, abstractmethod From 9cbe875117b8051896fdeddd74131a672ed26c5f Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Fri, 20 Jun 2025 10:30:51 +0800 Subject: [PATCH 3/7] Revert recording --- examples/base/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/base/game.py b/examples/base/game.py index 0488426..c20be4d 100644 --- a/examples/base/game.py +++ b/examples/base/game.py @@ -18,7 +18,7 @@ ctx = gamms.create_context(vis_engine=vis_engine) -# ctx.record.start(path="recording") +ctx.record.start(path="recording") # Load the graph with open(graph_path, 'rb') as f: From 3f344300c3e0d9978fb750b0cacefeb373956686 Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Fri, 20 Jun 2025 11:43:31 +0800 Subject: [PATCH 4/7] Code cleanup --- examples/custom_drawers/game.py | 1 - gamms/VisualizationEngine/default_drawers.py | 22 ++++--------- gamms/VisualizationEngine/pygame_engine.py | 12 +++---- gamms/VisualizationEngine/render_command.py | 24 +++++++------- .../render_command_data.py | 33 +++++++++++++++---- gamms/VisualizationEngine/render_manager.py | 3 +- 6 files changed, 53 insertions(+), 42 deletions(-) diff --git a/examples/custom_drawers/game.py b/examples/custom_drawers/game.py index 0655d90..91d7031 100644 --- a/examples/custom_drawers/game.py +++ b/examples/custom_drawers/game.py @@ -75,7 +75,6 @@ def custom_circle_drawer(ctx: IContext, data: dict): radius = data.get('radius') color = data.get('color') return [RenderCommand.circle(x, y, radius, color)] - # ctx.visual.render_circle(x, y, radius, color) # Special nodes n1 = ctx.graph.graph.get_node(0) diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index fdd1bae..a4d5a08 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -22,7 +22,6 @@ def render_circle(ctx: IContext, data: Dict[str, Any]): color = data.get('color', Color.Cyan) render_command = RenderCommand.circle(x, y, radius, color) return [render_command] - # ctx.visual.render_circle(x, y, radius, color) def render_rectangle(ctx: IContext, data: Dict[str, Any]): @@ -40,7 +39,7 @@ def render_rectangle(ctx: IContext, data: Dict[str, Any]): color = data.get('color', Color.Cyan) render_command = RenderCommand.rectangle(x, y, width, height, color) return [render_command] - # ctx.visual.render_rectangle(x, y, width, height, color) + def render_agent(ctx: IContext, data: Dict[str, Any]): """ @@ -91,7 +90,6 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): render_command = RenderCommand.polygon([point1, point2, point3], color) return [render_command] - # ctx.visual.render_polygon([point1, point2, point3], color) def render_graph(ctx: IContext, data: Dict[str, Any]): @@ -119,6 +117,7 @@ def render_graph(ctx: IContext, data: Dict[str, Any]): return command_list + def render_input_overlay(ctx: IContext, data: Dict[str, Any]): """ Render the graph by drawing its nodes and edges on the screen. This is the default rendering method for graphs. @@ -174,20 +173,16 @@ def _render_graph_edge(ctx: IContext, graph_data: GraphData, edge: OSMEdge, colo edge_line_points[edge.id] = linestring line_points = edge_line_points[edge.id] - command_list.append(RenderCommand.linestring(line_points, color, True)) - # ctx.visual.render_linestring(line_points, color, is_aa=True, perform_culling_test=False) + command_list.append(RenderCommand.linestring(line_points, color, perform_culling_test=False)) else: - command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, color)) - # ctx.visual.render_line(source.x, source.y, target.x, target.y, color, 2, perform_culling_test=False, is_aa=False) + command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, color, 2, is_aa=False, perform_culling_test=False)) def _render_graph_node(ctx: IContext, node: Node, color: ColorType, radius: float, draw_id: bool, command_list: List[RenderCommand]): command_list.append(RenderCommand.circle(node.x, node.y, radius, color)) - # ctx.visual.render_circle(node.x, node.y, radius, color) if draw_id: command_list.append(RenderCommand.text(node.x, node.y + 10, str(node.id), (0, 0, 0))) - # ctx.visual.render_text(str(node.id), node.x, node.y + 10, (0, 0, 0)) def render_neighbor_sensor(ctx: IContext, data: Dict[str, Any]): @@ -205,7 +200,6 @@ def render_neighbor_sensor(ctx: IContext, data: Dict[str, Any]): for neighbor_node_id in sensor_data: neighbor_node = ctx.graph.graph.get_node(neighbor_node_id) command_list.append(RenderCommand.circle(neighbor_node.x, neighbor_node.y, 2, color)) - # ctx.visual.render_circle(neighbor_node.x, neighbor_node.y, 2, color) return command_list @@ -228,7 +222,6 @@ def render_map_sensor(ctx: IContext, data: Dict[str, Any]): for node_id in sensed_nodes: node = ctx.graph.graph.get_node(node_id) command_list.append(RenderCommand.circle(node.x, node.y, 1, node_color)) - # ctx.visual.render_circle(node.x, node.y, 1, node_color) edge_color = data.get('edge_color', Color.Cyan) sensed_edges = sensor_data.get('edges', []) @@ -242,11 +235,9 @@ def render_map_sensor(ctx: IContext, data: Dict[str, Any]): line_points = ([(source.x, source.y)] + [(x, y) for (x, y) in edge.linestring.coords] + [(target.x, target.y)]) - command_list.append(RenderCommand.linestring(line_points, edge_color)) - # ctx.visual.render_linestring(line_points, edge_color, 4, is_aa=False, perform_culling_test=False) + command_list.append(RenderCommand.linestring(line_points, edge_color, 4, is_aa=False, perform_culling_test=False)) else: - command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, edge_color)) - # ctx.visual.render_line(source.x, source.y, target.x, target.y, edge_color, 4, perform_culling_test=False) + command_list.append(RenderCommand.line(source.x, source.y, target.x, target.y, edge_color, 4, perform_culling_test=False)) return command_list @@ -274,6 +265,5 @@ def render_agent_sensor(ctx: IContext, data: Dict[str, Any]): point3 = (position[0] + size * math.cos(angle - 2.5), position[1] + size * math.sin(angle - 2.5)) command_list.append(RenderCommand.polygon([point1, point2, point3], color)) - # ctx.visual.render_polygon([point1, point2, point3], color) return command_list \ No newline at end of file diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index 122d611..7fafaea 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -195,22 +195,22 @@ def execute_command_list(self, command_list: List[RenderCommand]): for command in command_list: if command.opcode == RenderOpCode.RenderText: data: TextRenderCommandData = command.data - self.render_text(data.text, data.x, data.y, data.color) + self.render_text(data.text, data.x, data.y, data.color, data.perform_culling_test) elif command.opcode == RenderOpCode.RenderRectangle: data: RectangleRenderCommandData = command.data - self.render_rectangle(data.x, data.y, data.width, data.height, data.color) + self.render_rectangle(data.x, data.y, data.width, data.height, data.color, data.perform_culling_test) elif command.opcode == RenderOpCode.RenderCircle: data: CircleRenderCommandData = command.data - self.render_circle(data.x, data.y, data.radius, data.color) + self.render_circle(data.x, data.y, data.radius, data.color, data.perform_culling_test) elif command.opcode == RenderOpCode.RenderLine: data: LineRenderCommandData = command.data - self.render_line(data.x1, data.y1, data.x2, data.y2, data.color) + self.render_line(data.x1, data.y1, data.x2, data.y2, data.color, data.width, data.is_aa, data.perform_culling_test) elif command.opcode == RenderOpCode.RenderPolygon: data: PolygonRenderCommandData = command.data - self.render_polygon(data.points, data.color) + self.render_polygon(data.points, data.color, data.width, data.perform_culling_test) elif command.opcode == RenderOpCode.RenderLineString: data: LineStringRenderCommandData = command.data - self.render_linestring(data.points, data.color) + self.render_linestring(data.points, data.color, data.width, data.closed, data.is_aa, data.perform_culling_test) def handle_input(self): pressed_keys = self._pygame.key.get_pressed() diff --git a/gamms/VisualizationEngine/render_command.py b/gamms/VisualizationEngine/render_command.py index 0f68aa9..d5bf51a 100644 --- a/gamms/VisualizationEngine/render_command.py +++ b/gamms/VisualizationEngine/render_command.py @@ -25,37 +25,37 @@ def __str__(self): return f"RenderCommand({self.opcode}, {self.data})" @staticmethod - def circle(x: float, y: float, radius: float, color: ColorType) -> 'RenderCommand': + def circle(x: float, y: float, radius: float, color: ColorType, perform_culling_test: bool=True) -> 'RenderCommand': """Create a circle render command""" - cmd = RenderCommand(RenderOpCode.RenderCircle, CircleRenderCommandData(x, y, radius, color)) + cmd = RenderCommand(RenderOpCode.RenderCircle, CircleRenderCommandData(perform_culling_test, x, y, radius, color)) return cmd @staticmethod - def rectangle(x: float, y: float, width: float, height: float, color: ColorType) -> 'RenderCommand': + def rectangle(x: float, y: float, width: float, height: float, color: ColorType, perform_culling_test: bool=True) -> 'RenderCommand': """Create a rectangle render command""" - cmd = RenderCommand(RenderOpCode.RenderRectangle, RectangleRenderCommandData(x, y, width, height, color)) + cmd = RenderCommand(RenderOpCode.RenderRectangle, RectangleRenderCommandData(perform_culling_test, x, y, width, height, color)) return cmd @staticmethod - def polygon(points: List[Tuple[float, float]], color: ColorType) -> 'RenderCommand': + def polygon(points: List[Tuple[float, float]], color: ColorType, width: float=0, perform_culling_test: bool=True) -> 'RenderCommand': """Create a polygon render command""" - cmd = RenderCommand(RenderOpCode.RenderPolygon, PolygonRenderCommandData(points, color)) + cmd = RenderCommand(RenderOpCode.RenderPolygon, PolygonRenderCommandData(perform_culling_test, points, color, width)) return cmd @staticmethod - def line(x1: float, y1: float, x2: float, y2: float, color: ColorType) -> 'RenderCommand': + def line(x1: float, y1: float, x2: float, y2: float, color: ColorType, width: float=1.0, is_aa: bool=False, perform_culling_test: bool=True) -> 'RenderCommand': """Create a line render command""" - cmd = RenderCommand(RenderOpCode.RenderLine, LineRenderCommandData(x1, y1, x2, y2, color)) + cmd = RenderCommand(RenderOpCode.RenderLine, LineRenderCommandData(perform_culling_test, x1, y1, x2, y2, color, width, is_aa)) return cmd @staticmethod - def linestring(points: List[Tuple[float, float]], color: ColorType, is_aa: bool = True) -> 'RenderCommand': + def linestring(points: List[Tuple[float, float]], color: ColorType, width: float=1.0, closed: bool=False, is_aa: bool=True, perform_culling_test: bool=True) -> 'RenderCommand': """Create a linestring render command""" - cmd = RenderCommand(RenderOpCode.RenderLineString, LineStringRenderCommandData(points, color, is_aa)) + cmd = RenderCommand(RenderOpCode.RenderLineString, LineStringRenderCommandData(perform_culling_test, points, color, width, closed, is_aa)) return cmd @staticmethod - def text(x: float, y: float, text: str, color: ColorType) -> 'RenderCommand': + def text(x: float, y: float, text: str, color: ColorType, perform_culling_test: bool=True) -> 'RenderCommand': """Create a text render command""" - cmd = RenderCommand(RenderOpCode.RenderText, TextRenderCommandData(x, y, text, color)) + cmd = RenderCommand(RenderOpCode.RenderText, TextRenderCommandData(perform_culling_test, x, y, text, color)) return cmd diff --git a/gamms/VisualizationEngine/render_command_data.py b/gamms/VisualizationEngine/render_command_data.py index 6165f36..bc00156 100644 --- a/gamms/VisualizationEngine/render_command_data.py +++ b/gamms/VisualizationEngine/render_command_data.py @@ -3,7 +3,17 @@ from typing import List, Tuple, Dict, Optional @dataclass() -class CircleRenderCommandData: +class BaseRenderCommandData: + """ + Base class for all render command data. + + Attributes: + perform_culling_test (bool): Whether to perform culling test for this render command. + """ + perform_culling_test: bool + +@dataclass() +class CircleRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing a circle. @@ -19,7 +29,7 @@ class CircleRenderCommandData: color: ColorType @dataclass() -class RectangleRenderCommandData: +class RectangleRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing a rectangle. @@ -37,19 +47,21 @@ class RectangleRenderCommandData: color: ColorType @dataclass() -class PolygonRenderCommandData: +class PolygonRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing a polygon. Attributes: points (List[Tuple[float, float]]): A list of points representing the vertices of the polygon. color (ColorType): The color of the polygon. + width (float): The width of the polygon's edges. """ points: List[Tuple[float, float]] color: ColorType + width: float @dataclass() -class LineRenderCommandData: +class LineRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing a line. @@ -59,28 +71,37 @@ class LineRenderCommandData: x2 (float): The x-coordinate of the end point of the line. y2 (float): The y-coordinate of the end point of the line. color (ColorType): The color of the line. + width (float): The width of the line. + is_aa (bool): Whether the line should be anti-aliased line. """ x1: float y1: float x2: float y2: float color: ColorType + width: float + is_aa: bool @dataclass() -class LineStringRenderCommandData: +class LineStringRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing a linestring. Attributes: points (List[Tuple[float, float]]): A list of points representing the vertices of the linestring. color (ColorType): The color of the linestring. + width (float): The width of the linestring. + closed (bool): Whether the linestring should be closed (i.e., the last point connects to the first). + is_aa (bool): Whether the linestring should be anti-aliased linestring. """ points: List[Tuple[float, float]] color: ColorType + width: float + closed: bool is_aa: bool @dataclass() -class TextRenderCommandData: +class TextRenderCommandData(BaseRenderCommandData): """ Contains all necessary data for drawing text. diff --git a/gamms/VisualizationEngine/render_manager.py b/gamms/VisualizationEngine/render_manager.py index 27f177f..900e256 100644 --- a/gamms/VisualizationEngine/render_manager.py +++ b/gamms/VisualizationEngine/render_manager.py @@ -278,7 +278,8 @@ def handle_render(self): artist.draw(True) # Execute the render commands - self.ctx.visual.execute_command_list(artist.render_commands) + if artist.get_artist_type() != ArtistType.GRAPH: + self.ctx.visual.execute_command_list(artist.render_commands) # Render the layer if it is a graph artist and not already rendered if artist.get_artist_type() == ArtistType.GRAPH and layer not in rendered_layers: From 95f602fb2141029c315c31321d5c092ffa8fcfb5 Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Fri, 20 Jun 2025 12:08:56 +0800 Subject: [PATCH 5/7] Fix typing --- gamms/VisualizationEngine/artist.py | 9 +++---- gamms/VisualizationEngine/render_command.py | 4 +-- .../render_command_data.py | 12 ++++----- gamms/VisualizationEngine/render_manager.py | 25 +++++++++---------- gamms/typing/artist.py | 2 +- gamms/typing/visualization_engine.py | 2 +- 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index ca06633..f717c49 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -1,11 +1,10 @@ -from gamms.typing import IArtist, ArtistType, IContext +from gamms.typing import IArtist, ArtistType, IContext, IRenderCommand from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle from gamms.VisualizationEngine import Shape -from gamms.VisualizationEngine.render_command import RenderCommand from typing import Callable, Union, Dict, List, Any class Artist(IArtist): - def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, Any]], List[RenderCommand]], Shape], layer: int = 30): + def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, Any]], List[IRenderCommand]], Shape], layer: int = 30): self.data = {} self._ctx = ctx @@ -14,7 +13,7 @@ def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, An self._visible = True self._is_rendering = True self._artist_type = ArtistType.GENERAL - self._render_commands: List[RenderCommand] = [] + self._render_commands: List[IRenderCommand] = [] if isinstance(drawer, Shape): if drawer == Shape.Circle: self._drawer = render_circle @@ -34,7 +33,7 @@ def layer_dirty(self, value: bool): self._layer_dirty = value @property - def render_commands(self) -> List[RenderCommand]: + def render_commands(self) -> List[IRenderCommand]: return self._render_commands def set_layer(self, layer: int): diff --git a/gamms/VisualizationEngine/render_command.py b/gamms/VisualizationEngine/render_command.py index d5bf51a..c495f85 100644 --- a/gamms/VisualizationEngine/render_command.py +++ b/gamms/VisualizationEngine/render_command.py @@ -1,8 +1,8 @@ from gamms.VisualizationEngine.render_command_data import * -from gamms.typing import ColorType, RenderOpCode +from gamms.typing import ColorType, RenderOpCode, IRenderCommand from typing import List, Tuple -class RenderCommand: +class RenderCommand(IRenderCommand): """ This represents an instance of a render command """ diff --git a/gamms/VisualizationEngine/render_command_data.py b/gamms/VisualizationEngine/render_command_data.py index bc00156..d9b8cc9 100644 --- a/gamms/VisualizationEngine/render_command_data.py +++ b/gamms/VisualizationEngine/render_command_data.py @@ -54,11 +54,11 @@ class PolygonRenderCommandData(BaseRenderCommandData): Attributes: points (List[Tuple[float, float]]): A list of points representing the vertices of the polygon. color (ColorType): The color of the polygon. - width (float): The width of the polygon's edges. + width (int): The width of the polygon's edges. """ points: List[Tuple[float, float]] color: ColorType - width: float + width: int @dataclass() class LineRenderCommandData(BaseRenderCommandData): @@ -71,7 +71,7 @@ class LineRenderCommandData(BaseRenderCommandData): x2 (float): The x-coordinate of the end point of the line. y2 (float): The y-coordinate of the end point of the line. color (ColorType): The color of the line. - width (float): The width of the line. + width (int): The width of the line. is_aa (bool): Whether the line should be anti-aliased line. """ x1: float @@ -79,7 +79,7 @@ class LineRenderCommandData(BaseRenderCommandData): x2: float y2: float color: ColorType - width: float + width: int is_aa: bool @dataclass() @@ -90,13 +90,13 @@ class LineStringRenderCommandData(BaseRenderCommandData): Attributes: points (List[Tuple[float, float]]): A list of points representing the vertices of the linestring. color (ColorType): The color of the linestring. - width (float): The width of the linestring. + width (int): The width of the linestring. closed (bool): Whether the linestring should be closed (i.e., the last point connects to the first). is_aa (bool): Whether the linestring should be anti-aliased linestring. """ points: List[Tuple[float, float]] color: ColorType - width: float + width: int closed: bool is_aa: bool diff --git a/gamms/VisualizationEngine/render_manager.py b/gamms/VisualizationEngine/render_manager.py index 900e256..7dec28e 100644 --- a/gamms/VisualizationEngine/render_manager.py +++ b/gamms/VisualizationEngine/render_manager.py @@ -272,18 +272,17 @@ def handle_render(self): self._current_drawing_artist = artist - if artist.is_visible(): - # Update the render commands - if artist.is_rendering(): - artist.draw(True) - - # Execute the render commands - if artist.get_artist_type() != ArtistType.GRAPH: - self.ctx.visual.execute_command_list(artist.render_commands) - - # Render the layer if it is a graph artist and not already rendered - if artist.get_artist_type() == ArtistType.GRAPH and layer not in rendered_layers: - self.ctx.visual.render_layer(layer) - rendered_layers.add(layer) + # Update the render commands + if artist.is_rendering(): + artist.draw(True) + + # Execute the render commands + if artist.get_artist_type() != ArtistType.GRAPH: + self.ctx.visual.execute_command_list(artist.render_commands) + + # Render the layer if it is a graph artist and not already rendered + if artist.get_artist_type() == ArtistType.GRAPH and layer not in rendered_layers: + self.ctx.visual.render_layer(layer) + rendered_layers.add(layer) self._current_drawing_artist = None \ No newline at end of file diff --git a/gamms/typing/artist.py b/gamms/typing/artist.py index fb41e89..7ba104b 100644 --- a/gamms/typing/artist.py +++ b/gamms/typing/artist.py @@ -121,7 +121,7 @@ def set_artist_type(self, artist_type: ArtistType) -> None: pass @abstractmethod - def draw(self, force = False) -> None: + def draw(self, force: bool) -> None: """ Draw the artist immediately. Note that if the artist is invisible, it will remain invisible. Later when the artist is set to visible, its content will be the updated content. diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index 7c38ebb..0d36c81 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -116,7 +116,7 @@ def execute_command_list(self, command_list: List[IRenderCommand]) -> None: Execute a list of rendering commands. Args: - command_list (List[RenderCommand]): A list of rendering commands to execute. + command_list (List[IRenderCommand]): A list of rendering commands to execute. """ pass From ccfcaa19699ce906f21f0f138533e93073dc42ff Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Fri, 20 Jun 2025 12:59:01 +0800 Subject: [PATCH 6/7] Fix inheritance issue --- gamms/VisualizationEngine/default_drawers.py | 4 ++-- gamms/VisualizationEngine/render_command.py | 9 +++++---- gamms/typing/render_command.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index a4d5a08..84973d8 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -128,13 +128,13 @@ def render_input_overlay(ctx: IContext, data: Dict[str, Any]): """ command_list = [] graph_data = cast(GraphData, data.get('graph_data')) - waiting_agent_name = data.get('_waiting_agent_name', None) + waiting_agent_name: str | None = data.get('_waiting_agent_name', None) input_options = data.get('_input_options', {}) waiting_user_input = data.get('_waiting_user_input', False) # Break checker if waiting_agent_name is None or waiting_user_input == False or input_options == {}: - return + return [] graph = ctx.graph.graph node_color = graph_data.node_color diff --git a/gamms/VisualizationEngine/render_command.py b/gamms/VisualizationEngine/render_command.py index c495f85..3934709 100644 --- a/gamms/VisualizationEngine/render_command.py +++ b/gamms/VisualizationEngine/render_command.py @@ -1,6 +1,6 @@ from gamms.VisualizationEngine.render_command_data import * from gamms.typing import ColorType, RenderOpCode, IRenderCommand -from typing import List, Tuple +from typing import List, Tuple, Any class RenderCommand(IRenderCommand): """ @@ -12,14 +12,15 @@ def __init__(self, op_code: RenderOpCode, data = None): self._opcode = op_code # contains all parameters for this command - self.data = data + self._data = data @property def opcode(self) -> RenderOpCode: return self._opcode - def validate(self) -> int: - pass + @property + def data(self) -> Any: + return self._data def __str__(self): return f"RenderCommand({self.opcode}, {self.data})" diff --git a/gamms/typing/render_command.py b/gamms/typing/render_command.py index 06de89c..291cebd 100644 --- a/gamms/typing/render_command.py +++ b/gamms/typing/render_command.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from enum import Enum, auto +from typing import Any class RenderOpCode(Enum): RenderCircle = auto() @@ -20,4 +21,15 @@ def opcode(self) -> RenderOpCode: Returns: RenderOpCode: The operation code for the command. """ + pass + + @property + @abstractmethod + def data(self) -> Any: + """ + Get the data associated with the render command. + + Returns: + Any: The data associated with the render command. + """ pass \ No newline at end of file From 803273027193cc4c3196bf06fc71fa34ccddabb1 Mon Sep 17 00:00:00 2001 From: Xiao Jiang Date: Fri, 20 Jun 2025 13:13:37 +0800 Subject: [PATCH 7/7] Type annotation --- gamms/VisualizationEngine/artist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index f717c49..c1d7371 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -70,7 +70,7 @@ def get_artist_type(self) -> ArtistType: def set_artist_type(self, artist_type: ArtistType): self._artist_type = artist_type - def draw(self, force = False): + def draw(self, force: bool=False): if self._is_rendering and not force: return