-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrenderer.py
More file actions
173 lines (142 loc) · 6.91 KB
/
renderer.py
File metadata and controls
173 lines (142 loc) · 6.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#Rendering the Frontend. map, sensor view, info bar
import pygame
import math
from config_parameters import *
class Renderer:
def __init__(self, screen):
self.screen=screen
self.font=pygame.font.Font(None,24)
#Coordinate mapping
view_w=VIEW_X_MAX - VIEW_X_MIN
view_h=VIEW_Y_MAX - VIEW_Y_MIN
margin=25
usable_w=MAP_WIDTH - 2*margin
usable_h=MAP_HEIGHT - 2*margin
#Fit the world viewport into the map panel while preserving aspect ratio
self.scale=min(usable_w/view_w, usable_h/view_h)
actual_w=view_w * self.scale
actual_h=view_h * self.scale
#Center the scaled viewport within the usable area
self.ox=margin + (usable_w - actual_w)/2
self.oy=margin + (usable_h - actual_h)/2
#Sensor view
self.scx=SENSOR_X + SENSOR_WIDTH//2
self.scy=SENSOR_HEIGHT//2
#Scale so the max range fits comfortably within the sensor panel
self.sscale=min(SENSOR_WIDTH,SENSOR_HEIGHT) / (LIDAR_MAX_RANGE*2.4)
#Map coordinate system-> Screen (y is flipped)
def map2screen(self, wx, wy):
return int(self.ox + (wx-VIEW_X_MIN)*self.scale), int(self.oy + (VIEW_Y_MAX-wy)*self.scale)
#Screen coordinate system-> Map
def screen2map(self, sx, sy):
return (sx-self.ox)/self.scale + VIEW_X_MIN, VIEW_Y_MAX - (sy-self.oy)/self.scale
#If a coordinate (such as selected by clicking) is in the actual map portion rather than out of bounds
def in_map(self, sx, sy):
return sx < MAP_WIDTH and sy < MAP_HEIGHT
#Draws Triangles, Rectangles, L-Walls (as line segments) and Circles (as actual circles)
def draw_obstacles(self, map):
for obs in map.obstacles:
for seg in obs.segments:
p1=self.map2screen(seg[0], seg[1])
p2=self.map2screen(seg[2], seg[3])
pygame.draw.line(self.screen,COLOR_OBSTACLE,p1,p2,2)
#Circles drawn with pygame instead of polygon approximation
for circ in map.circles:
cx,cy=self.map2screen(circ[0], circ[1])
#Radius needs to be scaled from world units to screen pixels
r=int(circ[2] * self.scale)
pygame.draw.circle(self.screen,COLOR_OBSTACLE,(cx,cy),r,2)
#The robot itself, a yellow square
def draw_robot(self, robot):
sx,sy=self.map2screen(robot.x, robot.y)
size=5
rect=pygame.Rect(sx-size, sy-size, size*2, size*2)
pygame.draw.rect(self.screen, COLOR_ROBOT, rect)
def draw_rays(self, robot, scan):
if scan is None: return
sx,sy=self.map2screen(robot.x, robot.y)
#Subsample to ~50 rays for drawing to avoid cluttering.
step=max(1, len(scan)//50)
for i in range(0, len(scan), step):
angle,dist,hx,hy,hit=scan[i]
#Recompute endpoint from angle+dist rather than using stored hx/hy (same result)
wa=robot.heading + angle
ewx=robot.x + dist * math.cos(wa)
ewy=robot.y + dist * math.sin(wa)
ex,ey=self.map2screen(ewx, ewy)
#Clip rays to map area so they dont bleed into the sensor panel
ex=min(ex, MAP_WIDTH)
pygame.draw.line(self.screen,COLOR_RAY,(sx,sy),(ex,ey),1)
if hit and ex < MAP_WIDTH:
pygame.draw.circle(self.screen,COLOR_RAY_HIT,(ex,ey),2)
def draw_target(self, tx, ty):
sx,sy=self.map2screen(tx, ty)
#Crosshair: circle + horizontal + vertical lines
pygame.draw.circle(self.screen,COLOR_TARGET,(sx,sy),7,2)
pygame.draw.line(self.screen,COLOR_TARGET,(sx-9,sy),(sx+9,sy),1)
pygame.draw.line(self.screen,COLOR_TARGET,(sx,sy-9),(sx,sy+9),1)
def draw_path(self, path):
if len(path) < 2: return
#Cap at last 3000 points to avoid slowdown on long runs
pts=[self.map2screen(px, py) for px,py in path[-3000:]]
if len(pts) >= 2:
pygame.draw.lines(self.screen,COLOR_PATH,False,pts,1)
def draw_bug_markers(self, nav):
if nav.hit_point is not None:
hx,hy=self.map2screen(nav.hit_point[0], nav.hit_point[1])
pygame.draw.circle(self.screen,COLOR_HIT_MARKER,(hx,hy),4,2)
self.screen.blit(self.font.render("H",True,COLOR_HIT_MARKER),(hx+5,hy-12))
#Only show leave marker while actively boundary-following, not after leaving
if nav.best_leave is not None and nav.state in (2,3):
lx,ly=self.map2screen(nav.best_leave[0], nav.best_leave[1])
pygame.draw.circle(self.screen,COLOR_LEAVE_MARKER,(lx,ly),4,2)
self.screen.blit(self.font.render("L",True,COLOR_LEAVE_MARKER),(lx+5,ly-12))
def draw_sensor_view(self, robot, scan):
pygame.draw.rect(self.screen,(30,30,50),(SENSOR_X,0,SENSOR_WIDTH,SENSOR_HEIGHT),1)
pygame.draw.circle(self.screen,COLOR_ROBOT,(self.scx,self.scy),3)
#No scan - no sensor data to display
if scan is None:
return
prev=None
for angle,dist,hx,hy,hit in scan:
#Skip rays that reached max range — they didn't hit anything real
if hit and dist < LIDAR_MAX_RANGE * 0.95:
#Convert polar (angle relative to heading, dist) to local screen coords
#sin/cos swapped so 0 degrees points up on screen rather than right
lx=dist * math.sin(angle)
ly=dist * math.cos(angle)
px=self.scx + int(lx * self.sscale)
py=self.scy - int(ly * self.sscale)
pygame.draw.circle(self.screen,COLOR_SENSOR_HIT,(px,py),2)
if prev:
dx=px-prev[0]
dy=py-prev[1]
#Only connect adjacent hits if they are close — avoids lines across gaps
if dx*dx+dy*dy < 1500:
pygame.draw.line(self.screen,(40,120,180),prev,(px,py),1)
prev=(px,py)
else:
prev=None
def draw_info(self, robot, nav, fps, steps, manual_mode=False):
bar_y=SCREEN_HEIGHT - INFO_BAR_HEIGHT
pygame.draw.rect(self.screen,COLOR_INFO_BG,(0,bar_y,SCREEN_WIDTH,INFO_BAR_HEIGHT))
if manual_mode:
parts=[
"MANUAL | Arrows to drive",
f"Pos: ({robot.x:.1f}, {robot.y:.1f})",
]
else:
parts=[
f"BUG {nav.algorithm} | {nav.get_state_name()}",
f"Pos: ({robot.x:.1f}, {robot.y:.1f})",
]
if nav.target_x is not None:
parts.append(f"Target: ({nav.target_x:.1f}, {nav.target_y:.1f})")
parts.append(f"Steps: {steps} FPS: {fps:.0f}")
parts.append("Click=Target 1/2/3=Mode R=Reset N=Noise")
#Lay out info sections left to right, spaced by each label's actual width
x=10
for t in parts:
lbl=self.font.render(t,True,COLOR_TEXT)
self.screen.blit(lbl,(x,bar_y+10))
x += lbl.get_width() + 25