diff --git a/examples/custom_drawers/game.py b/examples/custom_drawers/game.py index 6ef3cfb..91d7031 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,16 @@ 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)] # 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..c1d7371 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -1,18 +1,19 @@ -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 typing import Callable, Union, Dict, Any +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[IRenderCommand]], Shape], layer: int = 30): self.data = {} self._ctx = ctx 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[IRenderCommand] = [] if isinstance(drawer, Shape): if drawer == Shape.Circle: self._drawer = render_circle @@ -31,6 +32,10 @@ def layer_dirty(self) -> bool: def layer_dirty(self, value: bool): self._layer_dirty = value + @property + def render_commands(self) -> List[IRenderCommand]: + return self._render_commands + def set_layer(self, layer: int): if self._layer == layer: return @@ -44,7 +49,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]): @@ -53,11 +58,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 @@ -65,9 +70,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: bool=False): + if self._is_rendering and not force: + return + 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 + 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/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index 8c5608e..84973d8 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,8 @@ 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] def render_rectangle(ctx: IContext, data: Dict[str, Any]): @@ -35,7 +37,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] + def render_agent(ctx: IContext, data: Dict[str, Any]): """ @@ -84,7 +88,8 @@ 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] def render_graph(ctx: IContext, data: Dict[str, Any]): @@ -95,6 +100,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 +109,14 @@ 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,14 +126,15 @@ 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) + 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 == None or waiting_user_input == False or input_options == {}: - return + if waiting_agent_name is None or waiting_user_input == False or input_options == {}: + return [] graph = ctx.graph.graph node_color = graph_data.node_color @@ -134,19 +144,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 +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] - 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: - 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): - 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)) 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))) def render_neighbor_sensor(ctx: IContext, data: Dict[str, Any]): @@ -180,12 +193,15 @@ 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)) + + return command_list def render_map_sensor(ctx: IContext, data: Dict[str, Any]): @@ -196,6 +212,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 +221,7 @@ 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)) edge_color = data.get('edge_color', Color.Cyan) sensed_edges = sensor_data.get('edges', []) @@ -218,9 +235,11 @@ 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, 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, 4, perform_culling_test=False)) + + return command_list def render_agent_sensor(ctx: IContext, data: Dict[str, Any]): @@ -231,6 +250,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 +264,6 @@ 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)) + + 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..7fafaea 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -1,8 +1,10 @@ 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.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.render_command import RenderCommand +from gamms.VisualizationEngine.render_command_data import * +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, @@ -10,7 +12,8 @@ IContext, SensorType, OpCodes, - ColorType + ColorType, + RenderOpCode ) from typing import Dict, Any, List, Tuple, Union, cast @@ -81,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 @@ -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, 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, data.perform_culling_test) + elif command.opcode == RenderOpCode.RenderCircle: + data: CircleRenderCommandData = command.data + 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, 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, data.width, data.perform_culling_test) + elif command.opcode == RenderOpCode.RenderLineString: + data: LineStringRenderCommandData = command.data + 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() 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..3934709 --- /dev/null +++ b/gamms/VisualizationEngine/render_command.py @@ -0,0 +1,62 @@ +from gamms.VisualizationEngine.render_command_data import * +from gamms.typing import ColorType, RenderOpCode, IRenderCommand +from typing import List, Tuple, Any + +class RenderCommand(IRenderCommand): + """ + 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 + + @property + def data(self) -> Any: + return self._data + + def __str__(self): + return f"RenderCommand({self.opcode}, {self.data})" + + @staticmethod + 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(perform_culling_test, x, y, radius, color)) + return cmd + + @staticmethod + 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(perform_culling_test, x, y, width, height, color)) + return cmd + + @staticmethod + 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(perform_culling_test, points, color, width)) + return cmd + + @staticmethod + 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(perform_culling_test, x1, y1, x2, y2, color, width, is_aa)) + return cmd + + @staticmethod + 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(perform_culling_test, points, color, width, closed, is_aa)) + return cmd + + @staticmethod + 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(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 new file mode 100644 index 0000000..d9b8cc9 --- /dev/null +++ b/gamms/VisualizationEngine/render_command_data.py @@ -0,0 +1,117 @@ +from dataclasses import dataclass, field +from gamms.typing import ColorType +from typing import List, Tuple, Dict, Optional + +@dataclass() +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. + + 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(BaseRenderCommandData): + """ + 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(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 (int): The width of the polygon's edges. + """ + points: List[Tuple[float, float]] + color: ColorType + width: int + +@dataclass() +class LineRenderCommandData(BaseRenderCommandData): + """ + 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. + width (int): 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: int + is_aa: bool + +@dataclass() +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 (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: int + closed: bool + is_aa: bool + +@dataclass() +class TextRenderCommandData(BaseRenderCommandData): + """ + 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..7dec28e 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): @@ -266,15 +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(): - continue - - if not artist.get_will_draw(): - if artist.get_artist_type() == ArtistType.GRAPH and layer not in rendered_layers: - self.ctx.visual.render_layer(layer) - rendered_layers.add(layer) + if not artist.is_visible(): continue self._current_drawing_artist = artist - artist.draw() + + # 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/__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..7ba104b 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 @@ -49,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. @@ -79,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 @@ -119,9 +121,22 @@ def set_artist_type(self, artist_type: ArtistType) -> None: pass @abstractmethod - def draw(self) -> None: + def draw(self, force: bool) -> 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 @@ -145,4 +160,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..291cebd --- /dev/null +++ b/gamms/typing/render_command.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +from enum import Enum, auto +from typing import Any + +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 + + @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 diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index a413df8..0d36c81 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -1,4 +1,5 @@ -from gamms.typing.artist import IArtist +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 @@ -109,6 +110,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[IRenderCommand]): A list of rendering commands to execute. + """ + pass + @abstractmethod def simulate(self) -> None: """