docs for maze-dataset v1.3.2
View Source on GitHub

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

COLOR_MAP: dict[tuple[int, int, int], str] = {(255, 255, 255): '#f0f0f0', (0, 255, 0): '#4caf50', (255, 0, 0): '#f44336', (0, 0, 255): '#2196f3'}
WALL_COLOR_HEX: str = '#222'
WALL_RGB: tuple[int, int, int] = (0, 0, 0)
def is_wall(y: int, x: int, grid: numpy.ndarray) -> bool:
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.

def create_tile_path( origin: tuple[float, float], tile_size: float, corner_radius: float, edges: tuple[bool, bool, bool, bool]) -> str:
 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]

def plot_svg_fancy( pixel_grid: numpy.ndarray, size: int = 40, corner_radius: float = 8.0, bounding_corner_radius: float = 20.0) -> str:
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 values
  • size : int Size (in px) of each grid cell
  • corner_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