blanchon commited on
Commit
3aea7c6
·
0 Parent(s):

squash: initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +22 -0
  2. .gitattributes +1 -0
  3. .gitignore +28 -0
  4. .npmrc +1 -0
  5. .prettierignore +6 -0
  6. .prettierrc +15 -0
  7. DOCKER_README.md +287 -0
  8. Dockerfile +71 -0
  9. README.md +265 -0
  10. ROBOT_ARCHITECTURE.md +416 -0
  11. ROBOT_INSTANCING_README.md +73 -0
  12. bun.lock +652 -0
  13. docker-compose.yml +14 -0
  14. eslint.config.js +36 -0
  15. package.json +47 -0
  16. src-python/.gitignore +10 -0
  17. src-python/.python-version +1 -0
  18. src-python/README.md +656 -0
  19. src-python/pyproject.toml +43 -0
  20. src-python/src/__init__.py +1 -0
  21. src-python/src/connection_manager.py +147 -0
  22. src-python/src/main.py +554 -0
  23. src-python/src/models.py +191 -0
  24. src-python/src/robot_manager.py +168 -0
  25. src-python/start_server.py +38 -0
  26. src-python/uv.lock +239 -0
  27. src/app.css +1 -0
  28. src/app.d.ts +13 -0
  29. src/app.html +12 -0
  30. src/lib/components/panel/ControlPanel.svelte +60 -0
  31. src/lib/components/panel/Panels.svelte +78 -0
  32. src/lib/components/panel/RobotControlPanel.svelte +649 -0
  33. src/lib/components/panel/SettingsPanel.svelte +248 -0
  34. src/lib/components/scene/Floor.svelte +56 -0
  35. src/lib/components/scene/Robot.svelte +44 -0
  36. src/lib/components/scene/Selectable.svelte +68 -0
  37. src/lib/components/scene/robot/URDF/createRobot.svelte.ts +27 -0
  38. src/lib/components/scene/robot/URDF/interfaces/IUrdfBox.ts +3 -0
  39. src/lib/components/scene/robot/URDF/interfaces/IUrdfCylinder.ts +4 -0
  40. src/lib/components/scene/robot/URDF/interfaces/IUrdfJoint.ts +42 -0
  41. src/lib/components/scene/robot/URDF/interfaces/IUrdfLink.ts +23 -0
  42. src/lib/components/scene/robot/URDF/interfaces/IUrdfMesh.ts +5 -0
  43. src/lib/components/scene/robot/URDF/interfaces/IUrdfRobot.ts +10 -0
  44. src/lib/components/scene/robot/URDF/interfaces/IUrdfVisual.ts +56 -0
  45. src/lib/components/scene/robot/URDF/interfaces/index.ts +7 -0
  46. src/lib/components/scene/robot/URDF/mesh/DAE.svelte +70 -0
  47. src/lib/components/scene/robot/URDF/mesh/OBJ.svelte +52 -0
  48. src/lib/components/scene/robot/URDF/mesh/STL.svelte +51 -0
  49. src/lib/components/scene/robot/URDF/primitives/UrdfJoint.svelte +148 -0
  50. 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 -->