maze_dataset.plotting.plot_svg_fancy
Plot a maze as SVG with rounded corners.
1"""Plot a maze as SVG with rounded corners.""" 2 3from xml.dom.minidom import parseString 4from xml.etree.ElementTree import Element, SubElement, tostring 5 6import numpy as np 7 8# Known color map (excluding walls). 9COLOR_MAP: dict[tuple[int, int, int], str] = { 10 (255, 255, 255): "#f0f0f0", 11 (0, 255, 0): "#4caf50", 12 (255, 0, 0): "#f44336", 13 (0, 0, 255): "#2196f3", 14} 15 16WALL_COLOR_HEX: str = "#222" # (0,0,0) in hex 17WALL_RGB: tuple[int, int, int] = (0, 0, 0) 18 19# Offsets in the order [top, right, bottom, left] 20_NEIGHBORS: np.ndarray = np.array( 21 [ 22 [-1, 0], # top 23 [0, +1], # right 24 [+1, 0], # bottom 25 [0, -1], # left 26 ], 27 dtype=int, 28) 29 30 31def is_wall(y: int, x: int, grid: np.ndarray) -> bool: 32 """True if (y, x) is out of bounds or has the wall color.""" 33 h, w, _ = grid.shape 34 if not (0 <= y < h and 0 <= x < w): 35 return True 36 return bool((grid[y, x] == WALL_RGB).all()) 37 38 39def create_tile_path( 40 origin: tuple[float, float], 41 tile_size: float, 42 corner_radius: float, 43 edges: tuple[bool, bool, bool, bool], 44) -> str: 45 """Generate an SVG path for a tile at `origin` with side length `tile_size`. 46 47 `edges` is (top, right, bottom, left) booleans, where True means that edge 48 borders a wall/outside. If both edges meeting at a corner are True and 49 corner_radius>0, we draw a rounded corner; else it's a sharp corner. 50 51 Corner order (clockwise): 52 c0 = top-left 53 c1 = top-right 54 c2 = bottom-right 55 c3 = bottom-left 56 57 edges = (top, right, bottom, left). 58 corner c0 is formed by edges top + left => edges[0] & edges[3] 59 corner c1 => top + right => edges[0] & edges[1] 60 corner c2 => right + bottom => edges[1] & edges[2] 61 corner c3 => bottom + left => edges[2] & edges[3] 62 """ 63 x0, y0 = origin 64 top, right, bottom, left = edges 65 66 # A corner is "exposed" if both adjoining edges are True 67 c0_exposed: bool = top and left # top-left 68 c1_exposed: bool = top and right # top-right 69 c2_exposed: bool = right and bottom # bottom-right 70 c3_exposed: bool = bottom and left # bottom-left 71 72 # If corner_radius=0, arcs become straight lines. 73 r: float = corner_radius 74 75 # We'll construct the path in a standard top-left -> top-right -> bottom-right -> bottom-left order. 76 path_cmds = [] 77 # Move to top-left corner, possibly offset if c0 is exposed 78 # (meaning both top and left edges are external). 79 start_x = x0 + (r if c0_exposed else 0) 80 start_y = y0 81 path_cmds.append(f"M {start_x},{start_y}") 82 83 # === TOP edge to top-right corner 84 end_x = x0 + tile_size - (r if c1_exposed else 0) 85 end_y = y0 86 path_cmds.append(f"L {end_x},{end_y}") 87 # Arc if c1_exposed 88 if c1_exposed and r > 0: 89 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + tile_size},{y0 + r}") 90 91 # === RIGHT edge to bottom-right corner 92 path_cmds.append(f"L {x0 + tile_size},{y0 + tile_size - (r if c2_exposed else 0)}") 93 if c2_exposed and r > 0: 94 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + tile_size - r},{y0 + tile_size}") 95 96 # === BOTTOM edge to bottom-left corner 97 path_cmds.append(f"L {x0 + (r if c3_exposed else 0)},{y0 + tile_size}") 98 if c3_exposed and r > 0: 99 path_cmds.append(f"A {r} {r} 0 0 1 {x0},{y0 + tile_size - r}") 100 101 # === LEFT edge back up to top-left corner 102 path_cmds.append(f"L {x0},{y0 + (r if c0_exposed else 0)}") 103 if c0_exposed and r > 0: 104 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + r},{y0}") 105 106 path_cmds.append("Z") 107 return " ".join(path_cmds) 108 109 110def plot_svg_fancy( 111 pixel_grid: np.ndarray, 112 size: int = 40, 113 corner_radius: float = 8.0, 114 bounding_corner_radius: float = 20.0, 115) -> str: 116 """plot the output of SolvedMaze(...).as_pixels() as a nice svg 117 118 Create an SVG with: 119 - A single rounded-square background (walls). 120 - Each non-wall cell is drawn via create_tile_path, with corner_radius controlling 121 whether corners are rounded. (Set corner_radius=0 for squares.) 122 123 # Parameters: 124 - `pixel_grid : np.ndarray` 125 3D array of shape (h, w, 3) with RGB values 126 - `size : int` 127 Size (in px) of each grid cell 128 - `corner_radius : float` 129 Radius for rounding corners of each tile (0 => squares) 130 - `bounding_corner_radius : float` 131 Radius for rounding the outer bounding rectangle 132 133 # Returns: 134 `str`: A pretty-printed SVG string 135 """ 136 h, w, _ = pixel_grid.shape 137 138 # Create the root <svg> 139 svg = Element( 140 "svg", 141 xmlns="http://www.w3.org/2000/svg", 142 width=str(w * size), 143 height=str(h * size), 144 viewBox=f"0 0 {w * size} {h * size}", 145 ) 146 147 # Single rounded-square background for the walls 148 SubElement( 149 svg, 150 "rect", 151 { 152 "x": "0", 153 "y": "0", 154 "width": str(w * size), 155 "height": str(h * size), 156 "fill": WALL_COLOR_HEX, 157 "rx": str(bounding_corner_radius), 158 "ry": str(bounding_corner_radius), 159 }, 160 ) 161 162 for yy in range(h): 163 for xx in range(w): 164 rgb_tuple = tuple(pixel_grid[yy, xx]) 165 if rgb_tuple == WALL_RGB: 166 # It's a wall => skip (already covered by background) 167 continue 168 169 fill_color: str | None = COLOR_MAP.get(rgb_tuple, None) # noqa: SIM910 170 if fill_color is None: 171 # Unknown color => skip or handle differently 172 continue 173 174 # Check which edges are "external" => next cell is wall 175 # edges in the order (top, right, bottom, left) 176 edges_bool = [ 177 is_wall(yy + dy, xx + dx, pixel_grid) for (dy, dx) in _NEIGHBORS 178 ] 179 180 d_path = create_tile_path( 181 origin=(xx * size, yy * size), 182 tile_size=size, 183 corner_radius=corner_radius, 184 edges=tuple(edges_bool), # type: ignore[arg-type] 185 ) 186 187 SubElement( 188 svg, 189 "path", 190 { 191 "d": d_path, 192 "fill": fill_color, 193 "stroke": "none", 194 }, 195 ) 196 197 raw_svg = tostring(svg, encoding="unicode") 198 # we are in charge of the svg so it's safe to decode 199 return parseString(raw_svg).toprettyxml(indent=" ") # noqa: S318
32def is_wall(y: int, x: int, grid: np.ndarray) -> bool: 33 """True if (y, x) is out of bounds or has the wall color.""" 34 h, w, _ = grid.shape 35 if not (0 <= y < h and 0 <= x < w): 36 return True 37 return bool((grid[y, x] == WALL_RGB).all())
True if (y, x) is out of bounds or has the wall color.
40def create_tile_path( 41 origin: tuple[float, float], 42 tile_size: float, 43 corner_radius: float, 44 edges: tuple[bool, bool, bool, bool], 45) -> str: 46 """Generate an SVG path for a tile at `origin` with side length `tile_size`. 47 48 `edges` is (top, right, bottom, left) booleans, where True means that edge 49 borders a wall/outside. If both edges meeting at a corner are True and 50 corner_radius>0, we draw a rounded corner; else it's a sharp corner. 51 52 Corner order (clockwise): 53 c0 = top-left 54 c1 = top-right 55 c2 = bottom-right 56 c3 = bottom-left 57 58 edges = (top, right, bottom, left). 59 corner c0 is formed by edges top + left => edges[0] & edges[3] 60 corner c1 => top + right => edges[0] & edges[1] 61 corner c2 => right + bottom => edges[1] & edges[2] 62 corner c3 => bottom + left => edges[2] & edges[3] 63 """ 64 x0, y0 = origin 65 top, right, bottom, left = edges 66 67 # A corner is "exposed" if both adjoining edges are True 68 c0_exposed: bool = top and left # top-left 69 c1_exposed: bool = top and right # top-right 70 c2_exposed: bool = right and bottom # bottom-right 71 c3_exposed: bool = bottom and left # bottom-left 72 73 # If corner_radius=0, arcs become straight lines. 74 r: float = corner_radius 75 76 # We'll construct the path in a standard top-left -> top-right -> bottom-right -> bottom-left order. 77 path_cmds = [] 78 # Move to top-left corner, possibly offset if c0 is exposed 79 # (meaning both top and left edges are external). 80 start_x = x0 + (r if c0_exposed else 0) 81 start_y = y0 82 path_cmds.append(f"M {start_x},{start_y}") 83 84 # === TOP edge to top-right corner 85 end_x = x0 + tile_size - (r if c1_exposed else 0) 86 end_y = y0 87 path_cmds.append(f"L {end_x},{end_y}") 88 # Arc if c1_exposed 89 if c1_exposed and r > 0: 90 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + tile_size},{y0 + r}") 91 92 # === RIGHT edge to bottom-right corner 93 path_cmds.append(f"L {x0 + tile_size},{y0 + tile_size - (r if c2_exposed else 0)}") 94 if c2_exposed and r > 0: 95 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + tile_size - r},{y0 + tile_size}") 96 97 # === BOTTOM edge to bottom-left corner 98 path_cmds.append(f"L {x0 + (r if c3_exposed else 0)},{y0 + tile_size}") 99 if c3_exposed and r > 0: 100 path_cmds.append(f"A {r} {r} 0 0 1 {x0},{y0 + tile_size - r}") 101 102 # === LEFT edge back up to top-left corner 103 path_cmds.append(f"L {x0},{y0 + (r if c0_exposed else 0)}") 104 if c0_exposed and r > 0: 105 path_cmds.append(f"A {r} {r} 0 0 1 {x0 + r},{y0}") 106 107 path_cmds.append("Z") 108 return " ".join(path_cmds)
Generate an SVG path for a tile at origin
with side length tile_size
.
edges
is (top, right, bottom, left) booleans, where True means that edge
borders a wall/outside. If both edges meeting at a corner are True and
corner_radius>0, we draw a rounded corner; else it's a sharp corner.
Corner order (clockwise): c0 = top-left c1 = top-right c2 = bottom-right c3 = bottom-left
edges = (top, right, bottom, left). corner c0 is formed by edges top + left => edges[0] & edges[3] corner c1 => top + right => edges[0] & edges[1] corner c2 => right + bottom => edges[1] & edges[2] corner c3 => bottom + left => edges[2] & edges[3]
111def plot_svg_fancy( 112 pixel_grid: np.ndarray, 113 size: int = 40, 114 corner_radius: float = 8.0, 115 bounding_corner_radius: float = 20.0, 116) -> str: 117 """plot the output of SolvedMaze(...).as_pixels() as a nice svg 118 119 Create an SVG with: 120 - A single rounded-square background (walls). 121 - Each non-wall cell is drawn via create_tile_path, with corner_radius controlling 122 whether corners are rounded. (Set corner_radius=0 for squares.) 123 124 # Parameters: 125 - `pixel_grid : np.ndarray` 126 3D array of shape (h, w, 3) with RGB values 127 - `size : int` 128 Size (in px) of each grid cell 129 - `corner_radius : float` 130 Radius for rounding corners of each tile (0 => squares) 131 - `bounding_corner_radius : float` 132 Radius for rounding the outer bounding rectangle 133 134 # Returns: 135 `str`: A pretty-printed SVG string 136 """ 137 h, w, _ = pixel_grid.shape 138 139 # Create the root <svg> 140 svg = Element( 141 "svg", 142 xmlns="http://www.w3.org/2000/svg", 143 width=str(w * size), 144 height=str(h * size), 145 viewBox=f"0 0 {w * size} {h * size}", 146 ) 147 148 # Single rounded-square background for the walls 149 SubElement( 150 svg, 151 "rect", 152 { 153 "x": "0", 154 "y": "0", 155 "width": str(w * size), 156 "height": str(h * size), 157 "fill": WALL_COLOR_HEX, 158 "rx": str(bounding_corner_radius), 159 "ry": str(bounding_corner_radius), 160 }, 161 ) 162 163 for yy in range(h): 164 for xx in range(w): 165 rgb_tuple = tuple(pixel_grid[yy, xx]) 166 if rgb_tuple == WALL_RGB: 167 # It's a wall => skip (already covered by background) 168 continue 169 170 fill_color: str | None = COLOR_MAP.get(rgb_tuple, None) # noqa: SIM910 171 if fill_color is None: 172 # Unknown color => skip or handle differently 173 continue 174 175 # Check which edges are "external" => next cell is wall 176 # edges in the order (top, right, bottom, left) 177 edges_bool = [ 178 is_wall(yy + dy, xx + dx, pixel_grid) for (dy, dx) in _NEIGHBORS 179 ] 180 181 d_path = create_tile_path( 182 origin=(xx * size, yy * size), 183 tile_size=size, 184 corner_radius=corner_radius, 185 edges=tuple(edges_bool), # type: ignore[arg-type] 186 ) 187 188 SubElement( 189 svg, 190 "path", 191 { 192 "d": d_path, 193 "fill": fill_color, 194 "stroke": "none", 195 }, 196 ) 197 198 raw_svg = tostring(svg, encoding="unicode") 199 # we are in charge of the svg so it's safe to decode 200 return parseString(raw_svg).toprettyxml(indent=" ") # noqa: S318
plot the output of SolvedMaze(...).as_pixels() as a nice svg
Create an SVG with:
- A single rounded-square background (walls).
- Each non-wall cell is drawn via create_tile_path, with corner_radius controlling whether corners are rounded. (Set corner_radius=0 for squares.)
Parameters:
pixel_grid : np.ndarray
3D array of shape (h, w, 3) with RGB valuessize : int
Size (in px) of each grid cellcorner_radius : float
Radius for rounding corners of each tile (0 => squares)bounding_corner_radius : float
Radius for rounding the outer bounding rectangle
Returns:
str
: A pretty-printed SVG string