From 9514194d8c39b75bbd515e864d86bece49eeb276 Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Sat, 11 Apr 2026 01:57:36 -0700 Subject: [PATCH 1/9] feature: add label as artist --- gamms/VisualizationEngine/__init__.py | 1 + gamms/VisualizationEngine/artist.py | 4 +- gamms/VisualizationEngine/builtin_artists.py | 23 ++++++- gamms/VisualizationEngine/default_drawers.py | 64 ++++++++++++++++++-- gamms/VisualizationEngine/no_engine.py | 7 +++ gamms/VisualizationEngine/pygame_engine.py | 26 +++++++- gamms/typing/visualization_engine.py | 39 ++++++++++++ snippets/custom_sensors/config.py | 16 +++++ snippets/custom_sensors/game.py | 3 + 9 files changed, 174 insertions(+), 9 deletions(-) diff --git a/gamms/VisualizationEngine/__init__.py b/gamms/VisualizationEngine/__init__.py index 2ad7f14..f37980d 100644 --- a/gamms/VisualizationEngine/__init__.py +++ b/gamms/VisualizationEngine/__init__.py @@ -30,6 +30,7 @@ class Space(IntEnum): class Shape(Enum): Circle = auto() Rectangle = auto() + Label = auto() import sys import importlib.util diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index fd61bd4..54621aa 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -1,7 +1,7 @@ from enum import Enum, auto from gamms.typing import IArtist, ArtistType, IContext -from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle +from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle, render_label from gamms.VisualizationEngine import Shape from typing import Callable, Union, Dict, Any @@ -25,6 +25,8 @@ def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, An self._drawer = render_circle elif drawer == Shape.Rectangle: self._drawer = render_rectangle + elif drawer == Shape.Label: + self._drawer = render_label else: raise ValueError("Unsupported shape type") else: diff --git a/gamms/VisualizationEngine/builtin_artists.py b/gamms/VisualizationEngine/builtin_artists.py index 8ec9dea..9fe3357 100644 --- a/gamms/VisualizationEngine/builtin_artists.py +++ b/gamms/VisualizationEngine/builtin_artists.py @@ -67,4 +67,25 @@ class GraphData: node_size: float edge_color: ColorType draw_id: bool - edge_line_points: Dict[int, List[Tuple[float, float]]] = field(default_factory=dict) \ No newline at end of file + edge_line_points: Dict[int, List[Tuple[float, float]]] = field(default_factory=dict) + +@dataclass +class LabelData: + """Contains all necessary data for drawing a label. + Attributes: + name (str): The name of the label. + text (str): The text of the label. + anchor (Optional[str]): The anchor object for the label. + position (Optional[Tuple[float, float]]): The position of the label (used if anchor is None). + color (Optional[ColorType]): The color of the label text. + size (Optional[float]): The font size of the label text. + offset (Tuple[float, float]): The offset of the label. + visible (bool): Whether the label is visible.""" + name: str + text: str + anchor: Optional[str] = None + position: Optional[Tuple[float, float]] = None # used if anchor is None + color: Optional[ColorType] = None + size: Optional[int] = None + offset: Tuple[float, float] = (0.0, 0.0) + visible: bool = True diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index 5614fd5..798913a 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -1,9 +1,9 @@ from gamms.AgentEngine.agent_engine import AerialAgent from gamms.VisualizationEngine import Color -from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData -from gamms.typing import IContext, OSMEdge, Node, ColorType, AgentType +from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData, LabelData +from gamms.typing import IContext, OSMEdge, Node, ColorType, AgentType, IArtist, IAgent -from typing import Dict, Any, cast, List, Optional +from typing import Dict, Any, Tuple, cast, List, Optional import math @@ -38,6 +38,58 @@ def render_rectangle(ctx: IContext, data: Dict[str, Any]): color = data.get('color', Color.Cyan) ctx.visual.render_rectangle(x, y, width, height, color) +def _get_agent_anchor_position(ctx: IContext, agent_visual: IArtist, agent: IAgent) -> Tuple[float, float]: + if agent_visual is not None: + agent_data = cast(Optional[AgentData], agent_visual.data.get('agent_data')) + if agent_data is not None and agent_data.current_position is not None: + return agent_data.current_position + + # Fallback for first frame (before agent_data.current_position is populated). + if agent.type == AgentType.BASIC: + node = ctx.graph.graph.get_node(agent.current_node_id) + return node.x, node.y + elif agent.type == AgentType.AERIAL: + return agent.position[0], agent.position[1] + else: + raise ValueError(f"Unsupported agent type: {agent.type}") + +def render_label(ctx: IContext, data: Dict[str, Any]): + """ + Render a label at the specified position with the specified text and color. + + Args: + ctx (Context): The current simulation context. + data (dict): The data containing the label's position, text, and color. + """ + label_data = cast(LabelData, data.get('label_data')) + if label_data.visible is False: + return + + color = cast(ColorType, label_data.color if label_data.color is not None else Color.Black) + size = cast(Optional[int], label_data.size) + + agent = ctx.agent.get_agent(label_data.anchor) if label_data.anchor else None + offset = cast(Tuple[float, float], label_data.offset) + + if agent is not None and label_data.anchor is not None: + agent_visual = ctx.visual.get_agent_visual(label_data.anchor) + if label_data.color is None: + color = Color.Black if agent_visual is None else cast(AgentData, agent_visual.data.get('agent_data')).color + if label_data.size is None: + size = None if agent_visual is None else cast(AgentData, agent_visual.data.get('agent_data')).size + + x, y = _get_agent_anchor_position(ctx, agent_visual, agent) + x += offset[0] + y += offset[1] + elif label_data.position is not None: + x, y = label_data.position + x += offset[0] + y += offset[1] + else: + raise ValueError("Label must have either an anchor or a position") + + ctx.visual.render_text(label_data.text, x, y, color, font_size=size) + def render_agent(ctx: IContext, data: Dict[str, Any]): """ Render an agent as a triangle at its current position on the screen. This is the default rendering method for agents. @@ -76,11 +128,11 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): else: position = (prev_position[0] + alpha * (target_position[0] - prev_position[0]), prev_position[1] + alpha * (target_position[1] - prev_position[1])) - - agent_data.current_position = position else: position = (target_node.x, target_node.y) + agent_data.current_position = position + # Draw each agent as a triangle at its current position angle = math.radians(45) @@ -101,6 +153,8 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): else: position = aerial_agent.position + agent_data.current_position = (position[0], position[1]) + quat = aerial_agent.quat x = quat[1] y = quat[2] diff --git a/gamms/VisualizationEngine/no_engine.py b/gamms/VisualizationEngine/no_engine.py index d5b8f0e..2891c26 100644 --- a/gamms/VisualizationEngine/no_engine.py +++ b/gamms/VisualizationEngine/no_engine.py @@ -23,10 +23,17 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) return Artist(self.ctx , dummy, layer=20) + def get_agent_visual(self, name: str) -> Union[IArtist, None]: + return None + def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) return Artist(self.ctx , dummy, layer=40) + def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: + dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) + return Artist(self.ctx , dummy, layer=50) + def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: if isinstance(artist, dict): dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index 681056b..c9b1cad 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -1,9 +1,9 @@ from gamms.AgentEngine.agent_engine import AerialAgent 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.builtin_artists import AgentData, GraphData, LabelData from gamms.VisualizationEngine.default_drawers import ( - render_circle, render_rectangle, + render_circle, render_rectangle, render_label, render_agent, render_graph, render_neighbor_sensor, render_map_sensor, render_agent_sensor, render_input_overlay, render_aerial_agent_sensor, @@ -135,6 +135,9 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: return artist + def get_agent_visual(self, name: str) -> Optional[IArtist]: + return self._agent_artists.get(name) + def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: sensor = self.ctx.sensor.get_sensor(name) sensor_type = sensor.type @@ -171,6 +174,25 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: return artist + def set_label_visual(self, name, **kwargs): + label_data = LabelData( + name=name, + text=cast(str, kwargs.get('text', name)), + anchor=kwargs.get('anchor', None), + position=kwargs.get('position', None), + color=cast(Optional[ColorType], kwargs.get('color', None)), + size=cast(Optional[int], kwargs.get('size', None)), + visible=cast(bool, kwargs.get('visible', True)), + offset=cast(Tuple[float, float], kwargs.get('offset', (0.0, -15.0))), + ) + + artist = Artist(self.ctx, render_label, 20) + artist.data['label_data'] = label_data + artist.set_artist_type(ArtistType.LABEL) + + self.add_artist(name, artist) + return artist + def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: if isinstance(artist, IArtist): artist_to_add = artist diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index eccae6e..6291bc9 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -58,6 +58,23 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ pass + @abstractmethod + def get_agent_visual(self, name: str) -> Optional[IArtist]: + """ + Get the visualization artist for an agent if available. + + The returned artist contains render-time data such as interpolated + position and can be used by other drawers (for example labels) to stay + synced with what is currently being rendered. + + Args: + name (str): Agent name. + + Returns: + Optional[IArtist]: Agent artist object, or None if unavailable. + """ + pass + @abstractmethod def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ @@ -80,6 +97,28 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ pass + @abstractmethod + def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: + """ + Configure the visual representation of a specific label. + + This method sets up visual parameters for an individual label, allowing + customization of how the label is displayed within the visualization. + + Args: + name (str): The unique name identifier of the label to configure. + **kwargs: Arbitrary keyword arguments representing visual settings. + Possible keys include: + - `text` (str): The text content of the label. + - `anchor` (str): The name of the agent or sensor to which the label should be anchored to. + - `position` (Tuple[float, float]): The optional absolute (x, y) coordinate for the label's position in the visualization. + - `color` (str): The color to represent the label. + - `size` (float): The size of the label in the visualization. + - `visible` (bool): Whether the label is visible in the visualization. + - `offset` (Tuple[float, float]): The (x, y) offset for the label's position relative to its attached element. + """ + pass + @abstractmethod def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: """ diff --git a/snippets/custom_sensors/config.py b/snippets/custom_sensors/config.py index c295733..678199a 100644 --- a/snippets/custom_sensors/config.py +++ b/snippets/custom_sensors/config.py @@ -54,17 +54,33 @@ agent_vis_config = {} +label_vis_config = {} + for i in range(RED_TEAM_AGENTS): agent_vis_config[f'agent_{i}'] = { 'color': (255, 0, 0), # color of the agent 'size': 10, # size of the agent } + label_vis_config[f'label_{i}'] = { + 'text': f'Agent {i}', + 'color': (255, 0, 0), # color of the label + 'size': 12, # size of the label + 'anchor': f'agent_{i}', # anchor the label to the agent + 'offset': (0, -15), # offset of the label from the agent + } for i in range(RED_TEAM_AGENTS, RED_TEAM_AGENTS + BLUE_TEAM_AGENTS): agent_vis_config[f'agent_{i}'] = { 'color': (0, 0, 255), # color of the agent 'size': 10, # size of the agent } + label_vis_config[f'label_{i}'] = { + 'text': f'Agent {i}', + 'color': (0, 0, 255), # color of the label + 'size': 12, # size of the label + 'anchor': f'agent_{i}', # anchor the label to the agent + 'offset': (0, -15), # offset of the label from the agent + } sensor_vis_config = {} diff --git a/snippets/custom_sensors/game.py b/snippets/custom_sensors/game.py index 9502265..f9c11df 100644 --- a/snippets/custom_sensors/game.py +++ b/snippets/custom_sensors/game.py @@ -235,6 +235,9 @@ def draw_capturable_nodes(ctx, data): for name, vis_config in config.agent_vis_config.items(): artist = ctx.visual.set_agent_visual(name, **vis_config) +for name, vis_config in config.label_vis_config.items(): + artist = ctx.visual.set_label_visual(name, **vis_config) + for node_id in nodes: for red_agent in red_team: start = config.agent_config[red_agent]['start_node_id'] From c4fdf7c056a2c38f351c4b369968e350a4196c9a Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Mon, 27 Apr 2026 19:12:46 -0700 Subject: [PATCH 2/9] label changes --- gamms/VisualizationEngine/pygame_engine.py | 29 ++++++++++------------ gamms/typing/visualization_engine.py | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index c9b1cad..9f7b4f7 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -135,8 +135,16 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: return artist - def get_agent_visual(self, name: str) -> Optional[IArtist]: - return self._agent_artists.get(name) + def get_agent_visual(self, name: str) -> IArtist: + if name not in self._dynamic_agent_artist_names: + raise KeyError(f"Agent artist {name} not found") + + artist = self._dynamic_artists.get(name) + if artist is None or 'agent_data' not in artist.data: + self._dynamic_agent_artist_names.discard(name) + raise KeyError(f"Agent artist {name} not found") + + return artist def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: sensor = self.ctx.sensor.get_sensor(name) @@ -188,7 +196,7 @@ def set_label_visual(self, name, **kwargs): artist = Artist(self.ctx, render_label, 20) artist.data['label_data'] = label_data - artist.set_artist_type(ArtistType.LABEL) + artist.set_artist_type(ArtistType.DYNAMIC) self.add_artist(name, artist) return artist @@ -393,17 +401,6 @@ def _iter_agent_artists(self) -> Iterator[IArtist]: for name in stale_names: self._dynamic_agent_artist_names.discard(name) - def _get_agent_artist(self, agent_name: str) -> IArtist: - if agent_name not in self._dynamic_agent_artist_names: - raise KeyError(f"Agent artist {agent_name} not found") - - artist = self._dynamic_artists.get(agent_name) - if artist is None or 'agent_data' not in artist.data: - self._dynamic_agent_artist_names.discard(agent_name) - raise KeyError(f"Agent artist {agent_name} not found") - - return artist - def _toggle_waiting_simulation(self, waiting_simulation: bool): self._waiting_simulation = waiting_simulation for agent_artist in self._iter_agent_artists(): @@ -566,11 +563,11 @@ def human_input(self, agent_name: str, state: Dict[str, Any]) -> Union[int, Tupl prev_waiting_agent_name = self._waiting_agent_name if prev_waiting_agent_name is not None: - prev_waiting_agent_artist = self._get_agent_artist(prev_waiting_agent_name) + prev_waiting_agent_artist = self.get_agent_visual(prev_waiting_agent_name) prev_waiting_agent_artist.data['_is_waiting'] = False self._waiting_agent_name = agent_name - waiting_agent_artist = self._get_agent_artist(agent_name) + waiting_agent_artist = self.get_agent_visual(agent_name) waiting_agent_artist.data['_is_waiting'] = True waiting_agent = self.ctx.agent.get_agent(agent_name) diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index 6291bc9..1928ae7 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -59,7 +59,7 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: pass @abstractmethod - def get_agent_visual(self, name: str) -> Optional[IArtist]: + def get_agent_visual(self, name: str) -> IArtist: """ Get the visualization artist for an agent if available. From b6312d1b34faac95cb3ef43f7382407b174d8d0e Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Mon, 27 Apr 2026 20:00:59 -0700 Subject: [PATCH 3/9] label changes --- snippets/custom_sensors/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/snippets/custom_sensors/config.py b/snippets/custom_sensors/config.py index 678199a..aef2140 100644 --- a/snippets/custom_sensors/config.py +++ b/snippets/custom_sensors/config.py @@ -63,10 +63,10 @@ } label_vis_config[f'label_{i}'] = { 'text': f'Agent {i}', - 'color': (255, 0, 0), # color of the label - 'size': 12, # size of the label 'anchor': f'agent_{i}', # anchor the label to the agent 'offset': (0, -15), # offset of the label from the agent + 'color': (255, 0, 0), # color of the label (optional as it will default to anchor agent color if not provided) + 'size': 12, # size of the label (optional as it will default to anchor agent size if not provided) } for i in range(RED_TEAM_AGENTS, RED_TEAM_AGENTS + BLUE_TEAM_AGENTS): @@ -76,10 +76,10 @@ } label_vis_config[f'label_{i}'] = { 'text': f'Agent {i}', - 'color': (0, 0, 255), # color of the label - 'size': 12, # size of the label 'anchor': f'agent_{i}', # anchor the label to the agent 'offset': (0, -15), # offset of the label from the agent + 'color': (0, 0, 255), # color of the label (optional as it will default to anchor agent color if not provided) + 'size': 12, # size of the label (optional as it will default to anchor agent size if not provided) } sensor_vis_config = {} From 2856b032c4653c53a2429e9e289ac1bcdde4c77b Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 10:30:05 -0700 Subject: [PATCH 4/9] artist update --- gamms/VisualizationEngine/__init__.py | 1 - gamms/VisualizationEngine/artist.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/gamms/VisualizationEngine/__init__.py b/gamms/VisualizationEngine/__init__.py index f37980d..2ad7f14 100644 --- a/gamms/VisualizationEngine/__init__.py +++ b/gamms/VisualizationEngine/__init__.py @@ -30,7 +30,6 @@ class Space(IntEnum): class Shape(Enum): Circle = auto() Rectangle = auto() - Label = auto() import sys import importlib.util diff --git a/gamms/VisualizationEngine/artist.py b/gamms/VisualizationEngine/artist.py index 54621aa..19ec038 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -25,8 +25,6 @@ def __init__(self, ctx: IContext, drawer: Union[Callable[[IContext, Dict[str, An self._drawer = render_circle elif drawer == Shape.Rectangle: self._drawer = render_rectangle - elif drawer == Shape.Label: - self._drawer = render_label else: raise ValueError("Unsupported shape type") else: From eaebf3c757b98c2b5fbe55892ab37178a8cef728 Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 10:30:39 -0700 Subject: [PATCH 5/9] artist update --- 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 19ec038..fd61bd4 100644 --- a/gamms/VisualizationEngine/artist.py +++ b/gamms/VisualizationEngine/artist.py @@ -1,7 +1,7 @@ from enum import Enum, auto from gamms.typing import IArtist, ArtistType, IContext -from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle, render_label +from gamms.VisualizationEngine.default_drawers import render_circle, render_rectangle from gamms.VisualizationEngine import Shape from typing import Callable, Union, Dict, Any From 2075e7cbe415449c133efb693828ccff3d03552f Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 10:33:44 -0700 Subject: [PATCH 6/9] artist update --- gamms/VisualizationEngine/no_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamms/VisualizationEngine/no_engine.py b/gamms/VisualizationEngine/no_engine.py index 2891c26..ea38373 100644 --- a/gamms/VisualizationEngine/no_engine.py +++ b/gamms/VisualizationEngine/no_engine.py @@ -32,7 +32,7 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) - return Artist(self.ctx , dummy, layer=50) + return Artist(self.ctx , dummy, layer=20) def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: if isinstance(artist, dict): From 97e8e8bbd347771ecc1a91be4c7a8866d21f639a Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 11:51:44 -0700 Subject: [PATCH 7/9] bug fix --- gamms/VisualizationEngine/builtin_artists.py | 2 +- gamms/VisualizationEngine/no_engine.py | 5 +++-- gamms/VisualizationEngine/pygame_engine.py | 2 +- gamms/typing/visualization_engine.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gamms/VisualizationEngine/builtin_artists.py b/gamms/VisualizationEngine/builtin_artists.py index 9fe3357..583b7ca 100644 --- a/gamms/VisualizationEngine/builtin_artists.py +++ b/gamms/VisualizationEngine/builtin_artists.py @@ -78,7 +78,7 @@ class LabelData: anchor (Optional[str]): The anchor object for the label. position (Optional[Tuple[float, float]]): The position of the label (used if anchor is None). color (Optional[ColorType]): The color of the label text. - size (Optional[float]): The font size of the label text. + size (Optional[int]): The font size of the label text. offset (Tuple[float, float]): The offset of the label. visible (bool): Whether the label is visible.""" name: str diff --git a/gamms/VisualizationEngine/no_engine.py b/gamms/VisualizationEngine/no_engine.py index ea38373..2fc07c2 100644 --- a/gamms/VisualizationEngine/no_engine.py +++ b/gamms/VisualizationEngine/no_engine.py @@ -23,8 +23,9 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) return Artist(self.ctx , dummy, layer=20) - def get_agent_visual(self, name: str) -> Union[IArtist, None]: - return None + def get_agent_visual(self, name: str) -> IArtist: + dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) + return Artist(self.ctx , dummy, layer=20) def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index 9f7b4f7..bce580a 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -182,7 +182,7 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: return artist - def set_label_visual(self, name, **kwargs): + def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: label_data = LabelData( name=name, text=cast(str, kwargs.get('text', name)), diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index 1928ae7..a289ccc 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -71,7 +71,7 @@ def get_agent_visual(self, name: str) -> IArtist: name (str): Agent name. Returns: - Optional[IArtist]: Agent artist object, or None if unavailable. + IArtist: Returns Agent artist object. """ pass From bf36ce630f7c3667621f877c23e35b96df5356ba Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 17:58:29 -0700 Subject: [PATCH 8/9] artist update --- gamms/VisualizationEngine/builtin_artists.py | 6 -- gamms/VisualizationEngine/default_drawers.py | 62 ++++++-------------- gamms/VisualizationEngine/no_engine.py | 8 --- gamms/VisualizationEngine/pygame_engine.py | 57 ++++++++---------- gamms/typing/visualization_engine.py | 39 ------------ snippets/custom_sensors/config.py | 26 ++++---- snippets/custom_sensors/game.py | 3 - 7 files changed, 52 insertions(+), 149 deletions(-) diff --git a/gamms/VisualizationEngine/builtin_artists.py b/gamms/VisualizationEngine/builtin_artists.py index 583b7ca..5e16c55 100644 --- a/gamms/VisualizationEngine/builtin_artists.py +++ b/gamms/VisualizationEngine/builtin_artists.py @@ -73,18 +73,12 @@ class GraphData: class LabelData: """Contains all necessary data for drawing a label. Attributes: - name (str): The name of the label. text (str): The text of the label. - anchor (Optional[str]): The anchor object for the label. - position (Optional[Tuple[float, float]]): The position of the label (used if anchor is None). color (Optional[ColorType]): The color of the label text. size (Optional[int]): The font size of the label text. offset (Tuple[float, float]): The offset of the label. visible (bool): Whether the label is visible.""" - name: str text: str - anchor: Optional[str] = None - position: Optional[Tuple[float, float]] = None # used if anchor is None color: Optional[ColorType] = None size: Optional[int] = None offset: Tuple[float, float] = (0.0, 0.0) diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index 798913a..a0a58ae 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -1,9 +1,9 @@ from gamms.AgentEngine.agent_engine import AerialAgent from gamms.VisualizationEngine import Color from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData, LabelData -from gamms.typing import IContext, OSMEdge, Node, ColorType, AgentType, IArtist, IAgent +from gamms.typing import IContext, OSMEdge, Node, ColorType, AgentType -from typing import Dict, Any, Tuple, cast, List, Optional +from typing import Dict, Any, cast, List, Optional import math @@ -38,56 +38,24 @@ def render_rectangle(ctx: IContext, data: Dict[str, Any]): color = data.get('color', Color.Cyan) ctx.visual.render_rectangle(x, y, width, height, color) -def _get_agent_anchor_position(ctx: IContext, agent_visual: IArtist, agent: IAgent) -> Tuple[float, float]: - if agent_visual is not None: - agent_data = cast(Optional[AgentData], agent_visual.data.get('agent_data')) - if agent_data is not None and agent_data.current_position is not None: - return agent_data.current_position - - # Fallback for first frame (before agent_data.current_position is populated). - if agent.type == AgentType.BASIC: - node = ctx.graph.graph.get_node(agent.current_node_id) - return node.x, node.y - elif agent.type == AgentType.AERIAL: - return agent.position[0], agent.position[1] - else: - raise ValueError(f"Unsupported agent type: {agent.type}") - -def render_label(ctx: IContext, data: Dict[str, Any]): +def render_label_text(ctx: IContext, label_data: LabelData, anchor_x: float, anchor_y: float, anchor_color: Optional[ColorType], anchor_size: Optional[int]): """ - Render a label at the specified position with the specified text and color. + Render a label with the specified text at the specified position. Args: ctx (Context): The current simulation context. - data (dict): The data containing the label's position, text, and color. + label_data (LabelData): The data containing the label's text, color, size, and offset. + anchor_x (float): The x-coordinate of the anchor point for the label. + anchor_y (float): The y-coordinate of the anchor point for the label. + anchor_color (Optional[ColorType]): The color of the anchor, used as a fallback if label_data.color is not provided. + anchor_size (Optional[int]): The size of the anchor, used as a fallback if label_data.size is not provided. """ - label_data = cast(LabelData, data.get('label_data')) if label_data.visible is False: return - - color = cast(ColorType, label_data.color if label_data.color is not None else Color.Black) - size = cast(Optional[int], label_data.size) - - agent = ctx.agent.get_agent(label_data.anchor) if label_data.anchor else None - offset = cast(Tuple[float, float], label_data.offset) - - if agent is not None and label_data.anchor is not None: - agent_visual = ctx.visual.get_agent_visual(label_data.anchor) - if label_data.color is None: - color = Color.Black if agent_visual is None else cast(AgentData, agent_visual.data.get('agent_data')).color - if label_data.size is None: - size = None if agent_visual is None else cast(AgentData, agent_visual.data.get('agent_data')).size - - x, y = _get_agent_anchor_position(ctx, agent_visual, agent) - x += offset[0] - y += offset[1] - elif label_data.position is not None: - x, y = label_data.position - x += offset[0] - y += offset[1] - else: - raise ValueError("Label must have either an anchor or a position") - + color = cast(ColorType, label_data.color if label_data.color is not None else anchor_color if anchor_color is not None else Color.Black) + size = cast(Optional[int], label_data.size if label_data.size is not None else anchor_size) + x = anchor_x + label_data.offset[0] + y = anchor_y + label_data.offset[1] ctx.visual.render_text(label_data.text, x, y, color, font_size=size) def render_agent(ctx: IContext, data: Dict[str, Any]): @@ -99,6 +67,7 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): data (dict): The data containing the agent's information. """ agent_data = cast(AgentData, data.get('agent_data')) + label_data = cast(Optional[LabelData], data.get('label_data')) size = agent_data.size color = agent_data.color is_waiting = data.get('_is_waiting', False) @@ -166,6 +135,9 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): else: raise ValueError(f"Unsupported agent type: {agent.type}") + + if label_data is not None: + render_label_text(ctx, label_data, agent_data.current_position[0], agent_data.current_position[1], color, size) def render_aerial_agent(ctx: IContext, position: tuple[float, float], angle: float, size: float, color: ColorType): diff --git a/gamms/VisualizationEngine/no_engine.py b/gamms/VisualizationEngine/no_engine.py index 2fc07c2..d5b8f0e 100644 --- a/gamms/VisualizationEngine/no_engine.py +++ b/gamms/VisualizationEngine/no_engine.py @@ -23,18 +23,10 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) return Artist(self.ctx , dummy, layer=20) - def get_agent_visual(self, name: str) -> IArtist: - dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) - return Artist(self.ctx , dummy, layer=20) - def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) return Artist(self.ctx , dummy, layer=40) - def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: - dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) - return Artist(self.ctx , dummy, layer=20) - def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: if isinstance(artist, dict): dummy = cast(Callable[[IContext, Dict[str, Any]], None], lambda ctx, data: None) diff --git a/gamms/VisualizationEngine/pygame_engine.py b/gamms/VisualizationEngine/pygame_engine.py index bce580a..150e765 100644 --- a/gamms/VisualizationEngine/pygame_engine.py +++ b/gamms/VisualizationEngine/pygame_engine.py @@ -3,7 +3,7 @@ from gamms.VisualizationEngine.render_manager import RenderManager from gamms.VisualizationEngine.builtin_artists import AgentData, GraphData, LabelData from gamms.VisualizationEngine.default_drawers import ( - render_circle, render_rectangle, render_label, + render_circle, render_rectangle, render_agent, render_graph, render_neighbor_sensor, render_map_sensor, render_agent_sensor, render_input_overlay, render_aerial_agent_sensor, @@ -131,19 +131,16 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: artist.set_artist_type(ArtistType.DYNAMIC) artist.data['_alpha'] = 1.0 - self.add_artist(name, artist) - - return artist - - def get_agent_visual(self, name: str) -> IArtist: - if name not in self._dynamic_agent_artist_names: - raise KeyError(f"Agent artist {name} not found") - - artist = self._dynamic_artists.get(name) - if artist is None or 'agent_data' not in artist.data: - self._dynamic_agent_artist_names.discard(name) - raise KeyError(f"Agent artist {name} not found") + if 'label_data' in kwargs: + artist.data['label_data'] = LabelData( + text=cast(str, kwargs['label_data'].get('text', name)), + color=cast(Optional[ColorType], kwargs['label_data'].get('color', None)), + size=cast(Optional[int], kwargs['label_data'].get('size', None)), + offset=cast(Tuple[float, float], kwargs['label_data'].get('offset', (0.0, -15.0))), + visible=cast(bool, kwargs['label_data'].get('visible', True)), + ) + self.add_artist(name, artist) return artist def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: @@ -182,25 +179,6 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: return artist - def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: - label_data = LabelData( - name=name, - text=cast(str, kwargs.get('text', name)), - anchor=kwargs.get('anchor', None), - position=kwargs.get('position', None), - color=cast(Optional[ColorType], kwargs.get('color', None)), - size=cast(Optional[int], kwargs.get('size', None)), - visible=cast(bool, kwargs.get('visible', True)), - offset=cast(Tuple[float, float], kwargs.get('offset', (0.0, -15.0))), - ) - - artist = Artist(self.ctx, render_label, 20) - artist.data['label_data'] = label_data - artist.set_artist_type(ArtistType.DYNAMIC) - - self.add_artist(name, artist) - return artist - def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: if isinstance(artist, IArtist): artist_to_add = artist @@ -401,6 +379,17 @@ def _iter_agent_artists(self) -> Iterator[IArtist]: for name in stale_names: self._dynamic_agent_artist_names.discard(name) + def _get_agent_artist(self, agent_name: str) -> IArtist: + if agent_name not in self._dynamic_agent_artist_names: + raise KeyError(f"Agent artist {agent_name} not found") + + artist = self._dynamic_artists.get(agent_name) + if artist is None or 'agent_data' not in artist.data: + self._dynamic_agent_artist_names.discard(agent_name) + raise KeyError(f"Agent artist {agent_name} not found") + + return artist + def _toggle_waiting_simulation(self, waiting_simulation: bool): self._waiting_simulation = waiting_simulation for agent_artist in self._iter_agent_artists(): @@ -563,11 +552,11 @@ def human_input(self, agent_name: str, state: Dict[str, Any]) -> Union[int, Tupl prev_waiting_agent_name = self._waiting_agent_name if prev_waiting_agent_name is not None: - prev_waiting_agent_artist = self.get_agent_visual(prev_waiting_agent_name) + prev_waiting_agent_artist = self._get_agent_artist(prev_waiting_agent_name) prev_waiting_agent_artist.data['_is_waiting'] = False self._waiting_agent_name = agent_name - waiting_agent_artist = self.get_agent_visual(agent_name) + waiting_agent_artist = self._get_agent_artist(agent_name) waiting_agent_artist.data['_is_waiting'] = True waiting_agent = self.ctx.agent.get_agent(agent_name) diff --git a/gamms/typing/visualization_engine.py b/gamms/typing/visualization_engine.py index a289ccc..eccae6e 100644 --- a/gamms/typing/visualization_engine.py +++ b/gamms/typing/visualization_engine.py @@ -58,23 +58,6 @@ def set_agent_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ pass - @abstractmethod - def get_agent_visual(self, name: str) -> IArtist: - """ - Get the visualization artist for an agent if available. - - The returned artist contains render-time data such as interpolated - position and can be used by other drawers (for example labels) to stay - synced with what is currently being rendered. - - Args: - name (str): Agent name. - - Returns: - IArtist: Returns Agent artist object. - """ - pass - @abstractmethod def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ @@ -97,28 +80,6 @@ def set_sensor_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: """ pass - @abstractmethod - def set_label_visual(self, name: str, **kwargs: Dict[str, Any]) -> IArtist: - """ - Configure the visual representation of a specific label. - - This method sets up visual parameters for an individual label, allowing - customization of how the label is displayed within the visualization. - - Args: - name (str): The unique name identifier of the label to configure. - **kwargs: Arbitrary keyword arguments representing visual settings. - Possible keys include: - - `text` (str): The text content of the label. - - `anchor` (str): The name of the agent or sensor to which the label should be anchored to. - - `position` (Tuple[float, float]): The optional absolute (x, y) coordinate for the label's position in the visualization. - - `color` (str): The color to represent the label. - - `size` (float): The size of the label in the visualization. - - `visible` (bool): Whether the label is visible in the visualization. - - `offset` (Tuple[float, float]): The (x, y) offset for the label's position relative to its attached element. - """ - pass - @abstractmethod def add_artist(self, name: str, artist: Union[IArtist, Dict[str, Any]]) -> IArtist: """ diff --git a/snippets/custom_sensors/config.py b/snippets/custom_sensors/config.py index aef2140..5d5fa24 100644 --- a/snippets/custom_sensors/config.py +++ b/snippets/custom_sensors/config.py @@ -60,26 +60,24 @@ agent_vis_config[f'agent_{i}'] = { 'color': (255, 0, 0), # color of the agent 'size': 10, # size of the agent - } - label_vis_config[f'label_{i}'] = { - 'text': f'Agent {i}', - 'anchor': f'agent_{i}', # anchor the label to the agent - 'offset': (0, -15), # offset of the label from the agent - 'color': (255, 0, 0), # color of the label (optional as it will default to anchor agent color if not provided) - 'size': 12, # size of the label (optional as it will default to anchor agent size if not provided) + 'label_data': { # label data for the agent + 'text': f'Agent {i}', # text of the label + 'color': (255, 0, 0), # color of the label (optional as it will default to agent color if not provided) + 'size': 12, + 'offset': (0, -15), + }, } for i in range(RED_TEAM_AGENTS, RED_TEAM_AGENTS + BLUE_TEAM_AGENTS): agent_vis_config[f'agent_{i}'] = { 'color': (0, 0, 255), # color of the agent 'size': 10, # size of the agent - } - label_vis_config[f'label_{i}'] = { - 'text': f'Agent {i}', - 'anchor': f'agent_{i}', # anchor the label to the agent - 'offset': (0, -15), # offset of the label from the agent - 'color': (0, 0, 255), # color of the label (optional as it will default to anchor agent color if not provided) - 'size': 12, # size of the label (optional as it will default to anchor agent size if not provided) + 'label_data': { # label data for the agent + 'text': f'Agent {i}', # text of the label + 'color': (0, 0, 255), # color of the label (optional as it will default to agent color if not provided) + 'size': 12, + 'offset': (0, -15), + }, } sensor_vis_config = {} diff --git a/snippets/custom_sensors/game.py b/snippets/custom_sensors/game.py index f9c11df..9502265 100644 --- a/snippets/custom_sensors/game.py +++ b/snippets/custom_sensors/game.py @@ -235,9 +235,6 @@ def draw_capturable_nodes(ctx, data): for name, vis_config in config.agent_vis_config.items(): artist = ctx.visual.set_agent_visual(name, **vis_config) -for name, vis_config in config.label_vis_config.items(): - artist = ctx.visual.set_label_visual(name, **vis_config) - for node_id in nodes: for red_agent in red_team: start = config.agent_config[red_agent]['start_node_id'] From 7d728949e9f3699ee053253549eab7920fdbf1c4 Mon Sep 17 00:00:00 2001 From: Mehul Sinha Date: Tue, 28 Apr 2026 18:04:41 -0700 Subject: [PATCH 9/9] artist update --- gamms/VisualizationEngine/default_drawers.py | 4 ++-- snippets/custom_sensors/config.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gamms/VisualizationEngine/default_drawers.py b/gamms/VisualizationEngine/default_drawers.py index a0a58ae..f39ad83 100644 --- a/gamms/VisualizationEngine/default_drawers.py +++ b/gamms/VisualizationEngine/default_drawers.py @@ -38,7 +38,7 @@ def render_rectangle(ctx: IContext, data: Dict[str, Any]): color = data.get('color', Color.Cyan) ctx.visual.render_rectangle(x, y, width, height, color) -def render_label_text(ctx: IContext, label_data: LabelData, anchor_x: float, anchor_y: float, anchor_color: Optional[ColorType], anchor_size: Optional[int]): +def render_label(ctx: IContext, label_data: LabelData, anchor_x: float, anchor_y: float, anchor_color: Optional[ColorType], anchor_size: Optional[int]): """ Render a label with the specified text at the specified position. @@ -137,7 +137,7 @@ def render_agent(ctx: IContext, data: Dict[str, Any]): raise ValueError(f"Unsupported agent type: {agent.type}") if label_data is not None: - render_label_text(ctx, label_data, agent_data.current_position[0], agent_data.current_position[1], color, size) + render_label(ctx, label_data, agent_data.current_position[0], agent_data.current_position[1], color, size) def render_aerial_agent(ctx: IContext, position: tuple[float, float], angle: float, size: float, color: ColorType): diff --git a/snippets/custom_sensors/config.py b/snippets/custom_sensors/config.py index 5d5fa24..cfa997e 100644 --- a/snippets/custom_sensors/config.py +++ b/snippets/custom_sensors/config.py @@ -54,8 +54,6 @@ agent_vis_config = {} -label_vis_config = {} - for i in range(RED_TEAM_AGENTS): agent_vis_config[f'agent_{i}'] = { 'color': (255, 0, 0), # color of the agent