Spatial-Gym 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Spatial_Gym/Spatial_Gym.py +1314 -0
- Spatial_Gym/__init__.py +12 -0
- Spatial_Gym/register_env.py +8 -0
- Spatial_Gym/render/__init__.py +4 -0
- Spatial_Gym/render/human_renderer.py +451 -0
- Spatial_Gym/render/llm_renderer.py +229 -0
- spatial_gym-0.1.1.dist-info/METADATA +381 -0
- spatial_gym-0.1.1.dist-info/RECORD +11 -0
- spatial_gym-0.1.1.dist-info/WHEEL +4 -0
- spatial_gym-0.1.1.dist-info/entry_points.txt +2 -0
- spatial_gym-0.1.1.dist-info/licenses/LICENCE +21 -0
Spatial_Gym/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module initializes the Spatial-Gym environment package.
|
|
3
|
+
|
|
4
|
+
It imports the `Spatial_Gym` class and registers the environment for use with Gymnasium.
|
|
5
|
+
|
|
6
|
+
Modules:
|
|
7
|
+
- Spatial_Gym: Contains the implementation of the Spatial-Gym environment.
|
|
8
|
+
- register_env: Handles the registration of the Spatial-Gym environment with Gymnasium.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .Spatial_Gym import Spatial_Gym
|
|
12
|
+
from .register_env import *
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from gymnasium.envs.registration import register
|
|
2
|
+
'''
|
|
3
|
+
This script registers the custom environment "Spatial-Gym" with Gymnasium.
|
|
4
|
+
'''
|
|
5
|
+
register(
|
|
6
|
+
id="Spatial-Gym", # Unique identifier for your environment
|
|
7
|
+
entry_point="Spatial_Gym.Spatial_Gym:Spatial_Gym", # Path to your environment class
|
|
8
|
+
)
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def draw_line_round_corners_polygon(surf, p1, p2, c, w):
|
|
6
|
+
p1v = pygame.math.Vector2(p1)
|
|
7
|
+
p2v = pygame.math.Vector2(p2)
|
|
8
|
+
lv = (p2v - p1v).normalize()
|
|
9
|
+
lnv = pygame.math.Vector2(-lv.y, lv.x) * w // 2
|
|
10
|
+
pts = [p1v + lnv, p2v + lnv, p2v - lnv, p1v - lnv]
|
|
11
|
+
pygame.draw.polygon(surf, c, pts)
|
|
12
|
+
pygame.draw.circle(surf, c, p1, round(w / 2))
|
|
13
|
+
pygame.draw.circle(surf, c, p2, round(w / 2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HumanRenderer:
|
|
17
|
+
"""
|
|
18
|
+
Simple renderer for human-friendly visualization with custom colors.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, scale_factor=1.0):
|
|
22
|
+
self.screen = None
|
|
23
|
+
self.clock = None
|
|
24
|
+
self.initialized = False
|
|
25
|
+
self.current_cells_x = None
|
|
26
|
+
self.current_cells_y = None
|
|
27
|
+
self.scale_factor = scale_factor
|
|
28
|
+
|
|
29
|
+
# Custom color scheme
|
|
30
|
+
self.GRID_BACKGROUND = (0, 170, 136) # #00AA88 - Teal for grid cells
|
|
31
|
+
self.OVERALL_BACKGROUND = (17, 56, 51) # #113833 - Dark green for overall background
|
|
32
|
+
self.LINE_COLOR = (51, 68, 68) # #334444 - Dark gray-blue for lines
|
|
33
|
+
|
|
34
|
+
def initialize(self, cells_x, cells_y):
|
|
35
|
+
"""Initialize pygame and create the display window."""
|
|
36
|
+
if not self.initialized:
|
|
37
|
+
pygame.init()
|
|
38
|
+
# Calculate window size: cells + 20px padding on each side (scaled)
|
|
39
|
+
cell_size = int(40 * self.scale_factor)
|
|
40
|
+
padding = int(20 * self.scale_factor)
|
|
41
|
+
window_width = cells_x * cell_size + 2 * padding # cells + padding
|
|
42
|
+
window_height = cells_y * cell_size + 2 * padding # cells + padding
|
|
43
|
+
self.screen = pygame.display.set_mode((window_width, window_height))
|
|
44
|
+
pygame.display.set_caption("SPaRC Environment - Human View")
|
|
45
|
+
self.clock = pygame.time.Clock()
|
|
46
|
+
self.initialized = True
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
"""Close the pygame display."""
|
|
50
|
+
if self.initialized:
|
|
51
|
+
pygame.display.quit()
|
|
52
|
+
self.initialized = False
|
|
53
|
+
|
|
54
|
+
def render(self, obs_array, color_array, additional_info, polyshapes, x_size, y_size, start_location, target_location, path):
|
|
55
|
+
"""
|
|
56
|
+
Renders a simple grid with the specified colors.
|
|
57
|
+
|
|
58
|
+
Parameters:
|
|
59
|
+
obs_array (dict): Dictionary of observation arrays
|
|
60
|
+
color_array (numpy.ndarray): Array containing color information
|
|
61
|
+
additional_info (numpy.ndarray): Array containing additional property info
|
|
62
|
+
polyshapes (dict): Dictionary of polyshape definitions
|
|
63
|
+
x_size (int): Width of the full grid
|
|
64
|
+
y_size (int): Height of the full grid
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
None
|
|
68
|
+
"""
|
|
69
|
+
# Calculate actual puzzle cells (excluding gaps)
|
|
70
|
+
cells_x = (x_size - 1) // 2
|
|
71
|
+
cells_y = (y_size - 1) // 2
|
|
72
|
+
|
|
73
|
+
# Check if grid size has changed and reinitialize if needed
|
|
74
|
+
if not self.initialized or self.current_cells_x != cells_x or self.current_cells_y != cells_y:
|
|
75
|
+
if self.initialized:
|
|
76
|
+
# Close existing window before creating new one
|
|
77
|
+
pygame.display.quit()
|
|
78
|
+
self.initialized = False
|
|
79
|
+
self.initialize(cells_x, cells_y)
|
|
80
|
+
self.current_cells_x = cells_x
|
|
81
|
+
self.current_cells_y = cells_y
|
|
82
|
+
|
|
83
|
+
cell_size = int(40 * self.scale_factor)
|
|
84
|
+
padding = int(20 * self.scale_factor)
|
|
85
|
+
|
|
86
|
+
# Fill with overall background color
|
|
87
|
+
self.screen.fill(self.OVERALL_BACKGROUND)
|
|
88
|
+
|
|
89
|
+
# Draw cells with no gaps (adjacent to each other)
|
|
90
|
+
for grid_y in range(cells_y):
|
|
91
|
+
for grid_x in range(cells_x):
|
|
92
|
+
# Calculate position with padding only
|
|
93
|
+
x_pos = padding + grid_x * cell_size
|
|
94
|
+
y_pos = padding + grid_y * cell_size
|
|
95
|
+
|
|
96
|
+
# Draw the cell
|
|
97
|
+
rect = pygame.Rect(x_pos, y_pos, cell_size, cell_size)
|
|
98
|
+
pygame.draw.rect(self.screen, self.GRID_BACKGROUND, rect)
|
|
99
|
+
|
|
100
|
+
# Draw complete grid using the round corner line function
|
|
101
|
+
line_width = int(12 * self.scale_factor)
|
|
102
|
+
gap_size = int(line_width * 1.5) # Size of the gap
|
|
103
|
+
|
|
104
|
+
# Calculate grid boundaries with simple integer coordinates
|
|
105
|
+
grid_left = padding
|
|
106
|
+
grid_right = padding + cells_x * cell_size
|
|
107
|
+
grid_top = padding
|
|
108
|
+
grid_bottom = padding + cells_y * cell_size
|
|
109
|
+
|
|
110
|
+
# Draw all vertical lines with gaps where needed
|
|
111
|
+
for line_x in range(cells_x + 1):
|
|
112
|
+
x_pos = padding + line_x * cell_size
|
|
113
|
+
self._draw_line_with_gaps(
|
|
114
|
+
(x_pos, grid_top), (x_pos, grid_bottom),
|
|
115
|
+
line_x * 2, True, obs_array, line_width, gap_size
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Draw all horizontal lines with gaps where needed
|
|
119
|
+
for line_y in range(cells_y + 1):
|
|
120
|
+
y_pos = padding + line_y * cell_size
|
|
121
|
+
self._draw_line_with_gaps(
|
|
122
|
+
(grid_left, y_pos), (grid_right, y_pos),
|
|
123
|
+
line_y * 2, False, obs_array, line_width, gap_size
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Use the provided start and target locations
|
|
127
|
+
start_line_x, start_line_y = start_location[0], start_location[1]
|
|
128
|
+
end_line_x, end_line_y = target_location[0], target_location[1]
|
|
129
|
+
|
|
130
|
+
# Find agent location from environment data
|
|
131
|
+
agent_line_x, agent_line_y = None, None
|
|
132
|
+
for y in range(y_size):
|
|
133
|
+
for x in range(x_size):
|
|
134
|
+
if "agent_location" in obs_array and obs_array["agent_location"][x, y] == 1:
|
|
135
|
+
agent_line_x, agent_line_y = x, y
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
# Draw a large circle at the start position
|
|
139
|
+
start_pixel_x = padding + start_line_x * cell_size//2
|
|
140
|
+
start_pixel_y = padding + start_line_y * cell_size//2
|
|
141
|
+
start_circle_radius = int(cell_size // 4)
|
|
142
|
+
|
|
143
|
+
# Check if start position has been visited (has white path through it) and choose circle color accordingly
|
|
144
|
+
start_is_visited = False
|
|
145
|
+
if "visited" in obs_array and start_line_x is not None and start_line_y is not None:
|
|
146
|
+
if (0 <= start_line_y < y_size and 0 <= start_line_x < x_size and
|
|
147
|
+
obs_array["visited"][start_line_x, start_line_y] == 1):
|
|
148
|
+
start_is_visited = True
|
|
149
|
+
|
|
150
|
+
if start_is_visited:
|
|
151
|
+
# Start position has been visited (white path goes through it), draw in white
|
|
152
|
+
start_circle_color = (255, 255, 255)
|
|
153
|
+
else:
|
|
154
|
+
# Start position hasn't been visited, draw in line color
|
|
155
|
+
start_circle_color = self.LINE_COLOR
|
|
156
|
+
|
|
157
|
+
pygame.draw.circle(self.screen, start_circle_color, (start_pixel_x, start_pixel_y), start_circle_radius)
|
|
158
|
+
|
|
159
|
+
# Draw half-line at end location facing outwards
|
|
160
|
+
end_pixel_x = padding + end_line_x * cell_size//2
|
|
161
|
+
end_pixel_y = padding + end_line_y * cell_size//2
|
|
162
|
+
|
|
163
|
+
# Determine which edge the end position is on and face outward
|
|
164
|
+
line_length = int(cell_size // 4)
|
|
165
|
+
|
|
166
|
+
# Check which edge we're on and set line direction accordingly
|
|
167
|
+
if end_line_x == 0: # Left edge
|
|
168
|
+
line_start = (end_pixel_x, end_pixel_y)
|
|
169
|
+
line_end = (end_pixel_x - line_length, end_pixel_y) # Face left
|
|
170
|
+
elif end_line_x == cells_x * 2: # Right edge
|
|
171
|
+
line_start = (end_pixel_x, end_pixel_y)
|
|
172
|
+
line_end = (end_pixel_x + line_length, end_pixel_y) # Face right
|
|
173
|
+
elif end_line_y == 0: # Top edge
|
|
174
|
+
line_start = (end_pixel_x, end_pixel_y)
|
|
175
|
+
line_end = (end_pixel_x, end_pixel_y - line_length) # Face up
|
|
176
|
+
elif end_line_y == cells_y * 2: # Bottom edge
|
|
177
|
+
line_start = (end_pixel_x, end_pixel_y)
|
|
178
|
+
line_end = (end_pixel_x, end_pixel_y + line_length) # Face down
|
|
179
|
+
else: # Default case (shouldn't happen if positioned on edge)
|
|
180
|
+
line_start = (end_pixel_x, end_pixel_y)
|
|
181
|
+
line_end = (end_pixel_x + line_length, end_pixel_y)
|
|
182
|
+
|
|
183
|
+
# Check if end position has been visited (reached by agent) and choose line color accordingly
|
|
184
|
+
end_is_visited = False
|
|
185
|
+
if "visited" in obs_array and end_line_x is not None and end_line_y is not None:
|
|
186
|
+
if (0 <= end_line_y < y_size and 0 <= end_line_x < x_size and
|
|
187
|
+
obs_array["visited"][end_line_x, end_line_y] == 1):
|
|
188
|
+
end_is_visited = True
|
|
189
|
+
|
|
190
|
+
if end_is_visited:
|
|
191
|
+
# End position has been visited (agent reached it), draw in white (agent color)
|
|
192
|
+
end_line_color = (255, 255, 255)
|
|
193
|
+
else:
|
|
194
|
+
# End position hasn't been visited, draw in default line color
|
|
195
|
+
end_line_color = self.LINE_COLOR
|
|
196
|
+
|
|
197
|
+
draw_line_round_corners_polygon(self.screen, line_start, line_end, end_line_color, line_width)
|
|
198
|
+
|
|
199
|
+
# Draw agent path in white
|
|
200
|
+
self._draw_agent_path(path, y_size, x_size, padding, cell_size, line_width)
|
|
201
|
+
|
|
202
|
+
# Draw other properties (symbols)
|
|
203
|
+
for prop, array in obs_array.items():
|
|
204
|
+
if prop in ["visited", "gaps", "agent_location", "target_location"]:
|
|
205
|
+
continue # Skip already visualized properties
|
|
206
|
+
|
|
207
|
+
for y in range(y_size):
|
|
208
|
+
for x in range(x_size):
|
|
209
|
+
if array[x, y]: # If the property exists at this cell
|
|
210
|
+
pixel_x = padding + x * cell_size//2
|
|
211
|
+
pixel_y = padding + y * cell_size//2
|
|
212
|
+
center = (pixel_x, pixel_y)
|
|
213
|
+
|
|
214
|
+
prop_type = prop # e.g., "star", "poly", "triangle", "dot"
|
|
215
|
+
color = color_array[x, y] # Get the color from the color array
|
|
216
|
+
color = self._get_color_from_name(color) # Extract color
|
|
217
|
+
|
|
218
|
+
# Draw the property based on its type
|
|
219
|
+
# Draw a star
|
|
220
|
+
if prop_type == "star":
|
|
221
|
+
self._draw_star(self.screen, color, center, int(cell_size // 6))
|
|
222
|
+
|
|
223
|
+
# Draw a polyshape
|
|
224
|
+
elif prop_type == "poly":
|
|
225
|
+
shape = f'{additional_info[x, y] }' # Get the polyshape name from additional_info
|
|
226
|
+
shape_array = polyshapes[shape]
|
|
227
|
+
top_left = (pixel_x - cell_size//4, pixel_y - cell_size//4)
|
|
228
|
+
self._draw_polyshape(self.screen, shape_array, top_left, cell_size//2, color)
|
|
229
|
+
|
|
230
|
+
# Draw a polyshape with "ylop" type
|
|
231
|
+
elif prop_type == "ylop":
|
|
232
|
+
shape = f'{additional_info[x, y]}' # Get the polyshape name from additional_info
|
|
233
|
+
shape_array = polyshapes[shape]
|
|
234
|
+
top_left = (pixel_x - cell_size//4, pixel_y - cell_size//4)
|
|
235
|
+
self._draw_polyshape(self.screen, shape_array, top_left, cell_size//2, color)
|
|
236
|
+
|
|
237
|
+
# Draw a triangle with a count
|
|
238
|
+
elif prop_type == "triangle":
|
|
239
|
+
triangle_size = int(cell_size // 6)
|
|
240
|
+
pygame.draw.polygon(self.screen, color, [
|
|
241
|
+
(center[0], center[1] - triangle_size), # Top
|
|
242
|
+
(center[0] - triangle_size, center[1] + triangle_size), # Bottom-left
|
|
243
|
+
(center[0] + triangle_size, center[1] + triangle_size) # Bottom-right
|
|
244
|
+
])
|
|
245
|
+
|
|
246
|
+
# Add text for triangle count
|
|
247
|
+
count = f'{additional_info[x, y]}' # Get the count from additional_info
|
|
248
|
+
font = pygame.font.Font(None, int(16 * self.scale_factor))
|
|
249
|
+
text = font.render(count, True, (255, 255, 255))
|
|
250
|
+
shadow = font.render(count, True, (0, 0, 0))
|
|
251
|
+
shadow_pos = (center[0] - int(4 * self.scale_factor) + 1, center[1] - int(12 * self.scale_factor) + 1)
|
|
252
|
+
self.screen.blit(shadow, shadow_pos)
|
|
253
|
+
text_pos = (center[0] - int(4 * self.scale_factor), center[1] - int(12 * self.scale_factor))
|
|
254
|
+
self.screen.blit(text, text_pos)
|
|
255
|
+
|
|
256
|
+
# Draw a square
|
|
257
|
+
elif prop_type == "square":
|
|
258
|
+
square_size = int(cell_size // 4)
|
|
259
|
+
square_rect = pygame.Rect(
|
|
260
|
+
center[0] - square_size // 2,
|
|
261
|
+
center[1] - square_size // 2,
|
|
262
|
+
square_size,
|
|
263
|
+
square_size
|
|
264
|
+
)
|
|
265
|
+
pygame.draw.rect(self.screen, color, square_rect)
|
|
266
|
+
|
|
267
|
+
# Draw a dot as black hexagon
|
|
268
|
+
elif prop_type == "dot":
|
|
269
|
+
self._draw_hexagon(pixel_x, pixel_y, int(line_width // 3), (0, 0, 0))
|
|
270
|
+
|
|
271
|
+
pygame.display.flip()
|
|
272
|
+
self.clock.tick(30) # Limit to 30 FPS
|
|
273
|
+
|
|
274
|
+
# Handle pygame events
|
|
275
|
+
for event in pygame.event.get():
|
|
276
|
+
if event.type == pygame.QUIT:
|
|
277
|
+
pygame.quit()
|
|
278
|
+
exit()
|
|
279
|
+
|
|
280
|
+
def _draw_hexagon(self, center_x, center_y, radius, color):
|
|
281
|
+
"""Draw a hexagon at the specified center position."""
|
|
282
|
+
points = []
|
|
283
|
+
for i in range(6):
|
|
284
|
+
angle = i * 60 * math.pi / 180 # Convert degrees to radians
|
|
285
|
+
x = center_x + radius * math.cos(angle)
|
|
286
|
+
y = center_y + radius * math.sin(angle)
|
|
287
|
+
points.append((x, y))
|
|
288
|
+
pygame.draw.polygon(self.screen, color, points)
|
|
289
|
+
|
|
290
|
+
def _draw_star(self, surface, color, center, radius):
|
|
291
|
+
"""
|
|
292
|
+
Draws a star shape at a specified location.
|
|
293
|
+
|
|
294
|
+
Parameters:
|
|
295
|
+
surface (pygame.Surface): The Pygame surface to draw on.
|
|
296
|
+
color (tuple): The RGB color to use for the star.
|
|
297
|
+
center (tuple): The (x, y) coordinates of the center of the star.
|
|
298
|
+
radius (int): The radius of the star, determining its size.
|
|
299
|
+
"""
|
|
300
|
+
points = []
|
|
301
|
+
for i in range(10):
|
|
302
|
+
angle = math.pi / 2 + i * math.pi / 5
|
|
303
|
+
r = radius if i % 2 == 0 else radius // 2
|
|
304
|
+
x = center[0] + int(math.cos(angle) * r)
|
|
305
|
+
y = center[1] - int(math.sin(angle) * r)
|
|
306
|
+
points.append((x, y))
|
|
307
|
+
pygame.draw.polygon(surface, color, points)
|
|
308
|
+
|
|
309
|
+
def _draw_polyshape(self, surface, shape_array, top_left, cell_size, color):
|
|
310
|
+
"""
|
|
311
|
+
Draws a polyshape inside a given cell.
|
|
312
|
+
|
|
313
|
+
Parameters:
|
|
314
|
+
surface (pygame.Surface): The Pygame surface to draw on.
|
|
315
|
+
shape_array (list of lists): A 2D array representing the polyshape, where 1 indicates a filled cell and 0 indicates an empty cell.
|
|
316
|
+
top_left (tuple): The top-left corner of the cell
|
|
317
|
+
cell_size (int): The size of the cell in pixels.
|
|
318
|
+
color (tuple): The RGB color to use for the polyshape.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
shape_height = len(shape_array)
|
|
322
|
+
shape_width = len(shape_array[0])
|
|
323
|
+
|
|
324
|
+
padding = cell_size // 6
|
|
325
|
+
|
|
326
|
+
mini_block_width = (cell_size - 2 * padding) // shape_width
|
|
327
|
+
mini_block_height = (cell_size - 2 * padding) // shape_height
|
|
328
|
+
|
|
329
|
+
for y, row in enumerate(shape_array):
|
|
330
|
+
for x, val in enumerate(row):
|
|
331
|
+
if val:
|
|
332
|
+
rect = pygame.Rect(
|
|
333
|
+
top_left[0] + padding + x * mini_block_width,
|
|
334
|
+
top_left[1] + padding + y * mini_block_height,
|
|
335
|
+
mini_block_width,
|
|
336
|
+
mini_block_height
|
|
337
|
+
)
|
|
338
|
+
pygame.draw.rect(surface, color, rect)
|
|
339
|
+
|
|
340
|
+
def _get_color_from_name(self, color):
|
|
341
|
+
"""
|
|
342
|
+
Helper function to extract color from property name parts.
|
|
343
|
+
"""
|
|
344
|
+
if color == 1:
|
|
345
|
+
return (255, 0, 0) # Red
|
|
346
|
+
elif color == 2:
|
|
347
|
+
return (0, 0, 255) # Blue
|
|
348
|
+
elif color == 3:
|
|
349
|
+
return (255, 255, 0) # Yellow
|
|
350
|
+
elif color == 4:
|
|
351
|
+
return (0, 255, 0) # Green
|
|
352
|
+
elif color == 5:
|
|
353
|
+
return (0, 0, 0) # Black
|
|
354
|
+
elif color == 6:
|
|
355
|
+
return (128, 0, 128) # Purple
|
|
356
|
+
elif color == 7:
|
|
357
|
+
return (255, 165, 0) # Orange
|
|
358
|
+
elif color == 8:
|
|
359
|
+
return (255, 255, 255) # White
|
|
360
|
+
else:
|
|
361
|
+
return (128, 128, 128) # Default: Gray
|
|
362
|
+
|
|
363
|
+
def _draw_agent_path(self, path, y_size, x_size, padding, cell_size, line_width):
|
|
364
|
+
"""Draw the agent's path as white lines connecting positions in order."""
|
|
365
|
+
if not path or len(path) < 2:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Draw white line segments connecting consecutive positions in the path
|
|
369
|
+
white_line_width = max(1, int(line_width))
|
|
370
|
+
|
|
371
|
+
for i in range(len(path) - 1):
|
|
372
|
+
current_pos = path[i] # [x, y] format
|
|
373
|
+
next_pos = path[i + 1] # [x, y] format
|
|
374
|
+
|
|
375
|
+
# Convert to pixel coordinates (note the coordinate swap: path uses [x,y] but pixel calc needs [y,x])
|
|
376
|
+
current_pixel = (
|
|
377
|
+
padding + current_pos[0] * cell_size // 2,
|
|
378
|
+
padding + current_pos[1] * cell_size // 2
|
|
379
|
+
)
|
|
380
|
+
next_pixel = (
|
|
381
|
+
padding + next_pos[0] * cell_size // 2,
|
|
382
|
+
padding + next_pos[1] * cell_size // 2
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Draw white line segment between consecutive path positions
|
|
386
|
+
draw_line_round_corners_polygon(
|
|
387
|
+
self.screen, current_pixel, next_pixel,
|
|
388
|
+
(255, 255, 255), white_line_width
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _draw_line_with_gaps(self, start_pos, end_pos, line_coord, is_vertical, obs_array, line_width, gap_size):
|
|
392
|
+
"""Draw a line with gaps where specified in the obs_array."""
|
|
393
|
+
cell_size = int(40 * self.scale_factor)
|
|
394
|
+
padding = int(20 * self.scale_factor)
|
|
395
|
+
|
|
396
|
+
# Find all gap positions along this line
|
|
397
|
+
gap_positions = []
|
|
398
|
+
if "gaps" in obs_array:
|
|
399
|
+
if is_vertical:
|
|
400
|
+
# For vertical lines, check all y positions at this x coordinate
|
|
401
|
+
for y in range(obs_array["gaps"].shape[1]):
|
|
402
|
+
if obs_array["gaps"][line_coord, y] == 1:
|
|
403
|
+
gap_pixel_y = padding + y * cell_size // 2
|
|
404
|
+
gap_positions.append(gap_pixel_y)
|
|
405
|
+
else:
|
|
406
|
+
# For horizontal lines, check all x positions at this y coordinate
|
|
407
|
+
for x in range(obs_array["gaps"].shape[0]):
|
|
408
|
+
if obs_array["gaps"][x, line_coord] == 1:
|
|
409
|
+
gap_pixel_x = padding + x * cell_size // 2
|
|
410
|
+
gap_positions.append(gap_pixel_x)
|
|
411
|
+
|
|
412
|
+
# If no gaps, draw the complete line
|
|
413
|
+
if not gap_positions:
|
|
414
|
+
draw_line_round_corners_polygon(self.screen, start_pos, end_pos, self.LINE_COLOR, line_width)
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Draw line segments around gaps using polygon line function
|
|
418
|
+
if is_vertical:
|
|
419
|
+
current_y = start_pos[1]
|
|
420
|
+
for gap_y in sorted(gap_positions):
|
|
421
|
+
# Draw segment before gap
|
|
422
|
+
if current_y < gap_y - gap_size // 2:
|
|
423
|
+
segment_start = (start_pos[0], current_y)
|
|
424
|
+
segment_end = (start_pos[0], gap_y - gap_size // 2)
|
|
425
|
+
draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
|
|
426
|
+
|
|
427
|
+
# Skip over the gap
|
|
428
|
+
current_y = gap_y + gap_size // 2
|
|
429
|
+
|
|
430
|
+
# Draw final segment after last gap
|
|
431
|
+
if current_y < end_pos[1]:
|
|
432
|
+
segment_start = (start_pos[0], current_y)
|
|
433
|
+
segment_end = end_pos
|
|
434
|
+
draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
|
|
435
|
+
else:
|
|
436
|
+
current_x = start_pos[0]
|
|
437
|
+
for gap_x in sorted(gap_positions):
|
|
438
|
+
# Draw segment before gap
|
|
439
|
+
if current_x < gap_x - gap_size // 2:
|
|
440
|
+
segment_start = (current_x, start_pos[1])
|
|
441
|
+
segment_end = (gap_x - gap_size // 2, start_pos[1])
|
|
442
|
+
draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
|
|
443
|
+
|
|
444
|
+
# Skip over the gap
|
|
445
|
+
current_x = gap_x + gap_size // 2
|
|
446
|
+
|
|
447
|
+
# Draw final segment after last gap
|
|
448
|
+
if current_x < end_pos[0]:
|
|
449
|
+
segment_start = (current_x, start_pos[1])
|
|
450
|
+
segment_end = end_pos
|
|
451
|
+
draw_line_round_corners_polygon(self.screen, segment_start, segment_end, self.LINE_COLOR, line_width)
|