nav-sim2d 0.0.1__tar.gz
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.
- nav_sim2d-0.0.1/PKG-INFO +236 -0
- nav_sim2d-0.0.1/README.md +224 -0
- nav_sim2d-0.0.1/nav_sim2d/__init__.py +4 -0
- nav_sim2d-0.0.1/nav_sim2d/camera.py +72 -0
- nav_sim2d-0.0.1/nav_sim2d/config.py +10 -0
- nav_sim2d-0.0.1/nav_sim2d/geometry.py +14 -0
- nav_sim2d-0.0.1/nav_sim2d/lidar.py +54 -0
- nav_sim2d-0.0.1/nav_sim2d/robots/__init__.py +16 -0
- nav_sim2d-0.0.1/nav_sim2d/robots/ackermann.py +167 -0
- nav_sim2d-0.0.1/nav_sim2d/robots/base.py +33 -0
- nav_sim2d-0.0.1/nav_sim2d/robots/differential.py +56 -0
- nav_sim2d-0.0.1/nav_sim2d/robots/holonomic.py +62 -0
- nav_sim2d-0.0.1/nav_sim2d/simulator.py +384 -0
- nav_sim2d-0.0.1/nav_sim2d/ui/__init__.py +0 -0
- nav_sim2d-0.0.1/nav_sim2d/ui/gui.py +251 -0
- nav_sim2d-0.0.1/nav_sim2d/waypoints.py +72 -0
- nav_sim2d-0.0.1/nav_sim2d/world.py +282 -0
- nav_sim2d-0.0.1/nav_sim2d.egg-info/PKG-INFO +236 -0
- nav_sim2d-0.0.1/nav_sim2d.egg-info/SOURCES.txt +23 -0
- nav_sim2d-0.0.1/nav_sim2d.egg-info/dependency_links.txt +1 -0
- nav_sim2d-0.0.1/nav_sim2d.egg-info/requires.txt +4 -0
- nav_sim2d-0.0.1/nav_sim2d.egg-info/top_level.txt +1 -0
- nav_sim2d-0.0.1/pyproject.toml +21 -0
- nav_sim2d-0.0.1/setup.cfg +4 -0
- nav_sim2d-0.0.1/tests/test_geometry.py +12 -0
nav_sim2d-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nav-sim2d
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Educational library of 2D simulation for mobile robot navigation.
|
|
5
|
+
Author: Elias J. R. Freitas
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: pygame>=2.5.0
|
|
9
|
+
Requires-Dist: pygame_gui>=0.6.9
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: PyYAML>=6.0
|
|
12
|
+
|
|
13
|
+
# Nav Sim
|
|
14
|
+
|
|
15
|
+
A lightweight 2D simulation framework for mobile robot navigation using **Pygame**, with support for sensors, external controllers, and interactive visualization.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Supported Robot Models
|
|
20
|
+
|
|
21
|
+
- `holonomic`
|
|
22
|
+
Command:
|
|
23
|
+
```python
|
|
24
|
+
{"vx": ..., "vy": ...}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `differential`
|
|
28
|
+
Command:
|
|
29
|
+
```python
|
|
30
|
+
{"v": ..., "omega": ...}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `ackermann`
|
|
34
|
+
Command:
|
|
35
|
+
```python
|
|
36
|
+
{"speed": ..., "steer": ...}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🧠 Architecture
|
|
42
|
+
|
|
43
|
+
The simulator follows a modular design:
|
|
44
|
+
|
|
45
|
+
- ✅ **Simulator (`nav_sim`)**
|
|
46
|
+
Handles physics, rendering, environment, and sensors
|
|
47
|
+
|
|
48
|
+
- ✅ **External Controllers**
|
|
49
|
+
Implement navigation strategies (APF, Vortex, etc.)
|
|
50
|
+
|
|
51
|
+
> Controllers are not part of the core library. They live in `examples/` or in the user’s project.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 📡 Sensors
|
|
56
|
+
|
|
57
|
+
### LiDAR (2D)
|
|
58
|
+
|
|
59
|
+
The simulator provides a configurable 2D LiDAR:
|
|
60
|
+
|
|
61
|
+
- Angular scan
|
|
62
|
+
- Range-limited
|
|
63
|
+
- Returns:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
(angle, distance, hit_point)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Passed to controllers as:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
lidar_readings
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ⚙️ Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m venv .venv
|
|
81
|
+
|
|
82
|
+
# Linux / macOS
|
|
83
|
+
source .venv/bin/activate
|
|
84
|
+
|
|
85
|
+
# Windows
|
|
86
|
+
.venv\Scripts\activate
|
|
87
|
+
|
|
88
|
+
pip install -r requirements.txt
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ▶️ Run Examples
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python examples/run_holonomic.py
|
|
97
|
+
python examples/run_differential.py
|
|
98
|
+
python examples/run_ackermann.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🧪 Basic Usage
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
from nav_sim import Simulator, load_config
|
|
108
|
+
from examples.controllers.potential_field import potential_field_controller
|
|
109
|
+
|
|
110
|
+
config = load_config(Path("config/default.yaml"))
|
|
111
|
+
config["robot"]["type"] = "differential"
|
|
112
|
+
|
|
113
|
+
sim = Simulator(
|
|
114
|
+
config=config,
|
|
115
|
+
controller=potential_field_controller
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
sim.run()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧩 Controller Interface
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
def controller(robot, world, goal, config, dt, lidar_readings=None) -> dict:
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Parameters
|
|
131
|
+
|
|
132
|
+
- `robot` → robot state (position, heading, limits)
|
|
133
|
+
- `world` → obstacle representation
|
|
134
|
+
- `goal` → current waypoint
|
|
135
|
+
- `config` → YAML configuration
|
|
136
|
+
- `dt` → simulation timestep
|
|
137
|
+
- `lidar_readings` → sensor data
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🧠 Artificial Potential Fields (APF)
|
|
142
|
+
|
|
143
|
+
This project includes a classical APF implementation based on Khatib’s method:
|
|
144
|
+
|
|
145
|
+
### Total force
|
|
146
|
+
|
|
147
|
+
F(q) = F_att(q) + F_rep(q)
|
|
148
|
+
|
|
149
|
+
### Attractive force
|
|
150
|
+
|
|
151
|
+
F_att = k_att (q_goal - q)
|
|
152
|
+
|
|
153
|
+
### Repulsive force
|
|
154
|
+
|
|
155
|
+
F_rep,i = k_r (1/d_i - 1/d_s) (1/d_i^2) r_hat_i
|
|
156
|
+
|
|
157
|
+
Where:
|
|
158
|
+
|
|
159
|
+
- d_i → LiDAR distance corrected by robot radius
|
|
160
|
+
- d_s → influence distance
|
|
161
|
+
- r_hat_i → unit vector away from obstacle
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 🛑 Collision Handling
|
|
166
|
+
|
|
167
|
+
The simulator detects collisions using:
|
|
168
|
+
|
|
169
|
+
d <= r_robot + r_obstacle
|
|
170
|
+
|
|
171
|
+
### Behavior
|
|
172
|
+
|
|
173
|
+
- The robot stops immediately
|
|
174
|
+
- A visual indicator (💥) is displayed
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 🎨 Custom Drawing
|
|
179
|
+
|
|
180
|
+
You can extend visualization with:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
def custom_draw(sim, screen):
|
|
184
|
+
...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
sim = Simulator(
|
|
191
|
+
config=config,
|
|
192
|
+
controller=controller,
|
|
193
|
+
draw_callback=custom_draw
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 🖱️ User Interaction
|
|
200
|
+
|
|
201
|
+
- Left click → add waypoint
|
|
202
|
+
- Right click → add obstacle
|
|
203
|
+
- Scroll → zoom
|
|
204
|
+
- Middle button + drag → pan
|
|
205
|
+
|
|
206
|
+
### GUI
|
|
207
|
+
|
|
208
|
+
- Center Camera → reset view
|
|
209
|
+
- Reset → restart simulation
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## ✅ Features
|
|
214
|
+
|
|
215
|
+
- ✅ Multiple robot models
|
|
216
|
+
- ✅ 2D LiDAR sensor
|
|
217
|
+
- ✅ External controllers
|
|
218
|
+
- ✅ Artificial Potential Fields (APF)
|
|
219
|
+
- ✅ Collision detection with geometry
|
|
220
|
+
- ✅ Custom rendering
|
|
221
|
+
- ✅ Real-time interaction
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 🚀 Roadmap
|
|
226
|
+
|
|
227
|
+
- Vortex APF
|
|
228
|
+
- Harmonic (Laplace) fields
|
|
229
|
+
- Local minima avoidance
|
|
230
|
+
- Advanced visualization
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 📄 License
|
|
235
|
+
|
|
236
|
+
Free for educational and research use.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Nav Sim
|
|
2
|
+
|
|
3
|
+
A lightweight 2D simulation framework for mobile robot navigation using **Pygame**, with support for sensors, external controllers, and interactive visualization.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Supported Robot Models
|
|
8
|
+
|
|
9
|
+
- `holonomic`
|
|
10
|
+
Command:
|
|
11
|
+
```python
|
|
12
|
+
{"vx": ..., "vy": ...}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `differential`
|
|
16
|
+
Command:
|
|
17
|
+
```python
|
|
18
|
+
{"v": ..., "omega": ...}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `ackermann`
|
|
22
|
+
Command:
|
|
23
|
+
```python
|
|
24
|
+
{"speed": ..., "steer": ...}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🧠 Architecture
|
|
30
|
+
|
|
31
|
+
The simulator follows a modular design:
|
|
32
|
+
|
|
33
|
+
- ✅ **Simulator (`nav_sim`)**
|
|
34
|
+
Handles physics, rendering, environment, and sensors
|
|
35
|
+
|
|
36
|
+
- ✅ **External Controllers**
|
|
37
|
+
Implement navigation strategies (APF, Vortex, etc.)
|
|
38
|
+
|
|
39
|
+
> Controllers are not part of the core library. They live in `examples/` or in the user’s project.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📡 Sensors
|
|
44
|
+
|
|
45
|
+
### LiDAR (2D)
|
|
46
|
+
|
|
47
|
+
The simulator provides a configurable 2D LiDAR:
|
|
48
|
+
|
|
49
|
+
- Angular scan
|
|
50
|
+
- Range-limited
|
|
51
|
+
- Returns:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
(angle, distance, hit_point)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Passed to controllers as:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
lidar_readings
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ⚙️ Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m venv .venv
|
|
69
|
+
|
|
70
|
+
# Linux / macOS
|
|
71
|
+
source .venv/bin/activate
|
|
72
|
+
|
|
73
|
+
# Windows
|
|
74
|
+
.venv\Scripts\activate
|
|
75
|
+
|
|
76
|
+
pip install -r requirements.txt
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ▶️ Run Examples
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python examples/run_holonomic.py
|
|
85
|
+
python examples/run_differential.py
|
|
86
|
+
python examples/run_ackermann.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 🧪 Basic Usage
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from pathlib import Path
|
|
95
|
+
from nav_sim import Simulator, load_config
|
|
96
|
+
from examples.controllers.potential_field import potential_field_controller
|
|
97
|
+
|
|
98
|
+
config = load_config(Path("config/default.yaml"))
|
|
99
|
+
config["robot"]["type"] = "differential"
|
|
100
|
+
|
|
101
|
+
sim = Simulator(
|
|
102
|
+
config=config,
|
|
103
|
+
controller=potential_field_controller
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
sim.run()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🧩 Controller Interface
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
def controller(robot, world, goal, config, dt, lidar_readings=None) -> dict:
|
|
115
|
+
...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Parameters
|
|
119
|
+
|
|
120
|
+
- `robot` → robot state (position, heading, limits)
|
|
121
|
+
- `world` → obstacle representation
|
|
122
|
+
- `goal` → current waypoint
|
|
123
|
+
- `config` → YAML configuration
|
|
124
|
+
- `dt` → simulation timestep
|
|
125
|
+
- `lidar_readings` → sensor data
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 🧠 Artificial Potential Fields (APF)
|
|
130
|
+
|
|
131
|
+
This project includes a classical APF implementation based on Khatib’s method:
|
|
132
|
+
|
|
133
|
+
### Total force
|
|
134
|
+
|
|
135
|
+
F(q) = F_att(q) + F_rep(q)
|
|
136
|
+
|
|
137
|
+
### Attractive force
|
|
138
|
+
|
|
139
|
+
F_att = k_att (q_goal - q)
|
|
140
|
+
|
|
141
|
+
### Repulsive force
|
|
142
|
+
|
|
143
|
+
F_rep,i = k_r (1/d_i - 1/d_s) (1/d_i^2) r_hat_i
|
|
144
|
+
|
|
145
|
+
Where:
|
|
146
|
+
|
|
147
|
+
- d_i → LiDAR distance corrected by robot radius
|
|
148
|
+
- d_s → influence distance
|
|
149
|
+
- r_hat_i → unit vector away from obstacle
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 🛑 Collision Handling
|
|
154
|
+
|
|
155
|
+
The simulator detects collisions using:
|
|
156
|
+
|
|
157
|
+
d <= r_robot + r_obstacle
|
|
158
|
+
|
|
159
|
+
### Behavior
|
|
160
|
+
|
|
161
|
+
- The robot stops immediately
|
|
162
|
+
- A visual indicator (💥) is displayed
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🎨 Custom Drawing
|
|
167
|
+
|
|
168
|
+
You can extend visualization with:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
def custom_draw(sim, screen):
|
|
172
|
+
...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
sim = Simulator(
|
|
179
|
+
config=config,
|
|
180
|
+
controller=controller,
|
|
181
|
+
draw_callback=custom_draw
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 🖱️ User Interaction
|
|
188
|
+
|
|
189
|
+
- Left click → add waypoint
|
|
190
|
+
- Right click → add obstacle
|
|
191
|
+
- Scroll → zoom
|
|
192
|
+
- Middle button + drag → pan
|
|
193
|
+
|
|
194
|
+
### GUI
|
|
195
|
+
|
|
196
|
+
- Center Camera → reset view
|
|
197
|
+
- Reset → restart simulation
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## ✅ Features
|
|
202
|
+
|
|
203
|
+
- ✅ Multiple robot models
|
|
204
|
+
- ✅ 2D LiDAR sensor
|
|
205
|
+
- ✅ External controllers
|
|
206
|
+
- ✅ Artificial Potential Fields (APF)
|
|
207
|
+
- ✅ Collision detection with geometry
|
|
208
|
+
- ✅ Custom rendering
|
|
209
|
+
- ✅ Real-time interaction
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 🚀 Roadmap
|
|
214
|
+
|
|
215
|
+
- Vortex APF
|
|
216
|
+
- Harmonic (Laplace) fields
|
|
217
|
+
- Local minima avoidance
|
|
218
|
+
- Advanced visualization
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 📄 License
|
|
223
|
+
|
|
224
|
+
Free for educational and research use.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Camera2D:
|
|
8
|
+
world_width: float
|
|
9
|
+
world_height: float
|
|
10
|
+
viewport_width: int
|
|
11
|
+
viewport_height: int
|
|
12
|
+
initial_zoom: float = 1.0
|
|
13
|
+
min_zoom: float = 0.25
|
|
14
|
+
max_zoom: float = 8.0
|
|
15
|
+
zoom_step: float = 1.1
|
|
16
|
+
margin_x: float = 1.0
|
|
17
|
+
margin_y: float = 1.0
|
|
18
|
+
|
|
19
|
+
def __post_init__(self):
|
|
20
|
+
self.zoom = self.initial_zoom
|
|
21
|
+
self.offset = np.array([-self.margin_x, -self.margin_y], dtype=float)
|
|
22
|
+
self.compute_scale()
|
|
23
|
+
|
|
24
|
+
def compute_scale(self):
|
|
25
|
+
scale_x = self.viewport_width / self.world_width
|
|
26
|
+
scale_y = self.viewport_height / self.world_height
|
|
27
|
+
self.base_scale = min(scale_x, scale_y)
|
|
28
|
+
self.scale = self.base_scale * self.zoom
|
|
29
|
+
|
|
30
|
+
def resize(self, viewport_width: int, viewport_height: int):
|
|
31
|
+
self.viewport_width = max(1, int(viewport_width))
|
|
32
|
+
self.viewport_height = max(1, int(viewport_height))
|
|
33
|
+
self.compute_scale()
|
|
34
|
+
|
|
35
|
+
def reset(self):
|
|
36
|
+
self.zoom = self.initial_zoom
|
|
37
|
+
self.offset = np.array([-self.margin_x, -self.margin_y], dtype=float)
|
|
38
|
+
self.compute_scale()
|
|
39
|
+
|
|
40
|
+
def center_on_origin_with_margin(self):
|
|
41
|
+
self.offset = np.array([-self.margin_x, -self.margin_y], dtype=float)
|
|
42
|
+
|
|
43
|
+
def zoom_at(self, mouse_pos, wheel_y: int):
|
|
44
|
+
old_world = self.screen_to_world(*mouse_pos)
|
|
45
|
+
factor = self.zoom_step if wheel_y > 0 else 1.0 / self.zoom_step
|
|
46
|
+
self.zoom = float(np.clip(self.zoom * factor, self.min_zoom, self.max_zoom))
|
|
47
|
+
self.compute_scale()
|
|
48
|
+
new_world = self.screen_to_world(*mouse_pos)
|
|
49
|
+
self.offset += old_world - new_world
|
|
50
|
+
|
|
51
|
+
def pan_pixels(self, dx: float, dy: float):
|
|
52
|
+
self.offset[0] -= dx / self.scale
|
|
53
|
+
self.offset[1] += dy / self.scale
|
|
54
|
+
|
|
55
|
+
def _display_offsets(self):
|
|
56
|
+
ox = (self.viewport_width - self.world_width * self.scale) / 2
|
|
57
|
+
oy = (self.viewport_height - self.world_height * self.scale) / 2
|
|
58
|
+
return ox, oy
|
|
59
|
+
|
|
60
|
+
def world_to_screen(self, px: float, py: float):
|
|
61
|
+
ox, oy = self._display_offsets()
|
|
62
|
+
px_cam = px - self.offset[0]
|
|
63
|
+
py_cam = py - self.offset[1]
|
|
64
|
+
sx = int(px_cam * self.scale + ox)
|
|
65
|
+
sy = int(self.viewport_height - (py_cam * self.scale + oy))
|
|
66
|
+
return sx, sy
|
|
67
|
+
|
|
68
|
+
def screen_to_world(self, sx: float, sy: float):
|
|
69
|
+
ox, oy = self._display_offsets()
|
|
70
|
+
px = (sx - ox) / self.scale + self.offset[0]
|
|
71
|
+
py = ((self.viewport_height - sy) - oy) / self.scale + self.offset[1]
|
|
72
|
+
return np.array([px, py], dtype=float)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def wrap_to_pi(angle: float) -> float:
|
|
6
|
+
return float((angle + np.pi) % (2 * np.pi) - np.pi)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def clamp(value: float, min_value: float, max_value: float) -> float:
|
|
10
|
+
return max(min_value, min(value, max_value))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def distance_between(a, b) -> float:
|
|
14
|
+
return float(np.linalg.norm(np.asarray(a, dtype=float) - np.asarray(b, dtype=float)))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import math
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pygame
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Lidar:
|
|
8
|
+
def __init__(self, config: dict):
|
|
9
|
+
c = config["lidar"]
|
|
10
|
+
self.enabled = bool(c.get("enabled", True))
|
|
11
|
+
self.num_rays = int(c.get("num_rays", 41))
|
|
12
|
+
self.range = float(c.get("range", 4.0))
|
|
13
|
+
self.fov = math.radians(float(c.get("fov_deg", 180)))
|
|
14
|
+
self.colors = config["colors"]
|
|
15
|
+
|
|
16
|
+
def ray_circle_intersection(self, ray_origin, ray_dir, obs):
|
|
17
|
+
circle_radius = obs[2]
|
|
18
|
+
circle_center = np.array(obs[:2])
|
|
19
|
+
oc = ray_origin - circle_center
|
|
20
|
+
a = np.dot(ray_dir, ray_dir)
|
|
21
|
+
b = 2.0 * np.dot(oc, ray_dir)
|
|
22
|
+
c = np.dot(oc, oc) - circle_radius ** 2
|
|
23
|
+
disc = b*b - 4*a*c
|
|
24
|
+
if disc < 0:
|
|
25
|
+
return None
|
|
26
|
+
sq = math.sqrt(disc)
|
|
27
|
+
valid = [t for t in [(-b - sq)/(2*a), (-b + sq)/(2*a)] if 0 <= t <= self.range]
|
|
28
|
+
return min(valid) if valid else None
|
|
29
|
+
|
|
30
|
+
def cast_rays(self, robot, world):
|
|
31
|
+
readings = []
|
|
32
|
+
angles = np.linspace(robot.theta - self.fov/2, robot.theta + self.fov/2, self.num_rays)
|
|
33
|
+
for angle in angles:
|
|
34
|
+
ray_dir = np.array([math.cos(angle), math.sin(angle)])
|
|
35
|
+
min_d = self.range
|
|
36
|
+
hit = robot.position + ray_dir * self.range
|
|
37
|
+
for obs in world.obstacles:
|
|
38
|
+
d = self.ray_circle_intersection(robot.position, ray_dir, obs)
|
|
39
|
+
if d is not None and d < min_d:
|
|
40
|
+
min_d = d
|
|
41
|
+
hit = robot.position + ray_dir * d
|
|
42
|
+
readings.append((angle, min_d, hit))
|
|
43
|
+
return readings
|
|
44
|
+
|
|
45
|
+
def draw(self, screen, camera, robot, world):
|
|
46
|
+
if not self.enabled:
|
|
47
|
+
return
|
|
48
|
+
origin = camera.world_to_screen(robot.x, robot.y)
|
|
49
|
+
color = tuple(self.colors["lidar"])
|
|
50
|
+
far = (80, 120, 120)
|
|
51
|
+
for _, dist, hit in self.cast_rays(robot, world):
|
|
52
|
+
pygame.draw.line(screen, color if dist < self.range else far, origin, camera.world_to_screen(*hit), 1)
|
|
53
|
+
if dist < self.range:
|
|
54
|
+
pygame.draw.circle(screen, color, camera.world_to_screen(*hit), 3)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .holonomic import HolonomicRobot
|
|
2
|
+
from .differential import DifferentialRobot
|
|
3
|
+
from .ackermann import AckermannRobot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_robot(config: dict):
|
|
7
|
+
robot_type = config["robot"].get("type", "ackermann").lower()
|
|
8
|
+
if robot_type in ["holonomic", "omni", "omnidirectional"]:
|
|
9
|
+
return HolonomicRobot(config)
|
|
10
|
+
if robot_type in ["differential", "diff", "unicycle", "nonholonomic"]:
|
|
11
|
+
return DifferentialRobot(config)
|
|
12
|
+
if robot_type in ["ackermann", "car", "racecar"]:
|
|
13
|
+
return AckermannRobot(config)
|
|
14
|
+
raise ValueError(f"Tipo de robô desconhecido: {robot_type}")
|
|
15
|
+
|
|
16
|
+
__all__ = ["create_robot", "HolonomicRobot", "DifferentialRobot", "AckermannRobot"]
|