Coverage for maze_dataset/plotting/plot_svg_fancy.py: 0%

57 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-09 12:48 -0600

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