diff --git a/pyrobosim/examples/demo.py b/pyrobosim/examples/demo.py index e918785b..12a96f5a 100755 --- a/pyrobosim/examples/demo.py +++ b/pyrobosim/examples/demo.py @@ -38,16 +38,24 @@ def create_world(multirobot=False): world.add_room( name="kitchen", footprint=r1coords, - color=[1, 0, 0], + color="red", nav_poses=[Pose(x=0.75, y=0.75, z=0.0, yaw=0.0)], ) r2coords = [(1.75, 2.5), (3.5, 2.5), (3.5, 4), (1.75, 4)] - world.add_room(name="bedroom", footprint=r2coords, color=[0, 0.6, 0]) + world.add_room(name="bedroom", footprint=r2coords, color="#009900") r3coords = [(-1, 1), (-1, 3.5), (-3.0, 3.5), (-2.5, 1)] - world.add_room(name="bathroom", footprint=r3coords, color=[0, 0, 0.6]) + world.add_room( + name="bathroom", + footprint=r3coords, + color=[0.0, 0.0, 0.6], + ) # Add hallways between the rooms - world.add_hallway(room_start="kitchen", room_end="bathroom", width=0.7) + world.add_hallway( + room_start="kitchen", + room_end="bathroom", + width=0.7, + ) world.add_hallway( room_start="bathroom", room_end="bedroom", diff --git a/pyrobosim/pyrobosim/core/room.py b/pyrobosim/pyrobosim/core/room.py index da025273..b38c396f 100644 --- a/pyrobosim/pyrobosim/core/room.py +++ b/pyrobosim/pyrobosim/core/room.py @@ -6,9 +6,12 @@ from shapely.geometry import Polygon from shapely.plotting import patch_from_polygon +from matplotlib.colors import CSS4_COLORS, to_rgb + from ..utils.pose import Pose from ..utils.polygon import inflate_polygon, polygon_and_height_from_footprint from ..utils.search_graph import Node +from ..utils.general import parse_color class Room: @@ -30,8 +33,11 @@ def __init__( :type name: str, optional :param footprint: Point list or Shapely polygon describing the room 2D footprint (required). :type footprint: :class:`shapely.geometry.Polygon`/list[:class:`pyrobosim.utils.pose.Pose`] - :param color: Visualization color as an (R, G, B) tuple in the range (0.0, 1.0) - :type color: (float, float, float), optional + :param color: Visualization color. Input can be + - an (R, G, B) tuple, list in the range (0.0, 1.0), + - a string (e.g., "red") + - a hexadecimal (e.g., "#FF0000"). + :type color: list[float] | tuple[float, float, float] | str :param wall_width: Width of room walls, in meters. :type wall_width: float, optional :param nav_poses: List of navigation poses in the room. If not specified, defaults to the centroid. @@ -41,7 +47,7 @@ def __init__( """ self.name = name self.wall_width = wall_width - self.viz_color = color + self.viz_color = parse_color(color) # Entities associated with the room self.hallways = [] diff --git a/pyrobosim/pyrobosim/data/test_world.yaml b/pyrobosim/pyrobosim/data/test_world.yaml index f2b73ffa..c03d0a38 100644 --- a/pyrobosim/pyrobosim/data/test_world.yaml +++ b/pyrobosim/pyrobosim/data/test_world.yaml @@ -74,7 +74,7 @@ rooms: x: 0.75 y: 0.5 wall_width: 0.2 - color: [1, 0, 0] + color: "red" - name: bedroom footprint: @@ -82,7 +82,7 @@ rooms: dims: [1.75, 1.5] offset: [2.625, 3.25] wall_width: 0.2 - color: [0, 0.6, 0] + color: "#009900" - name: bathroom footprint: diff --git a/pyrobosim/pyrobosim/data/test_world_multirobot.yaml b/pyrobosim/pyrobosim/data/test_world_multirobot.yaml index 2258e6c8..c2e4476f 100644 --- a/pyrobosim/pyrobosim/data/test_world_multirobot.yaml +++ b/pyrobosim/pyrobosim/data/test_world_multirobot.yaml @@ -106,7 +106,7 @@ rooms: x: 0.75 y: 0.5 wall_width: 0.2 - color: [1, 0, 0] + color: "red" - name: bedroom footprint: @@ -114,7 +114,7 @@ rooms: dims: [1.75, 1.5] offset: [2.625, 3.25] wall_width: 0.2 - color: [0, 0.6, 0] + color: "#009900" - name: bathroom footprint: diff --git a/pyrobosim/pyrobosim/utils/general.py b/pyrobosim/pyrobosim/utils/general.py index 36e764f5..59ee4916 100644 --- a/pyrobosim/pyrobosim/utils/general.py +++ b/pyrobosim/pyrobosim/utils/general.py @@ -2,6 +2,8 @@ import os import yaml +import re +from matplotlib.colors import CSS4_COLORS, to_rgb def get_data_folder(): @@ -99,3 +101,30 @@ def replace_special_yaml_tokens(in_text, root_dir=None): out_text = out_text.replace("$PWD", root_dir) return out_text + + +def parse_color(color): + """ + Parses a color input and returns an RGB tuple. + + :param color: Input color as a list, tuple, string, or hexadecimal. + :type color: list[float] | tuple[float, float, float] | str + :return: RGB tuple in range (0.0, 1.0). + :rtype: tuple[float, float, float] + """ + if isinstance(color, (list, tuple)) and len(color) == 3: + return tuple(color) + + if isinstance(color, str): + if color in CSS4_COLORS: + return to_rgb(CSS4_COLORS[color]) + + hex_pattern = r"^#(?:[0-9a-fA-F]{3}){1,2}$" + if re.match(hex_pattern, color): + return to_rgb(color) + + raise ValueError(f"Invalid color string or hex: {color}") + + raise ValueError( + f"Unsupported color format. Supported types are list[float] and string" + ) diff --git a/pyrobosim/test/core/test_room.py b/pyrobosim/test/core/test_room.py index eb1992ca..c425483e 100644 --- a/pyrobosim/test/core/test_room.py +++ b/pyrobosim/test/core/test_room.py @@ -5,6 +5,7 @@ """ import pytest + from pyrobosim.core import Room, World @@ -32,7 +33,7 @@ def test_add_room_to_world_from_args(self): world = World() coords = [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] - color = [1.0, 0.0, 0.1] + color = (1.0, 0.0, 0.1) result = world.add_room(footprint=coords, color=color) assert isinstance(result, Room) diff --git a/pyrobosim/test/core/test_yaml_utils.py b/pyrobosim/test/core/test_yaml_utils.py index e9154a59..fe5eb062 100644 --- a/pyrobosim/test/core/test_yaml_utils.py +++ b/pyrobosim/test/core/test_yaml_utils.py @@ -124,14 +124,14 @@ def test_create_rooms_from_yaml(): poly, _ = polygon_and_height_from_footprint(rooms_dict["rooms"][0]["footprint"]) assert loader.world.rooms[0].polygon == poly assert loader.world.rooms[0].wall_width == 0.2 - assert loader.world.rooms[0].viz_color == [1, 0, 0] + assert loader.world.rooms[0].viz_color == (1, 0, 0) assert loader.world.rooms[0].nav_poses == [Pose.from_list([0.75, 0.5, 0.0])] assert loader.world.rooms[1].name == "bedroom" poly, _ = polygon_and_height_from_footprint(rooms_dict["rooms"][1]["footprint"]) assert loader.world.rooms[1].polygon == poly assert loader.world.rooms[1].wall_width == 0.2 - assert loader.world.rooms[1].viz_color == [0, 1, 0] + assert loader.world.rooms[1].viz_color == (0, 1, 0) assert loader.world.rooms[1].nav_poses == [ Pose.from_list(loader.world.rooms[1].centroid) ] diff --git a/pyrobosim/test/utils/test_general_utils.py b/pyrobosim/test/utils/test_general_utils.py new file mode 100644 index 00000000..28f6cb0b --- /dev/null +++ b/pyrobosim/test/utils/test_general_utils.py @@ -0,0 +1,43 @@ +import pytest +from matplotlib.colors import CSS4_COLORS, to_rgb + +from pyrobosim.utils.general import parse_color + + +def test_parse_color(): + """Testing parse color with different input color formats""" + # Test with RGB list + color_rgb_list = (1.0, 0.0, 0.0) + assert parse_color([1.0, 0.0, 0.0]) == color_rgb_list + + # Test with RGB tuple + color_rgb_tuple = (0.0, 1.0, 0.0) + assert parse_color((0.0, 1.0, 0.0)) == color_rgb_tuple + + # Test with named color + color_by_name = "red" + assert parse_color("red") == to_rgb(CSS4_COLORS[color_by_name]) + + # Test with hexadecimal color format + color_hex = "#00FFFF" + assert parse_color("#00FFFF") == to_rgb(color_hex) + + # Test with invalid RGB list + with pytest.raises(ValueError): + parse_color([1.0, 0.0]) + + # Test with invalid RGB tuple + with pytest.raises(ValueError): + parse_color((1.0, 0.0)) + + # Test with invalid named color + with pytest.raises(ValueError): + parse_color("notavalidcolor") + + # Test with invalid hexadecimal color format + with pytest.raises(ValueError): + parse_color("#ZZZ") + + # Test with unsupported input type + with pytest.raises(ValueError): + parse_color(12345)