Spaces:
Running
Running
Commit
·
3aea7c6
0
Parent(s):
squash: initial commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +22 -0
- .gitattributes +1 -0
- .gitignore +28 -0
- .npmrc +1 -0
- .prettierignore +6 -0
- .prettierrc +15 -0
- DOCKER_README.md +287 -0
- Dockerfile +71 -0
- README.md +265 -0
- ROBOT_ARCHITECTURE.md +416 -0
- ROBOT_INSTANCING_README.md +73 -0
- bun.lock +652 -0
- docker-compose.yml +14 -0
- eslint.config.js +36 -0
- package.json +47 -0
- src-python/.gitignore +10 -0
- src-python/.python-version +1 -0
- src-python/README.md +656 -0
- src-python/pyproject.toml +43 -0
- src-python/src/__init__.py +1 -0
- src-python/src/connection_manager.py +147 -0
- src-python/src/main.py +554 -0
- src-python/src/models.py +191 -0
- src-python/src/robot_manager.py +168 -0
- src-python/start_server.py +38 -0
- src-python/uv.lock +239 -0
- src/app.css +1 -0
- src/app.d.ts +13 -0
- src/app.html +12 -0
- src/lib/components/panel/ControlPanel.svelte +60 -0
- src/lib/components/panel/Panels.svelte +78 -0
- src/lib/components/panel/RobotControlPanel.svelte +649 -0
- src/lib/components/panel/SettingsPanel.svelte +248 -0
- src/lib/components/scene/Floor.svelte +56 -0
- src/lib/components/scene/Robot.svelte +44 -0
- src/lib/components/scene/Selectable.svelte +68 -0
- src/lib/components/scene/robot/URDF/createRobot.svelte.ts +27 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfBox.ts +3 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfCylinder.ts +4 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfJoint.ts +42 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfLink.ts +23 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfMesh.ts +5 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfRobot.ts +10 -0
- src/lib/components/scene/robot/URDF/interfaces/IUrdfVisual.ts +56 -0
- src/lib/components/scene/robot/URDF/interfaces/index.ts +7 -0
- src/lib/components/scene/robot/URDF/mesh/DAE.svelte +70 -0
- src/lib/components/scene/robot/URDF/mesh/OBJ.svelte +52 -0
- src/lib/components/scene/robot/URDF/mesh/STL.svelte +51 -0
- src/lib/components/scene/robot/URDF/primitives/UrdfJoint.svelte +148 -0
- src/lib/components/scene/robot/URDF/primitives/UrdfLink.svelte +112 -0
.dockerignore
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
node_modules
|
2 |
+
.git
|
3 |
+
.gitignore
|
4 |
+
README.md
|
5 |
+
Dockerfile
|
6 |
+
docker-compose.yml
|
7 |
+
.dockerignore
|
8 |
+
.env
|
9 |
+
.env.local
|
10 |
+
.svelte-kit
|
11 |
+
dist
|
12 |
+
build
|
13 |
+
.vscode
|
14 |
+
*.log
|
15 |
+
.DS_Store
|
16 |
+
src-python/.venv
|
17 |
+
src-python/.git
|
18 |
+
src-python/target
|
19 |
+
src-python/dist
|
20 |
+
src-python/build
|
21 |
+
*.pyc
|
22 |
+
__pycache__
|
.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
*.stl filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
node_modules
|
2 |
+
|
3 |
+
# Output
|
4 |
+
.output
|
5 |
+
.vercel
|
6 |
+
.netlify
|
7 |
+
.wrangler
|
8 |
+
/.svelte-kit
|
9 |
+
/build
|
10 |
+
|
11 |
+
# OS
|
12 |
+
.DS_Store
|
13 |
+
Thumbs.db
|
14 |
+
|
15 |
+
# Env
|
16 |
+
.env
|
17 |
+
.env.*
|
18 |
+
!.env.example
|
19 |
+
!.env.test
|
20 |
+
|
21 |
+
# Vite
|
22 |
+
vite.config.js.timestamp-*
|
23 |
+
vite.config.ts.timestamp-*
|
24 |
+
|
25 |
+
# Box-packager / PyApp build artifacts
|
26 |
+
src-python/target/
|
27 |
+
src-python/dist/
|
28 |
+
src-python/build/
|
.npmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
engine-strict=true
|
.prettierignore
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Package Managers
|
2 |
+
package-lock.json
|
3 |
+
pnpm-lock.yaml
|
4 |
+
yarn.lock
|
5 |
+
bun.lock
|
6 |
+
bun.lockb
|
.prettierrc
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"useTabs": true,
|
3 |
+
"singleQuote": true,
|
4 |
+
"trailingComma": "none",
|
5 |
+
"printWidth": 100,
|
6 |
+
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
7 |
+
"overrides": [
|
8 |
+
{
|
9 |
+
"files": "*.svelte",
|
10 |
+
"options": {
|
11 |
+
"parser": "svelte"
|
12 |
+
}
|
13 |
+
}
|
14 |
+
]
|
15 |
+
}
|
DOCKER_README.md
ADDED
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🐳 LeRobot Arena Docker Setup
|
2 |
+
|
3 |
+
This Docker setup provides a containerized environment that runs both the Python backend and Svelte frontend for LeRobot Arena using modern tools like Bun, uv, and box-packager.
|
4 |
+
|
5 |
+
## 🚀 Quick Start
|
6 |
+
|
7 |
+
### Using Docker Compose (Recommended)
|
8 |
+
|
9 |
+
```bash
|
10 |
+
# Build and start both services
|
11 |
+
docker-compose up --build
|
12 |
+
|
13 |
+
# Or run in detached mode
|
14 |
+
docker-compose up -d --build
|
15 |
+
```
|
16 |
+
|
17 |
+
### Using Docker directly
|
18 |
+
|
19 |
+
```bash
|
20 |
+
# Build the image
|
21 |
+
docker build -t lerobot-arena .
|
22 |
+
|
23 |
+
# Run the container
|
24 |
+
docker run -p 8080:8080 -p 7860:7860 lerobot-arena
|
25 |
+
```
|
26 |
+
|
27 |
+
## 🌐 Access the Application
|
28 |
+
|
29 |
+
After starting the container, you can access:
|
30 |
+
|
31 |
+
- **Frontend (Svelte)**: http://localhost:7860
|
32 |
+
- **Backend (Python/FastAPI)**: http://localhost:8080
|
33 |
+
- **Backend API Docs**: http://localhost:8080/docs
|
34 |
+
|
35 |
+
## 📋 What's Included
|
36 |
+
|
37 |
+
The Docker container includes:
|
38 |
+
|
39 |
+
1. **Frontend**:
|
40 |
+
- Svelte application built as static files using **Bun**
|
41 |
+
- Served on port 7860 using Python's built-in HTTP server
|
42 |
+
- Production-ready build with all optimizations
|
43 |
+
|
44 |
+
2. **Backend**:
|
45 |
+
- FastAPI Python server with dependencies managed by **uv**
|
46 |
+
- **Standalone executable** created with **box-packager** for faster startup
|
47 |
+
- WebSocket support for real-time robot communication
|
48 |
+
- Runs on port 8080
|
49 |
+
- Auto-configured for container environment
|
50 |
+
|
51 |
+
## 🛠️ Development vs Production
|
52 |
+
|
53 |
+
### Production (Default)
|
54 |
+
The Dockerfile builds the Svelte app as static files using Bun and packages the Python backend as a standalone executable using box-packager.
|
55 |
+
|
56 |
+
### Development
|
57 |
+
For development with hot-reload, you can use local tools:
|
58 |
+
|
59 |
+
```bash
|
60 |
+
# Terminal 1: Frontend development
|
61 |
+
bun run dev
|
62 |
+
|
63 |
+
# Terminal 2: Backend development
|
64 |
+
cd src-python && python start_server.py
|
65 |
+
```
|
66 |
+
|
67 |
+
## 📁 Container Structure
|
68 |
+
|
69 |
+
```
|
70 |
+
/home/user/app/
|
71 |
+
├── src-python/ # Python backend source
|
72 |
+
│ └── target/release/ # Box-packager standalone executable
|
73 |
+
├── static-frontend/ # Built Svelte app (static files)
|
74 |
+
└── start_services.sh # Startup script for both services
|
75 |
+
```
|
76 |
+
|
77 |
+
## 🔧 Build Process
|
78 |
+
|
79 |
+
The Docker build process includes these key steps:
|
80 |
+
|
81 |
+
1. **Frontend Build Stage (Bun)**:
|
82 |
+
- Uses `oven/bun:1-alpine` for fast package management
|
83 |
+
- Builds Svelte app with `bun run build`
|
84 |
+
- Produces optimized static files
|
85 |
+
|
86 |
+
2. **Backend Build Stage (Python + Rust)**:
|
87 |
+
- Installs Rust/Cargo for box-packager
|
88 |
+
- Uses `uv` for fast Python dependency management
|
89 |
+
- Packages backend with `box package` into standalone executable
|
90 |
+
- Configures proper user permissions for HF Spaces
|
91 |
+
|
92 |
+
3. **Runtime**:
|
93 |
+
- Runs standalone executable (faster startup)
|
94 |
+
- Serves frontend static files
|
95 |
+
- Both services managed by startup script
|
96 |
+
|
97 |
+
## 🚀 Performance Benefits
|
98 |
+
|
99 |
+
### Box-packager Advantages:
|
100 |
+
- **Faster startup**: No Python interpreter overhead
|
101 |
+
- **Self-contained**: All dependencies bundled
|
102 |
+
- **Smaller runtime**: No need for full Python environment
|
103 |
+
- **Cross-platform**: Single executable works anywhere
|
104 |
+
|
105 |
+
### Build Optimization:
|
106 |
+
- **Bun**: Faster JavaScript package manager and bundler
|
107 |
+
- **uv**: Ultra-fast Python package manager
|
108 |
+
- **Multi-stage build**: Minimal final image size
|
109 |
+
- **Alpine base**: Lightweight frontend build stage
|
110 |
+
|
111 |
+
## 🔧 Customization
|
112 |
+
|
113 |
+
### Environment Variables
|
114 |
+
|
115 |
+
You can customize the container behavior using environment variables:
|
116 |
+
|
117 |
+
```bash
|
118 |
+
docker run -p 8080:8080 -p 7860:7860 \
|
119 |
+
-e PYTHONUNBUFFERED=1 \
|
120 |
+
-e NODE_ENV=production \
|
121 |
+
lerobot-arena
|
122 |
+
```
|
123 |
+
|
124 |
+
### Port Configuration
|
125 |
+
|
126 |
+
To use different ports:
|
127 |
+
|
128 |
+
```bash
|
129 |
+
# Map to different host ports
|
130 |
+
docker run -p 9000:8080 -p 3000:7860 lerobot-arena
|
131 |
+
```
|
132 |
+
|
133 |
+
Then access:
|
134 |
+
- Frontend: http://localhost:3000
|
135 |
+
- Backend: http://localhost:9000
|
136 |
+
|
137 |
+
### Volume Mounts for Persistence
|
138 |
+
|
139 |
+
```bash
|
140 |
+
# Mount data directory for persistence
|
141 |
+
docker run -p 8080:8080 -p 7860:7860 \
|
142 |
+
-v $(pwd)/data:/home/user/app/data \
|
143 |
+
lerobot-arena
|
144 |
+
```
|
145 |
+
|
146 |
+
## 🐛 Troubleshooting
|
147 |
+
|
148 |
+
### Container won't start
|
149 |
+
```bash
|
150 |
+
# Check logs
|
151 |
+
docker-compose logs lerobot-arena
|
152 |
+
|
153 |
+
# Or for direct docker run
|
154 |
+
docker logs <container-id>
|
155 |
+
```
|
156 |
+
|
157 |
+
### Port conflicts
|
158 |
+
```bash
|
159 |
+
# Check what's using the ports
|
160 |
+
lsof -i :8080
|
161 |
+
lsof -i :7860
|
162 |
+
|
163 |
+
# Kill processes or use different ports
|
164 |
+
docker run -p 8081:8080 -p 7861:7860 lerobot-arena
|
165 |
+
```
|
166 |
+
|
167 |
+
### Frontend not loading
|
168 |
+
```bash
|
169 |
+
# Verify the frontend was built correctly
|
170 |
+
docker exec -it <container-id> ls -la /home/user/app/static-frontend
|
171 |
+
|
172 |
+
# Check if frontend server is running
|
173 |
+
docker exec -it <container-id> ps aux | grep python
|
174 |
+
```
|
175 |
+
|
176 |
+
### Backend API errors
|
177 |
+
```bash
|
178 |
+
# Check if standalone executable is running
|
179 |
+
docker exec -it <container-id> ps aux | grep lerobot-arena-server
|
180 |
+
|
181 |
+
# Test backend directly
|
182 |
+
curl http://localhost:8080/
|
183 |
+
```
|
184 |
+
|
185 |
+
### Box-packager Build Issues
|
186 |
+
```bash
|
187 |
+
# Force rebuild without cache to fix cargo/box issues
|
188 |
+
docker-compose build --no-cache
|
189 |
+
docker-compose up
|
190 |
+
|
191 |
+
# Check if Rust/Cargo is properly installed
|
192 |
+
docker exec -it <container-id> cargo --version
|
193 |
+
```
|
194 |
+
|
195 |
+
## 🔄 Updates and Rebuilding
|
196 |
+
|
197 |
+
```bash
|
198 |
+
# Pull latest code and rebuild
|
199 |
+
git pull
|
200 |
+
docker-compose down
|
201 |
+
docker-compose up --build
|
202 |
+
|
203 |
+
# Force rebuild without cache
|
204 |
+
docker-compose build --no-cache
|
205 |
+
docker-compose up
|
206 |
+
```
|
207 |
+
|
208 |
+
## 🚀 Hugging Face Spaces Deployment
|
209 |
+
|
210 |
+
This Docker setup is optimized for Hugging Face Spaces:
|
211 |
+
|
212 |
+
### Key Features for HF Spaces:
|
213 |
+
- **Port 7860**: Uses the default HF Spaces port
|
214 |
+
- **User permissions**: Runs as user ID 1000 as required
|
215 |
+
- **Proper ownership**: All files owned by the user
|
216 |
+
- **Git support**: Includes git for dependency resolution
|
217 |
+
- **Standalone executable**: Faster cold starts on HF infrastructure
|
218 |
+
|
219 |
+
### Deployment Steps:
|
220 |
+
1. Push your code to a GitHub repository
|
221 |
+
2. Create a new Space on Hugging Face Spaces
|
222 |
+
3. Connect your GitHub repository
|
223 |
+
4. The YAML frontmatter in README.md will auto-configure the Space
|
224 |
+
5. HF will build and deploy your Docker container automatically
|
225 |
+
|
226 |
+
### Accessing on HF Spaces:
|
227 |
+
- Your Space URL will serve the frontend directly
|
228 |
+
- Backend API will be available at your-space-url/api/ (if using a reverse proxy)
|
229 |
+
|
230 |
+
## 🔧 Advanced Configuration
|
231 |
+
|
232 |
+
### Using with nginx (Production)
|
233 |
+
```nginx
|
234 |
+
server {
|
235 |
+
listen 80;
|
236 |
+
|
237 |
+
# Serve frontend
|
238 |
+
location / {
|
239 |
+
proxy_pass http://localhost:7860;
|
240 |
+
}
|
241 |
+
|
242 |
+
# Proxy API calls
|
243 |
+
location /api/ {
|
244 |
+
proxy_pass http://localhost:8080;
|
245 |
+
proxy_http_version 1.1;
|
246 |
+
proxy_set_header Upgrade $http_upgrade;
|
247 |
+
proxy_set_header Connection 'upgrade';
|
248 |
+
proxy_set_header Host $host;
|
249 |
+
proxy_cache_bypass $http_upgrade;
|
250 |
+
}
|
251 |
+
|
252 |
+
# WebSocket support
|
253 |
+
location /ws/ {
|
254 |
+
proxy_pass http://localhost:8080;
|
255 |
+
proxy_http_version 1.1;
|
256 |
+
proxy_set_header Upgrade $http_upgrade;
|
257 |
+
proxy_set_header Connection "upgrade";
|
258 |
+
}
|
259 |
+
}
|
260 |
+
```
|
261 |
+
|
262 |
+
## 📊 Container Stats
|
263 |
+
|
264 |
+
```bash
|
265 |
+
# Monitor resource usage
|
266 |
+
docker stats lerobot-arena
|
267 |
+
|
268 |
+
# View container info
|
269 |
+
docker inspect lerobot-arena
|
270 |
+
```
|
271 |
+
|
272 |
+
## 🧹 Cleanup
|
273 |
+
|
274 |
+
```bash
|
275 |
+
# Stop and remove containers
|
276 |
+
docker-compose down
|
277 |
+
|
278 |
+
# Remove images
|
279 |
+
docker rmi lerobot-arena
|
280 |
+
|
281 |
+
# Clean up unused images and containers
|
282 |
+
docker system prune -a
|
283 |
+
```
|
284 |
+
|
285 |
+
---
|
286 |
+
|
287 |
+
**Happy containerized robotics! 🤖🐳**
|
Dockerfile
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Multi-stage Dockerfile for LeRobot Arena
|
2 |
+
# Stage 1: Build Svelte frontend with Bun
|
3 |
+
FROM oven/bun:1-alpine AS frontend-builder
|
4 |
+
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Install git for dependencies that might need it
|
8 |
+
RUN apk add --no-cache git
|
9 |
+
|
10 |
+
# Copy package files
|
11 |
+
COPY package.json bun.lockb* ./
|
12 |
+
|
13 |
+
# Install dependencies with bun
|
14 |
+
RUN bun install
|
15 |
+
|
16 |
+
# Copy source code
|
17 |
+
COPY . .
|
18 |
+
|
19 |
+
# Build the Svelte app
|
20 |
+
RUN bun run build
|
21 |
+
|
22 |
+
# Stage 2: Python backend with official uv image
|
23 |
+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
24 |
+
|
25 |
+
# Set up a new user named "user" with user ID 1000 (required for HF Spaces)
|
26 |
+
RUN useradd -m -u 1000 user
|
27 |
+
|
28 |
+
# Switch to the "user" user
|
29 |
+
USER user
|
30 |
+
|
31 |
+
# Set home to the user's home directory
|
32 |
+
ENV HOME=/home/user \
|
33 |
+
PATH=/home/user/.local/bin:$PATH
|
34 |
+
|
35 |
+
# Set the working directory to the user's home directory
|
36 |
+
WORKDIR $HOME/app
|
37 |
+
|
38 |
+
# Copy Python project files for dependency resolution
|
39 |
+
COPY --chown=user src-python/pyproject.toml src-python/uv.lock ./src-python/
|
40 |
+
|
41 |
+
# Install dependencies first (better caching)
|
42 |
+
WORKDIR $HOME/app/src-python
|
43 |
+
RUN uv sync --no-install-project
|
44 |
+
|
45 |
+
# Copy the rest of the Python backend
|
46 |
+
COPY --chown=user src-python/ ./
|
47 |
+
|
48 |
+
# Install the project itself
|
49 |
+
RUN uv sync
|
50 |
+
|
51 |
+
# Copy built frontend from previous stage with proper ownership
|
52 |
+
COPY --chown=user --from=frontend-builder /app/build $HOME/app/static-frontend
|
53 |
+
|
54 |
+
# Create a startup script that runs both services
|
55 |
+
WORKDIR $HOME/app
|
56 |
+
RUN echo '#!/bin/bash\n\
|
57 |
+
echo "Starting LeRobot Arena services..."\n\
|
58 |
+
echo "🚀 Starting Python backend on port 8080..."\n\
|
59 |
+
cd $HOME/app/src-python && uv run python start_server.py &\n\
|
60 |
+
echo "🌐 Starting static file server on port 7860..."\n\
|
61 |
+
cd $HOME/app/static-frontend && python -m http.server 7860 --bind 0.0.0.0 &\n\
|
62 |
+
echo "✅ Both services started!"\n\
|
63 |
+
echo "Backend: http://localhost:8080"\n\
|
64 |
+
echo "Frontend: http://localhost:7860"\n\
|
65 |
+
wait' > start_services.sh && chmod +x start_services.sh
|
66 |
+
|
67 |
+
# Expose both ports (7860 is default for HF Spaces)
|
68 |
+
EXPOSE 7860 8080
|
69 |
+
|
70 |
+
# Start both services
|
71 |
+
CMD ["./start_services.sh"]
|
README.md
ADDED
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: LeRobot Arena
|
3 |
+
emoji: 🤖
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
+
sdk: docker
|
7 |
+
app_port: 7860
|
8 |
+
suggested_hardware: cpu-upgrade
|
9 |
+
suggested_storage: small
|
10 |
+
short_description: A web-based robotics control and simulation platform that bridges digital twins and physical robots
|
11 |
+
tags:
|
12 |
+
- robotics
|
13 |
+
- control
|
14 |
+
- simulation
|
15 |
+
- websocket
|
16 |
+
- fastapi
|
17 |
+
- svelte
|
18 |
+
- urdf
|
19 |
+
- 3d-visualization
|
20 |
+
pinned: false
|
21 |
+
fullWidth: true
|
22 |
+
---
|
23 |
+
|
24 |
+
# 🤖 LeRobot Arena
|
25 |
+
|
26 |
+
A web-based robotics control and simulation platform that bridges digital twins and physical robots. Built with Svelte for the frontend and FastAPI for the backend.
|
27 |
+
|
28 |
+
## 🚀 Quick Start with Docker
|
29 |
+
|
30 |
+
The easiest way to run LeRobot Arena is using Docker, which sets up both the frontend and backend automatically.
|
31 |
+
|
32 |
+
### Prerequisites
|
33 |
+
|
34 |
+
- [Docker](https://www.docker.com/get-started) installed on your system
|
35 |
+
- [Docker Compose](https://docs.docker.com/compose/install/) (usually included with Docker Desktop)
|
36 |
+
|
37 |
+
### Step-by-Step Instructions
|
38 |
+
|
39 |
+
1. **Clone the repository**
|
40 |
+
```bash
|
41 |
+
git clone <your-repo-url>
|
42 |
+
cd lerobot-arena
|
43 |
+
```
|
44 |
+
|
45 |
+
2. **Build and start the services**
|
46 |
+
```bash
|
47 |
+
docker-compose up --build
|
48 |
+
```
|
49 |
+
|
50 |
+
3. **Access the application**
|
51 |
+
- **Frontend**: http://localhost:7860
|
52 |
+
- **Backend API**: http://localhost:8080
|
53 |
+
- **API Documentation**: http://localhost:8080/docs
|
54 |
+
|
55 |
+
4. **Stop the services**
|
56 |
+
```bash
|
57 |
+
# Press Ctrl+C to stop, or in another terminal:
|
58 |
+
docker-compose down
|
59 |
+
```
|
60 |
+
|
61 |
+
### Alternative Docker Commands
|
62 |
+
|
63 |
+
If you prefer using Docker directly:
|
64 |
+
|
65 |
+
```bash
|
66 |
+
# Build the image
|
67 |
+
docker build -t lerobot-arena .
|
68 |
+
|
69 |
+
# Run the container
|
70 |
+
docker run -p 8080:8080 -p 7860:7860 lerobot-arena
|
71 |
+
```
|
72 |
+
|
73 |
+
## 🛠️ Development Setup
|
74 |
+
|
75 |
+
For local development with hot-reload capabilities:
|
76 |
+
|
77 |
+
### Frontend Development
|
78 |
+
|
79 |
+
```bash
|
80 |
+
# Install dependencies
|
81 |
+
bun install
|
82 |
+
|
83 |
+
# Start the development server
|
84 |
+
bun run dev
|
85 |
+
|
86 |
+
# Or open in browser automatically
|
87 |
+
bun run dev -- --open
|
88 |
+
```
|
89 |
+
|
90 |
+
### Backend Development
|
91 |
+
|
92 |
+
```bash
|
93 |
+
# Navigate to Python backend
|
94 |
+
cd src-python
|
95 |
+
|
96 |
+
# Install Python dependencies (using uv)
|
97 |
+
uv sync
|
98 |
+
|
99 |
+
# Or using pip
|
100 |
+
pip install -e .
|
101 |
+
|
102 |
+
# Start the backend server
|
103 |
+
python start_server.py
|
104 |
+
```
|
105 |
+
|
106 |
+
### Building Standalone Executable
|
107 |
+
|
108 |
+
The backend can be packaged as a standalone executable using box-packager:
|
109 |
+
|
110 |
+
```bash
|
111 |
+
# Navigate to Python backend
|
112 |
+
cd src-python
|
113 |
+
|
114 |
+
# Install box-packager (if not already installed)
|
115 |
+
uv pip install box-packager
|
116 |
+
|
117 |
+
# Package the application
|
118 |
+
box package
|
119 |
+
|
120 |
+
# The executable will be in target/release/lerobot-arena-server
|
121 |
+
./target/release/lerobot-arena-server
|
122 |
+
```
|
123 |
+
|
124 |
+
Note: Requires [Rust/Cargo](https://rustup.rs/) to be installed for box-packager to work.
|
125 |
+
|
126 |
+
## 📋 Project Structure
|
127 |
+
|
128 |
+
```
|
129 |
+
lerobot-arena/
|
130 |
+
├── src/ # Svelte frontend source
|
131 |
+
│ ├── lib/ # Reusable components and utilities
|
132 |
+
│ ├── routes/ # SvelteKit routes
|
133 |
+
│ └── app.html # App template
|
134 |
+
├── src-python/ # Python backend
|
135 |
+
│ ├── src/ # Python source code
|
136 |
+
│ ├── start_server.py # Server entry point
|
137 |
+
│ ├── target/ # Box-packager build output (excluded from git)
|
138 |
+
│ └── pyproject.toml # Python dependencies
|
139 |
+
├── static/ # Static assets
|
140 |
+
├── Dockerfile # Docker configuration
|
141 |
+
├── docker-compose.yml # Docker Compose setup
|
142 |
+
└── package.json # Node.js dependencies
|
143 |
+
```
|
144 |
+
|
145 |
+
## 🐳 Docker Information
|
146 |
+
|
147 |
+
The Docker setup includes:
|
148 |
+
|
149 |
+
- **Multi-stage build**: Optimized for production using Bun and uv
|
150 |
+
- **Automatic startup**: Both services start together
|
151 |
+
- **Port mapping**: Backend on 8080, Frontend on 7860 (HF Spaces compatible)
|
152 |
+
- **Static file serving**: Compiled Svelte app served efficiently
|
153 |
+
- **User permissions**: Properly configured for Hugging Face Spaces
|
154 |
+
- **Standalone executable**: Backend packaged with box-packager for faster startup
|
155 |
+
|
156 |
+
For detailed Docker documentation, see [DOCKER_README.md](./DOCKER_README.md).
|
157 |
+
|
158 |
+
## 🔧 Building for Production
|
159 |
+
|
160 |
+
### Frontend Only
|
161 |
+
```bash
|
162 |
+
bun run build
|
163 |
+
```
|
164 |
+
|
165 |
+
### Backend Standalone Executable
|
166 |
+
```bash
|
167 |
+
cd src-python
|
168 |
+
box package
|
169 |
+
```
|
170 |
+
|
171 |
+
### Complete Docker Build
|
172 |
+
```bash
|
173 |
+
docker-compose up --build
|
174 |
+
```
|
175 |
+
|
176 |
+
## 🌐 What's Included
|
177 |
+
|
178 |
+
- **Real-time Robot Control**: WebSocket-based communication
|
179 |
+
- **3D Visualization**: Three.js integration for robot visualization
|
180 |
+
- **URDF Support**: Load and display robot models
|
181 |
+
- **Multi-robot Management**: Control multiple robots simultaneously
|
182 |
+
- **WebSocket API**: Real-time bidirectional communication
|
183 |
+
- **Standalone Distribution**: Self-contained executable with box-packager
|
184 |
+
|
185 |
+
## 🚨 Troubleshooting
|
186 |
+
|
187 |
+
### Port Conflicts
|
188 |
+
If ports 8080 or 7860 are already in use:
|
189 |
+
|
190 |
+
```bash
|
191 |
+
# Check what's using the ports
|
192 |
+
lsof -i :8080
|
193 |
+
lsof -i :7860
|
194 |
+
|
195 |
+
# Use different ports
|
196 |
+
docker run -p 8081:8080 -p 7861:7860 lerobot-arena
|
197 |
+
```
|
198 |
+
|
199 |
+
### Container Issues
|
200 |
+
```bash
|
201 |
+
# View logs
|
202 |
+
docker-compose logs lerobot-arena
|
203 |
+
|
204 |
+
# Rebuild without cache
|
205 |
+
docker-compose build --no-cache
|
206 |
+
docker-compose up
|
207 |
+
```
|
208 |
+
|
209 |
+
### Development Issues
|
210 |
+
```bash
|
211 |
+
# Clear node modules and reinstall
|
212 |
+
rm -rf node_modules
|
213 |
+
bun install
|
214 |
+
|
215 |
+
# Clear Svelte kit cache
|
216 |
+
rm -rf .svelte-kit
|
217 |
+
bun run dev
|
218 |
+
```
|
219 |
+
|
220 |
+
### Box-packager Issues
|
221 |
+
```bash
|
222 |
+
# Clean build artifacts
|
223 |
+
cd src-python
|
224 |
+
box clean
|
225 |
+
|
226 |
+
# Rebuild executable
|
227 |
+
box package
|
228 |
+
|
229 |
+
# Install cargo if missing
|
230 |
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
231 |
+
```
|
232 |
+
|
233 |
+
## 🚀 Hugging Face Spaces Deployment
|
234 |
+
|
235 |
+
This project is configured for deployment on Hugging Face Spaces:
|
236 |
+
|
237 |
+
1. **Fork** this repository to your GitHub account
|
238 |
+
2. **Create a new Space** on Hugging Face Spaces
|
239 |
+
3. **Connect** your GitHub repository
|
240 |
+
4. **Select Docker SDK** (should be auto-detected from the frontmatter)
|
241 |
+
5. **Deploy** - The Space will automatically build and run
|
242 |
+
|
243 |
+
The frontend will be available at your Space URL, and the backend API will be accessible at `/api/` endpoints.
|
244 |
+
|
245 |
+
## 📚 Additional Documentation
|
246 |
+
|
247 |
+
- [Docker Setup Guide](./DOCKER_README.md) - Detailed Docker instructions
|
248 |
+
- [Robot Architecture](./ROBOT_ARCHITECTURE.md) - System architecture overview
|
249 |
+
- [Robot Instancing Guide](./ROBOT_INSTANCING_README.md) - Multi-robot setup
|
250 |
+
|
251 |
+
## 🤝 Contributing
|
252 |
+
|
253 |
+
1. Fork the repository
|
254 |
+
2. Create a feature branch
|
255 |
+
3. Make your changes
|
256 |
+
4. Test with Docker: `docker-compose up --build`
|
257 |
+
5. Submit a pull request
|
258 |
+
|
259 |
+
## 📄 License
|
260 |
+
|
261 |
+
This project is licensed under the MIT License.
|
262 |
+
|
263 |
+
---
|
264 |
+
|
265 |
+
**Built with ❤️ for the robotics community** 🤖
|
ROBOT_ARCHITECTURE.md
ADDED
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Robot Control Architecture v2.0 - Master/Slave Pattern
|
2 |
+
|
3 |
+
This document describes the revolutionary new **Master-Slave Architecture** that enables sophisticated robot control with clear separation between command sources (Masters) and execution targets (Slaves).
|
4 |
+
|
5 |
+
## 🏗️ Architecture Overview
|
6 |
+
|
7 |
+
The new architecture follows a **Master-Slave Pattern** with complete separation of concerns:
|
8 |
+
|
9 |
+
```
|
10 |
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
11 |
+
│ UI Panel │ │ Robot Manager │ │ Masters │
|
12 |
+
│ │◄──►│ │◄──►│ │
|
13 |
+
│ • Manual Control│ │ • Master/Slave │ │ • Remote Server │
|
14 |
+
│ • Monitoring │ │ • Command Router │ │ • Mock Sequence │
|
15 |
+
│ (disabled if │ │ • State Sync │ │ • Script Player │
|
16 |
+
│ master active) │ └──────────────────┘ └─────────────────┘
|
17 |
+
└─────────────────┘ │
|
18 |
+
▼
|
19 |
+
┌──────────────────┐ ┌─────────────────┐
|
20 |
+
│ Robot Model │◄──►│ Slaves │
|
21 |
+
│ │ │ │
|
22 |
+
│ • URDF State │ │ • USB Robots │
|
23 |
+
│ • Joint States │ │ • Mock Hardware │
|
24 |
+
│ • Command Queue │ │ • Simulations │
|
25 |
+
└──────────────────┘ └─────────────────┘
|
26 |
+
```
|
27 |
+
|
28 |
+
## 🎯 Key Concepts
|
29 |
+
|
30 |
+
### Masters (Command Sources)
|
31 |
+
- **Purpose**: Generate commands and control sequences
|
32 |
+
- **Examples**: Remote servers, predefined sequences, scripts
|
33 |
+
- **Limitation**: Only 1 master per robot (exclusive control)
|
34 |
+
- **Effect**: When active, disables manual panel control
|
35 |
+
|
36 |
+
### Slaves (Execution Targets)
|
37 |
+
- **Purpose**: Execute commands on physical/virtual robots
|
38 |
+
- **Examples**: USB robots, mock robots, simulations
|
39 |
+
- **Limitation**: Multiple slaves per robot (parallel execution)
|
40 |
+
- **Effect**: All connected slaves execute the same commands
|
41 |
+
|
42 |
+
### Control Flow
|
43 |
+
1. **No Master**: Manual panel control → All slaves
|
44 |
+
2. **With Master**: Master commands → All slaves (panel disabled)
|
45 |
+
3. **Bidirectional**: Slaves provide real-time feedback
|
46 |
+
|
47 |
+
## 📁 File Structure
|
48 |
+
|
49 |
+
```
|
50 |
+
src/lib/
|
51 |
+
├── robot/
|
52 |
+
│ ├── Robot.svelte.ts # Master-slave robot management
|
53 |
+
│ ├── RobotManager.svelte.ts # Global orchestration
|
54 |
+
│ └── drivers/
|
55 |
+
│ ├── masters/
|
56 |
+
│ │ ├── MockSequenceMaster.ts # Demo sequences
|
57 |
+
│ │ ├── RemoteServerMaster.ts # HTTP/WebSocket commands
|
58 |
+
│ │ └── ScriptPlayerMaster.ts # Script execution
|
59 |
+
│ └── slaves/
|
60 |
+
│ ├── MockSlave.ts # Development/testing
|
61 |
+
│ ├── USBSlave.ts # Physical USB robots
|
62 |
+
│ └── SimulationSlave.ts # Physics simulation
|
63 |
+
├── types/
|
64 |
+
│ └── robotDriver.ts # Master/Slave interfaces
|
65 |
+
└── components/
|
66 |
+
├── scene/
|
67 |
+
│ └── Robot.svelte # 3D visualization
|
68 |
+
└── panel/
|
69 |
+
└── RobotControlPanel.svelte # Master/Slave connection UI
|
70 |
+
```
|
71 |
+
|
72 |
+
## 🔧 Core Components
|
73 |
+
|
74 |
+
### RobotManager
|
75 |
+
**Central orchestrator with master-slave management**
|
76 |
+
|
77 |
+
```typescript
|
78 |
+
import { robotManager } from '$lib/robot/RobotManager.svelte';
|
79 |
+
|
80 |
+
// Create robot
|
81 |
+
const robot = await robotManager.createRobot('my-robot', urdfConfig);
|
82 |
+
|
83 |
+
// Connect demo sequence master (disables manual control)
|
84 |
+
await robotManager.connectDemoSequences('my-robot', true);
|
85 |
+
|
86 |
+
// Connect mock slave (executes commands)
|
87 |
+
await robotManager.connectMockSlave('my-robot', 50);
|
88 |
+
|
89 |
+
// Connect USB slave (real hardware)
|
90 |
+
await robotManager.connectUSBSlave('my-robot');
|
91 |
+
|
92 |
+
// Disconnect master (restores manual control)
|
93 |
+
await robotManager.disconnectMaster('my-robot');
|
94 |
+
```
|
95 |
+
|
96 |
+
### Robot Class
|
97 |
+
**Individual robot with master-slave coordination**
|
98 |
+
|
99 |
+
```typescript
|
100 |
+
// Master management
|
101 |
+
await robot.setMaster(sequenceMaster);
|
102 |
+
await robot.removeMaster();
|
103 |
+
|
104 |
+
// Slave management
|
105 |
+
await robot.addSlave(usbSlave);
|
106 |
+
await robot.addSlave(mockSlave);
|
107 |
+
await robot.removeSlave(slaveId);
|
108 |
+
|
109 |
+
// Control state
|
110 |
+
robot.controlState.hasActiveMaster // true when master connected
|
111 |
+
robot.controlState.manualControlEnabled // false when master active
|
112 |
+
robot.controlState.lastCommandSource // "master" | "manual" | "none"
|
113 |
+
|
114 |
+
// Manual control (only when no master)
|
115 |
+
await robot.updateJointValue('joint_1', 45);
|
116 |
+
```
|
117 |
+
|
118 |
+
## 🔌 Master Drivers
|
119 |
+
|
120 |
+
### Mock Sequence Master
|
121 |
+
**Predefined movement sequences for testing**
|
122 |
+
|
123 |
+
```typescript
|
124 |
+
const config: MasterDriverConfig = {
|
125 |
+
type: "mock-sequence",
|
126 |
+
sequences: DEMO_SEQUENCES, // Wave, Scan, Pick-Place patterns
|
127 |
+
autoStart: true,
|
128 |
+
loopMode: true
|
129 |
+
};
|
130 |
+
|
131 |
+
await robotManager.connectMaster('robot-1', config);
|
132 |
+
```
|
133 |
+
|
134 |
+
**Demo Sequences:**
|
135 |
+
- **Wave Pattern**: Greeting gesture with wrist roll
|
136 |
+
- **Scanning Pattern**: Horizontal sweep with pitch variation
|
137 |
+
- **Pick and Place**: Complete manipulation sequence
|
138 |
+
|
139 |
+
### Remote Server Master *(Planned)*
|
140 |
+
**HTTP/WebSocket command reception**
|
141 |
+
|
142 |
+
```typescript
|
143 |
+
const config: MasterDriverConfig = {
|
144 |
+
type: "remote-server",
|
145 |
+
url: "ws://robot-server:8080/ws",
|
146 |
+
apiKey: "your-api-key",
|
147 |
+
pollInterval: 100
|
148 |
+
};
|
149 |
+
```
|
150 |
+
|
151 |
+
### Script Player Master *(Planned)*
|
152 |
+
**JavaScript/Python script execution**
|
153 |
+
|
154 |
+
```typescript
|
155 |
+
const config: MasterDriverConfig = {
|
156 |
+
type: "script-player",
|
157 |
+
scriptUrl: "/scripts/robot-dance.js",
|
158 |
+
variables: { speed: 1.5, amplitude: 45 }
|
159 |
+
};
|
160 |
+
```
|
161 |
+
|
162 |
+
## 🤖 Slave Drivers
|
163 |
+
|
164 |
+
### Mock Slave
|
165 |
+
**Perfect simulation for development**
|
166 |
+
|
167 |
+
```typescript
|
168 |
+
const config: SlaveDriverConfig = {
|
169 |
+
type: "mock-slave",
|
170 |
+
simulateLatency: 50, // ms response delay
|
171 |
+
simulateErrors: false, // random failures
|
172 |
+
responseDelay: 20 // command execution time
|
173 |
+
};
|
174 |
+
|
175 |
+
await robotManager.connectSlave('robot-1', config);
|
176 |
+
```
|
177 |
+
|
178 |
+
**Features:**
|
179 |
+
- ✅ Perfect command execution (real = virtual)
|
180 |
+
- ✅ Configurable latency and errors
|
181 |
+
- ✅ Real-time state feedback
|
182 |
+
- ✅ Instant connection
|
183 |
+
|
184 |
+
### USB Slave *(Planned)*
|
185 |
+
**Physical robot via feetech.js**
|
186 |
+
|
187 |
+
```typescript
|
188 |
+
const config: SlaveDriverConfig = {
|
189 |
+
type: "usb-slave",
|
190 |
+
port: "/dev/ttyUSB0", // auto-detect if undefined
|
191 |
+
baudRate: 115200
|
192 |
+
};
|
193 |
+
```
|
194 |
+
|
195 |
+
**Features:**
|
196 |
+
- ✅ Direct hardware control
|
197 |
+
- ✅ Position/speed commands
|
198 |
+
- ✅ Real servo feedback
|
199 |
+
- ✅ Calibration support
|
200 |
+
|
201 |
+
### Simulation Slave *(Planned)*
|
202 |
+
**Physics-based simulation**
|
203 |
+
|
204 |
+
```typescript
|
205 |
+
const config: SlaveDriverConfig = {
|
206 |
+
type: "simulation-slave",
|
207 |
+
physics: true,
|
208 |
+
collisionDetection: true
|
209 |
+
};
|
210 |
+
```
|
211 |
+
|
212 |
+
## 🔄 Command Flow Architecture
|
213 |
+
|
214 |
+
### Command Structure
|
215 |
+
```typescript
|
216 |
+
interface RobotCommand {
|
217 |
+
timestamp: number;
|
218 |
+
joints: {
|
219 |
+
name: string;
|
220 |
+
value: number; // degrees for revolute, speed for continuous
|
221 |
+
speed?: number; // optional movement speed
|
222 |
+
}[];
|
223 |
+
duration?: number; // optional execution time
|
224 |
+
metadata?: Record<string, unknown>;
|
225 |
+
}
|
226 |
+
```
|
227 |
+
|
228 |
+
### Command Sources
|
229 |
+
|
230 |
+
#### Master Commands
|
231 |
+
```typescript
|
232 |
+
// Continuous sequence from master
|
233 |
+
master.onCommand((commands) => {
|
234 |
+
// Route to all connected slaves
|
235 |
+
robot.slaves.forEach(slave => slave.executeCommands(commands));
|
236 |
+
});
|
237 |
+
```
|
238 |
+
|
239 |
+
#### Manual Commands
|
240 |
+
```typescript
|
241 |
+
// Only when no master active
|
242 |
+
if (robot.manualControlEnabled) {
|
243 |
+
await robot.updateJointValue('shoulder', 45);
|
244 |
+
}
|
245 |
+
```
|
246 |
+
|
247 |
+
### Command Execution
|
248 |
+
```typescript
|
249 |
+
// All slaves execute in parallel
|
250 |
+
const executePromises = robot.connectedSlaves.map(slave =>
|
251 |
+
slave.executeCommand(command)
|
252 |
+
);
|
253 |
+
await Promise.allSettled(executePromises);
|
254 |
+
```
|
255 |
+
|
256 |
+
## 🎮 Usage Examples
|
257 |
+
|
258 |
+
### Basic Master-Slave Setup
|
259 |
+
```typescript
|
260 |
+
// 1. Create robot
|
261 |
+
const robot = await robotManager.createRobot('arm-1', urdfConfig);
|
262 |
+
|
263 |
+
// 2. Add slaves for execution
|
264 |
+
await robotManager.connectMockSlave('arm-1'); // Development
|
265 |
+
await robotManager.connectUSBSlave('arm-1'); // Real hardware
|
266 |
+
|
267 |
+
// 3. Connect master for control
|
268 |
+
await robotManager.connectDemoSequences('arm-1'); // Automated sequences
|
269 |
+
|
270 |
+
// 4. Monitor status
|
271 |
+
const status = robotManager.getRobotStatus('arm-1');
|
272 |
+
console.log(`Master: ${status.masterName}, Slaves: ${status.connectedSlaves}`);
|
273 |
+
```
|
274 |
+
|
275 |
+
### Multiple Robots with Different Masters
|
276 |
+
```typescript
|
277 |
+
// Robot 1: Demo sequences
|
278 |
+
await robotManager.createRobot('demo-1', armConfig);
|
279 |
+
await robotManager.connectDemoSequences('demo-1', true);
|
280 |
+
await robotManager.connectMockSlave('demo-1');
|
281 |
+
|
282 |
+
// Robot 2: Remote control
|
283 |
+
await robotManager.createRobot('remote-1', armConfig);
|
284 |
+
await robotManager.connectMaster('remote-1', remoteServerConfig);
|
285 |
+
await robotManager.connectUSBSlave('remote-1');
|
286 |
+
|
287 |
+
// Robot 3: Manual control only
|
288 |
+
await robotManager.createRobot('manual-1', armConfig);
|
289 |
+
await robotManager.connectMockSlave('manual-1');
|
290 |
+
// No master = manual control enabled
|
291 |
+
```
|
292 |
+
|
293 |
+
### Master Switching
|
294 |
+
```typescript
|
295 |
+
// Switch from manual to automated control
|
296 |
+
await robotManager.connectDemoSequences('robot-1');
|
297 |
+
// Panel inputs now disabled, sequences control robot
|
298 |
+
|
299 |
+
// Switch back to manual control
|
300 |
+
await robotManager.disconnectMaster('robot-1');
|
301 |
+
// Panel inputs re-enabled, manual control restored
|
302 |
+
```
|
303 |
+
|
304 |
+
## 🚀 Benefits
|
305 |
+
|
306 |
+
### 1. **Clear Control Hierarchy**
|
307 |
+
- Masters provide commands exclusively
|
308 |
+
- Slaves execute commands in parallel
|
309 |
+
- No ambiguity about command source
|
310 |
+
|
311 |
+
### 2. **Flexible Command Sources**
|
312 |
+
- Remote servers for network control
|
313 |
+
- Predefined sequences for automation
|
314 |
+
- Manual control for testing/setup
|
315 |
+
- Easy to add new master types
|
316 |
+
|
317 |
+
### 3. **Multiple Execution Targets**
|
318 |
+
- Physical robots via USB
|
319 |
+
- Simulated robots for testing
|
320 |
+
- Mock robots for development
|
321 |
+
- All execute same commands simultaneously
|
322 |
+
|
323 |
+
### 4. **Automatic Panel Management**
|
324 |
+
- Panel disabled when master active
|
325 |
+
- Panel re-enabled when master disconnected
|
326 |
+
- Clear visual feedback about control state
|
327 |
+
|
328 |
+
### 5. **Safe Operation**
|
329 |
+
- Masters cannot conflict (only 1 allowed)
|
330 |
+
- Graceful disconnection with rest positioning
|
331 |
+
- Error isolation between slaves
|
332 |
+
|
333 |
+
### 6. **Development Workflow**
|
334 |
+
- Test with mock slaves during development
|
335 |
+
- Add real slaves for deployment
|
336 |
+
- Switch masters for different scenarios
|
337 |
+
|
338 |
+
## 🔮 Implementation Roadmap
|
339 |
+
|
340 |
+
### Phase 1: Core Architecture ✅
|
341 |
+
- [x] Master-Slave interfaces
|
342 |
+
- [x] Robot class with dual management
|
343 |
+
- [x] RobotManager orchestration
|
344 |
+
- [x] MockSequenceMaster with demo patterns
|
345 |
+
- [x] MockSlave for testing
|
346 |
+
|
347 |
+
### Phase 2: Real Hardware
|
348 |
+
- [ ] USBSlave implementation (feetech.js integration)
|
349 |
+
- [ ] Calibration system for hardware slaves
|
350 |
+
- [ ] Error handling and recovery
|
351 |
+
|
352 |
+
### Phase 3: Remote Control
|
353 |
+
- [ ] RemoteServerMaster (HTTP/WebSocket)
|
354 |
+
- [ ] Authentication and security
|
355 |
+
- [ ] Real-time command streaming
|
356 |
+
|
357 |
+
### Phase 4: Advanced Features
|
358 |
+
- [ ] ScriptPlayerMaster (JS/Python execution)
|
359 |
+
- [ ] SimulationSlave (physics integration)
|
360 |
+
- [ ] Command recording and playback
|
361 |
+
- [ ] Multi-robot coordination
|
362 |
+
|
363 |
+
## 🛡️ Safety Features
|
364 |
+
|
365 |
+
### Master Safety
|
366 |
+
- Only 1 master per robot prevents conflicts
|
367 |
+
- Master disconnect automatically restores manual control
|
368 |
+
- Command validation before execution
|
369 |
+
|
370 |
+
### Slave Safety
|
371 |
+
- Rest positioning before disconnect
|
372 |
+
- Smooth movement transitions
|
373 |
+
- Individual slave error isolation
|
374 |
+
- Calibration offset compensation
|
375 |
+
|
376 |
+
### System Safety
|
377 |
+
- Graceful degradation on slave failures
|
378 |
+
- Command queuing prevents overwhelming
|
379 |
+
- Real-time state monitoring
|
380 |
+
- Emergency stop capabilities *(planned)*
|
381 |
+
|
382 |
+
## 🔧 Migration from v1.0
|
383 |
+
|
384 |
+
The new architecture is **completely different** from the old driver pattern:
|
385 |
+
|
386 |
+
### Old Architecture (v1.0)
|
387 |
+
```typescript
|
388 |
+
// Single driver per robot
|
389 |
+
const driver = new USBRobotDriver(config);
|
390 |
+
await robot.setDriver(driver);
|
391 |
+
await robot.updateJointValue('joint', 45);
|
392 |
+
```
|
393 |
+
|
394 |
+
### New Architecture (v2.0)
|
395 |
+
```typescript
|
396 |
+
// Separate masters and slaves
|
397 |
+
await robotManager.connectMaster(robotId, masterConfig); // Command source
|
398 |
+
await robotManager.connectSlave(robotId, slaveConfig); // Execution target
|
399 |
+
|
400 |
+
// Manual control only when no master
|
401 |
+
if (robot.manualControlEnabled) {
|
402 |
+
await robot.updateJointValue('joint', 45);
|
403 |
+
}
|
404 |
+
```
|
405 |
+
|
406 |
+
### Key Changes
|
407 |
+
1. **Drivers → Masters + Slaves**: Split command/execution concerns
|
408 |
+
2. **Single Connection → Multiple**: 1 master + N slaves per robot
|
409 |
+
3. **Always Manual → Conditional**: Panel disabled when master active
|
410 |
+
4. **Direct Control → Command Routing**: Masters route to all slaves
|
411 |
+
|
412 |
+
The new architecture provides **dramatically more flexibility** while maintaining complete backward compatibility for URDF visualization and joint control.
|
413 |
+
|
414 |
+
---
|
415 |
+
|
416 |
+
*This architecture enables sophisticated robot control scenarios from simple manual operation to complex multi-robot coordination, all with a clean, extensible design.*
|
ROBOT_INSTANCING_README.md
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Robot Instancing Optimization
|
2 |
+
|
3 |
+
This implementation optimizes the rendering of multiple robots using Threlte's instancing capabilities for improved performance.
|
4 |
+
|
5 |
+
## Key Features
|
6 |
+
|
7 |
+
### 1. **Smart Instancing by Robot Type**
|
8 |
+
- Robots are grouped by their URDF type (URL) for optimal instancing
|
9 |
+
- Single robots render with full detail
|
10 |
+
- Multiple robots of the same type use instanced rendering
|
11 |
+
|
12 |
+
### 2. **Geometry-Specific Instancing**
|
13 |
+
- **Box Geometries**: Instanced with `T.boxGeometry`
|
14 |
+
- **Cylinder Geometries**: Instanced with `T.cylinderGeometry`
|
15 |
+
- **Mesh Geometries**: Instanced with simplified `T.sphereGeometry` for performance
|
16 |
+
|
17 |
+
### 3. **Hybrid Rendering Strategy**
|
18 |
+
- First robot of each type: Full detailed rendering with all URDF components
|
19 |
+
- Additional robots: Simplified instanced representation
|
20 |
+
- Maintains visual quality while optimizing performance
|
21 |
+
|
22 |
+
### 4. **Performance Benefits**
|
23 |
+
- Reduces draw calls when rendering multiple robots
|
24 |
+
- Optimizes GPU memory usage through instancing
|
25 |
+
- Scales better with increasing robot count
|
26 |
+
- Maintains interactivity for detailed robots
|
27 |
+
|
28 |
+
## Implementation Details
|
29 |
+
|
30 |
+
### State Management
|
31 |
+
```typescript
|
32 |
+
// Robots grouped by URDF type for optimal batching
|
33 |
+
let robotsByType: Record<string, Array<{
|
34 |
+
id: string;
|
35 |
+
position: [number, number, number];
|
36 |
+
robotState: RobotState
|
37 |
+
}>> = $state({});
|
38 |
+
```
|
39 |
+
|
40 |
+
### Instancing Logic
|
41 |
+
1. **Single Robot**: Full `UrdfLink` rendering with all details
|
42 |
+
2. **Multiple Robots**:
|
43 |
+
- Geometry analysis and grouping
|
44 |
+
- Instanced rendering for primitive shapes
|
45 |
+
- Simplified representations for complex meshes
|
46 |
+
|
47 |
+
### Automatic Demonstration
|
48 |
+
- Spawns additional robots after 2 seconds to showcase instancing
|
49 |
+
- Shows performance difference between single and multiple robot rendering
|
50 |
+
|
51 |
+
## Usage
|
52 |
+
|
53 |
+
Simply use the `Robot.svelte` component with a `urdfConfig`. The component automatically:
|
54 |
+
- Detects when multiple robots of the same type exist
|
55 |
+
- Switches to instanced rendering for optimal performance
|
56 |
+
- Maintains full detail for single robots
|
57 |
+
|
58 |
+
```svelte
|
59 |
+
<Robot {urdfConfig} />
|
60 |
+
```
|
61 |
+
|
62 |
+
## Performance Impact
|
63 |
+
|
64 |
+
- **Before**: O(n) draw calls for n robots
|
65 |
+
- **After**: O(1) draw calls per geometry type regardless of robot count
|
66 |
+
- **Memory**: Shared geometry instances reduce GPU memory usage
|
67 |
+
- **Scalability**: Linear performance improvement with robot count
|
68 |
+
|
69 |
+
This optimization is particularly beneficial for:
|
70 |
+
- Robot swarms
|
71 |
+
- Multi-robot simulations
|
72 |
+
- Arena scenarios with many similar robots
|
73 |
+
- Performance-critical real-time applications
|
bun.lock
ADDED
@@ -0,0 +1,652 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"lockfileVersion": 1,
|
3 |
+
"workspaces": {
|
4 |
+
"": {
|
5 |
+
"name": "my-app",
|
6 |
+
"dependencies": {
|
7 |
+
"@threlte/core": "^8.0.4",
|
8 |
+
"@threlte/extras": "^9.2.1",
|
9 |
+
"@types/three": "^0.176.0",
|
10 |
+
"clsx": "^2.1.1",
|
11 |
+
"tailwind-merge": "^3.3.0",
|
12 |
+
"three": "^0.177.0",
|
13 |
+
"zod": "^3.25.42",
|
14 |
+
},
|
15 |
+
"devDependencies": {
|
16 |
+
"@eslint/compat": "^1.2.5",
|
17 |
+
"@eslint/js": "^9.18.0",
|
18 |
+
"@sveltejs/adapter-auto": "^6.0.0",
|
19 |
+
"@sveltejs/adapter-static": "^3.0.8",
|
20 |
+
"@sveltejs/kit": "^2.16.0",
|
21 |
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
22 |
+
"@tailwindcss/vite": "^4.0.0",
|
23 |
+
"eslint": "^9.18.0",
|
24 |
+
"eslint-config-prettier": "^10.0.1",
|
25 |
+
"eslint-plugin-svelte": "^3.0.0",
|
26 |
+
"globals": "^16.0.0",
|
27 |
+
"prettier": "^3.4.2",
|
28 |
+
"prettier-plugin-svelte": "^3.3.3",
|
29 |
+
"prettier-plugin-tailwindcss": "^0.6.11",
|
30 |
+
"svelte": "^5.0.0",
|
31 |
+
"svelte-check": "^4.0.0",
|
32 |
+
"tailwindcss": "^4.0.0",
|
33 |
+
"typescript": "^5.0.0",
|
34 |
+
"typescript-eslint": "^8.20.0",
|
35 |
+
"vite": "^6.2.6",
|
36 |
+
},
|
37 |
+
},
|
38 |
+
},
|
39 |
+
"packages": {
|
40 |
+
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
41 |
+
|
42 |
+
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
43 |
+
|
44 |
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
45 |
+
|
46 |
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
47 |
+
|
48 |
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
49 |
+
|
50 |
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
51 |
+
|
52 |
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
53 |
+
|
54 |
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
55 |
+
|
56 |
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
57 |
+
|
58 |
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
59 |
+
|
60 |
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
61 |
+
|
62 |
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
63 |
+
|
64 |
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
65 |
+
|
66 |
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
67 |
+
|
68 |
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
69 |
+
|
70 |
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
71 |
+
|
72 |
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
73 |
+
|
74 |
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
75 |
+
|
76 |
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
77 |
+
|
78 |
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
79 |
+
|
80 |
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
81 |
+
|
82 |
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
83 |
+
|
84 |
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
85 |
+
|
86 |
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
87 |
+
|
88 |
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
89 |
+
|
90 |
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
91 |
+
|
92 |
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
93 |
+
|
94 |
+
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
95 |
+
|
96 |
+
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
97 |
+
|
98 |
+
"@eslint/compat": ["@eslint/compat@1.2.9", "", { "peerDependencies": { "eslint": "^9.10.0" }, "optionalPeers": ["eslint"] }, "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA=="],
|
99 |
+
|
100 |
+
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
|
101 |
+
|
102 |
+
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
|
103 |
+
|
104 |
+
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
|
105 |
+
|
106 |
+
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
107 |
+
|
108 |
+
"@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
|
109 |
+
|
110 |
+
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
111 |
+
|
112 |
+
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
|
113 |
+
|
114 |
+
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
115 |
+
|
116 |
+
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
117 |
+
|
118 |
+
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
119 |
+
|
120 |
+
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
121 |
+
|
122 |
+
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
123 |
+
|
124 |
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
125 |
+
|
126 |
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
127 |
+
|
128 |
+
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
129 |
+
|
130 |
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
131 |
+
|
132 |
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
133 |
+
|
134 |
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
135 |
+
|
136 |
+
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
137 |
+
|
138 |
+
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
139 |
+
|
140 |
+
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
141 |
+
|
142 |
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.1", "", { "os": "android", "cpu": "arm" }, "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw=="],
|
143 |
+
|
144 |
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.41.1", "", { "os": "android", "cpu": "arm64" }, "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA=="],
|
145 |
+
|
146 |
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.41.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w=="],
|
147 |
+
|
148 |
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.41.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg=="],
|
149 |
+
|
150 |
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.41.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg=="],
|
151 |
+
|
152 |
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.41.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA=="],
|
153 |
+
|
154 |
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg=="],
|
155 |
+
|
156 |
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA=="],
|
157 |
+
|
158 |
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA=="],
|
159 |
+
|
160 |
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg=="],
|
161 |
+
|
162 |
+
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw=="],
|
163 |
+
|
164 |
+
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.41.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A=="],
|
165 |
+
|
166 |
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw=="],
|
167 |
+
|
168 |
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw=="],
|
169 |
+
|
170 |
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.41.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g=="],
|
171 |
+
|
172 |
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A=="],
|
173 |
+
|
174 |
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ=="],
|
175 |
+
|
176 |
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.41.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ=="],
|
177 |
+
|
178 |
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.41.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg=="],
|
179 |
+
|
180 |
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw=="],
|
181 |
+
|
182 |
+
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
|
183 |
+
|
184 |
+
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ=="],
|
185 |
+
|
186 |
+
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
|
187 |
+
|
188 |
+
"@sveltejs/kit": ["@sveltejs/kit@2.21.1", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-vLbtVwtDcK8LhJKnFkFYwM0uCdFmzioQnif0bjEYH1I24Arz22JPr/hLUiXGVYAwhu8INKx5qrdvr4tHgPwX6w=="],
|
189 |
+
|
190 |
+
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.0.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.15", "vitefu": "^1.0.4" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw=="],
|
191 |
+
|
192 |
+
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
|
193 |
+
|
194 |
+
"@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="],
|
195 |
+
|
196 |
+
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="],
|
197 |
+
|
198 |
+
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="],
|
199 |
+
|
200 |
+
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="],
|
201 |
+
|
202 |
+
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="],
|
203 |
+
|
204 |
+
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="],
|
205 |
+
|
206 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="],
|
207 |
+
|
208 |
+
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="],
|
209 |
+
|
210 |
+
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="],
|
211 |
+
|
212 |
+
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="],
|
213 |
+
|
214 |
+
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="],
|
215 |
+
|
216 |
+
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="],
|
217 |
+
|
218 |
+
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="],
|
219 |
+
|
220 |
+
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="],
|
221 |
+
|
222 |
+
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="],
|
223 |
+
|
224 |
+
"@threejs-kit/instanced-sprite-mesh": ["@threejs-kit/instanced-sprite-mesh@2.5.1", "", { "dependencies": { "diet-sprite": "^0.0.1", "earcut": "^2.2.4", "maath": "^0.10.7", "three-instanced-uniforms-mesh": "^0.52.4", "troika-three-utils": "^0.52.4" }, "peerDependencies": { "three": ">=0.170.0" } }, "sha512-pmt1ALRhbHhCJQTj2FuthH6PeLIeaM4hOuS2JO3kWSwlnvx/9xuUkjFR3JOi/myMqsH7pSsLIROSaBxDfttjeA=="],
|
225 |
+
|
226 |
+
"@threlte/core": ["@threlte/core@8.0.4", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.155" } }, "sha512-kg0nTq00RqgTExIEKkyO/yCupCt73lybuFZObKHCRaY7dPcCvRD4tHkyKIFeYLYV4R6u1nqIuzfgDzYJqHbLlQ=="],
|
227 |
+
|
228 |
+
"@threlte/extras": ["@threlte/extras@9.2.1", "", { "dependencies": { "@threejs-kit/instanced-sprite-mesh": "^2.5.1", "camera-controls": "^2.10.1", "three-mesh-bvh": "^0.9.0", "three-perf": "github:jerzakm/three-perf#three-kit-threlte-fork", "three-viewport-gizmo": "^2.2.0", "troika-three-text": "^0.52.4" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.155" } }, "sha512-FzHGZadxtOIlD2ZAaH6m/vAiBVJWl/MWrW7O3DMl42ixYrGBR3FQwUbdxdHQR0akVqPESm6bwvwvRZlWqiOhAQ=="],
|
229 |
+
|
230 |
+
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
231 |
+
|
232 |
+
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
233 |
+
|
234 |
+
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
235 |
+
|
236 |
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
237 |
+
|
238 |
+
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
|
239 |
+
|
240 |
+
"@types/three": ["@types/three@0.176.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "^0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw=="],
|
241 |
+
|
242 |
+
"@types/webxr": ["@types/webxr@0.5.22", "", {}, "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A=="],
|
243 |
+
|
244 |
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/type-utils": "8.33.0", "@typescript-eslint/utils": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ=="],
|
245 |
+
|
246 |
+
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ=="],
|
247 |
+
|
248 |
+
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.0", "@typescript-eslint/types": "^8.33.0", "debug": "^4.3.4" } }, "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A=="],
|
249 |
+
|
250 |
+
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0" } }, "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw=="],
|
251 |
+
|
252 |
+
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug=="],
|
253 |
+
|
254 |
+
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ=="],
|
255 |
+
|
256 |
+
"@typescript-eslint/types": ["@typescript-eslint/types@8.33.0", "", {}, "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg=="],
|
257 |
+
|
258 |
+
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.0", "@typescript-eslint/tsconfig-utils": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ=="],
|
259 |
+
|
260 |
+
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw=="],
|
261 |
+
|
262 |
+
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.0", "", { "dependencies": { "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ=="],
|
263 |
+
|
264 |
+
"@webgpu/types": ["@webgpu/types@0.1.61", "", {}, "sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ=="],
|
265 |
+
|
266 |
+
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
267 |
+
|
268 |
+
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
269 |
+
|
270 |
+
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
271 |
+
|
272 |
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
273 |
+
|
274 |
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
275 |
+
|
276 |
+
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
277 |
+
|
278 |
+
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
279 |
+
|
280 |
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
281 |
+
|
282 |
+
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
283 |
+
|
284 |
+
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
285 |
+
|
286 |
+
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
287 |
+
|
288 |
+
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
289 |
+
|
290 |
+
"camera-controls": ["camera-controls@2.10.1", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w=="],
|
291 |
+
|
292 |
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
293 |
+
|
294 |
+
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
295 |
+
|
296 |
+
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
297 |
+
|
298 |
+
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
299 |
+
|
300 |
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
301 |
+
|
302 |
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
303 |
+
|
304 |
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
305 |
+
|
306 |
+
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
307 |
+
|
308 |
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
309 |
+
|
310 |
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
311 |
+
|
312 |
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
313 |
+
|
314 |
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
315 |
+
|
316 |
+
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
317 |
+
|
318 |
+
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
319 |
+
|
320 |
+
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
|
321 |
+
|
322 |
+
"diet-sprite": ["diet-sprite@0.0.1", "", {}, "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A=="],
|
323 |
+
|
324 |
+
"earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="],
|
325 |
+
|
326 |
+
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
|
327 |
+
|
328 |
+
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
329 |
+
|
330 |
+
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
331 |
+
|
332 |
+
"eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="],
|
333 |
+
|
334 |
+
"eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="],
|
335 |
+
|
336 |
+
"eslint-plugin-svelte": ["eslint-plugin-svelte@3.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.36.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.2.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-nvIUNyyPGbr5922Kd1p/jXe+FfNdVPXsxLyrrXpwfSbZZEFdAYva9O/gm2lObC/wXkQo/AUmQkAihfmNJYeCjA=="],
|
337 |
+
|
338 |
+
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
339 |
+
|
340 |
+
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
|
341 |
+
|
342 |
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
343 |
+
|
344 |
+
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
|
345 |
+
|
346 |
+
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
347 |
+
|
348 |
+
"esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="],
|
349 |
+
|
350 |
+
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
351 |
+
|
352 |
+
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
353 |
+
|
354 |
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
355 |
+
|
356 |
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
357 |
+
|
358 |
+
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
359 |
+
|
360 |
+
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
361 |
+
|
362 |
+
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
363 |
+
|
364 |
+
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
365 |
+
|
366 |
+
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
367 |
+
|
368 |
+
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
369 |
+
|
370 |
+
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
371 |
+
|
372 |
+
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
373 |
+
|
374 |
+
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
375 |
+
|
376 |
+
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
377 |
+
|
378 |
+
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
379 |
+
|
380 |
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
381 |
+
|
382 |
+
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
383 |
+
|
384 |
+
"globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
|
385 |
+
|
386 |
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
387 |
+
|
388 |
+
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
389 |
+
|
390 |
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
391 |
+
|
392 |
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
393 |
+
|
394 |
+
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
395 |
+
|
396 |
+
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
397 |
+
|
398 |
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
399 |
+
|
400 |
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
401 |
+
|
402 |
+
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
403 |
+
|
404 |
+
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
405 |
+
|
406 |
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
407 |
+
|
408 |
+
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
409 |
+
|
410 |
+
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
411 |
+
|
412 |
+
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
413 |
+
|
414 |
+
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
415 |
+
|
416 |
+
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
417 |
+
|
418 |
+
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
419 |
+
|
420 |
+
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
421 |
+
|
422 |
+
"known-css-properties": ["known-css-properties@0.36.0", "", {}, "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA=="],
|
423 |
+
|
424 |
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
425 |
+
|
426 |
+
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
427 |
+
|
428 |
+
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
429 |
+
|
430 |
+
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
431 |
+
|
432 |
+
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
433 |
+
|
434 |
+
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
435 |
+
|
436 |
+
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
437 |
+
|
438 |
+
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
439 |
+
|
440 |
+
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
441 |
+
|
442 |
+
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
443 |
+
|
444 |
+
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
445 |
+
|
446 |
+
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
447 |
+
|
448 |
+
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
449 |
+
|
450 |
+
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
451 |
+
|
452 |
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
453 |
+
|
454 |
+
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
455 |
+
|
456 |
+
"maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
|
457 |
+
|
458 |
+
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
459 |
+
|
460 |
+
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
461 |
+
|
462 |
+
"meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="],
|
463 |
+
|
464 |
+
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
465 |
+
|
466 |
+
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
467 |
+
|
468 |
+
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
469 |
+
|
470 |
+
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
471 |
+
|
472 |
+
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
473 |
+
|
474 |
+
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
475 |
+
|
476 |
+
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
477 |
+
|
478 |
+
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
479 |
+
|
480 |
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
481 |
+
|
482 |
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
483 |
+
|
484 |
+
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
485 |
+
|
486 |
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
487 |
+
|
488 |
+
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
489 |
+
|
490 |
+
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
491 |
+
|
492 |
+
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
493 |
+
|
494 |
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
495 |
+
|
496 |
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
497 |
+
|
498 |
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
499 |
+
|
500 |
+
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
501 |
+
|
502 |
+
"postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
|
503 |
+
|
504 |
+
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
|
505 |
+
|
506 |
+
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
|
507 |
+
|
508 |
+
"postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
|
509 |
+
|
510 |
+
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
|
511 |
+
|
512 |
+
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
513 |
+
|
514 |
+
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
515 |
+
|
516 |
+
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
|
517 |
+
|
518 |
+
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.12", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw=="],
|
519 |
+
|
520 |
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
521 |
+
|
522 |
+
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
523 |
+
|
524 |
+
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
525 |
+
|
526 |
+
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
527 |
+
|
528 |
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
529 |
+
|
530 |
+
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
531 |
+
|
532 |
+
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
533 |
+
|
534 |
+
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
535 |
+
|
536 |
+
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
537 |
+
|
538 |
+
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
539 |
+
|
540 |
+
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
541 |
+
|
542 |
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
543 |
+
|
544 |
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
545 |
+
|
546 |
+
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
547 |
+
|
548 |
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
549 |
+
|
550 |
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
551 |
+
|
552 |
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
553 |
+
|
554 |
+
"svelte": ["svelte@5.33.10", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-/yArPQIBoQS2p86LKnvJywOXkVHeEXnFgrDPSxkEfIAEkykopYuy2bF6UUqHG4IbZlJD6OurLxJT8Kn7kTk9WA=="],
|
555 |
+
|
556 |
+
"svelte-check": ["svelte-check@4.2.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA=="],
|
557 |
+
|
558 |
+
"svelte-eslint-parser": ["svelte-eslint-parser@1.2.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw=="],
|
559 |
+
|
560 |
+
"tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="],
|
561 |
+
|
562 |
+
"tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="],
|
563 |
+
|
564 |
+
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
565 |
+
|
566 |
+
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
567 |
+
|
568 |
+
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
569 |
+
|
570 |
+
"three-instanced-uniforms-mesh": ["three-instanced-uniforms-mesh@0.52.4", "", { "dependencies": { "troika-three-utils": "^0.52.4" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-YwDBy05hfKZQtU+Rp0KyDf9yH4GxfhxMbVt9OYruxdgLfPwmDG5oAbGoW0DrKtKZSM3BfFcCiejiOHCjFBTeng=="],
|
571 |
+
|
572 |
+
"three-mesh-bvh": ["three-mesh-bvh@0.9.0", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-xAwZj0hZknpwVsdK5BBJTIAZDjDPZCRzURY1o+z/JHBON/jc2UetK1CzPeQZiiOVSfI4jV2z7sXnnGtgsgnjaA=="],
|
573 |
+
|
574 |
+
"three-perf": ["three-perf@github:jerzakm/three-perf#322d7d3", { "dependencies": { "troika-three-text": "^0.52.0", "tweakpane": "^3.1.10" }, "peerDependencies": { "three": ">=0.170" } }, "jerzakm-three-perf-322d7d3"],
|
575 |
+
|
576 |
+
"three-viewport-gizmo": ["three-viewport-gizmo@2.2.0", "", { "peerDependencies": { "three": ">=0.162.0 <1.0.0" } }, "sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q=="],
|
577 |
+
|
578 |
+
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
579 |
+
|
580 |
+
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
581 |
+
|
582 |
+
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
583 |
+
|
584 |
+
"troika-three-text": ["troika-three-text@0.52.4", "", { "dependencies": { "bidi-js": "^1.0.2", "troika-three-utils": "^0.52.4", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg=="],
|
585 |
+
|
586 |
+
"troika-three-utils": ["troika-three-utils@0.52.4", "", { "peerDependencies": { "three": ">=0.125.0" } }, "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A=="],
|
587 |
+
|
588 |
+
"troika-worker-utils": ["troika-worker-utils@0.52.0", "", {}, "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="],
|
589 |
+
|
590 |
+
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
591 |
+
|
592 |
+
"tweakpane": ["tweakpane@3.1.10", "", {}, "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ=="],
|
593 |
+
|
594 |
+
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
595 |
+
|
596 |
+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
597 |
+
|
598 |
+
"typescript-eslint": ["typescript-eslint@8.33.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/parser": "8.33.0", "@typescript-eslint/utils": "8.33.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ=="],
|
599 |
+
|
600 |
+
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
601 |
+
|
602 |
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
603 |
+
|
604 |
+
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
605 |
+
|
606 |
+
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
607 |
+
|
608 |
+
"webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="],
|
609 |
+
|
610 |
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
611 |
+
|
612 |
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
613 |
+
|
614 |
+
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
615 |
+
|
616 |
+
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
617 |
+
|
618 |
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
619 |
+
|
620 |
+
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
621 |
+
|
622 |
+
"zod": ["zod@3.25.42", "", {}, "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ=="],
|
623 |
+
|
624 |
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
625 |
+
|
626 |
+
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
627 |
+
|
628 |
+
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
629 |
+
|
630 |
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
631 |
+
|
632 |
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
633 |
+
|
634 |
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
635 |
+
|
636 |
+
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
|
637 |
+
|
638 |
+
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
639 |
+
|
640 |
+
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
641 |
+
|
642 |
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
643 |
+
|
644 |
+
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
645 |
+
|
646 |
+
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
647 |
+
|
648 |
+
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
649 |
+
|
650 |
+
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
651 |
+
}
|
652 |
+
}
|
docker-compose.yml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3.8'
|
2 |
+
|
3 |
+
services:
|
4 |
+
lerobot-arena:
|
5 |
+
build:
|
6 |
+
context: .
|
7 |
+
dockerfile: Dockerfile
|
8 |
+
ports:
|
9 |
+
- "8080:8080" # Python backend
|
10 |
+
- "7860:7860" # Svelte frontend (HF Spaces default)
|
11 |
+
environment:
|
12 |
+
- NODE_ENV=production
|
13 |
+
- PYTHONUNBUFFERED=1
|
14 |
+
restart: unless-stopped
|
eslint.config.js
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import prettier from 'eslint-config-prettier';
|
2 |
+
import js from '@eslint/js';
|
3 |
+
import { includeIgnoreFile } from '@eslint/compat';
|
4 |
+
import svelte from 'eslint-plugin-svelte';
|
5 |
+
import globals from 'globals';
|
6 |
+
import { fileURLToPath } from 'node:url';
|
7 |
+
import ts from 'typescript-eslint';
|
8 |
+
import svelteConfig from './svelte.config.js';
|
9 |
+
|
10 |
+
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
11 |
+
|
12 |
+
export default ts.config(
|
13 |
+
includeIgnoreFile(gitignorePath),
|
14 |
+
js.configs.recommended,
|
15 |
+
...ts.configs.recommended,
|
16 |
+
...svelte.configs.recommended,
|
17 |
+
prettier,
|
18 |
+
...svelte.configs.prettier,
|
19 |
+
{
|
20 |
+
languageOptions: {
|
21 |
+
globals: { ...globals.browser, ...globals.node }
|
22 |
+
},
|
23 |
+
rules: { 'no-undef': 'off' }
|
24 |
+
},
|
25 |
+
{
|
26 |
+
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
27 |
+
languageOptions: {
|
28 |
+
parserOptions: {
|
29 |
+
projectService: true,
|
30 |
+
extraFileExtensions: ['.svelte'],
|
31 |
+
parser: ts.parser,
|
32 |
+
svelteConfig
|
33 |
+
}
|
34 |
+
}
|
35 |
+
}
|
36 |
+
);
|
package.json
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "my-app",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.1",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite dev",
|
8 |
+
"build": "vite build",
|
9 |
+
"preview": "vite preview",
|
10 |
+
"prepare": "svelte-kit sync || echo ''",
|
11 |
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
12 |
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
13 |
+
"format": "prettier --write .",
|
14 |
+
"lint": "prettier --check . && eslint ."
|
15 |
+
},
|
16 |
+
"devDependencies": {
|
17 |
+
"@eslint/compat": "^1.2.5",
|
18 |
+
"@eslint/js": "^9.18.0",
|
19 |
+
"@sveltejs/adapter-auto": "^6.0.0",
|
20 |
+
"@sveltejs/adapter-static": "^3.0.8",
|
21 |
+
"@sveltejs/kit": "^2.16.0",
|
22 |
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
23 |
+
"@tailwindcss/vite": "^4.0.0",
|
24 |
+
"eslint": "^9.18.0",
|
25 |
+
"eslint-config-prettier": "^10.0.1",
|
26 |
+
"eslint-plugin-svelte": "^3.0.0",
|
27 |
+
"globals": "^16.0.0",
|
28 |
+
"prettier": "^3.4.2",
|
29 |
+
"prettier-plugin-svelte": "^3.3.3",
|
30 |
+
"prettier-plugin-tailwindcss": "^0.6.11",
|
31 |
+
"svelte": "^5.0.0",
|
32 |
+
"svelte-check": "^4.0.0",
|
33 |
+
"tailwindcss": "^4.0.0",
|
34 |
+
"typescript": "^5.0.0",
|
35 |
+
"typescript-eslint": "^8.20.0",
|
36 |
+
"vite": "^6.2.6"
|
37 |
+
},
|
38 |
+
"dependencies": {
|
39 |
+
"@threlte/core": "^8.0.4",
|
40 |
+
"@threlte/extras": "^9.2.1",
|
41 |
+
"@types/three": "^0.176.0",
|
42 |
+
"clsx": "^2.1.1",
|
43 |
+
"tailwind-merge": "^3.3.0",
|
44 |
+
"three": "^0.177.0",
|
45 |
+
"zod": "^3.25.42"
|
46 |
+
}
|
47 |
+
}
|
src-python/.gitignore
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python-generated files
|
2 |
+
__pycache__/
|
3 |
+
*.py[oc]
|
4 |
+
build/
|
5 |
+
dist/
|
6 |
+
wheels/
|
7 |
+
*.egg-info
|
8 |
+
|
9 |
+
# Virtual environments
|
10 |
+
.venv
|
src-python/.python-version
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
3.12
|
src-python/README.md
ADDED
@@ -0,0 +1,656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🤖 LeRobot Arena - Complete Robot Control System
|
2 |
+
|
3 |
+
A comprehensive WebSocket-based robot control platform featuring **master-slave architecture**, real-time 3D visualization, and seamless integration between physical robots and digital twins.
|
4 |
+
|
5 |
+
## 🏛️ Core Architecture
|
6 |
+
|
7 |
+
### Master-Slave Pattern
|
8 |
+
```
|
9 |
+
┌─────────────────┐ Commands ┌─────────────────┐ Execution ┌─────────────────┐
|
10 |
+
│ 🎮 MASTERS │ ───────────────►│ 🤖 ROBOT CORE │ ───────────────►│ 🔧 SLAVES │
|
11 |
+
│ │ │ │ │ │
|
12 |
+
│ • Remote Server │ WebSocket │ • Robot Manager │ USB/Mock/WS │ • Physical Bot │
|
13 |
+
│ • Demo Scripts │ HTTP API │ • State Sync │ Direct I/O │ • 3D Simulation │
|
14 |
+
│ • Manual UI │ Sequences │ • Safety Logic │ Commands │ • Mock Testing │
|
15 |
+
│ • AI Agents │ │ • Multi-Robot │ │ • Remote Proxy │
|
16 |
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
17 |
+
```
|
18 |
+
|
19 |
+
**Key Principles:**
|
20 |
+
- 🎯 **Single Source of Truth**: Only one master per robot at a time
|
21 |
+
- 🔒 **Safety First**: Manual control disabled when master active
|
22 |
+
- 🌐 **Multi-Modal**: Same commands work across all slave types
|
23 |
+
- 🔄 **Real-Time Sync**: Virtual and physical states synchronized
|
24 |
+
- 📡 **Network Native**: Built for remote operation from day one
|
25 |
+
|
26 |
+
## 🚀 Quick Start Guide
|
27 |
+
|
28 |
+
### 1. Server Setup
|
29 |
+
```bash
|
30 |
+
# Install dependencies
|
31 |
+
cd src-python
|
32 |
+
uv sync
|
33 |
+
|
34 |
+
# Start the WebSocket server
|
35 |
+
python start_server.py
|
36 |
+
# Server runs on http://localhost:8080
|
37 |
+
```
|
38 |
+
|
39 |
+
### 2. Frontend Integration
|
40 |
+
```bash
|
41 |
+
# In your Svelte app
|
42 |
+
npm run dev
|
43 |
+
# Visit http://localhost:5173
|
44 |
+
```
|
45 |
+
|
46 |
+
### 3. Create & Control Robot
|
47 |
+
```javascript
|
48 |
+
// Create robot in UI or via API
|
49 |
+
const robot = await robotManager.createRobot('my-robot', robotUrdfConfig);
|
50 |
+
|
51 |
+
// Option 1: Manual control (sliders in UI)
|
52 |
+
await robot.updateJointValue('Rotation', 45);
|
53 |
+
|
54 |
+
// Option 2: Demo sequences
|
55 |
+
await robotManager.connectDemoSequences('my-robot');
|
56 |
+
|
57 |
+
// Option 3: Remote server control
|
58 |
+
await robotManager.connectMaster('my-robot', {
|
59 |
+
type: "remote-server",
|
60 |
+
url: "ws://localhost:8080"
|
61 |
+
});
|
62 |
+
|
63 |
+
// Option 4: Direct API sequence trigger
|
64 |
+
curl -X POST http://localhost:8080/api/robots/my-robot/play-sequence/gentle-wave
|
65 |
+
```
|
66 |
+
|
67 |
+
## 🎮 Master Drivers (Command Sources)
|
68 |
+
|
69 |
+
### MockSequenceMaster
|
70 |
+
**Pre-programmed movement patterns for testing and demos**
|
71 |
+
|
72 |
+
```typescript
|
73 |
+
const config: MasterDriverConfig = {
|
74 |
+
type: "mock-sequence",
|
75 |
+
sequences: DEMO_SEQUENCES,
|
76 |
+
autoStart: true,
|
77 |
+
loopMode: true
|
78 |
+
};
|
79 |
+
|
80 |
+
await robotManager.connectMaster('robot-1', config);
|
81 |
+
```
|
82 |
+
|
83 |
+
**Available Sequences:**
|
84 |
+
- **🌊 Gentle Wave Pattern** (6s): Smooth greeting gesture with wrist movements
|
85 |
+
- **🔍 Small Scanning Pattern** (8s): Horizontal sweep for environment scanning
|
86 |
+
- **💪 Tiny Flex Pattern** (8s): Articulation demonstration with elbow/jaw coordination
|
87 |
+
|
88 |
+
**Features:**
|
89 |
+
- ✅ Safe movement ranges (±25° max)
|
90 |
+
- ✅ Smooth interpolation between keyframes
|
91 |
+
- ✅ Loop mode for continuous operation
|
92 |
+
- ✅ Automatic master takeover
|
93 |
+
|
94 |
+
### RemoteServerMaster
|
95 |
+
**WebSocket connection to external control systems**
|
96 |
+
|
97 |
+
```typescript
|
98 |
+
const config: MasterDriverConfig = {
|
99 |
+
type: "remote-server",
|
100 |
+
url: "ws://localhost:8080",
|
101 |
+
apiKey: "optional-auth-token"
|
102 |
+
};
|
103 |
+
|
104 |
+
await robotManager.connectMaster('robot-1', config);
|
105 |
+
```
|
106 |
+
|
107 |
+
**Features:**
|
108 |
+
- ✅ Real-time bidirectional communication
|
109 |
+
- ✅ Automatic reconnection with exponential backoff
|
110 |
+
- ✅ Heartbeat monitoring (30s intervals)
|
111 |
+
- ✅ Command acknowledgment
|
112 |
+
- ✅ Status and error reporting
|
113 |
+
|
114 |
+
**Message Format:**
|
115 |
+
```json
|
116 |
+
{
|
117 |
+
"type": "command",
|
118 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
119 |
+
"data": {
|
120 |
+
"timestamp": 1704110400000,
|
121 |
+
"joints": [
|
122 |
+
{"name": "Rotation", "value": 45, "speed": 100}
|
123 |
+
]
|
124 |
+
}
|
125 |
+
}
|
126 |
+
```
|
127 |
+
|
128 |
+
## 🔧 Slave Drivers (Execution Targets)
|
129 |
+
|
130 |
+
### MockSlave
|
131 |
+
**Perfect simulation for development and testing**
|
132 |
+
|
133 |
+
```typescript
|
134 |
+
const config: SlaveDriverConfig = {
|
135 |
+
type: "mock-slave",
|
136 |
+
simulateLatency: 50, // Realistic response delay
|
137 |
+
simulateErrors: false, // Random connection issues
|
138 |
+
responseDelay: 20 // Command execution time
|
139 |
+
};
|
140 |
+
|
141 |
+
await robotManager.connectSlave('robot-1', config);
|
142 |
+
```
|
143 |
+
|
144 |
+
**Features:**
|
145 |
+
- ✅ Perfect command execution (real_value = virtual_value)
|
146 |
+
- ✅ Configurable network latency simulation
|
147 |
+
- ✅ Error injection for robustness testing
|
148 |
+
- ✅ Instant connection without hardware
|
149 |
+
- ✅ Real-time state feedback
|
150 |
+
|
151 |
+
### USBSlave
|
152 |
+
**Direct serial communication with physical robots**
|
153 |
+
|
154 |
+
```typescript
|
155 |
+
const config: SlaveDriverConfig = {
|
156 |
+
type: "usb-slave",
|
157 |
+
port: "/dev/ttyUSB0", // Auto-detect if undefined
|
158 |
+
baudRate: 115200
|
159 |
+
};
|
160 |
+
|
161 |
+
await robotManager.connectSlave('robot-1', config);
|
162 |
+
```
|
163 |
+
|
164 |
+
**Features:**
|
165 |
+
- ✅ Feetech servo protocol support
|
166 |
+
- ✅ Position and speed control
|
167 |
+
- ✅ Real servo position feedback
|
168 |
+
- ✅ Calibration workflow for sync
|
169 |
+
- ✅ Error handling and recovery
|
170 |
+
|
171 |
+
**Calibration Process:**
|
172 |
+
1. Manually position robot to match digital twin
|
173 |
+
2. Click "Calibrate" to sync virtual/real coordinates
|
174 |
+
3. System calculates offset: `real_pos = raw_pos + offset`
|
175 |
+
4. All future commands automatically compensated
|
176 |
+
|
177 |
+
### WebSocketSlave
|
178 |
+
**Remote robot control via WebSocket relay**
|
179 |
+
|
180 |
+
```typescript
|
181 |
+
const config: SlaveDriverConfig = {
|
182 |
+
type: "websocket-slave",
|
183 |
+
url: "ws://robot-proxy:8080",
|
184 |
+
robotId: "remote-arm-1"
|
185 |
+
};
|
186 |
+
|
187 |
+
await robotManager.connectSlave('robot-1', config);
|
188 |
+
```
|
189 |
+
|
190 |
+
**Use Cases:**
|
191 |
+
- 🌐 Control robots across internet
|
192 |
+
- 🏢 Enterprise robot fleet management
|
193 |
+
- 🔒 Firewall-friendly robot access
|
194 |
+
- 📡 Proxy through edge devices
|
195 |
+
|
196 |
+
## 📡 WebSocket Protocol Specification
|
197 |
+
|
198 |
+
### Master → Server Communication
|
199 |
+
|
200 |
+
#### Send Joint Command
|
201 |
+
```json
|
202 |
+
{
|
203 |
+
"type": "command",
|
204 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
205 |
+
"data": {
|
206 |
+
"timestamp": 1704110400000,
|
207 |
+
"joints": [
|
208 |
+
{"name": "Rotation", "value": 45, "speed": 100},
|
209 |
+
{"name": "Pitch", "value": -30, "speed": 150}
|
210 |
+
],
|
211 |
+
"duration": 2000,
|
212 |
+
"metadata": {"source": "manual_control"}
|
213 |
+
}
|
214 |
+
}
|
215 |
+
```
|
216 |
+
|
217 |
+
#### Send Movement Sequence
|
218 |
+
```json
|
219 |
+
{
|
220 |
+
"type": "sequence",
|
221 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
222 |
+
"data": {
|
223 |
+
"id": "custom-dance",
|
224 |
+
"name": "Custom Dance Sequence",
|
225 |
+
"commands": [
|
226 |
+
{
|
227 |
+
"timestamp": 0,
|
228 |
+
"joints": [{"name": "Rotation", "value": -30}],
|
229 |
+
"duration": 1000
|
230 |
+
},
|
231 |
+
{
|
232 |
+
"timestamp": 1000,
|
233 |
+
"joints": [{"name": "Rotation", "value": 30}],
|
234 |
+
"duration": 1000
|
235 |
+
}
|
236 |
+
],
|
237 |
+
"total_duration": 2000,
|
238 |
+
"loop": false
|
239 |
+
}
|
240 |
+
}
|
241 |
+
```
|
242 |
+
|
243 |
+
#### Heartbeat
|
244 |
+
```json
|
245 |
+
{
|
246 |
+
"type": "heartbeat",
|
247 |
+
"timestamp": "2024-01-01T12:00:00Z"
|
248 |
+
}
|
249 |
+
```
|
250 |
+
|
251 |
+
### Slave → Server Communication
|
252 |
+
|
253 |
+
#### Status Update
|
254 |
+
```json
|
255 |
+
{
|
256 |
+
"type": "status_update",
|
257 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
258 |
+
"data": {
|
259 |
+
"isConnected": true,
|
260 |
+
"lastConnected": "2024-01-01T11:58:00Z",
|
261 |
+
"error": null
|
262 |
+
}
|
263 |
+
}
|
264 |
+
```
|
265 |
+
|
266 |
+
#### Joint State Feedback
|
267 |
+
```json
|
268 |
+
{
|
269 |
+
"type": "joint_states",
|
270 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
271 |
+
"data": [
|
272 |
+
{
|
273 |
+
"name": "Rotation",
|
274 |
+
"servo_id": 1,
|
275 |
+
"type": "revolute",
|
276 |
+
"virtual_value": 45.0,
|
277 |
+
"real_value": 44.8,
|
278 |
+
"limits": {
|
279 |
+
"lower": -180,
|
280 |
+
"upper": 180,
|
281 |
+
"velocity": 200
|
282 |
+
}
|
283 |
+
}
|
284 |
+
]
|
285 |
+
}
|
286 |
+
```
|
287 |
+
|
288 |
+
#### Error Reporting
|
289 |
+
```json
|
290 |
+
{
|
291 |
+
"type": "error",
|
292 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
293 |
+
"data": {
|
294 |
+
"code": "SERVO_TIMEOUT",
|
295 |
+
"message": "Servo 3 not responding",
|
296 |
+
"joint": "Elbow",
|
297 |
+
"severity": "warning"
|
298 |
+
}
|
299 |
+
}
|
300 |
+
```
|
301 |
+
|
302 |
+
## 🛠️ REST API Reference
|
303 |
+
|
304 |
+
### Robot Management
|
305 |
+
|
306 |
+
| Method | Endpoint | Description | Example |
|
307 |
+
|--------|----------|-------------|---------|
|
308 |
+
| GET | `/` | Server status & metrics | `curl http://localhost:8080/` |
|
309 |
+
| GET | `/api/robots` | List all robots | `curl http://localhost:8080/api/robots` |
|
310 |
+
| POST | `/api/robots` | Create new robot | `curl -X POST -H "Content-Type: application/json" -d '{"robot_type":"so-arm100","name":"My Robot"}' http://localhost:8080/api/robots` |
|
311 |
+
| GET | `/api/robots/{id}` | Get robot details | `curl http://localhost:8080/api/robots/robot-123` |
|
312 |
+
| GET | `/api/robots/{id}/status` | Get connection status | `curl http://localhost:8080/api/robots/robot-123/status` |
|
313 |
+
| DELETE | `/api/robots/{id}` | Delete robot | `curl -X DELETE http://localhost:8080/api/robots/robot-123` |
|
314 |
+
|
315 |
+
### Sequence Control
|
316 |
+
|
317 |
+
| Method | Endpoint | Description | Example |
|
318 |
+
|--------|----------|-------------|---------|
|
319 |
+
| GET | `/api/sequences` | List demo sequences | `curl http://localhost:8080/api/sequences` |
|
320 |
+
| POST | `/api/robots/{id}/play-sequence/{seq_id}` | Play sequence | `curl -X POST http://localhost:8080/api/robots/robot-123/play-sequence/gentle-wave` |
|
321 |
+
| POST | `/api/robots/{id}/stop-sequence` | Stop sequences | `curl -X POST http://localhost:8080/api/robots/robot-123/stop-sequence` |
|
322 |
+
|
323 |
+
### WebSocket Endpoints
|
324 |
+
|
325 |
+
| Endpoint | Purpose | Client Type | Example |
|
326 |
+
|----------|---------|-------------|---------|
|
327 |
+
| `/ws/master/{robot_id}` | Send commands | Control sources | `ws://localhost:8080/ws/master/robot-123` |
|
328 |
+
| `/ws/slave/{robot_id}` | Receive commands | Execution targets | `ws://localhost:8080/ws/slave/robot-123` |
|
329 |
+
|
330 |
+
## 🎯 Usage Scenarios
|
331 |
+
|
332 |
+
### 1. 🧪 Development & Testing
|
333 |
+
```bash
|
334 |
+
# Create robot with mock slave for safe testing
|
335 |
+
robot = await robotManager.createRobot('test-bot', urdfConfig);
|
336 |
+
await robotManager.connectMockSlave('test-bot', 50);
|
337 |
+
await robotManager.connectDemoSequences('test-bot');
|
338 |
+
```
|
339 |
+
|
340 |
+
**Perfect for:**
|
341 |
+
- Algorithm development without hardware risk
|
342 |
+
- UI/UX testing with realistic feedback
|
343 |
+
- Automated testing pipelines
|
344 |
+
- Demo presentations
|
345 |
+
|
346 |
+
### 2. 🦾 Physical Robot Control
|
347 |
+
```bash
|
348 |
+
# Connect real hardware
|
349 |
+
robot = await robotManager.createRobot('real-bot', urdfConfig);
|
350 |
+
await robotManager.connectUSBSlave('real-bot', '/dev/ttyUSB0');
|
351 |
+
|
352 |
+
# Manual control via UI sliders
|
353 |
+
# OR automated via demo sequences
|
354 |
+
await robotManager.connectDemoSequences('real-bot');
|
355 |
+
```
|
356 |
+
|
357 |
+
**Calibration Workflow:**
|
358 |
+
1. Connect USB slave to robot
|
359 |
+
2. Manually position to match 3D model rest pose
|
360 |
+
3. Click "Calibrate" to sync coordinate systems
|
361 |
+
4. Robot now mirrors 3D model movements precisely
|
362 |
+
|
363 |
+
### 3. 🌐 Remote Operation
|
364 |
+
```bash
|
365 |
+
# Master controls slave over internet
|
366 |
+
# Master side:
|
367 |
+
await robotManager.connectMaster('local-avatar', {
|
368 |
+
type: "remote-server",
|
369 |
+
url: "wss://robot-farm.com:8080"
|
370 |
+
});
|
371 |
+
|
372 |
+
# Slave side (at robot location):
|
373 |
+
await robotManager.connectSlave('physical-robot', {
|
374 |
+
type: "websocket-slave",
|
375 |
+
url: "wss://robot-farm.com:8080",
|
376 |
+
robotId: "local-avatar"
|
377 |
+
});
|
378 |
+
```
|
379 |
+
|
380 |
+
**Use Cases:**
|
381 |
+
- Telepresence robotics
|
382 |
+
- Remote maintenance and inspection
|
383 |
+
- Distributed manufacturing
|
384 |
+
- Educational robot sharing
|
385 |
+
|
386 |
+
### 4. 🤖 Multi-Robot Coordination
|
387 |
+
```bash
|
388 |
+
# One master controlling multiple robots
|
389 |
+
await robotManager.connectMaster('fleet-commander', masterConfig);
|
390 |
+
|
391 |
+
// Multiple slaves executing same commands
|
392 |
+
for (const robot of robotFleet) {
|
393 |
+
await robotManager.connectSlave(robot.id, slaveConfig);
|
394 |
+
}
|
395 |
+
```
|
396 |
+
|
397 |
+
**Applications:**
|
398 |
+
- Synchronized dance performances
|
399 |
+
- Assembly line coordination
|
400 |
+
- Swarm robotics research
|
401 |
+
- Entertainment shows
|
402 |
+
|
403 |
+
### 5. 🧠 AI Agent Integration
|
404 |
+
```javascript
|
405 |
+
// AI agent as master driver
|
406 |
+
class AIAgentMaster implements MasterDriver {
|
407 |
+
async generateNextCommand(): Promise<RobotCommand> {
|
408 |
+
const sensorData = await this.readEnvironment();
|
409 |
+
const decision = await this.aiModel.predict(sensorData);
|
410 |
+
return this.convertToRobotCommand(decision);
|
411 |
+
}
|
412 |
+
}
|
413 |
+
|
414 |
+
await robot.setMaster(new AIAgentMaster(config));
|
415 |
+
```
|
416 |
+
|
417 |
+
## 🔧 Integration Guide
|
418 |
+
|
419 |
+
### Frontend Integration (Svelte)
|
420 |
+
```typescript
|
421 |
+
import { robotManager } from '$lib/robot/RobotManager.svelte';
|
422 |
+
import { robotUrdfConfigMap } from '$lib/configs/robotUrdfConfig';
|
423 |
+
|
424 |
+
// Create robot
|
425 |
+
const robot = await robotManager.createRobot('my-robot', robotUrdfConfigMap['so-arm100']);
|
426 |
+
|
427 |
+
// Add visualization
|
428 |
+
await robotManager.connectMockSlave('my-robot');
|
429 |
+
|
430 |
+
// Add control
|
431 |
+
await robotManager.connectDemoSequences('my-robot', true);
|
432 |
+
|
433 |
+
// Monitor state
|
434 |
+
robot.joints.forEach(joint => {
|
435 |
+
console.log(`${joint.name}: virtual=${joint.virtualValue}° real=${joint.realValue}°`);
|
436 |
+
});
|
437 |
+
```
|
438 |
+
|
439 |
+
### Backend Integration (Python)
|
440 |
+
```python
|
441 |
+
import asyncio
|
442 |
+
import websockets
|
443 |
+
import json
|
444 |
+
|
445 |
+
async def robot_controller():
|
446 |
+
uri = "ws://localhost:8080/ws/master/my-robot"
|
447 |
+
async with websockets.connect(uri) as websocket:
|
448 |
+
# Send command
|
449 |
+
command = {
|
450 |
+
"type": "command",
|
451 |
+
"timestamp": "2024-01-01T12:00:00Z",
|
452 |
+
"data": {
|
453 |
+
"timestamp": 1704110400000,
|
454 |
+
"joints": [{"name": "Rotation", "value": 45}]
|
455 |
+
}
|
456 |
+
}
|
457 |
+
await websocket.send(json.dumps(command))
|
458 |
+
|
459 |
+
# Listen for responses
|
460 |
+
async for message in websocket:
|
461 |
+
data = json.loads(message)
|
462 |
+
if data["type"] == "joint_states":
|
463 |
+
print(f"Robot position: {data['data']}")
|
464 |
+
|
465 |
+
asyncio.run(robot_controller())
|
466 |
+
```
|
467 |
+
|
468 |
+
### Hardware Integration (Arduino/C++)
|
469 |
+
```cpp
|
470 |
+
#include <WiFi.h>
|
471 |
+
#include <WebSocketsClient.h>
|
472 |
+
#include <ArduinoJson.h>
|
473 |
+
|
474 |
+
WebSocketsClient webSocket;
|
475 |
+
|
476 |
+
void onWebSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
|
477 |
+
if (type == WStype_TEXT) {
|
478 |
+
DynamicJsonDocument doc(1024);
|
479 |
+
deserializeJson(doc, payload);
|
480 |
+
|
481 |
+
if (doc["type"] == "execute_command") {
|
482 |
+
JsonArray joints = doc["data"]["joints"];
|
483 |
+
for (JsonObject joint : joints) {
|
484 |
+
String name = joint["name"];
|
485 |
+
float value = joint["value"];
|
486 |
+
moveServo(name, value);
|
487 |
+
}
|
488 |
+
}
|
489 |
+
}
|
490 |
+
}
|
491 |
+
|
492 |
+
void setup() {
|
493 |
+
webSocket.begin("192.168.1.100", 8080, "/ws/slave/arduino-bot");
|
494 |
+
webSocket.onEvent(onWebSocketEvent);
|
495 |
+
}
|
496 |
+
```
|
497 |
+
|
498 |
+
## 🛡️ Security & Production
|
499 |
+
|
500 |
+
### Authentication
|
501 |
+
```typescript
|
502 |
+
// API key authentication (planned)
|
503 |
+
const master = new RemoteServerMaster({
|
504 |
+
type: "remote-server",
|
505 |
+
url: "wss://secure-robot-farm.com:8080",
|
506 |
+
apiKey: "your-secret-api-key"
|
507 |
+
}, robotId);
|
508 |
+
```
|
509 |
+
|
510 |
+
### TLS/SSL
|
511 |
+
```bash
|
512 |
+
# Production deployment with SSL
|
513 |
+
uvicorn main:app --host 0.0.0.0 --port 443 --ssl-keyfile key.pem --ssl-certfile cert.pem
|
514 |
+
```
|
515 |
+
|
516 |
+
### Rate Limiting & Safety
|
517 |
+
```python
|
518 |
+
# Built-in protections
|
519 |
+
- Command rate limiting (100 commands/second max)
|
520 |
+
- Joint velocity limits from URDF
|
521 |
+
- Emergency stop on connection loss
|
522 |
+
- Position bounds checking
|
523 |
+
- Servo temperature monitoring
|
524 |
+
```
|
525 |
+
|
526 |
+
## 🚀 Deployment Options
|
527 |
+
|
528 |
+
### Development
|
529 |
+
```bash
|
530 |
+
cd src-python && python start_server.py
|
531 |
+
```
|
532 |
+
|
533 |
+
### Docker
|
534 |
+
```dockerfile
|
535 |
+
FROM python:3.12-slim
|
536 |
+
COPY src-python/ /app/
|
537 |
+
WORKDIR /app
|
538 |
+
RUN pip install -r requirements.txt
|
539 |
+
EXPOSE 8080
|
540 |
+
CMD ["python", "start_server.py"]
|
541 |
+
```
|
542 |
+
|
543 |
+
### Cloud (Railway/Heroku)
|
544 |
+
```bash
|
545 |
+
# Procfile
|
546 |
+
web: cd src-python && python start_server.py
|
547 |
+
```
|
548 |
+
|
549 |
+
### Raspberry Pi (Edge)
|
550 |
+
```bash
|
551 |
+
# systemd service for autostart
|
552 |
+
sudo systemctl enable lerobot-arena
|
553 |
+
sudo systemctl start lerobot-arena
|
554 |
+
```
|
555 |
+
|
556 |
+
## 🧪 Testing & Debugging
|
557 |
+
|
558 |
+
### Unit Tests
|
559 |
+
```bash
|
560 |
+
cd src-python
|
561 |
+
pytest tests/ -v
|
562 |
+
```
|
563 |
+
|
564 |
+
### Integration Tests
|
565 |
+
```javascript
|
566 |
+
// Frontend testing
|
567 |
+
import { expect, test } from '@playwright/test';
|
568 |
+
|
569 |
+
test('robot creation and control', async ({ page }) => {
|
570 |
+
await page.goto('/');
|
571 |
+
await page.click('[data-testid="create-robot"]');
|
572 |
+
await page.click('[data-testid="connect-demo-sequences"]');
|
573 |
+
await expect(page.locator('[data-testid="robot-status"]')).toContainText('Master + Slaves');
|
574 |
+
});
|
575 |
+
```
|
576 |
+
|
577 |
+
### Debug Mode
|
578 |
+
```bash
|
579 |
+
# Enable verbose logging
|
580 |
+
export LOG_LEVEL=DEBUG
|
581 |
+
python start_server.py
|
582 |
+
|
583 |
+
# Frontend debug
|
584 |
+
export VITE_DEBUG=true
|
585 |
+
npm run dev
|
586 |
+
```
|
587 |
+
|
588 |
+
### Health Monitoring
|
589 |
+
```bash
|
590 |
+
# Check server health
|
591 |
+
curl http://localhost:8080/
|
592 |
+
|
593 |
+
# Monitor WebSocket connections
|
594 |
+
curl http://localhost:8080/api/robots
|
595 |
+
```
|
596 |
+
|
597 |
+
## 🔮 Roadmap
|
598 |
+
|
599 |
+
### v2.0 - Enhanced Control
|
600 |
+
- [ ] **Script Player Master**: Execute Python/JS scripts
|
601 |
+
- [ ] **Simulation Slave**: Physics-based simulation
|
602 |
+
- [ ] **Force Control**: Torque and compliance modes
|
603 |
+
- [ ] **Vision Integration**: Camera feeds and computer vision
|
604 |
+
|
605 |
+
### v2.1 - Enterprise Features
|
606 |
+
- [ ] **Authentication**: JWT tokens and user management
|
607 |
+
- [ ] **Multi-tenancy**: Isolated robot fleets per organization
|
608 |
+
- [ ] **Monitoring**: Prometheus metrics and Grafana dashboards
|
609 |
+
- [ ] **Recording**: Command sequences and replay
|
610 |
+
|
611 |
+
### v2.2 - Advanced Robotics
|
612 |
+
- [ ] **Path Planning**: Trajectory optimization
|
613 |
+
- [ ] **Collision Detection**: Safety in shared workspaces
|
614 |
+
- [ ] **AI Integration**: Reinforcement learning environments
|
615 |
+
- [ ] **ROS Bridge**: Integration with ROS2 ecosystem
|
616 |
+
|
617 |
+
## 🤝 Contributing
|
618 |
+
|
619 |
+
### Development Setup
|
620 |
+
```bash
|
621 |
+
# Frontend
|
622 |
+
npm install
|
623 |
+
npm run dev
|
624 |
+
|
625 |
+
# Backend
|
626 |
+
cd src-python
|
627 |
+
uv sync
|
628 |
+
python start_server.py
|
629 |
+
|
630 |
+
# Tests
|
631 |
+
npm run test
|
632 |
+
cd src-python && pytest
|
633 |
+
```
|
634 |
+
|
635 |
+
### Code Style
|
636 |
+
- **TypeScript**: ESLint + Prettier
|
637 |
+
- **Python**: Black + isort + mypy
|
638 |
+
- **Commits**: Conventional commits format
|
639 |
+
|
640 |
+
### Pull Request Process
|
641 |
+
1. Fork repository
|
642 |
+
2. Create feature branch
|
643 |
+
3. Add tests for new functionality
|
644 |
+
4. Ensure all tests pass
|
645 |
+
5. Update documentation
|
646 |
+
6. Submit PR with clear description
|
647 |
+
|
648 |
+
## 📄 License
|
649 |
+
|
650 |
+
MIT License - Feel free to use in commercial and personal projects.
|
651 |
+
|
652 |
+
---
|
653 |
+
|
654 |
+
**Built with ❤️ for the robotics community**
|
655 |
+
|
656 |
+
*LeRobot Arena bridges the gap between digital twins and physical robots, making robotics accessible to developers, researchers, and enthusiasts worldwide.*
|
src-python/pyproject.toml
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "lerobot-arena-server"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"fastapi>=0.115.12",
|
9 |
+
"uvicorn>=0.34.3",
|
10 |
+
"websockets>=15.0.1",
|
11 |
+
]
|
12 |
+
|
13 |
+
|
14 |
+
[tool.hatch.build.targets.wheel]
|
15 |
+
packages = ["src"]
|
16 |
+
|
17 |
+
[project.scripts]
|
18 |
+
lerobot-arena-server = "start_server:main"
|
19 |
+
|
20 |
+
[project.optional-dependencies]
|
21 |
+
packaging = [
|
22 |
+
"box-packager>=0.4.0",
|
23 |
+
]
|
24 |
+
|
25 |
+
[build-system]
|
26 |
+
requires = ["hatchling"]
|
27 |
+
build-backend = "hatchling.build"
|
28 |
+
|
29 |
+
[dependency-groups]
|
30 |
+
dev = [
|
31 |
+
"box-packager>=0.4.0",
|
32 |
+
]
|
33 |
+
|
34 |
+
[tool.box]
|
35 |
+
builder = "hatch"
|
36 |
+
is_gui = false
|
37 |
+
app_entry = "start_server:main"
|
38 |
+
entry_type = "spec"
|
39 |
+
python_version = "3.12"
|
40 |
+
|
41 |
+
[tool.box.env-vars]
|
42 |
+
PYAPP_UV_ENABLED = "true"
|
43 |
+
PYAPP_EXPOSE_METADATA = "false"
|
src-python/src/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
# LeRobot Arena Server Package
|
src-python/src/connection_manager.py
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import WebSocket
|
2 |
+
from typing import Dict, List, Optional
|
3 |
+
import logging
|
4 |
+
|
5 |
+
logger = logging.getLogger(__name__)
|
6 |
+
|
7 |
+
class ConnectionManager:
|
8 |
+
"""Manages WebSocket connections for masters and slaves"""
|
9 |
+
|
10 |
+
def __init__(self):
|
11 |
+
# robot_id -> websocket
|
12 |
+
self.master_connections: Dict[str, WebSocket] = {}
|
13 |
+
|
14 |
+
# robot_id -> list of websockets
|
15 |
+
self.slave_connections: Dict[str, List[WebSocket]] = {}
|
16 |
+
|
17 |
+
# connection_id -> (robot_id, websocket)
|
18 |
+
self.connection_registry: Dict[str, tuple] = {}
|
19 |
+
|
20 |
+
async def connect_master(self, connection_id: str, robot_id: str, websocket: WebSocket):
|
21 |
+
"""Connect a master to a robot"""
|
22 |
+
# Only one master per robot
|
23 |
+
if robot_id in self.master_connections:
|
24 |
+
logger.warning(f"Disconnecting existing master for robot {robot_id}")
|
25 |
+
await self.disconnect_master_by_robot(robot_id)
|
26 |
+
|
27 |
+
self.master_connections[robot_id] = websocket
|
28 |
+
self.connection_registry[connection_id] = (robot_id, websocket)
|
29 |
+
logger.info(f"Master {connection_id} connected to robot {robot_id}")
|
30 |
+
|
31 |
+
async def connect_slave(self, connection_id: str, robot_id: str, websocket: WebSocket):
|
32 |
+
"""Connect a slave to a robot"""
|
33 |
+
if robot_id not in self.slave_connections:
|
34 |
+
self.slave_connections[robot_id] = []
|
35 |
+
|
36 |
+
self.slave_connections[robot_id].append(websocket)
|
37 |
+
self.connection_registry[connection_id] = (robot_id, websocket)
|
38 |
+
logger.info(f"Slave {connection_id} connected to robot {robot_id} ({len(self.slave_connections[robot_id])} total slaves)")
|
39 |
+
|
40 |
+
async def disconnect_master(self, connection_id: str):
|
41 |
+
"""Disconnect a master connection"""
|
42 |
+
if connection_id in self.connection_registry:
|
43 |
+
robot_id, websocket = self.connection_registry[connection_id]
|
44 |
+
|
45 |
+
if robot_id in self.master_connections:
|
46 |
+
del self.master_connections[robot_id]
|
47 |
+
|
48 |
+
del self.connection_registry[connection_id]
|
49 |
+
logger.info(f"Master {connection_id} disconnected from robot {robot_id}")
|
50 |
+
|
51 |
+
async def disconnect_master_by_robot(self, robot_id: str):
|
52 |
+
"""Disconnect master by robot ID"""
|
53 |
+
if robot_id in self.master_connections:
|
54 |
+
websocket = self.master_connections[robot_id]
|
55 |
+
|
56 |
+
# Find and remove from connection registry
|
57 |
+
for conn_id, (r_id, ws) in list(self.connection_registry.items()):
|
58 |
+
if r_id == robot_id and ws == websocket:
|
59 |
+
del self.connection_registry[conn_id]
|
60 |
+
break
|
61 |
+
|
62 |
+
del self.master_connections[robot_id]
|
63 |
+
|
64 |
+
try:
|
65 |
+
await websocket.close()
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"Error closing master websocket for robot {robot_id}: {e}")
|
68 |
+
|
69 |
+
async def disconnect_slave(self, connection_id: str):
|
70 |
+
"""Disconnect a slave connection"""
|
71 |
+
if connection_id in self.connection_registry:
|
72 |
+
robot_id, websocket = self.connection_registry[connection_id]
|
73 |
+
|
74 |
+
if robot_id in self.slave_connections:
|
75 |
+
try:
|
76 |
+
self.slave_connections[robot_id].remove(websocket)
|
77 |
+
if not self.slave_connections[robot_id]: # Remove empty list
|
78 |
+
del self.slave_connections[robot_id]
|
79 |
+
except ValueError:
|
80 |
+
logger.warning(f"Slave websocket not found in connections for robot {robot_id}")
|
81 |
+
|
82 |
+
del self.connection_registry[connection_id]
|
83 |
+
logger.info(f"Slave {connection_id} disconnected from robot {robot_id}")
|
84 |
+
|
85 |
+
def get_master_connection(self, robot_id: str) -> Optional[WebSocket]:
|
86 |
+
"""Get master connection for a robot"""
|
87 |
+
return self.master_connections.get(robot_id)
|
88 |
+
|
89 |
+
def get_slave_connections(self, robot_id: str) -> List[WebSocket]:
|
90 |
+
"""Get all slave connections for a robot"""
|
91 |
+
return self.slave_connections.get(robot_id, [])
|
92 |
+
|
93 |
+
def get_connection_count(self) -> int:
|
94 |
+
"""Get total number of active connections"""
|
95 |
+
master_count = len(self.master_connections)
|
96 |
+
slave_count = sum(len(slaves) for slaves in self.slave_connections.values())
|
97 |
+
return master_count + slave_count
|
98 |
+
|
99 |
+
def get_robot_connection_info(self, robot_id: str) -> dict:
|
100 |
+
"""Get connection information for a robot"""
|
101 |
+
has_master = robot_id in self.master_connections
|
102 |
+
slave_count = len(self.slave_connections.get(robot_id, []))
|
103 |
+
|
104 |
+
return {
|
105 |
+
"robot_id": robot_id,
|
106 |
+
"has_master": has_master,
|
107 |
+
"slave_count": slave_count,
|
108 |
+
"total_connections": (1 if has_master else 0) + slave_count
|
109 |
+
}
|
110 |
+
|
111 |
+
async def cleanup_robot_connections(self, robot_id: str):
|
112 |
+
"""Clean up all connections for a robot"""
|
113 |
+
# Close master connection
|
114 |
+
if robot_id in self.master_connections:
|
115 |
+
try:
|
116 |
+
await self.master_connections[robot_id].close()
|
117 |
+
except Exception as e:
|
118 |
+
logger.error(f"Error closing master connection for robot {robot_id}: {e}")
|
119 |
+
del self.master_connections[robot_id]
|
120 |
+
|
121 |
+
# Close slave connections
|
122 |
+
if robot_id in self.slave_connections:
|
123 |
+
for websocket in self.slave_connections[robot_id]:
|
124 |
+
try:
|
125 |
+
await websocket.close()
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(f"Error closing slave connection for robot {robot_id}: {e}")
|
128 |
+
del self.slave_connections[robot_id]
|
129 |
+
|
130 |
+
# Clean up connection registry
|
131 |
+
to_remove = []
|
132 |
+
for conn_id, (r_id, _) in self.connection_registry.items():
|
133 |
+
if r_id == robot_id:
|
134 |
+
to_remove.append(conn_id)
|
135 |
+
|
136 |
+
for conn_id in to_remove:
|
137 |
+
del self.connection_registry[conn_id]
|
138 |
+
|
139 |
+
logger.info(f"Cleaned up all connections for robot {robot_id}")
|
140 |
+
|
141 |
+
def list_all_connections(self) -> dict:
|
142 |
+
"""List all active connections for debugging"""
|
143 |
+
return {
|
144 |
+
"masters": {robot_id: "connected" for robot_id in self.master_connections.keys()},
|
145 |
+
"slaves": {robot_id: len(slaves) for robot_id, slaves in self.slave_connections.items()},
|
146 |
+
"total_connections": self.get_connection_count()
|
147 |
+
}
|
src-python/src/main.py
ADDED
@@ -0,0 +1,554 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import logging
|
3 |
+
import uuid
|
4 |
+
from datetime import UTC, datetime
|
5 |
+
|
6 |
+
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
8 |
+
|
9 |
+
from .connection_manager import ConnectionManager
|
10 |
+
from .models import (
|
11 |
+
CreateRobotRequest,
|
12 |
+
Robot,
|
13 |
+
RobotStatus,
|
14 |
+
)
|
15 |
+
from .robot_manager import RobotManager
|
16 |
+
|
17 |
+
# Configure logging
|
18 |
+
logging.basicConfig(level=logging.INFO)
|
19 |
+
logger = logging.getLogger(__name__)
|
20 |
+
|
21 |
+
app = FastAPI(
|
22 |
+
title="LeRobot Arena Server",
|
23 |
+
description="WebSocket-based robot control server for master-slave architecture",
|
24 |
+
version="1.0.0",
|
25 |
+
)
|
26 |
+
|
27 |
+
# CORS middleware for web frontend
|
28 |
+
app.add_middleware(
|
29 |
+
CORSMiddleware,
|
30 |
+
allow_origins=[
|
31 |
+
"http://localhost:5173",
|
32 |
+
"http://localhost:5174",
|
33 |
+
"http://localhost:3000",
|
34 |
+
], # Add your frontend URLs
|
35 |
+
allow_credentials=True,
|
36 |
+
allow_methods=["*"],
|
37 |
+
allow_headers=["*"],
|
38 |
+
)
|
39 |
+
|
40 |
+
# Global managers
|
41 |
+
connection_manager = ConnectionManager()
|
42 |
+
robot_manager = RobotManager()
|
43 |
+
|
44 |
+
|
45 |
+
@app.on_event("startup")
|
46 |
+
async def startup_event():
|
47 |
+
logger.info("🤖 LeRobot Arena Server starting up...")
|
48 |
+
|
49 |
+
# Create some demo robots for testing
|
50 |
+
await robot_manager.create_robot("demo-arm-1", "so-arm100", "Demo Arm Robot 1")
|
51 |
+
await robot_manager.create_robot("demo-arm-2", "so-arm100", "Demo Arm Robot 2")
|
52 |
+
|
53 |
+
logger.info("✅ Server ready for robot connections!")
|
54 |
+
|
55 |
+
|
56 |
+
@app.get("/")
|
57 |
+
async def root():
|
58 |
+
return {
|
59 |
+
"message": "LeRobot Arena Server",
|
60 |
+
"version": "1.0.0",
|
61 |
+
"robots_connected": len(robot_manager.robots),
|
62 |
+
"active_connections": connection_manager.get_connection_count(),
|
63 |
+
}
|
64 |
+
|
65 |
+
|
66 |
+
# ============= ROBOT MANAGEMENT API =============
|
67 |
+
|
68 |
+
|
69 |
+
@app.get("/api/robots", response_model=list[Robot])
|
70 |
+
async def list_robots():
|
71 |
+
"""Get list of all available robots"""
|
72 |
+
robots = list(robot_manager.robots.values())
|
73 |
+
print("🔍 DEBUG: /api/robots called")
|
74 |
+
print(f"🔍 DEBUG: Found {len(robots)} robots")
|
75 |
+
for robot in robots:
|
76 |
+
print(
|
77 |
+
f"🔍 DEBUG: Robot - ID: {robot.id}, Name: {robot.name}, Type: {robot.robot_type}"
|
78 |
+
)
|
79 |
+
return robots
|
80 |
+
|
81 |
+
|
82 |
+
@app.post("/api/robots", response_model=Robot)
|
83 |
+
async def create_robot(request: CreateRobotRequest):
|
84 |
+
"""Create a new robot"""
|
85 |
+
robot_id = f"robot-{uuid.uuid4().hex[:8]}"
|
86 |
+
return await robot_manager.create_robot(
|
87 |
+
robot_id, request.robot_type, request.name or f"Robot {robot_id}"
|
88 |
+
)
|
89 |
+
|
90 |
+
|
91 |
+
@app.get("/api/robots/{robot_id}", response_model=Robot)
|
92 |
+
async def get_robot(robot_id: str):
|
93 |
+
"""Get robot details"""
|
94 |
+
robot = robot_manager.get_robot(robot_id)
|
95 |
+
if not robot:
|
96 |
+
raise HTTPException(status_code=404, detail="Robot not found")
|
97 |
+
return robot
|
98 |
+
|
99 |
+
|
100 |
+
@app.get("/api/robots/{robot_id}/status", response_model=RobotStatus)
|
101 |
+
async def get_robot_status(robot_id: str):
|
102 |
+
"""Get robot connection status"""
|
103 |
+
status = robot_manager.get_robot_status(robot_id)
|
104 |
+
if not status:
|
105 |
+
raise HTTPException(status_code=404, detail="Robot not found")
|
106 |
+
return status
|
107 |
+
|
108 |
+
|
109 |
+
@app.delete("/api/robots/{robot_id}")
|
110 |
+
async def delete_robot(robot_id: str):
|
111 |
+
"""Delete a robot"""
|
112 |
+
if not robot_manager.get_robot(robot_id):
|
113 |
+
raise HTTPException(status_code=404, detail="Robot not found")
|
114 |
+
|
115 |
+
await robot_manager.delete_robot(robot_id)
|
116 |
+
return {"message": f"Robot {robot_id} deleted"}
|
117 |
+
|
118 |
+
|
119 |
+
# ============= DEMO SEQUENCE API =============
|
120 |
+
|
121 |
+
|
122 |
+
@app.get("/api/sequences")
|
123 |
+
async def list_demo_sequences():
|
124 |
+
"""Get list of available demo sequences"""
|
125 |
+
from .models import DEMO_SEQUENCES
|
126 |
+
|
127 |
+
return [
|
128 |
+
{
|
129 |
+
"id": seq.id,
|
130 |
+
"name": seq.name,
|
131 |
+
"total_duration": seq.total_duration,
|
132 |
+
"command_count": len(seq.commands),
|
133 |
+
}
|
134 |
+
for seq in DEMO_SEQUENCES
|
135 |
+
]
|
136 |
+
|
137 |
+
|
138 |
+
@app.post("/api/robots/{robot_id}/play-sequence/{sequence_id}")
|
139 |
+
async def play_demo_sequence(robot_id: str, sequence_id: str):
|
140 |
+
"""Play a demo sequence on a robot"""
|
141 |
+
# Check if robot exists
|
142 |
+
robot = robot_manager.get_robot(robot_id)
|
143 |
+
if not robot:
|
144 |
+
raise HTTPException(status_code=404, detail="Robot not found")
|
145 |
+
|
146 |
+
# Find the sequence
|
147 |
+
from .models import DEMO_SEQUENCES
|
148 |
+
|
149 |
+
sequence = next((seq for seq in DEMO_SEQUENCES if seq.id == sequence_id), None)
|
150 |
+
if not sequence:
|
151 |
+
raise HTTPException(status_code=404, detail="Sequence not found")
|
152 |
+
|
153 |
+
# Get available connections
|
154 |
+
slave_connections = connection_manager.get_slave_connections(robot_id)
|
155 |
+
master_connection = connection_manager.get_master_connection(robot_id)
|
156 |
+
|
157 |
+
if not slave_connections and not master_connection:
|
158 |
+
raise HTTPException(
|
159 |
+
status_code=400,
|
160 |
+
detail="No connections available. Connect a master (for 3D visualization) or slave (for robot execution) to play sequences.",
|
161 |
+
)
|
162 |
+
|
163 |
+
# Send sequence to slaves and/or master
|
164 |
+
notifications_sent = 0
|
165 |
+
try:
|
166 |
+
# Send to slaves if available (physical robots)
|
167 |
+
if slave_connections:
|
168 |
+
await broadcast_to_slaves(
|
169 |
+
robot_id,
|
170 |
+
{
|
171 |
+
"type": "execute_sequence",
|
172 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
173 |
+
"data": sequence.model_dump(mode="json"),
|
174 |
+
},
|
175 |
+
)
|
176 |
+
notifications_sent += len(slave_connections)
|
177 |
+
logger.info(
|
178 |
+
f"🤖 Sent sequence '{sequence.name}' to {len(slave_connections)} slaves"
|
179 |
+
)
|
180 |
+
|
181 |
+
# Send to master if available (3D visualization)
|
182 |
+
if master_connection:
|
183 |
+
await broadcast_to_master(
|
184 |
+
robot_id,
|
185 |
+
{
|
186 |
+
"type": "play_sequence",
|
187 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
188 |
+
"data": sequence.model_dump(mode="json"),
|
189 |
+
},
|
190 |
+
)
|
191 |
+
notifications_sent += 1
|
192 |
+
logger.info(
|
193 |
+
f"🎬 Sent sequence '{sequence.name}' to master for visualization"
|
194 |
+
)
|
195 |
+
|
196 |
+
logger.info(
|
197 |
+
f"🎬 Playing sequence '{sequence.name}' on robot {robot_id} ({notifications_sent} connections)"
|
198 |
+
)
|
199 |
+
|
200 |
+
return {
|
201 |
+
"message": f"Sequence '{sequence.name}' started on robot {robot_id}",
|
202 |
+
"sequence": {
|
203 |
+
"id": sequence.id,
|
204 |
+
"name": sequence.name,
|
205 |
+
"duration": sequence.total_duration,
|
206 |
+
},
|
207 |
+
"slaves_notified": len(slave_connections),
|
208 |
+
"master_notified": 1 if master_connection else 0,
|
209 |
+
"total_notifications": notifications_sent,
|
210 |
+
}
|
211 |
+
|
212 |
+
except Exception as e:
|
213 |
+
logger.error(f"Failed to play sequence {sequence_id} on robot {robot_id}: {e}")
|
214 |
+
raise HTTPException(status_code=500, detail=f"Failed to play sequence: {e}")
|
215 |
+
|
216 |
+
|
217 |
+
@app.post("/api/robots/{robot_id}/stop-sequence")
|
218 |
+
async def stop_sequence(robot_id: str):
|
219 |
+
"""Stop any running sequence on a robot"""
|
220 |
+
# Check if robot exists
|
221 |
+
robot = robot_manager.get_robot(robot_id)
|
222 |
+
if not robot:
|
223 |
+
raise HTTPException(status_code=404, detail="Robot not found")
|
224 |
+
|
225 |
+
# Check if robot has slaves
|
226 |
+
slave_connections = connection_manager.get_slave_connections(robot_id)
|
227 |
+
if not slave_connections:
|
228 |
+
raise HTTPException(status_code=400, detail="No slave connections available")
|
229 |
+
|
230 |
+
try:
|
231 |
+
# Send stop command to all slaves
|
232 |
+
await broadcast_to_slaves(
|
233 |
+
robot_id,
|
234 |
+
{
|
235 |
+
"type": "stop_sequence",
|
236 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
237 |
+
"data": {},
|
238 |
+
},
|
239 |
+
)
|
240 |
+
|
241 |
+
logger.info(f"⏹️ Stopped sequences on robot {robot_id}")
|
242 |
+
|
243 |
+
return {
|
244 |
+
"message": f"Sequences stopped on robot {robot_id}",
|
245 |
+
"slaves_notified": len(slave_connections),
|
246 |
+
}
|
247 |
+
|
248 |
+
except Exception as e:
|
249 |
+
logger.error(f"Failed to stop sequences on robot {robot_id}: {e}")
|
250 |
+
raise HTTPException(status_code=500, detail=f"Failed to stop sequences: {e}")
|
251 |
+
|
252 |
+
|
253 |
+
# ============= WEBSOCKET ENDPOINTS =============
|
254 |
+
|
255 |
+
|
256 |
+
@app.websocket("/ws/master/{robot_id}")
|
257 |
+
async def websocket_master_endpoint(websocket: WebSocket, robot_id: str):
|
258 |
+
"""WebSocket endpoint for master connections (command sources)"""
|
259 |
+
await websocket.accept()
|
260 |
+
|
261 |
+
robot = robot_manager.get_robot(robot_id)
|
262 |
+
if not robot:
|
263 |
+
# Auto-create robot if it doesn't exist
|
264 |
+
logger.info(f"🤖 Auto-creating robot {robot_id} for master connection")
|
265 |
+
try:
|
266 |
+
robot = await robot_manager.create_robot(
|
267 |
+
robot_id, "so-arm100", f"Auto-created Robot {robot_id}"
|
268 |
+
)
|
269 |
+
except Exception as e:
|
270 |
+
logger.error(f"Failed to auto-create robot {robot_id}: {e}")
|
271 |
+
await websocket.close(code=4003, reason="Failed to create robot")
|
272 |
+
return
|
273 |
+
|
274 |
+
connection_id = f"master-{uuid.uuid4().hex[:8]}"
|
275 |
+
logger.info(f"🎮 Master connected: {connection_id} for robot {robot_id}")
|
276 |
+
|
277 |
+
try:
|
278 |
+
# Register master connection
|
279 |
+
await connection_manager.connect_master(connection_id, robot_id, websocket)
|
280 |
+
await robot_manager.set_master_connected(robot_id, connection_id)
|
281 |
+
|
282 |
+
# Send initial robot state
|
283 |
+
await websocket.send_json({
|
284 |
+
"type": "robot_state",
|
285 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
286 |
+
"data": robot.model_dump(mode="json"),
|
287 |
+
})
|
288 |
+
|
289 |
+
# Handle incoming messages
|
290 |
+
async for message in websocket.iter_json():
|
291 |
+
await handle_master_message(connection_id, robot_id, message)
|
292 |
+
|
293 |
+
except WebSocketDisconnect:
|
294 |
+
logger.info(f"🔌 Master disconnected: {connection_id}")
|
295 |
+
except Exception as e:
|
296 |
+
logger.error(f"❌ Master connection error: {e}")
|
297 |
+
finally:
|
298 |
+
await connection_manager.disconnect_master(connection_id)
|
299 |
+
await robot_manager.set_master_disconnected(robot_id)
|
300 |
+
|
301 |
+
|
302 |
+
@app.websocket("/ws/slave/{robot_id}")
|
303 |
+
async def websocket_slave_endpoint(websocket: WebSocket, robot_id: str):
|
304 |
+
"""WebSocket endpoint for slave connections (execution targets)"""
|
305 |
+
print(f"🔍 DEBUG: Slave WebSocket connection attempt for robot {robot_id}")
|
306 |
+
await websocket.accept()
|
307 |
+
|
308 |
+
robot = robot_manager.get_robot(robot_id)
|
309 |
+
if not robot:
|
310 |
+
# Auto-create robot if it doesn't exist
|
311 |
+
print(f"🔍 DEBUG: Robot {robot_id} not found, auto-creating...")
|
312 |
+
logger.info(f"🤖 Auto-creating robot {robot_id} for slave connection")
|
313 |
+
try:
|
314 |
+
robot = await robot_manager.create_robot(
|
315 |
+
robot_id, "so-arm100", f"Auto-created Robot {robot_id}"
|
316 |
+
)
|
317 |
+
print(f"🔍 DEBUG: Successfully auto-created robot {robot_id}")
|
318 |
+
except Exception as e:
|
319 |
+
print(f"🔍 DEBUG: Failed to auto-create robot {robot_id}: {e}")
|
320 |
+
logger.error(f"Failed to auto-create robot {robot_id}: {e}")
|
321 |
+
await websocket.close(code=4003, reason="Failed to create robot")
|
322 |
+
return
|
323 |
+
else:
|
324 |
+
print(f"🔍 DEBUG: Robot {robot_id} found, proceeding with connection")
|
325 |
+
|
326 |
+
connection_id = f"slave-{uuid.uuid4().hex[:8]}"
|
327 |
+
print(f"🔍 DEBUG: Generated slave connection ID: {connection_id}")
|
328 |
+
logger.info(f"🤖 Slave connected: {connection_id} for robot {robot_id}")
|
329 |
+
|
330 |
+
try:
|
331 |
+
# Register slave connection
|
332 |
+
await connection_manager.connect_slave(connection_id, robot_id, websocket)
|
333 |
+
await robot_manager.add_slave_connection(robot_id, connection_id)
|
334 |
+
print(f"🔍 DEBUG: Slave {connection_id} registered successfully")
|
335 |
+
|
336 |
+
# Send initial commands if any
|
337 |
+
await sync_slave_with_current_state(robot_id, websocket)
|
338 |
+
print(f"🔍 DEBUG: Initial state sync sent to slave {connection_id}")
|
339 |
+
|
340 |
+
# Handle incoming messages (mainly status updates)
|
341 |
+
async for message in websocket.iter_json():
|
342 |
+
print(f"🔍 DEBUG: Received message from slave {connection_id}: {message}")
|
343 |
+
await handle_slave_message(connection_id, robot_id, message)
|
344 |
+
|
345 |
+
except WebSocketDisconnect:
|
346 |
+
print(f"🔍 DEBUG: Slave {connection_id} disconnected normally")
|
347 |
+
logger.info(f"🔌 Slave disconnected: {connection_id}")
|
348 |
+
except Exception as e:
|
349 |
+
print(f"🔍 DEBUG: Slave {connection_id} connection error: {e}")
|
350 |
+
logger.error(f"❌ Slave connection error: {e}")
|
351 |
+
finally:
|
352 |
+
print(f"🔍 DEBUG: Cleaning up slave {connection_id}")
|
353 |
+
await connection_manager.disconnect_slave(connection_id)
|
354 |
+
await robot_manager.remove_slave_connection(robot_id, connection_id)
|
355 |
+
|
356 |
+
|
357 |
+
# ============= MESSAGE HANDLERS =============
|
358 |
+
|
359 |
+
|
360 |
+
async def handle_master_message(connection_id: str, robot_id: str, message: dict):
|
361 |
+
"""Handle incoming messages from master connections"""
|
362 |
+
print(f"🔍 DEBUG: Received message from master {connection_id}: {message}")
|
363 |
+
try:
|
364 |
+
msg_type = message.get("type")
|
365 |
+
print(f"🔍 DEBUG: Message type: {msg_type}")
|
366 |
+
|
367 |
+
if msg_type == "command":
|
368 |
+
print("🔍 DEBUG: Processing command message")
|
369 |
+
# Forward command to all slaves
|
370 |
+
await broadcast_to_slaves(
|
371 |
+
robot_id,
|
372 |
+
{
|
373 |
+
"type": "execute_command",
|
374 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
375 |
+
"data": message.get("data"),
|
376 |
+
},
|
377 |
+
)
|
378 |
+
|
379 |
+
elif msg_type == "sequence":
|
380 |
+
print("🔍 DEBUG: Processing sequence message")
|
381 |
+
# Forward sequence to all slaves
|
382 |
+
await broadcast_to_slaves(
|
383 |
+
robot_id,
|
384 |
+
{
|
385 |
+
"type": "execute_sequence",
|
386 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
387 |
+
"data": message.get("data"),
|
388 |
+
},
|
389 |
+
)
|
390 |
+
|
391 |
+
elif msg_type == "start_control":
|
392 |
+
print("🔍 DEBUG: Processing start_control message")
|
393 |
+
# Handle start control message (acknowledge it)
|
394 |
+
master_ws = connection_manager.get_master_connection(robot_id)
|
395 |
+
if master_ws:
|
396 |
+
await master_ws.send_json({
|
397 |
+
"type": "control_started",
|
398 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
399 |
+
"data": {"robot_id": robot_id},
|
400 |
+
})
|
401 |
+
print("🔍 DEBUG: Sent control_started acknowledgment")
|
402 |
+
|
403 |
+
elif msg_type == "stop_control":
|
404 |
+
print("🔍 DEBUG: Processing stop_control message")
|
405 |
+
# Handle stop control message
|
406 |
+
|
407 |
+
elif msg_type == "pause_control":
|
408 |
+
print("🔍 DEBUG: Processing pause_control message")
|
409 |
+
# Handle pause control message
|
410 |
+
|
411 |
+
elif msg_type == "resume_control":
|
412 |
+
print("🔍 DEBUG: Processing resume_control message")
|
413 |
+
# Handle resume control message
|
414 |
+
|
415 |
+
elif msg_type == "heartbeat":
|
416 |
+
print("🔍 DEBUG: Processing heartbeat message")
|
417 |
+
# Respond to heartbeat
|
418 |
+
master_ws = connection_manager.get_master_connection(robot_id)
|
419 |
+
if master_ws:
|
420 |
+
await master_ws.send_json({
|
421 |
+
"type": "heartbeat_ack",
|
422 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
423 |
+
})
|
424 |
+
print("🔍 DEBUG: Sent heartbeat_ack")
|
425 |
+
|
426 |
+
else:
|
427 |
+
print(f"🔍 DEBUG: Unknown message type: {msg_type}")
|
428 |
+
logger.warning(
|
429 |
+
f"Unknown message type from master {connection_id}: {msg_type}"
|
430 |
+
)
|
431 |
+
|
432 |
+
except Exception as e:
|
433 |
+
print(f"🔍 DEBUG: Error handling master message: {e}")
|
434 |
+
logger.error(f"Error handling master message: {e}")
|
435 |
+
|
436 |
+
|
437 |
+
async def handle_slave_message(connection_id: str, robot_id: str, message: dict):
|
438 |
+
"""Handle incoming messages from slave connections"""
|
439 |
+
print(f"🔍 DEBUG: Processing slave message from {connection_id}: {message}")
|
440 |
+
try:
|
441 |
+
msg_type = message.get("type")
|
442 |
+
print(f"🔍 DEBUG: Slave message type: {msg_type}")
|
443 |
+
|
444 |
+
if msg_type == "status_update":
|
445 |
+
print("🔍 DEBUG: Processing slave status_update")
|
446 |
+
# Forward status to master
|
447 |
+
await broadcast_to_master(
|
448 |
+
robot_id,
|
449 |
+
{
|
450 |
+
"type": "slave_status",
|
451 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
452 |
+
"slave_id": connection_id,
|
453 |
+
"data": message.get("data"),
|
454 |
+
},
|
455 |
+
)
|
456 |
+
|
457 |
+
elif msg_type == "joint_states":
|
458 |
+
print("🔍 DEBUG: Processing slave joint_states")
|
459 |
+
# Forward joint states to master
|
460 |
+
await broadcast_to_master(
|
461 |
+
robot_id,
|
462 |
+
{
|
463 |
+
"type": "joint_states",
|
464 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
465 |
+
"slave_id": connection_id,
|
466 |
+
"data": message.get("data"),
|
467 |
+
},
|
468 |
+
)
|
469 |
+
|
470 |
+
elif msg_type == "error":
|
471 |
+
print("🔍 DEBUG: Processing slave error")
|
472 |
+
# Forward error to master
|
473 |
+
await broadcast_to_master(
|
474 |
+
robot_id,
|
475 |
+
{
|
476 |
+
"type": "slave_error",
|
477 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
478 |
+
"slave_id": connection_id,
|
479 |
+
"data": message.get("data"),
|
480 |
+
},
|
481 |
+
)
|
482 |
+
|
483 |
+
else:
|
484 |
+
print(f"🔍 DEBUG: Unknown slave message type: {msg_type}")
|
485 |
+
logger.warning(
|
486 |
+
f"Unknown message type from slave {connection_id}: {msg_type}"
|
487 |
+
)
|
488 |
+
|
489 |
+
except Exception as e:
|
490 |
+
print(f"🔍 DEBUG: Error handling slave message: {e}")
|
491 |
+
logger.error(f"Error handling slave message: {e}")
|
492 |
+
|
493 |
+
|
494 |
+
# ============= UTILITY FUNCTIONS =============
|
495 |
+
|
496 |
+
|
497 |
+
async def broadcast_to_slaves(robot_id: str, message: dict):
|
498 |
+
"""Broadcast message to all slaves of a robot"""
|
499 |
+
slave_connections = connection_manager.get_slave_connections(robot_id)
|
500 |
+
print(f"🔍 DEBUG: Broadcasting to slaves for robot {robot_id}")
|
501 |
+
print(f"🔍 DEBUG: Found {len(slave_connections)} slave connections")
|
502 |
+
print(f"🔍 DEBUG: Message to broadcast: {message}")
|
503 |
+
if slave_connections:
|
504 |
+
logger.info(
|
505 |
+
f"📡 Broadcasting to {len(slave_connections)} slaves for robot {robot_id}"
|
506 |
+
)
|
507 |
+
results = await asyncio.gather(
|
508 |
+
*[ws.send_json(message) for ws in slave_connections], return_exceptions=True
|
509 |
+
)
|
510 |
+
print(f"🔍 DEBUG: Broadcast results: {results}")
|
511 |
+
|
512 |
+
|
513 |
+
async def broadcast_to_master(robot_id: str, message: dict):
|
514 |
+
"""Send message to master of a robot"""
|
515 |
+
master_ws = connection_manager.get_master_connection(robot_id)
|
516 |
+
print(f"🔍 DEBUG: Broadcasting to master for robot {robot_id}")
|
517 |
+
print(f"🔍 DEBUG: Master connection found: {master_ws is not None}")
|
518 |
+
print(f"🔍 DEBUG: Message to send: {message}")
|
519 |
+
if master_ws:
|
520 |
+
await master_ws.send_json(message)
|
521 |
+
print("🔍 DEBUG: Message sent to master successfully")
|
522 |
+
|
523 |
+
|
524 |
+
async def sync_slave_with_current_state(robot_id: str, websocket: WebSocket):
|
525 |
+
"""Send current robot state to newly connected slave"""
|
526 |
+
robot = robot_manager.get_robot(robot_id)
|
527 |
+
if robot:
|
528 |
+
await websocket.send_json({
|
529 |
+
"type": "sync_state",
|
530 |
+
"timestamp": datetime.now(UTC).isoformat(),
|
531 |
+
"data": robot.model_dump(mode="json"),
|
532 |
+
})
|
533 |
+
|
534 |
+
|
535 |
+
if __name__ == "__main__":
|
536 |
+
import uvicorn
|
537 |
+
|
538 |
+
"""Start the LeRobot Arena server"""
|
539 |
+
logging.basicConfig(
|
540 |
+
level=logging.INFO,
|
541 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
542 |
+
)
|
543 |
+
|
544 |
+
logger = logging.getLogger("lerobot-arena")
|
545 |
+
logger.info("🚀 Starting LeRobot Arena WebSocket Server...")
|
546 |
+
|
547 |
+
# Start the server
|
548 |
+
uvicorn.run(
|
549 |
+
app,
|
550 |
+
host="0.0.0.0",
|
551 |
+
port=8080,
|
552 |
+
log_level="info",
|
553 |
+
reload=False, # Auto-reload on code changes
|
554 |
+
)
|
src-python/src/models.py
ADDED
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
from typing import Dict, List, Optional, Any, Literal
|
3 |
+
from datetime import datetime
|
4 |
+
from enum import Enum
|
5 |
+
|
6 |
+
# ============= ROBOT COMMAND MODELS =============
|
7 |
+
|
8 |
+
class JointUpdate(BaseModel):
|
9 |
+
name: str
|
10 |
+
value: float
|
11 |
+
speed: Optional[float] = None
|
12 |
+
|
13 |
+
class RobotCommand(BaseModel):
|
14 |
+
timestamp: int
|
15 |
+
joints: List[JointUpdate]
|
16 |
+
duration: Optional[int] = None
|
17 |
+
metadata: Optional[Dict[str, Any]] = None
|
18 |
+
|
19 |
+
class CommandSequence(BaseModel):
|
20 |
+
id: str
|
21 |
+
name: str
|
22 |
+
commands: List[RobotCommand]
|
23 |
+
loop: Optional[bool] = False
|
24 |
+
total_duration: int
|
25 |
+
|
26 |
+
# ============= COMMUNICATION MODELS =============
|
27 |
+
|
28 |
+
class ControlMessage(BaseModel):
|
29 |
+
type: Literal["command", "sequence", "heartbeat", "status_request"]
|
30 |
+
timestamp: str
|
31 |
+
data: Optional[Dict[str, Any]] = None
|
32 |
+
|
33 |
+
class StatusMessage(BaseModel):
|
34 |
+
type: Literal["status", "joint_states", "error", "heartbeat_ack", "slave_status"]
|
35 |
+
timestamp: str
|
36 |
+
slave_id: Optional[str] = None
|
37 |
+
data: Optional[Dict[str, Any]] = None
|
38 |
+
|
39 |
+
# ============= ROBOT MODELS =============
|
40 |
+
|
41 |
+
class JointLimits(BaseModel):
|
42 |
+
lower: Optional[float] = None
|
43 |
+
upper: Optional[float] = None
|
44 |
+
velocity: Optional[float] = None
|
45 |
+
effort: Optional[float] = None
|
46 |
+
|
47 |
+
class DriverJointState(BaseModel):
|
48 |
+
name: str
|
49 |
+
servo_id: int
|
50 |
+
type: Literal["revolute", "continuous"]
|
51 |
+
virtual_value: float
|
52 |
+
real_value: Optional[float] = None
|
53 |
+
limits: Optional[JointLimits] = None
|
54 |
+
|
55 |
+
class ConnectionStatus(BaseModel):
|
56 |
+
is_connected: bool
|
57 |
+
last_connected: Optional[datetime] = None
|
58 |
+
error: Optional[str] = None
|
59 |
+
|
60 |
+
# ============= ROBOT MANAGEMENT =============
|
61 |
+
|
62 |
+
class Robot(BaseModel):
|
63 |
+
id: str
|
64 |
+
name: str
|
65 |
+
robot_type: str
|
66 |
+
created_at: datetime
|
67 |
+
joints: List[DriverJointState] = []
|
68 |
+
|
69 |
+
# Connection state
|
70 |
+
master_connected: bool = False
|
71 |
+
master_connection_id: Optional[str] = None
|
72 |
+
slave_connections: List[str] = []
|
73 |
+
|
74 |
+
# Control state
|
75 |
+
last_command_source: Literal["master", "manual", "none"] = "none"
|
76 |
+
last_command_time: Optional[datetime] = None
|
77 |
+
|
78 |
+
class RobotStatus(BaseModel):
|
79 |
+
robot_id: str
|
80 |
+
master_connected: bool
|
81 |
+
slave_count: int
|
82 |
+
last_command_source: str
|
83 |
+
last_command_time: Optional[datetime]
|
84 |
+
last_seen: datetime
|
85 |
+
|
86 |
+
class CreateRobotRequest(BaseModel):
|
87 |
+
name: Optional[str] = None
|
88 |
+
robot_type: str = "so-arm100"
|
89 |
+
|
90 |
+
# ============= DEMO SEQUENCES =============
|
91 |
+
|
92 |
+
DEMO_SEQUENCES = [
|
93 |
+
CommandSequence(
|
94 |
+
id="gentle-wave",
|
95 |
+
name="Gentle Wave Pattern",
|
96 |
+
total_duration=6000,
|
97 |
+
commands=[
|
98 |
+
RobotCommand(
|
99 |
+
timestamp=0,
|
100 |
+
joints=[
|
101 |
+
JointUpdate(name="Rotation", value=-10),
|
102 |
+
JointUpdate(name="Pitch", value=8),
|
103 |
+
JointUpdate(name="Elbow", value=-12)
|
104 |
+
],
|
105 |
+
duration=2000
|
106 |
+
),
|
107 |
+
RobotCommand(
|
108 |
+
timestamp=2000,
|
109 |
+
joints=[JointUpdate(name="Wrist_Roll", value=10)],
|
110 |
+
duration=1000
|
111 |
+
),
|
112 |
+
RobotCommand(
|
113 |
+
timestamp=3000,
|
114 |
+
joints=[JointUpdate(name="Wrist_Roll", value=-10)],
|
115 |
+
duration=1000
|
116 |
+
),
|
117 |
+
RobotCommand(
|
118 |
+
timestamp=4000,
|
119 |
+
joints=[
|
120 |
+
JointUpdate(name="Wrist_Roll", value=0),
|
121 |
+
JointUpdate(name="Rotation", value=0),
|
122 |
+
JointUpdate(name="Pitch", value=0),
|
123 |
+
JointUpdate(name="Elbow", value=0)
|
124 |
+
],
|
125 |
+
duration=2000
|
126 |
+
)
|
127 |
+
]
|
128 |
+
),
|
129 |
+
CommandSequence(
|
130 |
+
id="small-scan",
|
131 |
+
name="Small Scanning Pattern",
|
132 |
+
total_duration=8000,
|
133 |
+
commands=[
|
134 |
+
RobotCommand(
|
135 |
+
timestamp=0,
|
136 |
+
joints=[
|
137 |
+
JointUpdate(name="Rotation", value=-15),
|
138 |
+
JointUpdate(name="Pitch", value=10)
|
139 |
+
],
|
140 |
+
duration=2000
|
141 |
+
),
|
142 |
+
RobotCommand(
|
143 |
+
timestamp=2000,
|
144 |
+
joints=[JointUpdate(name="Rotation", value=15)],
|
145 |
+
duration=3000
|
146 |
+
),
|
147 |
+
RobotCommand(
|
148 |
+
timestamp=5000,
|
149 |
+
joints=[
|
150 |
+
JointUpdate(name="Rotation", value=0),
|
151 |
+
JointUpdate(name="Pitch", value=0)
|
152 |
+
],
|
153 |
+
duration=3000
|
154 |
+
)
|
155 |
+
]
|
156 |
+
),
|
157 |
+
CommandSequence(
|
158 |
+
id="tiny-flex",
|
159 |
+
name="Tiny Flex Pattern",
|
160 |
+
total_duration=8000,
|
161 |
+
commands=[
|
162 |
+
RobotCommand(
|
163 |
+
timestamp=0,
|
164 |
+
joints=[
|
165 |
+
JointUpdate(name="Elbow", value=-15),
|
166 |
+
JointUpdate(name="Wrist_Pitch", value=8)
|
167 |
+
],
|
168 |
+
duration=2000
|
169 |
+
),
|
170 |
+
RobotCommand(
|
171 |
+
timestamp=2000,
|
172 |
+
joints=[JointUpdate(name="Jaw", value=8)],
|
173 |
+
duration=1000
|
174 |
+
),
|
175 |
+
RobotCommand(
|
176 |
+
timestamp=3000,
|
177 |
+
joints=[JointUpdate(name="Elbow", value=-25)],
|
178 |
+
duration=2000
|
179 |
+
),
|
180 |
+
RobotCommand(
|
181 |
+
timestamp=5000,
|
182 |
+
joints=[
|
183 |
+
JointUpdate(name="Jaw", value=0),
|
184 |
+
JointUpdate(name="Elbow", value=0),
|
185 |
+
JointUpdate(name="Wrist_Pitch", value=0)
|
186 |
+
],
|
187 |
+
duration=3000
|
188 |
+
)
|
189 |
+
]
|
190 |
+
)
|
191 |
+
]
|
src-python/src/robot_manager.py
ADDED
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Optional, List
|
2 |
+
from datetime import datetime, timezone
|
3 |
+
import logging
|
4 |
+
|
5 |
+
from .models import Robot, RobotStatus, DriverJointState
|
6 |
+
|
7 |
+
logger = logging.getLogger(__name__)
|
8 |
+
|
9 |
+
class RobotManager:
|
10 |
+
"""Manages robot lifecycle and state"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
self.robots: Dict[str, Robot] = {}
|
14 |
+
|
15 |
+
async def create_robot(self, robot_id: str, robot_type: str, name: str) -> Robot:
|
16 |
+
"""Create a new robot"""
|
17 |
+
if robot_id in self.robots:
|
18 |
+
raise ValueError(f"Robot {robot_id} already exists")
|
19 |
+
|
20 |
+
# Create demo joint configuration for so-arm100
|
21 |
+
joints = self._create_demo_joints(robot_type)
|
22 |
+
|
23 |
+
robot = Robot(
|
24 |
+
id=robot_id,
|
25 |
+
name=name,
|
26 |
+
robot_type=robot_type,
|
27 |
+
created_at=datetime.now(timezone.utc),
|
28 |
+
joints=joints
|
29 |
+
)
|
30 |
+
|
31 |
+
self.robots[robot_id] = robot
|
32 |
+
logger.info(f"Created robot {robot_id} ({robot_type}) with {len(joints)} joints")
|
33 |
+
|
34 |
+
return robot
|
35 |
+
|
36 |
+
def get_robot(self, robot_id: str) -> Optional[Robot]:
|
37 |
+
"""Get robot by ID"""
|
38 |
+
return self.robots.get(robot_id)
|
39 |
+
|
40 |
+
async def delete_robot(self, robot_id: str):
|
41 |
+
"""Delete a robot"""
|
42 |
+
if robot_id in self.robots:
|
43 |
+
del self.robots[robot_id]
|
44 |
+
logger.info(f"Deleted robot {robot_id}")
|
45 |
+
|
46 |
+
async def set_master_connected(self, robot_id: str, connection_id: str):
|
47 |
+
"""Mark robot as having a master connection"""
|
48 |
+
robot = self.robots.get(robot_id)
|
49 |
+
if robot:
|
50 |
+
robot.master_connected = True
|
51 |
+
robot.master_connection_id = connection_id
|
52 |
+
robot.last_command_source = "master"
|
53 |
+
robot.last_command_time = datetime.now(timezone.utc)
|
54 |
+
|
55 |
+
async def set_master_disconnected(self, robot_id: str):
|
56 |
+
"""Mark robot as having no master connection"""
|
57 |
+
robot = self.robots.get(robot_id)
|
58 |
+
if robot:
|
59 |
+
robot.master_connected = False
|
60 |
+
robot.master_connection_id = None
|
61 |
+
robot.last_command_source = "none"
|
62 |
+
|
63 |
+
async def add_slave_connection(self, robot_id: str, connection_id: str):
|
64 |
+
"""Add a slave connection to robot"""
|
65 |
+
robot = self.robots.get(robot_id)
|
66 |
+
if robot:
|
67 |
+
if connection_id not in robot.slave_connections:
|
68 |
+
robot.slave_connections.append(connection_id)
|
69 |
+
logger.info(f"Added slave {connection_id} to robot {robot_id} ({len(robot.slave_connections)} total)")
|
70 |
+
|
71 |
+
async def remove_slave_connection(self, robot_id: str, connection_id: str):
|
72 |
+
"""Remove a slave connection from robot"""
|
73 |
+
robot = self.robots.get(robot_id)
|
74 |
+
if robot:
|
75 |
+
try:
|
76 |
+
robot.slave_connections.remove(connection_id)
|
77 |
+
logger.info(f"Removed slave {connection_id} from robot {robot_id} ({len(robot.slave_connections)} remaining)")
|
78 |
+
except ValueError:
|
79 |
+
logger.warning(f"Slave {connection_id} not found in robot {robot_id} connections")
|
80 |
+
|
81 |
+
def get_robot_status(self, robot_id: str) -> Optional[RobotStatus]:
|
82 |
+
"""Get robot connection status"""
|
83 |
+
robot = self.robots.get(robot_id)
|
84 |
+
if not robot:
|
85 |
+
return None
|
86 |
+
|
87 |
+
return RobotStatus(
|
88 |
+
robot_id=robot_id,
|
89 |
+
master_connected=robot.master_connected,
|
90 |
+
slave_count=len(robot.slave_connections),
|
91 |
+
last_command_source=robot.last_command_source,
|
92 |
+
last_command_time=robot.last_command_time,
|
93 |
+
last_seen=datetime.now(timezone.utc)
|
94 |
+
)
|
95 |
+
|
96 |
+
def list_robots_with_masters(self) -> List[Robot]:
|
97 |
+
"""Get all robots that have master connections"""
|
98 |
+
return [robot for robot in self.robots.values() if robot.master_connected]
|
99 |
+
|
100 |
+
def list_robots_with_slaves(self) -> List[Robot]:
|
101 |
+
"""Get all robots that have slave connections"""
|
102 |
+
return [robot for robot in self.robots.values() if robot.slave_connections]
|
103 |
+
|
104 |
+
def _create_demo_joints(self, robot_type: str) -> List[DriverJointState]:
|
105 |
+
"""Create demo joint configuration based on robot type"""
|
106 |
+
if robot_type == "so-arm100":
|
107 |
+
return [
|
108 |
+
DriverJointState(
|
109 |
+
name="Rotation",
|
110 |
+
servo_id=1,
|
111 |
+
type="revolute",
|
112 |
+
virtual_value=0.0,
|
113 |
+
real_value=0.0
|
114 |
+
),
|
115 |
+
DriverJointState(
|
116 |
+
name="Pitch",
|
117 |
+
servo_id=2,
|
118 |
+
type="revolute",
|
119 |
+
virtual_value=0.0,
|
120 |
+
real_value=0.0
|
121 |
+
),
|
122 |
+
DriverJointState(
|
123 |
+
name="Elbow",
|
124 |
+
servo_id=3,
|
125 |
+
type="revolute",
|
126 |
+
virtual_value=0.0,
|
127 |
+
real_value=0.0
|
128 |
+
),
|
129 |
+
DriverJointState(
|
130 |
+
name="Wrist_Pitch",
|
131 |
+
servo_id=4,
|
132 |
+
type="revolute",
|
133 |
+
virtual_value=0.0,
|
134 |
+
real_value=0.0
|
135 |
+
),
|
136 |
+
DriverJointState(
|
137 |
+
name="Wrist_Roll",
|
138 |
+
servo_id=5,
|
139 |
+
type="revolute",
|
140 |
+
virtual_value=0.0,
|
141 |
+
real_value=0.0
|
142 |
+
),
|
143 |
+
DriverJointState(
|
144 |
+
name="Jaw",
|
145 |
+
servo_id=6,
|
146 |
+
type="revolute",
|
147 |
+
virtual_value=0.0,
|
148 |
+
real_value=0.0
|
149 |
+
)
|
150 |
+
]
|
151 |
+
else:
|
152 |
+
# Default generic robot
|
153 |
+
return [
|
154 |
+
DriverJointState(
|
155 |
+
name="joint_1",
|
156 |
+
servo_id=1,
|
157 |
+
type="revolute",
|
158 |
+
virtual_value=0.0,
|
159 |
+
real_value=0.0
|
160 |
+
),
|
161 |
+
DriverJointState(
|
162 |
+
name="joint_2",
|
163 |
+
servo_id=2,
|
164 |
+
type="revolute",
|
165 |
+
virtual_value=0.0,
|
166 |
+
real_value=0.0
|
167 |
+
)
|
168 |
+
]
|
src-python/start_server.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
LeRobot Arena WebSocket Server
|
4 |
+
|
5 |
+
Run with: python start_server.py
|
6 |
+
"""
|
7 |
+
|
8 |
+
import uvicorn
|
9 |
+
import logging
|
10 |
+
import sys
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# Add src to path
|
14 |
+
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
15 |
+
|
16 |
+
from src.main import app
|
17 |
+
|
18 |
+
def main():
|
19 |
+
"""Start the LeRobot Arena server"""
|
20 |
+
logging.basicConfig(
|
21 |
+
level=logging.INFO,
|
22 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
23 |
+
)
|
24 |
+
|
25 |
+
logger = logging.getLogger("lerobot-arena")
|
26 |
+
logger.info("🚀 Starting LeRobot Arena WebSocket Server...")
|
27 |
+
|
28 |
+
# Start the server
|
29 |
+
uvicorn.run(
|
30 |
+
app,
|
31 |
+
host="0.0.0.0",
|
32 |
+
port=8080,
|
33 |
+
log_level="info",
|
34 |
+
reload=False # Auto-reload on code changes
|
35 |
+
)
|
36 |
+
|
37 |
+
if __name__ == "__main__":
|
38 |
+
main()
|
src-python/uv.lock
ADDED
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version = 1
|
2 |
+
revision = 2
|
3 |
+
requires-python = ">=3.12"
|
4 |
+
|
5 |
+
[[package]]
|
6 |
+
name = "annotated-types"
|
7 |
+
version = "0.7.0"
|
8 |
+
source = { registry = "https://pypi.org/simple" }
|
9 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
10 |
+
wheels = [
|
11 |
+
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
12 |
+
]
|
13 |
+
|
14 |
+
[[package]]
|
15 |
+
name = "anyio"
|
16 |
+
version = "4.9.0"
|
17 |
+
source = { registry = "https://pypi.org/simple" }
|
18 |
+
dependencies = [
|
19 |
+
{ name = "idna" },
|
20 |
+
{ name = "sniffio" },
|
21 |
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
22 |
+
]
|
23 |
+
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
24 |
+
wheels = [
|
25 |
+
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
26 |
+
]
|
27 |
+
|
28 |
+
[[package]]
|
29 |
+
name = "click"
|
30 |
+
version = "8.2.1"
|
31 |
+
source = { registry = "https://pypi.org/simple" }
|
32 |
+
dependencies = [
|
33 |
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
34 |
+
]
|
35 |
+
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
36 |
+
wheels = [
|
37 |
+
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
38 |
+
]
|
39 |
+
|
40 |
+
[[package]]
|
41 |
+
name = "colorama"
|
42 |
+
version = "0.4.6"
|
43 |
+
source = { registry = "https://pypi.org/simple" }
|
44 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
45 |
+
wheels = [
|
46 |
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
47 |
+
]
|
48 |
+
|
49 |
+
[[package]]
|
50 |
+
name = "fastapi"
|
51 |
+
version = "0.115.12"
|
52 |
+
source = { registry = "https://pypi.org/simple" }
|
53 |
+
dependencies = [
|
54 |
+
{ name = "pydantic" },
|
55 |
+
{ name = "starlette" },
|
56 |
+
{ name = "typing-extensions" },
|
57 |
+
]
|
58 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
|
59 |
+
wheels = [
|
60 |
+
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
|
61 |
+
]
|
62 |
+
|
63 |
+
[[package]]
|
64 |
+
name = "h11"
|
65 |
+
version = "0.16.0"
|
66 |
+
source = { registry = "https://pypi.org/simple" }
|
67 |
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
68 |
+
wheels = [
|
69 |
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
70 |
+
]
|
71 |
+
|
72 |
+
[[package]]
|
73 |
+
name = "idna"
|
74 |
+
version = "3.10"
|
75 |
+
source = { registry = "https://pypi.org/simple" }
|
76 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
77 |
+
wheels = [
|
78 |
+
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
79 |
+
]
|
80 |
+
|
81 |
+
[[package]]
|
82 |
+
name = "lerobot-arena-server"
|
83 |
+
version = "0.1.0"
|
84 |
+
source = { virtual = "." }
|
85 |
+
dependencies = [
|
86 |
+
{ name = "fastapi" },
|
87 |
+
{ name = "uvicorn" },
|
88 |
+
{ name = "websockets" },
|
89 |
+
]
|
90 |
+
|
91 |
+
[package.metadata]
|
92 |
+
requires-dist = [
|
93 |
+
{ name = "fastapi", specifier = ">=0.115.12" },
|
94 |
+
{ name = "uvicorn", specifier = ">=0.34.3" },
|
95 |
+
{ name = "websockets", specifier = ">=15.0.1" },
|
96 |
+
]
|
97 |
+
|
98 |
+
[[package]]
|
99 |
+
name = "pydantic"
|
100 |
+
version = "2.11.5"
|
101 |
+
source = { registry = "https://pypi.org/simple" }
|
102 |
+
dependencies = [
|
103 |
+
{ name = "annotated-types" },
|
104 |
+
{ name = "pydantic-core" },
|
105 |
+
{ name = "typing-extensions" },
|
106 |
+
{ name = "typing-inspection" },
|
107 |
+
]
|
108 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
|
109 |
+
wheels = [
|
110 |
+
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
|
111 |
+
]
|
112 |
+
|
113 |
+
[[package]]
|
114 |
+
name = "pydantic-core"
|
115 |
+
version = "2.33.2"
|
116 |
+
source = { registry = "https://pypi.org/simple" }
|
117 |
+
dependencies = [
|
118 |
+
{ name = "typing-extensions" },
|
119 |
+
]
|
120 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
121 |
+
wheels = [
|
122 |
+
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
123 |
+
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
124 |
+
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
125 |
+
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
126 |
+
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
127 |
+
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
128 |
+
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
129 |
+
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
130 |
+
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
131 |
+
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
132 |
+
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
133 |
+
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
134 |
+
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
135 |
+
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
136 |
+
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
137 |
+
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
138 |
+
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
139 |
+
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
140 |
+
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
141 |
+
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
142 |
+
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
143 |
+
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
144 |
+
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
145 |
+
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
146 |
+
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
147 |
+
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
148 |
+
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
149 |
+
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
150 |
+
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
151 |
+
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
152 |
+
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
153 |
+
]
|
154 |
+
|
155 |
+
[[package]]
|
156 |
+
name = "sniffio"
|
157 |
+
version = "1.3.1"
|
158 |
+
source = { registry = "https://pypi.org/simple" }
|
159 |
+
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
160 |
+
wheels = [
|
161 |
+
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
162 |
+
]
|
163 |
+
|
164 |
+
[[package]]
|
165 |
+
name = "starlette"
|
166 |
+
version = "0.46.2"
|
167 |
+
source = { registry = "https://pypi.org/simple" }
|
168 |
+
dependencies = [
|
169 |
+
{ name = "anyio" },
|
170 |
+
]
|
171 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
|
172 |
+
wheels = [
|
173 |
+
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
|
174 |
+
]
|
175 |
+
|
176 |
+
[[package]]
|
177 |
+
name = "typing-extensions"
|
178 |
+
version = "4.13.2"
|
179 |
+
source = { registry = "https://pypi.org/simple" }
|
180 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
181 |
+
wheels = [
|
182 |
+
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
183 |
+
]
|
184 |
+
|
185 |
+
[[package]]
|
186 |
+
name = "typing-inspection"
|
187 |
+
version = "0.4.1"
|
188 |
+
source = { registry = "https://pypi.org/simple" }
|
189 |
+
dependencies = [
|
190 |
+
{ name = "typing-extensions" },
|
191 |
+
]
|
192 |
+
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
193 |
+
wheels = [
|
194 |
+
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
195 |
+
]
|
196 |
+
|
197 |
+
[[package]]
|
198 |
+
name = "uvicorn"
|
199 |
+
version = "0.34.3"
|
200 |
+
source = { registry = "https://pypi.org/simple" }
|
201 |
+
dependencies = [
|
202 |
+
{ name = "click" },
|
203 |
+
{ name = "h11" },
|
204 |
+
]
|
205 |
+
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
206 |
+
wheels = [
|
207 |
+
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
208 |
+
]
|
209 |
+
|
210 |
+
[[package]]
|
211 |
+
name = "websockets"
|
212 |
+
version = "15.0.1"
|
213 |
+
source = { registry = "https://pypi.org/simple" }
|
214 |
+
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
215 |
+
wheels = [
|
216 |
+
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
217 |
+
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
218 |
+
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
219 |
+
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
220 |
+
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
221 |
+
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
222 |
+
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
223 |
+
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
224 |
+
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
225 |
+
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
226 |
+
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
227 |
+
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
228 |
+
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
229 |
+
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
230 |
+
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
231 |
+
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
232 |
+
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
233 |
+
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
234 |
+
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
235 |
+
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
236 |
+
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
237 |
+
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
238 |
+
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
239 |
+
]
|
src/app.css
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
@import 'tailwindcss';
|
src/app.d.ts
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// See https://svelte.dev/docs/kit/types#app.d.ts
|
2 |
+
// for information about these interfaces
|
3 |
+
declare global {
|
4 |
+
namespace App {
|
5 |
+
// interface Error {}
|
6 |
+
// interface Locals {}
|
7 |
+
// interface PageData {}
|
8 |
+
// interface PageState {}
|
9 |
+
// interface Platform {}
|
10 |
+
}
|
11 |
+
}
|
12 |
+
|
13 |
+
export {};
|
src/app.html
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7 |
+
%sveltekit.head%
|
8 |
+
</head>
|
9 |
+
<body data-sveltekit-preload-data="hover">
|
10 |
+
<div style="display: contents">%sveltekit.body%</div>
|
11 |
+
</body>
|
12 |
+
</html>
|
src/lib/components/panel/ControlPanel.svelte
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { settings } from "$lib/configs/settings.svelte";
|
3 |
+
import RobotControlPanel from "./RobotControlPanel.svelte";
|
4 |
+
|
5 |
+
interface Props {}
|
6 |
+
|
7 |
+
let {}: Props = $props();
|
8 |
+
</script>
|
9 |
+
|
10 |
+
<div class="space-y-6">
|
11 |
+
<!-- Robot Control Section -->
|
12 |
+
<RobotControlPanel />
|
13 |
+
|
14 |
+
</div>
|
15 |
+
|
16 |
+
<style>
|
17 |
+
.slider::-webkit-slider-thumb {
|
18 |
+
appearance: none;
|
19 |
+
height: 18px;
|
20 |
+
width: 18px;
|
21 |
+
border-radius: 50%;
|
22 |
+
background: #38bdf8; /* sky-400 */
|
23 |
+
cursor: pointer;
|
24 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
25 |
+
border: 2px solid #0f172a; /* slate-900 */
|
26 |
+
}
|
27 |
+
|
28 |
+
.slider::-moz-range-thumb {
|
29 |
+
height: 18px;
|
30 |
+
width: 18px;
|
31 |
+
border-radius: 50%;
|
32 |
+
background: #38bdf8; /* sky-400 */
|
33 |
+
cursor: pointer;
|
34 |
+
border: 2px solid #0f172a; /* slate-900 */
|
35 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
36 |
+
}
|
37 |
+
|
38 |
+
.slider::-webkit-slider-track {
|
39 |
+
background: #475569; /* slate-600 */
|
40 |
+
border-radius: 4px;
|
41 |
+
height: 8px;
|
42 |
+
}
|
43 |
+
|
44 |
+
.slider::-moz-range-track {
|
45 |
+
background: #475569; /* slate-600 */
|
46 |
+
border-radius: 4px;
|
47 |
+
height: 8px;
|
48 |
+
border: none;
|
49 |
+
}
|
50 |
+
|
51 |
+
.slider:hover::-webkit-slider-thumb {
|
52 |
+
background: #0ea5e9; /* sky-500 */
|
53 |
+
transform: scale(1.1);
|
54 |
+
}
|
55 |
+
|
56 |
+
.slider:hover::-moz-range-thumb {
|
57 |
+
background: #0ea5e9; /* sky-500 */
|
58 |
+
transform: scale(1.1);
|
59 |
+
}
|
60 |
+
</style>
|
src/lib/components/panel/Panels.svelte
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import ControlPanel from "./ControlPanel.svelte";
|
3 |
+
import SettingsPanel from "./SettingsPanel.svelte";
|
4 |
+
|
5 |
+
type Tab = 'controls' | 'settings';
|
6 |
+
|
7 |
+
interface Props {
|
8 |
+
activeTab?: Tab;
|
9 |
+
isOpen?: boolean;
|
10 |
+
}
|
11 |
+
|
12 |
+
let { activeTab = $bindable("controls"), isOpen = $bindable(false) }: Props = $props();
|
13 |
+
</script>
|
14 |
+
|
15 |
+
{#if isOpen}
|
16 |
+
<div class="absolute bottom-5 left-5 z-50">
|
17 |
+
<button
|
18 |
+
onclick={() => (isOpen = false)}
|
19 |
+
class="rounded bg-gray-700 px-3 py-1.5 text-sm text-white transition-colors hover:bg-gray-600"
|
20 |
+
>
|
21 |
+
Hide Controls
|
22 |
+
</button>
|
23 |
+
</div>
|
24 |
+
{:else}
|
25 |
+
<div
|
26 |
+
class="absolute top-0 right-0 h-screen w-80 overflow-y-auto border-l border-slate-600 bg-gradient-to-b from-slate-700 to-slate-800 text-white shadow-2xl"
|
27 |
+
>
|
28 |
+
<div
|
29 |
+
class="flex flex-wrap items-center justify-between gap-2 border-b border-slate-600 bg-slate-700/80 p-6 backdrop-blur-sm"
|
30 |
+
>
|
31 |
+
<h2 class="m-0 text-xl font-semibold text-slate-100">Robot Controls</h2>
|
32 |
+
<div class="flex flex-wrap items-center gap-2">
|
33 |
+
<span class="rounded-xl bg-sky-400 px-3 py-1 text-sm font-medium text-slate-900"
|
34 |
+
>Robot</span
|
35 |
+
>
|
36 |
+
</div>
|
37 |
+
<button
|
38 |
+
onclick={() => (isOpen = true)}
|
39 |
+
class="ml-2 rounded-full px-2 text-xl transition-colors hover:bg-slate-800"
|
40 |
+
title="Collapse"
|
41 |
+
>
|
42 |
+
×
|
43 |
+
</button>
|
44 |
+
</div>
|
45 |
+
|
46 |
+
<!-- Tab Navigation (only show if custom URDF is enabled) -->
|
47 |
+
<div class="flex border-b border-slate-600">
|
48 |
+
<button
|
49 |
+
class="flex-1 cursor-pointer border-none bg-transparent px-4 py-3 text-sm text-slate-300 transition-all hover:bg-slate-700/50 {activeTab ===
|
50 |
+
'controls'
|
51 |
+
? 'border-b-2 border-sky-400 bg-sky-400/10 text-slate-100'
|
52 |
+
: ''}"
|
53 |
+
onclick={() => (activeTab = 'controls')}
|
54 |
+
>
|
55 |
+
Controls
|
56 |
+
</button>
|
57 |
+
<button
|
58 |
+
class="flex-1 cursor-pointer border-none bg-transparent px-4 py-3 text-sm text-slate-300 transition-all hover:bg-slate-700/50 {activeTab ===
|
59 |
+
'settings'
|
60 |
+
? 'border-b-2 border-sky-400 bg-sky-400/10 text-slate-100'
|
61 |
+
: ''}"
|
62 |
+
onclick={() => (activeTab = 'settings')}
|
63 |
+
>
|
64 |
+
Settings
|
65 |
+
</button>
|
66 |
+
</div>
|
67 |
+
|
68 |
+
<div class="p-4">
|
69 |
+
{#if activeTab === 'controls'}
|
70 |
+
<ControlPanel />
|
71 |
+
{:else if activeTab === 'settings'}
|
72 |
+
<SettingsPanel />
|
73 |
+
{/if}
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
{/if}
|
77 |
+
|
78 |
+
|
src/lib/components/panel/RobotControlPanel.svelte
ADDED
@@ -0,0 +1,649 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { robotManager } from "$lib/robot/RobotManager.svelte";
|
3 |
+
import { robotUrdfConfigMap } from "$lib/configs/robotUrdfConfig";
|
4 |
+
import type { Robot } from "$lib/robot/Robot.svelte";
|
5 |
+
|
6 |
+
interface Props {}
|
7 |
+
|
8 |
+
let {}: Props = $props();
|
9 |
+
|
10 |
+
// Local state
|
11 |
+
let selectedRobotType = $state("so-arm100");
|
12 |
+
let isCreating = $state(false);
|
13 |
+
let error = $state<string | undefined>(undefined);
|
14 |
+
|
15 |
+
// Robot selection modal state
|
16 |
+
let showRobotSelectionModal = $state(false);
|
17 |
+
let availableServerRobots = $state<{ id: string; name: string; robot_type: string }[]>([]);
|
18 |
+
let selectedServerRobotId = $state<string>("");
|
19 |
+
let pendingLocalRobot: Robot | null = $state(null);
|
20 |
+
|
21 |
+
// Reactive values
|
22 |
+
const robots = $derived(robotManager.robots);
|
23 |
+
const robotsWithSlaves = $derived(robotManager.robotsWithSlaves.length);
|
24 |
+
const robotsWithMaster = $derived(robotManager.robotsWithMaster.length);
|
25 |
+
|
26 |
+
async function createRobot() {
|
27 |
+
if (isCreating) return;
|
28 |
+
|
29 |
+
console.log('Creating robot...');
|
30 |
+
isCreating = true;
|
31 |
+
error = undefined;
|
32 |
+
|
33 |
+
try {
|
34 |
+
const urdfConfig = robotUrdfConfigMap[selectedRobotType];
|
35 |
+
if (!urdfConfig) {
|
36 |
+
throw new Error(`Unknown robot type: ${selectedRobotType}`);
|
37 |
+
}
|
38 |
+
|
39 |
+
const robotId = `robot-${Date.now()}`;
|
40 |
+
console.log('Creating robot with ID:', robotId, 'and config:', urdfConfig);
|
41 |
+
const robot = await robotManager.createRobot(robotId, urdfConfig);
|
42 |
+
console.log('Robot created successfully:', robot);
|
43 |
+
|
44 |
+
} catch (err) {
|
45 |
+
error = `Failed to create robot: ${err}`;
|
46 |
+
console.error('Robot creation failed:', err);
|
47 |
+
} finally {
|
48 |
+
isCreating = false;
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
// Master connection functions
|
53 |
+
async function connectMockSequenceMaster(robot: Robot) {
|
54 |
+
try {
|
55 |
+
await robotManager.connectDemoSequences(robot.id, true);
|
56 |
+
} catch (err) {
|
57 |
+
error = `Failed to connect demo sequences: ${err}`;
|
58 |
+
console.error(err);
|
59 |
+
}
|
60 |
+
}
|
61 |
+
|
62 |
+
async function connectRemoteServerMaster(robot: Robot) {
|
63 |
+
try {
|
64 |
+
const config: import('$lib/types/robotDriver').MasterDriverConfig = {
|
65 |
+
type: "remote-server",
|
66 |
+
url: "ws://localhost:8080",
|
67 |
+
apiKey: undefined,
|
68 |
+
pollInterval: 100
|
69 |
+
};
|
70 |
+
await robotManager.connectMaster(robot.id, config);
|
71 |
+
} catch (err) {
|
72 |
+
error = `Failed to connect remote server: ${err}`;
|
73 |
+
console.error(err);
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
async function disconnectMaster(robot: Robot) {
|
78 |
+
try {
|
79 |
+
await robotManager.disconnectMaster(robot.id);
|
80 |
+
} catch (err) {
|
81 |
+
error = `Failed to disconnect master: ${err}`;
|
82 |
+
console.error(err);
|
83 |
+
}
|
84 |
+
}
|
85 |
+
|
86 |
+
// Slave connection functions
|
87 |
+
async function connectMockSlave(robot: Robot) {
|
88 |
+
try {
|
89 |
+
await robotManager.connectMockSlave(robot.id, 50);
|
90 |
+
} catch (err) {
|
91 |
+
error = `Failed to connect mock slave: ${err}`;
|
92 |
+
console.error(err);
|
93 |
+
}
|
94 |
+
}
|
95 |
+
|
96 |
+
async function connectUSBSlave(robot: Robot) {
|
97 |
+
try {
|
98 |
+
await robotManager.connectUSBSlave(robot.id);
|
99 |
+
} catch (err) {
|
100 |
+
error = `Failed to connect USB slave: ${err}`;
|
101 |
+
console.error(err);
|
102 |
+
}
|
103 |
+
}
|
104 |
+
|
105 |
+
async function connectRemoteServerSlave(robot: Robot) {
|
106 |
+
try {
|
107 |
+
// First, fetch available robots from the server
|
108 |
+
const serverUrl = "http://localhost:8080";
|
109 |
+
const response = await fetch(`${serverUrl}/api/robots`);
|
110 |
+
|
111 |
+
if (!response.ok) {
|
112 |
+
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
113 |
+
}
|
114 |
+
|
115 |
+
const robots = await response.json();
|
116 |
+
|
117 |
+
if (robots.length === 0) {
|
118 |
+
error = "No robots available on the server. Create a robot on the server first.";
|
119 |
+
return;
|
120 |
+
}
|
121 |
+
|
122 |
+
// Show modal for robot selection
|
123 |
+
availableServerRobots = robots;
|
124 |
+
pendingLocalRobot = robot;
|
125 |
+
selectedServerRobotId = robots[0]?.id || "";
|
126 |
+
showRobotSelectionModal = true;
|
127 |
+
|
128 |
+
} catch (err) {
|
129 |
+
error = `Failed to fetch server robots: ${err}`;
|
130 |
+
console.error(err);
|
131 |
+
}
|
132 |
+
}
|
133 |
+
|
134 |
+
async function confirmRobotSelection() {
|
135 |
+
if (!pendingLocalRobot || !selectedServerRobotId) return;
|
136 |
+
|
137 |
+
try {
|
138 |
+
await robotManager.connectRemoteServerSlave(
|
139 |
+
pendingLocalRobot.id,
|
140 |
+
"ws://localhost:8080",
|
141 |
+
undefined,
|
142 |
+
selectedServerRobotId
|
143 |
+
);
|
144 |
+
|
145 |
+
// Close modal
|
146 |
+
showRobotSelectionModal = false;
|
147 |
+
pendingLocalRobot = null;
|
148 |
+
|
149 |
+
} catch (err) {
|
150 |
+
error = `Failed to connect remote server slave: ${err}`;
|
151 |
+
console.error(err);
|
152 |
+
}
|
153 |
+
}
|
154 |
+
|
155 |
+
function cancelRobotSelection() {
|
156 |
+
showRobotSelectionModal = false;
|
157 |
+
pendingLocalRobot = null;
|
158 |
+
selectedServerRobotId = "";
|
159 |
+
}
|
160 |
+
|
161 |
+
async function disconnectSlave(robot: Robot, slaveId: string) {
|
162 |
+
try {
|
163 |
+
await robotManager.disconnectSlave(robot.id, slaveId);
|
164 |
+
} catch (err) {
|
165 |
+
error = `Failed to disconnect slave: ${err}`;
|
166 |
+
console.error(err);
|
167 |
+
}
|
168 |
+
}
|
169 |
+
|
170 |
+
// Robot management
|
171 |
+
async function calibrateRobot(robot: Robot) {
|
172 |
+
try {
|
173 |
+
await robot.calibrateRobot();
|
174 |
+
} catch (err) {
|
175 |
+
error = `Failed to calibrate: ${err}`;
|
176 |
+
console.error(err);
|
177 |
+
}
|
178 |
+
}
|
179 |
+
|
180 |
+
async function moveToRest(robot: Robot) {
|
181 |
+
try {
|
182 |
+
await robot.moveToRestPosition();
|
183 |
+
} catch (err) {
|
184 |
+
error = `Failed to move to rest: ${err}`;
|
185 |
+
console.error(err);
|
186 |
+
}
|
187 |
+
}
|
188 |
+
|
189 |
+
function clearCalibration(robot: Robot) {
|
190 |
+
robot.clearCalibration();
|
191 |
+
}
|
192 |
+
|
193 |
+
async function removeRobot(robot: Robot) {
|
194 |
+
try {
|
195 |
+
await robotManager.removeRobot(robot.id);
|
196 |
+
} catch (err) {
|
197 |
+
error = `Failed to remove robot: ${err}`;
|
198 |
+
console.error(err);
|
199 |
+
}
|
200 |
+
}
|
201 |
+
|
202 |
+
function clearError() {
|
203 |
+
error = undefined;
|
204 |
+
}
|
205 |
+
|
206 |
+
// Helper functions
|
207 |
+
function getConnectionStatusText(robot: Robot): string {
|
208 |
+
const hasActiveMaster = robot.controlState.hasActiveMaster;
|
209 |
+
const connectedSlaves = robot.connectedSlaves.length;
|
210 |
+
const totalSlaves = robot.slaves.length;
|
211 |
+
|
212 |
+
if (hasActiveMaster && connectedSlaves > 0) {
|
213 |
+
return `Master + ${connectedSlaves}/${totalSlaves} Slaves`;
|
214 |
+
} else if (hasActiveMaster) {
|
215 |
+
return `Master Only`;
|
216 |
+
} else if (connectedSlaves > 0) {
|
217 |
+
return `${connectedSlaves}/${totalSlaves} Slaves`;
|
218 |
+
} else {
|
219 |
+
return "Manual Control";
|
220 |
+
}
|
221 |
+
}
|
222 |
+
|
223 |
+
function getConnectionStatusColor(robot: Robot): string {
|
224 |
+
const hasActiveMaster = robot.controlState.hasActiveMaster;
|
225 |
+
const connectedSlaves = robot.connectedSlaves.length;
|
226 |
+
|
227 |
+
if (hasActiveMaster && connectedSlaves > 0) {
|
228 |
+
return "green"; // Full master-slave setup
|
229 |
+
} else if (hasActiveMaster || connectedSlaves > 0) {
|
230 |
+
return "yellow"; // Partial connection
|
231 |
+
} else {
|
232 |
+
return "red"; // No connections
|
233 |
+
}
|
234 |
+
}
|
235 |
+
|
236 |
+
async function connectUSBMaster(robot: Robot) {
|
237 |
+
try {
|
238 |
+
await robotManager.connectUSBMaster(robot.id, {
|
239 |
+
pollInterval: 200,
|
240 |
+
smoothing: true
|
241 |
+
});
|
242 |
+
} catch (err) {
|
243 |
+
error = `Failed to connect USB master: ${err}`;
|
244 |
+
console.error(err);
|
245 |
+
}
|
246 |
+
}
|
247 |
+
</script>
|
248 |
+
|
249 |
+
<div class="space-y-6 p-4">
|
250 |
+
<div class="flex items-center justify-between">
|
251 |
+
<h2 class="text-xl font-bold text-slate-100">Robot Control - Master/Slave Architecture</h2>
|
252 |
+
<div class="text-sm text-slate-400">
|
253 |
+
{robots.length} robots, {robotsWithMaster} with masters, {robotsWithSlaves} with slaves
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
|
257 |
+
<!-- Error Display -->
|
258 |
+
{#if error}
|
259 |
+
<div class="bg-red-500/20 border border-red-500 rounded-lg p-3 flex items-center justify-between">
|
260 |
+
<span class="text-red-200 text-sm">{error}</span>
|
261 |
+
<button
|
262 |
+
onclick={clearError}
|
263 |
+
class="text-red-200 hover:text-white"
|
264 |
+
>×</button>
|
265 |
+
</div>
|
266 |
+
{/if}
|
267 |
+
|
268 |
+
<!-- Create Robot Section -->
|
269 |
+
<div class="bg-slate-800 rounded-lg p-4 space-y-4">
|
270 |
+
<h3 class="text-lg font-semibold text-slate-100">Create Robot</h3>
|
271 |
+
|
272 |
+
<div class="flex gap-3">
|
273 |
+
<select
|
274 |
+
bind:value={selectedRobotType}
|
275 |
+
class="flex-1 px-3 py-2 bg-slate-700 border border-slate-600 rounded-md text-slate-100"
|
276 |
+
>
|
277 |
+
{#each Object.keys(robotUrdfConfigMap) as robotType}
|
278 |
+
<option value={robotType}>{robotType}</option>
|
279 |
+
{/each}
|
280 |
+
</select>
|
281 |
+
|
282 |
+
<button
|
283 |
+
onclick={createRobot}
|
284 |
+
disabled={isCreating}
|
285 |
+
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-md transition-colors"
|
286 |
+
>
|
287 |
+
{isCreating ? "Creating..." : "Create Robot"}
|
288 |
+
</button>
|
289 |
+
</div>
|
290 |
+
</div>
|
291 |
+
|
292 |
+
<!-- Robots List -->
|
293 |
+
<div class="space-y-3">
|
294 |
+
{#each robots as robot (robot.id)}
|
295 |
+
<div class="bg-slate-800 rounded-lg p-4">
|
296 |
+
<div class="flex items-center justify-between mb-3">
|
297 |
+
<div>
|
298 |
+
<h4 class="font-semibold text-slate-100">{robot.id}</h4>
|
299 |
+
<div class="text-sm text-slate-400">
|
300 |
+
Status:
|
301 |
+
<span class="text-{getConnectionStatusColor(robot)}-400">
|
302 |
+
{getConnectionStatusText(robot)}
|
303 |
+
</span>
|
304 |
+
{#if robot.controlState.lastCommandSource !== "none"}
|
305 |
+
<span class="text-blue-400 ml-2">• Last: {robot.controlState.lastCommandSource}</span>
|
306 |
+
{/if}
|
307 |
+
{#if robot.isCalibrated}
|
308 |
+
<span class="text-green-400 ml-2">• Calibrated</span>
|
309 |
+
{/if}
|
310 |
+
</div>
|
311 |
+
</div>
|
312 |
+
|
313 |
+
<button
|
314 |
+
onclick={() => removeRobot(robot)}
|
315 |
+
class="px-3 py-1 bg-red-600 hover:bg-red-700 text-white text-sm rounded transition-colors"
|
316 |
+
>
|
317 |
+
Remove
|
318 |
+
</button>
|
319 |
+
</div>
|
320 |
+
|
321 |
+
<!-- Master Controls -->
|
322 |
+
<div class="mb-4 p-3 bg-orange-500/10 border border-orange-500/30 rounded-lg">
|
323 |
+
<div class="text-sm text-orange-200 mb-2">
|
324 |
+
<strong>Masters (Control Sources)</strong>
|
325 |
+
{#if robot.controlState.hasActiveMaster}
|
326 |
+
- <span class="text-green-400">Connected: {robot.controlState.masterName}</span>
|
327 |
+
{:else}
|
328 |
+
- <span class="text-slate-400">None Connected</span>
|
329 |
+
{/if}
|
330 |
+
</div>
|
331 |
+
|
332 |
+
<div class="flex flex-wrap gap-2">
|
333 |
+
<button
|
334 |
+
class="px-3 py-1.5 bg-orange-500 text-white rounded text-sm hover:bg-orange-600 transition-colors disabled:bg-slate-600 disabled:cursor-not-allowed"
|
335 |
+
onclick={() => connectMockSequenceMaster(robot)}
|
336 |
+
disabled={robot.controlState.hasActiveMaster}
|
337 |
+
>
|
338 |
+
Demo Sequences
|
339 |
+
</button>
|
340 |
+
|
341 |
+
<button
|
342 |
+
class="px-3 py-1.5 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 transition-colors disabled:bg-slate-600 disabled:cursor-not-allowed"
|
343 |
+
onclick={() => connectRemoteServerMaster(robot)}
|
344 |
+
disabled={robot.controlState.hasActiveMaster}
|
345 |
+
>
|
346 |
+
Connect Remote Server
|
347 |
+
</button>
|
348 |
+
|
349 |
+
<button
|
350 |
+
class="px-3 py-1.5 bg-blue-500 text-white rounded text-sm hover:bg-blue-600 transition-colors disabled:bg-slate-600 disabled:cursor-not-allowed"
|
351 |
+
onclick={() => connectUSBMaster(robot)}
|
352 |
+
disabled={robot.controlState.hasActiveMaster}
|
353 |
+
>
|
354 |
+
Connect USB Master
|
355 |
+
</button>
|
356 |
+
|
357 |
+
{#if robot.controlState.hasActiveMaster}
|
358 |
+
<button
|
359 |
+
class="px-3 py-1.5 bg-red-500 text-white rounded text-sm hover:bg-red-600 transition-colors"
|
360 |
+
onclick={() => disconnectMaster(robot)}
|
361 |
+
>
|
362 |
+
Disconnect Master
|
363 |
+
</button>
|
364 |
+
{/if}
|
365 |
+
</div>
|
366 |
+
</div>
|
367 |
+
|
368 |
+
<!-- Slave Controls -->
|
369 |
+
<div class="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
370 |
+
<div class="text-sm text-blue-200 mb-2">
|
371 |
+
<strong>Slaves (Execution Targets)</strong>
|
372 |
+
- <span class="text-green-400">{robot.connectedSlaves.length} Connected</span>
|
373 |
+
/ <span class="text-slate-400">{robot.slaves.length} Total</span>
|
374 |
+
</div>
|
375 |
+
|
376 |
+
<div class="flex gap-2 mb-2">
|
377 |
+
<button
|
378 |
+
onclick={() => connectMockSlave(robot)}
|
379 |
+
class="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 text-white text-sm rounded transition-colors"
|
380 |
+
>
|
381 |
+
Add Mock Slave
|
382 |
+
</button>
|
383 |
+
<button
|
384 |
+
onclick={() => connectUSBSlave(robot)}
|
385 |
+
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded transition-colors"
|
386 |
+
>
|
387 |
+
Add USB Slave
|
388 |
+
</button>
|
389 |
+
<button
|
390 |
+
onclick={() => connectRemoteServerSlave(robot)}
|
391 |
+
class="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded transition-colors"
|
392 |
+
>
|
393 |
+
Add Remote Server Slave
|
394 |
+
</button>
|
395 |
+
</div>
|
396 |
+
|
397 |
+
{#if robot.slaves.length > 0}
|
398 |
+
<div class="text-xs text-blue-300">
|
399 |
+
<strong>Connected Slaves:</strong>
|
400 |
+
{#each robot.slaves as slave}
|
401 |
+
<div class="flex items-center justify-between mt-1">
|
402 |
+
<span>{slave.name} ({slave.id})</span>
|
403 |
+
<button
|
404 |
+
onclick={() => disconnectSlave(robot, slave.id)}
|
405 |
+
class="px-2 py-1 bg-red-500 hover:bg-red-600 text-white text-xs rounded"
|
406 |
+
>
|
407 |
+
Remove
|
408 |
+
</button>
|
409 |
+
</div>
|
410 |
+
{/each}
|
411 |
+
</div>
|
412 |
+
{/if}
|
413 |
+
</div>
|
414 |
+
|
415 |
+
<!-- Calibration Section (for USB slaves) -->
|
416 |
+
{#if robot.connectedSlaves.some(slave => slave.name.includes("USB"))}
|
417 |
+
<div class="mb-3 p-3 bg-orange-500/10 border border-orange-500/30 rounded-lg">
|
418 |
+
<div class="text-sm text-orange-200 mb-2">
|
419 |
+
<strong>USB Robot Calibration</strong>
|
420 |
+
</div>
|
421 |
+
{#if !robot.isCalibrated}
|
422 |
+
<div class="text-xs text-orange-300 mb-2">
|
423 |
+
1. Manually position your robot to match the digital twin's rest pose<br>
|
424 |
+
2. Click "Calibrate" when positioned correctly
|
425 |
+
</div>
|
426 |
+
<div class="flex gap-2">
|
427 |
+
<button
|
428 |
+
onclick={() => moveToRest(robot)}
|
429 |
+
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors"
|
430 |
+
>
|
431 |
+
Show Rest Pose
|
432 |
+
</button>
|
433 |
+
<button
|
434 |
+
onclick={() => calibrateRobot(robot)}
|
435 |
+
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded transition-colors"
|
436 |
+
>
|
437 |
+
Calibrate
|
438 |
+
</button>
|
439 |
+
</div>
|
440 |
+
{:else}
|
441 |
+
<div class="text-xs text-green-300 mb-2">
|
442 |
+
✓ Robot calibrated at {robot.calibrationState.calibrationTime?.toLocaleTimeString()}
|
443 |
+
</div>
|
444 |
+
<button
|
445 |
+
onclick={() => clearCalibration(robot)}
|
446 |
+
class="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white text-sm rounded transition-colors"
|
447 |
+
>
|
448 |
+
Clear Calibration
|
449 |
+
</button>
|
450 |
+
{/if}
|
451 |
+
</div>
|
452 |
+
{/if}
|
453 |
+
|
454 |
+
<!-- Manual Joint Controls (only when no master) -->
|
455 |
+
{#if robot.manualControlEnabled}
|
456 |
+
<div class="space-y-3">
|
457 |
+
<h5 class="text-sm font-medium text-slate-300">Manual Control ({robot.activeJoints.length} active joints)</h5>
|
458 |
+
{#each robot.activeJoints as joint}
|
459 |
+
<div class="joint-control">
|
460 |
+
<div class="joint-header">
|
461 |
+
<span class="joint-name">{joint.name}</span>
|
462 |
+
<div class="joint-values">
|
463 |
+
<span class="virtual-value">{joint.virtualValue.toFixed(0)}°</span>
|
464 |
+
{#if joint.realValue !== undefined}
|
465 |
+
<span class="real-value" title="Real robot position">
|
466 |
+
Real: {joint.realValue.toFixed(0)}°
|
467 |
+
</span>
|
468 |
+
{:else}
|
469 |
+
<span class="real-value disconnected">N/A</span>
|
470 |
+
{/if}
|
471 |
+
</div>
|
472 |
+
</div>
|
473 |
+
<input
|
474 |
+
type="range"
|
475 |
+
min="-180"
|
476 |
+
max="180"
|
477 |
+
step="1"
|
478 |
+
value={joint.virtualValue}
|
479 |
+
oninput={(e) => {
|
480 |
+
const target = e.target as HTMLInputElement;
|
481 |
+
robot.updateJointValue(joint.name, parseFloat(target.value));
|
482 |
+
}}
|
483 |
+
class="joint-slider"
|
484 |
+
/>
|
485 |
+
</div>
|
486 |
+
{/each}
|
487 |
+
|
488 |
+
{#if robot.activeJoints.length === 0}
|
489 |
+
<div class="text-sm text-slate-500 italic">No active joints</div>
|
490 |
+
{/if}
|
491 |
+
</div>
|
492 |
+
{:else}
|
493 |
+
<div class="p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
494 |
+
<div class="text-sm text-purple-200">
|
495 |
+
🎮 <strong>Master Control Active</strong><br>
|
496 |
+
<span class="text-xs text-purple-300">
|
497 |
+
Robot is controlled by: {robot.controlState.masterName}<br>
|
498 |
+
Manual controls are disabled. Disconnect master to regain manual control.
|
499 |
+
</span>
|
500 |
+
</div>
|
501 |
+
</div>
|
502 |
+
{/if}
|
503 |
+
</div>
|
504 |
+
{/each}
|
505 |
+
|
506 |
+
{#if robots.length === 0}
|
507 |
+
<div class="text-center text-slate-500 py-8">
|
508 |
+
No robots created yet. Create one above to get started with the master-slave architecture.
|
509 |
+
</div>
|
510 |
+
{/if}
|
511 |
+
</div>
|
512 |
+
</div>
|
513 |
+
|
514 |
+
<!-- Robot Selection Modal -->
|
515 |
+
{#if showRobotSelectionModal}
|
516 |
+
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
517 |
+
<div class="bg-slate-800 rounded-lg p-6 w-96 max-w-full mx-4">
|
518 |
+
<h3 class="text-lg font-semibold text-slate-100 mb-4">Select Server Robot</h3>
|
519 |
+
|
520 |
+
<div class="mb-4">
|
521 |
+
<label class="block text-sm font-medium text-slate-300 mb-2">
|
522 |
+
Available robots on server:
|
523 |
+
</label>
|
524 |
+
<select
|
525 |
+
bind:value={selectedServerRobotId}
|
526 |
+
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md text-slate-100"
|
527 |
+
>
|
528 |
+
{#each availableServerRobots as serverRobot}
|
529 |
+
<option value={serverRobot.id}>
|
530 |
+
{serverRobot.name} ({serverRobot.id}) - {serverRobot.robot_type}
|
531 |
+
</option>
|
532 |
+
{/each}
|
533 |
+
</select>
|
534 |
+
</div>
|
535 |
+
|
536 |
+
<div class="text-sm text-slate-400 mb-4">
|
537 |
+
This will connect your local robot "{pendingLocalRobot?.id}" as a slave to
|
538 |
+
receive commands from the selected server robot.
|
539 |
+
</div>
|
540 |
+
|
541 |
+
<div class="flex gap-3 justify-end">
|
542 |
+
<button
|
543 |
+
onclick={cancelRobotSelection}
|
544 |
+
class="px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded-md transition-colors"
|
545 |
+
>
|
546 |
+
Cancel
|
547 |
+
</button>
|
548 |
+
<button
|
549 |
+
onclick={confirmRobotSelection}
|
550 |
+
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-md transition-colors"
|
551 |
+
disabled={!selectedServerRobotId}
|
552 |
+
>
|
553 |
+
Connect Slave
|
554 |
+
</button>
|
555 |
+
</div>
|
556 |
+
</div>
|
557 |
+
</div>
|
558 |
+
{/if}
|
559 |
+
|
560 |
+
<style>
|
561 |
+
.joint-control {
|
562 |
+
background: rgba(71, 85, 105, 0.3);
|
563 |
+
border-radius: 8px;
|
564 |
+
padding: 12px;
|
565 |
+
border: 1px solid rgba(71, 85, 105, 0.5);
|
566 |
+
}
|
567 |
+
|
568 |
+
.joint-header {
|
569 |
+
display: flex;
|
570 |
+
justify-content: space-between;
|
571 |
+
align-items: center;
|
572 |
+
margin-bottom: 8px;
|
573 |
+
}
|
574 |
+
|
575 |
+
.joint-name {
|
576 |
+
font-weight: 500;
|
577 |
+
color: #e2e8f0;
|
578 |
+
font-size: 14px;
|
579 |
+
}
|
580 |
+
|
581 |
+
.joint-values {
|
582 |
+
display: flex;
|
583 |
+
gap: 12px;
|
584 |
+
font-size: 12px;
|
585 |
+
}
|
586 |
+
|
587 |
+
.virtual-value {
|
588 |
+
color: #60a5fa;
|
589 |
+
font-weight: 500;
|
590 |
+
}
|
591 |
+
|
592 |
+
.real-value {
|
593 |
+
color: #34d399;
|
594 |
+
font-weight: 500;
|
595 |
+
}
|
596 |
+
|
597 |
+
.real-value.disconnected {
|
598 |
+
color: #f87171;
|
599 |
+
}
|
600 |
+
|
601 |
+
.joint-slider {
|
602 |
+
width: 100%;
|
603 |
+
height: 6px;
|
604 |
+
background: #374151;
|
605 |
+
border-radius: 3px;
|
606 |
+
outline: none;
|
607 |
+
cursor: pointer;
|
608 |
+
appearance: none;
|
609 |
+
}
|
610 |
+
|
611 |
+
.joint-slider::-webkit-slider-thumb {
|
612 |
+
appearance: none;
|
613 |
+
width: 18px;
|
614 |
+
height: 18px;
|
615 |
+
border-radius: 50%;
|
616 |
+
background: #3b82f6;
|
617 |
+
cursor: pointer;
|
618 |
+
border: 2px solid #1e293b;
|
619 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
620 |
+
transition: all 0.15s ease;
|
621 |
+
}
|
622 |
+
|
623 |
+
.joint-slider::-webkit-slider-thumb:hover {
|
624 |
+
background: #2563eb;
|
625 |
+
transform: scale(1.1);
|
626 |
+
}
|
627 |
+
|
628 |
+
.joint-slider::-moz-range-thumb {
|
629 |
+
width: 18px;
|
630 |
+
height: 18px;
|
631 |
+
border-radius: 50%;
|
632 |
+
background: #3b82f6;
|
633 |
+
cursor: pointer;
|
634 |
+
border: 2px solid #1e293b;
|
635 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
636 |
+
transition: all 0.15s ease;
|
637 |
+
}
|
638 |
+
|
639 |
+
.joint-slider::-moz-range-track {
|
640 |
+
height: 6px;
|
641 |
+
background: #374151;
|
642 |
+
border-radius: 3px;
|
643 |
+
border: none;
|
644 |
+
}
|
645 |
+
|
646 |
+
.joint-slider:focus {
|
647 |
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
648 |
+
}
|
649 |
+
</style>
|
src/lib/components/panel/SettingsPanel.svelte
ADDED
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { environment } from '$lib/runes/env.svelte';
|
3 |
+
import type IUrdfJoint from '$lib/components/scene/robot/URDF/interfaces/IUrdfJoint';
|
4 |
+
|
5 |
+
interface Props {}
|
6 |
+
|
7 |
+
let {}: Props = $props();
|
8 |
+
|
9 |
+
// Helper function to get all robots
|
10 |
+
const robots = $derived(Object.entries(environment.robots));
|
11 |
+
|
12 |
+
// Helper function to convert degrees to radians
|
13 |
+
function degreesToRadians(degrees: number): number {
|
14 |
+
return degrees * (Math.PI / 180);
|
15 |
+
}
|
16 |
+
|
17 |
+
// Helper function to convert radians to degrees
|
18 |
+
function radiansToDegrees(radians: number): number {
|
19 |
+
return radians * (180 / Math.PI);
|
20 |
+
}
|
21 |
+
|
22 |
+
// Helper function to get current joint rotation value in degrees
|
23 |
+
function getJointRotationValue(joint: any): number {
|
24 |
+
const axis = joint.axis_xyz || [0, 0, 1];
|
25 |
+
const rotation = joint.rotation || [0, 0, 0];
|
26 |
+
|
27 |
+
// Find the primary axis and get the rotation value
|
28 |
+
for (let i = 0; i < 3; i++) {
|
29 |
+
if (Math.abs(axis[i]) > 0.001) {
|
30 |
+
return radiansToDegrees(rotation[i] / axis[i]);
|
31 |
+
}
|
32 |
+
}
|
33 |
+
return 0;
|
34 |
+
}
|
35 |
+
|
36 |
+
// Helper function to update joint rotation using axis
|
37 |
+
function updateJointRotation(robotId: string, jointIndex: number, degrees: number): void {
|
38 |
+
const robot = environment.robots[robotId];
|
39 |
+
if (!robot?.robot.joints[jointIndex]) return;
|
40 |
+
|
41 |
+
const joint = robot.robot.joints[jointIndex];
|
42 |
+
const radians = degreesToRadians(degrees);
|
43 |
+
const axis = joint.axis_xyz || [0, 0, 1];
|
44 |
+
|
45 |
+
// Calculate rotation based on axis
|
46 |
+
joint.rotation = [
|
47 |
+
radians * axis[0],
|
48 |
+
radians * axis[1],
|
49 |
+
radians * axis[2]
|
50 |
+
];
|
51 |
+
}
|
52 |
+
|
53 |
+
// Helper function to get joint limits
|
54 |
+
function getJointLimits(joint: IUrdfJoint): { min: number; max: number } {
|
55 |
+
if (joint.limit) {
|
56 |
+
return {
|
57 |
+
min: joint.limit.lower ? radiansToDegrees(joint.limit.lower) : -180,
|
58 |
+
max: joint.limit.upper ? radiansToDegrees(joint.limit.upper) : 180
|
59 |
+
};
|
60 |
+
}
|
61 |
+
return joint.type === 'continuous' ? { min: -360, max: 360 } : { min: -180, max: 180 };
|
62 |
+
}
|
63 |
+
</script>
|
64 |
+
|
65 |
+
<div class="space-y-6">
|
66 |
+
<h3 class="text-lg font-semibold text-slate-100 mb-4">Robot Joint Settings</h3>
|
67 |
+
|
68 |
+
{#if robots.length === 0}
|
69 |
+
<div class="text-slate-400 text-sm">
|
70 |
+
No robots in the environment. Create a robot first using the Control Panel.
|
71 |
+
</div>
|
72 |
+
{:else}
|
73 |
+
{#each robots as [robotId, robotState]}
|
74 |
+
<div class="space-y-4 p-4 bg-slate-700/50 rounded-lg border border-slate-600">
|
75 |
+
<h4 class="text-md font-medium text-slate-200 border-b border-slate-600 pb-2">
|
76 |
+
Robot: <span class="text-sky-400">{robotId.slice(0, 8)}...</span>
|
77 |
+
</h4>
|
78 |
+
|
79 |
+
{#if robotState.robot.joints.length === 0}
|
80 |
+
<div class="text-slate-400 text-sm">No joints found for this robot.</div>
|
81 |
+
{:else}
|
82 |
+
<div class="text-slate-400 text-sm">
|
83 |
+
Joints:
|
84 |
+
</div>
|
85 |
+
<div class="space-y-4">
|
86 |
+
{#each robotState.robot.joints as joint, jointIndex}
|
87 |
+
{#if joint.type === 'revolute'}
|
88 |
+
{@const currentValue = getJointRotationValue(joint)}
|
89 |
+
{@const limits = getJointLimits(joint)}
|
90 |
+
{@const axis = joint.axis_xyz || [0, 0, 1]}
|
91 |
+
|
92 |
+
<div class="space-y-3 p-3 bg-slate-800/50 rounded border border-slate-600">
|
93 |
+
<div class="flex justify-between items-start">
|
94 |
+
<div>
|
95 |
+
<h5 class="text-sm font-medium text-slate-300">
|
96 |
+
<span class="text-yellow-400">{joint.name || `Joint ${jointIndex}`}</span>
|
97 |
+
<span class="text-xs text-slate-400 ml-2">({joint.type})</span>
|
98 |
+
</h5>
|
99 |
+
<div class="text-xs text-slate-500 mt-1">
|
100 |
+
Axis: [{axis[0].toFixed(1)}, {axis[1].toFixed(1)}, {axis[2].toFixed(1)}]
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
<div class="text-right">
|
104 |
+
<div class="text-sky-400 font-mono text-sm">{currentValue.toFixed(1)}°</div>
|
105 |
+
<div class="text-xs text-slate-500">
|
106 |
+
{limits.min.toFixed(0)}° to {limits.max.toFixed(0)}°
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
|
111 |
+
<!-- Single Rotation Control -->
|
112 |
+
<div class="space-y-2">
|
113 |
+
<input
|
114 |
+
type="range"
|
115 |
+
min={limits.min}
|
116 |
+
max={limits.max}
|
117 |
+
step="1"
|
118 |
+
value={currentValue}
|
119 |
+
oninput={(e) => updateJointRotation(robotId, jointIndex, parseFloat(e.currentTarget.value))}
|
120 |
+
class="w-full h-2 bg-slate-600 rounded-lg appearance-none cursor-pointer slider"
|
121 |
+
/>
|
122 |
+
</div>
|
123 |
+
|
124 |
+
<!-- Reset Joint Button -->
|
125 |
+
<button
|
126 |
+
onclick={() => updateJointRotation(robotId, jointIndex, 0)}
|
127 |
+
class="text-xs px-3 py-1 bg-slate-600 hover:bg-slate-500 text-slate-200 rounded transition-colors"
|
128 |
+
>
|
129 |
+
Reset to 0°
|
130 |
+
</button>
|
131 |
+
</div>
|
132 |
+
{:else if joint.type === 'continuous'}
|
133 |
+
{@const currentValue = getJointRotationValue(joint)}
|
134 |
+
{@const limits = getJointLimits(joint)}
|
135 |
+
{@const axis = joint.axis_xyz || [0, 0, 1]}
|
136 |
+
<div class="space-y-3 p-3 bg-slate-800/50 rounded border border-slate-600">
|
137 |
+
<div class="flex justify-between items-start">
|
138 |
+
<div>
|
139 |
+
<h5 class="text-sm font-medium text-slate-300">
|
140 |
+
<span class="text-yellow-400">{joint.name || `Joint ${jointIndex}`}</span>
|
141 |
+
<span class="text-xs text-slate-400 ml-2">({joint.type})</span>
|
142 |
+
</h5>
|
143 |
+
<div class="text-xs text-slate-500 mt-1">
|
144 |
+
Axis: [{axis[0].toFixed(1)}, {axis[1].toFixed(1)}, {axis[2].toFixed(1)}]
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
<div class="text-right">
|
148 |
+
<div class="text-sky-400 font-mono text-sm">{currentValue.toFixed(1)}°</div>
|
149 |
+
<div class="text-xs text-slate-500">
|
150 |
+
{limits.min.toFixed(0)}° to {limits.max.toFixed(0)}°
|
151 |
+
</div>
|
152 |
+
</div>
|
153 |
+
</div>
|
154 |
+
</div>
|
155 |
+
{:else if joint.type === 'fixed'}
|
156 |
+
<div class="p-2 bg-slate-800/30 rounded border border-slate-700">
|
157 |
+
<div class="text-xs text-slate-500">
|
158 |
+
<span class="text-slate-400">{joint.name || `Joint ${jointIndex}`}</span>
|
159 |
+
<span class="ml-2">(fixed joint)</span>
|
160 |
+
</div>
|
161 |
+
</div>
|
162 |
+
{/if}
|
163 |
+
{/each}
|
164 |
+
</div>
|
165 |
+
{#if robotState.urdfConfig.compoundMovements}
|
166 |
+
<div class="text-slate-400 text-sm">
|
167 |
+
Compound Movements:
|
168 |
+
</div>
|
169 |
+
<div class="space-y-4">
|
170 |
+
{#each robotState.urdfConfig.compoundMovements as movement}
|
171 |
+
<div class="text-slate-400 text-sm">
|
172 |
+
{movement.name}
|
173 |
+
{#each movement.dependents as dependent}
|
174 |
+
<div class="text-slate-400 text-sm">
|
175 |
+
{dependent.joint}
|
176 |
+
</div>
|
177 |
+
{/each}
|
178 |
+
</div>
|
179 |
+
{/each}
|
180 |
+
</div>
|
181 |
+
{/if}
|
182 |
+
{/if}
|
183 |
+
|
184 |
+
<!-- Reset All Joints Button -->
|
185 |
+
<button
|
186 |
+
onclick={() => {
|
187 |
+
robotState.robot.joints.forEach((joint, index) => {
|
188 |
+
if (joint.type === 'revolute') {
|
189 |
+
updateJointRotation(robotId, index, 0);
|
190 |
+
} else if (joint.type === 'continuous') {
|
191 |
+
updateJointRotation(robotId, index, 0);
|
192 |
+
}
|
193 |
+
});
|
194 |
+
}}
|
195 |
+
class="w-full px-3 py-2 bg-sky-600 hover:bg-sky-500 text-white rounded transition-colors text-sm font-medium"
|
196 |
+
>
|
197 |
+
Reset All Joints to 0°
|
198 |
+
</button>
|
199 |
+
</div>
|
200 |
+
{/each}
|
201 |
+
{/if}
|
202 |
+
</div>
|
203 |
+
|
204 |
+
<style>
|
205 |
+
.slider::-webkit-slider-thumb {
|
206 |
+
appearance: none;
|
207 |
+
height: 16px;
|
208 |
+
width: 16px;
|
209 |
+
border-radius: 50%;
|
210 |
+
background: #38bdf8; /* sky-400 */
|
211 |
+
cursor: pointer;
|
212 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
213 |
+
border: 2px solid #0f172a; /* slate-900 */
|
214 |
+
}
|
215 |
+
|
216 |
+
.slider::-moz-range-thumb {
|
217 |
+
height: 16px;
|
218 |
+
width: 16px;
|
219 |
+
border-radius: 50%;
|
220 |
+
background: #38bdf8; /* sky-400 */
|
221 |
+
cursor: pointer;
|
222 |
+
border: 2px solid #0f172a; /* slate-900 */
|
223 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
224 |
+
}
|
225 |
+
|
226 |
+
.slider::-webkit-slider-track {
|
227 |
+
background: #475569; /* slate-600 */
|
228 |
+
border-radius: 4px;
|
229 |
+
height: 8px;
|
230 |
+
}
|
231 |
+
|
232 |
+
.slider::-moz-range-track {
|
233 |
+
background: #475569; /* slate-600 */
|
234 |
+
border-radius: 4px;
|
235 |
+
height: 8px;
|
236 |
+
border: none;
|
237 |
+
}
|
238 |
+
|
239 |
+
.slider:hover::-webkit-slider-thumb {
|
240 |
+
background: #0ea5e9; /* sky-500 */
|
241 |
+
transform: scale(1.05);
|
242 |
+
}
|
243 |
+
|
244 |
+
.slider:hover::-moz-range-thumb {
|
245 |
+
background: #0ea5e9; /* sky-500 */
|
246 |
+
transform: scale(1.05);
|
247 |
+
}
|
248 |
+
</style>
|
src/lib/components/scene/Floor.svelte
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T } from '@threlte/core';
|
3 |
+
import * as THREE from 'three';
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Creates a grid texture for the ground plane
|
7 |
+
* Matches the original React implementation for visual consistency
|
8 |
+
*/
|
9 |
+
function createGridTexture(): THREE.CanvasTexture {
|
10 |
+
const size = 200;
|
11 |
+
const canvas = document.createElement('canvas');
|
12 |
+
canvas.width = canvas.height = size;
|
13 |
+
const ctx = canvas.getContext('2d');
|
14 |
+
|
15 |
+
if (ctx) {
|
16 |
+
// Base color
|
17 |
+
ctx.fillStyle = '#aaa';
|
18 |
+
ctx.fillRect(0, 0, size, size);
|
19 |
+
|
20 |
+
// Grid lines
|
21 |
+
ctx.strokeStyle = '#707070';
|
22 |
+
ctx.lineWidth = 8;
|
23 |
+
ctx.beginPath();
|
24 |
+
ctx.moveTo(0, 0);
|
25 |
+
ctx.lineTo(0, size);
|
26 |
+
ctx.lineTo(size, size);
|
27 |
+
ctx.lineTo(size, 0);
|
28 |
+
ctx.closePath();
|
29 |
+
ctx.stroke();
|
30 |
+
}
|
31 |
+
|
32 |
+
const texture = new THREE.CanvasTexture(canvas);
|
33 |
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
34 |
+
texture.repeat.set(100, 100);
|
35 |
+
return texture;
|
36 |
+
}
|
37 |
+
|
38 |
+
// Create texture reactively using Svelte 5 runes
|
39 |
+
const gridTexture = createGridTexture();
|
40 |
+
</script>
|
41 |
+
|
42 |
+
<!-- Ground plane with grid texture matching React version -->
|
43 |
+
<T.Mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
44 |
+
<T.PlaneGeometry args={[30, 30]} />
|
45 |
+
<T.MeshPhysicalMaterial
|
46 |
+
color={0x808080}
|
47 |
+
metalness={0.7}
|
48 |
+
roughness={0.3}
|
49 |
+
reflectivity={0.1}
|
50 |
+
clearcoat={0.3}
|
51 |
+
side={THREE.DoubleSide}
|
52 |
+
transparent
|
53 |
+
opacity={0.7}
|
54 |
+
map={gridTexture}
|
55 |
+
/>
|
56 |
+
</T.Mesh>
|
src/lib/components/scene/Robot.svelte
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T } from '@threlte/core';
|
3 |
+
import { getRootLinks } from '$lib/components/scene/robot/URDF/utils/UrdfParser';
|
4 |
+
import UrdfLink from '$lib/components/scene/robot/URDF/primitives/UrdfLink.svelte';
|
5 |
+
import { robotManager } from '$lib/robot/RobotManager.svelte';
|
6 |
+
|
7 |
+
interface Props {}
|
8 |
+
|
9 |
+
let {}: Props = $props();
|
10 |
+
|
11 |
+
// Get all robots from the manager
|
12 |
+
const robots = $derived(robotManager.robots);
|
13 |
+
</script>
|
14 |
+
|
15 |
+
{#each robots as robot, index (robot.id)}
|
16 |
+
{@const xPosition = index * 5} <!-- Space robots 5 units apart -->
|
17 |
+
<T.Group position.x={xPosition} position.y={0} position.z={0} quaternion={[0, 0, 0, 1]} scale={[10, 10, 10]} rotation={[-Math.PI / 2, 0, 0]}>
|
18 |
+
{#each getRootLinks(robot.robotState.robot) as link}
|
19 |
+
<UrdfLink
|
20 |
+
robot={robot.robotState.robot}
|
21 |
+
{link}
|
22 |
+
textScale={0.2}
|
23 |
+
showName={true}
|
24 |
+
showVisual={true}
|
25 |
+
showCollision={false}
|
26 |
+
visualColor="#333333"
|
27 |
+
visualOpacity={0.7}
|
28 |
+
collisionOpacity={0.7}
|
29 |
+
collisionColor="#813d9c"
|
30 |
+
jointNames={true}
|
31 |
+
joints={true}
|
32 |
+
jointColor="#62a0ea"
|
33 |
+
jointIndicatorColor="#f66151"
|
34 |
+
nameHeight={0.1}
|
35 |
+
selectedLink={robot.robotState.selection.selectedLink}
|
36 |
+
selectedJoint={robot.robotState.selection.selectedJoint}
|
37 |
+
highlightColor="#ffa348"
|
38 |
+
showLine={false}
|
39 |
+
opacity={1}
|
40 |
+
isInteractive={false}
|
41 |
+
/>
|
42 |
+
{/each}
|
43 |
+
</T.Group>
|
44 |
+
{/each}
|
src/lib/components/scene/Selectable.svelte
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T } from '@threlte/core';
|
3 |
+
import { TransformControls, interactivity } from '@threlte/extras';
|
4 |
+
import type { Object3D } from 'three';
|
5 |
+
import type { Snippet } from 'svelte';
|
6 |
+
import type IUrdfJoint from './robot/URDF/interfaces/IUrdfJoint';
|
7 |
+
import { updateOrigin } from './robot/URDF/utils/UrdfParser';
|
8 |
+
import type { IUrdfVisual } from './robot/URDF/interfaces/IUrdfVisual';
|
9 |
+
import type { TransformControlsMode } from 'three/examples/jsm/Addons.js';
|
10 |
+
|
11 |
+
interface Props {
|
12 |
+
origin: IUrdfJoint | IUrdfVisual;
|
13 |
+
children?: Snippet; // renderable
|
14 |
+
selected?: boolean;
|
15 |
+
translationSnap?: number;
|
16 |
+
scaleSnap?: number;
|
17 |
+
rotationSnap?: number;
|
18 |
+
tool?: TransformControlsMode;
|
19 |
+
enableEdit?: boolean;
|
20 |
+
}
|
21 |
+
|
22 |
+
let {
|
23 |
+
origin,
|
24 |
+
children,
|
25 |
+
selected = false,
|
26 |
+
translationSnap = 0.1,
|
27 |
+
scaleSnap = 0.1,
|
28 |
+
rotationSnap = 0.1,
|
29 |
+
tool = 'translate',
|
30 |
+
enableEdit = true
|
31 |
+
}: Props = $props();
|
32 |
+
|
33 |
+
const updateData = (obj: Object3D) => {
|
34 |
+
origin.origin_xyz = obj.position.toArray();
|
35 |
+
origin.origin_rpy = [obj.rotation.x, obj.rotation.y, obj.rotation.z];
|
36 |
+
updateOrigin(origin);
|
37 |
+
};
|
38 |
+
|
39 |
+
const onobjectChange = (event: any) => {
|
40 |
+
if (!event.target) {
|
41 |
+
return;
|
42 |
+
}
|
43 |
+
const obj = event.target.object;
|
44 |
+
updateData(obj);
|
45 |
+
};
|
46 |
+
|
47 |
+
interactivity();
|
48 |
+
</script>
|
49 |
+
|
50 |
+
{#if selected && enableEdit}
|
51 |
+
<TransformControls
|
52 |
+
{translationSnap}
|
53 |
+
{scaleSnap}
|
54 |
+
rotationSnap={Math.PI / rotationSnap}
|
55 |
+
position={origin.origin_xyz}
|
56 |
+
rotation={origin.origin_rpy}
|
57 |
+
mode={tool}
|
58 |
+
{onobjectChange}
|
59 |
+
>
|
60 |
+
{@render children?.()}
|
61 |
+
</TransformControls>
|
62 |
+
{:else}
|
63 |
+
<T.Group position={origin.origin_xyz} rotation={origin.origin_rpy}>
|
64 |
+
{@render children?.()}
|
65 |
+
</T.Group>
|
66 |
+
{/if}
|
67 |
+
|
68 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|
src/lib/components/scene/robot/URDF/createRobot.svelte.ts
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { RobotState } from "$lib/types/robot";
|
2 |
+
import type { RobotUrdfConfig } from "$lib/types/urdf";
|
3 |
+
import { UrdfParser } from "./utils/UrdfParser";
|
4 |
+
|
5 |
+
export async function createRobot(urdfConfig: RobotUrdfConfig): Promise<RobotState> {
|
6 |
+
const customParser = new UrdfParser(urdfConfig.urdfUrl, "/robots/so-100/");
|
7 |
+
const urdfData = await customParser.load();
|
8 |
+
const robot = customParser.fromString(urdfData);
|
9 |
+
|
10 |
+
// Make the robot data reactive so mutations to joint.rotation trigger reactivity
|
11 |
+
const reactiveRobot = $state(robot);
|
12 |
+
|
13 |
+
// Make the selection state reactive as well
|
14 |
+
const reactiveSelection = $state({
|
15 |
+
isSelected: false,
|
16 |
+
selectedLink: undefined,
|
17 |
+
selectedJoint: undefined
|
18 |
+
});
|
19 |
+
|
20 |
+
const robotState: RobotState = {
|
21 |
+
robot: reactiveRobot,
|
22 |
+
urdfConfig: urdfConfig,
|
23 |
+
selection: reactiveSelection
|
24 |
+
};
|
25 |
+
|
26 |
+
return robotState;
|
27 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfBox.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export default interface IUrdfBox {
|
2 |
+
size: [x: number, y: number, z: number]
|
3 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfCylinder.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default interface IUrdfCylinder {
|
2 |
+
radius: number;
|
3 |
+
length: number
|
4 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfJoint.ts
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type IUrdfLink from "./IUrdfLink"
|
2 |
+
|
3 |
+
export default interface IUrdfJoint {
|
4 |
+
name?: string
|
5 |
+
type?: 'revolute' | 'continuous' | 'prismatic' | 'fixed' | 'floating' | 'planar'
|
6 |
+
// rpy = roll, pitch, yaw (values between -pi and +pi)
|
7 |
+
origin_rpy: [roll: number, pitch: number, yaw: number]
|
8 |
+
origin_xyz: [x: number, y: number, z: number]
|
9 |
+
// calculated rotation for non-fixed joints based on origin_rpy and axis_xyz
|
10 |
+
rotation: [x: number, y: number, z: number]
|
11 |
+
parent: IUrdfLink
|
12 |
+
child: IUrdfLink
|
13 |
+
// axis for revolute and continuous joints defaults to (1,0,0)
|
14 |
+
axis_xyz?: [x: number, y: number, z: number]
|
15 |
+
calibration?: {
|
16 |
+
rising?: number, // Calibration rising value in radians
|
17 |
+
falling?: number // Calibration falling value in radians
|
18 |
+
}
|
19 |
+
dynamics?: {
|
20 |
+
damping?: number
|
21 |
+
friction?: number
|
22 |
+
}
|
23 |
+
// only for revolute joints
|
24 |
+
limit?: {
|
25 |
+
lower?: number
|
26 |
+
upper?: number
|
27 |
+
effort: number
|
28 |
+
velocity: number
|
29 |
+
}
|
30 |
+
mimic?: {
|
31 |
+
joint: string
|
32 |
+
multiplier?: number
|
33 |
+
offset?: number
|
34 |
+
}
|
35 |
+
safety_controller?: {
|
36 |
+
soft_lower_limit?: number
|
37 |
+
soft_upper_limit?: number
|
38 |
+
k_position?: number
|
39 |
+
k_velocity: number
|
40 |
+
}
|
41 |
+
elem: Element
|
42 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfLink.ts
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type {IUrdfVisual} from "./IUrdfVisual"
|
2 |
+
|
3 |
+
interface IUrdfInertia {
|
4 |
+
ixx: number;
|
5 |
+
ixy: number;
|
6 |
+
ixz: number;
|
7 |
+
iyy: number;
|
8 |
+
iyz: number;
|
9 |
+
izz: number;
|
10 |
+
}
|
11 |
+
|
12 |
+
export default interface IUrdfLink {
|
13 |
+
name: string
|
14 |
+
inertial?: {
|
15 |
+
origin_xyz?: [x: number, y: number, z: number]
|
16 |
+
origin_rpy?: [roll: number, pitch: number, yaw: number]
|
17 |
+
mass: number
|
18 |
+
inertia: IUrdfInertia
|
19 |
+
}
|
20 |
+
visual: IUrdfVisual[]
|
21 |
+
collision: IUrdfVisual[]
|
22 |
+
elem: Element
|
23 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfMesh.ts
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default interface IUrdfMesh {
|
2 |
+
filename: string;
|
3 |
+
type: 'stl' | 'fbx' | 'obj' | 'dae';
|
4 |
+
scale: [x: number, y: number, z: number];
|
5 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfRobot.ts
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type IUrdfJoint from "./IUrdfJoint"
|
2 |
+
import type IUrdfLink from "./IUrdfLink"
|
3 |
+
|
4 |
+
export default interface IUrdfRobot {
|
5 |
+
name: string
|
6 |
+
links: {[name: string]: IUrdfLink}
|
7 |
+
joints: IUrdfJoint[]
|
8 |
+
// the DOM element holding the XML, so we can work non-destructive
|
9 |
+
elem?: Element
|
10 |
+
}
|
src/lib/components/scene/robot/URDF/interfaces/IUrdfVisual.ts
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type IUrdfBox from "./IUrdfBox";
|
2 |
+
import type IUrdfCylinder from "./IUrdfCylinder";
|
3 |
+
import type IUrdfMesh from "./IUrdfMesh";
|
4 |
+
|
5 |
+
// 1) Box‐type visual
|
6 |
+
interface IUrdfVisualBox {
|
7 |
+
name: string;
|
8 |
+
origin_xyz: [x: number, y: number, z: number];
|
9 |
+
origin_rpy: [roll: number, pitch: number, yaw: number];
|
10 |
+
geometry: IUrdfBox;
|
11 |
+
material?: {
|
12 |
+
name: string;
|
13 |
+
color?: string;
|
14 |
+
texture?: string;
|
15 |
+
},
|
16 |
+
type: 'box';
|
17 |
+
// optional RGBA color override
|
18 |
+
color_rgba?: [r: number, g: number, b: number, a: number];
|
19 |
+
// XML Element reference
|
20 |
+
elem: Element;
|
21 |
+
}
|
22 |
+
|
23 |
+
// 2) Cylinder‐type visual
|
24 |
+
interface IUrdfVisualCylinder {
|
25 |
+
name: string;
|
26 |
+
origin_xyz: [x: number, y: number, z: number];
|
27 |
+
origin_rpy: [roll: number, pitch: number, yaw: number];
|
28 |
+
geometry: IUrdfCylinder;
|
29 |
+
material?: {
|
30 |
+
name: string;
|
31 |
+
color?: string;
|
32 |
+
texture?: string;
|
33 |
+
},
|
34 |
+
type: 'cylinder';
|
35 |
+
color_rgba?: [r: number, g: number, b: number, a: number];
|
36 |
+
elem: Element;
|
37 |
+
}
|
38 |
+
|
39 |
+
// 3) Mesh‐type visual
|
40 |
+
interface IUrdfVisualMesh {
|
41 |
+
name: string;
|
42 |
+
origin_xyz: [x: number, y: number, z: number];
|
43 |
+
origin_rpy: [roll: number, pitch: number, yaw: number];
|
44 |
+
geometry: IUrdfMesh;
|
45 |
+
material?: {
|
46 |
+
name: string;
|
47 |
+
color?: string;
|
48 |
+
texture?: string;
|
49 |
+
},
|
50 |
+
type: 'mesh';
|
51 |
+
color_rgba?: [r: number, g: number, b: number, a: number];
|
52 |
+
elem: Element;
|
53 |
+
}
|
54 |
+
|
55 |
+
// Now make a union of the three:
|
56 |
+
export type IUrdfVisual = IUrdfVisualBox | IUrdfVisualCylinder | IUrdfVisualMesh;
|
src/lib/components/scene/robot/URDF/interfaces/index.ts
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export * from './IUrdfBox';
|
2 |
+
export * from './IUrdfCylinder';
|
3 |
+
export * from './IUrdfJoint';
|
4 |
+
export * from './IUrdfLink';
|
5 |
+
export * from './IUrdfMesh';
|
6 |
+
export * from './IUrdfRobot';
|
7 |
+
export * from './IUrdfVisual';
|
src/lib/components/scene/robot/URDF/mesh/DAE.svelte
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T, useLoader } from '@threlte/core';
|
3 |
+
import type { InteractivityProps } from '@threlte/extras';
|
4 |
+
import { DoubleSide, Mesh, type Side } from 'three';
|
5 |
+
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
|
6 |
+
|
7 |
+
type Props = InteractivityProps & {
|
8 |
+
filename: string;
|
9 |
+
color?: string;
|
10 |
+
scale?: [number, number, number];
|
11 |
+
position?: [number, number, number];
|
12 |
+
rotation?: [number, number, number];
|
13 |
+
castShadow?: boolean;
|
14 |
+
receiveShadow?: boolean;
|
15 |
+
opacity?: number;
|
16 |
+
isInteractive?: boolean;
|
17 |
+
wireframe?: boolean;
|
18 |
+
side?: Side;
|
19 |
+
};
|
20 |
+
let {
|
21 |
+
filename,
|
22 |
+
color = '#ffffff',
|
23 |
+
scale = [1, 1, 1],
|
24 |
+
position = [0, 0, 0],
|
25 |
+
rotation = [0, 0, 0],
|
26 |
+
opacity = 1.0,
|
27 |
+
castShadow = true,
|
28 |
+
receiveShadow = true,
|
29 |
+
isInteractive = false,
|
30 |
+
wireframe = false,
|
31 |
+
side = DoubleSide,
|
32 |
+
...restProps
|
33 |
+
}: Props = $props();
|
34 |
+
|
35 |
+
const sceneUp: [x: number, y: number, z: number] = [Math.PI / 2, -Math.PI / 2, -Math.PI / 2];
|
36 |
+
|
37 |
+
const { load } = useLoader(ColladaLoader);
|
38 |
+
</script>
|
39 |
+
|
40 |
+
{#await load(filename) then dae}
|
41 |
+
{@html `<!-- include dae: ${filename} ${scale} -->`}
|
42 |
+
<T.Group {scale} {position} {rotation}>
|
43 |
+
<T.Group rotation={sceneUp}>
|
44 |
+
<T.Group
|
45 |
+
scale={dae.scene.scale.toArray()}
|
46 |
+
position={dae.scene.position.toArray()}
|
47 |
+
rotation={dae.scene.rotation ? dae.scene.rotation.toArray() : [0, 0, 0]}
|
48 |
+
>
|
49 |
+
{#each dae.scene.children as obj}
|
50 |
+
{#if obj.type === 'Mesh'}
|
51 |
+
{@const mesh = obj as Mesh}
|
52 |
+
<T.Mesh
|
53 |
+
{castShadow}
|
54 |
+
{receiveShadow}
|
55 |
+
geometry={mesh.geometry}
|
56 |
+
scale={mesh.scale ? mesh.scale.toArray() : [1, 1, 1]}
|
57 |
+
position={mesh.position ? mesh.position.toArray() : [0, 0, 0]}
|
58 |
+
material={mesh.material}
|
59 |
+
{...restProps}
|
60 |
+
>
|
61 |
+
<T.MeshLambertMaterial {color} {opacity} transparent={opacity < 1.0} />
|
62 |
+
</T.Mesh>
|
63 |
+
{/if}
|
64 |
+
{/each}
|
65 |
+
</T.Group>
|
66 |
+
</T.Group>
|
67 |
+
</T.Group>
|
68 |
+
{/await}
|
69 |
+
|
70 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|
src/lib/components/scene/robot/URDF/mesh/OBJ.svelte
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T, useLoader } from '@threlte/core';
|
3 |
+
import type { InteractivityProps } from '@threlte/extras';
|
4 |
+
import { DoubleSide, type Side } from 'three';
|
5 |
+
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
6 |
+
|
7 |
+
type Props = InteractivityProps & {
|
8 |
+
filename: string;
|
9 |
+
color?: string;
|
10 |
+
scale?: [number, number, number];
|
11 |
+
position?: [number, number, number];
|
12 |
+
rotation?: [number, number, number];
|
13 |
+
castShadow?: boolean;
|
14 |
+
receiveShadow?: boolean;
|
15 |
+
opacity?: number;
|
16 |
+
isInteractive?: boolean;
|
17 |
+
wireframe?: boolean;
|
18 |
+
side?: Side;
|
19 |
+
};
|
20 |
+
let {
|
21 |
+
filename,
|
22 |
+
color = '#ffffff',
|
23 |
+
scale = [1, 1, 1],
|
24 |
+
rotation = [0, 0, 0],
|
25 |
+
position = [0, 0, 0],
|
26 |
+
opacity = 1.0,
|
27 |
+
castShadow = true,
|
28 |
+
receiveShadow = true,
|
29 |
+
isInteractive = false,
|
30 |
+
wireframe = false,
|
31 |
+
side = DoubleSide,
|
32 |
+
...restProps
|
33 |
+
}: Props = $props();
|
34 |
+
|
35 |
+
const { load } = useLoader(OBJLoader);
|
36 |
+
|
37 |
+
// let obj = $state<undefined | Group>();
|
38 |
+
|
39 |
+
// const objLoader = new OBJLoader();
|
40 |
+
// objLoader.load(filename, (data: Group) => {
|
41 |
+
// obj = data;
|
42 |
+
// });
|
43 |
+
</script>
|
44 |
+
|
45 |
+
{#await load(filename) then obj}
|
46 |
+
{@html `<!-- include obj: ${filename} ${scale} -->`}
|
47 |
+
<T is={obj} {position} {rotation} {scale} {castShadow} {receiveShadow} {side} {...restProps}>
|
48 |
+
<T.MeshLambertMaterial {color} {opacity} transparent={opacity < 1.0} {wireframe} {side} />
|
49 |
+
</T>
|
50 |
+
{/await}
|
51 |
+
|
52 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|
src/lib/components/scene/robot/URDF/mesh/STL.svelte
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T, useLoader } from '@threlte/core';
|
3 |
+
import type { InteractivityProps } from '@threlte/extras';
|
4 |
+
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
5 |
+
import { type Side, DoubleSide } from 'three';
|
6 |
+
import { interactivity } from '@threlte/extras';
|
7 |
+
|
8 |
+
type Props = InteractivityProps & {
|
9 |
+
filename: string;
|
10 |
+
color?: string;
|
11 |
+
scale?: [number, number, number];
|
12 |
+
position?: [number, number, number];
|
13 |
+
rotation?: [number, number, number];
|
14 |
+
castShadow?: boolean;
|
15 |
+
receiveShadow?: boolean;
|
16 |
+
opacity?: number;
|
17 |
+
isInteractive?: boolean;
|
18 |
+
wireframe?: boolean;
|
19 |
+
side?: Side;
|
20 |
+
};
|
21 |
+
|
22 |
+
let {
|
23 |
+
filename,
|
24 |
+
color = '#ffffff',
|
25 |
+
scale = [1, 1, 1],
|
26 |
+
position = [0, 0, 0],
|
27 |
+
rotation = [0, 0, 0],
|
28 |
+
opacity = 1.0,
|
29 |
+
castShadow = true,
|
30 |
+
receiveShadow = true,
|
31 |
+
isInteractive = false,
|
32 |
+
wireframe = false,
|
33 |
+
side = DoubleSide,
|
34 |
+
...restProps
|
35 |
+
}: Props = $props();
|
36 |
+
|
37 |
+
if (isInteractive) {
|
38 |
+
interactivity();
|
39 |
+
}
|
40 |
+
|
41 |
+
const { load } = useLoader(STLLoader);
|
42 |
+
</script>
|
43 |
+
|
44 |
+
{#await load(filename) then stl}
|
45 |
+
{@html `<!-- include stl: ${filename} ${scale} -->`}
|
46 |
+
<T.Mesh geometry={stl} {scale} {castShadow} {receiveShadow} {position} {rotation} {...restProps}>
|
47 |
+
<T.MeshLambertMaterial color={color} {opacity} transparent={opacity < 1.0} {wireframe} {side} />
|
48 |
+
</T.Mesh>
|
49 |
+
{/await}
|
50 |
+
|
51 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|
src/lib/components/scene/robot/URDF/primitives/UrdfJoint.svelte
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type IUrdfJoint from '../interfaces/IUrdfJoint';
|
3 |
+
import { T } from '@threlte/core';
|
4 |
+
import UrdfLink from './UrdfLink.svelte';
|
5 |
+
import { Vector3 } from 'three';
|
6 |
+
import {
|
7 |
+
Billboard,
|
8 |
+
interactivity,
|
9 |
+
MeshLineGeometry,
|
10 |
+
Text,
|
11 |
+
type InteractivityProps
|
12 |
+
} from '@threlte/extras';
|
13 |
+
import Selectable from '../../../Selectable.svelte';
|
14 |
+
|
15 |
+
import type IUrdfLink from '../interfaces/IUrdfLink';
|
16 |
+
import type IUrdfRobot from '../interfaces/IUrdfRobot';
|
17 |
+
|
18 |
+
const defaultOnClick: InteractivityProps['onclick'] = (event) => {
|
19 |
+
event.stopPropagation();
|
20 |
+
selectedLink = undefined;
|
21 |
+
selectedJoint = joint;
|
22 |
+
};
|
23 |
+
|
24 |
+
type Props = InteractivityProps & {
|
25 |
+
robot: IUrdfRobot;
|
26 |
+
joint: IUrdfJoint;
|
27 |
+
selectedJoint?: IUrdfJoint;
|
28 |
+
selectedLink?: IUrdfLink;
|
29 |
+
showName?: boolean;
|
30 |
+
nameHeight?: number;
|
31 |
+
highlightColor?: string;
|
32 |
+
jointColor?: string;
|
33 |
+
jointIndicatorColor?: string;
|
34 |
+
showLine?: boolean;
|
35 |
+
opacity?: number;
|
36 |
+
isInteractive?: boolean;
|
37 |
+
showVisual?: boolean;
|
38 |
+
showCollision?: boolean;
|
39 |
+
visualOpacity?: number;
|
40 |
+
collisionOpacity?: number;
|
41 |
+
collisionColor?: string;
|
42 |
+
jointNames?: boolean;
|
43 |
+
joints?: boolean;
|
44 |
+
};
|
45 |
+
|
46 |
+
let {
|
47 |
+
robot,
|
48 |
+
joint,
|
49 |
+
selectedJoint = $bindable(),
|
50 |
+
selectedLink = $bindable(),
|
51 |
+
showLine = false,
|
52 |
+
nameHeight = 0.02,
|
53 |
+
highlightColor = '#ff0000',
|
54 |
+
jointColor = '#000000',
|
55 |
+
jointIndicatorColor = '#000000',
|
56 |
+
opacity = 0.7,
|
57 |
+
isInteractive = false,
|
58 |
+
showName = false,
|
59 |
+
showVisual = true,
|
60 |
+
showCollision = true,
|
61 |
+
visualOpacity = 1,
|
62 |
+
collisionOpacity = 1,
|
63 |
+
collisionColor = '#000000',
|
64 |
+
jointNames = true,
|
65 |
+
onclick = defaultOnClick,
|
66 |
+
...restProps
|
67 |
+
}: Props = $props();
|
68 |
+
|
69 |
+
if (isInteractive) {
|
70 |
+
interactivity();
|
71 |
+
}
|
72 |
+
</script>
|
73 |
+
|
74 |
+
{@html `<!-- Joint ${joint.name} (${joint.type}) -->`}
|
75 |
+
{#if showName}
|
76 |
+
<Billboard
|
77 |
+
position.x={joint.origin_xyz[0]}
|
78 |
+
position.y={joint.origin_xyz[1]}
|
79 |
+
position.z={joint.origin_xyz[2]}
|
80 |
+
>
|
81 |
+
<Text
|
82 |
+
scale={nameHeight}
|
83 |
+
color={selectedJoint == joint ? highlightColor : jointColor}
|
84 |
+
text={joint.name}
|
85 |
+
characters="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
86 |
+
></Text>
|
87 |
+
</Billboard>
|
88 |
+
{/if}
|
89 |
+
|
90 |
+
<!-- draw line from parent-frame to joint origin -->
|
91 |
+
{#if showLine}
|
92 |
+
<T.Line>
|
93 |
+
<MeshLineGeometry
|
94 |
+
points={[
|
95 |
+
new Vector3(0, 0, 0),
|
96 |
+
new Vector3(joint.origin_xyz[0], joint.origin_xyz[1], joint.origin_xyz[2])
|
97 |
+
]}
|
98 |
+
/>
|
99 |
+
<T.LineBasicMaterial color={jointColor} />
|
100 |
+
</T.Line>
|
101 |
+
{/if}
|
102 |
+
|
103 |
+
<Selectable origin={joint} selected={selectedJoint == joint}>
|
104 |
+
<T.Group rotation={joint.rotation || [0, 0, 0]}>
|
105 |
+
{#if joint.child}
|
106 |
+
<UrdfLink
|
107 |
+
{robot}
|
108 |
+
link={joint.child}
|
109 |
+
textScale={0.02}
|
110 |
+
{showName}
|
111 |
+
{showVisual}
|
112 |
+
{showCollision}
|
113 |
+
{visualOpacity}
|
114 |
+
{collisionOpacity}
|
115 |
+
{collisionColor}
|
116 |
+
jointNames={true}
|
117 |
+
joints={true}
|
118 |
+
{jointColor}
|
119 |
+
{jointIndicatorColor}
|
120 |
+
{nameHeight}
|
121 |
+
{selectedLink}
|
122 |
+
{selectedJoint}
|
123 |
+
{highlightColor}
|
124 |
+
showLine={true}
|
125 |
+
opacity={1}
|
126 |
+
isInteractive={true}
|
127 |
+
/>
|
128 |
+
{/if}
|
129 |
+
|
130 |
+
{#if showLine}
|
131 |
+
<T.Line>
|
132 |
+
<MeshLineGeometry points={[new Vector3(0, 0, 0), new Vector3(0, -0.02, 0)]} />
|
133 |
+
<T.LineBasicMaterial color={jointIndicatorColor} />
|
134 |
+
</T.Line>
|
135 |
+
|
136 |
+
<T.Mesh rotation={[Math.PI / 2, 0, 0]} {...restProps}>
|
137 |
+
<T.CylinderGeometry args={[0.004, 0.004, 0.03]} />
|
138 |
+
<T.MeshBasicMaterial
|
139 |
+
color={selectedJoint == joint ? highlightColor : jointColor}
|
140 |
+
{opacity}
|
141 |
+
transparent={opacity < 1.0}
|
142 |
+
/>
|
143 |
+
</T.Mesh>
|
144 |
+
{/if}
|
145 |
+
</T.Group>
|
146 |
+
</Selectable>
|
147 |
+
|
148 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|
src/lib/components/scene/robot/URDF/primitives/UrdfLink.svelte
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
// The link element describes a rigid body with an inertia, visual features, and collision properties.
|
3 |
+
import type IUrdfLink from '../interfaces/IUrdfLink';
|
4 |
+
import UrdfVisual from './UrdfVisual.svelte';
|
5 |
+
import { getChildJoints } from '../utils/UrdfParser';
|
6 |
+
import UrdfJoint from './UrdfJoint.svelte';
|
7 |
+
import { Billboard, Text } from '@threlte/extras';
|
8 |
+
import type IUrdfRobot from '../interfaces/IUrdfRobot';
|
9 |
+
import type IUrdfJoint from '../interfaces/IUrdfJoint';
|
10 |
+
|
11 |
+
interface Props {
|
12 |
+
robot: IUrdfRobot;
|
13 |
+
link: IUrdfLink;
|
14 |
+
textScale?: number;
|
15 |
+
showName?: boolean;
|
16 |
+
showVisual?: boolean;
|
17 |
+
showCollision?: boolean;
|
18 |
+
visualColor?: string;
|
19 |
+
visualOpacity?: number;
|
20 |
+
collisionOpacity?: number;
|
21 |
+
collisionColor?: string;
|
22 |
+
jointNames?: boolean;
|
23 |
+
joints?: boolean;
|
24 |
+
jointColor?: string;
|
25 |
+
jointIndicatorColor?: string;
|
26 |
+
nameHeight?: number;
|
27 |
+
selectedLink?: IUrdfLink;
|
28 |
+
selectedJoint?: IUrdfJoint;
|
29 |
+
highlightColor?: string;
|
30 |
+
showLine?: boolean;
|
31 |
+
opacity?: number;
|
32 |
+
isInteractive?: boolean;
|
33 |
+
}
|
34 |
+
|
35 |
+
let {
|
36 |
+
robot,
|
37 |
+
link,
|
38 |
+
textScale = 1,
|
39 |
+
showName = true,
|
40 |
+
showVisual = true,
|
41 |
+
showCollision = true,
|
42 |
+
visualColor = '#000000',
|
43 |
+
visualOpacity = 1,
|
44 |
+
collisionOpacity = 1,
|
45 |
+
collisionColor = '#000000',
|
46 |
+
jointNames = true,
|
47 |
+
joints = true,
|
48 |
+
jointColor = '#000000',
|
49 |
+
jointIndicatorColor = '#000000',
|
50 |
+
nameHeight = 0.1,
|
51 |
+
selectedLink = undefined,
|
52 |
+
selectedJoint = undefined,
|
53 |
+
highlightColor = '#000000',
|
54 |
+
showLine = true,
|
55 |
+
opacity = 0.7,
|
56 |
+
isInteractive = false
|
57 |
+
}: Props = $props();
|
58 |
+
</script>
|
59 |
+
|
60 |
+
{@html `<!-- Link ${link.name} -->`}
|
61 |
+
{#if showName}
|
62 |
+
<!-- <Billboard position.x={0} position.y={0} position.z={0}>
|
63 |
+
<Text anchorY={-0.2} scale={textScale} text={link.name} characters="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
64 |
+
></Text>
|
65 |
+
</Billboard> -->
|
66 |
+
{/if}
|
67 |
+
|
68 |
+
{#if showVisual}
|
69 |
+
{#each link.visual as visual}
|
70 |
+
<UrdfVisual
|
71 |
+
opacity={visualOpacity}
|
72 |
+
{visual}
|
73 |
+
defaultColor={visualColor}
|
74 |
+
/>
|
75 |
+
{/each}
|
76 |
+
{/if}
|
77 |
+
|
78 |
+
{#if showCollision}
|
79 |
+
{#each link.collision as visual}
|
80 |
+
<UrdfVisual
|
81 |
+
opacity={collisionOpacity}
|
82 |
+
visual={visual}
|
83 |
+
defaultColor={collisionColor}
|
84 |
+
/>
|
85 |
+
{/each}
|
86 |
+
{/if}
|
87 |
+
|
88 |
+
{#each getChildJoints(robot, link) as joint}
|
89 |
+
<UrdfJoint
|
90 |
+
{robot}
|
91 |
+
joint={joint}
|
92 |
+
{selectedJoint}
|
93 |
+
{selectedLink}
|
94 |
+
{showName}
|
95 |
+
{nameHeight}
|
96 |
+
{highlightColor}
|
97 |
+
{jointColor}
|
98 |
+
{jointIndicatorColor}
|
99 |
+
{showLine}
|
100 |
+
{opacity}
|
101 |
+
{isInteractive}
|
102 |
+
{showVisual}
|
103 |
+
{showCollision}
|
104 |
+
{visualOpacity}
|
105 |
+
{collisionOpacity}
|
106 |
+
{collisionColor}
|
107 |
+
{jointNames}
|
108 |
+
{joints}
|
109 |
+
/>
|
110 |
+
{/each}
|
111 |
+
|
112 |
+
<!-- From https://github.com/brean/urdf-viewer -->
|