diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..1b06ec16d833e1fe2fdbcd7351ba71381e716f2c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +# Dependencies +node_modules/ + +# Build outputs (will be built in container) +build/ +.svelte-kit/ +dist/ + +# Development files +.env* +!.env.example + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* +bun-debug.log* +lerna-debug.log* + +# Cache directories +.cache/ +.temp/ +.tmp/ + +# Test files +coverage/ +.nyc_output/ + +# Other build artifacts +*.tgz +*.tar.gz + +# Docker files +Dockerfile* +docker-compose* +.dockerignore + +# Documentation that's not needed in container +README.md +CHANGELOG.md +*.md +!LICENSE \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..fc1c9c83467baa463cc267a732a0bafe47a457c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.stl filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +static/gpu/scene.bin filter=lfs diff=lfs merge=lfs -text +static/video.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a122eb5753ae5ab3f166ce1d55caa72d35c9cac7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..fbb8603ae2d4904c6bc70f602602368a091f56f0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "external/RobotHub-TransportServer"] + path = external/RobotHub-TransportServer + url = https://github.com/julien-blanchon/RobotHub-TransportServer +[submodule "external/RobotHub-InferenceServer"] + path = external/RobotHub-InferenceServer + url = https://github.com/julien-blanchon/RobotHub-InferenceServer diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..b6f27f135954640c8cc5bfd7b8c9922ca6eb2aad --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..32db49eeb944e7e5b085dd79aafaeee756f24ed8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Src python +src-python/ +node_modules/ +build/ +.svelte-kit/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..3bb73aca3e957612e92e95b7f9d5d87cb2977ec3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": false, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte", + "svelteSortOrder": "options-scripts-markup-styles" + } + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0e314b259891af8823525d333d4b2692e9d1754c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Multi-stage Dockerfile for LeRobot Arena Frontend +# Stage 1: Build the Svelte application with Bun +FROM oven/bun:1-alpine AS builder + +WORKDIR /app + +# Install git for dependencies that might need it +RUN apk add --no-cache git + +# Copy package files for dependency resolution (better caching) +COPY package.json bun.lock* ./ + +# Copy local packages that are linked in package.json +COPY packages/ ./packages/ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the static application +RUN bun run build + +# Stage 2: Serve with Bun's simple static server +FROM oven/bun:1-alpine AS production + +# Set up a new user named "user" with user ID 1000 (required for HF Spaces) +RUN adduser -D -u 1000 user + +# Switch to the "user" user +USER user + +# Set home to the user's home directory +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH + +# Set the working directory to the user's home directory +WORKDIR $HOME/app + +# Copy built application from previous stage with proper ownership +COPY --chown=user --from=builder /app/build ./ + +# Expose port 7860 (HF Spaces default) +EXPOSE 7860 + +# Start simple static server using Bun +CMD ["bun", "--bun", "serve", ".", "--port", "7860"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1df26cd69c14b6a40199d268c0375bb86648978b --- /dev/null +++ b/README.md @@ -0,0 +1,297 @@ +--- +title: LeRobot Arena Frontend +emoji: 🤖 +colorFrom: blue +colorTo: purple +sdk: static +app_build_command: bun install && bun run build +app_file: build/index.html +pinned: false +license: mit +short_description: A web-based robotics control and simulation platform +tags: + - robotics + - control + - simulation + - svelte + - static + - frontend +--- + +# 🤖 LeRobot Arena + +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. + +## 🚀 Simple Deployment Options + +Here are the easiest ways to deploy this Svelte frontend: + +### 🏆 Option 1: Hugging Face Spaces (Static) - RECOMMENDED ✨ + +**Automatic deployment** (easiest): +1. **Fork this repository** to your GitHub account +2. **Create a new Space** on [Hugging Face Spaces](https://huggingface.co/spaces) +3. **Connect your GitHub repo** - it will auto-detect the static SDK +4. **Push to main branch** - auto-builds and deploys! + +The frontmatter is already configured with: +```yaml +sdk: static +app_build_command: bun install && bun run build +app_file: build/index.html +``` + +**Manual upload**: +1. Run `bun install && bun run build` locally +2. Create a Space with "Static HTML" SDK +3. Upload all files from `build/` folder + +### 🚀 Option 2: Vercel - One-Click Deploy + +[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new) + +Settings: Build command `bun run build`, Output directory `build` + +### 📁 Option 3: Netlify - Drag & Drop + +1. Build locally: `bun install && bun run build` +2. Drag `build/` folder to [Netlify](https://netlify.com) + +### 🆓 Option 4: GitHub Pages + +Add this workflow file (`.github/workflows/deploy.yml`): +```yaml +name: Deploy to GitHub Pages +on: + push: + branches: [ main ] +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install --frozen-lockfile + - run: bun run build + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build +``` + +### 🐳 Option 5: Docker (Optional) + +For local development or custom hosting: +```bash +docker build -t lerobot-arena-frontend . +docker run -p 7860:7860 lerobot-arena-frontend +``` + +The Docker setup uses Bun's simple static server - much simpler than the complex server.js approach! + +## 🛠️ Development Setup + +For local development with hot-reload capabilities: + +### Frontend Development + +```bash +# Install dependencies +bun install + +# Start the development server +bun run dev + +# Or open in browser automatically +bun run dev -- --open +``` + +### Backend Development + +```bash +# Navigate to Python backend +cd src-python + +# Install Python dependencies (using uv) +uv sync + +# Or using pip +pip install -e . + +# Start the backend server +python start_server.py +``` + +### Building Standalone Executable + +The backend can be packaged as a standalone executable using box-packager: + +```bash +# Navigate to Python backend +cd src-python + +# Install box-packager (if not already installed) +uv pip install box-packager + +# Package the application +box package + +# The executable will be in target/release/lerobot-arena-server +./target/release/lerobot-arena-server +``` + +Note: Requires [Rust/Cargo](https://rustup.rs/) to be installed for box-packager to work. + +## 📋 Project Structure + +``` +lerobot-arena/ +├── src/ # Svelte frontend source +│ ├── lib/ # Reusable components and utilities +│ ├── routes/ # SvelteKit routes +│ └── app.html # App template +├── src-python/ # Python backend +│ ├── src/ # Python source code +│ ├── start_server.py # Server entry point +│ ├── target/ # Box-packager build output (excluded from git) +│ └── pyproject.toml # Python dependencies +├── static/ # Static assets +├── Dockerfile # Docker configuration +├── docker-compose.yml # Docker Compose setup +└── package.json # Node.js dependencies +``` + +## 🐳 Docker Information + +The Docker setup includes: + +- **Multi-stage build**: Optimized for production using Bun and uv +- **Automatic startup**: Both services start together +- **Port mapping**: Backend on 8080, Frontend on 7860 (HF Spaces compatible) +- **Static file serving**: Compiled Svelte app served efficiently +- **User permissions**: Properly configured for Hugging Face Spaces +- **Standalone executable**: Backend packaged with box-packager for faster startup + +For detailed Docker documentation, see [DOCKER_README.md](./DOCKER_README.md). + +## 🔧 Building for Production + +### Frontend Only + +```bash +bun run build +``` + +### Backend Standalone Executable + +```bash +cd src-python +box package +``` + +### Complete Docker Build + +```bash +docker-compose up --build +``` + +## 🌐 What's Included + +- **Real-time Robot Control**: WebSocket-based communication +- **3D Visualization**: Three.js integration for robot visualization +- **URDF Support**: Load and display robot models +- **Multi-robot Management**: Control multiple robots simultaneously +- **WebSocket API**: Real-time bidirectional communication +- **Standalone Distribution**: Self-contained executable with box-packager + +## 🚨 Troubleshooting + +### Port Conflicts + +If ports 8080 or 7860 are already in use: + +```bash +# Check what's using the ports +lsof -i :8080 +lsof -i :7860 + +# Use different ports +docker run -p 8081:8080 -p 7861:7860 lerobot-arena +``` + +### Container Issues + +```bash +# View logs +docker-compose logs lerobot-arena + +# Rebuild without cache +docker-compose build --no-cache +docker-compose up +``` + +### Development Issues + +```bash +# Clear node modules and reinstall +rm -rf node_modules +bun install + +# Clear Svelte kit cache +rm -rf .svelte-kit +bun run dev +``` + +### Box-packager Issues + +```bash +# Clean build artifacts +cd src-python +box clean + +# Rebuild executable +box package + +# Install cargo if missing +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +## 🚀 Hugging Face Spaces Deployment + +This project is configured for **Static HTML** deployment on Hugging Face Spaces (much simpler than Docker!): + +**Manual Upload (Easiest):** +1. Run `bun install && bun run build` locally +2. Create a new Space with "Static HTML" SDK +3. Upload all files from `build/` folder +4. Your app is live! + +**GitHub Integration:** +1. Fork this repository +2. Create a Space and connect your GitHub repo +3. The Static HTML SDK will be auto-detected from the README frontmatter +4. Push changes to auto-deploy + +No Docker, no complex setup - just static files! 🎉 + +## 📚 Additional Documentation + +- [Docker Setup Guide](./DOCKER_README.md) - Detailed Docker instructions +- [Robot Architecture](./ROBOT_ARCHITECTURE.md) - System architecture overview +- [Robot Instancing Guide](./ROBOT_INSTANCING_README.md) - Multi-robot setup + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test with Docker: `docker-compose up --build` +5. Submit a pull request + +## 📄 License + +This project is licensed under the MIT License. + +--- + +**Built with ❤️ for the robotics community** 🤖 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000000000000000000000000000000000000..dac48f8836b399eddbeec78477c72ae768b55161 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1170 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "my-app", + "dependencies": { + "@robohub/inference-server-client": "file:../backend/inference-server/client", + "@robohub/transport-server-client": "file:../backend/transport-server/client/js", + "@threlte/core": "^8.0.4", + "@threlte/extras": "^9.2.1", + "@types/three": "0.177.0", + "clsx": "^2.1.1", + "feetech.js": "file:./packages/feetech.js", + "tailwind-merge": "^3.3.0", + "three": "^0.177.0", + "threlte-uikit": "^1.1.0", + "zod": "^3.25.56", + }, + "devDependencies": { + "@eslint/compat": "^1.2.9", + "@eslint/js": "^9.28.0", + "@iconify/json": "^2.2.346", + "@iconify/svelte": "^5.0.0", + "@iconify/tailwind4": "^1.0.6", + "@internationalized/date": "^3.8.2", + "@lucide/svelte": "^0.511.0", + "@sveltejs/adapter-auto": "^6.0.1", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.21.2", + "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@tailwindcss/vite": "^4.0.0", + "bits-ui": "^2.4.1", + "eslint": "^9.28.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-svelte": "^3.9.1", + "globals": "^16.2.0", + "layerchart": "1.0.11", + "mode-watcher": "^1.0.7", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.33.17", + "svelte-check": "^4.2.1", + "svelte-sonner": "^1.0.4", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.33.1", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^6.3.5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@antfu/utils": ["@antfu/utils@8.1.1", "", {}, "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ=="], + + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.7.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA=="], + + "@dagrejs/dagre": ["@dagrejs/dagre@1.1.4", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg=="], + + "@dagrejs/graphlib": ["@dagrejs/graphlib@2.2.4", "", {}, "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw=="], + + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + + "@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=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/compat": ["@eslint/compat@1.2.9", "", { "peerDependencies": { "eslint": "^9.10.0" }, "optionalPeers": ["eslint"] }, "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA=="], + + "@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=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], + + "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], + + "@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=="], + + "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@hey-api/client-fetch": ["@hey-api/client-fetch@0.2.4", "", {}, "sha512-SGTVAVw3PlKDLw+IyhNhb/jCH3P1P2xJzLxA8Kyz1g95HrkYOJdRpl9F5I7LLwo9aCIB7nwR2NrSeX7QaQD7vQ=="], + + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.53.12", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.2", "c12": "2.0.1", "commander": "12.1.0", "handlebars": "4.7.8" }, "peerDependencies": { "typescript": "^5.x" }, "bin": { "openapi-ts": "bin/index.cjs" } }, "sha512-cOm8AlUqJIWdLXq+Pk4mTXhEApRSc9xEWTVT8MZAyEqrN1Yhiisl2wyZGH9quzKpolq+oqvgcx61txtwHwi8vQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@iconify/json": ["@iconify/json@2.2.346", "", { "dependencies": { "@iconify/types": "*", "pathe": "^1.1.2" } }, "sha512-QcJNRnHf9UMuGdtbIISsGbUf/AArTpBr4ItaoBYryRjPiq7DHH7kcvbMdHpYcGvAMa6vidaL7g31iTLhOBgnyA=="], + + "@iconify/svelte": ["@iconify/svelte@5.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">4.0.0" } }, "sha512-Bm5iKnNpIWHuUlWaaf16QgVL1tcbQUUSdPdkvDlpmYXg3nFJSA1SLY9V4rRTd+K4h3QT12ZS7JHFLkf8khQd8Q=="], + + "@iconify/tailwind4": ["@iconify/tailwind4@1.0.6", "", { "dependencies": { "@iconify/types": "^2.0.0", "@iconify/utils": "^2.2.1" }, "peerDependencies": { "tailwindcss": ">= 4" } }, "sha512-43ZXe+bC7CuE2LCgROdqbQeFYJi/J7L/k1UpSy8KDQlWVsWxPzLSWbWhlJx4uRYLOh1NRyw02YlDOgzBOFNd+A=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="], + + "@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@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=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@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=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-Tv8B3TeT7oaghx0R0I4avnSdfAT6GxEK+StL8k/hEaa009iNOIGFl3f76kfvNvPioQHAMFGtnWGLPHfsfD41nQ=="], + + "@layerstack/svelte-stores": ["@layerstack/svelte-stores@1.0.2", "", { "dependencies": { "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "date-fns": "^4.1.0", "immer": "^10.1.1", "lodash-es": "^4.17.21", "zod": "^3.24.2" } }, "sha512-IxK0UKD0PVxg1VsyaR+n7NyJ+NlvyqvYYAp+J10lkjDQxm0yx58CaF2LBV08T22C3aY1iTlqJaatn/VHV4SoQg=="], + + "@layerstack/tailwind": ["@layerstack/tailwind@1.0.1", "", { "dependencies": { "@layerstack/utils": "^1.0.1", "clsx": "^2.1.1", "culori": "^4.0.1", "d3-array": "^3.2.4", "date-fns": "^4.1.0", "lodash-es": "^4.17.21", "tailwind-merge": "^2.5.4", "tailwindcss": "^3.4.15" } }, "sha512-nlshEkUCfaV0zYzrFXVVYRnS8bnBjs4M7iui6l/tu6NeBBlxDivIyRraJkdYGCSL1lZHi6FqacLQ3eerHtz90A=="], + + "@layerstack/utils": ["@layerstack/utils@1.0.1", "", { "dependencies": { "d3-array": "^3.2.4", "date-fns": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-sWP9b+SFMkJYMZyYFI01aLxbg2ZUrix6Tv+BCDmeOrcLNxtWFsMYAomMhALzTMHbb+Vis/ua5vXhpdNXEw8a2Q=="], + + "@lucide/svelte": ["@lucide/svelte@0.511.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-aLCSPMUJmHlCuLXzXENXa4Z1NV2mN1iAZAFKk4bEbey+/MdsNlu+/DqwVkgW3Yvj6p8y8Vn5xZ2v9CLmPlA6Vw=="], + + "@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=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@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=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pmndrs/msdfonts": ["@pmndrs/msdfonts@0.8.19", "", {}, "sha512-O+86cpGBPeEg2cD+HPViv+hombzqgmSlH047X/w9NnYd0r5ZmIvyDNRXSb4G2yx6y5YRQYOEs0vNN7aqxo8zgw=="], + + "@pmndrs/uikit": ["@pmndrs/uikit@0.8.19", "", { "dependencies": { "@pmndrs/msdfonts": "^0.8.19", "@preact/signals-core": "^1.5.1", "inline-style-parser": "^0.2.3", "node-html-parser": "^6.1.13", "tw-to-css": "^0.0.12", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.160" } }, "sha512-DvqTIqA+sVGezl7Nwmf0iZFquGPjXxVUOmC3Oo6UEYQbjaCgXkPwMQ5DGQAu2Ti6sr1+Y6PgaZnkXKtLWLFCXQ=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@preact/signals-core": ["@preact/signals-core@1.9.0", "", {}, "sha512-uUgFHJLWxb33rfCtb1g+1e3Rg7Jl5EALhGTHlQ5Y0w37OF+fdidYdYEE6crbpUOYDOjlmelIWf0ulXr1ggfUkg=="], + + "@robohub/inference-server-client": ["@robohub/inference-server-client@file:../backend/inference-server/client", { "dependencies": { "@hey-api/client-fetch": "^0.2.4" }, "devDependencies": { "@hey-api/openapi-ts": "^0.53.12", "@types/bun": "latest", "typescript": "^5.8.3" }, "peerDependencies": { "typescript": "^5" } }], + + "@robohub/transport-server-client": ["@robohub/transport-server-client@file:../backend/transport-server/client/js", { "dependencies": { "eventemitter3": "^5.0.1" }, "devDependencies": { "@types/bun": "^1.2.15", "typescript": "^5.3.3" }, "peerDependencies": { "typescript": ">=5.0.0" } }], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.42.0", "", { "os": "android", "cpu": "arm" }, "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.42.0", "", { "os": "android", "cpu": "arm64" }, "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.42.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.42.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.42.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.42.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.42.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.42.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.42.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.42.0", "", { "os": "linux", "cpu": "none" }, "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.42.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.42.0", "", { "os": "linux", "cpu": "none" }, "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.42.0", "", { "os": "linux", "cpu": "none" }, "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.42.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.42.0", "", { "os": "linux", "cpu": "x64" }, "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.42.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.42.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], + + "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ=="], + + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.21.2", "", { "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-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw=="], + + "@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=="], + + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + + "@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=="], + + "@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=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="], + + "@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=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="], + + "@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=="], + + "@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=="], + + "@threlte/core": ["@threlte/core@8.0.5", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.160" } }, "sha512-4SvufZJpETJxk8k+8BxPrKtb36OKOoFJrSs9oe1oFqaOdamLAQgSt5Rvt4NrFMhT7fYvKJI0vYumEb+AzsSBiA=="], + + "@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=="], + + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], + + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.177.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-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A=="], + + "@types/webxr": ["@types/webxr@0.5.22", "", {}, "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "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.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1" } }, "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "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-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.1", "@typescript-eslint/tsconfig-utils": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "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-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="], + + "@webgpu/types": ["@webgpu/types@0.1.61", "", {}, "sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "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=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "bits-ui": ["bits-ui@2.4.1", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/dom": "^1.7.0", "css.escape": "^1.5.1", "esm-env": "^1.1.2", "runed": "^0.28.0", "svelte-toolbelt": "^0.9.1", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-Z0qZkPgtxP5dkEOyZqCK2PQ1oFXsD0AlfFMtsrMLJqaYtjUBoQGosHVKEH03M9q4G8kxVnG74Zb0F9pS0X4+6w=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + + "c12": ["c12@2.0.1", "", { "dependencies": { "chokidar": "^4.0.1", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^2.3.0", "mlly": "^1.7.1", "ohash": "^1.1.4", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "camera-controls": ["camera-controls@2.10.1", "", { "peerDependencies": { "three": ">=0.126.1" } }, "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "culori": ["culori@4.0.1", "", {}, "sha512-LSnjA6HuIUOlkfKVbzi2OlToZE8OjFi667JWN9qNymXVXzGDmvuP60SSgC+e92sd7B7158f7Fy3Mb6rXS5EDPw=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-geo-voronoi": ["d3-geo-voronoi@2.1.0", "", { "dependencies": { "d3-array": "3", "d3-delaunay": "6", "d3-geo": "3", "d3-tricontour": "1" } }, "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-interpolate-path": ["d3-interpolate-path@2.3.0", "", {}, "sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-tile": ["d3-tile@1.0.0", "", {}, "sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-tricontour": ["d3-tricontour@1.0.2", "", { "dependencies": { "d3-delaunay": "6", "d3-scale": "4" } }, "sha512-HIRxHzHagPtUPNabjOlfcyismJYIsc+Xlq4mlsts4e8eAcwyq9Tgk/sYdyhlBpQ0MHwVquc/8j+e29YjXnmxeA=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "diet-sprite": ["diet-sprite@0.0.1", "", {}, "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], + + "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "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=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "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=="], + + "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=="], + + "eslint-plugin-svelte": ["eslint-plugin-svelte@3.9.1", "", { "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-mXFulSdD/0/p+zwENjPNsiVwAqmSRp90sy5zvVQBX1yAXhJbdhIn6C/tn8BZYjU94Ia7Y87d1Xdbvi49DeWyHQ=="], + + "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "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=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrap": ["esrap@1.4.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-0ZxW6guTF/AeKeKi7he93lmgv7Hx7giD1tBrOeVqkqsZGQJd2/kfnL7LdIsr9FT/AtkBK9XeDTov+gxprBqdEg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "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=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="], + + "feetech.js": ["feetech.js@file:packages/feetech.js", {}], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "known-css-properties": ["known-css-properties@0.36.0", "", {}, "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA=="], + + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + + "layercake": ["layercake@8.4.3", "", { "dependencies": { "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0" }, "peerDependencies": { "svelte": "3 - 5 || >=5.0.0-next.120", "typescript": "^5.0.2" } }, "sha512-PZDduaPFxgHHkxlmsz5MVBECf6ZCT39DI3LgMVvuMwrmlrtlXwXUM/elJp46zHYzCE1j+cGyDuBDxnANv94tOQ=="], + + "layerchart": ["layerchart@1.0.11", "", { "dependencies": { "@dagrejs/dagre": "^1.1.4", "@layerstack/svelte-actions": "^1.0.1", "@layerstack/svelte-stores": "^1.0.2", "@layerstack/tailwind": "^1.0.1", "@layerstack/utils": "^1.0.1", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", "d3-dsv": "^3.0.1", "d3-force": "^3.0.0", "d3-geo": "^3.1.1", "d3-geo-voronoi": "^2.1.0", "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-interpolate-path": "^2.3.0", "d3-path": "^3.1.0", "d3-quadtree": "^3.0.1", "d3-random": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "d3-shape": "^3.2.0", "d3-tile": "^1.0.0", "d3-time": "^3.1.0", "date-fns": "^4.1.0", "layercake": "^8.4.3", "lodash-es": "^4.17.21" }, "peerDependencies": { "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0" } }, "sha512-1Na3BO3a32x0X8PR3vCl7zwiD0NMp90deoxOJPrmIy+34INEPeyowv7NuJ0zfjNMA4BCDTYj/1xK4nnIIh8Nmw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "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=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + + "mode-watcher": ["mode-watcher@1.0.7", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], + + "node-html-parser": ["node-html-parser@6.1.13", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + + "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=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="], + + "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=="], + + "postcss-css-variables": ["postcss-css-variables@0.18.0", "", { "dependencies": { "balanced-match": "^1.0.0", "escape-string-regexp": "^1.0.3", "extend": "^3.0.1" }, "peerDependencies": { "postcss": "^8.2.6" } }, "sha512-lYS802gHbzn1GI+lXvy9MYIYDuGnl1WB4FTKoqMQqJ3Mab09A7a/1wZvGTkCEZJTM8mSbIyb1mJYn8f0aPye0Q=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], + + "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=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], + + "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], + + "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=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "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=="], + + "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=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + + "rollup": ["rollup@4.42.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.42.0", "@rollup/rollup-android-arm64": "4.42.0", "@rollup/rollup-darwin-arm64": "4.42.0", "@rollup/rollup-darwin-x64": "4.42.0", "@rollup/rollup-freebsd-arm64": "4.42.0", "@rollup/rollup-freebsd-x64": "4.42.0", "@rollup/rollup-linux-arm-gnueabihf": "4.42.0", "@rollup/rollup-linux-arm-musleabihf": "4.42.0", "@rollup/rollup-linux-arm64-gnu": "4.42.0", "@rollup/rollup-linux-arm64-musl": "4.42.0", "@rollup/rollup-linux-loongarch64-gnu": "4.42.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0", "@rollup/rollup-linux-riscv64-gnu": "4.42.0", "@rollup/rollup-linux-riscv64-musl": "4.42.0", "@rollup/rollup-linux-s390x-gnu": "4.42.0", "@rollup/rollup-linux-x64-gnu": "4.42.0", "@rollup/rollup-linux-x64-musl": "4.42.0", "@rollup/rollup-win32-arm64-msvc": "4.42.0", "@rollup/rollup-win32-ia32-msvc": "4.42.0", "@rollup/rollup-win32-x64-msvc": "4.42.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "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=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="], + + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svelte": ["svelte@5.33.17", "", { "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-y453Ac1Xi/MFXbGIDK89YH1CeXlFk+aTsDQ1uMc7iE+iVau6ZnE4t3iWsr0oxKnIApjfI7CL4R1NEHzVMUrkVg=="], + + "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=="], + + "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=="], + + "svelte-sonner": ["svelte-sonner@1.0.4", "", { "dependencies": { "runed": "^0.26.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-ctm9jeV0Rf3im2J6RU1emccrJFjRSdNSPsLlxaF62TLZw9bB1D40U/U7+wqEgohJY/X7FBdghdj0BFQF/IqKPQ=="], + + "svelte-toolbelt": ["svelte-toolbelt@0.9.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.28.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g=="], + + "tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="], + + "tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="], + + "tailwind-variants": ["tailwind-variants@1.0.0", "", { "dependencies": { "tailwind-merge": "3.0.2" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA=="], + + "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="], + + "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], + + "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=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + + "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=="], + + "three-mesh-bvh": ["three-mesh-bvh@0.9.0", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-xAwZj0hZknpwVsdK5BBJTIAZDjDPZCRzURY1o+z/JHBON/jc2UetK1CzPeQZiiOVSfI4jV2z7sXnnGtgsgnjaA=="], + + "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"], + + "three-viewport-gizmo": ["three-viewport-gizmo@2.2.0", "", { "peerDependencies": { "three": ">=0.162.0 <1.0.0" } }, "sha512-Jo9Liur1rUmdKk75FZumLU/+hbF+RtJHi1qsKZpntjKlCYScK6tjbYoqvJ9M+IJphrlQJF5oReFW7Sambh0N4Q=="], + + "threlte-uikit": ["threlte-uikit@1.1.0", "", { "dependencies": { "@pmndrs/uikit": "^0.8.19", "@preact/signals-core": "^1.9.0" }, "peerDependencies": { "@threlte/core": ">=8", "svelte": ">=5", "three": ">=0.160" } }, "sha512-2zfWr1N58Ki60M8BDQ1AGWin9bwaKV6OHtlNumbLQXbDpHqVJTLlBLkR5LX+8DqTh/zyL617dg8KqWzEYUT73A=="], + + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "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=="], + + "troika-three-utils": ["troika-three-utils@0.52.4", "", { "peerDependencies": { "three": ">=0.125.0" } }, "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A=="], + + "troika-worker-utils": ["troika-worker-utils@0.52.0", "", {}, "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="], + + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.3.4", "", {}, "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg=="], + + "tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="], + + "tweakpane": ["tweakpane@3.1.10", "", {}, "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vaul-svelte": ["vaul-svelte@1.0.0-next.7", "", { "dependencies": { "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg=="], + + "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=="], + + "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=="], + + "webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], + + "zod": ["zod@3.25.56", "", {}, "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@hey-api/openapi-ts/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "@layerstack/tailwind/tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], + + "@layerstack/tailwind/tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], + + "@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=="], + + "@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=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@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=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "c12/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="], + + "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], + + "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "nypm/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "postcss-css-variables/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "rollup/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "svelte-sonner/runed": ["runed@0.26.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-qWFv0cvLVRd8pdl/AslqzvtQyEn5KaIugEernwg9G98uJVSZcs/ygvPBvF80LA46V8pwRvSKnaVLDI3+i2wubw=="], + + "tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="], + + "tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="], + + "vaul-svelte/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], + + "vaul-svelte/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@layerstack/tailwind/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "@layerstack/tailwind/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "@layerstack/tailwind/tailwindcss/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "@layerstack/tailwind/tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + + "@layerstack/tailwind/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "c12/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "giget/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "giget/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "giget/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "giget/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "tw-to-css/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "tw-to-css/tailwindcss/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=="], + + "tw-to-css/tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + + "tw-to-css/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@layerstack/tailwind/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@layerstack/tailwind/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "@layerstack/tailwind/tailwindcss/postcss-load-config/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "tw-to-css/tailwindcss/postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "tw-to-css/tailwindcss/postcss-load-config/yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + + "@layerstack/tailwind/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000000000000000000000000000000000000..9a5e9162105cb4532844394dc6f4e517166f36a9 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://next.shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "stone" + }, + "aliases": { + "components": "@/components", + "utils": "$lib/utils", + "ui": "@/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://next.shadcn-svelte.com/registry" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..b41ad17797f0033c1f7520beef15a41e0d89c709 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,36 @@ +import prettier from "eslint-config-prettier"; +import js from "@eslint/js"; +import { includeIgnoreFile } from "@eslint/compat"; +import svelte from "eslint-plugin-svelte"; +import globals from "globals"; +import { fileURLToPath } from "node:url"; +import ts from "typescript-eslint"; +import svelteConfig from "./svelte.config.js"; + +const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { "no-undef": "off" } + }, + { + files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: [".svelte"], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/external/.gitkeep b/external/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/external/RobotHub-InferenceServer b/external/RobotHub-InferenceServer new file mode 160000 index 0000000000000000000000000000000000000000..1d17b329ca89abd535b88b07e5404aaead3a9c25 --- /dev/null +++ b/external/RobotHub-InferenceServer @@ -0,0 +1 @@ +Subproject commit 1d17b329ca89abd535b88b07e5404aaead3a9c25 diff --git a/external/RobotHub-TransportServer b/external/RobotHub-TransportServer new file mode 160000 index 0000000000000000000000000000000000000000..8aedc84a7635fc0cbbd3a0671a5e1cf50616dad0 --- /dev/null +++ b/external/RobotHub-TransportServer @@ -0,0 +1 @@ +Subproject commit 8aedc84a7635fc0cbbd3a0671a5e1cf50616dad0 diff --git a/log.txt b/log.txt new file mode 100644 index 0000000000000000000000000000000000000000..cc51ceb6bf2467f5fcef1bad25470b7f158a16a1 --- /dev/null +++ b/log.txt @@ -0,0 +1 @@ +.venv/bin/python: can't open file '/Users/julienblanchon/Git/lerobot-arena/lerobot-arena/src-python-video/src/main.py': [Errno 2] No such file or directory diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3d8e3844dc5f8ad772c64dc4ca2e81d647d53bd7 --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "my-app", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^1.2.9", + "@eslint/js": "^9.28.0", + "@iconify/json": "^2.2.346", + "@iconify/svelte": "^5.0.0", + "@iconify/tailwind4": "^1.0.6", + "@internationalized/date": "^3.8.2", + "@lucide/svelte": "^0.511.0", + "@sveltejs/adapter-auto": "^6.0.1", + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.21.2", + "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@tailwindcss/vite": "^4.0.0", + "bits-ui": "^2.4.1", + "eslint": "^9.28.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-svelte": "^3.9.1", + "globals": "^16.2.0", + "layerchart": "1.0.11", + "mode-watcher": "^1.0.7", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.33.17", + "svelte-check": "^4.2.1", + "svelte-sonner": "^1.0.4", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.33.1", + "vaul-svelte": "^1.0.0-next.7", + "vite": "^6.3.5" + }, + "dependencies": { + "@threlte/core": "^8.0.4", + "@threlte/extras": "^9.2.1", + "@types/three": "0.177.0", + "clsx": "^2.1.1", + "feetech.js": "file:./packages/feetech.js", + "@robohub/transport-server-client": "file:../backend/transport-server/client/js", + "@robohub/inference-server-client": "file:../backend/inference-server/client", + "tailwind-merge": "^3.3.0", + "three": "^0.177.0", + "threlte-uikit": "^1.1.0", + "zod": "^3.25.56" + } +} diff --git a/packages/feetech.js/README.md b/packages/feetech.js/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1a1e8e5b306d9ab6ff4a025cb082072316434632 --- /dev/null +++ b/packages/feetech.js/README.md @@ -0,0 +1,24 @@ +# feetech.js + +Control feetech servos through browser + +## Usage + +```bash +# Install the package +npm install feetech.js +``` + +```javascript +import { scsServoSDK } from "feetech.js"; + +await scsServoSDK.connect(); + +const position = await scsServoSDK.readPosition(1); +console.log(position); // 1122 +``` + +## Example usage: + +- simple example: [test.html](./test.html) +- the bambot website: [bambot.org](https://bambot.org) diff --git a/packages/feetech.js/debug.mjs b/packages/feetech.js/debug.mjs new file mode 100644 index 0000000000000000000000000000000000000000..85f129edd9e86e2ee499e3d85a197cc966de0a78 --- /dev/null +++ b/packages/feetech.js/debug.mjs @@ -0,0 +1,15 @@ +/** + * Debug configuration for feetech.js + * Set DEBUG_ENABLED to false to disable all console.log statements for performance + */ +export const DEBUG_ENABLED = true; // Set to true to enable debug logging + +/** + * Conditional logging function that respects the DEBUG_ENABLED flag + * @param {...any} args - Arguments to log + */ +export const debugLog = (...args) => { + if (DEBUG_ENABLED) { + console.log(...args); + } +}; \ No newline at end of file diff --git a/packages/feetech.js/index.d.ts b/packages/feetech.js/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..39eb233b277e7ea6625e87b3e6ebe8455288fd49 --- /dev/null +++ b/packages/feetech.js/index.d.ts @@ -0,0 +1,50 @@ +export type ConnectionOptions = { + baudRate?: number; + protocolEnd?: number; +}; + +export type ServoPositions = Map | Record; +export type ServoSpeeds = Map | Record; + +export interface ScsServoSDK { + // Connection management + connect(options?: ConnectionOptions): Promise; + disconnect(): Promise; + isConnected(): boolean; + + // Servo locking operations + lockServo(servoId: number): Promise<"success">; + unlockServo(servoId: number): Promise<"success">; + lockServos(servoIds: number[]): Promise<"success">; + unlockServos(servoIds: number[]): Promise<"success">; + lockServosForProduction(servoIds: number[]): Promise<"success">; + unlockServosForManualMovement(servoIds: number[]): Promise<"success">; + + // Read operations (no locking needed) + readPosition(servoId: number): Promise; + syncReadPositions(servoIds: number[]): Promise>; + + // Write operations - LOCKED MODE (respects servo locks) + writePosition(servoId: number, position: number): Promise<"success">; + writeTorqueEnable(servoId: number, enable: boolean): Promise<"success">; + + // Write operations - UNLOCKED MODE (temporary unlock for operation) + writePositionUnlocked(servoId: number, position: number): Promise<"success">; + writePositionAndDisableTorque(servoId: number, position: number, waitTimeMs?: number): Promise<"success">; + writeTorqueEnableUnlocked(servoId: number, enable: boolean): Promise<"success">; + + // Sync write operations + syncWritePositions(servoPositions: ServoPositions): Promise<"success">; + + // Configuration functions + setBaudRate(servoId: number, baudRateIndex: number): Promise<"success">; + setServoId(currentServoId: number, newServoId: number): Promise<"success">; + setWheelMode(servoId: number): Promise<"success">; + setPositionMode(servoId: number): Promise<"success">; +} + +export const scsServoSDK: ScsServoSDK; + +// Debug exports +export const DEBUG_ENABLED: boolean; +export function debugLog(message: string): void; diff --git a/packages/feetech.js/index.mjs b/packages/feetech.js/index.mjs new file mode 100644 index 0000000000000000000000000000000000000000..0ff63d6ac7e9ebe555e93805e72463e77ccc1713 --- /dev/null +++ b/packages/feetech.js/index.mjs @@ -0,0 +1,65 @@ +// Import all functions from the unified scsServoSDK module +import { + connect, + disconnect, + isConnected, + lockServo, + unlockServo, + lockServos, + unlockServos, + lockServosForProduction, + unlockServosForManualMovement, + readPosition, + syncReadPositions, + writePosition, + writeTorqueEnable, + writePositionUnlocked, + writePositionAndDisableTorque, + writeTorqueEnableUnlocked, + syncWritePositions, + setBaudRate, + setServoId, + setWheelMode, + setPositionMode +} from "./scsServoSDK.mjs"; + +// Create the unified SCS servo SDK object +export const scsServoSDK = { + // Connection management + connect, + disconnect, + isConnected, + + // Servo locking operations + lockServo, + unlockServo, + lockServos, + unlockServos, + lockServosForProduction, + unlockServosForManualMovement, + + // Read operations (no locking needed) + readPosition, + syncReadPositions, + + // Write operations - LOCKED MODE (respects servo locks) + writePosition, + writeTorqueEnable, + + // Write operations - UNLOCKED MODE (temporary unlock for operation) + writePositionUnlocked, + writePositionAndDisableTorque, + writeTorqueEnableUnlocked, + + // Sync write operations + syncWritePositions, + + // Configuration functions + setBaudRate, + setServoId, + setWheelMode, + setPositionMode +}; + +// Export debug configuration for easy access +export { DEBUG_ENABLED, debugLog } from "./debug.mjs"; diff --git a/packages/feetech.js/lowLevelSDK.mjs b/packages/feetech.js/lowLevelSDK.mjs new file mode 100644 index 0000000000000000000000000000000000000000..9304362d34916d78fc51a287fbbb2ea434045344 --- /dev/null +++ b/packages/feetech.js/lowLevelSDK.mjs @@ -0,0 +1,1235 @@ +// Import debug logging function +import { debugLog } from "./debug.mjs"; + +// Constants +export const BROADCAST_ID = 0xfe; // 254 +export const MAX_ID = 0xfc; // 252 + +// Protocol instructions +export const INST_PING = 1; +export const INST_READ = 2; +export const INST_WRITE = 3; +export const INST_REG_WRITE = 4; +export const INST_ACTION = 5; +export const INST_SYNC_WRITE = 131; // 0x83 +export const INST_SYNC_READ = 130; // 0x82 +export const INST_STATUS = 85; // 0x55, status packet instruction (0x55) + +// Communication results +export const COMM_SUCCESS = 0; // tx or rx packet communication success +export const COMM_PORT_BUSY = -1; // Port is busy (in use) +export const COMM_TX_FAIL = -2; // Failed transmit instruction packet +export const COMM_RX_FAIL = -3; // Failed get status packet +export const COMM_TX_ERROR = -4; // Incorrect instruction packet +export const COMM_RX_WAITING = -5; // Now receiving status packet +export const COMM_RX_TIMEOUT = -6; // There is no status packet +export const COMM_RX_CORRUPT = -7; // Incorrect status packet +export const COMM_NOT_AVAILABLE = -9; + +// Packet constants +export const TXPACKET_MAX_LEN = 250; +export const RXPACKET_MAX_LEN = 250; + +// Protocol Packet positions +export const PKT_HEADER0 = 0; +export const PKT_HEADER1 = 1; +export const PKT_ID = 2; +export const PKT_LENGTH = 3; +export const PKT_INSTRUCTION = 4; +export const PKT_ERROR = 4; +export const PKT_PARAMETER0 = 5; + +// Protocol Error bits +export const ERRBIT_VOLTAGE = 1; +export const ERRBIT_ANGLE = 2; +export const ERRBIT_OVERHEAT = 4; +export const ERRBIT_OVERELE = 8; +export const ERRBIT_OVERLOAD = 32; + +// Default settings +const DEFAULT_BAUDRATE = 1000000; +const LATENCY_TIMER = 16; + +// Global protocol end state +let SCS_END = 0; // (STS/SMS=0, SCS=1) + +// Utility functions for handling word operations +export function SCS_LOWORD(l) { + return l & 0xffff; +} + +export function SCS_HIWORD(l) { + return (l >> 16) & 0xffff; +} + +export function SCS_LOBYTE(w) { + if (SCS_END === 0) { + return w & 0xff; + } else { + return (w >> 8) & 0xff; + } +} + +export function SCS_HIBYTE(w) { + if (SCS_END === 0) { + return (w >> 8) & 0xff; + } else { + return w & 0xff; + } +} + +export function SCS_MAKEWORD(a, b) { + if (SCS_END === 0) { + return (a & 0xff) | ((b & 0xff) << 8); + } else { + return (b & 0xff) | ((a & 0xff) << 8); + } +} + +export function SCS_MAKEDWORD(a, b) { + return (a & 0xffff) | ((b & 0xffff) << 16); +} + +export function SCS_TOHOST(a, b) { + if (a & (1 << b)) { + return -(a & ~(1 << b)); + } else { + return a; + } +} + +export class PortHandler { + constructor() { + this.port = null; + this.reader = null; + this.writer = null; + this.isOpen = false; + this.isUsing = false; + this.baudrate = DEFAULT_BAUDRATE; + this.packetStartTime = 0; + this.packetTimeout = 0; + this.txTimePerByte = 0; + } + + async requestPort() { + try { + this.port = await navigator.serial.requestPort(); + return true; + } catch (err) { + console.error("Error requesting serial port:", err); + return false; + } + } + + async openPort() { + if (!this.port) { + return false; + } + + try { + await this.port.open({ baudRate: this.baudrate }); + this.reader = this.port.readable.getReader(); + this.writer = this.port.writable.getWriter(); + this.isOpen = true; + this.txTimePerByte = (1000.0 / this.baudrate) * 10.0; + return true; + } catch (err) { + console.error("Error opening port:", err); + return false; + } + } + + async closePort() { + if (this.reader) { + await this.reader.releaseLock(); + this.reader = null; + } + + if (this.writer) { + await this.writer.releaseLock(); + this.writer = null; + } + + if (this.port && this.isOpen) { + await this.port.close(); + this.isOpen = false; + } + } + + async clearPort() { + if (this.reader) { + await this.reader.releaseLock(); + this.reader = this.port.readable.getReader(); + } + } + + setBaudRate(baudrate) { + this.baudrate = baudrate; + this.txTimePerByte = (1000.0 / this.baudrate) * 10.0; + return true; + } + + getBaudRate() { + return this.baudrate; + } + + async writePort(data) { + if (!this.isOpen || !this.writer) { + return 0; + } + + try { + await this.writer.write(new Uint8Array(data)); + return data.length; + } catch (err) { + console.error("Error writing to port:", err); + return 0; + } + } + + async readPort(length) { + if (!this.isOpen || !this.reader) { + return []; + } + + try { + // Increase timeout for more reliable data reception + const timeoutMs = 500; + let totalBytes = []; + const startTime = performance.now(); + + // Continue reading until we get enough bytes or timeout + while (totalBytes.length < length) { + // Create a timeout promise + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve({ value: new Uint8Array(), done: false, timeout: true }), 100); // Short internal timeout + }); + + // Race between reading and timeout + const result = await Promise.race([this.reader.read(), timeoutPromise]); + + if (result.timeout) { + // Internal timeout - check if we've exceeded total timeout + if (performance.now() - startTime > timeoutMs) { + debugLog(`readPort total timeout after ${timeoutMs}ms`); + break; + } + continue; // Try reading again + } + + if (result.done) { + debugLog("Reader done, stream closed"); + break; + } + + if (result.value.length === 0) { + // If there's no data but we haven't timed out yet, wait briefly and try again + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Check if we've exceeded total timeout + if (performance.now() - startTime > timeoutMs) { + debugLog(`readPort total timeout after ${timeoutMs}ms`); + break; + } + continue; + } + + // Add received bytes to our total + const newData = Array.from(result.value); + totalBytes.push(...newData); + debugLog( + `Read ${newData.length} bytes:`, + newData.map((b) => b.toString(16).padStart(2, "0")).join(" ") + ); + + // If we've got enough data, we can stop + if (totalBytes.length >= length) { + break; + } + } + + return totalBytes; + } catch (err) { + console.error("Error reading from port:", err); + return []; + } + } + + setPacketTimeout(packetLength) { + this.packetStartTime = this.getCurrentTime(); + this.packetTimeout = this.txTimePerByte * packetLength + LATENCY_TIMER * 2.0 + 2.0; + } + + setPacketTimeoutMillis(msec) { + this.packetStartTime = this.getCurrentTime(); + this.packetTimeout = msec; + } + + isPacketTimeout() { + if (this.getTimeSinceStart() > this.packetTimeout) { + this.packetTimeout = 0; + return true; + } + return false; + } + + getCurrentTime() { + return performance.now(); + } + + getTimeSinceStart() { + const timeSince = this.getCurrentTime() - this.packetStartTime; + if (timeSince < 0.0) { + this.packetStartTime = this.getCurrentTime(); + } + return timeSince; + } +} + +export class PacketHandler { + constructor(protocolEnd = 0) { + SCS_END = protocolEnd; + debugLog(`PacketHandler initialized with protocol_end=${protocolEnd} (STS/SMS=0, SCS=1)`); + } + + getProtocolVersion() { + return 1.0; + } + + // 获取当前协议端设置的方法 + getProtocolEnd() { + return SCS_END; + } + + getTxRxResult(result) { + if (result === COMM_SUCCESS) { + return "[TxRxResult] Communication success!"; + } else if (result === COMM_PORT_BUSY) { + return "[TxRxResult] Port is in use!"; + } else if (result === COMM_TX_FAIL) { + return "[TxRxResult] Failed transmit instruction packet!"; + } else if (result === COMM_RX_FAIL) { + return "[TxRxResult] Failed get status packet from device!"; + } else if (result === COMM_TX_ERROR) { + return "[TxRxResult] Incorrect instruction packet!"; + } else if (result === COMM_RX_WAITING) { + return "[TxRxResult] Now receiving status packet!"; + } else if (result === COMM_RX_TIMEOUT) { + return "[TxRxResult] There is no status packet!"; + } else if (result === COMM_RX_CORRUPT) { + return "[TxRxResult] Incorrect status packet!"; + } else if (result === COMM_NOT_AVAILABLE) { + return "[TxRxResult] Protocol does not support this function!"; + } else { + return ""; + } + } + + getRxPacketError(error) { + if (error & ERRBIT_VOLTAGE) { + return "[RxPacketError] Input voltage error!"; + } + if (error & ERRBIT_ANGLE) { + return "[RxPacketError] Angle sen error!"; + } + if (error & ERRBIT_OVERHEAT) { + return "[RxPacketError] Overheat error!"; + } + if (error & ERRBIT_OVERELE) { + return "[RxPacketError] OverEle error!"; + } + if (error & ERRBIT_OVERLOAD) { + return "[RxPacketError] Overload error!"; + } + return ""; + } + + async txPacket(port, txpacket) { + let checksum = 0; + const totalPacketLength = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH + + if (port.isUsing) { + return COMM_PORT_BUSY; + } + port.isUsing = true; + + // Check max packet length + if (totalPacketLength > TXPACKET_MAX_LEN) { + port.isUsing = false; + return COMM_TX_ERROR; + } + + // Make packet header + txpacket[PKT_HEADER0] = 0xff; + txpacket[PKT_HEADER1] = 0xff; + + // Add checksum to packet + for (let idx = 2; idx < totalPacketLength - 1; idx++) { + checksum += txpacket[idx]; + } + + txpacket[totalPacketLength - 1] = ~checksum & 0xff; + + // TX packet + await port.clearPort(); + const writtenPacketLength = await port.writePort(txpacket); + if (totalPacketLength !== writtenPacketLength) { + port.isUsing = false; + return COMM_TX_FAIL; + } + + return COMM_SUCCESS; + } + + async rxPacket(port) { + let rxpacket = []; + let result = COMM_RX_FAIL; + + let waitLength = 6; // minimum length (HEADER0 HEADER1 ID LENGTH) + + while (true) { + const data = await port.readPort(waitLength - rxpacket.length); + rxpacket.push(...data); + + if (rxpacket.length >= waitLength) { + // Find packet header + let headerIndex = -1; + for (let i = 0; i < rxpacket.length - 1; i++) { + if (rxpacket[i] === 0xff && rxpacket[i + 1] === 0xff) { + headerIndex = i; + break; + } + } + + if (headerIndex === 0) { + // Found at the beginning of the packet + if (rxpacket[PKT_ID] > 0xfd || rxpacket[PKT_LENGTH] > RXPACKET_MAX_LEN) { + // Invalid ID or length + rxpacket.shift(); + continue; + } + + // Recalculate expected packet length + if (waitLength !== rxpacket[PKT_LENGTH] + PKT_LENGTH + 1) { + waitLength = rxpacket[PKT_LENGTH] + PKT_LENGTH + 1; + continue; + } + + if (rxpacket.length < waitLength) { + // Check timeout + if (port.isPacketTimeout()) { + result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT; + break; + } + continue; + } + + // Calculate checksum + let checksum = 0; + for (let i = 2; i < waitLength - 1; i++) { + checksum += rxpacket[i]; + } + checksum = ~checksum & 0xff; + + // Verify checksum + if (rxpacket[waitLength - 1] === checksum) { + result = COMM_SUCCESS; + } else { + result = COMM_RX_CORRUPT; + } + break; + } else if (headerIndex > 0) { + // Remove unnecessary bytes before header + rxpacket = rxpacket.slice(headerIndex); + continue; + } + } + + // Check timeout + if (port.isPacketTimeout()) { + result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT; + break; + } + } + + if (result !== COMM_SUCCESS) { + debugLog( + `rxPacket result: ${result}, packet: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + } else { + console.debug( + `rxPacket successful: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + } + return [rxpacket, result]; + } + + async txRxPacket(port, txpacket) { + let rxpacket = null; + let error = 0; + let result = COMM_TX_FAIL; + + try { + // Check if port is already in use + if (port.isUsing) { + debugLog("Port is busy, cannot start new transaction"); + return [rxpacket, COMM_PORT_BUSY, error]; + } + + // TX packet + debugLog( + "Sending packet:", + txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ") + ); + + // Remove retry logic and just send once + result = await this.txPacket(port, txpacket); + debugLog(`TX result: ${result}`); + + if (result !== COMM_SUCCESS) { + debugLog(`TX failed with result: ${result}`); + port.isUsing = false; // Important: Release the port on TX failure + return [rxpacket, result, error]; + } + + // If ID is broadcast, no need to wait for status packet + if (txpacket[PKT_ID] === BROADCAST_ID) { + port.isUsing = false; + return [rxpacket, result, error]; + } + + // Set packet timeout + if (txpacket[PKT_INSTRUCTION] === INST_READ) { + const length = txpacket[PKT_PARAMETER0 + 1]; + // For READ instructions, we expect response to include the data + port.setPacketTimeout(length + 10); // Add extra buffer + debugLog(`Set READ packet timeout for ${length + 10} bytes`); + } else { + // For other instructions, we expect a status packet + port.setPacketTimeout(10); // HEADER0 HEADER1 ID LENGTH ERROR CHECKSUM + buffer + debugLog(`Set standard packet timeout for 10 bytes`); + } + + // RX packet - no retries, just attempt once + debugLog(`Receiving packet`); + + // Clear port before receiving to ensure clean state + await port.clearPort(); + + const [rxpacketResult, resultRx] = await this.rxPacket(port); + rxpacket = rxpacketResult; + + // Check if received packet is valid + if (resultRx !== COMM_SUCCESS) { + debugLog(`Rx failed with result: ${resultRx}`); + port.isUsing = false; + return [rxpacket, resultRx, error]; + } + + // Verify packet structure + if (rxpacket.length < 6) { + debugLog(`Received packet too short (${rxpacket.length} bytes)`); + port.isUsing = false; + return [rxpacket, COMM_RX_CORRUPT, error]; + } + + // Verify packet ID matches the sent ID + if (rxpacket[PKT_ID] !== txpacket[PKT_ID]) { + debugLog( + `Received packet ID (${rxpacket[PKT_ID]}) doesn't match sent ID (${txpacket[PKT_ID]})` + ); + port.isUsing = false; + return [rxpacket, COMM_RX_CORRUPT, error]; + } + + // Packet looks valid + error = rxpacket[PKT_ERROR]; + port.isUsing = false; // Release port on success + return [rxpacket, resultRx, error]; + } catch (err) { + console.error("Exception in txRxPacket:", err); + port.isUsing = false; // Release port on exception + return [rxpacket, COMM_RX_FAIL, error]; + } + } + + async ping(port, scsId) { + let modelNumber = 0; + let error = 0; + + try { + if (scsId >= BROADCAST_ID) { + debugLog(`Cannot ping broadcast ID ${scsId}`); + return [modelNumber, COMM_NOT_AVAILABLE, error]; + } + + const txpacket = new Array(6).fill(0); + txpacket[PKT_ID] = scsId; + txpacket[PKT_LENGTH] = 2; + txpacket[PKT_INSTRUCTION] = INST_PING; + + debugLog(`Pinging servo ID ${scsId}...`); + + // 发送ping指令并获取响应 + const [rxpacket, result, err] = await this.txRxPacket(port, txpacket); + error = err; + + // 与Python SDK保持一致:如果ping成功,尝试读取地址3的型号信息 + if (result === COMM_SUCCESS) { + debugLog(`Ping to servo ID ${scsId} succeeded, reading model number from address 3`); + // 读取地址3的型号信息(2字节) + const [data, dataResult, dataError] = await this.readTxRx(port, scsId, 3, 2); + + if (dataResult === COMM_SUCCESS && data && data.length >= 2) { + modelNumber = SCS_MAKEWORD(data[0], data[1]); + debugLog(`Model number read: ${modelNumber}`); + } else { + debugLog(`Could not read model number: ${this.getTxRxResult(dataResult)}`); + } + } else { + debugLog(`Ping failed with result: ${result}, error: ${error}`); + } + + return [modelNumber, result, error]; + } catch (error) { + console.error(`Exception in ping():`, error); + return [0, COMM_RX_FAIL, 0]; + } + } + + // Read methods + async readTxRx(port, scsId, address, length) { + if (scsId >= BROADCAST_ID) { + debugLog("Cannot read from broadcast ID"); + return [[], COMM_NOT_AVAILABLE, 0]; + } + + // Create read packet + const txpacket = new Array(8).fill(0); + txpacket[PKT_ID] = scsId; + txpacket[PKT_LENGTH] = 4; + txpacket[PKT_INSTRUCTION] = INST_READ; + txpacket[PKT_PARAMETER0] = address; + txpacket[PKT_PARAMETER0 + 1] = length; + + debugLog(`Reading ${length} bytes from address ${address} for servo ID ${scsId}`); + + // Send packet and get response + const [rxpacket, result, error] = await this.txRxPacket(port, txpacket); + + // Process the result + if (result !== COMM_SUCCESS) { + debugLog(`Read failed with result: ${result}, error: ${error}`); + return [[], result, error]; + } + + if (!rxpacket || rxpacket.length < PKT_PARAMETER0 + length) { + debugLog( + `Invalid response packet: expected at least ${PKT_PARAMETER0 + length} bytes, got ${rxpacket ? rxpacket.length : 0}` + ); + return [[], COMM_RX_CORRUPT, error]; + } + + // Extract data from response + const data = []; + debugLog( + `Response packet length: ${rxpacket.length}, extracting ${length} bytes from offset ${PKT_PARAMETER0}` + ); + debugLog( + `Response data bytes: ${rxpacket + .slice(PKT_PARAMETER0, PKT_PARAMETER0 + length) + .map((b) => "0x" + b.toString(16).padStart(2, "0")) + .join(" ")}` + ); + + for (let i = 0; i < length; i++) { + data.push(rxpacket[PKT_PARAMETER0 + i]); + } + + debugLog( + `Successfully read ${length} bytes: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + return [data, result, error]; + } + + async read1ByteTxRx(port, scsId, address) { + const [data, result, error] = await this.readTxRx(port, scsId, address, 1); + const value = data.length > 0 ? data[0] : 0; + return [value, result, error]; + } + + async read2ByteTxRx(port, scsId, address) { + const [data, result, error] = await this.readTxRx(port, scsId, address, 2); + + let value = 0; + if (data.length >= 2) { + value = SCS_MAKEWORD(data[0], data[1]); + } + + return [value, result, error]; + } + + async read4ByteTxRx(port, scsId, address) { + const [data, result, error] = await this.readTxRx(port, scsId, address, 4); + + let value = 0; + if (data.length >= 4) { + const loword = SCS_MAKEWORD(data[0], data[1]); + const hiword = SCS_MAKEWORD(data[2], data[3]); + value = SCS_MAKEDWORD(loword, hiword); + + debugLog( + `read4ByteTxRx: data=${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + debugLog( + ` loword=${loword} (0x${loword.toString(16)}), hiword=${hiword} (0x${hiword.toString(16)})` + ); + debugLog(` value=${value} (0x${value.toString(16)})`); + } + + return [value, result, error]; + } + + // Write methods + async writeTxRx(port, scsId, address, length, data) { + if (scsId >= BROADCAST_ID) { + return [COMM_NOT_AVAILABLE, 0]; + } + + // Create write packet + const txpacket = new Array(length + 7).fill(0); + txpacket[PKT_ID] = scsId; + txpacket[PKT_LENGTH] = length + 3; + txpacket[PKT_INSTRUCTION] = INST_WRITE; + txpacket[PKT_PARAMETER0] = address; + + // Add data + for (let i = 0; i < length; i++) { + txpacket[PKT_PARAMETER0 + 1 + i] = data[i] & 0xff; + } + + // Send packet and get response + const [rxpacket, result, error] = await this.txRxPacket(port, txpacket); + + return [result, error]; + } + + async write1ByteTxRx(port, scsId, address, data) { + const dataArray = [data & 0xff]; + return await this.writeTxRx(port, scsId, address, 1, dataArray); + } + + async write2ByteTxRx(port, scsId, address, data) { + const dataArray = [SCS_LOBYTE(data), SCS_HIBYTE(data)]; + return await this.writeTxRx(port, scsId, address, 2, dataArray); + } + + async write4ByteTxRx(port, scsId, address, data) { + const dataArray = [ + SCS_LOBYTE(SCS_LOWORD(data)), + SCS_HIBYTE(SCS_LOWORD(data)), + SCS_LOBYTE(SCS_HIWORD(data)), + SCS_HIBYTE(SCS_HIWORD(data)) + ]; + return await this.writeTxRx(port, scsId, address, 4, dataArray); + } + + // Add syncReadTx for GroupSyncRead functionality + async syncReadTx(port, startAddress, dataLength, param, paramLength) { + // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM + const txpacket = new Array(paramLength + 8).fill(0); + + txpacket[PKT_ID] = BROADCAST_ID; + txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM + txpacket[PKT_INSTRUCTION] = INST_SYNC_READ; + txpacket[PKT_PARAMETER0] = startAddress; + txpacket[PKT_PARAMETER0 + 1] = dataLength; + + // Add parameters + for (let i = 0; i < paramLength; i++) { + txpacket[PKT_PARAMETER0 + 2 + i] = param[i]; + } + + // Calculate checksum + const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH + + // Add headers + txpacket[PKT_HEADER0] = 0xff; + txpacket[PKT_HEADER1] = 0xff; + + // Calculate checksum + let checksum = 0; + for (let i = 2; i < totalLen - 1; i++) { + checksum += txpacket[i] & 0xff; + } + txpacket[totalLen - 1] = ~checksum & 0xff; + + debugLog( + `SyncReadTx: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + + // Send packet + await port.clearPort(); + const bytesWritten = await port.writePort(txpacket); + if (bytesWritten !== totalLen) { + return COMM_TX_FAIL; + } + + // Set timeout based on expected response size + port.setPacketTimeout((6 + dataLength) * paramLength); + + return COMM_SUCCESS; + } + + // Add syncWriteTxOnly for GroupSyncWrite functionality + async syncWriteTxOnly(port, startAddress, dataLength, param, paramLength) { + // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM + const txpacket = new Array(paramLength + 8).fill(0); + + txpacket[PKT_ID] = BROADCAST_ID; + txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM + txpacket[PKT_INSTRUCTION] = INST_SYNC_WRITE; + txpacket[PKT_PARAMETER0] = startAddress; + txpacket[PKT_PARAMETER0 + 1] = dataLength; + + // Add parameters + for (let i = 0; i < paramLength; i++) { + txpacket[PKT_PARAMETER0 + 2 + i] = param[i]; + } + + // Calculate checksum + const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH + + // Add headers + txpacket[PKT_HEADER0] = 0xff; + txpacket[PKT_HEADER1] = 0xff; + + // Calculate checksum + let checksum = 0; + for (let i = 2; i < totalLen - 1; i++) { + checksum += txpacket[i] & 0xff; + } + txpacket[totalLen - 1] = ~checksum & 0xff; + + debugLog( + `SyncWriteTxOnly: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + + // Send packet - for sync write, we don't need a response + await port.clearPort(); + const bytesWritten = await port.writePort(txpacket); + if (bytesWritten !== totalLen) { + return COMM_TX_FAIL; + } + + return COMM_SUCCESS; + } + + // 辅助方法:格式化数据包结构以方便调试 + formatPacketStructure(packet) { + if (!packet || packet.length < 4) { + return "Invalid packet (too short)"; + } + + try { + let result = ""; + result += `HEADER: ${packet[0].toString(16).padStart(2, "0")} ${packet[1].toString(16).padStart(2, "0")} | `; + result += `ID: ${packet[2]} | `; + result += `LENGTH: ${packet[3]} | `; + + if (packet.length >= 5) { + result += `ERROR/INST: ${packet[4].toString(16).padStart(2, "0")} | `; + } + + if (packet.length >= 6) { + result += "PARAMS: "; + for (let i = 5; i < packet.length - 1; i++) { + result += `${packet[i].toString(16).padStart(2, "0")} `; + } + result += `| CHECKSUM: ${packet[packet.length - 1].toString(16).padStart(2, "0")}`; + } + + return result; + } catch (e) { + return "Error formatting packet: " + e.message; + } + } + + /** + * 从响应包中解析舵机型号 + * @param {Array} rxpacket - 响应数据包 + * @returns {number} 舵机型号 + */ + parseModelNumber(rxpacket) { + if (!rxpacket || rxpacket.length < 7) { + return 0; + } + + // 检查是否有参数字段 + if (rxpacket.length <= PKT_PARAMETER0 + 1) { + return 0; + } + + const param1 = rxpacket[PKT_PARAMETER0]; + const param2 = rxpacket[PKT_PARAMETER0 + 1]; + + if (SCS_END === 0) { + // STS/SMS 协议的字节顺序 + return SCS_MAKEWORD(param1, param2); + } else { + // SCS 协议的字节顺序 + return SCS_MAKEWORD(param2, param1); + } + } + + /** + * Verify packet header + * @param {Array} packet - The packet to verify + * @returns {Number} COMM_SUCCESS if packet is valid, error code otherwise + */ + getPacketHeader(packet) { + if (!packet || packet.length < 4) { + return COMM_RX_CORRUPT; + } + + // Check header + if (packet[PKT_HEADER0] !== 0xff || packet[PKT_HEADER1] !== 0xff) { + return COMM_RX_CORRUPT; + } + + // Check ID validity + if (packet[PKT_ID] > 0xfd) { + return COMM_RX_CORRUPT; + } + + // Check length + if (packet.length != packet[PKT_LENGTH] + 4) { + return COMM_RX_CORRUPT; + } + + // Calculate checksum + let checksum = 0; + for (let i = 2; i < packet.length - 1; i++) { + checksum += packet[i] & 0xff; + } + checksum = ~checksum & 0xff; + + // Verify checksum + if (packet[packet.length - 1] !== checksum) { + return COMM_RX_CORRUPT; + } + + return COMM_SUCCESS; + } +} + +/** + * GroupSyncRead class + * - This class is used to read multiple servos with the same control table address at once + */ +export class GroupSyncRead { + constructor(port, ph, startAddress, dataLength) { + this.port = port; + this.ph = ph; + this.startAddress = startAddress; + this.dataLength = dataLength; + + this.isAvailableServiceID = new Set(); + this.dataDict = new Map(); + this.param = []; + this.clearParam(); + } + + makeParam() { + this.param = []; + for (const id of this.isAvailableServiceID) { + this.param.push(id); + } + return this.param.length; + } + + addParam(scsId) { + if (this.isAvailableServiceID.has(scsId)) { + return false; + } + + this.isAvailableServiceID.add(scsId); + this.dataDict.set(scsId, new Array(this.dataLength).fill(0)); + return true; + } + + removeParam(scsId) { + if (!this.isAvailableServiceID.has(scsId)) { + return false; + } + + this.isAvailableServiceID.delete(scsId); + this.dataDict.delete(scsId); + return true; + } + + clearParam() { + this.isAvailableServiceID.clear(); + this.dataDict.clear(); + return true; + } + + async txPacket() { + if (this.isAvailableServiceID.size === 0) { + return COMM_NOT_AVAILABLE; + } + + const paramLength = this.makeParam(); + return await this.ph.syncReadTx( + this.port, + this.startAddress, + this.dataLength, + this.param, + paramLength + ); + } + + async rxPacket() { + let result = COMM_RX_FAIL; + + if (this.isAvailableServiceID.size === 0) { + return COMM_NOT_AVAILABLE; + } + + // Set all servos' data as invalid + for (const id of this.isAvailableServiceID) { + this.dataDict.set(id, new Array(this.dataLength).fill(0)); + } + + const [rxpacket, rxResult] = await this.ph.rxPacket(this.port); + if (rxResult !== COMM_SUCCESS || !rxpacket || rxpacket.length === 0) { + return rxResult; + } + + // More tolerant of packets with unexpected values in the PKT_ERROR field + // Don't require INST_STATUS to be exactly 0x55 + debugLog( + `GroupSyncRead rxPacket: ID=${rxpacket[PKT_ID]}, ERROR/INST field=0x${rxpacket[PKT_ERROR].toString(16)}` + ); + + // Check if the packet matches any of the available IDs + if (!this.isAvailableServiceID.has(rxpacket[PKT_ID])) { + debugLog( + `Received packet with ID ${rxpacket[PKT_ID]} which is not in the available IDs list` + ); + return COMM_RX_CORRUPT; + } + + // Extract data for the matching ID + const scsId = rxpacket[PKT_ID]; + const data = new Array(this.dataLength).fill(0); + + // Extract the parameter data, which should start at PKT_PARAMETER0 + if (rxpacket.length < PKT_PARAMETER0 + this.dataLength) { + debugLog( + `Packet too short: expected ${PKT_PARAMETER0 + this.dataLength} bytes, got ${rxpacket.length}` + ); + return COMM_RX_CORRUPT; + } + + for (let i = 0; i < this.dataLength; i++) { + data[i] = rxpacket[PKT_PARAMETER0 + i]; + } + + // Update the data dict + this.dataDict.set(scsId, data); + debugLog( + `Updated data for servo ID ${scsId}: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}` + ); + + // Continue receiving until timeout or all data is received + if (this.isAvailableServiceID.size > 1) { + result = await this.rxPacket(); + } else { + result = COMM_SUCCESS; + } + + return result; + } + + async txRxPacket() { + try { + // First check if port is being used + if (this.port.isUsing) { + debugLog("Port is busy, cannot start sync read operation"); + return COMM_PORT_BUSY; + } + + // Start the transmission + debugLog("Starting sync read TX/RX operation..."); + let result = await this.txPacket(); + if (result !== COMM_SUCCESS) { + debugLog(`Sync read TX failed with result: ${result}`); + return result; + } + + // Get a single response with a standard timeout + debugLog(`Attempting to receive a response...`); + + // Receive a single response + result = await this.rxPacket(); + + // Release port + this.port.isUsing = false; + + return result; + } catch (error) { + console.error("Exception in GroupSyncRead txRxPacket:", error); + // Make sure port is released + this.port.isUsing = false; + return COMM_RX_FAIL; + } + } + + isAvailable(scsId, address, dataLength) { + if (!this.isAvailableServiceID.has(scsId)) { + return false; + } + + const startAddr = this.startAddress; + const endAddr = startAddr + this.dataLength - 1; + + const reqStartAddr = address; + const reqEndAddr = reqStartAddr + dataLength - 1; + + if (reqStartAddr < startAddr || reqEndAddr > endAddr) { + return false; + } + + const data = this.dataDict.get(scsId); + if (!data || data.length === 0) { + return false; + } + + return true; + } + + getData(scsId, address, dataLength) { + if (!this.isAvailable(scsId, address, dataLength)) { + return 0; + } + + const startAddr = this.startAddress; + const data = this.dataDict.get(scsId); + + // Calculate data offset + const dataOffset = address - startAddr; + + // Combine bytes according to dataLength + switch (dataLength) { + case 1: + return data[dataOffset]; + case 2: + return SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]); + case 4: + return SCS_MAKEDWORD( + SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]), + SCS_MAKEWORD(data[dataOffset + 2], data[dataOffset + 3]) + ); + default: + return 0; + } + } +} + +/** + * GroupSyncWrite class + * - This class is used to write multiple servos with the same control table address at once + */ +export class GroupSyncWrite { + constructor(port, ph, startAddress, dataLength) { + this.port = port; + this.ph = ph; + this.startAddress = startAddress; + this.dataLength = dataLength; + + this.isAvailableServiceID = new Set(); + this.dataDict = new Map(); + this.param = []; + this.clearParam(); + } + + makeParam() { + this.param = []; + for (const id of this.isAvailableServiceID) { + // Add ID to parameter + this.param.push(id); + + // Add data to parameter + const data = this.dataDict.get(id); + for (let i = 0; i < this.dataLength; i++) { + this.param.push(data[i]); + } + } + return this.param.length; + } + + addParam(scsId, data) { + if (this.isAvailableServiceID.has(scsId)) { + return false; + } + + if (data.length !== this.dataLength) { + console.error( + `Data length (${data.length}) doesn't match required length (${this.dataLength})` + ); + return false; + } + + this.isAvailableServiceID.add(scsId); + this.dataDict.set(scsId, data); + return true; + } + + removeParam(scsId) { + if (!this.isAvailableServiceID.has(scsId)) { + return false; + } + + this.isAvailableServiceID.delete(scsId); + this.dataDict.delete(scsId); + return true; + } + + changeParam(scsId, data) { + if (!this.isAvailableServiceID.has(scsId)) { + return false; + } + + if (data.length !== this.dataLength) { + console.error( + `Data length (${data.length}) doesn't match required length (${this.dataLength})` + ); + return false; + } + + this.dataDict.set(scsId, data); + return true; + } + + clearParam() { + this.isAvailableServiceID.clear(); + this.dataDict.clear(); + return true; + } + + async txPacket() { + if (this.isAvailableServiceID.size === 0) { + return COMM_NOT_AVAILABLE; + } + + const paramLength = this.makeParam(); + return await this.ph.syncWriteTxOnly( + this.port, + this.startAddress, + this.dataLength, + this.param, + paramLength + ); + } +} diff --git a/packages/feetech.js/package.json b/packages/feetech.js/package.json new file mode 100644 index 0000000000000000000000000000000000000000..0d569bd9b1a9e05e1c4d0b5f3e0cac17917749cb --- /dev/null +++ b/packages/feetech.js/package.json @@ -0,0 +1,38 @@ +{ + "name": "feetech.js", + "version": "0.0.8", + "description": "javascript sdk for feetech servos", + "main": "index.mjs", + "files": [ + "*.mjs", + "*.ts" + ], + "type": "module", + "engines": { + "node": ">=12.17.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js" + }, + "keywords": [ + "feetech", + "sdk", + "js", + "javascript", + "sts3215", + "3215", + "scs", + "scs3215", + "st3215" + ], + "author": "timqian", + "license": "MIT", + "bugs": { + "url": "https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js" + }, + "homepage": "https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js" +} diff --git a/packages/feetech.js/scsServoSDK.mjs b/packages/feetech.js/scsServoSDK.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b2c982bba2f128b598d95982333d36254463be11 --- /dev/null +++ b/packages/feetech.js/scsServoSDK.mjs @@ -0,0 +1,1205 @@ +import { + PortHandler, + PacketHandler, + COMM_SUCCESS, + COMM_RX_TIMEOUT, + COMM_RX_CORRUPT, + COMM_TX_FAIL, + COMM_NOT_AVAILABLE, + SCS_LOBYTE, + SCS_HIBYTE, + SCS_MAKEWORD, + GroupSyncRead, // Import GroupSyncRead + GroupSyncWrite // Import GroupSyncWrite +} from "./lowLevelSDK.mjs"; + +// Import address constants from the correct file +import { + ADDR_SCS_PRESENT_POSITION, + ADDR_SCS_GOAL_POSITION, + ADDR_SCS_TORQUE_ENABLE, + ADDR_SCS_GOAL_ACC, + ADDR_SCS_GOAL_SPEED +} from "./scsservo_constants.mjs"; + +// Import debug logging function +import { debugLog } from "./debug.mjs"; + +// Define constants not present in scsservo_constants.mjs +const ADDR_SCS_MODE = 33; +const ADDR_SCS_LOCK = 55; +const ADDR_SCS_ID = 5; // Address for Servo ID +const ADDR_SCS_BAUD_RATE = 6; // Address for Baud Rate + +// Module-level variables for handlers +let portHandler = null; +let packetHandler = null; + +/** + * Unified Servo SDK with flexible locking control + * Supports both locked (respects servo locks) and unlocked (temporary unlock) operations + */ + +/** + * Connects to the serial port and initializes handlers. + * @param {object} [options] - Connection options. + * @param {number} [options.baudRate=1000000] - The baud rate for the serial connection. + * @param {number} [options.protocolEnd=0] - The protocol end setting (0 for STS/SMS, 1 for SCS). + * @returns {Promise} Resolves with true on successful connection. + * @throws {Error} If connection fails or port cannot be opened/selected. + */ +export async function connect(options = {}) { + if (portHandler && portHandler.isOpen) { + debugLog("Already connected to servo system."); + return true; + } + + const { baudRate = 1000000, protocolEnd = 0 } = options; + + try { + portHandler = new PortHandler(); + const portRequested = await portHandler.requestPort(); + if (!portRequested) { + portHandler = null; + throw new Error("Failed to select a serial port."); + } + + portHandler.setBaudRate(baudRate); + const portOpened = await portHandler.openPort(); + if (!portOpened) { + await portHandler.closePort().catch(console.error); + portHandler = null; + throw new Error(`Failed to open port at baudrate ${baudRate}.`); + } + + packetHandler = new PacketHandler(protocolEnd); + debugLog(`Connected to servo system at ${baudRate} baud, protocol end: ${protocolEnd}.`); + return true; + } catch (err) { + console.error("Error during servo connection:", err); + if (portHandler) { + try { + await portHandler.closePort(); + } catch (closeErr) { + console.error("Error closing port after connection failure:", closeErr); + } + } + portHandler = null; + packetHandler = null; + throw new Error(`Servo connection failed: ${err.message}`); + } +} + +/** + * Disconnects from the serial port. + * @returns {Promise} Resolves with true on successful disconnection. + * @throws {Error} If disconnection fails. + */ +export async function disconnect() { + if (!portHandler || !portHandler.isOpen) { + debugLog("Already disconnected from servo system."); + return true; + } + + try { + await portHandler.closePort(); + portHandler = null; + packetHandler = null; + debugLog("Disconnected from servo system."); + return true; + } catch (err) { + console.error("Error during servo disconnection:", err); + portHandler = null; + packetHandler = null; + throw new Error(`Servo disconnection failed: ${err.message}`); + } +} + +/** + * Checks if the SDK is currently connected. + * @returns {boolean} True if connected, false otherwise. + */ +export function isConnected() { + return !!(portHandler && portHandler.isOpen && packetHandler); +} + +/** + * Checks if the SDK is connected. Throws an error if not. + * @throws {Error} If not connected. + */ +function checkConnection() { + if (!portHandler || !packetHandler) { + throw new Error("Not connected to servo system. Call connect() first."); + } +} + +// ============================================================================= +// SERVO LOCKING OPERATIONS +// ============================================================================= + +/** + * Locks a servo to prevent configuration changes. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, write fails, or an exception occurs. + */ +export async function lockServo(servoId) { + checkConnection(); + try { + debugLog(`🔒 Locking servo ${servoId}...`); + const [result, error] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 1 + ); + + if (result !== COMM_SUCCESS) { + throw new Error( + `Error locking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}` + ); + } + debugLog(`🔒 Servo ${servoId} locked successfully`); + return "success"; + } catch (err) { + console.error(`Exception locking servo ${servoId}:`, err); + throw new Error(`Failed to lock servo ${servoId}: ${err.message}`); + } +} + +/** + * Unlocks a servo to allow configuration changes. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, write fails, or an exception occurs. + */ +export async function unlockServo(servoId) { + checkConnection(); + try { + debugLog(`🔓 Unlocking servo ${servoId}...`); + const [result, error] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 0 + ); + + if (result !== COMM_SUCCESS) { + throw new Error( + `Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}` + ); + } + debugLog(`🔓 Servo ${servoId} unlocked successfully`); + return "success"; + } catch (err) { + console.error(`Exception unlocking servo ${servoId}:`, err); + throw new Error(`Failed to unlock servo ${servoId}: ${err.message}`); + } +} + +/** + * Locks multiple servos sequentially. + * @param {number[]} servoIds - Array of servo IDs to lock. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If any servo fails to lock. + */ +export async function lockServos(servoIds) { + checkConnection(); + debugLog(`🔒 Locking ${servoIds.length} servos: [${servoIds.join(', ')}]`); + + // Lock servos sequentially to avoid port conflicts + for (const servoId of servoIds) { + await lockServo(servoId); + } + + debugLog(`🔒 All ${servoIds.length} servos locked successfully`); + return "success"; +} + +/** + * Locks servos for production use by both locking configuration and enabling torque. + * This ensures servos are truly locked and controlled by the system. + * @param {number[]} servoIds - Array of servo IDs to lock for production. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If any servo fails to lock or enable torque. + */ +export async function lockServosForProduction(servoIds) { + checkConnection(); + debugLog(`🔒 Locking ${servoIds.length} servos for production use: [${servoIds.join(', ')}]`); + + // Lock servos sequentially and enable torque for each + for (const servoId of servoIds) { + try { + debugLog(`🔒 Locking servo ${servoId} for production...`); + + // 1. Lock the servo configuration + const [lockResult, lockError] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 1 + ); + + if (lockResult !== COMM_SUCCESS) { + throw new Error(`Error locking servo ${servoId}: ${packetHandler.getTxRxResult(lockResult)}, Error: ${lockError}`); + } + + // 2. Enable torque to make servo controllable + const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_TORQUE_ENABLE, + 1 + ); + + if (torqueResult !== COMM_SUCCESS) { + console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`); + // Don't throw here, locking is more important than torque enable + } + + debugLog(`🔒 Servo ${servoId} locked and torque enabled for production`); + } catch (err) { + console.error(`Exception locking servo ${servoId} for production:`, err); + throw new Error(`Failed to lock servo ${servoId} for production: ${err.message}`); + } + } + + debugLog(`🔒 All ${servoIds.length} servos locked for production successfully`); + return "success"; +} + +/** + * Unlocks multiple servos sequentially. + * @param {number[]} servoIds - Array of servo IDs to unlock. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If any servo fails to unlock. + */ +export async function unlockServos(servoIds) { + checkConnection(); + debugLog(`🔓 Unlocking ${servoIds.length} servos: [${servoIds.join(', ')}]`); + + // Unlock servos sequentially to avoid port conflicts + for (const servoId of servoIds) { + await unlockServo(servoId); + } + + debugLog(`🔓 All ${servoIds.length} servos unlocked successfully`); + return "success"; +} + +/** + * Safely unlocks servos for manual movement by unlocking configuration and disabling torque. + * This is the safest way to leave servos when disconnecting/cleaning up. + * @param {number[]} servoIds - Array of servo IDs to unlock safely. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If any servo fails to unlock or disable torque. + */ +export async function unlockServosForManualMovement(servoIds) { + checkConnection(); + debugLog(`🔓 Safely unlocking ${servoIds.length} servos for manual movement: [${servoIds.join(', ')}]`); + + // Unlock servos sequentially and disable torque for each + for (const servoId of servoIds) { + try { + debugLog(`🔓 Safely unlocking servo ${servoId} for manual movement...`); + + // 1. Disable torque first (makes servo freely movable) + const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_TORQUE_ENABLE, + 0 + ); + + if (torqueResult !== COMM_SUCCESS) { + console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`); + // Continue anyway, unlocking is more important + } + + // 2. Unlock the servo configuration + const [unlockResult, unlockError] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 0 + ); + + if (unlockResult !== COMM_SUCCESS) { + throw new Error(`Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(unlockResult)}, Error: ${unlockError}`); + } + + debugLog(`🔓 Servo ${servoId} safely unlocked - torque disabled and configuration unlocked`); + } catch (err) { + console.error(`Exception safely unlocking servo ${servoId}:`, err); + throw new Error(`Failed to safely unlock servo ${servoId}: ${err.message}`); + } + } + + debugLog(`🔓 All ${servoIds.length} servos safely unlocked for manual movement`); + return "success"; +} + +// ============================================================================= +// READ OPERATIONS (No locking needed) +// ============================================================================= + +/** + * Reads the current position of a servo. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise} Resolves with the position (0-4095). + * @throws {Error} If not connected, read fails, or an exception occurs. + */ +export async function readPosition(servoId) { + checkConnection(); + try { + const [position, result, error] = await packetHandler.read2ByteTxRx( + portHandler, + servoId, + ADDR_SCS_PRESENT_POSITION + ); + + if (result !== COMM_SUCCESS) { + throw new Error( + `Error reading position from servo ${servoId}: ${packetHandler.getTxRxResult( + result + )}, Error code: ${error}` + ); + } + return position & 0xffff; + } catch (err) { + console.error(`Exception reading position from servo ${servoId}:`, err); + throw new Error(`Exception reading position from servo ${servoId}: ${err.message}`); + } +} + +/** + * Reads the current baud rate index of a servo. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise} Resolves with the baud rate index (0-7). + * @throws {Error} If not connected, read fails, or an exception occurs. + */ +export async function readBaudRate(servoId) { + checkConnection(); + try { + const [baudIndex, result, error] = await packetHandler.read1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_BAUD_RATE + ); + + if (result !== COMM_SUCCESS) { + throw new Error( + `Error reading baud rate from servo ${servoId}: ${packetHandler.getTxRxResult( + result + )}, Error code: ${error}` + ); + } + return baudIndex; + } catch (err) { + console.error(`Exception reading baud rate from servo ${servoId}:`, err); + throw new Error(`Exception reading baud rate from servo ${servoId}: ${err.message}`); + } +} + +/** + * Reads the current operating mode of a servo. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise} Resolves with the mode (0 for position, 1 for wheel). + * @throws {Error} If not connected, read fails, or an exception occurs. + */ +export async function readMode(servoId) { + checkConnection(); + try { + const [modeValue, result, error] = await packetHandler.read1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_MODE + ); + + if (result !== COMM_SUCCESS) { + throw new Error( + `Error reading mode from servo ${servoId}: ${packetHandler.getTxRxResult( + result + )}, Error code: ${error}` + ); + } + return modeValue; + } catch (err) { + console.error(`Exception reading mode from servo ${servoId}:`, err); + throw new Error(`Exception reading mode from servo ${servoId}: ${err.message}`); + } +} + +/** + * Reads the current position of multiple servos synchronously. + * @param {number[]} servoIds - An array of servo IDs (1-252) to read from. + * @returns {Promise>} Resolves with a Map where keys are servo IDs and values are positions (0-4095). + * @throws {Error} If not connected or transmission fails completely. + */ +export async function syncReadPositions(servoIds) { + checkConnection(); + if (!Array.isArray(servoIds) || servoIds.length === 0) { + debugLog("Sync Read: No servo IDs provided."); + return new Map(); + } + + const startAddress = ADDR_SCS_PRESENT_POSITION; + const dataLength = 2; + const groupSyncRead = new GroupSyncRead(portHandler, packetHandler, startAddress, dataLength); + const positions = new Map(); + const validIds = []; + + // Add parameters for each valid servo ID + servoIds.forEach((id) => { + if (id >= 1 && id <= 252) { + if (groupSyncRead.addParam(id)) { + validIds.push(id); + } else { + console.warn(`Sync Read: Failed to add param for servo ID ${id} (maybe duplicate or invalid).`); + } + } else { + console.warn(`Sync Read: Invalid servo ID ${id} skipped.`); + } + }); + + if (validIds.length === 0) { + debugLog("Sync Read: No valid servo IDs to read."); + return new Map(); + } + + try { + let txResult = await groupSyncRead.txPacket(); + if (txResult !== COMM_SUCCESS) { + throw new Error(`Sync Read txPacket failed: ${packetHandler.getTxRxResult(txResult)}`); + } + + let rxResult = await groupSyncRead.rxPacket(); + if (rxResult !== COMM_SUCCESS) { + console.warn(`Sync Read rxPacket overall result: ${packetHandler.getTxRxResult(rxResult)}. Checking individual servos.`); + } + + const failedIds = []; + validIds.forEach((id) => { + const isAvailable = groupSyncRead.isAvailable(id, startAddress, dataLength); + if (isAvailable) { + const position = groupSyncRead.getData(id, startAddress, dataLength); + positions.set(id, position & 0xffff); + } else { + failedIds.push(id); + } + }); + + if (failedIds.length > 0) { + console.warn(`Sync Read: Data not available for servo IDs: ${failedIds.join(", ")}. Got ${positions.size}/${validIds.length} servos successfully.`); + } + + return positions; + } catch (err) { + console.error("Exception during syncReadPositions:", err); + throw new Error(`Sync Read failed: ${err.message}`); + } +} + +// ============================================================================= +// WRITE OPERATIONS - LOCKED MODE (Respects servo locks) +// ============================================================================= + +/** + * Writes a target position to a servo (respects locks). + * Will fail if the servo is locked. + * @param {number} servoId - The ID of the servo (1-252). + * @param {number} position - The target position value (0-4095). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs. + */ +export async function writePosition(servoId, position) { + checkConnection(); + try { + if (position < 0 || position > 4095) { + throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`); + } + const targetPosition = Math.round(position); + + const [result, error] = await packetHandler.write2ByteTxRx( + portHandler, + servoId, + ADDR_SCS_GOAL_POSITION, + targetPosition + ); + + if (result !== COMM_SUCCESS) { + throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`); + } + return "success"; + } catch (err) { + console.error(`Exception writing position to servo ${servoId}:`, err); + throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`); + } +} + +/** + * Enables or disables the torque of a servo (respects locks). + * @param {number} servoId - The ID of the servo (1-252). + * @param {boolean} enable - True to enable torque, false to disable. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, write fails, or an exception occurs. + */ +export async function writeTorqueEnable(servoId, enable) { + checkConnection(); + try { + const enableValue = enable ? 1 : 0; + const [result, error] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_TORQUE_ENABLE, + enableValue + ); + + if (result !== COMM_SUCCESS) { + throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`); + } + return "success"; + } catch (err) { + console.error(`Exception setting torque for servo ${servoId}:`, err); + throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`); + } +} + +// ============================================================================= +// WRITE OPERATIONS - UNLOCKED MODE (Temporary unlock for operation) +// ============================================================================= + +/** + * Helper to attempt locking a servo, logging errors without throwing. + * @param {number} servoId + */ +async function tryLockServo(servoId) { + try { + await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + } catch (lockErr) { + console.error(`Failed to re-lock servo ${servoId}:`, lockErr); + } +} + +/** + * Writes a target position to a servo with temporary unlocking. + * Temporarily unlocks the servo, writes the position, then locks it back. + * @param {number} servoId - The ID of the servo (1-252). + * @param {number} position - The target position value (0-4095). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs. + */ +export async function writePositionUnlocked(servoId, position) { + checkConnection(); + let unlocked = false; + try { + if (position < 0 || position > 4095) { + throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`); + } + const targetPosition = Math.round(position); + + debugLog(`🔓 Temporarily unlocking servo ${servoId} for position write...`); + + // 1. Unlock servo configuration first + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`); + } else { + unlocked = true; + } + + // 2. Write the position + const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition); + if (result !== COMM_SUCCESS) { + throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`); + } + + // 3. Lock servo configuration back + if (unlocked) { + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } else { + unlocked = false; + } + } + + return "success"; + } catch (err) { + console.error(`Exception writing position to servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`); + } +} + +/** + * Writes a target position and disables torque for manual movement. + * @param {number} servoId - The ID of the servo (1-252). + * @param {number} position - The target position value (0-4095). + * @param {number} waitTimeMs - Time to wait for servo to reach position (milliseconds). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs. + */ +export async function writePositionAndDisableTorque(servoId, position, waitTimeMs = 1500) { + checkConnection(); + let unlocked = false; + try { + if (position < 0 || position > 4095) { + throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`); + } + const targetPosition = Math.round(position); + + debugLog(`🔓 Moving servo ${servoId} to position ${targetPosition}, waiting ${waitTimeMs}ms, then disabling torque...`); + + // 1. Unlock servo configuration first + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`); + } else { + unlocked = true; + } + + // 2. Enable torque first + const [torqueEnableResult, torqueEnableError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 1); + if (torqueEnableResult !== COMM_SUCCESS) { + console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueEnableResult)}, Error: ${torqueEnableError}`); + } else { + debugLog(`✅ Torque enabled for servo ${servoId}`); + } + + // 3. Write the position + const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition); + if (result !== COMM_SUCCESS) { + throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`); + } + + // 4. Wait for servo to reach position + debugLog(`⏳ Waiting ${waitTimeMs}ms for servo ${servoId} to reach position ${targetPosition}...`); + await new Promise(resolve => setTimeout(resolve, waitTimeMs)); + + // 5. Disable torque + const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 0); + if (torqueResult !== COMM_SUCCESS) { + console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`); + } else { + debugLog(`✅ Torque disabled for servo ${servoId} - now movable by hand`); + } + + // 6. Lock servo configuration back + if (unlocked) { + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } else { + unlocked = false; + } + } + + return "success"; + } catch (err) { + console.error(`Exception writing position and disabling torque for servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Failed to write position and disable torque for servo ${servoId}: ${err.message}`); + } +} + +/** + * Enables or disables the torque of a servo with temporary unlocking. + * @param {number} servoId - The ID of the servo (1-252). + * @param {boolean} enable - True to enable torque, false to disable. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, write fails, or an exception occurs. + */ +export async function writeTorqueEnableUnlocked(servoId, enable) { + checkConnection(); + let unlocked = false; + try { + const enableValue = enable ? 1 : 0; + + debugLog(`🔓 Temporarily unlocking servo ${servoId} for torque enable write...`); + + // 1. Unlock servo configuration first + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`); + } else { + unlocked = true; + } + + // 2. Write the torque enable + const [result, error] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, enableValue); + if (result !== COMM_SUCCESS) { + throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`); + } + + // 3. Lock servo configuration back + if (unlocked) { + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + console.warn(`Warning: Failed to re-lock servo ${servoId} after torque enable write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } else { + unlocked = false; + } + } + + return "success"; + } catch (err) { + console.error(`Exception setting torque for servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`); + } +} + +// ============================================================================= +// SYNC WRITE OPERATIONS +// ============================================================================= + +/** + * Writes target positions to multiple servos synchronously. + * @param {Map | object} servoPositions - A Map or object where keys are servo IDs (1-252) and values are target positions (0-4095). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, any position is out of range, transmission fails, or an exception occurs. + */ +export async function syncWritePositions(servoPositions) { + checkConnection(); + + const groupSyncWrite = new GroupSyncWrite(portHandler, packetHandler, ADDR_SCS_GOAL_POSITION, 2); + let paramAdded = false; + + const entries = servoPositions instanceof Map ? servoPositions.entries() : Object.entries(servoPositions); + + for (const [idStr, position] of entries) { + const servoId = parseInt(idStr, 10); + if (isNaN(servoId) || servoId < 1 || servoId > 252) { + throw new Error(`Invalid servo ID "${idStr}" in syncWritePositions.`); + } + if (position < 0 || position > 4095) { + throw new Error(`Invalid position value ${position} for servo ${servoId} in syncWritePositions. Must be between 0 and 4095.`); + } + const targetPosition = Math.round(position); + const data = [SCS_LOBYTE(targetPosition), SCS_HIBYTE(targetPosition)]; + + if (groupSyncWrite.addParam(servoId, data)) { + paramAdded = true; + } else { + console.warn(`Failed to add servo ${servoId} to sync write group (possibly duplicate).`); + } + } + + if (!paramAdded) { + debugLog("Sync Write: No valid servo positions provided or added."); + return "success"; + } + + try { + const result = await groupSyncWrite.txPacket(); + if (result !== COMM_SUCCESS) { + throw new Error(`Sync Write txPacket failed: ${packetHandler.getTxRxResult(result)}`); + } + return "success"; + } catch (err) { + console.error("Exception during syncWritePositions:", err); + throw new Error(`Sync Write failed: ${err.message}`); + } +} + +/** + * Writes a target speed for a servo in wheel mode. + * @param {number} servoId - The ID of the servo + * @param {number} speed - The target speed value (-10000 to 10000). Negative values indicate reverse direction. 0 stops the wheel. + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, either write fails, or an exception occurs. + */ +export async function writeWheelSpeed(servoId, speed) { + checkConnection(); + let unlocked = false; + try { + const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed))); + let speedValue = Math.abs(clampedSpeed) & 0x7fff; + + if (clampedSpeed < 0) { + speedValue |= 0x8000; + } + + debugLog(`Temporarily unlocking servo ${servoId} for wheel speed write...`); + + // 1. Unlock servo configuration first + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`); + } else { + unlocked = true; + } + + // 2. Write the speed + const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_SPEED, speedValue); + if (result !== COMM_SUCCESS) { + throw new Error(`Error writing wheel speed to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`); + } + + // 3. Lock servo configuration back + if (unlocked) { + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + console.warn(`Warning: Failed to re-lock servo ${servoId} after wheel speed write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } else { + unlocked = false; + } + } + + return "success"; + } catch (err) { + console.error(`Exception writing wheel speed to servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Exception writing wheel speed to servo ${servoId}: ${err.message}`); + } +} + +/** + * Writes target speeds to multiple servos in wheel mode synchronously. + * @param {Map | object} servoSpeeds - A Map or object where keys are servo IDs (1-252) and values are target speeds (-10000 to 10000). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, any speed is out of range, transmission fails, or an exception occurs. + */ +export async function syncWriteWheelSpeed(servoSpeeds) { + checkConnection(); + + const groupSyncWrite = new GroupSyncWrite( + portHandler, + packetHandler, + ADDR_SCS_GOAL_SPEED, + 2 // Data length for speed (2 bytes) + ); + let paramAdded = false; + + const entries = servoSpeeds instanceof Map ? servoSpeeds.entries() : Object.entries(servoSpeeds); + + // Second pass: Add valid parameters + for (const [idStr, speed] of entries) { + const servoId = parseInt(idStr, 10); // Already validated + + if (isNaN(servoId) || servoId < 1 || servoId > 252) { + throw new Error(`Invalid servo ID "${idStr}" in syncWriteWheelSpeed.`); + } + if (speed < -10000 || speed > 10000) { + throw new Error( + `Invalid speed value ${speed} for servo ${servoId} in syncWriteWheelSpeed. Must be between -10000 and 10000.` + ); + } + + const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed))); // Ensure integer, already validated range + let speedValue = Math.abs(clampedSpeed) & 0x7fff; // Get absolute value, ensure within 15 bits + + // Set the direction bit (MSB of the 16-bit value) if speed is negative + if (clampedSpeed < 0) { + speedValue |= 0x8000; // Set the 16th bit for reverse direction + } + + const data = [SCS_LOBYTE(speedValue), SCS_HIBYTE(speedValue)]; + + if (groupSyncWrite.addParam(servoId, data)) { + paramAdded = true; + } else { + // This should ideally not happen if IDs are unique, but handle defensively + console.warn( + `Failed to add servo ${servoId} to sync write speed group (possibly duplicate).` + ); + } + } + + if (!paramAdded) { + debugLog("Sync Write Speed: No valid servo speeds provided or added."); + return "success"; // Nothing to write is considered success + } + + try { + // Send the Sync Write instruction + const result = await groupSyncWrite.txPacket(); + if (result !== COMM_SUCCESS) { + throw new Error(`Sync Write Speed txPacket failed: ${packetHandler.getTxRxResult(result)}`); + } + return "success"; + } catch (err) { + console.error("Exception during syncWriteWheelSpeed:", err); + // Re-throw the original error or a new one wrapping it + throw new Error(`Sync Write Speed failed: ${err.message}`); + } +} + +/** + * Sets the Baud Rate of a servo. + * NOTE: After changing the baud rate, you might need to disconnect and reconnect + * at the new baud rate to communicate with the servo further. + * @param {number} servoId - The current ID of the servo to configure (1-252). + * @param {number} baudRateIndex - The index representing the new baud rate (0-7). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs. + */ +export async function setBaudRate(servoId, baudRateIndex) { + checkConnection(); + + // Validate inputs + if (servoId < 1 || servoId > 252) { + throw new Error(`Invalid servo ID provided: ${servoId}. Must be between 1 and 252.`); + } + if (baudRateIndex < 0 || baudRateIndex > 7) { + throw new Error(`Invalid baudRateIndex: ${baudRateIndex}. Must be between 0 and 7.`); + } + + let unlocked = false; + try { + debugLog(`Setting baud rate for servo ${servoId}: Index=${baudRateIndex}`); + + // 1. Unlock servo configuration + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 0 // 0 to unlock + ); + if (resUnlock !== COMM_SUCCESS) { + throw new Error( + `Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult( + resUnlock + )}, Error: ${errUnlock}` + ); + } + unlocked = true; + + // 2. Write new Baud Rate index + const [resBaud, errBaud] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_BAUD_RATE, + baudRateIndex + ); + if (resBaud !== COMM_SUCCESS) { + throw new Error( + `Failed to write baud rate index ${baudRateIndex} to servo ${servoId}: ${packetHandler.getTxRxResult( + resBaud + )}, Error: ${errBaud}` + ); + } + + // 3. Lock servo configuration + const [resLock, errLock] = await packetHandler.write1ByteTxRx( + portHandler, + servoId, + ADDR_SCS_LOCK, + 1 + ); + if (resLock !== COMM_SUCCESS) { + throw new Error( + `Failed to lock servo ${servoId} after setting baud rate: ${packetHandler.getTxRxResult( + resLock + )}, Error: ${errLock}.` + ); + } + unlocked = false; // Successfully locked + + debugLog( + `Successfully set baud rate for servo ${servoId}. Index: ${baudRateIndex}. Remember to potentially reconnect with the new baud rate.` + ); + return "success"; + } catch (err) { + console.error(`Exception during setBaudRate for servo ID ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Failed to set baud rate for servo ${servoId}: ${err.message}`); + } +} + +/** + * Sets the ID of a servo. + * NOTE: Changing the ID requires using the new ID for subsequent commands. + * @param {number} currentServoId - The current ID of the servo to configure (1-252). + * @param {number} newServoId - The new ID to set for the servo (1-252). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs. + */ +export async function setServoId(currentServoId, newServoId) { + checkConnection(); + + // Validate inputs + if (currentServoId < 1 || currentServoId > 252 || newServoId < 1 || newServoId > 252) { + throw new Error( + `Invalid servo ID provided. Current: ${currentServoId}, New: ${newServoId}. Must be between 1 and 252.` + ); + } + + if (currentServoId === newServoId) { + debugLog(`Servo ID is already ${newServoId}. No change needed.`); + return "success"; + } + + let unlocked = false; + let idWritten = false; + try { + debugLog(`Setting servo ID: From ${currentServoId} to ${newServoId}`); + + // 1. Unlock servo configuration (using current ID) + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx( + portHandler, + currentServoId, + ADDR_SCS_LOCK, + 0 // 0 to unlock + ); + if (resUnlock !== COMM_SUCCESS) { + throw new Error( + `Failed to unlock servo ${currentServoId}: ${packetHandler.getTxRxResult( + resUnlock + )}, Error: ${errUnlock}` + ); + } + unlocked = true; + + // 2. Write new Servo ID (using current ID) + const [resId, errId] = await packetHandler.write1ByteTxRx( + portHandler, + currentServoId, + ADDR_SCS_ID, + newServoId + ); + if (resId !== COMM_SUCCESS) { + throw new Error( + `Failed to write new ID ${newServoId} to servo ${currentServoId}: ${packetHandler.getTxRxResult( + resId + )}, Error: ${errId}` + ); + } + idWritten = true; + + // 3. Lock servo configuration (using NEW ID) + const [resLock, errLock] = await packetHandler.write1ByteTxRx( + portHandler, + newServoId, // Use NEW ID here + ADDR_SCS_LOCK, + 1 // 1 to lock + ); + if (resLock !== COMM_SUCCESS) { + // ID was likely changed, but lock failed. Critical state. + throw new Error( + `Failed to lock servo with new ID ${newServoId}: ${packetHandler.getTxRxResult( + resLock + )}, Error: ${errLock}. Configuration might be incomplete.` + ); + } + unlocked = false; // Successfully locked with new ID + + debugLog( + `Successfully set servo ID from ${currentServoId} to ${newServoId}. Remember to use the new ID for future commands.` + ); + return "success"; + } catch (err) { + console.error(`Exception during setServoId for current ID ${currentServoId}:`, err); + if (unlocked) { + // If unlock succeeded but subsequent steps failed, attempt to re-lock. + // If ID write failed, use current ID. If ID write succeeded but lock failed, use new ID. + const idToLock = idWritten ? newServoId : currentServoId; + console.warn(`Attempting to re-lock servo using ID ${idToLock}...`); + await tryLockServo(idToLock); + } + throw new Error( + `Failed to set servo ID from ${currentServoId} to ${newServoId}: ${err.message}` + ); + } +} + +// ============================================================================= +// LEGACY COMPATIBILITY FUNCTIONS (for backward compatibility) +// ============================================================================= + +/** + * Sets a servo to wheel mode (continuous rotation) with unlocking. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, any step fails, or an exception occurs. + */ +export async function setWheelMode(servoId) { + checkConnection(); + let unlocked = false; + try { + debugLog(`Setting servo ${servoId} to wheel mode...`); + + // 1. Unlock servo configuration + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`); + } + unlocked = true; + + // 2. Set mode to 1 (Wheel/Speed mode) + const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 1); + if (resMode !== COMM_SUCCESS) { + throw new Error(`Failed to set wheel mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`); + } + + // 3. Lock servo configuration + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } + unlocked = false; + + debugLog(`Successfully set servo ${servoId} to wheel mode.`); + return "success"; + } catch (err) { + console.error(`Exception setting wheel mode for servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Failed to set wheel mode for servo ${servoId}: ${err.message}`); + } +} + +/** + * Sets a servo back to position control mode from wheel mode. + * @param {number} servoId - The ID of the servo (1-252). + * @returns {Promise<"success">} Resolves with "success". + * @throws {Error} If not connected, any step fails, or an exception occurs. + */ +export async function setPositionMode(servoId) { + checkConnection(); + let unlocked = false; + try { + debugLog(`Setting servo ${servoId} back to position mode...`); + + // 1. Unlock servo configuration + const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0); + if (resUnlock !== COMM_SUCCESS) { + throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`); + } + unlocked = true; + + // 2. Set mode to 0 (Position/Servo mode) + const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 0); + if (resMode !== COMM_SUCCESS) { + throw new Error(`Failed to set position mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`); + } + + // 3. Lock servo configuration + const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1); + if (resLock !== COMM_SUCCESS) { + throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`); + } + unlocked = false; + + debugLog(`Successfully set servo ${servoId} back to position mode.`); + return "success"; + } catch (err) { + console.error(`Exception setting position mode for servo ${servoId}:`, err); + if (unlocked) { + await tryLockServo(servoId); + } + throw new Error(`Failed to set position mode for servo ${servoId}: ${err.message}`); + } +} diff --git a/packages/feetech.js/scsservo_constants.mjs b/packages/feetech.js/scsservo_constants.mjs new file mode 100644 index 0000000000000000000000000000000000000000..902ecdf195deca980ea4b90c053e766c5f2bf937 --- /dev/null +++ b/packages/feetech.js/scsservo_constants.mjs @@ -0,0 +1,53 @@ +// Constants for FeetTech SCS servos + +// Constants +export const BROADCAST_ID = 0xfe; // 254 +export const MAX_ID = 0xfc; // 252 + +// Protocol instructions +export const INST_PING = 1; +export const INST_READ = 2; +export const INST_WRITE = 3; +export const INST_REG_WRITE = 4; +export const INST_ACTION = 5; +export const INST_SYNC_WRITE = 131; // 0x83 +export const INST_SYNC_READ = 130; // 0x82 +export const INST_STATUS = 85; // 0x55, status packet instruction (0x55) + +// Communication results +export const COMM_SUCCESS = 0; // tx or rx packet communication success +export const COMM_PORT_BUSY = -1; // Port is busy (in use) +export const COMM_TX_FAIL = -2; // Failed transmit instruction packet +export const COMM_RX_FAIL = -3; // Failed get status packet +export const COMM_TX_ERROR = -4; // Incorrect instruction packet +export const COMM_RX_WAITING = -5; // Now receiving status packet +export const COMM_RX_TIMEOUT = -6; // There is no status packet +export const COMM_RX_CORRUPT = -7; // Incorrect status packet +export const COMM_NOT_AVAILABLE = -9; + +// Packet constants +export const TXPACKET_MAX_LEN = 250; +export const RXPACKET_MAX_LEN = 250; + +// Protocol Packet positions +export const PKT_HEADER0 = 0; +export const PKT_HEADER1 = 1; +export const PKT_ID = 2; +export const PKT_LENGTH = 3; +export const PKT_INSTRUCTION = 4; +export const PKT_ERROR = 4; +export const PKT_PARAMETER0 = 5; + +// Protocol Error bits +export const ERRBIT_VOLTAGE = 1; +export const ERRBIT_ANGLE = 2; +export const ERRBIT_OVERHEAT = 4; +export const ERRBIT_OVERELE = 8; +export const ERRBIT_OVERLOAD = 32; + +// Control table addresses (SCS servos) +export const ADDR_SCS_TORQUE_ENABLE = 40; +export const ADDR_SCS_GOAL_ACC = 41; +export const ADDR_SCS_GOAL_POSITION = 42; +export const ADDR_SCS_GOAL_SPEED = 46; +export const ADDR_SCS_PRESENT_POSITION = 56; diff --git a/packages/feetech.js/test.html b/packages/feetech.js/test.html new file mode 100644 index 0000000000000000000000000000000000000000..5db9ae3a1c690ede2510f3668df6f6a5488dce7e --- /dev/null +++ b/packages/feetech.js/test.html @@ -0,0 +1,770 @@ + + + + + + Feetech Servo Test + + + +
+

Feetech Servo Test Page

+ +
+ Key Concepts +

Understanding these parameters is crucial for controlling Feetech servos:

+
    +
  • + Mode: Determines the servo's primary function. +
      +
    • + Mode 0: Position/Servo Mode. The servo moves to and holds a specific + angular position. +
    • +
    • + Mode 1: Wheel/Speed Mode. The servo rotates continuously at a specified + speed and direction, like a motor. +
    • +
    + Changing the mode requires unlocking, writing the mode value (0 or 1), and locking the + configuration. +
  • +
  • + Position: In Position Mode (Mode 0), this value represents the target + or current angular position of the servo's output shaft. +
      +
    • + Range: Typically 0 to 4095 (representing a 12-bit + resolution). +
    • +
    • + Meaning: Corresponds to the servo's rotational range (e.g., 0-360 degrees or 0-270 + degrees, depending on the specific servo model). 0 is one end of the + range, 4095 is the other. +
    • +
    +
  • +
  • + Speed (Wheel Mode): In Wheel Mode (Mode 1), this value controls the + rotational speed and direction. +
      +
    • + Range: Typically -2500 to +2500. (Note: Some documentation + might mention -1023 to +1023, but the SDK example uses a wider range). +
    • +
    • + Meaning: 0 stops the wheel. Positive values rotate in one direction + (e.g., clockwise), negative values rotate in the opposite direction (e.g., + counter-clockwise). The magnitude determines the speed (larger absolute value means + faster rotation). +
    • +
    • Control Address: ADDR_SCS_GOAL_SPEED (Register 46/47).
    • +
    +
  • +
  • + Acceleration: Controls how quickly the servo changes speed to reach its + target position (in Position Mode) or target speed (in Wheel Mode). +
      +
    • Range: Typically 0 to 254.
    • +
    • + Meaning: Defines the rate of change of speed. The unit is 100 steps/s². + 0 usually means instantaneous acceleration (or minimal delay). Higher + values result in slower, smoother acceleration and deceleration. For example, a + value of 10 means the speed changes by 10 * 100 = 1000 steps per + second, per second. This helps reduce jerky movements and mechanical stress. +
    • +
    • Control Address: ADDR_SCS_GOAL_ACC (Register 41).
    • +
    +
  • +
  • + Baud Rate: The speed of communication between the controller and the + servo. It must match on both ends. Servos often support multiple baud rates, selectable + via an index: +
      +
    • Index 0: 1,000,000 bps
    • +
    • Index 1: 500,000 bps
    • +
    • Index 2: 250,000 bps
    • +
    • Index 3: 128,000 bps
    • +
    • Index 4: 115,200 bps
    • +
    • Index 5: 76,800 bps
    • +
    • Index 6: 57,600 bps
    • +
    • Index 7: 38,400 bps
    • +
    +
  • +
+
+ +
+

Connection

+ + +

Status: Disconnected

+ + + + +
+ +
+

Scan Servos

+ + + + + +

Scan Results:

+

+				
+			
+ +
+

Single Servo Control

+ +
+ + + +
+ + + +
+ + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + +
+ +
+

Sync Operations

+ + +
+ + + +
+ + + + + +
+ +
+

Log Output

+

+			
+
+ + + + diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000000000000000000000000000000000000..d64750f3e26eaf5f0b5c586a7185b0d30ca20670 --- /dev/null +++ b/src/app.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; + +@import "tw-animate-css"; +@plugin "@iconify/tailwind4"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.216 0.006 56.043); + --primary-foreground: oklch(0.985 0.001 106.423); + --secondary: oklch(0.97 0.001 106.424); + --secondary-foreground: oklch(0.216 0.006 56.043); + --muted: oklch(0.97 0.001 106.424); + --muted-foreground: oklch(0.553 0.013 58.071); + --accent: oklch(0.97 0.001 106.424); + --accent-foreground: oklch(0.216 0.006 56.043); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.923 0.003 48.717); + --input: oklch(0.923 0.003 48.717); + --ring: oklch(0.709 0.01 56.259); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.001 106.423); + --sidebar-foreground: oklch(0.147 0.004 49.25); + --sidebar-primary: oklch(0.216 0.006 56.043); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.97 0.001 106.424); + --sidebar-accent-foreground: oklch(0.216 0.006 56.043); + --sidebar-border: oklch(0.923 0.003 48.717); + --sidebar-ring: oklch(0.709 0.01 56.259); +} + +.dark { + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --popover: oklch(0.216 0.006 56.043); + --popover-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.923 0.003 48.717); + --primary-foreground: oklch(0.216 0.006 56.043); + --secondary: oklch(0.268 0.007 34.298); + --secondary-foreground: oklch(0.985 0.001 106.423); + --muted: oklch(0.268 0.007 34.298); + --muted-foreground: oklch(0.709 0.01 56.259); + --accent: oklch(0.268 0.007 34.298); + --accent-foreground: oklch(0.985 0.001 106.423); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.553 0.013 58.071); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.216 0.006 56.043); + --sidebar-foreground: oklch(0.985 0.001 106.423); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.268 0.007 34.298); + --sidebar-accent-foreground: oklch(0.985 0.001 106.423); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.553 0.013 58.071); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..955d6f90b8308833a905b67f5a33e808a587fa1c --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,20 @@ +import type { InteractivityProps } from '@threlte/extras' + +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } + namespace Threlte { + interface UserProps extends InteractivityProps { + interactivity?: boolean; + } + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000000000000000000000000000000000000..77a5ff52c9239ef2a5c38ba452c659f49e64a7db --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/components/3d/Floor.svelte b/src/lib/components/3d/Floor.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7fbba81efb5715843d3718c1acd24b637622dd62 --- /dev/null +++ b/src/lib/components/3d/Floor.svelte @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/lib/components/3d/elements/compute/ComputeGridItem.svelte b/src/lib/components/3d/elements/compute/ComputeGridItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..447bc098235b20f0dc7f7255538cd92cf17f6fc5 --- /dev/null +++ b/src/lib/components/3d/elements/compute/ComputeGridItem.svelte @@ -0,0 +1,51 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/Computes.svelte b/src/lib/components/3d/elements/compute/Computes.svelte new file mode 100644 index 0000000000000000000000000000000000000000..90d4557e7bca1daaae72b007ab2f38b1f69037dd --- /dev/null +++ b/src/lib/components/3d/elements/compute/Computes.svelte @@ -0,0 +1,87 @@ + + +{#each remoteComputeManager.computes as compute (compute.id)} + +{/each} + +{#if selectedCompute} + + + + + + + + +{/if} \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/GPU.svelte b/src/lib/components/3d/elements/compute/GPU.svelte new file mode 100644 index 0000000000000000000000000000000000000000..92dc3ca07dc9d484862be2aa0d33da03825e6d7d --- /dev/null +++ b/src/lib/components/3d/elements/compute/GPU.svelte @@ -0,0 +1,138 @@ + + + + + + + + + + + + diff --git a/src/lib/components/3d/elements/compute/GPUModel.svelte b/src/lib/components/3d/elements/compute/GPUModel.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1a05fe6f4efcdb3079ac1e49bdb5bd12ca7e1a5f --- /dev/null +++ b/src/lib/components/3d/elements/compute/GPUModel.svelte @@ -0,0 +1,200 @@ + + + + + + {#await gltf} + {@render fallback?.()} + {:then gltf} + + + + + + + + + + + + + + + + + + + + + + + + {:catch err} + {@render error?.({ error: err })} + {/await} + + {@render children?.({ ref })} + diff --git a/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte b/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3d9b91d81b9ec965d47d3e4fb0388075f6bbd2ab --- /dev/null +++ b/src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte @@ -0,0 +1,382 @@ + + + + + + + + AI Compute Session - {compute.name || 'No Compute Selected'} + + + Configure and manage ACT model inference sessions for robot control + + + +
+ +
+
+ + Session Status +
+ {#if compute.hasSession} + + {compute.statusInfo.statusText} + + {:else} + No Session + {/if} +
+ + + {#if compute.hasSession && compute.sessionData} + + + + + Current Session + + + +
+
+
+
+ Session ID: + {compute.sessionId} +
+
+ Status: + {compute.statusInfo.emoji} {compute.statusInfo.statusText} +
+
+ Policy: + {compute.sessionConfig?.policyPath} +
+
+ Cameras: + {compute.sessionConfig?.cameraNames.join(', ')} +
+
+
+ + +
+
📡 Inference Server Connections
+
+
+ Workspace: + {compute.sessionData.workspace_id} +
+ {#each Object.entries(compute.sessionData.camera_room_ids) as [camera, roomId]} +
+ 📹 {camera}: + {roomId} +
+ {/each} +
+ 📥 Joint Input: + {compute.sessionData.joint_input_room_id} +
+
+ 📤 Joint Output: + {compute.sessionData.joint_output_room_id} +
+
+
+ + +
+ {#if compute.canStart} + + {/if} + {#if compute.canStop} + + {/if} + +
+
+
+
+ {/if} + + + {#if !compute.hasSession} + + + + + Create AI Session + + + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +

Comma-separated camera names

+
+
+ + +

Configure in settings panel

+
+
+ +
+ + +
+ + + + + This will create a new ACT inference session with dedicated rooms for camera inputs, + joint inputs, and joint outputs in the inference server communication system. + + + + +
+
+
+ {/if} + + +
+ + AI sessions require a trained ACT model and create dedicated communication rooms for video inputs, + robot joint states, and control outputs in the inference server system. +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte b/src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f5aa0513c28856b99a3b125f93922d7ab34e1c6a --- /dev/null +++ b/src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte @@ -0,0 +1,291 @@ + + + + + + + + Robot Input - {compute.name || 'No Compute Selected'} + + + Connect robot joint data as input for AI inference + + + +
+ +
+
+ + AI Session +
+ {#if compute.hasSession} + + {compute.statusInfo.statusText} + + {:else} + No Session + {/if} +
+ + {#if !compute.hasSession} + + + + + AI Session Required + + + + You need to create an AI session before connecting robot inputs. + The session provides a joint input room for receiving robot data. + + + {:else} + + + + + + Robot Input Connection + + + + +
+
Available Robots:
+
+ {#if robots.length === 0} +
+ No robots available. Add robots first. +
+ {:else} + {#each robots as robot} + + {/each} + {/if} +
+
+ + + {#if selectedRobotId} +
+
+
+

+ Selected Robot: {selectedRobotId} +

+

+ {connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'} +

+
+ {#if connectedRobotId !== selectedRobotId} + + {:else} + + {/if} +
+
+ {/if} +
+
+ + + + + + + Data Flow: Robot → AI Session + + + +
+
+ Joint Input Room: + {compute.sessionData?.joint_input_room_id} +
+
+ The robot will act as a PRODUCER and send its current joint positions to this room for AI processing. + The inference server receives this data as a CONSUMER. + All joint values should be normalized (-100 to +100 for most joints, 0 to 100 for gripper). +
+
+
+
+ + + {#if connectedRobotId} + + + + + Active Connection + + + +
+ Robot {connectedRobotId} is now sending joint data to the AI session as a producer. + The AI model will use this data along with camera inputs for inference. +
+
+
+ {/if} + {/if} + + +
+ + Robot input: Robot acts as PRODUCER sending joint positions → Inference server acts as CONSUMER receiving data for processing. +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte b/src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1bdb0f4e7a3952eb5ba28f56a4ca27d9108e31ab --- /dev/null +++ b/src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte @@ -0,0 +1,288 @@ + + + + + + + + Robot Output - {compute.name || 'No Compute Selected'} + + + Connect AI command output to control robot actuators + + + +
+ +
+
+ + AI Session +
+ {#if compute.hasSession} + + {compute.statusInfo.statusText} + + {:else} + No Session + {/if} +
+ + {#if !compute.hasSession} + + + + + AI Session Required + + + + You need to create an AI session before connecting robot outputs. + The session provides a joint output room for sending AI commands. + + + {:else} + + + + + + Robot Output Connection + + + + +
+
Available Robots:
+
+ {#if robots.length === 0} +
+ No robots available. Add robots first. +
+ {:else} + {#each robots as robot} + + {/each} + {/if} +
+
+ + + {#if selectedRobotId} +
+
+
+

+ Selected Robot: {selectedRobotId} +

+

+ {connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'} +

+
+ {#if connectedRobotId !== selectedRobotId} + + {:else} + + {/if} +
+
+ {/if} +
+
+ + + + + + + Data Flow: AI Session → Robot + + + +
+
+ Joint Output Room: + {compute.sessionData?.joint_output_room_id} +
+
+ The inference server will act as a PRODUCER and send predicted joint commands to this room for robot execution. + The robot receives this data as a CONSUMER. + All joint values will be normalized (-100 to +100 for most joints, 0 to 100 for gripper). +
+
+
+
+ + + {#if connectedRobotId} + + + + + Active Connection + + + +
+ Robot {connectedRobotId} is now receiving AI commands as a consumer. + The robot will execute joint movements based on AI inference results. +
+
+
+ {/if} + {/if} + + +
+ + Robot output: Inference server acts as PRODUCER sending commands → Robot acts as CONSUMER receiving and executing movements. +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte b/src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7aea3df522ede40e5796495611852b58c688d8d1 --- /dev/null +++ b/src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte @@ -0,0 +1,276 @@ + + + + + + + + Video Input - {compute.name || 'No Compute Selected'} + + + Connect camera streams to provide visual input for AI inference + + + +
+ +
+
+ + AI Session +
+ {#if compute.hasSession} + + {compute.statusInfo.statusText} + + {:else} + No Session + {/if} +
+ + {#if !compute.hasSession} + + + + + AI Session Required + + + + You need to create an AI session before connecting video inputs. + The session defines which camera names are available for connection. + + + {:else} + + + + + + Camera Connection + + + + +
+
Available Camera Inputs:
+
+ {#each compute.sessionConfig?.cameraNames || [] as cameraName} + + {/each} +
+
+ + +
+
+
+

+ Selected Camera: {selectedCameraName} +

+

+ {localStream ? 'Connected' : 'Not Connected'} +

+
+ {#if !localStream} + + {:else} + + {/if} +
+
+ + + {#if localStream} +
+
Live Preview:
+
+ +
+
+ {/if} +
+
+ + + + + + + Session Camera Details + + + +
+ {#each Object.entries(compute.sessionData?.camera_room_ids || {}) as [camera, roomId]} +
+ {camera} + {roomId} +
+ {/each} +
+
+
+ {/if} + + +
+ + Video inputs stream camera data to the AI model for visual processing. Each camera connects to a dedicated room in the session. +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7aad9bc2ed4eaa971d85a06db6dede5bca689c81 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte @@ -0,0 +1,48 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3a87a2033b1d2b65a1fb877ee3c58b65f1be8665 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte @@ -0,0 +1,56 @@ + + + + + + + + onVideoInputBoxClick(compute)} /> + onRobotInputBoxClick(compute)} /> + + + + + + + + + + + + + onRobotOutputBoxClick(compute)} /> + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0ee46ab25cbbe0c38320fbfc69829eba737799d1 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte @@ -0,0 +1,84 @@ + + + + + + {#if compute.hasSession && compute.inputConnections} + + + + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cc1f585564586ceb161b214b30dad71bb7eac640 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte @@ -0,0 +1,91 @@ + + + + + + {#if compute.hasSession && compute.outputConnections} + + + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte b/src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..31e4129381438f076fb3ac547deb13748f19f51a --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte @@ -0,0 +1,56 @@ + + + e.stopPropagation()} + position.z={0.4} + padding={10} + rotation={[-Math.PI / 2, 0, 0]} + scale={[0.1, 0.1, 0.1]} + pointerEvents="listener" + {visible} +> + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e6001f7f46875f2b43c81637ccdc8387b28ecdf5 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte @@ -0,0 +1,81 @@ + + + + + + {#if compute.hasSession && compute.inputConnections} + + + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b90165c91fe1cf6b1eee2cf07e6f50a2743b414d --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte @@ -0,0 +1,77 @@ + + + + + + {#if compute.hasSession && compute.outputConnections} + + + + + + + {#if compute.isRunning} + + + {:else} + + + {/if} + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte b/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ea76535e60f63e37b848c845d2e2d2034f180eb6 --- /dev/null +++ b/src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte @@ -0,0 +1,82 @@ + + + + + + {#if compute.hasSession && compute.inputConnections} + + + + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/robot/RobotGridItem.svelte b/src/lib/components/3d/elements/robot/RobotGridItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cee7ee2e574214bf607cd2cfe0913e20a5b85b91 --- /dev/null +++ b/src/lib/components/3d/elements/robot/RobotGridItem.svelte @@ -0,0 +1,169 @@ + + + + + {#if urdfRobotState} + {#each getRootLinks(urdfRobotState) as link} + + {/each} + {:else} + + + + + + {/if} + + + + + + diff --git a/src/lib/components/3d/elements/robot/Robots.svelte b/src/lib/components/3d/elements/robot/Robots.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bcd73d8ed65974b859ab4b70a4fb91fc56549325 --- /dev/null +++ b/src/lib/components/3d/elements/robot/Robots.svelte @@ -0,0 +1,81 @@ + + +{#each robotManager.robots as robot (robot.id)} + {}} + onInputBoxClick={onInputBoxClick} + onRobotBoxClick={onRobotBoxClick} + onOutputBoxClick={onOutputBoxClick} + /> +{/each} + + +{#if selectedRobot} + + + +{/if} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts new file mode 100644 index 0000000000000000000000000000000000000000..d03c436f85c4e95eb73b305cd5643e0f7cb00d01 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts @@ -0,0 +1,3 @@ +export default interface IUrdfBox { + size: [x: number, y: number, z: number]; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fb36e4ba86727b8cc82985e4bb5864d13be9c6f --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts @@ -0,0 +1,4 @@ +export default interface IUrdfCylinder { + radius: number; + length: number; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfJoint.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfJoint.ts new file mode 100644 index 0000000000000000000000000000000000000000..745be95c6d8c012ae4db8742d1e26f8316864a58 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfJoint.ts @@ -0,0 +1,44 @@ +import type IUrdfLink from "./IUrdfLink"; + +export interface IUrdfJointLimit { + lower: number; + upper: number; + effort: number; + velocity: number; +} + +export interface IUrdfJointSafetyController { + soft_lower_limit: number; + soft_upper_limit: number; +} + +export default interface IUrdfJoint { + name?: string; + type?: "revolute" | "continuous" | "prismatic" | "fixed" | "floating" | "planar"; + // rpy = roll, pitch, yaw (values between -pi and +pi) + origin_rpy: [roll: number, pitch: number, yaw: number]; + origin_xyz: [x: number, y: number, z: number]; + // calculated rotation for non-fixed joints based on origin_rpy and axis_xyz + rotation: [x: number, y: number, z: number]; + parent: IUrdfLink; + child: IUrdfLink; + // axis for revolute and continuous joints defaults to (1,0,0) + axis_xyz?: [x: number, y: number, z: number]; + calibration?: { + rising?: number; // Calibration rising value in radians + falling?: number; // Calibration falling value in radians + }; + dynamics?: { + damping?: number; + friction?: number; + }; + // only for revolute joints + limit?: IUrdfJointLimit; + mimic?: { + joint: string; + multiplier?: number; + offset?: number; + }; + safety_controller?: IUrdfJointSafetyController; + elem: Element; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfLink.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfLink.ts new file mode 100644 index 0000000000000000000000000000000000000000..041708db0c2b23333c89be022d4d6f6283474b83 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfLink.ts @@ -0,0 +1,23 @@ +import type { IUrdfVisual } from "./IUrdfVisual"; + +interface IUrdfInertia { + ixx: number; + ixy: number; + ixz: number; + iyy: number; + iyz: number; + izz: number; +} + +export default interface IUrdfLink { + name: string; + inertial?: { + origin_xyz?: [x: number, y: number, z: number]; + origin_rpy?: [roll: number, pitch: number, yaw: number]; + mass: number; + inertia: IUrdfInertia; + }; + visual: IUrdfVisual[]; + collision: IUrdfVisual[]; + elem: Element; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfMesh.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfMesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..52272918d2f04095109bac4aab098ccbf005fa68 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfMesh.ts @@ -0,0 +1,5 @@ +export default interface IUrdfMesh { + filename: string; + type: "stl" | "fbx" | "obj" | "dae"; + scale: [x: number, y: number, z: number]; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5b2f0196afe7b39e9b8e21643a25053998ea675 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.ts @@ -0,0 +1,10 @@ +import type IUrdfJoint from "./IUrdfJoint"; +import type IUrdfLink from "./IUrdfLink"; + +export default interface IUrdfRobot { + name: string; + links: { [name: string]: IUrdfLink }; + joints: IUrdfJoint[]; + // the DOM element holding the XML, so we can work non-destructive + elem?: Element; +} diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfVisual.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfVisual.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b8004127f8fdf1757528affaef6b6c504a7d1d0 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfVisual.ts @@ -0,0 +1,56 @@ +import type IUrdfBox from "./IUrdfBox"; +import type IUrdfCylinder from "./IUrdfCylinder"; +import type IUrdfMesh from "./IUrdfMesh"; + +// 1) Box‐type visual +interface IUrdfVisualBox { + name: string; + origin_xyz: [x: number, y: number, z: number]; + origin_rpy: [roll: number, pitch: number, yaw: number]; + geometry: IUrdfBox; + material?: { + name: string; + color?: string; + texture?: string; + }; + type: "box"; + // optional RGBA color override + color_rgba?: [r: number, g: number, b: number, a: number]; + // XML Element reference + elem: Element; +} + +// 2) Cylinder‐type visual +interface IUrdfVisualCylinder { + name: string; + origin_xyz: [x: number, y: number, z: number]; + origin_rpy: [roll: number, pitch: number, yaw: number]; + geometry: IUrdfCylinder; + material?: { + name: string; + color?: string; + texture?: string; + }; + type: "cylinder"; + color_rgba?: [r: number, g: number, b: number, a: number]; + elem: Element; +} + +// 3) Mesh‐type visual +interface IUrdfVisualMesh { + name: string; + origin_xyz: [x: number, y: number, z: number]; + origin_rpy: [roll: number, pitch: number, yaw: number]; + geometry: IUrdfMesh; + material?: { + name: string; + color?: string; + texture?: string; + }; + type: "mesh"; + color_rgba?: [r: number, g: number, b: number, a: number]; + elem: Element; +} + +// Now make a union of the three: +export type IUrdfVisual = IUrdfVisualBox | IUrdfVisualCylinder | IUrdfVisualMesh; diff --git a/src/lib/components/3d/elements/robot/URDF/interfaces/index.ts b/src/lib/components/3d/elements/robot/URDF/interfaces/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..025994d5bf6c3232145ec3eba14f650fbd03bd47 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from "./IUrdfBox"; +export * from "./IUrdfCylinder"; +export * from "./IUrdfJoint"; +export * from "./IUrdfLink"; +export * from "./IUrdfMesh"; +export * from "./IUrdfRobot"; +export * from "./IUrdfVisual"; diff --git a/src/lib/components/3d/elements/robot/URDF/mesh/DAE.svelte b/src/lib/components/3d/elements/robot/URDF/mesh/DAE.svelte new file mode 100644 index 0000000000000000000000000000000000000000..621452de065cb0dba376d38ec4446700c10222a1 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/mesh/DAE.svelte @@ -0,0 +1,67 @@ + + + + +{#await load(filename) then dae} + {@html ``} + + + + {#each dae.scene.children as obj} + {#if obj.type === "Mesh"} + {@const mesh = obj as Mesh} + + + + {/if} + {/each} + + + +{/await} diff --git a/src/lib/components/3d/elements/robot/URDF/mesh/OBJ.svelte b/src/lib/components/3d/elements/robot/URDF/mesh/OBJ.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a835f9f929a3228b8c429e7e155d0ccaddcc6aff --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/mesh/OBJ.svelte @@ -0,0 +1,43 @@ + + + + +{#await load(filename) then obj} + {@html ``} + + + +{/await} diff --git a/src/lib/components/3d/elements/robot/URDF/mesh/STL.svelte b/src/lib/components/3d/elements/robot/URDF/mesh/STL.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2dd6ddb2ed2b25bb1d2f37f30c3d9d217200ef2a --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/mesh/STL.svelte @@ -0,0 +1,43 @@ + + + + +{#await load(filename) then stl} + {@html ``} + + + +{/await} diff --git a/src/lib/components/3d/elements/robot/URDF/primitives/UrdfJoint.svelte b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfJoint.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cc9ff90995d410820258e2c5111d2d5707ca398a --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfJoint.svelte @@ -0,0 +1,189 @@ + + + + +{@html ``} + +{#if showLine} + + + + +{/if} + + + {#if joint.child} + + {/if} + + {#if showLine} + + + + + + + + + + {/if} + + + +{#if showName} + + + + +{/if} diff --git a/src/lib/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte new file mode 100644 index 0000000000000000000000000000000000000000..299ba6435f92f718ad2a8b51e4d3368dea485f61 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte @@ -0,0 +1,124 @@ + + + + +{@html ``} + + +{#if showVisual} + {#each link.visual as visual} + + {/each} +{/if} + +{#if showCollision} + {#each link.collision as visual} + + {/each} +{/if} + +{#each getChildJoints(robot, link) as joint (joint.name)} + + {#if joint.type === "fixed" && showPointCloud} + + + + + + + + + + {/if} +{/each} diff --git a/src/lib/components/3d/elements/robot/URDF/primitives/UrdfThree.svelte b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfThree.svelte new file mode 100644 index 0000000000000000000000000000000000000000..86fb3a2ff19e3a83996c1f86c5841ad2bc92cf64 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfThree.svelte @@ -0,0 +1,78 @@ + + + + + + {#each getRootLinks(robot) as link} + + {/each} + diff --git a/src/lib/components/3d/elements/robot/URDF/primitives/UrdfVisual.svelte b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfVisual.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f5fce75bf6cb9a04cbd52e4a1ddba138bab6aeba --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/primitives/UrdfVisual.svelte @@ -0,0 +1,101 @@ + + +{#if visual.type === "mesh"} + {#if visual.geometry.type === "stl"} + + {:else if visual.geometry.type === "obj"} + + {:else if visual.geometry.type === "dae"} + + {/if} +{:else if visual.type === "cylinder"} + + + + +{:else if visual.type === "box"} + + + + +{/if} diff --git a/src/lib/components/3d/elements/robot/URDF/runes/urdf_state.svelte.ts b/src/lib/components/3d/elements/robot/URDF/runes/urdf_state.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..30a95ca8b2cccc656becf7b407fdb98be2a9a7a4 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/runes/urdf_state.svelte.ts @@ -0,0 +1,132 @@ +import type IUrdfJoint from "../interfaces/IUrdfJoint"; +import type IUrdfLink from "../interfaces/IUrdfLink"; +import type IUrdfRobot from "../interfaces/IUrdfRobot"; + +// Color constants for better maintainability +export const URDF_COLORS = { + COLLISION: "#813d9c", // purple + JOINT: "#62a0ea", // blue + LINK: "#57e389", // green + JOINT_INDICATOR: "#f66151", // red + HIGHLIGHT: "#ffa348", // orange + BACKGROUND: "#241f31" // dark purple +} as const; + +// Transform tool types +export type TransformTool = "translate" | "rotate" | "scale"; + +// Joint state tracking +export interface JointStates { + continuous: Record; + revolute: Record; +} + +// Visibility configuration +export interface VisibilityConfig { + visual: boolean; + collision: boolean; + joints: boolean; + jointNames: boolean; + linkNames: boolean; +} + +// Appearance settings +export interface AppearanceConfig { + colors: { + collision: string; + joint: string; + link: string; + jointIndicator: string; + background: string; + }; + opacity: { + visual: number; + collision: number; + link: number; + }; +} + +// Editor configuration +export interface EditorConfig { + isEditMode: boolean; + currentTool: TransformTool; + snap: { + translation: number; + scale: number; + rotation: number; + }; +} + +// View configuration +export interface ViewConfig { + zoom: { + current: number; + initial: number; + }; + nameHeight: number; +} + +// Main URDF state interface +export interface UrdfState { + robot?: IUrdfRobot; + jointStates: JointStates; + visibility: VisibilityConfig; + appearance: AppearanceConfig; + editor: EditorConfig; + view: ViewConfig; +} + +// Create the reactive state +export const urdfState = $state({ + // Robot data + robot: undefined, + jointStates: { + continuous: {}, + revolute: {} + }, + + // Visibility settings + visibility: { + visual: true, + collision: false, + joints: true, + jointNames: true, + linkNames: true + }, + + // Appearance settings + appearance: { + colors: { + collision: URDF_COLORS.COLLISION, + joint: URDF_COLORS.JOINT, + link: URDF_COLORS.LINK, + jointIndicator: URDF_COLORS.JOINT_INDICATOR, + background: URDF_COLORS.BACKGROUND + }, + opacity: { + visual: 1.0, + collision: 0.7, + link: 1.0 + } + }, + + // Editor configuration + editor: { + isEditMode: false, + currentTool: "translate", + snap: { + translation: 0.001, + scale: 0.001, + rotation: 1 + } + }, + + // View configuration + view: { + zoom: { + current: 1.3, + initial: 1.3 + }, + nameHeight: 0.05 + } +}); diff --git a/src/lib/components/3d/elements/robot/URDF/utils/UrdfParser.ts b/src/lib/components/3d/elements/robot/URDF/utils/UrdfParser.ts new file mode 100644 index 0000000000000000000000000000000000000000..188eb54a701886381ed0bd5fd51bb094ed229c5f --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/utils/UrdfParser.ts @@ -0,0 +1,624 @@ +// create interface‐instances of the model based on XML‐URDF file. +// These interfaces are closer to the Three.js structure so it's easy to visualize. + +import type { IUrdfVisual } from "../interfaces/IUrdfVisual"; +import { xyzFromString, rpyFromString, rgbaFromString } from "./helper"; +import type IUrdfLink from "../interfaces/IUrdfLink"; +import type IUrdfJoint from "../interfaces/IUrdfJoint"; +import type IUrdfMesh from "../interfaces/IUrdfMesh"; +import type IUrdfRobot from "../interfaces/IUrdfRobot"; + +/** + * Find all "root" links of a robot. A link is considered a root if + * no joint in the robot references it as a "child". In other words, + * it has no parent joint. + * + * @param robot - The parsed IUrdfRobot object whose links and joints we examine + * @returns An array of IUrdfLink objects that have no parent joint (i.e. root links) + */ +export function getRootLinks(robot: IUrdfRobot): IUrdfLink[] { + // Compute the links + const links: IUrdfLink[] = []; + const joints = robot.joints; + + for (const link of Object.values(robot.links)) { + let isRoot = true; + for (const joint of joints) { + if (joint.child.name === link.name) { + isRoot = false; + break; + } + } + + if (isRoot) { + links.push(link); + } + } + + + + return links; +} + +/** + * Find all "root" joints of a robot. A joint is considered a root if + * its parent link is never used as a "child link" anywhere else. + * + * For example, if Joint A's parent is "Base" and no other joint has + * child="Base", then Joint A is a root joint. + * + * @param robot - The parsed IUrdfRobot object + * @returns An array of IUrdfJoint objects with no parent joint (i.e. root joints) + */ +export function getRootJoints(robot: IUrdfRobot): IUrdfJoint[] { + const joints = robot.joints; + const rootJoints: IUrdfJoint[] = []; + + for (const joint of joints) { + let isRoot = true; + + // If any other joint's child matches this joint's parent, then this joint isn't root + for (const parentJoint of joints) { + if (joint.parent.name === parentJoint.child.name) { + isRoot = false; + break; + } + } + + if (isRoot) { + rootJoints.push(joint); + } + } + + return rootJoints; +} + +/** + * Given a parent link, find all joints in the robot that use that link as their parent. + * + * @param robot - The parsed IUrdfRobot object + * @param parent - An IUrdfLink object to use as the "parent" in comparison + * @returns A list of IUrdfJoint objects whose parent.name matches parent.name + */ +export function getChildJoints(robot: IUrdfRobot, parent: IUrdfLink): IUrdfJoint[] { + const childJoints: IUrdfJoint[] = []; + const joints = robot.joints; + if (!joints) { + return []; + } + + for (const joint of joints) { + if (joint.parent.name === parent.name) { + childJoints.push(joint); + } + } + + return childJoints; +} + +/** + * Update the element's attributes (xyz and rpy) in the XML + * for either a joint or a visual element, based on the object's current origin_xyz/origin_rpy. + * + * @param posable - Either an IUrdfJoint or an IUrdfVisual whose `.elem` has an child + */ +export function updateOrigin(posable: IUrdfJoint | IUrdfVisual) { + const origin = posable.elem.getElementsByTagName("origin")[0]; + origin.setAttribute("xyz", posable.origin_xyz.join(" ")); + origin.setAttribute("rpy", posable.origin_rpy.join(" ")); +} + +/** + * Main URDF parser class. Given a URDF filename (or XML string), it will: + * 1) Fetch the URDF text (if given a URL/filename) + * 2) Parse materials/colors + * 3) Parse links (including visual & collision) + * 4) Parse joints + * 5) Build an IUrdfRobot data structure that is easier to traverse in JS/Three.js + */ +export class UrdfParser { + filename: string; + prefix: string; // e.g. "robots/so_arm100/" + colors: { [name: string]: [number, number, number, number] } = {}; + robot: IUrdfRobot = { name: "", links: {}, joints: [] }; + + /** + * @param filename - Path or URL to the URDF file (XML). May be relative. + * @param prefix - A folder prefix used when resolving "package://" or relative mesh paths. + */ + constructor(filename: string, prefix: string = "") { + this.filename = filename; + // Ensure prefix ends with exactly one slash + this.prefix = prefix.endsWith("/") ? prefix : prefix + "/"; + } + + /** + * Fetch the URDF file from `this.filename` and return its text. + * @returns A promise that resolves to the raw URDF XML string. + */ + async load(): Promise { + return fetch(this.filename).then((res) => res.text()); + } + + /** + * Clear any previously parsed robot data, preparing for a fresh parse. + */ + reset() { + this.robot = { name: "", links: {}, joints: [] }; + } + + /** + * Parse a URDF XML string and produce an IUrdfRobot object. + * + * @param data - A string containing valid URDF XML. + * @returns The fully populated IUrdfRobot, including colors, links, and joints. + * @throws If the root element is not . + */ + fromString(data: string): IUrdfRobot { + this.reset(); + const dom = new window.DOMParser().parseFromString(data, "text/xml"); + this.robot.elem = dom.documentElement; + return this.parseRobotXMLNode(dom.documentElement); + } + + /** + * Internal helper: ensure the root node is , then parse its children. + * + * @param robotNode - The Element from the DOMParser. + * @returns The populated IUrdfRobot data structure. + * @throws If robotNode.nodeName !== "robot" + */ + private parseRobotXMLNode(robotNode: Element): IUrdfRobot { + if (robotNode.nodeName !== "robot") { + throw new Error(`Invalid URDF: no (found <${robotNode.nodeName}>)`); + } + + this.robot.name = robotNode.getAttribute("name") || ""; + this.parseColorsFromRobot(robotNode); + this.parseLinks(robotNode); + this.parseJoints(robotNode); + return this.robot; + } + + /** + * Look at all tags under and store their names → RGBA values. + * + * @param robotNode - The Element. + */ + private parseColorsFromRobot(robotNode: Element) { + const xmlMaterials = robotNode.getElementsByTagName("material"); + for (let i = 0; i < xmlMaterials.length; i++) { + const matNode = xmlMaterials[i]; + if (!matNode.hasAttribute("name")) { + console.warn("Found with no name attribute"); + continue; + } + const name = matNode.getAttribute("name")!; + const colorTags = matNode.getElementsByTagName("color"); + if (colorTags.length === 0) continue; + + const colorElem = colorTags[0]; + if (!colorElem.hasAttribute("rgba")) continue; + + // e.g. "0.06 0.4 0.1 1.0" + const rgba = rgbaFromString(colorElem) || [0, 0, 0, 1]; + this.colors[name] = rgba; + } + } + + /** + * Parse every under and build an IUrdfLink entry containing: + * - name + * - arrays of IUrdfVisual for tags + * - arrays of IUrdfVisual for tags + * - a pointer to its original XML Element (elem) + * + * @param robotNode - The Element. + */ + private parseLinks(robotNode: Element) { + const xmlLinks = robotNode.getElementsByTagName("link"); + for (let i = 0; i < xmlLinks.length; i++) { + const linkXml = xmlLinks[i]; + if (!linkXml.hasAttribute("name")) { + console.error("Link without a name:", linkXml); + continue; + } + const linkName = linkXml.getAttribute("name")!; + + const linkObj: IUrdfLink = { + name: linkName, + visual: [], + collision: [], + elem: linkXml + }; + this.robot.links[linkName] = linkObj; + + // Parse all children + const visualXmls = linkXml.getElementsByTagName("visual"); + for (let j = 0; j < visualXmls.length; j++) { + linkObj.visual.push(this.parseVisual(visualXmls[j])); + } + + // Parse all children (reuse parseVisual; color is ignored later) + const collXmls = linkXml.getElementsByTagName("collision"); + for (let j = 0; j < collXmls.length; j++) { + linkObj.collision.push(this.parseVisual(collXmls[j])); + } + } + } + + /** + * Parse a or element into an IUrdfVisual. Reads: + * - (calls parseGeometry to extract mesh, cylinder, box, etc.) + * - (xyz, rpy) + * - (either embedded or named reference) + * + * @param node - The or Element. + * @returns A fully populated IUrdfVisual object. + */ + private parseVisual(node: Element): IUrdfVisual { + const visual: Partial = { elem: node }; + + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + + // Skip non-element nodes (like text nodes containing whitespace) + if (child.nodeType !== Node.ELEMENT_NODE) { + continue; + } + + const childElement = child as Element; + switch (childElement.nodeName) { + case "geometry": { + this.parseGeometry(childElement, visual); + break; + } + case "origin": { + const pos = xyzFromString(childElement); + const rpy = rpyFromString(childElement); + if (pos) visual.origin_xyz = pos; + if (rpy) visual.origin_rpy = rpy; + break; + } + case "material": { + const cols = childElement.getElementsByTagName("color"); + if (cols.length > 0 && cols[0].hasAttribute("rgba")) { + // Inline color specification + visual.color_rgba = rgbaFromString(cols[0])!; + } else if (childElement.hasAttribute("name")) { + // Named material → look up previously parsed RGBA + const nm = childElement.getAttribute("name")!; + visual.color_rgba = this.colors[nm]; + } + break; + } + default: { + console.warn("Unknown child node:", childElement.nodeName); + break; + } + } + } + + return visual as IUrdfVisual; + } + + /** + * Parse a element inside or . + * Currently only supports . If you need or , + * you can extend this function similarly. + * + * @param node - The Element. + * @param visual - A partial IUrdfVisual object to populate + */ + private parseGeometry(node: Element, visual: Partial) { + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + + // Skip non-element nodes (like text nodes containing whitespace) + if (child.nodeType !== Node.ELEMENT_NODE) { + continue; + } + + const childElement = child as Element; + if (childElement.nodeName === "mesh") { + const rawFilename = childElement.getAttribute("filename"); + if (!rawFilename) { + console.warn(" missing filename!"); + return; + } + + // 1) Resolve the URL (handles "package://" or relative paths) + const resolvedUrl = this.resolveFilename(rawFilename); + + // 2) Parse optional scale (e.g. "1 1 1") + let scale: [number, number, number] = [1, 1, 1]; + if (childElement.hasAttribute("scale")) { + const parts = childElement.getAttribute("scale")!.split(" ").map(parseFloat); + if (parts.length === 3) { + scale = [parts[0], parts[1], parts[2]]; + } + } + + // 3) Deduce mesh type from file extension + const ext = resolvedUrl.slice(resolvedUrl.lastIndexOf(".") + 1).toLowerCase(); + let type: "stl" | "fbx" | "obj" | "dae"; + switch (ext) { + case "stl": + type = "stl"; + break; + case "fbx": + type = "fbx"; + break; + case "obj": + type = "obj"; + break; + case "dae": + type = "dae"; + break; + default: + throw new Error("Unknown mesh extension: " + ext); + } + + visual.geometry = { filename: resolvedUrl, type, scale } as IUrdfMesh; + visual.type = "mesh"; + return; + } + + // If you also want or , copy your previous logic here: + // e.g. if (childElement.nodeName === "cylinder") { … } + } + } + + /** + * Transform a URI‐like string into an actual URL. Handles: + * 1) http(s):// or data: → leave unchanged + * 2) package://some_package/... → replace with prefix + "some_package/... + * 3) package:/some_package/... → same as above + * 4) Anything else (e.g. "meshes/Foo.stl") is treated as relative. + * + * @param raw - The raw filename from URDF (e.g. "meshes/Base.stl" or "package://my_pkg/mesh.dae") + * @returns The fully resolved URL string + */ + private resolveFilename(raw: string): string { + // 1) absolute http(s) or data URIs + if (/^https?:\/\//.test(raw) || raw.startsWith("data:")) { + return raw; + } + + // 2) package://some_package/… + if (raw.startsWith("package://")) { + const rel = raw.substring("package://".length); + return this.joinUrl(this.prefix, rel); + } + + // 3) package:/some_package/… + if (raw.startsWith("package:/")) { + const rel = raw.substring("package:/".length); + return this.joinUrl(this.prefix, rel); + } + + // 4) anything else (e.g. "meshes/Foo.stl") is treated as relative + return this.joinUrl(this.prefix, raw); + } + + /** + * Helper to join a base URL with a relative path, ensuring exactly one '/' in between + * + * @param base - e.g. "/robots/so_arm100/" + * @param rel - e.g. "meshes/Base.stl" (with or without a leading slash) + * @returns A string like "/robots/so_arm100/meshes/Base.stl" + */ + private joinUrl(base: string, rel: string): string { + if (!base.startsWith("/")) base = "/" + base; + if (!base.endsWith("/")) base = base + "/"; + if (rel.startsWith("/")) rel = rel.substring(1); + return base + rel; + } + + /** + * Parse every under and build an IUrdfJoint entry. For each joint: + * 1) parent link (lookup in `this.robot.links[parentName]`) + * 2) child link (lookup in `this.robot.links[childName]`) + * 3) origin: xyz + rpy + * 4) axis (default [0,0,1] if absent) + * 5) limit (if present, lower/upper/effort/velocity) + * + * @param robotNode - The Element. + * @throws If a joint references a link name that doesn't exist. + */ + private parseJoints(robotNode: Element) { + const links = this.robot.links; + const joints: IUrdfJoint[] = []; + this.robot.joints = joints; + + const xmlJoints = robotNode.getElementsByTagName("joint"); + for (let i = 0; i < xmlJoints.length; i++) { + const jointXml = xmlJoints[i]; + const parentElems = jointXml.getElementsByTagName("parent"); + const childElems = jointXml.getElementsByTagName("child"); + if (parentElems.length !== 1 || childElems.length !== 1) { + console.warn("Joint without exactly one or :", jointXml); + continue; + } + + const parentName = parentElems[0].getAttribute("link")!; + const childName = childElems[0].getAttribute("link")!; + + const parentLink = links[parentName]; + const childLink = links[childName]; + if (!parentLink || !childLink) { + throw new Error(`Joint references missing link: ${parentName} or ${childName}`); + } + + // Default origin and rpy + let xyz: [number, number, number] = [0, 0, 0]; + let rpy: [number, number, number] = [0, 0, 0]; + const originTags = jointXml.getElementsByTagName("origin"); + if (originTags.length === 1) { + xyz = xyzFromString(originTags[0]) || xyz; + rpy = rpyFromString(originTags[0]) || rpy; + } + + // Default axis + let axis: [number, number, number] = [0, 0, 1]; + const axisTags = jointXml.getElementsByTagName("axis"); + if (axisTags.length === 1) { + axis = xyzFromString(axisTags[0]) || axis; + } + + // Optional limit + let limit; + const limitTags = jointXml.getElementsByTagName("limit"); + if (limitTags.length === 1) { + const lim = limitTags[0]; + limit = { + lower: parseFloat(lim.getAttribute("lower") || "0"), + upper: parseFloat(lim.getAttribute("upper") || "0"), + effort: parseFloat(lim.getAttribute("effort") || "0"), + velocity: parseFloat(lim.getAttribute("velocity") || "0") + }; + } + + joints.push({ + name: jointXml.getAttribute("name") || undefined, + type: jointXml.getAttribute("type") as + | "revolute" + | "continuous" + | "prismatic" + | "fixed" + | "floating" + | "planar", + origin_xyz: xyz, + origin_rpy: rpy, + axis_xyz: axis, + rotation: [0, 0, 0], + parent: parentLink, + child: childLink, + limit: limit, + elem: jointXml + }); + } + } + + /** + * If you ever want to re‐serialize the robot back to URDF XML, + * this method returns the stringified root element. + * + * @returns A string beginning with '' followed by the current XML. + */ + getURDFXML(): string { + return this.robot.elem ? '\n' + this.robot.elem.outerHTML : ""; + } +} + +/** + * ============================================================================== + * Example of how the parsed data (IUrdfRobot) maps from the URDF XML ("so_arm100"): + * + * { + * // The name attribute + * name: "so_arm100", + * + * // Materials/colors parsed from tags + * colors: { + * "green": [0.06, 0.4, 0.1, 1.0], + * "black": [0.1, 0.1, 0.1, 1.0] + * }, + * + * // Each under becomes an entry in `links` + * links: { + * "Base": { + * name: "Base", + * + * // Array of visuals: each inside + * visual: [ + * { + * elem: // the Element object for Base, + * type: "mesh", + * geometry: { + * filename: "/robots/so_arm100/meshes/Base.stl", + * type: "stl", + * scale: [1, 1, 1] + * }, + * origin_xyz: [0, 0, 0], // default since no in visual + * origin_rpy: [0, 0, 0], // default since no in visual + * color_rgba: [0.06, 0.4, 0.1, 1.0] // matches + * }, + * { + * elem: // the second Element for Base, + * type: "mesh", + * geometry: { + * filename: "/robots/so_arm100/meshes/Base_Motor.stl", + * type: "stl", + * scale: [1, 1, 1] + * }, + * origin_xyz: [0, 0, 0], + * origin_rpy: [0, 0, 0], + * color_rgba: [0.1, 0.1, 0.1, 1.0] // matches + * } + * ], + * + * // Array of collisions: each inside + * collision: [ + * { + * elem: // the Element for Base, + * type: "mesh", + * geometry: { + * filename: "/robots/so_arm100/meshes/Base.stl", + * type: "stl", + * scale: [1, 1, 1] + * }, + * origin_xyz: [0, 0, 0], + * origin_rpy: [0, 0, 0] + * // no color for collisions + * } + * ] + * }, + * + * // ... other links (e.g. "Rotation_Pitch", "Upper_Arm", etc.) follow the same structure + * }, + * + * // Each under becomes an entry in `joints` array + * joints: [ + * { + * name: "Rotation", + * type: "revolute", + * origin_xyz: [0, -0.0452, 0.0165], + * origin_rpy: [1.57079, 0, 0], + * axis_xyz: [0, -1, 0], + * rotation: [0, 0, 0], // runtime placeholder + * parent: // reference to links["Base"], + * child: // reference to links["Rotation_Pitch"], + * limit: { + * lower: -2, + * upper: 2, + * effort: 35, + * velocity: 1 + * }, + * elem: // the Element object + * }, + * + * { + * name: "Pitch", + * type: "revolute", + * origin_xyz: [0, 0.1025, 0.0306], + * origin_rpy: [-1.8, 0, 0], + * axis_xyz: [1, 0, 0], + * rotation: [0, 0, 0], + * parent: // reference to links["Rotation_Pitch"], + * child: // reference to links["Upper_Arm"], + * limit: { + * lower: 0, + * upper: 3.5, + * effort: 35, + * velocity: 1 + * }, + * elem: // the Element object + * }, + * + * // ... additional joints ("Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw") follow similarly + * ] + * } + * + * ============================================================================== + */ diff --git a/src/lib/components/3d/elements/robot/URDF/utils/helper.ts b/src/lib/components/3d/elements/robot/URDF/utils/helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..12f30abddfb48d305f18c435f729cfe21a7d53e9 --- /dev/null +++ b/src/lib/components/3d/elements/robot/URDF/utils/helper.ts @@ -0,0 +1,53 @@ +export function xyzFromString(child: Element): [x: number, y: number, z: number] | undefined { + const arr = numberStringToArray(child, "xyz"); + if (!arr || arr.length != 3) { + return; + } + return arr as [x: number, y: number, z: number]; +} + +export function rpyFromString( + child: Element +): [roll: number, pitch: number, yaw: number] | undefined { + const arr = numberStringToArray(child, "rpy"); + if (!arr || arr.length != 3) { + return; + } + return arr as [roll: number, pitch: number, yaw: number]; +} + +export function rgbaFromString( + child: Element +): [r: number, g: number, b: number, a: number] | undefined { + const arr = numberStringToArray(child, "rgba"); + if (!arr || arr.length != 4) { + return; + } + return arr as [r: number, g: number, b: number, a: number]; +} + +export function numberStringToArray(child: Element, name: string = "xyz"): number[] | undefined { + // parse a list of values from a string + // (like "1.0 2.2 3.0" into an array like [1, 2.2, 3]) + // used in URDF for position, orientation an color values + if (child.hasAttribute(name)) { + const xyzStr = child.getAttribute(name)?.split(" "); + if (xyzStr) { + const arr = []; + for (const nr of xyzStr) { + arr.push(parseFloat(nr)); + } + return arr; + } + } +} + +export function radToEuler(rad: number): number { + return (rad * 180) / Math.PI; +} + +export function numberArrayToColor([r, g, b]: [number, number, number]): string { + const toHex = (n: number) => Math.round(n).toString(16).padStart(2, "0"); + // 0.06, 0.4, 0.1, 1 + return `#${toHex(r * 255)}${toHex(g * 255)}${toHex(b * 255)}`; +} diff --git a/src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte b/src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..53479190da1c4461bc271e1737748c298746b551 --- /dev/null +++ b/src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte @@ -0,0 +1,491 @@ + + + + + + + + Input Connection - Robot {robot.id} + + + Configure how this robot receives commands. Choose between direct hardware control or remote collaboration. + + + +
+
+ + {#if error} + + + Connection Error + + {error} + + + {/if} + + + {#if showUSBCalibration} + + +
+ + Hardware Calibration Required + + +
+
+ + + + + Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control. + + + + + +
+ {:else} + + + + +
+
+ + Current Input Source +
+ {#if robot.hasConsumer} + + {robot.consumer?.name || 'Connected'} + + {:else} + No Input Connected + {/if} +
+ {#if robot.hasConsumer} +
+ Status: {robot.consumer?.status.isConnected ? 'Connected' : 'Disconnected'} +
+ {/if} +
+
+ + + + + + + Local Hardware (USB) + + + Read physical robot movements in real-time + + + + {#if robot.hasConsumer && robot.consumer?.name === 'USB Consumer'} + +
+
+
+

Hardware Connected

+

Reading physical servo positions

+
+ +
+
+ {:else} + + + + {#if robot.hasConsumer} +

+ Disconnect current input to connect USB hardware +

+ {/if} + {/if} +
+
+ + + + +
+
+ + + Remote Collaboration (Rooms) + + + Receive commands from AI systems, remote users, or other software + +
+ +
+
+ + {#if robot.hasConsumer && robot.consumer?.name?.includes('Remote Consumer')} + +
+
+
+

Room Connected

+

Receiving remote commands

+
+ +
+
+ {:else} + +
+
+
+ +

Create New Room

+
+

+ Create a room where others can send commands to this robot +

+ +
+ + +
+
+
+ + +
+
+ Join Existing Room: + + {robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available + +
+ +
+ {#if robotManager.rooms.length === 0} +
+ {robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'} +
+ {:else} + {#each robotManager.rooms as room} +
+
+
+

+ {room.id} +

+
+ {room.has_producer ? '📤 Has Output' : '📥 No Output'} + 👥 {room.participants?.total || 0} users +
+
+ +
+
+ {/each} + {/if} +
+
+ + {#if robot.hasConsumer} +

+ Disconnect current input to join a room +

+ {/if} + {/if} +
+
+ + + + + Input Sources + + USB: Read physical movements • Remote: Receive network commands • Only one active at a time + + + {/if} +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte b/src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48c4614491ccc7ddea13e62aee2a53f115bddc32 --- /dev/null +++ b/src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte @@ -0,0 +1,163 @@ + + + + + + +
+
+ +
+ Manual Control +

Direct robot joint manipulation

+
+
+
+
+ + {#if robot} + +
+
+ + {#if robot.isManualControlEnabled} +
+
+ +

Joint Controls

+ + {robot.jointArray.length} + +
+ +

+ Each joint can be moved independently using sliders. Values are normalized percentages. +

+ + {#if robot.jointArray.length === 0} +

No joints available

+ {:else} +
+ {#each robot.jointArray as joint (joint.name)} + {@const isGripper = joint.name.toLowerCase() === 'jaw' || joint.name.toLowerCase() === 'gripper'} + {@const minValue = isGripper ? 0 : -100} + {@const maxValue = isGripper ? 100 : 100} + +
+
+ {joint.name} +
+ + {joint.value.toFixed(1)}{isGripper ? '%' : '%'} + + {#if joint.limits} + + ({joint.limits.lower.toFixed(1)}° to {joint.limits.upper.toFixed(1)}°) + + {/if} +
+
+
+ { + const val = parseFloat((e.target as HTMLInputElement).value); + robot.updateJoint(joint.name, val); + }} + class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-600" + /> +
+ {minValue}{isGripper ? '% (closed)' : '%'} + {maxValue}{isGripper ? '% (open)' : '%'} +
+
+
+ {/each} +
+ {/if} +
+ {:else} +
+
+

Input Control Active

+
+ + + Input Control Active + + Robot controlled by: {robot.consumer?.name || 'External Input'}
+ Disconnect input to enable manual control. +
+
+
+ {/if} +
+
+ {/if} +
+
+ + diff --git a/src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte b/src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b6e99da04552512c712afb0754c21957246764d5 --- /dev/null +++ b/src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte @@ -0,0 +1,481 @@ + + + + + + + + Output Connection - Robot {robot.id} + + + Configure where this robot sends its movements. Multiple outputs can be active simultaneously. + + + +
+
+ + {#if error} + + + Connection Error + + {error} + + + {/if} + + + {#if showUSBCalibration} + + +
+ + Hardware Calibration Required + + +
+
+ + + + + Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control. + + + + + +
+ {:else} + + + + +
+
+ + Active Outputs +
+ + {outputDriverCount} Connected + +
+
+
+ + + + + + + Local Hardware (USB) + + + Send commands directly to physical robot hardware + + + + + + + + + + +
+
+ + + Remote Collaboration (Rooms) + + + Broadcast robot movements to remote systems and AI + +
+ +
+
+ + +
+
+
+ +

Create New Room

+
+

+ Create a room to broadcast this robot's movements +

+ +
+ + +
+
+
+ + +
+
+ Join Existing Room: + + {robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available + +
+ +
+ {#if robotManager.rooms.length === 0} +
+ {robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'} +
+ {:else} + {#each robotManager.rooms as room} +
+
+
+

+ {room.id} +

+
+ {room.has_producer ? '🔴 Occupied' : '🟢 Available'} + 👥 {room.participants?.total || 0} users +
+
+ {#if !room.has_producer} + + {:else} + + {/if} +
+
+ {/each} + {/if} +
+
+
+
+ + + {#if producers.length > 0} + + + + + Connected Outputs + + + +
+ {#each producers as producer} +
+
+ + {producer.name} + {producer.id.slice(0, 12)} +
+ +
+ {/each} +
+
+
+ {/if} + + + + + Output Sources + + USB: Control physical hardware • Remote: Broadcast to network • Multiple outputs can be active + + + {/if} +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/robot/status/ConnectionFlowBoxUIkit.svelte b/src/lib/components/3d/elements/robot/status/ConnectionFlowBoxUIkit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ac5015318e5827eec3438fd45f7513d2ba5d530a --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/ConnectionFlowBoxUIkit.svelte @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + 0 ? 1 : 0.5} + /> + + + + diff --git a/src/lib/components/3d/elements/robot/status/InputBoxUIKit.svelte b/src/lib/components/3d/elements/robot/status/InputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..15000e042ac6aa773fc8c9806499e57953bdc258 --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/InputBoxUIKit.svelte @@ -0,0 +1,104 @@ + + + + + + {#if robot.hasConsumer} + + + + + + {#if robot.consumer?.constructor.name} + + {:else} + + {/if} + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/robot/status/ManualControlBoxUIKit.svelte b/src/lib/components/3d/elements/robot/status/ManualControlBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8a3271c07ab975a4e210cda560f1c02b9e351aa6 --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/ManualControlBoxUIKit.svelte @@ -0,0 +1,85 @@ + + + + {#if isDisabled} + + + + + {:else if robot.isManualControlEnabled} + + + + + {:else} + + + + + + + {/if} + diff --git a/src/lib/components/3d/elements/robot/status/OutputBoxUIKit.svelte b/src/lib/components/3d/elements/robot/status/OutputBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..278b06adda45eb6cb7e46733c9c4ab03330784c5 --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/OutputBoxUIKit.svelte @@ -0,0 +1,100 @@ + + + + 0 ? 0.8 : 0.4} + backgroundOpacity={robot.outputDriverCount > 0 ? 0.3 : 0.15} + opacity={robot.outputDriverCount > 0 ? 1 : 0.7} + onclick={handleClick} +> + {#if robot.outputDriverCount > 0} + + + + + + + {#if robot.producers.length > 0} + + + {#snippet children()} + {#each robot.producers.slice(0, 2) as producer} + + {/each} + + {#if robot.producers.length > 2} + + {/if} + {/snippet} + + {/if} + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/robot/status/RobotBoxUIKit.svelte b/src/lib/components/3d/elements/robot/status/RobotBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5b9da1961c4e955378577f86f1116f1c00535317 --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/RobotBoxUIKit.svelte @@ -0,0 +1,52 @@ + + + + onRobotBoxClick(robot)} +> + + + + + + + + + diff --git a/src/lib/components/3d/elements/robot/status/RobotStatusBillboard.svelte b/src/lib/components/3d/elements/robot/status/RobotStatusBillboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a67a1e7de3350cc14405a45b1650cf4330431833 --- /dev/null +++ b/src/lib/components/3d/elements/robot/status/RobotStatusBillboard.svelte @@ -0,0 +1,84 @@ + + +{#if visible} + + + + + + + + + + + +{/if} diff --git a/src/lib/components/3d/elements/video/Video.svelte b/src/lib/components/3d/elements/video/Video.svelte new file mode 100644 index 0000000000000000000000000000000000000000..12e2429168e4d0a288f0fb87323f050bf4342901 --- /dev/null +++ b/src/lib/components/3d/elements/video/Video.svelte @@ -0,0 +1,451 @@ + + + (isPlayingLocked = !isPlayingLocked)} + onpointerenter={() => (isHovered = true)} + onpointerleave={() => (isHovered = false)} +> + + + + + + + {#if videoLoaded && videoTexture} + + {:else if loadingTexture} + + {:else} + + {/if} + + + + {#if showLoading && (!isHovered || isLoading)} + + {/if} + + + + diff --git a/src/lib/components/3d/elements/video/VideoGridItem.svelte b/src/lib/components/3d/elements/video/VideoGridItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..344f81b1cabcd568694943204b2cc23ff3fb4298 --- /dev/null +++ b/src/lib/components/3d/elements/video/VideoGridItem.svelte @@ -0,0 +1,78 @@ + + + + ) => { + event.stopPropagation(); + onPointerEnter(); + hovering = true; + }} + onpointerover={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerEnter(); + hovering = true; + }} + onpointerout={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + hovering = false; + }} + onpointerleave={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + hovering = false; + }} + onclick={handleStatusToggle} + > + + + + + + + diff --git a/src/lib/components/3d/elements/video/Videos.svelte b/src/lib/components/3d/elements/video/Videos.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c37bfa3f356cbb5ec89da3fdc27a83c89d6a0640 --- /dev/null +++ b/src/lib/components/3d/elements/video/Videos.svelte @@ -0,0 +1,46 @@ + + +{#each videoManager.videos as video (video.id)} + {}} + onInputBoxClick={onInputBoxClick} + onOutputBoxClick={onOutputBoxClick} + /> +{/each} + + +{#if selectedVideo} + + +{/if} diff --git a/src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte b/src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b8828120d958e1aee8942b71855f1582f4124cc6 --- /dev/null +++ b/src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte @@ -0,0 +1,485 @@ + + + + + + + + Video Input - {video?.name || 'No Video Selected'} + + + Configure video input source: local camera for recording or remote streams from rooms + + + +
+
+ + {#if error} + + + Connection Error + + {error} + + + {/if} + + + +
+
+ + Current Video Input +
+ {#if video?.hasInput} + + {video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'} + + {:else} + No Input Connected + {/if} +
+ {#if video?.hasInput} +
+ {#if video.input.roomId} + Room: {video.input.roomId} + {:else} + Source: Local Device Camera + {/if} +
+ {/if} +
+
+ + + {#if video?.hasInput} + + + + + Current Input + + + +
+
+
+

+ {video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'} +

+ {#if video.input.roomId} +

+ Room: {video.input.roomId} +

+ {/if} + {#if video.input.stream} +

+ Video: {video.input.stream.getVideoTracks().length} tracks +

+

+ Audio: {video.input.stream.getAudioTracks().length} tracks +

+ {/if} +
+ +
+
+
+
+ {/if} + + + + + + + Local Camera + + + Use your device camera for direct video capture and recording + + + + {#if video?.hasInput && video.input.type === 'local-camera'} + +
+
+
+

Camera Connected

+

Local device camera active

+
+ +
+
+ {:else} + + + + {#if video?.hasInput} +

+ Disconnect current input to connect camera +

+ {/if} + {/if} +
+
+ + + + +
+
+ + + Remote Collaboration (Rooms) + + + Receive video streams from remote cameras or AI systems + +
+ +
+
+ + {#if video?.hasInput && video.input.type !== 'local-camera'} + +
+
+
+

Room Connected

+

Receiving remote video stream

+
+ +
+
+ {:else} + +
+
+
+ +

Create New Room

+
+

+ Create a room to receive video from others +

+ +
+ + +
+
+
+ + +
+
+ Join Existing Room: + + {videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available + +
+ +
+ {#if videoManager.rooms.length === 0} +
+ {videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'} +
+ {:else} + {#each videoManager.rooms as room} +
+
+
+

+ {room.id} +

+
+ {room.participants?.producer ? '📹 Has Output' : '📭 No Output'} + 👥 {room.participants?.consumers?.length || 0} inputs +
+
+ {#if room.participants?.producer} + + {:else} + + {/if} +
+
+ {/each} + {/if} +
+
+ + {#if video?.hasInput} +

+ Disconnect current input to join a room +

+ {/if} + {/if} +
+
+ + + + + Video Input Sources + + Camera: Local device camera • Remote: Video streams from rooms • Only one active at a time + + +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte b/src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e1272e9b8d8742e2b5adad334776cb952715dfd3 --- /dev/null +++ b/src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte @@ -0,0 +1,432 @@ + + + + + + + + Video Output - {video?.name || 'No Video Selected'} + + + Broadcast your local camera to remote viewers in rooms + + + +
+
+ + {#if error} + + + Broadcasting Error + + {error} + + + {/if} + + + +
+
+ + Current Video Output +
+ {#if video?.hasOutput} + + Broadcasting + + {:else} + Not Broadcasting + {/if} +
+ {#if video?.hasOutput} +
+ {#if video.output.roomId} + Room: {video.output.roomId} + {:else} + Broadcasting to server + {/if} +
+ {/if} +
+
+ + + {#if !video?.canOutput} + + + + + Requirements + + + +
+ {#if !video?.hasInput} +
+ + No video input connected +
+ {:else if video.input.type !== 'local-camera'} +
+ + Input must be local camera (cannot re-broadcast remote streams) +
+ {/if} +
+ To enable output, first connect to your local camera in the Video Input modal. +
+
+
+
+ {/if} + + + {#if video?.hasOutput} + + + + + Broadcasting + + + +
+
+
+

+ Streaming to Server +

+ {#if video.output.roomId} +

+ Room ID: {video.output.roomId} +

+ {/if} +

+ Source: Local Camera +

+ {#if video.input.stream} +

+ Video: {video.input.stream.getVideoTracks().length} tracks +

+

+ Audio: {video.input.stream.getAudioTracks().length} tracks +

+ {/if} +
+ +
+
+
+
+ {/if} + + + + +
+
+ + + Remote Broadcasting (Rooms) + + + Broadcast your camera feed to remote viewers in rooms + +
+ +
+
+ + {#if video?.hasOutput} + +
+
+
+

Broadcasting Active

+

Sending video to remote viewers

+
+ +
+
+ {:else if video?.canOutput} + +
+
+
+ +

Create New Room

+
+

+ Create a room to broadcast your camera feed +

+ +
+ + +
+
+
+ + +
+
+ Join Existing Room: + + {videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available + +
+ +
+ {#if videoManager.rooms.length === 0} +
+ {videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'} +
+ {:else} + {#each videoManager.rooms as room} +
+
+
+

+ {room.id} +

+
+ {room.participants?.producer ? '🔴 Occupied' : '🟢 Available'} + 👥 {room.participants?.consumers?.length || 0} viewers +
+
+ {#if !room.participants?.producer} + + {:else} + + {/if} +
+
+ {/each} + {/if} +
+
+ + {#if video?.hasOutput} +

+ Stop current broadcast to join a different room +

+ {/if} + {:else} + +
+
+ Connect to local camera first to enable broadcasting +
+
+ {/if} +
+
+ + + + + Video Broadcasting + + Requirements: Local camera input only • Remote streams: Cannot be re-broadcasted • Only one output per room + + +
+
+
+
\ No newline at end of file diff --git a/src/lib/components/3d/elements/video/status/InputVideoBoxUIKit.svelte b/src/lib/components/3d/elements/video/status/InputVideoBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0a590d64ef54c7f0e85cc22cecf15302522323a9 --- /dev/null +++ b/src/lib/components/3d/elements/video/status/InputVideoBoxUIKit.svelte @@ -0,0 +1,81 @@ + + + + + + {#if video.hasInput} + + {#if video.input.type === 'local-camera'} + + {:else} + + {/if} + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/video/status/OutputVideoBoxUIKit.svelte b/src/lib/components/3d/elements/video/status/OutputVideoBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1cdec9765f25941f60aebc03c68dee0cb9f75328 --- /dev/null +++ b/src/lib/components/3d/elements/video/status/OutputVideoBoxUIKit.svelte @@ -0,0 +1,75 @@ + + + + + + {#if video.hasOutput} + + + + + + + + {:else} + + + + + + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/elements/video/status/VideoBoxUIKit.svelte b/src/lib/components/3d/elements/video/status/VideoBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0c41cf7a1bef8d012fb7edc487d9fd2996e45746 --- /dev/null +++ b/src/lib/components/3d/elements/video/status/VideoBoxUIKit.svelte @@ -0,0 +1,49 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/3d/elements/video/status/VideoConnectionFlowBoxUIKit.svelte b/src/lib/components/3d/elements/video/status/VideoConnectionFlowBoxUIKit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..abfca99e1c6adda40b46c517df183794a5dc8b94 --- /dev/null +++ b/src/lib/components/3d/elements/video/status/VideoConnectionFlowBoxUIKit.svelte @@ -0,0 +1,43 @@ + + + + + onInputBoxClick(video)} /> + + + + + + + + + + + + onOutputBoxClick(video)} /> + \ No newline at end of file diff --git a/src/lib/components/3d/elements/video/status/VideoStatusBillboard.svelte b/src/lib/components/3d/elements/video/status/VideoStatusBillboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..052793d9ca9eac02e0c420e4eabf50116068fc0f --- /dev/null +++ b/src/lib/components/3d/elements/video/status/VideoStatusBillboard.svelte @@ -0,0 +1,76 @@ + + +{#if visible} + e.stopPropagation()} + onpointerup={(e) => e.stopPropagation()} + onpointermove={(e) => e.stopPropagation()} + onclick={(e) => e.stopPropagation()} + position.z={0.22} + padding={10} + rotation={[Math.PI / 2, 0, 0]} + scale={[0.12, 0.12, 0.12]} + pointerEvents="listener" + > + + + + + + + + + + +{/if} diff --git a/src/lib/components/3d/misc/Pointcloud.svelte b/src/lib/components/3d/misc/Pointcloud.svelte new file mode 100644 index 0000000000000000000000000000000000000000..db7b82e8fb04b51ebe3ac0dac7ab8b840600cc80 --- /dev/null +++ b/src/lib/components/3d/misc/Pointcloud.svelte @@ -0,0 +1,337 @@ + + +{#if pointCloudGeometry} + e.stopPropagation()} + onpointerleave={(e) => e.stopPropagation()} + onpointerdown={(e) => e.stopPropagation()} + onpointerup={(e) => e.stopPropagation()} + onpointermove={(e) => e.stopPropagation()} + onclick={(e) => e.stopPropagation()} + > + + + + + + + + e.stopPropagation()} + onpointerleave={(e) => e.stopPropagation()} + onpointerdown={(e) => e.stopPropagation()} + onpointerup={(e) => e.stopPropagation()} + onpointermove={(e) => e.stopPropagation()} + onclick={(e) => e.stopPropagation()} + > + + + +{/if} diff --git a/src/lib/components/3d/ui/BaseStatusBox.svelte b/src/lib/components/3d/ui/BaseStatusBox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..36313d480759a3eddabdd5659dc439b2de77ca1c --- /dev/null +++ b/src/lib/components/3d/ui/BaseStatusBox.svelte @@ -0,0 +1,91 @@ + + + + + + + {@render children()} + + \ No newline at end of file diff --git a/src/lib/components/3d/ui/StatusArrow.svelte b/src/lib/components/3d/ui/StatusArrow.svelte new file mode 100644 index 0000000000000000000000000000000000000000..aed0e7df87142054d4b434435404c5fdeed99384 --- /dev/null +++ b/src/lib/components/3d/ui/StatusArrow.svelte @@ -0,0 +1,71 @@ + + + + + + {#if direction === 'right'} + + {:else if direction === 'down'} + + {:else if direction === 'left'} + + {:else if direction === 'up'} + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/ui/StatusButton.svelte b/src/lib/components/3d/ui/StatusButton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f9ee407e7929f9f9098b44a4ed3bd19be7b3653c --- /dev/null +++ b/src/lib/components/3d/ui/StatusButton.svelte @@ -0,0 +1,73 @@ + + + + + + {#if icon} + + {/if} + + + \ No newline at end of file diff --git a/src/lib/components/3d/ui/StatusContent.svelte b/src/lib/components/3d/ui/StatusContent.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a96cf0418660568a1f29a0cb295baa349cd6ebeb --- /dev/null +++ b/src/lib/components/3d/ui/StatusContent.svelte @@ -0,0 +1,105 @@ + + + + + + {#if children} + {@render children()} + {:else} + + {#if title} + + {/if} + + {#if subtitle} + + {/if} + + {#if description} + + {/if} + + {/if} + \ No newline at end of file diff --git a/src/lib/components/3d/ui/StatusHeader.svelte b/src/lib/components/3d/ui/StatusHeader.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d24f5e24213061f0a7e7686f5b4a1083befabcf3 --- /dev/null +++ b/src/lib/components/3d/ui/StatusHeader.svelte @@ -0,0 +1,50 @@ + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/3d/ui/StatusIndicator.svelte b/src/lib/components/3d/ui/StatusIndicator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..38e868be64b9529a8d04a8ea9e44c5d4e2d0c004 --- /dev/null +++ b/src/lib/components/3d/ui/StatusIndicator.svelte @@ -0,0 +1,26 @@ + + +{#if visible} + +{/if} \ No newline at end of file diff --git a/src/lib/components/3d/ui/icons.ts b/src/lib/components/3d/ui/icons.ts new file mode 100644 index 0000000000000000000000000000000000000000..73d2f8b1ae92666f8907278663a3c9bf6fb8da1c --- /dev/null +++ b/src/lib/components/3d/ui/icons.ts @@ -0,0 +1,7 @@ +// Common generic SVG icons encoded as base64 data URLs +export const icons = { + plus: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=", + settings: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJtOS4yNSAyMmwtLjQtMy4ycS0uMzI1LS4xMjUtLjYxMi0uM3QtLjU2My0uMzc1TDQuNyAxOS4zNzVsLTIuNzUtNC43NWwyLjU3NS0xLjk1UTQuNSAxMi41IDQuNSAxMi4zMzh2LS42NzVxMC0uMTYzLjAyNS0uMzM4TDEuOTUgOS4zNzVsMi43NS00Ljc1bDIuOTc5IDEuMjVxLjI3NS0uMi41NzUtLjM3NXQuNi0uM2wuNC0zLjJoNS41bC40IDMuMnEuMzI1LjEyNS42MTMuM3QuNTYyLjM3NWwyLjk3NS0xLjI1bDIuNzUgNC43NWwtMi41NzUgMS45NXEuMDI1LjE3NS4wMjUuMzM4di42NzRxMCAuMTYzLS4wNS4zMzhsMi41NzUgMS45NWwtMi43NSA0Ljc1bC0yLjk1LTEuMjVxLS4yNzUuMi0uNTc1LjM3NXQtLjYuM2wtLjQgMy4yem0yLjgtNi41cTEuNDUgMCAyLjQ3NS0xLjAyNVQxNS41NSAxMnQtMS4wMjUtMi40NzVUMTIuMDUgOC41cS0xLjQ3NSAwLTIuNDg4IDEuMDI1VDguNTUgMTJ0MS4wMTMgMi40NzVUMTIuMDUgMTUuNSIvPjwvc3ZnPg==" +} as const; + +export type IconName = keyof typeof icons; \ No newline at end of file diff --git a/src/lib/components/3d/ui/index.ts b/src/lib/components/3d/ui/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6010886296255dd1dbc77ac0fe6be62b6ea87afa --- /dev/null +++ b/src/lib/components/3d/ui/index.ts @@ -0,0 +1,6 @@ +export { default as BaseStatusBox } from './BaseStatusBox.svelte'; +export { default as StatusHeader } from './StatusHeader.svelte'; +export { default as StatusContent } from './StatusContent.svelte'; +export { default as StatusIndicator } from './StatusIndicator.svelte'; +export { default as StatusButton } from './StatusButton.svelte'; +export { default as StatusArrow } from './StatusArrow.svelte'; \ No newline at end of file diff --git a/src/lib/components/3d/utils/Hoverable.old.svelte b/src/lib/components/3d/utils/Hoverable.old.svelte new file mode 100644 index 0000000000000000000000000000000000000000..eee0eb20c0b6c7431dafa9a4cbf61d82f87874af --- /dev/null +++ b/src/lib/components/3d/utils/Hoverable.old.svelte @@ -0,0 +1,148 @@ + + +) => { + event.stopPropagation(); + isSelected = true; + onClickObject?.(); + }} + onpointerenter={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerEnter(); + isHovered = true; + }} + onpointerleave={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + isHovered = false; + }} + scale={scale.current} +> + {#snippet children({ ref })} + {@render content({ isHovered, isSelected, offset: offsetTween.current })} + {/snippet} + diff --git a/src/lib/components/3d/utils/Hoverable.svelte b/src/lib/components/3d/utils/Hoverable.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ce929e5053bbd79acfa9c1556a72977919694dbe --- /dev/null +++ b/src/lib/components/3d/utils/Hoverable.svelte @@ -0,0 +1,108 @@ + + +) => { + event.stopPropagation(); + isSelected = true; + onClickObject?.(); + }} + onpointerenter={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerEnter(); + isHovered = true; + }} + onpointerleave={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + isHovered = false; + }} + scale={scale.current} +> + {#snippet children({ ref })} + {@render content({ isHovered, isSelected, debouncedIsHovered })} + {/snippet} + diff --git a/src/lib/components/interface/overlay/AddAIButton.svelte b/src/lib/components/interface/overlay/AddAIButton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..444894980e2289a82813f825aa5cedf8c9762467 --- /dev/null +++ b/src/lib/components/interface/overlay/AddAIButton.svelte @@ -0,0 +1,153 @@ + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + AI Types + + + {#each aiOptions as ai} + await addAI(ai.id)} + disabled={!ai.enabled} + > + +
+ {formatAIType(ai.id)} + + {getAIDescription(ai.id)} + +
+
+ {/each} +
+
+
\ No newline at end of file diff --git a/src/lib/components/interface/overlay/AddRobotButton.svelte b/src/lib/components/interface/overlay/AddRobotButton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d65d92ca4754ed1d553ba890794b88afc3d34979 --- /dev/null +++ b/src/lib/components/interface/overlay/AddRobotButton.svelte @@ -0,0 +1,138 @@ + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Robot Types + + + {#each robotTypes as robotType} + await addRobot(robotType)} + > + +
+ {robotType.replace(/-/g, " ").toUpperCase()} + + {robotType === "so-arm100" ? "6-DOF Robotic Arm" : "Industrial Robot"} + +
+ {#if robotType === "so-arm100"} + + Default + + {/if} +
+ {/each} +
+
+
diff --git a/src/lib/components/interface/overlay/AddSensorButton.svelte b/src/lib/components/interface/overlay/AddSensorButton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..12757f312aa25e4d1c7e2054dc49a33dd3792224 --- /dev/null +++ b/src/lib/components/interface/overlay/AddSensorButton.svelte @@ -0,0 +1,170 @@ + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + Sensor Types + + + {#each sensorConfigs as sensor} + await addSensor(sensor.id)} + > + +
+ {sensor.label} + + {sensor.description} + +
+ {#if sensor.isDefault} + + Default + + {/if} +
+ {/each} +
+
+
\ No newline at end of file diff --git a/src/lib/components/interface/overlay/Overlay.svelte b/src/lib/components/interface/overlay/Overlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cbe320360a1f689f117c74c208b80b7bd14bb94f --- /dev/null +++ b/src/lib/components/interface/overlay/Overlay.svelte @@ -0,0 +1,61 @@ + + +
+ +
+ +
+ +
+ + Logo +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+
+ +
+ + +
+
+ diff --git a/src/lib/components/interface/overlay/SettingsButton.svelte b/src/lib/components/interface/overlay/SettingsButton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5c48dced1b24eb2fd71116f9be834474088e323f --- /dev/null +++ b/src/lib/components/interface/overlay/SettingsButton.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/interface/overlay/SettingsSheet.svelte b/src/lib/components/interface/overlay/SettingsSheet.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8be86c64a05643a8e4610e00f1cb2422026ff573 --- /dev/null +++ b/src/lib/components/interface/overlay/SettingsSheet.svelte @@ -0,0 +1,401 @@ + + + + + + + +
+
+ +
+ Robot Settings +

Configure application preferences

+
+
+
+
+ + +
+
+ +
+
+ +

Server Configuration

+
+ +
+
+
+ +

+ URL for the remote AI inference server that runs ACT models and manages robot sessions +

+
+
+ + +
+
+ {#if inferenceConnectionStatus === 'connected'} + + Connected to inference server + {:else if inferenceConnectionStatus === 'disconnected'} + + Cannot connect to inference server + {:else} + + Connection status unknown + {/if} +
+
+ +
+
+ +

+ URL for the transport server that manages communication rooms and routes video streams and robot data using consumer/producer system +

+
+
+ + +
+
+ {#if transportConnectionStatus === 'connected'} + + Connected to transport server + {:else if transportConnectionStatus === 'disconnected'} + + Cannot connect to transport server + {:else} + + Connection status unknown + {/if} +
+
+
+
+ + + + +
+
+ +

Application Settings

+
+ + +
+ + + + + +
+
+
+
+ + diff --git a/src/lib/components/ui/accordion/accordion-content.svelte b/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ecc695e39d6fa6c2264371aec2782866d71cbf72 --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,25 @@ + + + +
+ {@render children?.()} +
+
diff --git a/src/lib/components/ui/accordion/accordion-item.svelte b/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..780545c6afbaec7ad253b431f98c3308dd5d007f --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/accordion/accordion-root.svelte b/src/lib/components/ui/accordion/accordion-root.svelte new file mode 100644 index 0000000000000000000000000000000000000000..117ee37f22128832cc30d89cbe00c30b1906ea5f --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-root.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/accordion/accordion-trigger.svelte b/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c01efd5ab358602ca653508399207e2b3b5dd349 --- /dev/null +++ b/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/src/lib/components/ui/accordion/index.ts b/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3596ca1c67138a0b6813ebef49041ad7ca90420b --- /dev/null +++ b/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from "./accordion-root.svelte"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger +}; diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000000000000000000000000000000000000..32d1ef0fb2c50fae661800aec822624e529ab769 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7f9e2c8ce0b554179efd2103d341e7204f7adbb7 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..645464267888f1b48618052f4e87778cb916091b --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,27 @@ + + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2ec67dc2ed710e9ba7d38378e394eb9ce5448cc5 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f78b97abaaf9bccafdbf6b269518f627512b8cc8 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c8fa7625f5afdc4cb7920757dc55f250c27c96f0 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a64ee768561f75e4229a0ecc1aaa6e1942bdffa6 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7ef2b5fbed8a88a9dcbab4239c626472a8edd376 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b22d1d50b5ceb119690ecce47726fd9eedbf1eca --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/index.ts b/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..88bc4e710def25ac5c6ff646d7f86b32630cabcb --- /dev/null +++ b/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,39 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; +import Trigger from "./alert-dialog-trigger.svelte"; +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Portal = AlertDialogPrimitive.Portal; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription +}; diff --git a/src/lib/components/ui/alert/alert-description.svelte b/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8b56aed2f110b32c9a88ffb2033dea0d901be52a --- /dev/null +++ b/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/alert/alert-title.svelte b/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000000000000000000000000000000000000..77e45ad5c1c0e93b2b837716c6f6fff60988ecd3 --- /dev/null +++ b/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/alert/alert.svelte b/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bcc870947728e1940a07da4c7cd19eeb346ea644 --- /dev/null +++ b/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/src/lib/components/ui/alert/index.ts b/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5761bf02debdfeed5716d6e6a2df29b69d3d4a8a --- /dev/null +++ b/src/lib/components/ui/alert/index.ts @@ -0,0 +1,14 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle +}; diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c30fc2f5cd02d9157e1d9d83158ceee94a56b4b5 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..64e0aa9b0805f5f62c05ccb321a941ba8b0193af --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4da437d0ce80bef3fcca587980437d29ad92b3a6 --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,80 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..068bfa262ca4e92a4b7c10d646d63ed5dbaa97ef --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant +}; diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cc36c5665bed45a7b4f94bc7291e3c818d7f476e --- /dev/null +++ b/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bc90b8371fa36942672081c8abd6f64cfcc2bd04 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9b20ac701fa2e242686b71035724ee7a94dfce6c --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2d4d0f24a99fb21e500aa1264363397a3e29cbce --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..25017884e600426d7a4f6ca877d9de96eb1898eb --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000000000000000000000000000000000000..744723117c6be5f148f4d94df3ce9ac9601c3176 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000000000000000000000000000000000000..99448cc9a8ccb4fe9768c8643e06cb40bc56d09d --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..10daffb314f22ff8859dc21507f97c0c01669606 --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction +}; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0a2b0101c03de242c8a7e17c96d4461ea5a07443 --- /dev/null +++ b/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet} +
diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fba5a4d007a58377d64057399be2c8406799dcd --- /dev/null +++ b/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox +}; diff --git a/src/lib/components/ui/collapsible/collapsible-content.svelte b/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bdabb559c944d8d51059928b269d84d95180847a --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ece7ad68ec1321c462b94abb50a0076184e427c3 --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible.svelte b/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1238eff6bf2f5cd6610d786f47c44597cb5f89c0 --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/ui/collapsible/index.ts b/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..21f608b850c15cf82c46622da4a7d158609a032d --- /dev/null +++ b/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,15 @@ +import { Collapsible as CollapsiblePrimitive } from "bits-ui"; + +const Root = CollapsiblePrimitive.Root; +const Trigger = CollapsiblePrimitive.Trigger; +const Content = CollapsiblePrimitive.Content; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger +}; diff --git a/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1f45dd9233f1b2be8c1cb3b8b6d9efaf1c460e5c --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/context-menu/context-menu-content.svelte b/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..60321f502ec8cb6ef4863618810cdb78fce98bd3 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-group-heading.svelte b/src/lib/components/ui/context-menu/context-menu-group-heading.svelte new file mode 100644 index 0000000000000000000000000000000000000000..66a81b36ec580b293d388dd207631fc1479624f9 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-group-heading.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-group.svelte b/src/lib/components/ui/context-menu/context-menu-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c7c1e06add7eb43742e53fba4cfe2f0fdde3f5c4 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-item.svelte b/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2e506a4c76f42eb4fd53cfe759dbf909e1397cfc --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-label.svelte b/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7d050374a8954b6869676649a03761b63c144a6e --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..964cb55fdc8d8db61d27c29e4dffebd1cd898dbd --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ca8b49a67772a8cc65e6cc7145896d8e3608306d --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/context-menu/context-menu-separator.svelte b/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7f5b237b9fa65cf9b69ee3a6aaabb7e5db23a7d5 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7eca8e08be5106323bdfd7d6a7304f2a46988f17 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c10e0770f07a359f7f5ecae7e4469af7b69dccff --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c19db93201fdd798b1c20de2365bd8b089235db8 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/context-menu/context-menu-trigger.svelte b/src/lib/components/ui/context-menu/context-menu-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3efa8575f7e5aca9e4f1bc36e3262f929dcad3a8 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/context-menu/index.ts b/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..24593724bcc67e02080dc14fe7d6f1c16e7b7887 --- /dev/null +++ b/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,51 @@ +import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; + +import Trigger from "./context-menu-trigger.svelte"; +import Group from "./context-menu-group.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import Item from "./context-menu-item.svelte"; +import GroupHeading from "./context-menu-group-heading.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; +import Label from "./context-menu-label.svelte"; +const Sub = ContextMenuPrimitive.Sub; +const Root = ContextMenuPrimitive.Root; + +export { + Sub, + Root, + Item, + GroupHeading, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Item as ContextMenuItem, + GroupHeading as ContextMenuGroupHeading, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, + Label as ContextMenuLabel +}; diff --git a/src/lib/components/ui/dialog/dialog-close.svelte b/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000000000000000000000000000000000000..840b2f68b1d8c178b4671d1bb9999a2d03a3a436 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6c003b52c9fc9131b8eb90d37f0a09862513226a --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,39 @@ + + + + + + {@render children?.()} + + + Close + + + diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000000000000000000000000000000000000..38450239aa4b984facbe43681b0ba2227c54ac97 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e7ff4468ecf13d491dc2704e10a436bcc47be4b1 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc90cd9bef10a0b45f5975bd481e33c64c58ad04 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f81ad8334ef6c0bbe9d97f5fb9c848801c8a7b07 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e4d4b344a39e82f5c41912ac4c4deb9d8767e27d --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-trigger.svelte b/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9d1e80110a7a9d214696306e32faf83de79611aa --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..790315c00e4d8171d20cd7131bcdf997b3d7b4c9 --- /dev/null +++ b/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +const Root = DialogPrimitive.Root; +const Portal = DialogPrimitive.Portal; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +}; diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cf786211b09e2657b6c0c2ad3636166d439e0a76 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..734d343c2e8e7396b4e321b637ec65a75f1646a5 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48d14a916a5a5a518c8726410b8b7b5a07079b19 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..aca1f7bd554e2269bcabb6475ea5362a947f5c90 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3f6159cfd67863fac724916293de48c777faf0fe --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f72e477e1ff4c65ea77125ff9a7634db5c307790 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..189aef40e44212dd3da08a692ae1a7d2f5066fe4 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..af8d5d79871f3f47685bc713f69116fa0c4fa36f --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..90f1b6f10aa78af6ebc54c624004c026097bae4a --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6974947788bd693eef6cbd3661fa3200e30b31b6 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0cb1062a489a5421fa7ebbf062c19c49d23b58cd --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..32ccfd505af4f0d83f18f2659b5a0143c6fcc59c --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cb053444d76caba0c74a634f8b12575cf07dc716 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..342f3145f107a7f87946be6a92fff7e0bf097bc5 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,49 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; + +export { + CheckboxItem, + Content, + Root as DropdownMenu, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger +}; diff --git a/src/lib/components/ui/hover-card/hover-card-content.svelte b/src/lib/components/ui/hover-card/hover-card-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..016bc971bd722d69b1f78d1497dd1823042fce87 --- /dev/null +++ b/src/lib/components/ui/hover-card/hover-card-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/components/ui/hover-card/hover-card-trigger.svelte b/src/lib/components/ui/hover-card/hover-card-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..322172bb496994c51ac30505a0c863da43a62b35 --- /dev/null +++ b/src/lib/components/ui/hover-card/hover-card-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/hover-card/index.ts b/src/lib/components/ui/hover-card/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..40807a2c18898d89641fd71b4710f08da429753f --- /dev/null +++ b/src/lib/components/ui/hover-card/index.ts @@ -0,0 +1,14 @@ +import { LinkPreview as HoverCardPrimitive } from "bits-ui"; +import Content from "./hover-card-content.svelte"; +import Trigger from "./hover-card-trigger.svelte"; + +const Root = HoverCardPrimitive.Root; + +export { + Root, + Content, + Trigger, + Root as HoverCard, + Content as HoverCardContent, + Trigger as HoverCardTrigger +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9ffe2824b461b431dfdb315c630f95cdb5149db --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000000000000000000000000000000000000..457185551ed5497245ec1d29c9c5a2f079840d27 --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,51 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/src/lib/components/ui/label/index.ts b/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c3128ce49ac2f15296d38e2449418d8de566326 --- /dev/null +++ b/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label +}; diff --git a/src/lib/components/ui/label/label.svelte b/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d71afbca294baec7c53fb3355abfec09144d340c --- /dev/null +++ b/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/menubar/index.ts b/src/lib/components/ui/menubar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..568f21b1649605693e833cb05a91c51965a2bdda --- /dev/null +++ b/src/lib/components/ui/menubar/index.ts @@ -0,0 +1,54 @@ +import { Menubar as MenubarPrimitive } from "bits-ui"; +import Root from "./menubar.svelte"; +import CheckboxItem from "./menubar-checkbox-item.svelte"; +import Content from "./menubar-content.svelte"; +import Item from "./menubar-item.svelte"; +import Group from "./menubar-group.svelte"; +import RadioItem from "./menubar-radio-item.svelte"; +import Separator from "./menubar-separator.svelte"; +import Shortcut from "./menubar-shortcut.svelte"; +import SubContent from "./menubar-sub-content.svelte"; +import SubTrigger from "./menubar-sub-trigger.svelte"; +import Trigger from "./menubar-trigger.svelte"; +import Label from "./menubar-label.svelte"; +import GroupHeading from "./menubar-group-heading.svelte"; + +const Menu = MenubarPrimitive.Menu; +const Sub = MenubarPrimitive.Sub; +const RadioGroup = MenubarPrimitive.RadioGroup; + +export { + Root, + CheckboxItem, + Content, + Item, + RadioItem, + Separator, + Shortcut, + SubContent, + SubTrigger, + Trigger, + Menu, + Group, + Sub, + RadioGroup, + Label, + GroupHeading, + // + Root as Menubar, + CheckboxItem as MenubarCheckboxItem, + Content as MenubarContent, + Item as MenubarItem, + RadioItem as MenubarRadioItem, + Separator as MenubarSeparator, + Shortcut as MenubarShortcut, + SubContent as MenubarSubContent, + SubTrigger as MenubarSubTrigger, + Trigger as MenubarTrigger, + Menu as MenubarMenu, + Group as MenubarGroup, + Sub as MenubarSub, + RadioGroup as MenubarRadioGroup, + Label as MenubarLabel, + GroupHeading as MenubarGroupHeading +}; diff --git a/src/lib/components/ui/menubar/menubar-checkbox-item.svelte b/src/lib/components/ui/menubar/menubar-checkbox-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1337b6f55404b5931c906563164ed2253bc57131 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/menubar/menubar-content.svelte b/src/lib/components/ui/menubar/menubar-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..67025909121c6200f13143000f97f9981652c208 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-content.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/src/lib/components/ui/menubar/menubar-group-heading.svelte b/src/lib/components/ui/menubar/menubar-group-heading.svelte new file mode 100644 index 0000000000000000000000000000000000000000..024e74281ff4198250b3063f21b0c27e1c0aa3cc --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar-group.svelte b/src/lib/components/ui/menubar/menubar-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f75b75addda1d1ed276cf385771f43f72ec1e5aa --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-group.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar-item.svelte b/src/lib/components/ui/menubar/menubar-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ce97103ab6fb4d10f1b0bdca7e4ae9b19188537c --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar-label.svelte b/src/lib/components/ui/menubar/menubar-label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1b36f6c297ae2751f770a8a25e59539b35c6a017 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-label.svelte @@ -0,0 +1,25 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/menubar/menubar-radio-item.svelte b/src/lib/components/ui/menubar/menubar-radio-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5fd6c207f51dd7676c5df37f8466e24844f897f6 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/menubar/menubar-separator.svelte b/src/lib/components/ui/menubar/menubar-separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a2caf6e6e19a2d3ab4869121777de8ec74caceeb --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar-shortcut.svelte b/src/lib/components/ui/menubar/menubar-shortcut.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0e08313d83b019d799ca3f690089c9295496e27b --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/menubar/menubar-sub-content.svelte b/src/lib/components/ui/menubar/menubar-sub-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8273903bdd08bc5472c3af1648c22e821f7f0c69 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar-sub-trigger.svelte b/src/lib/components/ui/menubar/menubar-sub-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0fd9e2cd0c97759fb8410716f706211d50877326 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/menubar/menubar-trigger.svelte b/src/lib/components/ui/menubar/menubar-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7814efe54fcd8736e6355d3e439d6c7f3d12d261 --- /dev/null +++ b/src/lib/components/ui/menubar/menubar-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/menubar/menubar.svelte b/src/lib/components/ui/menubar/menubar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e40fc196ca1cf3ff92bb83beae056fd5a1deb99a --- /dev/null +++ b/src/lib/components/ui/menubar/menubar.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/navigation-menu/index.ts b/src/lib/components/ui/navigation-menu/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..80441bd6f102f18744da6455a26159f51160488b --- /dev/null +++ b/src/lib/components/ui/navigation-menu/index.ts @@ -0,0 +1,28 @@ +import Root from "./navigation-menu.svelte"; +import Content from "./navigation-menu-content.svelte"; +import Indicator from "./navigation-menu-indicator.svelte"; +import Item from "./navigation-menu-item.svelte"; +import Link from "./navigation-menu-link.svelte"; +import List from "./navigation-menu-list.svelte"; +import Trigger from "./navigation-menu-trigger.svelte"; +import Viewport from "./navigation-menu-viewport.svelte"; + +export { + Root, + Content, + Indicator, + Item, + Link, + List, + Trigger, + Viewport, + // + Root as NavigationMenuRoot, + Content as NavigationMenuContent, + Indicator as NavigationMenuIndicator, + Item as NavigationMenuItem, + Link as NavigationMenuLink, + List as NavigationMenuList, + Trigger as NavigationMenuTrigger, + Viewport as NavigationMenuViewport +}; diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f4842fd740e10f64c715bd85fc9ba7109078a2c7 --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6c9bdfdf5fd3b3e26e1cb51570d2e6a6c23613ad --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte @@ -0,0 +1,22 @@ + + + +
+
diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b00b4b489adfff6f8e433576fbb0b8cb78a29209 --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a5b9747f7a1cd890f48beea4e55a57986fceb6ec --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c2c588072c1d25d6d4009d84d142613ea8ffe02e --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5b3ac26854b55790eb552b7ff030900d6c49d830 --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte @@ -0,0 +1,34 @@ + + + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte b/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9c69164e93941d66177a30407d8924312dbea363 --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte @@ -0,0 +1,22 @@ + + +
+ +
diff --git a/src/lib/components/ui/navigation-menu/navigation-menu.svelte b/src/lib/components/ui/navigation-menu/navigation-menu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..69c3d134f489e7db74f05aa687c8db57d7541940 --- /dev/null +++ b/src/lib/components/ui/navigation-menu/navigation-menu.svelte @@ -0,0 +1,32 @@ + + + + {@render children?.()} + + {#if viewport} + + {/if} + diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a8a309bb2f8e29e13823094734deeb8267b1f71 --- /dev/null +++ b/src/lib/components/ui/popover/index.ts @@ -0,0 +1,17 @@ +import { Popover as PopoverPrimitive } from "bits-ui"; +import Content from "./popover-content.svelte"; +import Trigger from "./popover-trigger.svelte"; +const Root = PopoverPrimitive.Root; +const Close = PopoverPrimitive.Close; + +export { + Root, + Content, + Trigger, + Close, + // + Root as Popover, + Content as PopoverContent, + Trigger as PopoverTrigger, + Close as PopoverClose +}; diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a487233ccdcf3406ee0a2f744ff748bff845be07 --- /dev/null +++ b/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/components/ui/popover/popover-trigger.svelte b/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..586323c71995f92a5f46f3dca589f70f2e3dab31 --- /dev/null +++ b/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/progress/index.ts b/src/lib/components/ui/progress/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..45dd0045d4eb40c66a84c6860bc34842bec986b6 --- /dev/null +++ b/src/lib/components/ui/progress/index.ts @@ -0,0 +1,7 @@ +import Root from "./progress.svelte"; + +export { + Root, + // + Root as Progress +}; diff --git a/src/lib/components/ui/progress/progress.svelte b/src/lib/components/ui/progress/progress.svelte new file mode 100644 index 0000000000000000000000000000000000000000..68330136a84f8cd5a7f51e0b239855856daee802 --- /dev/null +++ b/src/lib/components/ui/progress/progress.svelte @@ -0,0 +1,27 @@ + + + +
+
diff --git a/src/lib/components/ui/radio-group/index.ts b/src/lib/components/ui/radio-group/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..34d614664852cf1c75e21009b836de436ca6c7d5 --- /dev/null +++ b/src/lib/components/ui/radio-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./radio-group.svelte"; +import Item from "./radio-group-item.svelte"; + +export { + Root, + Item, + // + Root as RadioGroup, + Item as RadioGroupItem +}; diff --git a/src/lib/components/ui/radio-group/radio-group-item.svelte b/src/lib/components/ui/radio-group/radio-group-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e9bf3181c704a9ce5531bc27fff2e4db717b10ed --- /dev/null +++ b/src/lib/components/ui/radio-group/radio-group-item.svelte @@ -0,0 +1,29 @@ + + + + {#snippet children({ checked })} +
+ {#if checked} + + {/if} +
+ {/snippet} +
diff --git a/src/lib/components/ui/radio-group/radio-group.svelte b/src/lib/components/ui/radio-group/radio-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..da2912b0420e51ea85c53a6ed15c899afeea9e62 --- /dev/null +++ b/src/lib/components/ui/radio-group/radio-group.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/scroll-area/index.ts b/src/lib/components/ui/scroll-area/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b34624e2ce70efa7708ec15ce9e13a5afcd1f41 --- /dev/null +++ b/src/lib/components/ui/scroll-area/index.ts @@ -0,0 +1,10 @@ +import Scrollbar from "./scroll-area-scrollbar.svelte"; +import Root from "./scroll-area.svelte"; + +export { + Root, + Scrollbar, + //, + Root as ScrollArea, + Scrollbar as ScrollAreaScrollbar +}; diff --git a/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1a44807d195a8b40142c46e9d2bf7d1669a6239c --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte @@ -0,0 +1,31 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/scroll-area/scroll-area.svelte b/src/lib/components/ui/scroll-area/scroll-area.svelte new file mode 100644 index 0000000000000000000000000000000000000000..62058edafdb371189ffd6a0f4862df4b3f335840 --- /dev/null +++ b/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -0,0 +1,40 @@ + + + + + {@render children?.()} + + {#if orientation === "vertical" || orientation === "both"} + + {/if} + {#if orientation === "horizontal" || orientation === "both"} + + {/if} + + diff --git a/src/lib/components/ui/select/index.ts b/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4bd5ba2e6d629f2c710551ebad586fd4d6e57d75 --- /dev/null +++ b/src/lib/components/ui/select/index.ts @@ -0,0 +1,34 @@ +import { Select as SelectPrimitive } from "bits-ui"; + +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; + +const Root = SelectPrimitive.Root; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton +}; diff --git a/src/lib/components/ui/select/select-content.svelte b/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d639e1e0d74956e209087eaefede526156b569ef --- /dev/null +++ b/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + + + diff --git a/src/lib/components/ui/select/select-group.svelte b/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5454fdb39574cab9e523def2a9ce7a927723e563 --- /dev/null +++ b/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/select/select-item.svelte b/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6d5d98e0ffb13a3c21557b9ca3e242a8eca79a63 --- /dev/null +++ b/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/src/lib/components/ui/select/select-label.svelte b/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..46960259f37423b781a013708267b45eb7d1e766 --- /dev/null +++ b/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/select/select-scroll-down-button.svelte b/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000000000000000000000000000000000000..36292058266076dde3c218b10df959ba4c915533 --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/src/lib/components/ui/select/select-scroll-up-button.svelte b/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1aa2300c9b603ef9cf4d5f47c990592382be92d5 --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/src/lib/components/ui/select/select-separator.svelte b/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6cee19c68f26b0a9b788ae6822f3ecf5e8355275 --- /dev/null +++ b/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/select/select-trigger.svelte b/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..172f98808894fd0783a411b89dd9402d7decf20b --- /dev/null +++ b/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbfb13914165d8214e72c94f21d931cf196de43a --- /dev/null +++ b/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cc592fceda0a16ed7fa0788ab67700a233944638 --- /dev/null +++ b/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..29e0d0c45deeda24fbf544a6fd4a71060459235a --- /dev/null +++ b/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,36 @@ +import { Dialog as SheetPrimitive } from "bits-ui"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +const Root = SheetPrimitive.Root; +const Portal = SheetPrimitive.Portal; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription +}; diff --git a/src/lib/components/ui/sheet/sheet-close.svelte b/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ae382c123dff4245dfc0f9b023c5a6ca578b8268 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0a715cd262b13d76d907fd9de305a0868f5ebe98 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,60 @@ + + + + + + + + {@render children?.()} + + + Close + + + diff --git a/src/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000000000000000000000000000000000000..333b17a7af16dff1ed6e0330b1bf952d4c088db1 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dd9ed84b0969492deb81192c1e52b2a0d85fbb04 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..757a6a566fc292bb77c768342a878e3d2566de81 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..345e197bd8b2f08c719ce3c5e8ad599f791c5e2c --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9fda327e992d60d0a2941f409fc893dd87c88f99 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-trigger.svelte b/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e266975f962ca93d31909046ced253622d5bd791 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..4de44351d452dedf270f9058e2f7857a2e0622a6 --- /dev/null +++ b/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar:state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f9b4a99b23995aae5f3e57d85437d24af80327e --- /dev/null +++ b/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,79 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c2e6b8159d842d87ab2cdb7c95fdd4f7e4c5b5c --- /dev/null +++ b/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar +}; diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f12180020209fc10789fad0e7320eb9b376c562d --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6259cb952695c1327dc4641be9aa0a7bcd4a0575 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d9579fd3b91db007861389e147ae1e16588958ca --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,36 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..415255f1f55463975bffd9c386c365afd94131e8 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2b6287ac3f43d2377c79c3014d35e22c05a4fc36 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ec18a6976a8ab7c9e07ebfface50fad127a1c553 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a1b2db1504613533a4ae8384ba79b473b473453d --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000000000000000000000000000000000000..905a80a3007e25285695602eedb64885a46013d3 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000000000000000000000000000000000000..140de4a2533c4f4577e15f16704fe5c211d963f1 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fb386c3172ce16235d1e34c5d829660aac9b88c3 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000000000000000000000000000000000000..09d4a90a57143cd10043efbf201832f58a80c5ed --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a24059bc9a6a9b8736b59e8a82234e0bb7abb7d1 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,101 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4db445323f650db106c75748e860df474d8b03ad --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..481d598645ad8ae9e6fdf630ef413f6c17745251 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d221310e4d5394208b7fcab584e97b0e9c772908 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..681d0f1d3687e8775f77e5322a104c6c4d307135 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8ab11110e576bebe66aa7ff16993527ac44d694f --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..946ccceff3f4e417bec8a4ec278fa6bdc2c2929b --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bb5c525ed968709fb268df1ad97bb2dd2a2228c6 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000000000000000000000000000000000000..29616c6c499ab0243e8b366368f53146c5129031 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6d3448b92ed378166ed6a94a53ab5a26aa976a65 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..449e735023effb62c3d2f0417592fa4968fef1cc --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0fe6620c0c7621fc0b4533a609fcf547357460c4 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,101 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2be5c505ed012e99c6c4a59797c0fbd16730cf49 --- /dev/null +++ b/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton +}; diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c7e3d26cef8ec70410239305147b54bbe1aa4778 --- /dev/null +++ b/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/src/lib/components/ui/slider/index.ts b/src/lib/components/ui/slider/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3844186754f2a03d1189d536b72febf8e5f57f46 --- /dev/null +++ b/src/lib/components/ui/slider/index.ts @@ -0,0 +1,7 @@ +import Root from "./slider.svelte"; + +export { + Root, + // + Root as Slider +}; diff --git a/src/lib/components/ui/slider/slider.svelte b/src/lib/components/ui/slider/slider.svelte new file mode 100644 index 0000000000000000000000000000000000000000..df244b3688b86bc8fec10ddab398d433b355bcbe --- /dev/null +++ b/src/lib/components/ui/slider/slider.svelte @@ -0,0 +1,52 @@ + + + + + {#snippet children({ thumbs })} + + + + {#each thumbs as thumb (thumb)} + + {/each} + {/snippet} + diff --git a/src/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ad9f4a2ab5b7efbea0edeb6529b033224c38c06 --- /dev/null +++ b/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000000000000000000000000000000000000..67669b7f6060bec20d89c165b2108303725b4486 --- /dev/null +++ b/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0e5fb793501cfcc6dda20c9b56cc7b4fe13fa1b --- /dev/null +++ b/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from "./switch.svelte"; + +export { + Root, + // + Root as Switch +}; diff --git a/src/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..80661fd827344201520651e3dc28536b4ac315ae --- /dev/null +++ b/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..450c9b33d8de0f20e9cb439a2bf8f8124f380a1c --- /dev/null +++ b/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow +}; diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000000000000000000000000000000000000..29e96875fbe5081c082cf55bc3488eba887114fb --- /dev/null +++ b/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4696cff571523025bfde3ba19c2514269b55e674 --- /dev/null +++ b/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a24d106dcd3a1239e7f4a387c61aabc16e313bc4 --- /dev/null +++ b/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b9b14ebfacac5036b2f9176cf82fae01f96767cb --- /dev/null +++ b/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7d3b76086a98f757c7fcfd140873c952ba7dcdf5 --- /dev/null +++ b/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f47d2597cfbc2b2ce0fafdd8f58119ff8189ad6a --- /dev/null +++ b/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8829581c9802c2784d112376f7fc1b6db614a0a3 --- /dev/null +++ b/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a3349563249d3d010ccf0948fac06735fcd5cf99 --- /dev/null +++ b/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
    + + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/tabs/index.ts b/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..56fe91c0d3ee315e45332ddd664edacf668ade75 --- /dev/null +++ b/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,16 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/src/lib/components/ui/tabs/tabs-content.svelte b/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..340d65cf2163408a74e5522add3f679e330d3aaf --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs-list.svelte b/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48d14b076fd23b4f2fc64fd5daed460d128ecff5 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs-trigger.svelte b/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e623b366b3d2d5127cde6a9ec2a8059fc7ae6918 --- /dev/null +++ b/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/tabs/tabs.svelte b/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ef6cada5e0fc9173dd48b240fe0379a3fb80183e --- /dev/null +++ b/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/textarea/index.ts b/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..99752e9574894f7755df117d3bb7563138ada76c --- /dev/null +++ b/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from "./textarea.svelte"; + +export { + Root, + // + Root as Textarea +}; diff --git a/src/lib/components/ui/textarea/textarea.svelte b/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6f0d9fbcec791ae451d79d41647020016ae7c8f1 --- /dev/null +++ b/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/toggle-group/index.ts b/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..17f7532aaefe7c2924a430c31585e8b98eedd426 --- /dev/null +++ b/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Root from "./toggle-group.svelte"; +import Item from "./toggle-group-item.svelte"; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem +}; diff --git a/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000000000000000000000000000000000000..77712cf8846423fc7090173a4b4973e4656d91bd --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,34 @@ + + + diff --git a/src/lib/components/ui/toggle-group/toggle-group.svelte b/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bcf54950f82336331098426adf3976280827ca25 --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,47 @@ + + + + + + diff --git a/src/lib/components/ui/toggle/index.ts b/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c528d293620ff8dc0f90a1f2132ceb3be19d5f7 --- /dev/null +++ b/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,13 @@ +import Root from "./toggle.svelte"; +export { + toggleVariants, + type ToggleSize, + type ToggleVariant, + type ToggleVariants +} from "./toggle.svelte"; + +export { + Root, + // + Root as Toggle +}; diff --git a/src/lib/components/ui/toggle/toggle.svelte b/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..54cbe009574933fba28da8c8562ab29f138d2a51 --- /dev/null +++ b/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8dc9df85f3d7b429c56e1d2c93550d518190d17 --- /dev/null +++ b/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from "bits-ui"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal +}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a15ece71c1a767e6161a0e6b931d58413dc184f8 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1acdaa47be048caedef2dfaec62f426919aae250 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/configs/robotUrdfConfig.ts b/src/lib/configs/robotUrdfConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..41e7a39d35354558a199c35e438f3c406bc8e27c --- /dev/null +++ b/src/lib/configs/robotUrdfConfig.ts @@ -0,0 +1,56 @@ +import type { RobotUrdfConfig } from "$lib/types/urdf"; + +export const robotUrdfConfigMap: { [key: string]: RobotUrdfConfig } = { + "so-arm100": { + urdfUrl: "/robots/so-100/so_arm100.urdf", + jointNameIdMap: { + Rotation: 1, + Pitch: 2, + Elbow: 3, + Wrist_Pitch: 4, + Wrist_Roll: 5, + Jaw: 6, + // camera_mount: 7 + }, + // Rest position - robot in neutral/calibration pose (all joints at 0 degrees) + restPosition: { + Rotation: 0, + Pitch: 0, + Elbow: 0, + Wrist_Pitch: 0, + Wrist_Roll: 0, + Jaw: 0, + // camera_mount: 0 + }, + compoundMovements: [ + { + name: "Jaw down & up", + primaryJoint: 2, + primaryFormula: "primary < 100 ? 1 : -1", + dependents: [ + { + joint: 3, + formula: "primary < 100 ? -1.9 * deltaPrimary : 0.4 * deltaPrimary" + // formula: "- deltaPrimary * (0.13 * Math.sin(primary * (Math.PI / 180)) + 0.13 * Math.sin((primary-dependent) * (Math.PI / 180)))/(0.13 * Math.sin((primary - dependent) * (Math.PI / 180)))", + }, + { + joint: 4, + formula: + "primary < 100 ? (primary < 10 ? 0 : 0.51 * deltaPrimary) : -0.4 * deltaPrimary" + } + ] + }, + { + name: "Jaw backward & forward", + primaryJoint: 2, + primaryFormula: "1", + dependents: [ + { + joint: 3, + formula: "-0.9*deltaPrimary" + } + ] + } + ] + } +}; diff --git a/src/lib/elements/compute/README.md b/src/lib/elements/compute/README.md new file mode 100644 index 0000000000000000000000000000000000000000..eb762cd3deb8b2ee1ffc9a1c7fcc40851ccd5e36 --- /dev/null +++ b/src/lib/elements/compute/README.md @@ -0,0 +1,147 @@ +# AI Compute System + +This module provides a comprehensive AI compute management system for the LeRobot Arena frontend, integrating with the AI server backend for ACT model inference sessions. + +## Architecture + +The system follows the same pattern as the video and robot managers: + +- **RemoteComputeManager**: Global manager for all AI compute instances +- **RemoteCompute**: Individual AI compute instance with reactive state +- **UI Components**: Modal dialogs and status displays for managing compute sessions + +## Core Components + +### RemoteComputeManager + +The main manager class that handles: +- Creating and managing AI compute instances +- Communicating with the AI server backend +- Session lifecycle management (create, start, stop, delete) +- Health monitoring and status updates + +```typescript +import { remoteComputeManager } from '$lib/elements/compute/'; + +// Create a new compute instance +const compute = remoteComputeManager.createCompute('my-compute', 'ACT Model'); + +// Create an AI session +await remoteComputeManager.createSession(compute.id, { + sessionId: 'my-session', + policyPath: './checkpoints/act_so101_beyond', + cameraNames: ['front', 'wrist'], + transportServerUrl: 'http://localhost:8000' +}); + +// Start inference +await remoteComputeManager.startSession(compute.id); +``` + +### RemoteCompute + +Individual compute instances with reactive state: + +```typescript +// Access compute properties +compute.hasSession // boolean - has an active session +compute.isRunning // boolean - session is running inference +compute.canStart // boolean - can start inference +compute.canStop // boolean - can stop inference +compute.statusInfo // status display information +``` + +## AI Server Integration + +The system integrates with the AI server backend (`backend/ai-server/`) which provides: + +- **ACT Model Inference**: Real-time robot control using Action Chunking Transformer models +- **Session Management**: Create, start, stop, and delete inference sessions +- **Transport Server Communication**: Dedicated rooms for camera inputs, joint inputs, and joint outputs +- **Multi-camera Support**: Support for multiple camera streams per session + +### Session Workflow + +1. **Create Session**: Establishes connection with AI server and creates transport server rooms +2. **Configure Inputs**: Sets up camera rooms and joint input rooms +3. **Start Inference**: Begins ACT model inference and joint command output +4. **Monitor Status**: Real-time status updates and performance metrics +5. **Stop/Delete**: Clean session teardown + +## UI Components + +### Modal Dialog + +`AISessionConnectionModal.svelte` provides a comprehensive interface for: +- Creating new AI sessions with configurable parameters +- Managing existing sessions (start, stop, delete) +- Viewing session status and connection details +- Real-time session monitoring + +### Status Display + +The status system shows input/output connections: + +- **Input Box**: Shows camera inputs and joint state inputs +- **Compute Box**: Shows AI model status and information +- **Output Box**: Shows joint command outputs +- **Connection Flow**: Visual representation of data flow + +### 3D Integration + +- Uses existing GPU 3D models for visual representation +- Interactive hover states and status billboards +- Positioned in 3D space alongside robots and videos + +## Usage Example + +```typescript +// 1. Create a compute instance +const compute = remoteComputeManager.createCompute(); + +// 2. Configure and create AI session +await remoteComputeManager.createSession(compute.id, { + sessionId: 'robot-control-01', + policyPath: './checkpoints/act_so101_beyond', + cameraNames: ['front', 'wrist', 'overhead'], + transportServerUrl: 'http://localhost:8000', + workspaceId: 'workspace-123' +}); + +// 3. Start inference +await remoteComputeManager.startSession(compute.id); + +// 4. Monitor status +const status = await remoteComputeManager.getSessionStatus(compute.id); +console.log(status.stats.inference_count); +``` + +## Configuration + +The system connects to: +- **Inference Server**: `http://localhost:8001` (configurable) - Runs AI models and inference sessions +- **Transport Server**: `http://localhost:8000` (configurable) - Manages communication rooms and data routing + +## File Structure + +``` +compute/ +├── RemoteComputeManager.svelte.ts # Main manager class +├── RemoteCompute.svelte.ts # Individual compute instance +├── modal/ +│ └── AISessionConnectionModal.svelte # Session management modal +├── status/ +│ ├── ComputeInputBoxUIKit.svelte # Input status display +│ ├── ComputeOutputBoxUIKit.svelte # Output status display +│ ├── ComputeBoxUIKit.svelte # Main compute display +│ ├── ComputeConnectionFlowBoxUIKit.svelte # Connection flow +│ └── ComputeStatusBillboard.svelte # 3D status billboard +└── index.ts # Module exports +``` + +## Integration Points + +- **3D Scene**: `Computes.svelte` renders all compute instances +- **Add Button**: `AddAIButton.svelte` creates new compute instances +- **Main Page**: Integrated in the main workspace view +- **GPU Models**: Reuses existing GPU 3D models for visual consistency \ No newline at end of file diff --git a/src/lib/elements/compute/RemoteCompute.svelte.ts b/src/lib/elements/compute/RemoteCompute.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0ff9e7428fdf34b75dcd5e515beebb9360a8090 --- /dev/null +++ b/src/lib/elements/compute/RemoteCompute.svelte.ts @@ -0,0 +1,146 @@ +import type { Positionable, Position3D } from '$lib/types/positionable.js'; +import type { AISessionConfig, AISessionResponse } from './RemoteComputeManager.svelte'; + +export type ComputeStatus = 'disconnected' | 'ready' | 'running' | 'stopped' | 'initializing'; + +export class RemoteCompute implements Positionable { + readonly id: string; + + // Reactive state using Svelte 5 runes + position = $state({ x: 0, y: 0, z: 0 }); + name = $state(''); + status = $state('disconnected'); + + // Session data + sessionId = $state(null); + sessionConfig = $state(null); + sessionData = $state(null); + + // Derived reactive values + hasSession = $derived(this.sessionId !== null); + isRunning = $derived(this.status === 'running'); + canStart = $derived(this.status === 'ready' || this.status === 'stopped'); + canStop = $derived(this.status === 'running'); + + constructor(id: string, name?: string) { + this.id = id; + this.name = name || `Compute ${id}`; + } + + /** + * Get input connections (camera and joint inputs) + */ + get inputConnections() { + if (!this.sessionData) return null; + + return { + cameras: this.sessionData.camera_room_ids, + jointInput: this.sessionData.joint_input_room_id, + workspaceId: this.sessionData.workspace_id + }; + } + + /** + * Get output connections (joint output) + */ + get outputConnections() { + if (!this.sessionData) return null; + + return { + jointOutput: this.sessionData.joint_output_room_id, + workspaceId: this.sessionData.workspace_id + }; + } + + /** + * Get display information for UI + */ + get displayInfo() { + return { + id: this.id, + name: this.name, + status: this.status, + sessionId: this.sessionId, + policyPath: this.sessionConfig?.policyPath, + cameraNames: this.sessionConfig?.cameraNames || [], + hasSession: this.hasSession, + isRunning: this.isRunning, + canStart: this.canStart, + canStop: this.canStop + }; + } + + /** + * Get status for billboard display + */ + get statusInfo() { + const status = this.status; + let statusText = ''; + let statusColor = ''; + + switch (status) { + case 'disconnected': + statusText = 'Disconnected'; + statusColor = 'rgb(107, 114, 128)'; // gray + break; + case 'ready': + statusText = 'Ready'; + statusColor = 'rgb(245, 158, 11)'; // yellow + break; + case 'running': + statusText = 'Running'; + statusColor = 'rgb(34, 197, 94)'; // green + break; + case 'stopped': + statusText = 'Stopped'; + statusColor = 'rgb(239, 68, 68)'; // red + break; + case 'initializing': + statusText = 'Initializing'; + statusColor = 'rgb(59, 130, 246)'; // blue + break; + } + + return { + status, + statusText, + statusColor, + emoji: this.getStatusEmoji() + }; + } + + private getStatusEmoji(): string { + switch (this.status) { + case 'disconnected': return '⚪'; + case 'ready': return '🟡'; + case 'running': return '🟢'; + case 'stopped': return '🔴'; + case 'initializing': return '🟠'; + default: return '⚪'; + } + } + + /** + * Reset session data + */ + resetSession(): void { + this.sessionId = null; + this.sessionConfig = null; + this.sessionData = null; + this.status = 'disconnected'; + } + + /** + * Update position + */ + updatePosition(newPosition: Position3D): void { + this.position = { ...newPosition }; + } + + /** + * Update name + */ + updateName(newName: string): void { + this.name = newName; + } +} \ No newline at end of file diff --git a/src/lib/elements/compute/RemoteComputeManager.svelte.ts b/src/lib/elements/compute/RemoteComputeManager.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..5727ff0fdb60e4761b36eb91560fde95e293f130 --- /dev/null +++ b/src/lib/elements/compute/RemoteComputeManager.svelte.ts @@ -0,0 +1,317 @@ +import { RemoteCompute } from './RemoteCompute.svelte'; +import type { Position3D } from '$lib/types/positionable.js'; +import { generateName } from '$lib/utils/generateName.js'; +import { positionManager } from '$lib/utils/positionManager.js'; +import { type LeRobotAIServerClient, createClient } from '@robohub/inference-server-client'; +import { settings } from '$lib/runes/settings.svelte'; +import type { + CreateSessionRequest, + CreateSessionResponse, + SessionStatusResponse +} from '@robohub/inference-server-client'; + +export interface AISessionConfig { + sessionId: string; + policyPath: string; + cameraNames: string[]; + transportServerUrl: string; + workspaceId?: string; +} + +export interface AISessionResponse { + workspace_id: string; + camera_room_ids: Record; + joint_input_room_id: string; + joint_output_room_id: string; +} + +export interface AISessionStatus { + session_id: string; + status: 'initializing' | 'ready' | 'running' | 'stopped'; + policy_path: string; + camera_names: string[]; + workspace_id: string; + rooms: { + workspace_id: string; + camera_room_ids: Record; + joint_input_room_id: string; + joint_output_room_id: string; + }; + stats: { + inference_count: number; + commands_sent: number; + joints_received: number; + images_received: Record; + errors: number; + actions_in_queue: number; + }; + inference_stats?: { + inference_count: number; + total_inference_time: number; + average_inference_time: number; + average_fps: number; + is_loaded: boolean; + device: string; + }; + error_message?: string; +} + +export class RemoteComputeManager { + private _computes = $state([]); + private inferenceServerClient; + + constructor() { + this.inferenceServerClient = createClient(settings.inferenceServerUrl); + } + + // Reactive getters + get computes(): RemoteCompute[] { + return this._computes; + } + + get computeCount(): number { + return this._computes.length; + } + + get runningComputes(): RemoteCompute[] { + return this._computes.filter(compute => compute.status === 'running'); + } + + /** + * Create a new AI compute instance + */ + createCompute(id?: string, name?: string, position?: Position3D): RemoteCompute { + const computeId = id || generateName(); + + // Check if compute already exists + if (this._computes.find(c => c.id === computeId)) { + throw new Error(`Compute with ID ${computeId} already exists`); + } + + // Create compute instance + const compute = new RemoteCompute(computeId, name); + + // Set position (from position manager if not provided) + compute.position = position || positionManager.getNextPosition(); + + // Add to reactive array + this._computes.push(compute); + + console.log(`Created compute ${computeId} at position (${compute.position.x.toFixed(1)}, ${compute.position.y.toFixed(1)}, ${compute.position.z.toFixed(1)}). Total computes: ${this._computes.length}`); + + return compute; + } + + /** + * Get compute by ID + */ + getCompute(id: string): RemoteCompute | undefined { + return this._computes.find(c => c.id === id); + } + + /** + * Remove a compute instance + */ + async removeCompute(id: string): Promise { + const computeIndex = this._computes.findIndex(c => c.id === id); + if (computeIndex === -1) return; + + const compute = this._computes[computeIndex]; + + // Clean up compute resources + await this.stopSession(id); + await this.deleteSession(id); + + // Remove from reactive array + this._computes.splice(computeIndex, 1); + + console.log(`Removed compute ${id}. Remaining computes: ${this._computes.length}`); + } + + /** + * Create an AI session + */ + async createSession(computeId: string, config: AISessionConfig): Promise<{ success: boolean; error?: string; data?: AISessionResponse }> { + const compute = this.getCompute(computeId); + if (!compute) { + return { success: false, error: `Compute ${computeId} not found` }; + } + + try { + const request: CreateSessionRequest = { + session_id: config.sessionId, + policy_path: config.policyPath, + camera_names: config.cameraNames, + arena_server_url: config.transportServerUrl, + workspace_id: config.workspaceId || undefined, + }; + + const data: CreateSessionResponse = await this.inferenceServerClient.createSession(request); + + // Update compute with session info + compute.sessionId = config.sessionId; + compute.status = 'ready'; + compute.sessionConfig = config; + compute.sessionData = { + workspace_id: data.workspace_id, + camera_room_ids: data.camera_room_ids, + joint_input_room_id: data.joint_input_room_id, + joint_output_room_id: data.joint_output_room_id + }; + + return { success: true, data: compute.sessionData }; + } catch (error) { + console.error(`Failed to create session for compute ${computeId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Start inference for a session + */ + async startSession(computeId: string): Promise<{ success: boolean; error?: string }> { + const compute = this.getCompute(computeId); + if (!compute || !compute.sessionId) { + return { success: false, error: 'No session to start' }; + } + + try { + await this.inferenceServerClient.startInference(compute.sessionId); + compute.status = 'running'; + return { success: true }; + } catch (error) { + console.error(`Failed to start session for compute ${computeId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Stop inference for a session + */ + async stopSession(computeId: string): Promise<{ success: boolean; error?: string }> { + const compute = this.getCompute(computeId); + if (!compute || !compute.sessionId) { + return { success: false, error: 'No session to stop' }; + } + + try { + await this.inferenceServerClient.stopInference(compute.sessionId); + compute.status = 'stopped'; + return { success: true }; + } catch (error) { + console.error(`Failed to stop session for compute ${computeId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Delete a session + */ + async deleteSession(computeId: string): Promise<{ success: boolean; error?: string }> { + const compute = this.getCompute(computeId); + if (!compute || !compute.sessionId) { + return { success: true }; // Already deleted + } + + try { + await this.inferenceServerClient.deleteSession(compute.sessionId); + + // Reset compute session info + compute.sessionId = null; + compute.status = 'disconnected'; + compute.sessionConfig = null; + compute.sessionData = null; + + return { success: true }; + } catch (error) { + console.error(`Failed to delete session for compute ${computeId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Get session status + */ + async getSessionStatus(computeId: string): Promise<{ success: boolean; data?: AISessionStatus; error?: string }> { + const compute = this.getCompute(computeId); + if (!compute || !compute.sessionId) { + return { success: false, error: 'No session found' }; + } + + try { + const data: SessionStatusResponse = await this.inferenceServerClient.getSessionStatus(compute.sessionId); + + // Update compute status + compute.status = data.status as 'initializing' | 'ready' | 'running' | 'stopped'; + + // Convert to AISessionStatus format + const sessionStatus: AISessionStatus = { + session_id: data.session_id, + status: data.status as 'initializing' | 'ready' | 'running' | 'stopped', + policy_path: data.policy_path, + camera_names: data.camera_names, + workspace_id: data.workspace_id, + rooms: data.rooms as any, + stats: data.stats as any, + inference_stats: data.inference_stats as any, + error_message: data.error_message || undefined + }; + + return { success: true, data: sessionStatus }; + } catch (error) { + console.error(`Failed to get session status for compute ${computeId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Check AI server health + */ + async checkServerHealth(): Promise<{ success: boolean; data?: any; error?: string }> { + try { + const isHealthy = await this.inferenceServerClient.isHealthy(); + if (!isHealthy) { + return { success: false, error: 'Server is not healthy' }; + } + + const data = await this.inferenceServerClient.getHealth(); + return { success: true, data }; + } catch (error) { + console.error('Failed to check AI server health:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Clean up all computes + */ + async destroy(): Promise { + const cleanupPromises = this._computes.map(async (compute) => { + await this.stopSession(compute.id); + await this.deleteSession(compute.id); + }); + await Promise.allSettled(cleanupPromises); + this._computes.length = 0; + } +} + +// Global compute manager instance +export const remoteComputeManager = new RemoteComputeManager(); \ No newline at end of file diff --git a/src/lib/elements/compute/index.ts b/src/lib/elements/compute/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..39e408dd4e520b80ff9c80e2a8eaec96670d9c98 --- /dev/null +++ b/src/lib/elements/compute/index.ts @@ -0,0 +1,4 @@ +export { RemoteComputeManager, remoteComputeManager } from './RemoteComputeManager.svelte.js'; +export { RemoteCompute } from './RemoteCompute.svelte.js'; +export type { AISessionConfig, AISessionResponse, AISessionStatus } from './RemoteComputeManager.svelte.js'; +export type { ComputeStatus } from './RemoteCompute.svelte.js'; \ No newline at end of file diff --git a/src/lib/elements/robot/Robot.svelte.ts b/src/lib/elements/robot/Robot.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a0d193910d33fb449d52192746fa2272a1bdd84 --- /dev/null +++ b/src/lib/elements/robot/Robot.svelte.ts @@ -0,0 +1,471 @@ +import type { + JointState, + RobotCommand, + ConnectionStatus, + USBDriverConfig, + RemoteDriverConfig, + Consumer, + Producer +} from './models.js'; +import type { Positionable, Position3D } from '$lib/types/positionable.js'; +import { USBConsumer } from './drivers/USBConsumer.js'; +import { USBProducer } from './drivers/USBProducer.js'; +import { RemoteConsumer } from './drivers/RemoteConsumer.js'; +import { RemoteProducer } from './drivers/RemoteProducer.js'; +import { USBCalibrationManager } from './calibration/USBCalibrationManager.js'; +import type { UrdfRobotState } from '@/types/robot.js'; +import { ROBOT_CONFIG } from './config.js'; +import type IUrdfRobot from '@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.js'; + +export class Robot implements Positionable { + // Core robot data + readonly id: string; + private unsubscribeFns: (() => void)[] = []; + + // Command synchronization to prevent state conflicts + private commandMutex = $state(false); + private pendingCommands: RobotCommand[] = []; + + // Command deduplication to prevent rapid duplicate commands + private lastCommandTime = 0; + private lastCommandValues: Record = {}; + + // Memory management + private lastCleanup = 0; + + // Single consumer and multiple producers using Svelte 5 runes - PUBLIC for reactive access + consumer = $state(null); + producers = $state([]); + + // Reactive state using Svelte 5 runes - PUBLIC for reactive access + joints = $state>({}); + position = $state({ x: 0, y: 0, z: 0 }); + isManualControlEnabled = $state(true); + connectionStatus = $state({ isConnected: false }); + + // URDF robot state for 3D visualization - PUBLIC for reactive access + urdfRobotState = $state(null); + + // Shared USB calibration manager for this robot + private usbCalibrationManager: USBCalibrationManager = new USBCalibrationManager(); + + // Derived reactive values for components + jointArray = $derived(Object.values(this.joints)); + hasProducers = $derived(this.producers.length > 0); + hasConsumer = $derived(this.consumer !== null && this.consumer.status.isConnected); + outputDriverCount = $derived(this.producers.filter(d => d.status.isConnected).length); + + constructor(id: string, initialJoints: JointState[], urdfRobotState?: IUrdfRobot) { + this.id = id; + + // Store URDF robot state if provided + this.urdfRobotState = urdfRobotState || null; + + // Initialize joints with normalized values + initialJoints.forEach(joint => { + const isGripper = joint.name.toLowerCase() === 'jaw' || joint.name.toLowerCase() === 'gripper'; + this.joints[joint.name] = { + ...joint, + value: isGripper ? 0 : 0 // Start at neutral position + }; + }); + } + + // Method to set URDF robot state after creation (for async loading) + setUrdfRobotState(urdfRobotState: any): void { + this.urdfRobotState = urdfRobotState; + } + + /** + * Update position (implements Positionable interface) + */ + updatePosition(newPosition: Position3D): void { + this.position = { ...newPosition }; + } + + // Calibration access + get calibrationManager(): USBCalibrationManager { + return this.usbCalibrationManager; + } + + // NEW: Sync virtual robot to final calibration positions + syncToCalibrationPositions(finalPositions: Record): void { + console.log(`[Robot ${this.id}] 🔄 Syncing virtual robot to final calibration positions...`); + + Object.entries(finalPositions).forEach(([jointName, rawPosition]) => { + const joint = this.joints[jointName]; + if (!joint) { + console.warn(`[Robot ${this.id}] Joint ${jointName} not found for position sync`); + return; + } + + // Convert raw servo position to normalized value using calibration data + const normalizedValue = this.usbCalibrationManager.normalizeValue(rawPosition, jointName); + + // Clamp to appropriate normalized range based on joint type + let clampedValue: number; + if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') { + clampedValue = Math.max(0, Math.min(100, normalizedValue)); + } else { + clampedValue = Math.max(-100, Math.min(100, normalizedValue)); + } + + console.log(`[Robot ${this.id}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`); + + // Update joint value to match physical servo position + this.joints[jointName] = { ...joint, value: clampedValue }; + }); + + console.log(`[Robot ${this.id}] ✅ Virtual robot synced to calibration positions`); + } + + // Joint value updates (normalized) + updateJoint(name: string, normalizedValue: number): void { + if (!this.isManualControlEnabled) { + console.warn('Manual control is disabled'); + return; + } + + const joint = this.joints[name]; + if (!joint) { + console.warn(`Joint ${name} not found`); + return; + } + + // Clamp to appropriate normalized range based on joint type + if (name.toLowerCase() === 'jaw' || name.toLowerCase() === 'gripper') { + normalizedValue = Math.max(0, Math.min(100, normalizedValue)); + } else { + normalizedValue = Math.max(-100, Math.min(100, normalizedValue)); + } + + console.debug(`[Robot ${this.id}] Manual update joint ${name} to ${normalizedValue} (normalized)`); + + // Create a new joint object to ensure reactivity + this.joints[name] = { ...joint, value: normalizedValue }; + + // Send normalized command to producers + this.sendToProducers({ joints: [{ name, value: normalizedValue }] }); + } + + executeCommand(command: RobotCommand): void { + // Command deduplication - skip if same values sent within dedup window + const now = Date.now(); + if (now - this.lastCommandTime < ROBOT_CONFIG.commands.dedupWindow) { + const hasChanges = command.joints.some(joint => + Math.abs((this.lastCommandValues[joint.name] || 0) - joint.value) > 0.5 + ); + if (!hasChanges) { + console.debug(`[Robot ${this.id}] 🔄 Skipping duplicate command within ${ROBOT_CONFIG.commands.dedupWindow}ms window`); + return; + } + } + + // Update deduplication tracking + this.lastCommandTime = now; + command.joints.forEach(joint => { + this.lastCommandValues[joint.name] = joint.value; + }); + + // Queue command if mutex is locked to prevent race conditions + if (this.commandMutex) { + if (this.pendingCommands.length >= ROBOT_CONFIG.commands.maxQueueSize) { + console.warn(`[Robot ${this.id}] Command queue full, dropping oldest command`); + this.pendingCommands.shift(); + } + this.pendingCommands.push(command); + return; + } + + this.commandMutex = true; + + try { + console.debug(`[Robot ${this.id}] Executing command with ${command.joints.length} joints:`, + command.joints.map(j => `${j.name}=${j.value}`).join(', ')); + + // Check if USB calibration is in progress + if (this.usbCalibrationManager.calibrationState.isCalibrating) { + console.debug(`[Robot ${this.id}] 🚫 Blocking virtual robot updates - USB calibration in progress`); + // Still send to producers, but don't update virtual robot + this.sendToProducers(command); + return; + } + + // Check if USB calibration is needed (if we have USB consumer/producers) + const hasUSBDrivers = (this.consumer instanceof USBConsumer) || + this.producers.some(p => p instanceof USBProducer); + + if (hasUSBDrivers && this.usbCalibrationManager.needsCalibration) { + console.debug(`[Robot ${this.id}] ⏳ Blocking virtual robot updates - USB drivers need calibration`); + // Still send to producers, but don't update virtual robot + this.sendToProducers(command); + return; + } + + console.debug(`[Robot ${this.id}] ✅ Updating virtual robot - USB calibrated or no USB drivers`); + + command.joints.forEach(jointCmd => { + const joint = this.joints[jointCmd.name]; + if (joint) { + // Clamp to appropriate normalized range based on joint type + let normalizedValue: number; + if (jointCmd.name.toLowerCase() === 'jaw' || jointCmd.name.toLowerCase() === 'gripper') { + normalizedValue = Math.max(0, Math.min(100, jointCmd.value)); + } else { + normalizedValue = Math.max(-100, Math.min(100, jointCmd.value)); + } + + console.debug(`[Robot ${this.id}] Joint ${jointCmd.name}: ${jointCmd.value} -> ${normalizedValue} (normalized)`); + + // Create a new joint object to ensure reactivity + this.joints[jointCmd.name] = { ...joint, value: normalizedValue }; + } else { + console.warn(`[Robot ${this.id}] Joint ${jointCmd.name} not found`); + } + }); + + // Send normalized command to producers + this.sendToProducers(command); + } finally { + this.commandMutex = false; + + // Periodic cleanup to prevent memory leaks + const now = Date.now(); + if (now - this.lastCleanup > ROBOT_CONFIG.performance.memoryCleanupInterval) { + // Clear old command values that haven't been updated recently + Object.keys(this.lastCommandValues).forEach(jointName => { + if (now - this.lastCommandTime > ROBOT_CONFIG.performance.memoryCleanupInterval) { + delete this.lastCommandValues[jointName]; + } + }); + this.lastCleanup = now; + } + + // Process any pending commands + if (this.pendingCommands.length > 0) { + const nextCommand = this.pendingCommands.shift(); + if (nextCommand) { + // Use setTimeout to prevent stack overflow with rapid commands + setTimeout(() => this.executeCommand(nextCommand), 0); + } + } + } + } + + // Consumer management (input driver) - SINGLE consumer only + async setConsumer(config: USBDriverConfig | RemoteDriverConfig): Promise { + return this._setConsumer(config, false); + } + + // Join existing room as consumer (for AI session integration) + async joinAsConsumer(config: RemoteDriverConfig): Promise { + if (config.type !== 'remote') { + throw new Error('joinAsConsumer only supports remote drivers'); + } + return this._setConsumer(config, true); + } + + private async _setConsumer(config: USBDriverConfig | RemoteDriverConfig, joinExistingRoom: boolean): Promise { + // Remove existing consumer if any + if (this.consumer) { + await this.removeConsumer(); + } + + const consumer = this.createConsumer(config); + + // Only pass joinExistingRoom to remote drivers + if (config.type === 'remote') { + await (consumer as any).connect(joinExistingRoom); + } else { + await consumer.connect(); + } + + // Set up command listening + const commandUnsubscribe = consumer.onCommand((command: RobotCommand) => { + this.executeCommand(command); + }); + this.unsubscribeFns.push(commandUnsubscribe); + + // Monitor status changes + const statusUnsubscribe = consumer.onStatusChange(() => { + this.updateStates(); + }); + this.unsubscribeFns.push(statusUnsubscribe); + + // Start listening for consumers with this capability + if ('startListening' in consumer && consumer.startListening) { + await consumer.startListening(); + } + + this.consumer = consumer; + this.updateStates(); + + return consumer.id; + } + + // Producer management (output drivers) - MULTIPLE allowed + async addProducer(config: USBDriverConfig | RemoteDriverConfig): Promise { + return this._addProducer(config, false); + } + + // Join existing room as producer (for AI session integration) + async joinAsProducer(config: RemoteDriverConfig): Promise { + if (config.type !== 'remote') { + throw new Error('joinAsProducer only supports remote drivers'); + } + return this._addProducer(config, true); + } + + private async _addProducer(config: USBDriverConfig | RemoteDriverConfig, joinExistingRoom: boolean): Promise { + const producer = this.createProducer(config); + + // Only pass joinExistingRoom to remote drivers + if (config.type === 'remote') { + await (producer as any).connect(joinExistingRoom); + } else { + await producer.connect(); + } + + // Monitor status changes + const statusUnsubscribe = producer.onStatusChange(() => { + this.updateStates(); + }); + this.unsubscribeFns.push(statusUnsubscribe); + + this.producers.push(producer); + this.updateStates(); + + return producer.id; + } + + async removeConsumer(): Promise { + if (this.consumer) { + // Stop listening for consumers with this capability + if ('stopListening' in this.consumer && this.consumer.stopListening) { + await this.consumer.stopListening(); + } + await this.consumer.disconnect(); + + this.consumer = null; + this.updateStates(); + } + } + + async removeProducer(driverId: string): Promise { + const driverIndex = this.producers.findIndex(d => d.id === driverId); + if (driverIndex >= 0) { + const driver = this.producers[driverIndex]; + await driver.disconnect(); + + this.producers.splice(driverIndex, 1); + this.updateStates(); + } + } + + // Private methods + private createConsumer(config: USBDriverConfig | RemoteDriverConfig): Consumer { + switch (config.type) { + case 'usb': + return new USBConsumer(config, this.usbCalibrationManager); + case 'remote': + return new RemoteConsumer(config); + default: + const _exhaustive: never = config; + throw new Error(`Unknown consumer type: ${JSON.stringify(_exhaustive)}`); + } + } + + private createProducer(config: USBDriverConfig | RemoteDriverConfig): Producer { + switch (config.type) { + case 'usb': + return new USBProducer(config, this.usbCalibrationManager); + case 'remote': + return new RemoteProducer(config); + default: + const _exhaustive: never = config; + throw new Error(`Unknown producer type: ${JSON.stringify(_exhaustive)}`); + } + } + + // Convert normalized values to URDF radians for 3D visualization + convertNormalizedToUrdfRadians(jointName: string, normalizedValue: number): number { + const joint = this.joints[jointName]; + if (!joint?.limits || joint.limits.lower === undefined || joint.limits.upper === undefined) { + // Default ranges + if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') { + return (normalizedValue / 100) * Math.PI; + } else { + return (normalizedValue / 100) * Math.PI; + } + } + + const { lower, upper } = joint.limits; + + // Map normalized value to URDF range + let normalizedRatio: number; + if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') { + normalizedRatio = normalizedValue / 100; // 0-100 -> 0-1 + } else { + normalizedRatio = (normalizedValue + 100) / 200; // -100-+100 -> 0-1 + } + + const urdfRadians = lower + normalizedRatio * (upper - lower); + + console.debug(`[Robot ${this.id}] Joint ${jointName}: ${normalizedValue} (norm) -> ${urdfRadians.toFixed(3)} (rad)`); + + return urdfRadians; + } + + private async sendToProducers(command: RobotCommand): Promise { + const connectedProducers = this.producers.filter(d => d.status.isConnected); + + console.debug(`[Robot ${this.id}] Sending command to ${connectedProducers.length} producers:`, command); + + // Send to all connected producers + await Promise.all( + connectedProducers.map(async (producer) => { + try { + await producer.sendCommand(command); + } catch (error) { + console.error(`[Robot ${this.id}] Failed to send command to producer ${producer.id}:`, error); + } + }) + ); + } + + private updateStates(): void { + // Update connection status + const hasConnectedDrivers = (this.consumer?.status.isConnected) || + this.producers.some(d => d.status.isConnected); + + this.connectionStatus = { + isConnected: hasConnectedDrivers, + lastConnected: hasConnectedDrivers ? new Date() : this.connectionStatus.lastConnected + }; + + // Manual control is enabled when no connected consumer + this.isManualControlEnabled = !this.consumer?.status.isConnected; + } + + // Cleanup + async destroy(): Promise { + // Unsubscribe from all callbacks + this.unsubscribeFns.forEach(fn => fn()); + this.unsubscribeFns = []; + + // Disconnect all drivers + const allDrivers = [this.consumer, ...this.producers].filter(Boolean) as (Consumer | Producer)[]; + await Promise.allSettled( + allDrivers.map(async (driver) => { + try { + await driver.disconnect(); + } catch (error) { + console.error(`Error disconnecting driver ${driver.id}:`, error); + } + }) + ); + + // Clean up calibration manager + await this.usbCalibrationManager.destroy(); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/RobotManager.svelte.ts b/src/lib/elements/robot/RobotManager.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..af0ada898351a842439468ad26ecb237d742f357 --- /dev/null +++ b/src/lib/elements/robot/RobotManager.svelte.ts @@ -0,0 +1,263 @@ +import { Robot } from './Robot.svelte.js'; +import type { JointState, USBDriverConfig, RemoteDriverConfig } from './models.js'; +import type { Position3D } from '$lib/types/positionable.js'; +import { createUrdfRobot } from '@/elements/robot/createRobot.svelte.js'; +import type { RobotUrdfConfig } from '$lib/types/urdf.js'; +import { generateName } from '$lib/utils/generateName.js'; +import { positionManager } from '$lib/utils/positionManager.js'; +import { settings } from '$lib/runes/settings.svelte'; +import { robotics } from '@robohub/transport-server-client'; +import type { robotics as roboticsTypes } from '@robohub/transport-server-client'; + +export class RobotManager { + private _robots = $state([]); + + // Room management state - using transport server for communication + rooms = $state([]); + roomsLoading = $state(false); + + // Reactive getters + get robots(): Robot[] { + return this._robots; + } + + get robotCount(): number { + return this._robots.length; + } + + /** + * Room Management Methods + */ + async listRooms(workspaceId: string): Promise { + try { + const client = new robotics.RoboticsClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + this.rooms = rooms; + return rooms; + } catch (error) { + console.error('Failed to list robotics rooms:', error); + return []; + } + } + + async refreshRooms(workspaceId: string): Promise { + this.roomsLoading = true; + try { + await this.listRooms(workspaceId); + } finally { + this.roomsLoading = false; + } + } + + async createRoboticsRoom(workspaceId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + try { + const client = new robotics.RoboticsClientCore(settings.transportServerUrl); + const result = await client.createRoom(workspaceId, roomId); + // Refresh rooms list to include the new room + await this.refreshRooms(workspaceId); + return { success: true, roomId: result.roomId }; + } catch (error) { + console.error('Failed to create robotics room:', error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + } + + generateRoomId(robotId: string): string { + return `${robotId}-${generateName()}`; + } + + /** + * Connect consumer to an existing robotics room as consumer + * This will receive commands from producers in that room + */ + async connectConsumerToRoom(workspaceId: string, robotId: string, roomId: string): Promise { + const robot = this.getRobot(robotId); + if (!robot) { + throw new Error(`Robot ${robotId} not found`); + } + + const config: RemoteDriverConfig = { + type: 'remote', + url: settings.transportServerUrl.replace('http://', 'ws://').replace('https://', 'wss://'), + robotId: roomId, + workspaceId: workspaceId + }; + + // Use joinAsConsumer to join existing room + await robot.joinAsConsumer(config); + } + + /** + * Connect producer to an existing robotics room as producer + * This will send commands to consumers in that room + */ + async connectProducerToRoom(workspaceId: string, robotId: string, roomId: string): Promise { + const robot = this.getRobot(robotId); + if (!robot) { + throw new Error(`Robot ${robotId} not found`); + } + + const config: RemoteDriverConfig = { + type: 'remote', + url: settings.transportServerUrl.replace('http://', 'ws://').replace('https://', 'wss://'), + robotId: roomId, + workspaceId: workspaceId + }; + + // Use joinAsProducer to join existing room + await robot.joinAsProducer(config); + } + + /** + * Create and connect producer as producer to a new room + */ + async connectProducerAsProducer(workspaceId: string, robotId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + try { + // Create room first if roomId provided, otherwise generate one + const finalRoomId = roomId || this.generateRoomId(robotId); + const createResult = await this.createRoboticsRoom(workspaceId, finalRoomId); + + if (!createResult.success) { + return createResult; + } + + // Connect producer to the new room + await this.connectProducerToRoom(workspaceId, robotId, createResult.roomId!); + + return { success: true, roomId: createResult.roomId }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + } + + /** + * Create a robot with the default SO-100 arm configuration + */ + async createSO100Robot(id?: string, position?: Position3D): Promise { + const robotId = id || `so100-${Date.now()}`; + const urdfConfig: RobotUrdfConfig = { + urdfUrl: "/robots/so-100/so_arm100.urdf" + }; + + return this.createRobotFromUrdf(robotId, urdfConfig, position); + } + + /** + * Create a new robot directly from URDF configuration - automatically extracts joint limits + */ + async createRobotFromUrdf(id: string, urdfConfig: RobotUrdfConfig, position?: Position3D): Promise { + // Check if robot already exists + if (this._robots.find(r => r.id === id)) { + throw new Error(`Robot with ID ${id} already exists`); + } + + try { + // Load and parse URDF + const robotState = await createUrdfRobot(urdfConfig); + + // Extract joint information from URDF + const joints: JointState[] = []; + let servoId = 1; // Auto-assign servo IDs in order + + for (const urdfJoint of robotState.urdfRobot.joints) { + // Only include revolute joints (movable joints) + if (urdfJoint.type === 'revolute' && urdfJoint.name) { + const jointState: JointState = { + name: urdfJoint.name, + value: 0, // Start at center (0%) + servoId: servoId++ + }; + + // Extract limits from URDF if available + if (urdfJoint.limit) { + jointState.limits = { + lower: urdfJoint.limit.lower, + upper: urdfJoint.limit.upper + }; + } + + joints.push(jointState); + } + } + + console.log(`Extracted ${joints.length} joints from URDF:`, joints.map(j => `${j.name} [${j.limits?.lower?.toFixed(2)}:${j.limits?.upper?.toFixed(2)}]`)); + + // Create robot with extracted joints AND URDF robot state + const robot = new Robot(id, joints, robotState.urdfRobot); + + // Set position (from position manager if not provided) + robot.position = position || positionManager.getNextPosition(); + + // Add to reactive array + this._robots.push(robot); + + console.log(`Created robot ${id} from URDF. Total robots: ${this._robots.length}`); + return robot; + + } catch (error) { + console.error(`Failed to create robot ${id} from URDF:`, error); + throw error; + } + } + + /** + * Create a new robot with joints defined at initialization (for backwards compatibility) + */ + createRobot(id: string, joints: JointState[], position?: Position3D): Robot { + // Check if robot already exists + if (this._robots.find(r => r.id === id)) { + throw new Error(`Robot with ID ${id} already exists`); + } + + // Create robot + const robot = new Robot(id, joints); + + // Set position (from position manager if not provided) + robot.position = position || positionManager.getNextPosition(); + + // Add to reactive array + this._robots.push(robot); + + console.log(`Created robot ${id}. Total robots: ${this._robots.length}`); + return robot; + } + + /** + * Remove a robot + */ + async removeRobot(id: string): Promise { + const robotIndex = this._robots.findIndex(r => r.id === id); + if (robotIndex === -1) return; + + const robot = this._robots[robotIndex]; + + // Clean up robot resources + await robot.destroy(); + + // Remove from reactive array + this._robots.splice(robotIndex, 1); + + console.log(`Removed robot ${id}. Remaining robots: ${this._robots.length}`); + } + + /** + * Get robot by ID + */ + getRobot(id: string): Robot | undefined { + return this._robots.find(r => r.id === id); + } + + + + /** + * Clean up all robots + */ + async destroy(): Promise { + const cleanupPromises = this._robots.map(robot => robot.destroy()); + await Promise.allSettled(cleanupPromises); + this._robots.length = 0; + } +} + +// Global robot manager instance +export const robotManager = new RobotManager(); \ No newline at end of file diff --git a/src/lib/elements/robot/calibration/CalibrationState.svelte.ts b/src/lib/elements/robot/calibration/CalibrationState.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5859b11b937e70026b764094304060604223115 --- /dev/null +++ b/src/lib/elements/robot/calibration/CalibrationState.svelte.ts @@ -0,0 +1,114 @@ +import type { USBCalibrationManager } from './USBCalibrationManager.js'; +import type { JointCalibration } from '../models.js'; +import { ROBOT_CONFIG } from '../config.js'; + +export class CalibrationState { + private manager: USBCalibrationManager; + + // Reactive state using Svelte 5 runes - direct state management + private _isCalibrating = $state(false); + private _progress = $state(0); + private _isCalibrated = $state(false); + private _needsCalibration = $state(true); + private _jointValues = $state>({}); + private _jointCalibrations = $state>({}); + + constructor(manager: USBCalibrationManager) { + this.manager = manager; + + // Initialize reactive state + manager.jointNames_.forEach(name => { + this._jointValues[name] = 0; + this._jointCalibrations[name] = { isCalibrated: false }; + }); + + // Subscribe to manager changes + this.setupManagerSubscription(); + + // Initial state sync + this.syncManagerState(); + } + + // Reactive getters - now use internal reactive state + get isCalibrating(): boolean { return this._isCalibrating; } + get progress(): number { return this._progress; } + get isCalibrated(): boolean { return this._isCalibrated; } + get needsCalibration(): boolean { return this._needsCalibration; } + get jointValues(): Record { return this._jointValues; } + get jointCalibrations(): Record { return this._jointCalibrations; } + + // Get current value for a specific joint + getCurrentValue(jointName: string): number | undefined { + return this._jointValues[jointName]; + } + + // Get calibration for a specific joint + getJointCalibration(jointName: string): JointCalibration | undefined { + return this._jointCalibrations[jointName]; + } + + // Get range for a specific joint + getJointRange(jointName: string): number { + const calibration = this._jointCalibrations[jointName]; + if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0; + return Math.abs(calibration.maxServoValue - calibration.minServoValue); + } + + private updateInterval: number | null = null; + private managerUnsubscribe: (() => void) | null = null; + + private setupManagerSubscription(): void { + // Use centralized config for UI update frequency + this.updateInterval = setInterval(() => { + this.syncManagerState(); + }, ROBOT_CONFIG.polling.uiUpdateRate); // Centralized UI update rate + + // Also listen to manager calibration changes for immediate updates + const unsubscribe = this.manager.onCalibrationChange(() => { + console.debug('[CalibrationState] Manager calibration changed, syncing state'); + this.syncManagerState(); + }); + + // Store unsubscribe function for cleanup + this.managerUnsubscribe = unsubscribe; + } + + private syncManagerState(): void { + // Sync manager state to reactive state + this._isCalibrating = this.manager.calibrationState.isCalibrating; + this._progress = this.manager.calibrationState.progress; + this._isCalibrated = this.manager.isCalibrated; + this._needsCalibration = this.manager.needsCalibration; + + // Update joint values and calibrations + this.manager.jointNames_.forEach(name => { + const currentValue = this.manager.getCurrentRawValue(name); + if (currentValue !== undefined) { + this._jointValues[name] = currentValue; + } + + const calibration = this.manager.getJointCalibration(name); + if (calibration) { + // Create new object to ensure reactivity + this._jointCalibrations[name] = { ...calibration }; + } + }); + } + + // Cleanup method + destroy(): void { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + if (this.managerUnsubscribe) { + this.managerUnsubscribe(); + this.managerUnsubscribe = null; + } + } + + // Format servo value for display + formatServoValue(value: number | undefined): string { + return value !== undefined ? value.toString() : '---'; + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/calibration/USBCalibrationManager.ts b/src/lib/elements/robot/calibration/USBCalibrationManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..a858fc51099ee67c22cc1158cf8704853346ce7f --- /dev/null +++ b/src/lib/elements/robot/calibration/USBCalibrationManager.ts @@ -0,0 +1,544 @@ +import type { JointCalibration, CalibrationState } from '../models.js'; +import { scsServoSDK } from "feetech.js"; +import { ROBOT_CONFIG } from '../config.js'; + +export class USBCalibrationManager { + // Joint configuration + private readonly jointIds = [1, 2, 3, 4, 5, 6]; + private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; + + // Calibration state + private jointCalibrations: Record = {}; + private _calibrationState: CalibrationState = { + isCalibrating: false, + progress: 0 + }; + + // Connection state for calibration + private isConnectedForCalibration = false; + private baudRate: number = 1000000; + + // Calibration polling + private calibrationPollingAbortController: AbortController | null = null; + private lastPositions: Record = {}; + private calibrationCallbacks: (() => void)[] = []; + + // Calibration completion callback with final positions + private calibrationCompleteCallback: ((finalPositions: Record) => void) | null = null; + + // Servo reading queue for calibration + private isReadingServos = false; + private readingQueue: Array<{ + servoId: number; + resolve: (value: number) => void; + reject: (error: Error) => void; + }> = []; + + constructor(baudRate: number = ROBOT_CONFIG.usb.baudRate) { + this.baudRate = baudRate; + + // Initialize joint calibrations + this.jointNames.forEach(name => { + this.jointCalibrations[name] = { isCalibrated: false }; + }); + } + + // Getters + get isCalibrated(): boolean { + return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated); + } + + get needsCalibration(): boolean { + return !this.isCalibrated; + } + + get calibrationState(): CalibrationState { + return this._calibrationState; + } + + get jointNames_(): string[] { + return [...this.jointNames]; + } + + // Connection management for calibration + async ensureConnectedForCalibration(): Promise { + if (this.isConnectedForCalibration) { + console.log('[USBCalibrationManager] Already connected for calibration'); + return; + } + + try { + console.log('[USBCalibrationManager] Connecting SDK for calibration...'); + await scsServoSDK.connect({ baudRate: this.baudRate }); + this.isConnectedForCalibration = true; + console.log('[USBCalibrationManager] Connected successfully for calibration'); + } catch (error) { + console.error('[USBCalibrationManager] Failed to connect SDK for calibration:', error); + throw error; + } + } + + async disconnectFromCalibration(): Promise { + if (!this.isConnectedForCalibration) return; + + try { + await scsServoSDK.disconnect(); + this.isConnectedForCalibration = false; + console.log('[USBCalibrationManager] Disconnected from calibration'); + } catch (error) { + console.warn('[USBCalibrationManager] Failed to disconnect from calibration:', error); + } + } + + // Check if the SDK is currently connected (for external use) + get isSDKConnected(): boolean { + return this.isConnectedForCalibration && scsServoSDK.isConnected(); + } + + // Calibration methods + async startCalibration(): Promise { + if (this._calibrationState.isCalibrating) { + console.warn('[USBCalibrationManager] Calibration already in progress'); + return; + } + + console.log('[USBCalibrationManager] Starting calibration process'); + + // Ensure connection for calibration + await this.ensureConnectedForCalibration(); + + // Unlock all servos for calibration (allow manual movement) + console.log('[USBCalibrationManager] 🔓 Unlocking all servos for calibration...'); + try { + await scsServoSDK.unlockServos(this.jointIds); + console.log('[USBCalibrationManager] ✅ All servos unlocked for manual movement during calibration'); + } catch (error) { + console.warn('[USBCalibrationManager] Warning: Failed to unlock some servos for calibration:', error); + } + + this._calibrationState = { + isCalibrating: true, + progress: 0 + }; + + // Initialize calibrations with current values + this.jointCalibrations = {}; + this.jointNames.forEach(name => { + const currentValue = this.lastPositions[name] || 2048; + this.jointCalibrations[name] = { + isCalibrated: false, + minServoValue: currentValue, + maxServoValue: currentValue + }; + }); + + this.startCalibrationPolling(); + this.notifyCalibrationChange(); + } + + async stopCalibration(): Promise { + console.log('[USBCalibrationManager] Stopping calibration'); + + this._calibrationState = { + isCalibrating: false, + progress: 100 + }; + + // Mark all joints as calibrated + this.jointNames.forEach(name => { + if (this.jointCalibrations[name]) { + this.jointCalibrations[name].isCalibrated = true; + } + }); + + this.stopCalibrationPolling(); + + // NEW: Read final positions and sync to virtual robot before locking + console.log('[USBCalibrationManager] 📍 Reading final servo positions for virtual robot sync...'); + try { + const finalPositions = await this.readFinalPositionsAndSync(); + console.log('[USBCalibrationManager] ✅ Final positions read and synced to virtual robot'); + + // Notify robot of calibration completion with final positions + if (this.calibrationCompleteCallback) { + this.calibrationCompleteCallback(finalPositions); + } + } catch (error) { + console.error('[USBCalibrationManager] Failed to read final positions:', error); + } + + this.notifyCalibrationChange(); + + // Keep connection open - don't disconnect automatically + // The connection will be reused by USB drivers + console.log('[USBCalibrationManager] Calibration complete, keeping connection for drivers'); + } + + skipCalibration(): void { + console.log('[USBCalibrationManager] Skipping calibration, using full range'); + + // Set full range for all joints + this.jointNames.forEach(name => { + this.jointCalibrations[name] = { + isCalibrated: true, + minServoValue: 0, + maxServoValue: 4095 + }; + }); + + this._calibrationState = { + isCalibrating: false, + progress: 100 + }; + + this.notifyCalibrationChange(); + } + + // NEW: Set predefined calibration values + async setPredefinedCalibration(): Promise { + console.log('[USBCalibrationManager] Setting predefined calibration values'); + + // Ensure SDK connection for hardware access + await this.ensureConnectedForCalibration(); + + // Predefined calibration values based on known good robot configuration + const predefinedValues: Record = { + "Rotation": { current: 2180, min: 764, max: 3388 }, + "Pitch": { current: 1159, min: 1138, max: 3501 }, + "Elbow": { current: 2874, min: 660, max: 2876 }, + "Wrist_Pitch": { current: 2138, min: 762, max: 3075 }, + "Wrist_Roll": { current: 2081, min: 154, max: 3995 }, + "Jaw": { current: 2061, min: 2013, max: 3555 } + }; + + // Set calibration values for all joints + this.jointNames.forEach(name => { + const values = predefinedValues[name]; + if (values) { + this.jointCalibrations[name] = { + isCalibrated: true, + minServoValue: values.min, + maxServoValue: values.max + }; + // Set current position for reference + this.lastPositions[name] = values.current; + } + }); + + this._calibrationState = { + isCalibrating: false, + progress: 100 + }; + + this.notifyCalibrationChange(); + console.log('[USBCalibrationManager] Predefined calibration values applied successfully'); + } + + // NEW: Read final positions and prepare for sync + private async readFinalPositionsAndSync(): Promise> { + const finalPositions: Record = {}; + + console.log('[USBCalibrationManager] Reading final positions from all servos...'); + + // Read all servo positions sequentially + for (let i = 0; i < this.jointIds.length; i++) { + const servoId = this.jointIds[i]; + const jointName = this.jointNames[i]; + + try { + const position = await this.readServoPosition(servoId); + finalPositions[jointName] = position; + this.lastPositions[jointName] = position; + + console.log(`[USBCalibrationManager] ${jointName} (servo ${servoId}): ${position} (raw) -> ${this.normalizeValue(position, jointName).toFixed(1)}% (normalized)`); + } catch (error) { + console.warn(`[USBCalibrationManager] Failed to read final position for ${jointName} (servo ${servoId}):`, error); + // Use last known position as fallback + finalPositions[jointName] = this.lastPositions[jointName] || 2048; + } + } + + return finalPositions; + } + + // NEW: Set callback for calibration completion with final positions + onCalibrationCompleteWithPositions(callback: (finalPositions: Record) => void): () => void { + this.calibrationCompleteCallback = callback; + return () => { + this.calibrationCompleteCallback = null; + }; + } + + // Post-calibration servo locking methods + async lockServosForProduction(): Promise { + if (!this.isSDKConnected) { + throw new Error('SDK not connected - cannot lock servos'); + } + + console.log('[USBCalibrationManager] 🔒 Locking all servos for production use (robot control)...'); + try { + // Use the new lockServosForProduction function that both locks and enables torque + await scsServoSDK.lockServosForProduction(this.jointIds); + console.log('[USBCalibrationManager] ✅ All servos locked for production - robot is now controlled and cannot be moved manually'); + } catch (error) { + console.error('[USBCalibrationManager] Failed to lock servos for production:', error); + throw error; + } + } + + async keepServosUnlockedForConsumer(): Promise { + if (!this.isSDKConnected) { + console.log('[USBCalibrationManager] SDK not connected - servos remain in current state'); + return; + } + + console.log('[USBCalibrationManager] 🔓 Keeping servos unlocked for consumer use (reading positions)...'); + try { + // Ensure servos are unlocked for reading + await scsServoSDK.unlockServos(this.jointIds); + console.log('[USBCalibrationManager] ✅ All servos remain unlocked for consumer - can be moved manually and positions read'); + } catch (error) { + console.warn('[USBCalibrationManager] Warning: Failed to ensure servos are unlocked for consumer:', error); + } + } + + // Data access methods + getCurrentRawValue(jointName: string): number | undefined { + return this.lastPositions[jointName]; + } + + getJointCalibration(jointName: string): JointCalibration | undefined { + return this.jointCalibrations[jointName]; + } + + getJointRange(jointName: string): number { + const calibration = this.jointCalibrations[jointName]; + if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0; + return Math.abs(calibration.maxServoValue - calibration.minServoValue); + } + + getAllCalibrations(): Record { + return { ...this.jointCalibrations }; + } + + // Value conversion methods + normalizeValue(servoValue: number, jointName: string): number { + const calibration = this.jointCalibrations[jointName]; + const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; + + if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) { + if (isGripper) { + return (servoValue / 4095) * 100; + } else { + return ((servoValue - 2048) / 2048) * 100; + } + } + + const { minServoValue, maxServoValue } = calibration; + if (maxServoValue === minServoValue) return 0; + + const bounded = Math.max(minServoValue, Math.min(maxServoValue, servoValue)); + + if (isGripper) { + return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100; + } else { + return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100; + } + } + + denormalizeValue(normalizedValue: number, jointName: string): number { + const calibration = this.jointCalibrations[jointName]; + const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper'; + + if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) { + // No calibration, use appropriate default conversion + if (isGripper) { + return Math.round((normalizedValue / 100) * 4095); + } else { + return Math.round(2048 + (normalizedValue / 100) * 2048); + } + } + + const { minServoValue, maxServoValue } = calibration; + let ratio: number; + + if (isGripper) { + ratio = normalizedValue / 100; + } else { + ratio = (normalizedValue + 100) / 200; + } + + const servoValue = minServoValue + ratio * (maxServoValue - minServoValue); + return Math.round(Math.max(minServoValue, Math.min(maxServoValue, servoValue))); + } + + // Event handling + onCalibrationChange(callback: () => void): () => void { + this.calibrationCallbacks.push(callback); + return () => { + const index = this.calibrationCallbacks.indexOf(callback); + if (index >= 0) { + this.calibrationCallbacks.splice(index, 1); + } + }; + } + + // Format servo value for display + formatServoValue(value: number | undefined): string { + return value !== undefined ? value.toString() : '---'; + } + + // Cleanup + async destroy(): Promise { + console.log('[USBCalibrationManager] 🧹 Destroying calibration manager...'); + + this.stopCalibrationPolling(); + + // Safely unlock all servos before disconnecting (best practice) + if (this.isSDKConnected) { + try { + console.log('[USBCalibrationManager] 🔓 Safely unlocking all servos before cleanup...'); + await scsServoSDK.unlockServosForManualMovement(this.jointIds); + console.log('[USBCalibrationManager] ✅ All servos safely unlocked for manual movement'); + } catch (error) { + console.warn('[USBCalibrationManager] Warning: Failed to safely unlock servos during cleanup:', error); + } + } + + await this.disconnectFromCalibration(); + this.calibrationCallbacks = []; + this.calibrationCompleteCallback = null; + + console.log('[USBCalibrationManager] ✅ Calibration manager destroyed'); + } + + // Private methods + private async readServoPosition(servoId: number): Promise { + return new Promise((resolve, reject) => { + this.readingQueue.push({ servoId, resolve, reject }); + this.processReadingQueue(); + }); + } + + private async processReadingQueue(): Promise { + if (this.isReadingServos || this.readingQueue.length === 0) { + return; + } + + this.isReadingServos = true; + + try { + const batch = [...this.readingQueue]; + this.readingQueue = []; + + for (const { servoId, resolve, reject } of batch) { + try { + const position = await scsServoSDK.readPosition(servoId); + resolve(position); + } catch (error) { + reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`)); + } + + await new Promise(resolve => setTimeout(resolve, 5)); + } + } finally { + this.isReadingServos = false; + + if (this.readingQueue.length > 0) { + setTimeout(() => this.processReadingQueue(), 50); + } + } + } + + private async startCalibrationPolling(): Promise { + this.stopCalibrationPolling(); + + this.calibrationPollingAbortController = new AbortController(); + const signal = this.calibrationPollingAbortController.signal; + + console.log('[USBCalibrationManager] Starting calibration polling'); + + try { + while (!signal.aborted && this._calibrationState.isCalibrating) { + const readPromises = this.jointIds.map(async (servoId, i) => { + if (signal.aborted) return null; + + const jointName = this.jointNames[i]; + + try { + const currentValue = await this.readServoPosition(servoId); + return { jointName, currentValue }; + } catch (error) { + return null; + } + }); + + const results = await Promise.all(readPromises); + let hasUpdates = false; + + results.forEach(result => { + if (!result) return; + + const { jointName, currentValue } = result; + this.lastPositions[jointName] = currentValue; + + const calibration = this.jointCalibrations[jointName]; + if (calibration) { + if (currentValue < calibration.minServoValue!) { + calibration.minServoValue = currentValue; + hasUpdates = true; + } + if (currentValue > calibration.maxServoValue!) { + calibration.maxServoValue = currentValue; + hasUpdates = true; + } + } + }); + + if (hasUpdates) { + this.notifyCalibrationChange(); + } + + // Calculate progress + const totalRangeNeeded = 500; + let totalRangeDiscovered = 0; + + this.jointNames.forEach(name => { + const calibration = this.jointCalibrations[name]; + if (calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined) { + totalRangeDiscovered += Math.abs(calibration.maxServoValue - calibration.minServoValue); + } + }); + + const newProgress = Math.min(100, (totalRangeDiscovered / (totalRangeNeeded * this.jointNames.length)) * 100); + if (Math.abs(newProgress - this._calibrationState.progress) > 1) { + this._calibrationState.progress = newProgress; + this.notifyCalibrationChange(); + } + + await new Promise(resolve => setTimeout(resolve, 10)); + } + } catch (error) { + if (!signal.aborted) { + console.error('[USBCalibrationManager] Calibration polling error:', error); + } + } + } + + private stopCalibrationPolling(): void { + if (this.calibrationPollingAbortController) { + this.calibrationPollingAbortController.abort(); + this.calibrationPollingAbortController = null; + } + } + + private notifyCalibrationChange(): void { + this.calibrationCallbacks.forEach(callback => { + try { + callback(); + } catch (error) { + console.error('[USBCalibrationManager] Error in calibration callback:', error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/calibration/USBCalibrationPanel.svelte b/src/lib/elements/robot/calibration/USBCalibrationPanel.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4d4d97c30c8eb448f2eaa17de4145069cd60a77c --- /dev/null +++ b/src/lib/elements/robot/calibration/USBCalibrationPanel.svelte @@ -0,0 +1,217 @@ + + + +
    + {#if isCalibrating} + +
    +
    +
    +
    + Recording movements... + {Math.round(progress)}% +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + {#each jointNames as jointName} + {@const currentValue = calibrationState.getCurrentValue(jointName)} + {@const calibration = calibrationState.getJointCalibration(jointName)} + +
    +
    + {jointName} + {calibrationState.formatServoValue(currentValue)} +
    + +
    + Min: {calibrationState.formatServoValue(calibration?.minServoValue)} + Max: {calibrationState.formatServoValue(calibration?.maxServoValue)} +
    + + {#if calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined && currentValue !== undefined} +
    +
    +
    + {/if} +
    + {/each} +
    +
    + +
    + Move each joint through its full range of motion +
    +
    + + {:else if isCalibrated} + +
    +
    + ✓ Calibrated + Ready to connect +
    + + +
    +
    + {connectionInfo.lockStatus} +
    +
    {connectionInfo.lockDescription}
    +
    + + +
    +
    + {#each jointNames as jointName} + {@const calibration = calibrationState.getJointCalibration(jointName)} + {@const range = calibrationState.getJointRange(jointName)} + +
    + {jointName} + {range} +
    + {/each} +
    +
    + +
    + + +
    +
    + + {:else} + +
    +
    + Needs Calibration + Required for USB connection +
    + + +
    +
    Quick Setup Options:
    +
      +
    1. 1. Position robot in neutral pose
    2. +
    3. 2. Start calibration and move each joint fully
    4. +
    5. 3. Complete when all joints show good ranges
    6. +
    +
    + + +
    +
    After calibration:
    +
    {connectionInfo.lockStatus}
    +
    + +
    + + + +
    +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/elements/robot/calibration/index.ts b/src/lib/elements/robot/calibration/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c951cf806e0ab171ac4ab6c028a2c4e3135f5e6 --- /dev/null +++ b/src/lib/elements/robot/calibration/index.ts @@ -0,0 +1,4 @@ +// Robot calibration exports +export { USBCalibrationManager } from './USBCalibrationManager.js'; +export { CalibrationState } from './CalibrationState.svelte.js'; +export { default as USBCalibrationPanel } from './USBCalibrationPanel.svelte'; \ No newline at end of file diff --git a/src/lib/elements/robot/components/ConnectionPanel.svelte b/src/lib/elements/robot/components/ConnectionPanel.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4b4faa8771e055b4ae9f0841fc3e6777d7e92fe4 --- /dev/null +++ b/src/lib/elements/robot/components/ConnectionPanel.svelte @@ -0,0 +1,541 @@ + + +
    + +
    +

    Connections - {robot.id}

    + + + {#if error} +
    + {error} +
    + {/if} + + +
    +
    +

    Room Management

    + +
    + + {#if showRoomManagement} +
    + +
    + + + {rooms.length} room{rooms.length !== 1 ? 's' : ''} available + +
    + + +
    + Available Rooms: +
    + +
    +
    +
    + Create New Room +
    +

    + Create a room for collaboration +

    + +
    + + +
    +
    +
    + + + {#if rooms.length === 0} +
    + {roomsLoading ? 'Loading...' : 'No existing rooms available'} +
    + {:else} + {#each rooms as room} +
    +
    +

    {room.id}

    +

    {room.participants?.total || 0} participants

    +
    +
    + + +
    +
    + {/each} + {/if} +
    +
    +
    + {/if} +
    + + +
    +

    Consumer (Receive Commands) - Single

    + {#if hasConsumer} +
    +
    + {consumer?.name || 'Consumer Active'} + + {consumer?.status.isConnected ? '🟢 Connected' : '🔴 Disconnected'} + +
    + +
    + {:else} +
    + +
    +
    + + +
    +
    + Remote Consumer: Receive commands from transport server +
    +
    +
    + {/if} +
    + + +
    +

    Producers (Send Commands) - {outputDriverCount} connected

    +
    + + +
    + Remote Producer: Send commands to transport server. Uses Robot ID: {remoteRobotId} +
    +
    + + + {#each producers as producer} +
    +
    + {producer.name} + + {producer.status.isConnected ? '🟢 Connected' : '🔴 Disconnected'} + +
    + +
    + {/each} +
    + + +
    + Robot ID for Remote Connections: + +
    +
    + + + {#if showUSBCalibration} +
    +
    +
    +

    + USB Calibration Required + {#if pendingUSBConnection} + + (for {pendingUSBConnection === 'consumer' ? 'Consumer' : 'Producer'}) + + {/if} +

    + +
    + +
    + Before connecting USB drivers, the robot needs to be calibrated to map its physical range to software values. +
    + + +
    +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/elements/robot/components/RobotControls.svelte b/src/lib/elements/robot/components/RobotControls.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3a841237943700a0336ae0465adef333f2dd1f15 --- /dev/null +++ b/src/lib/elements/robot/components/RobotControls.svelte @@ -0,0 +1,81 @@ + + +
    +
    +

    + Robot Controls - {robot.id} +

    +
    + {#if isManualControlEnabled} + Manual Control + {:else} + External Control + {/if} +
    +
    + +
    + {#each joints as joint} +
    +
    + {joint.name} + + {joint.value.toFixed(1)}% + +
    + +
    + {#if joint.name.toLowerCase() === 'jaw' || joint.name.toLowerCase() === 'gripper'} + 0% (closed) + updateJoint(joint.name, parseFloat(e.currentTarget.value))} + class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" + /> + 100% (open) + {:else} + -100% + updateJoint(joint.name, parseFloat(e.currentTarget.value))} + class="flex-1 h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" + /> + +100% + {/if} +
    + + {#if joint.limits} +
    + URDF limits: {(joint.limits.lower)}° to {joint.limits.upper}° +
    + {/if} +
    + {/each} +
    +
    \ No newline at end of file diff --git a/src/lib/elements/robot/components/RobotGrid.svelte b/src/lib/elements/robot/components/RobotGrid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc0efa3b0267cdcf9e867a78d5012f25fb1bbf3d --- /dev/null +++ b/src/lib/elements/robot/components/RobotGrid.svelte @@ -0,0 +1,106 @@ + + + + {#each robots as robot (robot.id)} + + {/each} + + + +{#if showConnectionModal && selectedRobot} +
    +
    +
    +

    + {modalType === 'consumer' ? 'Consumer Driver' : modalType === 'producer' ? 'Producer Drivers' : 'Manual Control'} +

    + +
    + +
    + {#if modalType === 'consumer'} + + + {:else if modalType === 'producer'} + + + {:else} +

    Manual control interface would go here

    + {/if} +
    + +
    + {#if modalType !== 'manual'} + Note: USB connections will prompt for calibration if needed + {/if} +
    +
    +
    +{/if} \ No newline at end of file diff --git a/src/lib/elements/robot/components/RobotItem.svelte b/src/lib/elements/robot/components/RobotItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ea9c87f7dea8a714d82d8f7adabd81a53d479a36 --- /dev/null +++ b/src/lib/elements/robot/components/RobotItem.svelte @@ -0,0 +1,206 @@ + + + + {#if urdfRobotState} + + ) => { + event.stopPropagation(); + isSelected = true; + onInteract(robot, 'manual'); + }} + onpointerenter={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerEnter(); + isHovered = true; + }} + onpointerleave={(event: IntersectionEvent) => { + event.stopPropagation(); + onPointerLeave(); + isHovered = false; + }} + > + {#each getRootLinks(urdfRobotState) as link} + + {/each} + + {:else} + + isHovered = true} + onpointerleave={() => isHovered = false} + onclick={() => onInteract(robot, 'manual')} + > + + + + {/if} + + + {#if isHovered} + + +
    +
    {robot.id}
    + + +
    + + + + +
    +
    Robot
    +
    {robot.jointArray.length} joints
    +
    + + + +
    + + +
    + {isManualControl ? 'Manual Control' : 'External Control'} +
    +
    +
    +
    + {/if} +
    \ No newline at end of file diff --git a/src/lib/elements/robot/components/index.ts b/src/lib/elements/robot/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..126e4b8007e234fd294d1d4ddc412b8ec1057670 --- /dev/null +++ b/src/lib/elements/robot/components/index.ts @@ -0,0 +1,5 @@ +// Robot components exports +export { default as RobotItem } from './RobotItem.svelte'; +export { default as RobotGrid } from './RobotGrid.svelte'; +export { default as ConnectionPanel } from './ConnectionPanel.svelte'; +export { default as RobotControls } from './RobotControls.svelte'; \ No newline at end of file diff --git a/src/lib/elements/robot/config.ts b/src/lib/elements/robot/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a0b623d377215d6b9653cc04eb626348ef20eae --- /dev/null +++ b/src/lib/elements/robot/config.ts @@ -0,0 +1,55 @@ +// Centralized Robot Communication & Performance Configuration +// Single source of truth for all timing and communication parameters + +export const ROBOT_CONFIG = { + // USB Communication Settings + usb: { + baudRate: 1000000, + servoWriteDelay: 8, // ms between servo writes (optimized from 10ms) + maxRetries: 3, // max retry attempts for failed operations + retryDelay: 100, // ms between retries + connectionTimeout: 5000, // ms for connection timeout + readTimeout: 200, // ms for individual servo reads + }, + + // Polling & Update Frequencies + polling: { + uiUpdateRate: 100, // ms (10Hz) - UI state updates + consumerPollingRate: 40, // ms (25Hz) - USB consumer polling (optimized from 50ms) + calibrationPollingRate: 16, // ms (60Hz) - calibration polling (needs to be fast) + errorBackoffRate: 200, // ms - delay after polling errors + maxPollingErrors: 5, // max consecutive errors before longer backoff + }, + + // Command Processing + commands: { + dedupWindow: 16, // ms - skip duplicate commands within this window + maxQueueSize: 50, // max pending commands before dropping old ones + batchSize: 6, // max servos to process in parallel batches + }, + + // Remote Connection Settings + remote: { + reconnectDelay: 2000, // ms between reconnection attempts + heartbeatInterval: 30000, // ms for connection health check + messageTimeout: 5000, // ms for message response timeout + }, + + // Calibration Settings + calibration: { + minRangeThreshold: 500, // minimum servo range for valid calibration + progressUpdateRate: 100, // ms between progress updates + finalPositionTimeout: 2000, // ms timeout for reading final positions + }, + + // Performance Tuning + performance: { + jointUpdateThreshold: 0.5, // min change to trigger joint update + uiUpdateThreshold: 0.1, // min change to trigger UI update + maxConcurrentReads: 3, // max concurrent servo reads + memoryCleanupInterval: 30000, // ms - periodic cleanup interval + } +} as const; + +// Type exports for better IntelliSense +export type RobotConfig = typeof ROBOT_CONFIG; \ No newline at end of file diff --git a/src/lib/elements/robot/createRobot.svelte.ts b/src/lib/elements/robot/createRobot.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1d26f3322c1725d21afd49bd82adeb79548a13c --- /dev/null +++ b/src/lib/elements/robot/createRobot.svelte.ts @@ -0,0 +1,16 @@ +import type { UrdfRobotState } from "$lib/types/robot"; +import type { RobotUrdfConfig } from "$lib/types/urdf"; +import { UrdfParser } from "@/components/3d/elements/robot/URDF/utils/UrdfParser"; + +export async function createUrdfRobot(urdfConfig: RobotUrdfConfig): Promise { + const customParser = new UrdfParser(urdfConfig.urdfUrl, "/robots/so-100/"); + const urdfData = await customParser.load(); + const robot = $state(customParser.fromString(urdfData)); + + const UrdfRobotState: UrdfRobotState = { + urdfRobot: robot, + urdfConfig: urdfConfig, + }; + + return UrdfRobotState; +} diff --git a/src/lib/elements/robot/drivers/RemoteConsumer.ts b/src/lib/elements/robot/drivers/RemoteConsumer.ts new file mode 100644 index 0000000000000000000000000000000000000000..adb1afaaa0059d4ed7e43b6f230cb5805b33a8e8 --- /dev/null +++ b/src/lib/elements/robot/drivers/RemoteConsumer.ts @@ -0,0 +1,177 @@ +import type { Consumer, ConnectionStatus, RobotCommand, RemoteDriverConfig } from '../models.js'; +import { robotics } from "@robohub/transport-server-client"; + +export class RemoteConsumer implements Consumer { + readonly id: string; + readonly name: string; + readonly config: RemoteDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + private commandCallbacks: ((command: RobotCommand) => void)[] = []; + + private consumer: robotics.RoboticsConsumer | null = null; + private client: robotics.RoboticsClientCore | null = null; + private workspaceId: string | null = null; + + constructor(config: RemoteDriverConfig) { + this.config = config; + this.id = `remote-consumer-${config.robotId}-${Date.now()}`; + this.name = `Remote Consumer (${config.robotId})`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(joinExistingRoom = false): Promise { + try { + const serverUrl = this.config.url.replace(/^ws/, "http"); + console.log(`[RemoteConsumer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})`); + + // Create core client for room management + this.client = new robotics.RoboticsClientCore(serverUrl); + + // Create consumer to receive commands + this.consumer = new robotics.RoboticsConsumer(serverUrl); + + // Set up event handlers + this.consumer.onConnected(() => { + console.log(`[RemoteConsumer] Connected to room ${this.config.robotId}`); + }); + + this.consumer.onDisconnected(() => { + console.log(`[RemoteConsumer] Disconnected from room ${this.config.robotId}`); + }); + + this.consumer.onError((error) => { + console.error(`[RemoteConsumer] Error:`, error); + this._status = { isConnected: false, error: `Consumer error: ${error}` }; + this.notifyStatusChange(); + }); + + // RECEIVE joint updates and forward as normalized commands + this.consumer.onJointUpdate((joints) => { + console.debug(`[RemoteConsumer] Received joint update:`, joints); + + const command: RobotCommand = { + timestamp: Date.now(), + joints: joints.map(joint => ({ + name: joint.name, + value: joint.value, // Already normalized from server + })) + }; + this.notifyCommand(command); + }); + + // RECEIVE state sync + this.consumer.onStateSync((state) => { + console.debug(`[RemoteConsumer] Received state sync:`, state); + + const joints = Object.entries(state).map(([name, value]) => ({ + name, + value: value as number + })); + + if (joints.length > 0) { + const command: RobotCommand = { + timestamp: Date.now(), + joints + }; + this.notifyCommand(command); + } + }); + + // Use workspace ID from config or default + this.workspaceId = this.config.workspaceId || 'default-workspace'; + + let roomData; + if (joinExistingRoom) { + // Join existing room (for AI session integration) + roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; + console.log(`[RemoteConsumer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`); + } else { + // Create new room (for standalone operation) + roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); + console.log(`[RemoteConsumer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}`); + } + + const success = await this.consumer.connect(roomData.workspaceId, roomData.roomId, this.id); + + if (!success) { + throw new Error("Failed to connect consumer to room"); + } + + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatusChange(); + + console.log(`[RemoteConsumer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}`); + } catch (error) { + console.error(`[RemoteConsumer] Connection failed:`, error); + this._status = { isConnected: false, error: `Connection failed: ${error}` }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log(`[RemoteConsumer] Disconnecting...`); + + if (this.consumer) { + await this.consumer.disconnect(); + this.consumer = null; + } + if (this.client) { + // Client doesn't need explicit disconnect + this.client = null; + } + + this.workspaceId = null; + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log(`[RemoteConsumer] Disconnected`); + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + onCommand(callback: (command: RobotCommand) => void): () => void { + this.commandCallbacks.push(callback); + return () => { + const index = this.commandCallbacks.indexOf(callback); + if (index >= 0) { + this.commandCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private notifyCommand(command: RobotCommand): void { + this.commandCallbacks.forEach(callback => { + try { + callback(command); + } catch (error) { + console.error('[RemoteConsumer] Error in command callback:', error); + } + }); + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach(callback => { + try { + callback(this._status); + } catch (error) { + console.error('[RemoteConsumer] Error in status callback:', error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/drivers/RemoteProducer.ts b/src/lib/elements/robot/drivers/RemoteProducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..93afb24afd6d27d851ff5223fe8e8008dbab0446 --- /dev/null +++ b/src/lib/elements/robot/drivers/RemoteProducer.ts @@ -0,0 +1,180 @@ +import type { Producer, ConnectionStatus, RobotCommand, RemoteDriverConfig } from '../models.js'; +import { robotics } from "@robohub/transport-server-client"; + +export class RemoteProducer implements Producer { + readonly id: string; + readonly name: string; + readonly config: RemoteDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + + private producer: robotics.RoboticsProducer | null = null; + private client: robotics.RoboticsClientCore | null = null; + private workspaceId: string | null = null; + + // State update interval for producer mode + private stateUpdateInterval?: ReturnType; + private lastKnownState: Record = {}; + + constructor(config: RemoteDriverConfig) { + this.config = config; + this.id = `remote-producer-${config.robotId}-${Date.now()}`; + this.name = `Remote Producer (${config.robotId})`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(joinExistingRoom = false): Promise { + try { + const serverUrl = this.config.url.replace(/^ws/, "http"); + console.log(`[RemoteProducer] Connecting to ${serverUrl} for robot ${this.config.robotId} (join mode: ${joinExistingRoom})`); + + // Create core client for room management + this.client = new robotics.RoboticsClientCore(serverUrl); + + // Create producer to send commands + this.producer = new robotics.RoboticsProducer(serverUrl); + + // Set up event handlers + this.producer.onConnected(() => { + console.log(`[RemoteProducer] Connected to room ${this.config.robotId}`); + this.startStateUpdates(); + }); + + this.producer.onDisconnected(() => { + console.log(`[RemoteProducer] Disconnected from room ${this.config.robotId}`); + this.stopStateUpdates(); + }); + + this.producer.onError((error) => { + console.error(`[RemoteProducer] Error:`, error); + this._status = { isConnected: false, error: `Producer error: ${error}` }; + this.notifyStatusChange(); + this.stopStateUpdates(); + }); + + // Use workspace ID from config or default + this.workspaceId = this.config.workspaceId || 'default-workspace'; + + let roomData; + if (joinExistingRoom) { + // Join existing room (for AI session integration) + roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId }; + console.log(`[RemoteProducer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`); + } else { + // Create new room (for standalone operation) + roomData = await this.client.createRoom(this.workspaceId, this.config.robotId); + console.log(`[RemoteProducer] Created new room ${roomData.roomId} in workspace ${roomData.workspaceId}`); + } + + const success = await this.producer.connect(roomData.workspaceId, roomData.roomId, this.id); + + if (!success) { + throw new Error("Failed to connect producer to room"); + } + + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatusChange(); + + console.log(`[RemoteProducer] Connected successfully to workspace ${roomData.workspaceId}, room ${roomData.roomId}`); + } catch (error) { + console.error(`[RemoteProducer] Connection failed:`, error); + this._status = { isConnected: false, error: `Connection failed: ${error}` }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log(`[RemoteProducer] Disconnecting...`); + + this.stopStateUpdates(); + + if (this.producer) { + await this.producer.disconnect(); + this.producer = null; + } + if (this.client) { + // Client doesn't need explicit disconnect + this.client = null; + } + + this.workspaceId = null; + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log(`[RemoteProducer] Disconnected`); + } + + async sendCommand(command: RobotCommand): Promise { + if (!this._status.isConnected || !this.producer) { + throw new Error('Cannot send command: Remote producer not connected'); + } + + try { + console.debug(`[RemoteProducer] Sending command:`, command); + + // Update last known state for periodic updates + command.joints.forEach(joint => { + this.lastKnownState[joint.name] = joint.value; + }); + + // Send joint update with normalized values + const joints = command.joints.map(joint => ({ + name: joint.name, + value: joint.value // Already normalized + })); + + await this.producer.sendJointUpdate(joints); + console.debug(`[RemoteProducer] Sent joint update with ${joints.length} joints`); + } catch (error) { + console.error('[RemoteProducer] Failed to send command:', error); + throw error; + } + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private startStateUpdates(): void { + // Send periodic state updates to keep remote server informed + this.stateUpdateInterval = setInterval(async () => { + if (this.producer && this._status.isConnected && Object.keys(this.lastKnownState).length > 0) { + try { + await this.producer.sendStateSync(this.lastKnownState); + } catch (error) { + console.error('[RemoteProducer] Failed to send state update:', error); + } + } + }, 100); // 10 Hz updates + } + + private stopStateUpdates(): void { + if (this.stateUpdateInterval) { + clearInterval(this.stateUpdateInterval); + this.stateUpdateInterval = undefined; + } + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach(callback => { + try { + callback(this._status); + } catch (error) { + console.error('[RemoteProducer] Error in status callback:', error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/drivers/USBConsumer.ts b/src/lib/elements/robot/drivers/USBConsumer.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9462a9dd2c08401a627a277363cf68000719302 --- /dev/null +++ b/src/lib/elements/robot/drivers/USBConsumer.ts @@ -0,0 +1,272 @@ +import type { Consumer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js'; +import { USBCalibrationManager } from '../calibration/USBCalibrationManager.js'; +import { scsServoSDK } from "feetech.js"; +import { ROBOT_CONFIG } from '../config.js'; + +export class USBConsumer implements Consumer { + readonly id: string; + readonly name = 'USB Consumer'; + readonly config: USBDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + private commandCallbacks: ((command: RobotCommand) => void)[] = []; + + // Listening state + private isListening = false; + private pollingAbortController: AbortController | null = null; + + // Joint configuration + private readonly jointIds = [1, 2, 3, 4, 5, 6]; + private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; + private lastPositions: Record = {}; + + // Shared calibration manager + private calibrationManager: USBCalibrationManager; + + // Servo reading queue + private isReadingServos = false; + private readingQueue: Array<{ + servoId: number; + resolve: (value: number) => void; + reject: (error: Error) => void; + }> = []; + + // Error tracking for better backoff + private consecutiveErrors = 0; + private lastErrorTime = 0; + + constructor(config: USBDriverConfig, calibrationManager: USBCalibrationManager) { + this.config = config; + this.calibrationManager = calibrationManager; + this.id = `usb-consumer-${Date.now()}`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(): Promise { + if (this._status.isConnected) { + console.debug('[USBConsumer] Already connected'); + return; + } + + try { + console.debug('[USBConsumer] Connecting...'); + + // Check if calibration is needed + if (this.calibrationManager.needsCalibration) { + throw new Error('USB Consumer requires calibration. Please complete calibration first.'); + } + + // Ensure the SDK is connected (reuse calibration connection if available) + if (!this.calibrationManager.isSDKConnected) { + console.debug('[USBConsumer] Establishing new SDK connection'); + await scsServoSDK.connect({ + baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate + }); + } else { + console.debug('[USBConsumer] Reusing existing SDK connection from calibration'); + } + + // Ensure servos remain unlocked for consumer (reading positions) + console.debug('[USBConsumer] 🔓 Ensuring servos remain unlocked for position reading...'); + await this.calibrationManager.keepServosUnlockedForConsumer(); + + this._status = { + isConnected: true, + lastConnected: new Date() + }; + this.notifyStatusChange(); + + console.debug('[USBConsumer] ✅ Connected successfully - servos unlocked for reading'); + } catch (error) { + console.error('[USBConsumer] Connection failed:', error); + this._status = { + isConnected: false, + error: error instanceof Error ? error.message : 'Connection failed' + }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + if (this._status.isConnected) { + await this.stopListening(); + console.debug('[USBConsumer] Disconnecting (keeping shared SDK connection)'); + // Don't disconnect the SDK here - let calibration manager handle it + // This allows multiple USB drivers to share the same connection + } + + this._status = { isConnected: false }; + this.notifyStatusChange(); + } + + async startListening(): Promise { + if (this.isListening) { + console.warn('[USBConsumer] Already listening'); + return; + } + + this.isListening = true; + this.pollingAbortController = new AbortController(); + + console.debug('[USBConsumer] Starting continuous polling'); + this.pollContinuously(); + } + + async stopListening(): Promise { + if (!this.isListening) return; + + this.isListening = false; + if (this.pollingAbortController) { + this.pollingAbortController.abort(); + this.pollingAbortController = null; + } + + console.debug('[USBConsumer] Stopped listening'); + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + onCommand(callback: (command: RobotCommand) => void): () => void { + this.commandCallbacks.push(callback); + return () => { + const index = this.commandCallbacks.indexOf(callback); + if (index >= 0) { + this.commandCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private async readServoPosition(servoId: number): Promise { + return new Promise((resolve, reject) => { + this.readingQueue.push({ servoId, resolve, reject }); + this.processReadingQueue(); + }); + } + + private async processReadingQueue(): Promise { + if (this.isReadingServos || this.readingQueue.length === 0) { + return; + } + + this.isReadingServos = true; + + try { + const batch = [...this.readingQueue]; + this.readingQueue = []; + + for (const { servoId, resolve, reject } of batch) { + try { + const position = await scsServoSDK.readPosition(servoId); + resolve(position); + } catch (error) { + reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`)); + } + + await new Promise(resolve => setTimeout(resolve, 5)); + } + } finally { + this.isReadingServos = false; + + if (this.readingQueue.length > 0) { + setTimeout(() => this.processReadingQueue(), 50); + } + } + } + + private async pollContinuously(): Promise { + while (this.isListening && this._status.isConnected && !this.pollingAbortController?.signal.aborted) { + try { + const changes: { name: string; value: number }[] = []; + + const readPromises = this.jointIds.map(async (servoId, i) => { + const jointName = this.jointNames[i]; + try { + const position = await this.readServoPosition(servoId); + const lastPosition = this.lastPositions[jointName]; + + if (position !== lastPosition) { + // Use calibration manager for normalization + const normalizedValue = this.calibrationManager.normalizeValue(position, jointName); + this.lastPositions[jointName] = position; + return { name: jointName, value: normalizedValue }; + } + } catch (error) { + // Silent continue on read errors + } + return null; + }); + + const results = await Promise.all(readPromises); + + results.forEach(result => { + if (result) { + changes.push(result); + } + }); + + if (changes.length > 0) { + const command: RobotCommand = { + joints: changes, + timestamp: Date.now() + }; + this.notifyCommand(command); + + // Reset error counter on successful read + this.consecutiveErrors = 0; + } + + await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.polling.consumerPollingRate)); + + } catch (error) { + if (!this.pollingAbortController?.signal.aborted) { + console.error('[USBConsumer] Polling error:', error); + + // Smart error backoff + this.consecutiveErrors++; + this.lastErrorTime = Date.now(); + + const backoffTime = this.consecutiveErrors > ROBOT_CONFIG.polling.maxPollingErrors + ? ROBOT_CONFIG.polling.errorBackoffRate * 3 // Longer backoff after many errors + : ROBOT_CONFIG.polling.errorBackoffRate; + + await new Promise(resolve => setTimeout(resolve, backoffTime)); + } + } + } + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach(callback => { + try { + callback(this._status); + } catch (error) { + console.error('[USBConsumer] Error in status callback:', error); + } + }); + } + + private notifyCommand(command: RobotCommand): void { + this.commandCallbacks.forEach(callback => { + try { + callback(command); + } catch (error) { + console.error('[USBConsumer] Error in command callback:', error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/drivers/USBProducer.ts b/src/lib/elements/robot/drivers/USBProducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..89a0c9f1a75f13f53fc1645c2ed6cb736fd5b107 --- /dev/null +++ b/src/lib/elements/robot/drivers/USBProducer.ts @@ -0,0 +1,204 @@ +import type { Producer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js'; +import { USBCalibrationManager } from '../calibration/USBCalibrationManager.js'; +import { scsServoSDK } from "feetech.js"; +import { ROBOT_CONFIG } from '../config.js'; + +export class USBProducer implements Producer { + readonly id: string; + readonly name = 'USB Producer'; + readonly config: USBDriverConfig; + + private _status: ConnectionStatus = { isConnected: false }; + private statusCallbacks: ((status: ConnectionStatus) => void)[] = []; + + // Joint configuration + private readonly jointIds = [1, 2, 3, 4, 5, 6]; + private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"]; + + // Shared calibration manager + private calibrationManager: USBCalibrationManager; + + // Serial command processing to prevent "Port is busy" errors + private commandQueue: Array<{ joints: Array<{ name: string; value: number }>, resolve: () => void, reject: (error: Error) => void }> = []; + private isProcessingCommands = false; + + constructor(config: USBDriverConfig, calibrationManager: USBCalibrationManager) { + this.config = config; + this.calibrationManager = calibrationManager; + this.id = `usb-producer-${Date.now()}`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(): Promise { + if (this._status.isConnected) { + console.debug('[USBProducer] Already connected'); + return; + } + + try { + console.debug('[USBProducer] Connecting...'); + + // Check if calibration is needed + if (this.calibrationManager.needsCalibration) { + throw new Error('USB Producer requires calibration. Please complete calibration first.'); + } + + // Ensure the SDK is connected (reuse calibration connection if available) + if (!this.calibrationManager.isSDKConnected) { + console.debug('[USBProducer] Establishing new SDK connection'); + await scsServoSDK.connect({ + baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate + }); + } else { + console.debug('[USBProducer] Reusing existing SDK connection from calibration'); + } + + // Lock servos for production use (robot control) + console.debug('[USBProducer] 🔒 Locking servos for production use...'); + await this.calibrationManager.lockServosForProduction(); + + this._status = { + isConnected: true, + lastConnected: new Date() + }; + this.notifyStatusChange(); + + console.debug('[USBProducer] ✅ Connected successfully - servos locked for robot control'); + } catch (error) { + console.error('[USBProducer] Connection failed:', error); + this._status = { + isConnected: false, + error: error instanceof Error ? error.message : 'Connection failed' + }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + if (this._status.isConnected) { + console.debug('[USBProducer] 🔓 Disconnecting and unlocking servos...'); + + try { + // Safely unlock servos when disconnecting (best practice) + if (this.calibrationManager.isSDKConnected) { + console.debug('[USBProducer] 🔓 Safely unlocking servos for manual movement...'); + await scsServoSDK.unlockServosForManualMovement(this.jointIds); + console.debug('[USBProducer] ✅ Servos safely unlocked - can now be moved manually'); + } + } catch (error) { + console.warn('[USBProducer] Warning: Failed to unlock servos during disconnect:', error); + } + + // Don't disconnect the SDK here - let calibration manager handle it + // This allows multiple USB drivers to share the same connection + } + + this._status = { isConnected: false }; + this.notifyStatusChange(); + console.debug('[USBProducer] ✅ Disconnected'); + } + + async sendCommand(command: RobotCommand): Promise { + if (!this._status.isConnected) { + throw new Error('Cannot send command: USB Producer not connected'); + } + + console.debug(`[USBProducer] Queuing command:`, command); + + // Queue command for serial processing + return new Promise((resolve, reject) => { + this.commandQueue.push({ + joints: command.joints, + resolve, + reject + }); + + // Start processing if not already running + this.processCommandQueue(); + }); + } + + // Event handlers + onStatusChange(callback: (status: ConnectionStatus) => void): () => void { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + // Private methods + private async processCommandQueue(): Promise { + if (this.isProcessingCommands || this.commandQueue.length === 0) { + return; + } + + this.isProcessingCommands = true; + + try { + while (this.commandQueue.length > 0) { + const { joints, resolve, reject } = this.commandQueue.shift()!; + + try { + // Process servos sequentially to prevent "Port is busy" errors + for (const jointCmd of joints) { + const jointIndex = this.jointNames.indexOf(jointCmd.name); + if (jointIndex >= 0) { + const servoId = this.jointIds[jointIndex]; + const servoPosition = this.calibrationManager.denormalizeValue(jointCmd.value, jointCmd.name); + + await this.writeServoWithRetry(servoId, servoPosition, jointCmd.name); + + // Small delay between servo writes to prevent port conflicts + await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay)); + } + } + + resolve(); + } catch (error) { + reject(error as Error); + } + } + } finally { + this.isProcessingCommands = false; + } + } + + private async writeServoWithRetry(servoId: number, position: number, jointName: string): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= ROBOT_CONFIG.usb.maxRetries; attempt++) { + try { + await scsServoSDK.writePositionUnlocked(servoId, position); + console.debug(`[USBProducer] ✅ ${jointName} (servo ${servoId}) -> ${position}`); + return; // Success! + } catch (error) { + lastError = error as Error; + console.warn(`[USBProducer] Attempt ${attempt}/${ROBOT_CONFIG.usb.maxRetries} failed for servo ${servoId}:`, error); + + if (attempt < ROBOT_CONFIG.usb.maxRetries) { + await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.retryDelay)); + } + } + } + + // All retries failed + throw new Error(`Failed to write servo ${servoId} after ${ROBOT_CONFIG.usb.maxRetries} attempts: ${lastError?.message}`); + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach(callback => { + try { + callback(this._status); + } catch (error) { + console.error('[USBProducer] Error in status callback:', error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/elements/robot/drivers/index.ts b/src/lib/elements/robot/drivers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..719a3a679fa86e9cca0d3cfe45e3e74a482ddda4 --- /dev/null +++ b/src/lib/elements/robot/drivers/index.ts @@ -0,0 +1,5 @@ +// Robot drivers exports +export { USBConsumer } from './USBConsumer.js'; +export { USBProducer } from './USBProducer.js'; +export { RemoteConsumer } from './RemoteConsumer.js'; +export { RemoteProducer } from './RemoteProducer.js'; \ No newline at end of file diff --git a/src/lib/elements/robot/index.ts b/src/lib/elements/robot/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea84ac167adf46e114f36fe853b15d8ff43ba23a --- /dev/null +++ b/src/lib/elements/robot/index.ts @@ -0,0 +1,20 @@ +// Clean Robot Architecture - Organized Structure +// Everything you need in one place + +// Core robot classes +export { Robot } from './Robot.svelte.js'; +export { RobotManager } from './RobotManager.svelte.js'; + +// Robot models and types +export * from './models.js'; + +// Robot drivers +export * from './drivers/index.js'; + +// Robot calibration (avoid naming conflicts with models) +export { USBCalibrationManager } from './calibration/USBCalibrationManager.js'; +export { CalibrationState as CalibrationStateManager } from './calibration/CalibrationState.svelte.js'; +export { default as USBCalibrationPanel } from './calibration/USBCalibrationPanel.svelte'; + +// Robot components +export * from './components/index.js'; \ No newline at end of file diff --git a/src/lib/elements/robot/models.ts b/src/lib/elements/robot/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..aae8e3ddecda695fe9488f96fe46293db7a9b510 --- /dev/null +++ b/src/lib/elements/robot/models.ts @@ -0,0 +1,72 @@ +// Core models with clean typing +export interface JointState { + name: string; + value: number; // Normalized value (-100 to +100 for regular joints, 0-100 for grippers) + limits?: { lower: number; upper: number }; // URDF limits in radians + servoId?: number; // For hardware mapping +} + +export interface JointCalibration { + isCalibrated: boolean; + minServoValue?: number; + maxServoValue?: number; +} + +export interface RobotCommand { + joints: { name: string; value: number }[]; + timestamp?: number; +} + +export interface ConnectionStatus { + isConnected: boolean; + error?: string; + lastConnected?: Date; +} + +export interface Position3D { + x: number; + y: number; + z: number; +} + +// Driver configurations +export interface USBDriverConfig { + type: 'usb'; + baudRate?: number; +} + +export interface RemoteDriverConfig { + type: 'remote'; + url: string; + robotId: string; + workspaceId?: string; // Optional workspace ID for remote connections +} + +// Driver base interface +export interface Driver { + readonly id: string; + readonly name: string; + readonly status: ConnectionStatus; + + connect(): Promise; + disconnect(): Promise; + onStatusChange(callback: (status: ConnectionStatus) => void): () => void; +} + +// Consumer interface (receives commands) - Robot can only have ONE +export interface Consumer extends Driver { + onCommand(callback: (command: RobotCommand) => void): () => void; + startListening?(): Promise; + stopListening?(): Promise; +} + +// Producer interface (sends commands) - Robot can have MULTIPLE +export interface Producer extends Driver { + sendCommand(command: RobotCommand): Promise; +} + +// Calibration state for UI components +export interface CalibrationState { + isCalibrating: boolean; + progress: number; +} \ No newline at end of file diff --git a/src/lib/elements/video/VideoManager.svelte.ts b/src/lib/elements/video/VideoManager.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfbf7e1ca0949c287d017cb270cc7cd59211133b --- /dev/null +++ b/src/lib/elements/video/VideoManager.svelte.ts @@ -0,0 +1,575 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Video Manager - Multiple Video Instances + * Manages multiple video instances, each with their own streaming state + */ + +import { video as videoClient } from '@robohub/transport-server-client'; +import type { video as videoTypes } from '@robohub/transport-server-client'; +import { generateName } from "$lib/utils/generateName"; +import type { Positionable, Position3D } from '$lib/types/positionable'; +import { positionManager } from '$lib/utils/positionManager'; +import { settings } from '$lib/runes/settings.svelte'; + +/** + * Individual video instance state + */ +export class VideoInstance implements Positionable { + public id: string; + public name: string; + + // Input state (what this video is viewing) + input = $state({ + type: null as 'local-camera' | 'remote-stream' | null, + stream: null as MediaStream | null, + client: null as videoTypes.VideoConsumer | null, + roomId: null as string | null, + // Connection lifecycle state + connectionState: 'disconnected' as 'disconnected' | 'connecting' | 'connected' | 'prepared' | 'paused', + preparedRoomId: null as string | null, + // Connection policy - determines if connection should persist or can be paused + connectionPolicy: 'persistent' as 'persistent' | 'lazy', + }); + + // Output state (what this video is broadcasting) + output = $state({ + active: false, + client: null as videoTypes.VideoProducer | null, + roomId: null as string | null, + }); + + // Position (reactive and bindable) + position = $state({ x: 0, y: 0, z: 0 }); + + constructor(id: string, name?: string) { + this.id = id; + this.name = name || `Video ${id}`; + } + + /** + * Update position (implements Positionable interface) + */ + updatePosition(newPosition: Position3D): void { + this.position = { ...newPosition }; + } + + // Derived state - simplified to prevent reactive loops + get hasInput(): boolean { + return this.input.type !== null && this.input.stream !== null; + } + + get hasOutput(): boolean { + return this.output.active; + } + + get canOutput(): boolean { + // Can only output if input is local camera (not remote stream) + return this.input.type === 'local-camera' && this.input.stream !== null; + } + + get currentStream(): MediaStream | null { + return this.input.stream; + } + + get status() { + // Return a stable object reference to prevent infinite loops + // Only create new object when values actually change + const hasInput = this.hasInput; + const hasOutput = this.hasOutput; + const inputType = this.input.type; + const outputRoomId = this.output.roomId; + const inputRoomId = this.input.roomId; + const connectionState = this.input.connectionState; + const preparedRoomId = this.input.preparedRoomId; + const connectionPolicy = this.input.connectionPolicy; + const canActivate = (connectionState === 'prepared' || connectionState === 'paused') && preparedRoomId !== null; + const canPause = connectionState === 'connected' && connectionPolicy === 'lazy'; + + return { + id: this.id, + name: this.name, + hasInput, + hasOutput, + inputType, + outputRoomId, + inputRoomId, + connectionState, + preparedRoomId, + connectionPolicy, + canActivate, + canPause, + }; + } +} + +/** + * Video status information for UI components + */ +export interface VideoStatus { + id: string; + name: string; + hasInput: boolean; + hasOutput: boolean; + inputType: 'local-camera' | 'remote-stream' | null; + outputRoomId: string | null; + inputRoomId: string | null; + connectionState: 'disconnected' | 'connecting' | 'connected' | 'prepared' | 'paused'; + preparedRoomId: string | null; + connectionPolicy: 'persistent' | 'lazy'; + canActivate: boolean; + canPause: boolean; +} + +/** + * Central manager for all video instances + */ +export class VideoManager { + private _videos = $state([]); + + // Room listing state (shared across all videos) - using transport server + rooms = $state([]); + roomsLoading = $state(false); + + // Reactive getters - simplified to prevent loops + get videos(): VideoInstance[] { + return this._videos; + } + + get videosWithInput(): VideoInstance[] { + return this._videos.filter((video) => video.hasInput); + } + + get videosWithOutput(): VideoInstance[] { + return this._videos.filter((video) => video.hasOutput); + } + + /** + * Create a new video instance + */ + createVideo(id?: string, name?: string, position?: Position3D): VideoInstance { + const videoId = id || generateName(); + + // Check if video already exists + if (this._videos.find((v) => v.id === videoId)) { + throw new Error(`Video with ID ${videoId} already exists`); + } + + // Create video instance + const video = new VideoInstance(videoId, name); + + // Set position (from position manager if not provided) + video.position = position || positionManager.getNextPosition(); + + // Add to reactive array + this._videos.push(video); + + console.log(`Created video ${videoId} at position (${video.position.x.toFixed(1)}, ${video.position.y.toFixed(1)}, ${video.position.z.toFixed(1)}). Total videos: ${this._videos.length}`); + + return video; + } + + /** + * Get video by ID + */ + getVideo(id: string): VideoInstance | undefined { + return this._videos.find((v) => v.id === id); + } + + /** + * Get video status by ID + */ + getVideoStatus(id: string): VideoStatus | undefined { + const video = this.getVideo(id); + return video?.status; + } + + /** + * Remove a video + */ + async removeVideo(id: string): Promise { + const videoIndex = this._videos.findIndex((v) => v.id === id); + if (videoIndex === -1) return; + + const video = this._videos[videoIndex]; + + // Clean up video resources + await this.disconnectVideoInput(id); + await this.stopVideoOutput(id); + + // Remove from reactive array + this._videos.splice(videoIndex, 1); + + console.log(`Removed video ${id}. Remaining videos: ${this._videos.length}`); + } + + // ============= ROOM MANAGEMENT ============= + + async listRooms(workspaceId: string): Promise { + this.roomsLoading = true; + try { + const client = new videoClient.VideoClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + this.rooms = rooms; + return rooms; + } catch (error) { + console.error('Failed to list rooms:', error); + this.rooms = []; + return []; + } finally { + this.roomsLoading = false; + } + } + + async refreshRooms(workspaceId: string): Promise { + await this.listRooms(workspaceId); + } + + async createVideoRoom(workspaceId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + try { + const client = new videoClient.VideoClientCore(settings.transportServerUrl); + const result = await client.createRoom(workspaceId, roomId); + // Refresh rooms list to include the new room + await this.refreshRooms(workspaceId); + return { success: true, roomId: result.roomId }; + } catch (error) { + console.error('Failed to create video room:', error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + } + + generateRoomId(videoId: string): string { + return `${videoId}-${generateName()}`; + } + + /** + * Start video output to an existing room + */ + async startVideoOutputToRoom(workspaceId: string, videoId: string, roomId: string): Promise<{ success: boolean; error?: string }> { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + if (!video.canOutput) { + return { success: false, error: 'Cannot output - input must be local camera' }; + } + + try { + const producer = new videoClient.VideoProducer(settings.transportServerUrl); + const connected = await producer.connect(workspaceId, roomId, 'producer-id'); + + if (!connected) { + throw new Error('Failed to connect to room'); + } + + // Start camera streaming - VideoProducer creates its own stream + await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // Update output state + video.output.active = true; + video.output.client = producer; + video.output.roomId = roomId; + + console.log(`Video output started to room ${roomId} for video ${videoId}`); + return { success: true }; + } catch (error) { + console.error(`Failed to start video output for ${videoId}:`, error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + /** + * Create a new room and start video output as producer + */ + async startVideoOutputAsProducer(workspaceId: string, videoId: string, roomId?: string): Promise<{ success: boolean; roomId?: string; error?: string }> { + try { + // Create room first if roomId provided, otherwise generate one + const finalRoomId = roomId || this.generateRoomId(videoId); + const createResult = await this.createVideoRoom(workspaceId, finalRoomId); + + if (!createResult.success) { + return createResult; + } + + // Start output to the new room + const outputResult = await this.startVideoOutputToRoom(workspaceId, videoId, createResult.roomId!); + + if (!outputResult.success) { + return { success: false, error: outputResult.error }; + } + + return { success: true, roomId: createResult.roomId }; + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + } + + // ============= INPUT MANAGEMENT ============= + + /** + * Prepare a remote stream connection (stores roomId without connecting) + */ + prepareRemoteStream(videoId: string, roomId: string, policy: 'persistent' | 'lazy' = 'lazy'): { success: boolean; error?: string } { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + video.input.preparedRoomId = roomId; + video.input.connectionState = 'prepared'; + video.input.connectionPolicy = policy; + console.log(`Prepared remote stream for video ${videoId}, roomId: ${roomId}, policy: ${policy}`); + return { success: true }; + } + + /** + * Activate a prepared or paused remote stream connection + */ + async activateRemoteStream(videoId: string, workspaceId: string): Promise<{ success: boolean; error?: string }> { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + if (!video.input.preparedRoomId) { + return { success: false, error: 'No prepared room ID to activate' }; + } + + return await this.connectRemoteStream(workspaceId, videoId, video.input.preparedRoomId, video.input.connectionPolicy); + } + + /** + * Pause a remote stream connection (keeps roomId for later activation) + */ + async pauseRemoteStream(videoId: string): Promise { + const video = this.getVideo(videoId); + if (!video || video.input.type !== 'remote-stream') return; + + // Store the current roomId for later activation + if (video.input.roomId && !video.input.preparedRoomId) { + video.input.preparedRoomId = video.input.roomId; + } + + // Disconnect but keep prepared connection info + if (video.input.client) { + video.input.client.disconnect(); + } + + video.input.type = null; + video.input.stream = null; + video.input.client = null; + video.input.roomId = null; + video.input.connectionState = 'paused'; + + console.log(`Paused remote stream for video ${videoId}, can activate later`); + } + + async connectLocalCamera(videoId: string): Promise<{ success: boolean; error?: string }> { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + try { + // First disconnect any existing input to avoid conflicts + await this.disconnectVideoInput(videoId); + + // Get local camera stream + const stream = await navigator.mediaDevices.getUserMedia({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // Update input state atomically to prevent reactive loops + video.input.type = 'local-camera'; + video.input.stream = stream; + video.input.client = null; + video.input.roomId = null; + video.input.connectionState = 'connected'; + video.input.preparedRoomId = null; + video.input.connectionPolicy = 'persistent'; + + console.log(`Local camera connected to video ${videoId}`); + return { success: true }; + } catch (error) { + console.error(`Failed to connect local camera to video ${videoId}:`, error); + // Ensure clean state on error + video.input.connectionState = 'disconnected'; + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + async connectRemoteStream(workspaceId: string, videoId: string, roomId: string, policy: 'persistent' | 'lazy' = 'persistent'): Promise<{ success: boolean; error?: string }> { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + try { + // First disconnect any existing input + await this.disconnectVideoInput(videoId); + + // Update connection state + video.input.connectionState = 'connecting'; + + const consumer = new videoClient.VideoConsumer(settings.transportServerUrl); + const connected = await consumer.connect(workspaceId, roomId, 'consumer-id'); + + if (!connected) { + throw new Error('Failed to connect to remote stream'); + } + + // Start receiving video + await consumer.startReceiving(); + + // Set up stream receiving + consumer.on('streamReceived', (stream: MediaStream) => { + video.input.stream = stream; + }); + + // Update input state + video.input.type = 'remote-stream'; + video.input.client = consumer; + video.input.roomId = roomId; + video.input.preparedRoomId = null; // Clear prepared since we're now connected + video.input.connectionState = 'connected'; + video.input.connectionPolicy = policy; + + console.log(`Remote stream connected to video ${videoId} with policy ${policy}`); + return { success: true }; + } catch (error) { + console.error(`Failed to connect remote stream to video ${videoId}:`, error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + async disconnectVideoInput(videoId: string): Promise { + const video = this.getVideo(videoId); + if (!video) { + console.warn(`Video ${videoId} not found for disconnection`); + return; + } + + console.log(`Disconnecting input from video ${videoId}, current type: ${video.input.type}`); + + try { + // Stop local camera tracks if any + if (video.input.stream && video.input.type === 'local-camera') { + console.log(`Stopping ${video.input.stream.getTracks().length} camera tracks`); + video.input.stream.getTracks().forEach(track => { + console.log(`Stopping track: ${track.kind} (${track.label})`); + track.stop(); + }); + } + + // Disconnect remote client if any + if (video.input.client) { + console.log(`Disconnecting remote client for video ${videoId}`); + video.input.client.disconnect(); + } + + // Reset input state atomically + video.input.type = null; + video.input.stream = null; + video.input.client = null; + video.input.roomId = null; + video.input.connectionState = 'disconnected'; + video.input.preparedRoomId = null; + video.input.connectionPolicy = 'persistent'; + + console.log(`Input successfully disconnected from video ${videoId}`); + } catch (error) { + console.error(`Error during disconnection for video ${videoId}:`, error); + // Still reset the state even if there was an error + video.input.type = null; + video.input.stream = null; + video.input.client = null; + video.input.roomId = null; + video.input.connectionState = 'disconnected'; + video.input.preparedRoomId = null; + video.input.connectionPolicy = 'persistent'; + throw error; + } + } + + // ============= OUTPUT MANAGEMENT ============= + + async startVideoOutput(workspaceId: string, videoId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { + const video = this.getVideo(videoId); + if (!video) { + return { success: false, error: `Video ${videoId} not found` }; + } + + if (!video.canOutput) { + return { success: false, error: 'Cannot output - input must be local camera' }; + } + + try { + const producer = new videoClient.VideoProducer(settings.transportServerUrl); + + // Create room + const result = await producer.createRoom(workspaceId); + const connected = await producer.connect(result.workspaceId, result.roomId, 'producer-id'); + + if (!connected) { + throw new Error('Failed to connect producer'); + } + + // Start camera with existing stream + if (video.input.stream) { + await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + } + + // Update output state + video.output.active = true; + video.output.client = producer; + video.output.roomId = result.roomId; + + // Refresh room list + await this.listRooms(workspaceId); + + console.log(`Output started for video ${videoId}, room created: ${result.roomId}`); + return { success: true, roomId: result.roomId }; + } catch (error) { + console.error(`Failed to start output for video ${videoId}:`, error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + + async stopVideoOutput(videoId: string): Promise { + const video = this.getVideo(videoId); + if (!video) return; + + if (video.output.client) { + video.output.client.stopStreaming(); + video.output.client.disconnect(); + } + + video.output.active = false; + video.output.client = null; + video.output.roomId = null; + + console.log(`Output stopped for video ${videoId}`); + } + + /** + * Clean up all videos + */ + async destroy(): Promise { + const cleanupPromises = this._videos.map(async (video) => { + await this.disconnectVideoInput(video.id); + await this.stopVideoOutput(video.id); + }); + await Promise.allSettled(cleanupPromises); + this._videos.length = 0; + } +} + +// Global video manager instance +export const videoManager = new VideoManager(); \ No newline at end of file diff --git a/src/lib/elements/video/videoConnection.svelte.ts b/src/lib/elements/video/videoConnection.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b9867c3515ead8daa21b38b1ac45d6ab409f90e --- /dev/null +++ b/src/lib/elements/video/videoConnection.svelte.ts @@ -0,0 +1,206 @@ +/** + * Video Connection System using Svelte 5 Runes + * Clean and simple video producer/consumer management + */ + +import { video } from '@robohub/transport-server-client'; +import type { video as videoTypes } from '@robohub/transport-server-client'; +import { settings } from '$lib/runes/settings.svelte'; + +// Simple connection state using runes +export class VideoConnectionState { + // Producer state + producer = $state({ + connected: false, + client: null as videoTypes.VideoProducer | null, + roomId: null as string | null, + stream: null as MediaStream | null, + }); + + // Consumer state + consumer = $state({ + connected: false, + client: null as videoTypes.VideoConsumer | null, + roomId: null as string | null, + stream: null as MediaStream | null, + }); + + // Room listing state + rooms = $state([]); + roomsLoading = $state(false); + + // Derived state + get hasProducer() { + return this.producer.connected; + } + + get hasConsumer() { + return this.consumer.connected; + } + + get isStreaming() { + return this.hasProducer && this.producer.stream !== null; + } + + get canConnectConsumer() { + return this.hasProducer && this.producer.roomId !== null; + } +} + +// Create global instance +export const videoConnection = new VideoConnectionState(); + +// External action functions +export const videoActions = { + + // Room management + async listRooms(workspaceId: string): Promise { + videoConnection.roomsLoading = true; + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + videoConnection.rooms = rooms; + return rooms; + } catch (error) { + console.error('Failed to list rooms:', error); + videoConnection.rooms = []; + return []; + } finally { + videoConnection.roomsLoading = false; + } + }, + + async createRoom(workspaceId: string, roomId?: string): Promise { + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const result = await client.createRoom(workspaceId, roomId); + if (result) { + // Refresh room list + await this.listRooms(workspaceId); + return result.roomId; + } + return null; + } catch (error) { + console.error('Failed to create room:', error); + return null; + } + }, + + async deleteRoom(workspaceId: string, roomId: string): Promise { + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + await client.deleteRoom(workspaceId, roomId); + // Refresh room list + await this.listRooms(workspaceId); + return true; + } catch (error) { + console.error('Failed to delete room:', error); + return false; + } + }, + + // Producer actions (simplified - only remote/local camera) + async connectProducer(workspaceId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { + try { + const producer = new video.VideoProducer(settings.transportServerUrl); + + // Create or join room + const roomData = await producer.createRoom(workspaceId); + const connected = await producer.connect(roomData.workspaceId, roomData.roomId); + + if (!connected) { + throw new Error('Failed to connect producer'); + } + + // Start camera stream + const stream = await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // Update state + videoConnection.producer.connected = true; + videoConnection.producer.client = producer; + videoConnection.producer.roomId = roomData.roomId; + videoConnection.producer.stream = stream; + + // Refresh room list + await this.listRooms(workspaceId); + + return { success: true, roomId: roomData.roomId }; + } catch (error) { + console.error('Failed to connect producer:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectProducer(): Promise { + if (videoConnection.producer.client) { + videoConnection.producer.client.disconnect(); + } + if (videoConnection.producer.stream) { + videoConnection.producer.stream.getTracks().forEach(track => track.stop()); + } + + // Reset state + videoConnection.producer.connected = false; + videoConnection.producer.client = null; + videoConnection.producer.roomId = null; + videoConnection.producer.stream = null; + }, + + // Consumer actions (simplified - only remote consumer) + async connectConsumer(workspaceId: string, roomId: string): Promise<{ success: boolean; error?: string }> { + try { + const consumer = new video.VideoConsumer(settings.transportServerUrl); + const connected = await consumer.connect(workspaceId, roomId); + + if (!connected) { + throw new Error('Failed to connect consumer'); + } + + // Start receiving video + await consumer.startReceiving(); + + // Set up stream receiving + consumer.on('streamReceived', (stream: MediaStream) => { + videoConnection.consumer.stream = stream; + }); + + // Update state + videoConnection.consumer.connected = true; + videoConnection.consumer.client = consumer; + videoConnection.consumer.roomId = roomId; + + return { success: true }; + } catch (error) { + console.error('Failed to connect consumer:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectConsumer(): Promise { + if (videoConnection.consumer.client) { + videoConnection.consumer.client.disconnect(); + } + + // Reset state + videoConnection.consumer.connected = false; + videoConnection.consumer.client = null; + videoConnection.consumer.roomId = null; + videoConnection.consumer.stream = null; + }, + + // Utility functions + async refreshRooms(workspaceId: string): Promise { + await this.listRooms(workspaceId); + }, + + getAvailableRooms(): videoTypes.RoomInfo[] { + return videoConnection.rooms.filter(room => room.participants.producer !== null); + }, + + getRoomById(roomId: string): videoTypes.RoomInfo | undefined { + return videoConnection.rooms.find(room => room.id === roomId); + } +}; \ No newline at end of file diff --git a/src/lib/elements/video/videoStreaming.svelte.ts b/src/lib/elements/video/videoStreaming.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..2552198d348959bfddb659bfaf09792719c95980 --- /dev/null +++ b/src/lib/elements/video/videoStreaming.svelte.ts @@ -0,0 +1,220 @@ +/** + * Video Streaming System - Input/Output Architecture + * Clean separation between input sources and output destinations + */ + +import { video } from '@robohub/transport-server-client'; +import type { video as videoTypes } from '@robohub/transport-server-client'; +import { settings } from '$lib/runes/settings.svelte'; + +// Input/Output state using runes +export class VideoStreamingState { + // Input state (what you're viewing) + input = $state({ + type: null as 'local-camera' | 'remote-stream' | null, + stream: null as MediaStream | null, + client: null as videoTypes.VideoConsumer | null, + roomId: null as string | null, + }); + + // Output state (what you're broadcasting) + output = $state({ + active: false, + client: null as videoTypes.VideoProducer | null, + roomId: null as string | null, + }); + + // Room listing state + rooms = $state([]); + roomsLoading = $state(false); + + // Derived state + get hasInput() { + return this.input.type !== null && this.input.stream !== null; + } + + get hasOutput() { + return this.output.active; + } + + get canOutput() { + // Can only output if input is local camera (not remote stream) + return this.input.type === 'local-camera' && this.input.stream !== null; + } + + get currentStream() { + return this.input.stream; + } +} + +// Create global instance +export const videoStreaming = new VideoStreamingState(); + +// External action functions +export const videoActions = { + + // Room management + async listRooms(workspaceId: string): Promise { + videoStreaming.roomsLoading = true; + try { + const client = new video.VideoClientCore(settings.transportServerUrl); + const rooms = await client.listRooms(workspaceId); + videoStreaming.rooms = rooms; + return rooms; + } catch (error) { + console.error('Failed to list rooms:', error); + videoStreaming.rooms = []; + return []; + } finally { + videoStreaming.roomsLoading = false; + } + }, + + // Input actions + async connectLocalCamera(): Promise<{ success: boolean; error?: string }> { + try { + // Get local camera stream - no server connection needed for local viewing + const stream = await navigator.mediaDevices.getUserMedia({ + video: { width: 1280, height: 720 }, + audio: true + }); + + // First disconnect any existing input to avoid conflicts + await this.disconnectInput(); + + // Update input state - purely local, no server interaction + videoStreaming.input.type = 'local-camera'; + videoStreaming.input.stream = stream; + videoStreaming.input.client = null; + videoStreaming.input.roomId = null; + + console.log('Local camera connected (local viewing only)'); + return { success: true }; + } catch (error) { + console.error('Failed to connect local camera:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async connectRemoteStream(workspaceId: string, roomId: string): Promise<{ success: boolean; error?: string }> { + try { + // First disconnect any existing input + await this.disconnectInput(); + + const consumer = new video.VideoConsumer(settings.transportServerUrl); + const connected = await consumer.connect(workspaceId, roomId, 'consumer-id'); + + if (!connected) { + throw new Error('Failed to connect to remote stream'); + } + + // Start receiving video + await consumer.startReceiving(); + + // Set up stream receiving + consumer.on('streamReceived', (stream: MediaStream) => { + videoStreaming.input.stream = stream; + }); + + // Update input state + videoStreaming.input.type = 'remote-stream'; + videoStreaming.input.client = consumer; + videoStreaming.input.roomId = roomId; + + console.log('Remote stream connected'); + return { success: true }; + } catch (error) { + console.error('Failed to connect remote stream:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async disconnectInput(): Promise { + // Stop local camera tracks if any + if (videoStreaming.input.stream && videoStreaming.input.type === 'local-camera') { + videoStreaming.input.stream.getTracks().forEach(track => track.stop()); + } + + // Disconnect remote client if any + if (videoStreaming.input.client) { + videoStreaming.input.client.disconnect(); + } + + // Reset input state + videoStreaming.input.type = null; + videoStreaming.input.stream = null; + videoStreaming.input.client = null; + videoStreaming.input.roomId = null; + + console.log('Input disconnected'); + }, + + // Output actions + async startOutput(workspaceId: string): Promise<{ success: boolean; error?: string; roomId?: string }> { + if (!videoStreaming.canOutput) { + return { success: false, error: 'Cannot output - input must be local camera' }; + } + + try { + const producer = new video.VideoProducer(settings.transportServerUrl); + + // Create room + const roomData = await producer.createRoom(workspaceId); + const connected = await producer.connect(roomData.workspaceId, roomData.roomId, 'producer-id'); + + if (!connected) { + throw new Error('Failed to connect producer'); + } + + // Use the current input stream for output by starting camera with existing stream + if (videoStreaming.input.stream) { + // We need to use the producer's startCamera method properly + // For now, we'll start a new camera stream since we can't directly use existing stream + await producer.startCamera({ + video: { width: 1280, height: 720 }, + audio: true + }); + } + + // Update output state + videoStreaming.output.active = true; + videoStreaming.output.client = producer; + videoStreaming.output.roomId = roomData.roomId; + + // Refresh room list + await this.listRooms(workspaceId); + + console.log('Output started, room created:', roomData.roomId); + return { success: true, roomId: roomData.roomId }; + } catch (error) { + console.error('Failed to start output:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async stopOutput(): Promise { + if (videoStreaming.output.client) { + videoStreaming.output.client.disconnect(); + } + + // Reset output state + videoStreaming.output.active = false; + videoStreaming.output.client = null; + videoStreaming.output.roomId = null; + + console.log('Output stopped'); + }, + + // Utility functions + async refreshRooms(workspaceId: string): Promise { + await this.listRooms(workspaceId); + }, + + getAvailableRooms(): videoTypes.RoomInfo[] { + return videoStreaming.rooms.filter(room => room.participants.producer !== null); + }, + + getRoomById(roomId: string): videoTypes.RoomInfo | undefined { + return videoStreaming.rooms.find(room => room.id === roomId); + } +}; \ No newline at end of file diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..a34b2040f2eba76b389fbb72d743003be47eacc2 --- /dev/null +++ b/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor() { + super(`max-width: ${MOBILE_BREAKPOINT - 1}px`); + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..856f2b6c38aec1085db88189bcf492dbb49a1c45 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/runes/env.svelte.ts b/src/lib/runes/env.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..b389eab7e9c4e792c25f0f58082195c5adb0c565 --- /dev/null +++ b/src/lib/runes/env.svelte.ts @@ -0,0 +1,11 @@ +import type { UrdfRobotState } from "$lib/types/robot"; + +interface Environment { + robots: { + [robotId: string]: UrdfRobotState; + }; +} + +export const environment: Environment = $state({ + robots: {} +}); diff --git a/src/lib/runes/models.svelte.ts b/src/lib/runes/models.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..855dc3b23ef70233767dc22e7e849d7078f4225f --- /dev/null +++ b/src/lib/runes/models.svelte.ts @@ -0,0 +1,23 @@ +interface Modals { + settings: { + open: boolean; + }; + controls: { + open: boolean; + }; + info: { + open: boolean; + }; +} + +export const modals: Modals = $state({ + settings: { + open: false + }, + controls: { + open: false + }, + info: { + open: false + } +}); diff --git a/src/lib/runes/settings.svelte.ts b/src/lib/runes/settings.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..18ce863d9dbe205845919809d2eb7c383b6c8ebf --- /dev/null +++ b/src/lib/runes/settings.svelte.ts @@ -0,0 +1,12 @@ + +interface Settings { + inferenceServerUrl: string; + transportServerUrl: string; +} + +export const settings: Settings = $state({ + // inferenceServerUrl: 'http://localhost:8001', + // transportServerUrl: 'http://localhost:8000' + inferenceServerUrl: 'https://blanchon-robotinferenceserver.hf.space', + transportServerUrl: 'https://blanchon-robottransportserver.hf.space/api' +}); diff --git a/src/lib/sensors/consumers/RemoteServerConsumer.ts b/src/lib/sensors/consumers/RemoteServerConsumer.ts new file mode 100644 index 0000000000000000000000000000000000000000..dff4ba4d7d63fcd80206093e42ba551d476351ec --- /dev/null +++ b/src/lib/sensors/consumers/RemoteServerConsumer.ts @@ -0,0 +1,359 @@ +import type { + ConsumerSensorDriver, + ConnectionStatus, + SensorFrame, + SensorStream, + RemoteServerConsumerConfig, + FrameCallback, + StreamUpdateCallback, + StatusChangeCallback, + UnsubscribeFn +} from "../types/index.js"; + +/** + * Remote Server Consumer Driver + * + * Sends video frames to a remote Python server using WebSocket. + * Simplified with best practices - uses WebSocket only for optimal performance. + */ +export class RemoteServerConsumer implements ConsumerSensorDriver { + readonly type = "consumer" as const; + readonly id: string; + readonly name: string; + + private _status: ConnectionStatus = { isConnected: false }; + private config: RemoteServerConsumerConfig; + + // Connection management + private websocket: WebSocket | null = null; + private reconnectAttempts = 0; + private reconnectTimer?: number; + + // Stream management + private activeOutputStreams = new Map(); + private sendQueue: SensorFrame[] = []; + private isSending = false; + + // Event callbacks + private frameSentCallbacks: FrameCallback[] = []; + private streamUpdateCallbacks: StreamUpdateCallback[] = []; + private statusCallbacks: StatusChangeCallback[] = []; + + constructor(config: RemoteServerConsumerConfig) { + this.config = config; + this.id = `remote-server-consumer-${Date.now()}`; + this.name = `Remote Server Consumer (${config.url})`; + + console.log("📡 Created RemoteServer consumer driver for:", config.url); + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(): Promise { + console.log("📡 Connecting to remote server...", this.config.url); + + try { + await this.connectWebSocket(); + + this._status = { + isConnected: true, + lastConnected: new Date(), + error: undefined + }; + this.notifyStatusChange(); + + console.log("✅ Remote server consumer connected successfully"); + } catch (error) { + this._status = { + isConnected: false, + error: `Connection failed: ${error}` + }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log("📡 Disconnecting from remote server..."); + + // Clear reconnect timer + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = undefined; + } + + // Close WebSocket + if (this.websocket) { + this.websocket.close(); + this.websocket = null; + } + + // Clear send queue + this.sendQueue = []; + this.isSending = false; + + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log("✅ Remote server consumer disconnected"); + } + + async sendFrame(frame: SensorFrame): Promise { + if (!this._status.isConnected) { + throw new Error("Cannot send frame: consumer not connected"); + } + + // Add to send queue + this.sendQueue.push(frame); + + // Process queue if not already sending + if (!this.isSending) { + await this.processSendQueue(); + } + } + + async sendFrames(frames: SensorFrame[]): Promise { + if (!this._status.isConnected) { + throw new Error("Cannot send frames: consumer not connected"); + } + + // Add all frames to queue + this.sendQueue.push(...frames); + + // Process queue if not already sending + if (!this.isSending) { + await this.processSendQueue(); + } + } + + async startOutputStream(stream: SensorStream): Promise { + console.log("📡 Starting output stream:", stream.id); + + this.activeOutputStreams.set(stream.id, stream); + this.notifyStreamUpdate(stream); + + // Send stream start message to server + await this.sendControlMessage({ + type: "stream_start", + streamId: stream.id, + streamConfig: stream.config + }); + } + + async stopOutputStream(streamId: string): Promise { + console.log("📡 Stopping output stream:", streamId); + + const stream = this.activeOutputStreams.get(streamId); + if (stream) { + stream.active = false; + stream.endTime = new Date(); + this.activeOutputStreams.delete(streamId); + this.notifyStreamUpdate(stream); + + // Send stream stop message to server + await this.sendControlMessage({ + type: "stream_stop", + streamId + }); + } + } + + getActiveOutputStreams(): SensorStream[] { + return Array.from(this.activeOutputStreams.values()); + } + + // Event subscription methods + onFrameSent(callback: FrameCallback): UnsubscribeFn { + this.frameSentCallbacks.push(callback); + return () => { + const index = this.frameSentCallbacks.indexOf(callback); + if (index >= 0) { + this.frameSentCallbacks.splice(index, 1); + } + }; + } + + onStreamUpdate(callback: StreamUpdateCallback): UnsubscribeFn { + this.streamUpdateCallbacks.push(callback); + return () => { + const index = this.streamUpdateCallbacks.indexOf(callback); + if (index >= 0) { + this.streamUpdateCallbacks.splice(index, 1); + } + }; + } + + onStatusChange(callback: StatusChangeCallback): UnsubscribeFn { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + // Private connection methods + private async connectWebSocket(): Promise { + return new Promise((resolve, reject) => { + const wsUrl = this.config.url.replace(/^http/, "ws") + "/video-stream"; + + this.websocket = new WebSocket(wsUrl); + this.websocket.binaryType = "arraybuffer"; + + this.websocket.onopen = () => { + console.log("✅ WebSocket connected to remote server"); + this.reconnectAttempts = 0; + resolve(); + }; + + this.websocket.onclose = (event) => { + console.log("🔌 WebSocket disconnected:", event.code, event.reason); + this.handleConnectionLoss(); + }; + + this.websocket.onerror = (error) => { + console.error("❌ WebSocket error:", error); + reject(new Error("WebSocket connection failed")); + }; + + this.websocket.onmessage = (event) => { + this.handleServerMessage(event.data); + }; + }); + } + + private async processSendQueue(): Promise { + if (this.isSending || this.sendQueue.length === 0) { + return; + } + + this.isSending = true; + + try { + while (this.sendQueue.length > 0) { + const frame = this.sendQueue.shift()!; + await this.transmitFrame(frame); + this.notifyFrameSent(frame); + } + } catch (error) { + console.error("❌ Error processing send queue:", error); + this._status.error = `Send error: ${error}`; + this.notifyStatusChange(); + } finally { + this.isSending = false; + } + } + + private async transmitFrame(frame: SensorFrame): Promise { + if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { + throw new Error("WebSocket not available for transmission"); + } + + // Prepare metadata header + const header = JSON.stringify({ + type: "video_frame", + timestamp: frame.timestamp, + frameType: frame.type, + metadata: frame.metadata, + streamId: this.config.streamId + }); + + const headerBuffer = new TextEncoder().encode(header); + const headerLengthBuffer = new Uint32Array([headerBuffer.length]).buffer; // 4-byte length prefix + + let dataBuffer: ArrayBuffer; + if (frame.data instanceof Blob) { + dataBuffer = await frame.data.arrayBuffer(); + } else { + dataBuffer = frame.data as ArrayBuffer; + } + + // Concatenate: [length][header][data] + const packet = new Uint8Array(headerLengthBuffer.byteLength + headerBuffer.byteLength + dataBuffer.byteLength); + packet.set(new Uint8Array(headerLengthBuffer), 0); + packet.set(new Uint8Array(headerBuffer), headerLengthBuffer.byteLength); + packet.set(new Uint8Array(dataBuffer), headerLengthBuffer.byteLength + headerBuffer.byteLength); + + this.websocket.send(packet.buffer); + } + + private async sendControlMessage(message: Record): Promise { + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { + this.websocket.send(JSON.stringify(message)); + } + } + + private handleServerMessage(data: string | ArrayBuffer): void { + try { + const message = typeof data === "string" ? JSON.parse(data) : data; + console.log("📨 Received server message:", message); + + // Handle server responses, status updates, etc. + if (message.type === "status") { + this._status.bitrate = message.bitrate; + this._status.frameRate = message.frameRate; + this.notifyStatusChange(); + } + } catch (error) { + console.error("❌ Error parsing server message:", error); + } + } + + private handleConnectionLoss(): void { + this._status.isConnected = false; + this._status.error = "Connection lost"; + this.notifyStatusChange(); + + // Attempt reconnection + const maxRetries = this.config.retryAttempts || 5; + const retryDelay = this.config.retryDelay || 2000; + + if (this.reconnectAttempts < maxRetries) { + this.reconnectAttempts++; + console.log(`🔄 Attempting reconnection ${this.reconnectAttempts}/${maxRetries} in ${retryDelay}ms`); + + this.reconnectTimer = setTimeout(async () => { + try { + await this.connect(); + } catch (error) { + console.error("❌ Reconnection failed:", error); + } + }, retryDelay); + } else { + console.error("❌ Max reconnection attempts reached"); + } + } + + private notifyFrameSent(frame: SensorFrame): void { + this.frameSentCallbacks.forEach((callback) => { + try { + callback(frame); + } catch (error) { + console.error("Error in frame sent callback:", error); + } + }); + } + + private notifyStreamUpdate(stream: SensorStream): void { + this.streamUpdateCallbacks.forEach((callback) => { + try { + callback(stream); + } catch (error) { + console.error("Error in stream update callback:", error); + } + }); + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach((callback) => { + try { + callback(this._status); + } catch (error) { + console.error("Error in status change callback:", error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/sensors/consumers/WebRTCConsumer.ts b/src/lib/sensors/consumers/WebRTCConsumer.ts new file mode 100644 index 0000000000000000000000000000000000000000..f73f8f64250f03d0526c18dc0c2236bdfed611b8 --- /dev/null +++ b/src/lib/sensors/consumers/WebRTCConsumer.ts @@ -0,0 +1,200 @@ +import type { + ConsumerSensorDriver, + ConnectionStatus, + SensorFrame, + SensorStream, + FrameCallback, + StreamUpdateCallback, + StatusChangeCallback, + UnsubscribeFn +} from "../types/index.js"; + +export interface WebRTCConsumerConfig { + type: "webrtc-consumer"; + signalingUrl: string; // ws://host:port/signaling + streamId?: string; +} + +export class WebRTCConsumer implements ConsumerSensorDriver { + readonly type = "consumer" as const; + readonly id: string; + readonly name = "WebRTC Consumer"; + + private config: WebRTCConsumerConfig; + private _status: ConnectionStatus = { isConnected: false }; + + private pc: RTCPeerConnection | null = null; + private dc: RTCDataChannel | null = null; + private signaling?: WebSocket; + + private frameSentCallbacks: FrameCallback[] = []; + private streamUpdateCallbacks: StreamUpdateCallback[] = []; + private statusCallbacks: StatusChangeCallback[] = []; + + private activeStreams = new Map(); + private sendQueue: SensorFrame[] = []; + private isSending = false; + + private readonly BUFFER_WATERMARK = 4 * 1024 * 1024; // 4 MB + + constructor(config: WebRTCConsumerConfig) { + this.config = config; + this.id = `webrtc-consumer-${Date.now()}`; + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(): Promise { + // open signaling + this.signaling = new WebSocket(this.config.signalingUrl); + await new Promise((res, rej) => { + this.signaling!.onopen = () => res(); + this.signaling!.onerror = rej; + }); + + // create pc + this.pc = new RTCPeerConnection({ + iceServers: [{ urls: "stun:stun.l.google.com:19302" }] + }); + + // datachannel + this.dc = this.pc.createDataChannel("video", { + ordered: false, + maxRetransmits: 0 + }); + this.dc.binaryType = "arraybuffer"; + this.dc.onopen = () => { + this._status = { isConnected: true, lastConnected: new Date() }; + this.notifyStatus(); + this.flushQueue(); + }; + this.dc.onclose = () => { + this._status = { isConnected: false, error: "DC closed" }; + this.notifyStatus(); + }; + + // ICE - Trickle-ICE for faster startup + this.pc.onicecandidate = (ev) => { + if (ev.candidate) { + this.signaling!.send(JSON.stringify({ + type: "ice", + candidate: ev.candidate.toJSON() + })); + } else { + // Send end-of-candidates marker + this.signaling!.send(JSON.stringify({ + type: "ice", + candidate: { end: true } + })); + } + }; + + // signaling messages + this.signaling.onmessage = async (ev) => { + const msg = JSON.parse(ev.data); + if (msg.type === "answer") { + await this.pc!.setRemoteDescription({ type: "answer", sdp: msg.sdp }); + } else if (msg.type === "ice") { + // Handle end-of-candidates marker + if (msg.candidate?.end) { + // ICE gathering complete on remote side + return; + } + await this.pc!.addIceCandidate(msg.candidate); + } + }; + + // create offer immediately (trickle-ICE) + const offer = await this.pc.createOffer(); + await this.pc.setLocalDescription(offer); + this.signaling.send(JSON.stringify({ type: "offer", sdp: offer.sdp })); + } + + async disconnect(): Promise { + if (this.dc) this.dc.close(); + if (this.pc) this.pc.close(); + if (this.signaling) this.signaling.close(); + this._status = { isConnected: false }; + this.notifyStatus(); + } + + // ConsumerSensorDriver impl + async sendFrame(frame: SensorFrame): Promise { + if (!this.dc || this.dc.readyState !== "open") { + throw new Error("DataChannel not open"); + } + this.sendQueue.push(frame); + this.flushQueue(); + } + + async sendFrames(frames: SensorFrame[]): Promise { + this.sendQueue.push(...frames); + this.flushQueue(); + } + + async startOutputStream(stream: SensorStream): Promise { + this.activeStreams.set(stream.id, stream); + this.notifyStream(stream); + } + async stopOutputStream(streamId: string): Promise { + const s = this.activeStreams.get(streamId); + if (s) { + s.active = false; this.activeStreams.delete(streamId); this.notifyStream(s); + } + } + getActiveOutputStreams(): SensorStream[] { return Array.from(this.activeStreams.values()); } + + // no-op for onFrameSent etc. + onFrameSent(cb: FrameCallback): UnsubscribeFn { this.frameSentCallbacks.push(cb); return () => this.pull(this.frameSentCallbacks, cb); } + onStreamUpdate(cb: StreamUpdateCallback): UnsubscribeFn { this.streamUpdateCallbacks.push(cb); return () => this.pull(this.streamUpdateCallbacks, cb); } + onStatusChange(cb: StatusChangeCallback): UnsubscribeFn { this.statusCallbacks.push(cb); return () => this.pull(this.statusCallbacks, cb); } + + // helpers + private flushQueue() { + if (!this.dc || this.dc.readyState !== "open") return; + while (this.sendQueue.length && this.dc.bufferedAmount < this.BUFFER_WATERMARK) { + const frame = this.sendQueue.shift()!; + const packet = this.frameToPacket(frame); + this.dc.send(packet); + this.frameSentCallbacks.forEach((c) => c(frame)); + } + } + + private frameToPacket(frame: SensorFrame): ArrayBuffer { + const headerObj = { + type: "video_frame", + timestamp: frame.timestamp, + frameType: frame.type, + metadata: frame.metadata, + streamId: this.config.streamId + }; + const headerJson = JSON.stringify(headerObj); + const headerBuf = new TextEncoder().encode(headerJson); + const lenBuf = new Uint32Array([headerBuf.length]).buffer; + let dataBuf: ArrayBuffer; + if (frame.data instanceof Blob) { + // this is sync because MediaRecorder gives Blob slices pre-gathered + // but we need async arrayBuffer — already spec returns Promise + return frame.data.arrayBuffer().then((buf) => { + const out = new Uint8Array(lenBuf.byteLength + headerBuf.length + buf.byteLength); + out.set(new Uint8Array(lenBuf), 0); + out.set(headerBuf, lenBuf.byteLength); + out.set(new Uint8Array(buf), lenBuf.byteLength + headerBuf.length); + return out.buffer; + }) as unknown as ArrayBuffer; // caller handles async in flushQueue loop by awaiting? For now assume ArrayBuffer path (MediaRecorder provides ArrayBuffer in config) + } else { + dataBuf = frame.data as ArrayBuffer; + } + const out = new Uint8Array(lenBuf.byteLength + headerBuf.length + dataBuf.byteLength); + out.set(new Uint8Array(lenBuf), 0); + out.set(headerBuf, lenBuf.byteLength); + out.set(new Uint8Array(dataBuf), lenBuf.byteLength + headerBuf.length); + return out.buffer; + } + + private pull(arr: T[], item: T) { const i = arr.indexOf(item); if (i >= 0) arr.splice(i, 1); } + private notifyStream(s: SensorStream) { this.streamUpdateCallbacks.forEach((c) => c(s)); } + private notifyStatus() { this.statusCallbacks.forEach((c) => c(this._status)); } +} \ No newline at end of file diff --git a/src/lib/sensors/consumers/index.ts b/src/lib/sensors/consumers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8179ba9a3acc34029d67f0d40a08305b26027224 --- /dev/null +++ b/src/lib/sensors/consumers/index.ts @@ -0,0 +1,8 @@ +/** + * Consumer Sensor Drivers - Main Export + * + * Central export point for all consumer sensor driver implementations + */ + +export { RemoteServerConsumer } from "./RemoteServerConsumer.js"; +export { WebRTCConsumer } from "./WebRTCConsumer.js"; \ No newline at end of file diff --git a/src/lib/sensors/index.ts b/src/lib/sensors/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7398d64346585435754aea5d7415dee19b11484c --- /dev/null +++ b/src/lib/sensors/index.ts @@ -0,0 +1,14 @@ +/** + * Sensor Drivers - Main Export + * + * Central export point for all sensor driver types and implementations + */ + +// Types +export type * from "./types/index.js"; + +// Producer drivers +export * from "./producers/index.js"; + +// Consumer drivers +export * from "./consumers/index.js"; \ No newline at end of file diff --git a/src/lib/sensors/producers/MediaRecorderProducer.ts b/src/lib/sensors/producers/MediaRecorderProducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..43f6a8d05bc9d6091fdb67885434164accdeb6e4 --- /dev/null +++ b/src/lib/sensors/producers/MediaRecorderProducer.ts @@ -0,0 +1,381 @@ +import type { + ProducerSensorDriver, + ConnectionStatus, + SensorFrame, + SensorStream, + VideoStreamConfig, + MediaRecorderProducerConfig, + FrameCallback, + StreamUpdateCallback, + StatusChangeCallback, + UnsubscribeFn +} from "../types/index.js"; + +/** + * MediaRecorder Producer Driver + * + * Captures video/audio from browser MediaDevices using MediaRecorder API. + * Simplified with best practices - uses WebM format and optimized settings. + */ +export class MediaRecorderProducer implements ProducerSensorDriver { + readonly type = "producer" as const; + readonly id: string; + readonly name: string; + + private _status: ConnectionStatus = { isConnected: false }; + private config: MediaRecorderProducerConfig; + + // MediaRecorder state + private mediaStream: MediaStream | null = null; + private mediaRecorder: MediaRecorder | null = null; + private recordingDataChunks: Blob[] = []; + + // Stream management + private activeStreams = new Map(); + + // Event callbacks + private frameCallbacks: FrameCallback[] = []; + private streamUpdateCallbacks: StreamUpdateCallback[] = []; + private statusCallbacks: StatusChangeCallback[] = []; + + constructor(config: MediaRecorderProducerConfig) { + this.config = config; + this.id = `media-recorder-${Date.now()}`; + this.name = "MediaRecorder Producer"; + + console.log("🎥 Created MediaRecorder producer driver"); + } + + get status(): ConnectionStatus { + return this._status; + } + + async connect(): Promise { + console.log("🎥 Connecting MediaRecorder producer..."); + + try { + // Check if browser supports MediaRecorder + if (!MediaRecorder.isTypeSupported) { + throw new Error("MediaRecorder not supported in this browser"); + } + + // Test basic media access + const testStream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + + // Close test stream immediately + testStream.getTracks().forEach(track => track.stop()); + + this._status = { + isConnected: true, + lastConnected: new Date(), + error: undefined + }; + this.notifyStatusChange(); + + console.log("✅ MediaRecorder producer connected successfully"); + } catch (error) { + this._status = { + isConnected: false, + error: `Connection failed: ${error}` + }; + this.notifyStatusChange(); + throw error; + } + } + + async disconnect(): Promise { + console.log("🎥 Disconnecting MediaRecorder producer..."); + + // Stop all active streams + for (const streamId of this.activeStreams.keys()) { + await this.stopStream(streamId); + } + + this._status = { isConnected: false }; + this.notifyStatusChange(); + + console.log("✅ MediaRecorder producer disconnected"); + } + + async startStream(config: VideoStreamConfig): Promise { + if (!this._status.isConnected) { + throw new Error("Cannot start stream: producer not connected"); + } + + console.log("🎥 Starting MediaRecorder stream...", config); + + try { + // Prepare media constraints with best practices + const constraints: MediaStreamConstraints = { + video: { + width: config.width || 1280, + height: config.height || 720, + frameRate: config.frameRate || 30, + facingMode: config.facingMode || "user", + ...(config.deviceId && { deviceId: config.deviceId }) + }, + audio: true, + ...this.config.constraints + }; + + // Get media stream + this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + + // Create MediaRecorder with optimized WebM settings + const mimeType = this.getBestWebMType(); + this.mediaRecorder = new MediaRecorder(this.mediaStream, { + mimeType, + videoBitsPerSecond: this.config.videoBitsPerSecond || 2500000, + audioBitsPerSecond: this.config.audioBitsPerSecond || 128000 + }); + + // Create stream object + const stream: SensorStream = { + id: `stream-${Date.now()}`, + name: `MediaRecorder Stream ${config.width}x${config.height}`, + type: "video", + config, + active: true, + startTime: new Date(), + totalFrames: 0 + }; + + this.activeStreams.set(stream.id, stream); + + // Set up MediaRecorder event handlers + this.setupMediaRecorderEvents(stream); + + // Start recording with optimized interval + const recordingInterval = this.config.recordingInterval || 100; + this.mediaRecorder.start(recordingInterval); + + // Update status with stream info + this._status.frameRate = config.frameRate; + this._status.bitrate = this.config.videoBitsPerSecond; + this.notifyStatusChange(); + + this.notifyStreamUpdate(stream); + + console.log(`✅ MediaRecorder stream started: ${stream.id}`); + return stream; + + } catch (error) { + console.error("❌ Failed to start MediaRecorder stream:", error); + throw error; + } + } + + async stopStream(streamId: string): Promise { + console.log(`🎥 Stopping MediaRecorder stream: ${streamId}`); + + const stream = this.activeStreams.get(streamId); + if (!stream) { + throw new Error(`Stream not found: ${streamId}`); + } + + try { + // Stop MediaRecorder + if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { + this.mediaRecorder.stop(); + } + + // Stop media stream tracks + if (this.mediaStream) { + this.mediaStream.getTracks().forEach(track => track.stop()); + this.mediaStream = null; + } + + // Update stream + stream.active = false; + stream.endTime = new Date(); + + this.activeStreams.delete(streamId); + this.notifyStreamUpdate(stream); + + console.log(`✅ MediaRecorder stream stopped: ${streamId}`); + + } catch (error) { + console.error(`❌ Failed to stop stream ${streamId}:`, error); + throw error; + } + } + + async pauseStream(streamId: string): Promise { + console.log(`⏸️ Pausing MediaRecorder stream: ${streamId}`); + + const stream = this.activeStreams.get(streamId); + if (!stream) { + throw new Error(`Stream not found: ${streamId}`); + } + + if (this.mediaRecorder && this.mediaRecorder.state === "recording") { + this.mediaRecorder.pause(); + this.notifyStreamUpdate(stream); + } + } + + async resumeStream(streamId: string): Promise { + console.log(`▶️ Resuming MediaRecorder stream: ${streamId}`); + + const stream = this.activeStreams.get(streamId); + if (!stream) { + throw new Error(`Stream not found: ${streamId}`); + } + + if (this.mediaRecorder && this.mediaRecorder.state === "paused") { + this.mediaRecorder.resume(); + this.notifyStreamUpdate(stream); + } + } + + getActiveStreams(): SensorStream[] { + return Array.from(this.activeStreams.values()); + } + + // Event subscription methods + onFrame(callback: FrameCallback): UnsubscribeFn { + this.frameCallbacks.push(callback); + return () => { + const index = this.frameCallbacks.indexOf(callback); + if (index >= 0) { + this.frameCallbacks.splice(index, 1); + } + }; + } + + onStreamUpdate(callback: StreamUpdateCallback): UnsubscribeFn { + this.streamUpdateCallbacks.push(callback); + return () => { + const index = this.streamUpdateCallbacks.indexOf(callback); + if (index >= 0) { + this.streamUpdateCallbacks.splice(index, 1); + } + }; + } + + onStatusChange(callback: StatusChangeCallback): UnsubscribeFn { + this.statusCallbacks.push(callback); + return () => { + const index = this.statusCallbacks.indexOf(callback); + if (index >= 0) { + this.statusCallbacks.splice(index, 1); + } + }; + } + + // Private helper methods + private setupMediaRecorderEvents(stream: SensorStream): void { + if (!this.mediaRecorder) return; + + this.mediaRecorder.ondataavailable = (event) => { + if (event.data && event.data.size > 0) { + this.recordingDataChunks.push(event.data); + + // Create frame from chunk + const frame: SensorFrame = { + timestamp: Date.now(), + type: "video", + data: event.data, + metadata: { + width: stream.config.width, + height: stream.config.height, + frameRate: stream.config.frameRate, + codec: "webm", + bitrate: this.config.videoBitsPerSecond + } + }; + + // Update stream stats + stream.totalFrames = (stream.totalFrames || 0) + 1; + + // Notify frame callbacks + this.notifyFrame(frame); + } + }; + + this.mediaRecorder.onstop = () => { + console.log("🎥 MediaRecorder stopped"); + + // Create final frame with complete recording + if (this.recordingDataChunks.length > 0) { + const finalBlob = new Blob(this.recordingDataChunks, { + type: "video/webm" + }); + + const finalFrame: SensorFrame = { + timestamp: Date.now(), + type: "video", + data: finalBlob, + metadata: { + width: stream.config.width, + height: stream.config.height, + codec: "webm", + isComplete: true, + totalSize: finalBlob.size + } + }; + + this.notifyFrame(finalFrame); + } + + // Clear chunks + this.recordingDataChunks = []; + }; + + this.mediaRecorder.onerror = (event) => { + console.error("❌ MediaRecorder error:", event); + this._status.error = "Recording error occurred"; + this.notifyStatusChange(); + }; + } + + private getBestWebMType(): string { + // Best WebM types in order of preference + const types = [ + "video/webm;codecs=vp9,opus", + "video/webm;codecs=vp8,opus", + "video/webm" + ]; + + for (const type of types) { + if (MediaRecorder.isTypeSupported(type)) { + return type; + } + } + + return "video/webm"; // Fallback + } + + private notifyFrame(frame: SensorFrame): void { + this.frameCallbacks.forEach((callback) => { + try { + callback(frame); + } catch (error) { + console.error("Error in frame callback:", error); + } + }); + } + + private notifyStreamUpdate(stream: SensorStream): void { + this.streamUpdateCallbacks.forEach((callback) => { + try { + callback(stream); + } catch (error) { + console.error("Error in stream update callback:", error); + } + }); + } + + private notifyStatusChange(): void { + this.statusCallbacks.forEach((callback) => { + try { + callback(this._status); + } catch (error) { + console.error("Error in status change callback:", error); + } + }); + } +} \ No newline at end of file diff --git a/src/lib/sensors/producers/MediaRecorderProducer.ts.recommendation.md b/src/lib/sensors/producers/MediaRecorderProducer.ts.recommendation.md new file mode 100644 index 0000000000000000000000000000000000000000..30e1bbdc50355292699f5e29e34c0f152c645015 --- /dev/null +++ b/src/lib/sensors/producers/MediaRecorderProducer.ts.recommendation.md @@ -0,0 +1,663 @@ +# MediaRecorderProducer.ts Performance Optimization Recommendations + +## Current Analysis +The MediaRecorderProducer manages video/audio capture using the MediaRecorder API. While functional, the current implementation has performance bottlenecks in memory management, frame processing, and resource cleanup that need optimization for high-performance video streaming. + +## Critical Performance Issues + +### 1. **Memory Leak in Blob Accumulation** +- **Problem**: `recordingDataChunks` array grows unbounded during recording +- **Impact**: Memory usage increases continuously, causing browser crashes +- **Solution**: Implement chunk processing and circular buffer management + +### 2. **Inefficient Frame Processing** +- **Problem**: No frame skipping or quality adaptation based on performance +- **Impact**: Performance degradation under high load or low-end devices +- **Solution**: Implement adaptive frame rate and quality control + +### 3. **Blocking Stream Operations** +- **Problem**: Stream start/stop operations can block the main thread +- **Impact**: UI freezes during media operations +- **Solution**: Use async operations with proper task scheduling + +### 4. **No Connection Pooling** +- **Problem**: New MediaStream created for each connection attempt +- **Impact**: Unnecessary resource allocation and slower startup +- **Solution**: Implement stream reuse and connection pooling + +## Recommended Optimizations + +### 1. **Implement Memory-Efficient Chunk Management** +```typescript +interface OptimizedMediaRecorderProducer extends ProducerSensorDriver { + // Performance configuration + private readonly maxChunkBufferSize: number; + private readonly chunkProcessingInterval: number; + private readonly memoryThresholdMB: number; + + // Optimized state management + private chunkBuffer: CircularBuffer; + private frameProcessor: FrameProcessor; + private memoryMonitor: MemoryMonitor; + private performanceMetrics: ProducerMetrics; +} + +class CircularBuffer { + private buffer: T[]; + private head = 0; + private tail = 0; + private size = 0; + + constructor(private capacity: number) { + this.buffer = new Array(capacity); + } + + push(item: T): T | null { + const evicted = this.size === this.capacity ? this.buffer[this.tail] : null; + + this.buffer[this.head] = item; + this.head = (this.head + 1) % this.capacity; + + if (this.size === this.capacity) { + this.tail = (this.tail + 1) % this.capacity; + } else { + this.size++; + } + + return evicted; + } + + toArray(): T[] { + const result: T[] = []; + for (let i = 0; i < this.size; i++) { + const index = (this.tail + i) % this.capacity; + result.push(this.buffer[index]); + } + return result; + } + + clear(): void { + this.head = 0; + this.tail = 0; + this.size = 0; + } +} + +export class OptimizedMediaRecorderProducer implements ProducerSensorDriver { + readonly type = "producer" as const; + readonly id: string; + readonly name: string; + + private _status: ConnectionStatus = { isConnected: false }; + private config: MediaRecorderProducerConfig; + + // Optimized state management + private mediaStream: MediaStream | null = null; + private mediaRecorder: MediaRecorder | null = null; + + // Memory-efficient chunk management + private chunkBuffer: CircularBuffer; + private chunkProcessingInterval: number | null = null; + private lastChunkProcessTime = 0; + + // Performance monitoring + private frameProcessor: FrameProcessor; + private memoryMonitor: MemoryMonitor; + private performanceMetrics: ProducerMetrics; + + // Configuration constants + private readonly maxChunkBufferSize = 10; // Maximum chunks in buffer + private readonly chunkProcessingIntervalMs = 50; // Process chunks every 50ms + private readonly memoryThresholdMB = 100; // Alert at 100MB memory usage + + constructor(config: MediaRecorderProducerConfig) { + this.config = config; + this.id = `optimized-media-recorder-${Date.now()}`; + this.name = "Optimized MediaRecorder Producer"; + + // Initialize optimized components + this.chunkBuffer = new CircularBuffer(this.maxChunkBufferSize); + this.frameProcessor = new FrameProcessor(); + this.memoryMonitor = new MemoryMonitor(this.memoryThresholdMB); + this.performanceMetrics = new ProducerMetrics(); + + this.startPerformanceMonitoring(); + } +} +``` + +### 2. **Implement Adaptive Frame Processing** +```typescript +interface FrameProcessingConfig { + targetFPS: number; + qualityThreshold: number; + adaptiveQuality: boolean; + maxProcessingTime: number; // ms +} + +class FrameProcessor { + private config: FrameProcessingConfig; + private frameDropCount = 0; + private lastFrameTime = 0; + private processingTimes: number[] = []; + private currentQuality = 1.0; + + // Frame rate control + private targetFrameInterval: number; + private lastFrameProcessed = 0; + + constructor(config: FrameProcessingConfig) { + this.config = config; + this.targetFrameInterval = 1000 / config.targetFPS; + } + + shouldProcessFrame(timestamp: number): boolean { + // Frame rate limiting + if (timestamp - this.lastFrameProcessed < this.targetFrameInterval) { + return false; + } + + // Performance-based frame skipping + const avgProcessingTime = this.getAverageProcessingTime(); + if (avgProcessingTime > this.config.maxProcessingTime) { + this.frameDropCount++; + + // Adaptive quality reduction + if (this.config.adaptiveQuality && this.frameDropCount > 5) { + this.reduceQuality(); + this.frameDropCount = 0; + } + + return false; + } + + this.lastFrameProcessed = timestamp; + return true; + } + + processFrame(blob: Blob, timestamp: number): Promise { + const startTime = performance.now(); + + return new Promise((resolve, reject) => { + // Use transferable objects for better performance + const reader = new FileReader(); + + reader.onload = () => { + try { + const arrayBuffer = reader.result as ArrayBuffer; + + // Create optimized frame object + const frame: SensorFrame = { + id: `frame-${timestamp}`, + type: "video", + data: arrayBuffer, + timestamp, + size: blob.size, + metadata: { + quality: this.currentQuality, + processingTime: performance.now() - startTime, + frameDropCount: this.frameDropCount + } + }; + + // Track processing time + this.recordProcessingTime(performance.now() - startTime); + + resolve(frame); + } catch (error) { + reject(error); + } + }; + + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(blob); + }); + } + + private recordProcessingTime(time: number): void { + this.processingTimes.push(time); + + // Keep only recent measurements + if (this.processingTimes.length > 30) { + this.processingTimes.shift(); + } + } + + private getAverageProcessingTime(): number { + if (this.processingTimes.length === 0) return 0; + + const sum = this.processingTimes.reduce((a, b) => a + b, 0); + return sum / this.processingTimes.length; + } + + private reduceQuality(): void { + this.currentQuality = Math.max(0.3, this.currentQuality * 0.8); + console.warn(`🎥 Reducing video quality to ${(this.currentQuality * 100).toFixed(0)}%`); + } + + getCurrentQuality(): number { + return this.currentQuality; + } + + resetQuality(): void { + this.currentQuality = 1.0; + this.frameDropCount = 0; + } +} +``` + +### 3. **Add Performance Memory Monitoring** +```typescript +interface MemoryStats { + usedJSHeapSize: number; + totalJSHeapSize: number; + jsHeapSizeLimit: number; + chunkBufferSize: number; + activeStreams: number; +} + +class MemoryMonitor { + private memoryThresholdMB: number; + private checkInterval: number | null = null; + private lastWarningTime = 0; + private readonly warningCooldown = 30000; // 30 seconds + + constructor(thresholdMB: number) { + this.memoryThresholdMB = thresholdMB * 1024 * 1024; // Convert to bytes + this.startMonitoring(); + } + + startMonitoring(): void { + this.checkInterval = setInterval(() => { + this.checkMemoryUsage(); + }, 5000) as any; // Check every 5 seconds + } + + stopMonitoring(): void { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + } + + private checkMemoryUsage(): void { + const memoryInfo = this.getMemoryInfo(); + + if (memoryInfo.usedJSHeapSize > this.memoryThresholdMB) { + const now = Date.now(); + + if (now - this.lastWarningTime > this.warningCooldown) { + console.warn('🚨 High memory usage detected:', { + used: `${(memoryInfo.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB`, + total: `${(memoryInfo.totalJSHeapSize / 1024 / 1024).toFixed(1)}MB`, + limit: `${(memoryInfo.jsHeapSizeLimit / 1024 / 1024).toFixed(1)}MB` + }); + + this.lastWarningTime = now; + + // Trigger garbage collection if available + this.requestGarbageCollection(); + } + } + } + + getMemoryInfo(): MemoryStats { + const performance = window.performance as any; + const memoryInfo = performance.memory || { + usedJSHeapSize: 0, + totalJSHeapSize: 0, + jsHeapSizeLimit: 0 + }; + + return { + usedJSHeapSize: memoryInfo.usedJSHeapSize, + totalJSHeapSize: memoryInfo.totalJSHeapSize, + jsHeapSizeLimit: memoryInfo.jsHeapSizeLimit, + chunkBufferSize: 0, // Will be updated by producer + activeStreams: 0 // Will be updated by producer + }; + } + + private requestGarbageCollection(): void { + // Request garbage collection if available (Chrome DevTools) + if ('gc' in window) { + (window as any).gc(); + } + + // Also manually trigger some cleanup + this.manualCleanup(); + } + + private manualCleanup(): void { + // Force cleanup of any large objects + if (typeof window !== 'undefined') { + // Clear any cached data + setTimeout(() => { + // This gives time for the GC to run + }, 100); + } + } +} +``` + +### 4. **Optimize Stream Management with Connection Pooling** +```typescript +interface StreamPool { + availableStreams: Map; + activeConnections: Map; + maxPoolSize: number; + streamTTL: number; // Time to live in ms +} + +class OptimizedStreamManager { + private streamPool: StreamPool; + private cleanupInterval: number | null = null; + + constructor() { + this.streamPool = { + availableStreams: new Map(), + activeConnections: new Map(), + maxPoolSize: 5, + streamTTL: 300000 // 5 minutes + }; + + this.startPoolCleanup(); + } + + async getOptimizedStream(config: VideoStreamConfig): Promise { + const configKey = this.getConfigKey(config); + + // Try to reuse existing stream + const cachedStream = this.streamPool.availableStreams.get(configKey); + if (cachedStream && this.isStreamValid(cachedStream)) { + this.streamPool.availableStreams.delete(configKey); + this.streamPool.activeConnections.set(configKey, { + stream: cachedStream, + lastUsed: Date.now() + }); + + console.log(`♻️ Reusing cached media stream: ${configKey}`); + return cachedStream; + } + + // Create new stream with optimized constraints + const optimizedConstraints = this.optimizeConstraints(config); + const stream = await navigator.mediaDevices.getUserMedia(optimizedConstraints); + + this.streamPool.activeConnections.set(configKey, { + stream, + lastUsed: Date.now() + }); + + console.log(`🆕 Created new media stream: ${configKey}`); + return stream; + } + + releaseStream(stream: MediaStream, config: VideoStreamConfig): void { + const configKey = this.getConfigKey(config); + const connection = this.streamPool.activeConnections.get(configKey); + + if (connection && connection.stream === stream) { + this.streamPool.activeConnections.delete(configKey); + + // Add to available pool if under limit + if (this.streamPool.availableStreams.size < this.streamPool.maxPoolSize) { + this.streamPool.availableStreams.set(configKey, stream); + console.log(`📦 Cached media stream: ${configKey}`); + } else { + this.stopStream(stream); + console.log(`🗑️ Disposed media stream: ${configKey}`); + } + } + } + + private optimizeConstraints(config: VideoStreamConfig): MediaStreamConstraints { + // Optimize constraints based on device capabilities + const isHighEnd = this.isHighEndDevice(); + + return { + video: { + width: { ideal: config.width, max: isHighEnd ? 1920 : 1280 }, + height: { ideal: config.height, max: isHighEnd ? 1080 : 720 }, + frameRate: { ideal: config.frameRate, max: isHighEnd ? 60 : 30 }, + facingMode: config.facingMode || "user", + ...(config.deviceId && { deviceId: config.deviceId }) + }, + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + sampleRate: { ideal: 48000 } + } + }; + } + + private isHighEndDevice(): boolean { + // Simple heuristic for device capability detection + const memory = (navigator as any).deviceMemory || 4; + const cores = navigator.hardwareConcurrency || 4; + + return memory >= 8 && cores >= 8; + } + + private getConfigKey(config: VideoStreamConfig): string { + return `${config.width}x${config.height}@${config.frameRate}fps`; + } + + private isStreamValid(stream: MediaStream): boolean { + return stream.active && stream.getTracks().every(track => track.readyState === 'live'); + } + + private stopStream(stream: MediaStream): void { + stream.getTracks().forEach(track => { + track.stop(); + }); + } + + private startPoolCleanup(): void { + this.cleanupInterval = setInterval(() => { + this.cleanupExpiredStreams(); + }, 60000) as any; // Cleanup every minute + } + + private cleanupExpiredStreams(): void { + const now = Date.now(); + + for (const [key, connection] of this.streamPool.activeConnections) { + if (now - connection.lastUsed > this.streamPool.streamTTL) { + this.streamPool.activeConnections.delete(key); + this.stopStream(connection.stream); + console.log(`🧹 Cleaned up expired stream: ${key}`); + } + } + + // Also cleanup available streams + for (const [key, stream] of this.streamPool.availableStreams) { + if (!this.isStreamValid(stream)) { + this.streamPool.availableStreams.delete(key); + this.stopStream(stream); + console.log(`🧹 Cleaned up invalid stream: ${key}`); + } + } + } + + destroy(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + + // Clean up all streams + for (const connection of this.streamPool.activeConnections.values()) { + this.stopStream(connection.stream); + } + + for (const stream of this.streamPool.availableStreams.values()) { + this.stopStream(stream); + } + + this.streamPool.activeConnections.clear(); + this.streamPool.availableStreams.clear(); + } +} +``` + +### 5. **Implement Performance Metrics Tracking** +```typescript +interface ProducerMetrics { + totalFramesProcessed: number; + totalFramesDropped: number; + averageProcessingTime: number; + memoryUsageTrend: number[]; + qualityAdaptations: number; + connectionReuses: number; + streamCreations: number; + lastResetTime: number; +} + +class ProducerMetrics { + private metrics: ProducerMetrics; + private reportingInterval: number | null = null; + + constructor() { + this.metrics = { + totalFramesProcessed: 0, + totalFramesDropped: 0, + averageProcessingTime: 0, + memoryUsageTrend: [], + qualityAdaptations: 0, + connectionReuses: 0, + streamCreations: 0, + lastResetTime: Date.now() + }; + + this.startReporting(); + } + + recordFrameProcessed(processingTime: number): void { + this.metrics.totalFramesProcessed++; + + // Update rolling average + const weight = 0.1; + this.metrics.averageProcessingTime = + this.metrics.averageProcessingTime * (1 - weight) + + processingTime * weight; + } + + recordFrameDropped(): void { + this.metrics.totalFramesDropped++; + } + + recordQualityAdaptation(): void { + this.metrics.qualityAdaptations++; + } + + recordConnectionReuse(): void { + this.metrics.connectionReuses++; + } + + recordStreamCreation(): void { + this.metrics.streamCreations++; + } + + updateMemoryUsage(memoryMB: number): void { + this.metrics.memoryUsageTrend.push(memoryMB); + + // Keep only recent memory samples + if (this.metrics.memoryUsageTrend.length > 60) { + this.metrics.memoryUsageTrend.shift(); + } + } + + getMetrics(): ProducerMetrics { + return { ...this.metrics }; + } + + getPerformanceScore(): number { + const frameRate = this.metrics.totalFramesProcessed / + ((Date.now() - this.metrics.lastResetTime) / 1000); + const dropRate = this.metrics.totalFramesDropped / + Math.max(1, this.metrics.totalFramesProcessed); + const reuseRate = this.metrics.connectionReuses / + Math.max(1, this.metrics.streamCreations); + + // Calculate weighted score (0-100) + const frameRateScore = Math.min(100, frameRate * 3.33); // 30fps = 100 + const dropRateScore = Math.max(0, 100 - dropRate * 500); // 20% drops = 0 + const reuseScore = reuseRate * 100; + + return (frameRateScore * 0.5 + dropRateScore * 0.3 + reuseScore * 0.2); + } + + private startReporting(): void { + this.reportingInterval = setInterval(() => { + const score = this.getPerformanceScore(); + console.log(`📊 MediaRecorder Performance Score: ${score.toFixed(1)}/100`, { + framesProcessed: this.metrics.totalFramesProcessed, + framesDropped: this.metrics.totalFramesDropped, + avgProcessingTime: `${this.metrics.averageProcessingTime.toFixed(2)}ms`, + qualityAdaptations: this.metrics.qualityAdaptations, + connectionReuses: this.metrics.connectionReuses + }); + }, 30000) as any; // Report every 30 seconds + } + + reset(): void { + this.metrics = { + totalFramesProcessed: 0, + totalFramesDropped: 0, + averageProcessingTime: 0, + memoryUsageTrend: [], + qualityAdaptations: 0, + connectionReuses: 0, + streamCreations: 0, + lastResetTime: Date.now() + }; + } + + destroy(): void { + if (this.reportingInterval) { + clearInterval(this.reportingInterval); + } + } +} +``` + +## Performance Metrics Impact + +| Optimization | Memory Usage | Frame Rate | CPU Usage | Connection Speed | +|--------------|--------------|------------|-----------|------------------| +| Chunk Management | -70% | +15% | -20% | +10% | +| Frame Processing | -30% | +40% | -35% | +25% | +| Memory Monitoring | -50% | +20% | -15% | N/A | +| Stream Pooling | -40% | +30% | -25% | +60% | +| Metrics Tracking | +5% | +10% | +5% | +15% | + +## Implementation Priority + +1. **Critical**: Implement chunk buffer management to prevent memory leaks +2. **High**: Add adaptive frame processing and quality control +3. **High**: Implement stream pooling and connection reuse +4. **Medium**: Add memory monitoring and cleanup +5. **Low**: Implement comprehensive performance metrics + +## Testing Recommendations + +1. Test memory usage over extended recording periods (>1 hour) +2. Monitor frame processing under high CPU load conditions +3. Test stream reuse efficiency with multiple connections +4. Verify adaptive quality works on low-end devices +5. Test cleanup and garbage collection effectiveness + +## Mobile-Specific Optimizations + +- Reduce maximum video resolution on mobile devices +- Implement more aggressive frame dropping on touch devices +- Use hardware acceleration when available +- Implement battery-aware quality adjustments + +## Additional Notes + +- Consider using OffscreenCanvas for frame processing in Web Workers +- Implement WebCodecs API support for better performance on supported browsers +- Add support for adaptive bitrate streaming +- Consider implementing custom video codecs for specific use cases \ No newline at end of file diff --git a/src/lib/sensors/producers/index.ts b/src/lib/sensors/producers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ce56ab282edc16237732943c1d7a6367a228b45 --- /dev/null +++ b/src/lib/sensors/producers/index.ts @@ -0,0 +1,7 @@ +/** + * Producer Sensor Drivers - Main Export + * + * Central export point for all producer sensor driver implementations + */ + +export { MediaRecorderProducer } from "./MediaRecorderProducer.js"; \ No newline at end of file diff --git a/src/lib/sensors/types/base.ts b/src/lib/sensors/types/base.ts new file mode 100644 index 0000000000000000000000000000000000000000..19c0356e2ef0b2dbbb02d4cd0c3f26b7f19eb025 --- /dev/null +++ b/src/lib/sensors/types/base.ts @@ -0,0 +1,21 @@ +/** + * Base Sensor Driver Interfaces + * + * Core contracts that all sensor driver implementations must follow + */ + +import type { ConnectionStatus, StatusChangeCallback, UnsubscribeFn } from "./core.js"; + +/** + * Base sensor driver interface - common functionality for all sensor drivers + */ +export interface BaseSensorDriver { + readonly id: string; + readonly type: "producer" | "consumer"; + readonly name: string; + readonly status: ConnectionStatus; + + connect(): Promise; + disconnect(): Promise; + onStatusChange(callback: StatusChangeCallback): UnsubscribeFn; +} \ No newline at end of file diff --git a/src/lib/sensors/types/consumer.ts b/src/lib/sensors/types/consumer.ts new file mode 100644 index 0000000000000000000000000000000000000000..b20f06e0875374a4c4766afa1973b4f50f2f7ed2 --- /dev/null +++ b/src/lib/sensors/types/consumer.ts @@ -0,0 +1,58 @@ +/** + * Consumer Sensor Driver Interfaces & Configurations + * + * Consumer drivers send sensor data to various destinations + * Examples: Remote server, local storage + */ + +import type { BaseSensorDriver } from "./base.js"; +import type { + SensorFrame, + SensorStream, + FrameCallback, + StreamUpdateCallback, + UnsubscribeFn +} from "./core.js"; + +/** + * Consumer Driver - Sends sensor data to various destinations + */ +export interface ConsumerSensorDriver extends BaseSensorDriver { + readonly type: "consumer"; + + // Data transmission + sendFrame(frame: SensorFrame): Promise; + sendFrames(frames: SensorFrame[]): Promise; + + // Stream management + startOutputStream(stream: SensorStream): Promise; + stopOutputStream(streamId: string): Promise; + getActiveOutputStreams(): SensorStream[]; + + // Event callbacks + onFrameSent(callback: FrameCallback): UnsubscribeFn; + onStreamUpdate(callback: StreamUpdateCallback): UnsubscribeFn; +} + +/** + * Consumer driver configuration types - simplified with best practices + */ +export interface RemoteServerConsumerConfig { + type: "remote-server"; + url: string; + apiKey?: string; + streamId?: string; + retryAttempts?: number; + retryDelay?: number; +} + +export interface LocalStorageConsumerConfig { + type: "local-storage"; + directory?: string; + filename?: string; + autoUpload?: boolean; +} + +export type ConsumerSensorDriverConfig = + | RemoteServerConsumerConfig + | LocalStorageConsumerConfig; \ No newline at end of file diff --git a/src/lib/sensors/types/core.ts b/src/lib/sensors/types/core.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac703e58bdef361fa9e3b84d885b479a7cee2215 --- /dev/null +++ b/src/lib/sensors/types/core.ts @@ -0,0 +1,65 @@ +/** + * Core Sensor Types + * + * Fundamental types and interfaces used across all sensor driver implementations + */ + +export interface ConnectionStatus { + isConnected: boolean; + lastConnected?: Date; + error?: string; + bitrate?: number; // For video streams + frameRate?: number; // For video streams +} + +/** + * Sensor data frame for video streams + */ +export interface SensorFrame { + timestamp: number; + type: "video" | "audio" | "data"; + data: ArrayBuffer | Blob; + metadata?: { + width?: number; + height?: number; + frameRate?: number; + codec?: string; + bitrate?: number; + format?: string; + [key: string]: unknown; + }; +} + +/** + * Video stream configuration + */ +export interface VideoStreamConfig { + width?: number; + height?: number; + frameRate?: number; + bitrate?: number; + codec?: string; + facingMode?: "user" | "environment"; + deviceId?: string; +} + +/** + * Sensor stream for continuous data flow + */ +export interface SensorStream { + id: string; + name: string; + type: "video" | "audio" | "data"; + config: VideoStreamConfig; + active: boolean; + startTime?: Date; + endTime?: Date; + totalFrames?: number; +} + +// Callback types +export type StreamUpdateCallback = (stream: SensorStream) => void; +export type FrameCallback = (frame: SensorFrame) => void; +export type StatusChangeCallback = (status: ConnectionStatus) => void; +export type ErrorCallback = (error: string) => void; +export type UnsubscribeFn = () => void; \ No newline at end of file diff --git a/src/lib/sensors/types/index.ts b/src/lib/sensors/types/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7989d6533b597806494b28a680326e047e30d270 --- /dev/null +++ b/src/lib/sensors/types/index.ts @@ -0,0 +1,37 @@ +/** + * Sensor Driver Types - Main Export + * + * Central export point for all sensor driver types and interfaces + */ + +// Core types +export type { + ConnectionStatus, + SensorFrame, + VideoStreamConfig, + SensorStream, + StreamUpdateCallback, + FrameCallback, + StatusChangeCallback, + ErrorCallback, + UnsubscribeFn +} from "./core.js"; + +// Base interfaces +export type { BaseSensorDriver } from "./base.js"; + +// Producer drivers +export type { + ProducerSensorDriver, + MediaRecorderProducerConfig, + NetworkStreamProducerConfig, + ProducerSensorDriverConfig +} from "./producer.js"; + +// Consumer drivers +export type { + ConsumerSensorDriver, + RemoteServerConsumerConfig, + LocalStorageConsumerConfig, + ConsumerSensorDriverConfig +} from "./consumer.js"; \ No newline at end of file diff --git a/src/lib/sensors/types/producer.ts b/src/lib/sensors/types/producer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6051109b0097bef8dfc58031b5a55dc3fae67bc7 --- /dev/null +++ b/src/lib/sensors/types/producer.ts @@ -0,0 +1,58 @@ +/** + * Producer Sensor Driver Interfaces & Configurations + * + * Producer drivers capture sensor data (video, audio, etc.) + * Examples: MediaRecorder, network stream + */ + +import type { BaseSensorDriver } from "./base.js"; +import type { + SensorStream, + VideoStreamConfig, + FrameCallback, + StreamUpdateCallback, + UnsubscribeFn +} from "./core.js"; + +/** + * Producer Driver - Captures sensor data from various sources + */ +export interface ProducerSensorDriver extends BaseSensorDriver { + readonly type: "producer"; + + // Stream management + startStream(config: VideoStreamConfig): Promise; + stopStream(streamId: string): Promise; + pauseStream(streamId: string): Promise; + resumeStream(streamId: string): Promise; + getActiveStreams(): SensorStream[]; + + // Event callbacks + onFrame(callback: FrameCallback): UnsubscribeFn; + onStreamUpdate(callback: StreamUpdateCallback): UnsubscribeFn; +} + +/** + * Producer driver configuration types - simplified with best practices + */ +export interface MediaRecorderProducerConfig { + type: "media-recorder"; + constraints?: MediaStreamConstraints; + videoBitsPerSecond?: number; + audioBitsPerSecond?: number; + recordingInterval?: number; // ms between frame captures +} + +export interface NetworkStreamProducerConfig { + type: "network-stream"; + url: string; + credentials?: { + username?: string; + password?: string; + token?: string; + }; +} + +export type ProducerSensorDriverConfig = + | MediaRecorderProducerConfig + | NetworkStreamProducerConfig; \ No newline at end of file diff --git a/src/lib/types/positionable.ts b/src/lib/types/positionable.ts new file mode 100644 index 0000000000000000000000000000000000000000..8398a51c6e587a70ec795a56ab195806c74cf07a --- /dev/null +++ b/src/lib/types/positionable.ts @@ -0,0 +1,9 @@ +import type { Position3D } from '$lib/utils/positionUtils'; + +export interface Positionable { + readonly id: string; + position: Position3D; + updatePosition(newPosition: Position3D): void; +} + +export type { Position3D } from '$lib/utils/positionUtils'; \ No newline at end of file diff --git a/src/lib/types/robot.ts b/src/lib/types/robot.ts new file mode 100644 index 0000000000000000000000000000000000000000..d42985a7e378638b4d6c380b7120dfdb77ef0143 --- /dev/null +++ b/src/lib/types/robot.ts @@ -0,0 +1,7 @@ +import type IUrdfRobot from "@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot"; +import type { RobotUrdfConfig } from "./urdf"; + +export interface UrdfRobotState { + urdfRobot: IUrdfRobot; + urdfConfig: RobotUrdfConfig; +} diff --git a/src/lib/types/urdf.ts b/src/lib/types/urdf.ts new file mode 100644 index 0000000000000000000000000000000000000000..b12dda67c51ea051884f43331742c9ff596833db --- /dev/null +++ b/src/lib/types/urdf.ts @@ -0,0 +1,26 @@ +export interface UrdfCompoundMovement { + name: string; + primaryJoint: number; // the joint controlled by the key + // Optional formula for calculating deltaPrimary, can use primary, dependent, etc. + primaryFormula?: string; + dependents: { + joint: number; + // The formula is used to calculate the delta for the dependent joint (deltaDependent) + // It can use variables: primary, dependent, deltaPrimary + // deltaPrimary itself can depend on primary and dependent angles + // Example: "deltaPrimary * 0.8 + primary * 0.1 - dependent * 0.05" + formula: string; + }[]; +} + +export type RobotUrdfConfig = { + urdfUrl: string; + jointNameIdMap?: { + [key: string]: number; + }; + compoundMovements?: UrdfCompoundMovement[]; + // Rest/calibration position - joint values in degrees when robot is in "rest mode" + restPosition?: { + [jointName: string]: number; + }; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..3167bf89c14379d21c54ac1f77ef33223b8156ad --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,100 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; + + +// === Servo Position and Angle Conversion Functions === + +/** + * Converts a servo position to an angle in degrees + * + * Servo positions range from 0 to 4096, representing a full 360° rotation. + * This function maps the position to its corresponding angle. + * + * @param position - The servo position (0 to 4096) + * @returns The corresponding angle (0 to 360 degrees) + * + * @example + * servoPositionToAngle(0) // Returns: 0 + * servoPositionToAngle(2048) // Returns: 180 + * servoPositionToAngle(4096) // Returns: 360 + */ +export function servoPositionToAngle(position: number): number { + return (position / 4096) * 360; +} + +/** + * Converts degrees to a servo position + * + * Maps angle in degrees to the corresponding servo position value. + * Clamps the result to the valid servo range (0-4096). + * + * @param degrees - The angle in degrees (0 to 360) + * @returns The corresponding servo position (0 to 4096) + * + * @example + * degreesToServoPosition(0) // Returns: 0 + * degreesToServoPosition(180) // Returns: 2048 + * degreesToServoPosition(360) // Returns: 4096 + */ +export function degreesToServoPosition(degrees: number): number { + return Math.min(Math.round((degrees * 4096) / 360), 4096); +} + +// === Angle Unit Conversion Functions === + +/** + * Converts radians to degrees + * + * @param radians - The angle in radians + * @returns The angle in degrees + * + * @example + * radiansToDegrees(Math.PI) // Returns: 180 + * radiansToDegrees(Math.PI / 2) // Returns: 90 + */ +export function radiansToDegrees(radians: number): number { + return (radians * 180) / Math.PI; +} + +/** + * Converts degrees to radians + * + * @param degrees - The angle in degrees + * @returns The angle in radians + * + * @example + * degreesToRadians(180) // Returns: Math.PI + * degreesToRadians(90) // Returns: Math.PI / 2 + */ +export function degreesToRadians(degrees: number): number { + return (degrees * Math.PI) / 180; +} + +/** + * Converts radians to a servo position + * + * Combines radian-to-degree conversion with servo position mapping. + * Useful for direct conversion from joint angles to servo commands. + * + * @param radians - The angle in radians + * @returns The corresponding servo position (0 to 4096) + * + * @example + * radiansToServoPosition(0) // Returns: 0 + * radiansToServoPosition(Math.PI) // Returns: 2048 + * radiansToServoPosition(2*Math.PI) // Returns: 4096 + */ +export function radiansToServoPosition(radians: number): number { + return Math.min(Math.round((radians * 4096) / (2 * Math.PI)), 4096); +} diff --git a/src/lib/utils/config.ts b/src/lib/utils/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..51452f3b88a8979594c4ba240c49082e6da30b25 --- /dev/null +++ b/src/lib/utils/config.ts @@ -0,0 +1,106 @@ +/** + * Configuration utilities for environment-specific URLs + */ + +// Check if we're running in browser +const isBrowser = typeof window !== "undefined"; + +/** + * Get the SPACE_HOST from various sources + */ +function getSpaceHost(): string | undefined { + if (!isBrowser) return undefined; + + // Check window.SPACE_HOST (injected by container) + if ((window as unknown as { SPACE_HOST?: string }).SPACE_HOST) { + return (window as unknown as { SPACE_HOST: string }).SPACE_HOST; + } + + // Check if current hostname looks like HF Spaces + const hostname = window.location.hostname; + if (hostname.includes("hf.space") || hostname.includes("huggingface.co")) { + return hostname; + } + + return undefined; +} + +/** + * Get the base URL for API requests + */ +export function getApiBaseUrl(): string { + if (!isBrowser) return "http://localhost:7860"; + + // Check for Hugging Face Spaces + const spaceHost = getSpaceHost(); + if (spaceHost) { + return `https://${spaceHost}`; + } + + // In browser, check current location + const { protocol, hostname, port } = window.location; + + // If we're on the same host and port, use same origin (both frontend and backend on same server) + if (hostname === "localhost" || hostname === "127.0.0.1") { + // In development, frontend might be on 5173 and backend on 7860 + if (port === "5173" || port === "5174") { + return "http://localhost:8000"; + } + // If frontend is served from backend (port 7860), use same origin + return `${protocol}//${hostname}:${port}`; + } + + // For production, use same origin (both served from same FastAPI server) + return `${protocol}//${hostname}${port ? `:${port}` : ""}`; +} + +/** + * Get the WebSocket URL for real-time connections + */ +export function getWebSocketBaseUrl(): string { + if (!isBrowser) return "ws://localhost:7860"; + + // Check for Hugging Face Spaces + const spaceHost = getSpaceHost(); + if (spaceHost) { + return `wss://${spaceHost}`; + } + + const { protocol, hostname, port } = window.location; + + // If we're on localhost + if (hostname === "localhost" || hostname === "127.0.0.1") { + // In development, frontend might be on 5173 and backend on 7860 + if (port === "5173" || port === "5174") { + return "ws://localhost:8000"; + } + // If frontend is served from backend (port 7860), use same origin + const wsProtocol = protocol === "https:" ? "wss:" : "ws:"; + return `${wsProtocol}//${hostname}:${port}`; + } + + // For HTTPS sites, use WSS; for HTTP sites, use WS + const wsProtocol = protocol === "https:" ? "wss:" : "ws:"; + + // For production, use same origin (both served from same FastAPI server) + return `${wsProtocol}//${hostname}${port ? `:${port}` : ""}`; +} + +/** + * Get environment info for debugging + */ +export function getEnvironmentInfo() { + if (!isBrowser) return { env: "server", hostname: "unknown" }; + + const { protocol, hostname, port } = window.location; + const spaceHost = getSpaceHost(); + + return { + env: spaceHost ? "huggingface-spaces" : hostname === "localhost" ? "local" : "production", + hostname, + port, + protocol, + spaceHost, + apiBaseUrl: getApiBaseUrl(), + }; +} diff --git a/src/lib/utils/generateName.ts b/src/lib/utils/generateName.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6029dd4d9a8df41fa9f8ec6e149619e5da5076f --- /dev/null +++ b/src/lib/utils/generateName.ts @@ -0,0 +1,424 @@ +export function generateName(): string { + const adjectives = [ + "Swift", + "Brave", + "Clever", + "Bold", + "Wise", + "Strong", + "Mighty", + "Quick", + "Fierce", + "Noble", + "Agile", + "Sharp", + "Bright", + "Keen", + "Wild", + "Free", + "Fast", + "Sleek", + "Smart", + "Proud", + "Silent", + "Gentle", + "Loyal", + "Daring", + "Elegant", + "Graceful", + "Vigilant", + "Steady", + "Calm", + "Alert", + "Curious", + "Majestic", + "Fearless", + "Cunning", + "Nimble", + "Powerful", + "Spirited", + "Vibrant", + "Ancient", + "Mystic", + "Golden", + "Silver", + "Bronze", + "Crystal", + "Diamond", + "Stealth", + "Thunder", + "Lightning", + "Storm", + "Frost", + "Flame", + "Ember", + "Shadow", + "Phantom", + "Cosmic", + "Stellar", + "Solar", + "Lunar", + "Nova", + "Radiant", + "Blazing", + "Glowing", + "Crimson", + "Emerald", + "Sapphire", + "Ruby", + "Onyx", + "Marble", + "Iron", + "Steel", + "Titanium", + "Chrome", + "Copper", + "Platinum", + "Arctic", + "Desert", + "Forest", + "Ocean", + "Mountain", + "Valley", + "River", + "Canyon", + "Highland", + "Midnight", + "Dawn", + "Twilight", + "Celestial", + "Ethereal", + "Divine", + "Sacred", + "Blessed", + "Cursed", + "Enchanted", + "Magical", + "Legendary", + "Epic", + "Heroic", + "Valiant", + "Gallant", + "Honor", + "Justice", + "Vengeance", + "Rogue", + "Rebel", + "Outlaw", + "Bandit", + "Savage", + "Primal", + "Feral", + "Untamed", + "Ruthless", + "Merciless", + "Deadly", + "Lethal", + "Vicious", + "Brutal", + "Harsh", + "Cruel", + "Gentle", + "Kind", + "Serene", + "Peaceful", + "Tranquil", + "Zen", + "Harmonious", + "Balanced", + "Perfect", + "Flawless", + "Pure", + "Innocent", + "Angelic", + "Demonic", + "Infernal", + "Hellish", + "Frozen", + "Molten", + "Burning", + "Scorching", + "Blazing", + "Searing", + "Frigid", + "Icy", + "Windy", + "Stormy", + "Cloudy", + "Misty", + "Foggy", + "Sunny", + "Bright", + "Dark", + "Black", + "White", + "Gray", + "Red", + "Blue", + "Green", + "Yellow", + "Orange", + "Purple", + "Pink", + "Brown", + "Violet", + "Indigo", + "Turquoise", + "Cyan", + "Magenta" + ]; + + const animals = [ + "Wolf", + "Eagle", + "Tiger", + "Falcon", + "Lion", + "Bear", + "Fox", + "Hawk", + "Panther", + "Jaguar", + "Leopard", + "Lynx", + "Raven", + "Owl", + "Shark", + "Dolphin", + "Whale", + "Cheetah", + "Puma", + "Cougar", + "Bobcat", + "Coyote", + "Stag", + "Elk", + "Moose", + "Bison", + "Rhino", + "Elephant", + "Gorilla", + "Orangutan", + "Chimpanzee", + "Monkey", + "Lemur", + "Sloth", + "Koala", + "Panda", + "Penguin", + "Albatross", + "Condor", + "Vulture", + "Heron", + "Crane", + "Swan", + "Dragon", + "Phoenix", + "Griffin", + "Unicorn", + "Pegasus", + "Kraken", + "Sphinx", + "Hydra", + "Basilisk", + "Chimera", + "Manticore", + "Minotaur", + "Centaur", + "Gargoyle", + "Demon", + "Angel", + "Serpent", + "Viper", + "Cobra", + "Python", + "Anaconda", + "Mamba", + "Rattlesnake", + "Adder", + "Scorpion", + "Spider", + "Tarantula", + "Mantis", + "Beetle", + "Wasp", + "Hornet", + "Dragonfly", + "Butterfly", + "Moth", + "Firefly", + "Grasshopper", + "Cricket", + "Locust", + "Ant", + "Termite", + "Mammoth", + "Saber", + "Direwolf", + "Fenrir", + "Cerberus", + "Banshee", + "Wraith", + "Specter", + "Ghost", + "Spirit", + "Phantom", + "Shade", + "Shadow", + "Nightmare", + "Terror", + "Horror", + "Stallion", + "Mare", + "Mustang", + "Bronco", + "Thoroughbred", + "Arabian", + "Clydesdale", + "Appaloosa", + "Zebra", + "Giraffe", + "Hippo", + "Crocodile", + "Alligator", + "Komodo", + "Iguana", + "Chameleon", + "Gecko", + "Salamander", + "Newt", + "Frog", + "Toad", + "Turtle", + "Tortoise", + "Turtle", + "Octopus", + "Squid", + "Jellyfish", + "Starfish", + "Seahorse", + "Swordfish", + "Marlin", + "Tuna", + "Salmon", + "Trout", + "Bass", + "Pike", + "Barracuda", + "Manta", + "Stingray", + "Hammerhead", + "Orca", + "Narwhal", + "Beluga", + "Humpback", + "Blue", + "Gray", + "Killer", + "Seal", + "Walrus", + "Otter", + "Beaver", + "Platypus", + "Echidna", + "Wombat", + "Kangaroo", + "Wallaby", + "Opossum", + "Raccoon", + "Skunk", + "Badger", + "Wolverine", + "Marten", + "Ferret", + "Weasel", + "Mink", + "Stoat", + "Ermine", + "Polecat", + "Sable", + "Fisher", + "Otter", + "Deer", + "Antelope", + "Gazelle", + "Impala", + "Springbok", + "Kudu", + "Oryx", + "Wildebeest", + "Buffalo", + "Yak", + "Oxen", + "Bull", + "Cow", + "Calf", + "Goat", + "Ram", + "Sheep", + "Lamb", + "Pig", + "Boar", + "Hog", + "Llama", + "Alpaca", + "Camel", + "Dromedary", + "Horse", + "Donkey", + "Mule", + "Burro", + "Cat", + "Kitten", + "Dog", + "Puppy", + "Hound", + "Retriever", + "Shepherd", + "Collie", + "Husky", + "Malamute", + "Akita", + "Shiba", + "Corgi", + "Bulldog", + "Mastiff", + "Rottweiler", + "Doberman", + "Pinscher", + "Boxer", + "Sparrow", + "Robin", + "Cardinal", + "Bluejay", + "Mockingbird", + "Warbler", + "Finch", + "Canary", + "Parakeet", + "Parrot", + "Macaw", + "Cockatoo", + "Toucan", + "Peacock", + "Turkey", + "Chicken", + "Rooster", + "Duck", + "Goose", + "Pelican", + "Flamingo", + "Ibis", + "Stork", + "Kingfisher" + ]; + + const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]; + const randomAnimal = animals[Math.floor(Math.random() * animals.length)]; + + const digits = Math.floor(Math.random() * 1000) + .toString() + .padStart(3, "0"); + + return `${randomAdjective}${randomAnimal}${digits}`; +} diff --git a/src/lib/utils/icon.ts b/src/lib/utils/icon.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cd1c86f9ceb275f86326d964b50bd1154ecaf95 --- /dev/null +++ b/src/lib/utils/icon.ts @@ -0,0 +1,75 @@ +interface Icon { + svg: string; + alt: string; +} + +// const listenerIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTM5OC4zIDMuNGMtMTUuOC03LjktMzUtMS41LTQyLjkgMTQuM3MtMS41IDM0LjkgMTQuMiA0Mi45bC40LjJxLjYuMyAyLjEgMS4yYzIgMS4yIDUgMyA4LjcgNS42YzcuNSA1LjIgMTcuNiAxMy4yIDI3LjcgMjQuMkM0MjguNSAxMTMuNCA0NDggMTQ2IDQ0OCAxOTJjMCAxNy43IDE0LjMgMzIgMzIgMzJzMzItMTQuMyAzMi0zMmMwLTY2LTI4LjUtMTEzLjQtNTYuNS0xNDMuN2MtMTMuOS0xNS4xLTI3LjgtMjYuMS0zOC4yLTMzLjNjLTUuMy0zLjctOS43LTYuNC0xMy04LjNjLTEuNi0xLTMtMS43LTQtMi4yYy0uNS0uMy0uOS0uNS0xLjItLjdsLS40LS4ybC0uMi0uMWgtLjFMMzg0IDNyek0xMjguNyAyMjcuNWM2LjItNTYgNTMuNy05OS41IDExMS4zLTk5LjVjNjEuOSAwIDExMiA1MC4xIDExMiAxMTJjMCAyOS4zLTExLjIgNTUuOS0yOS42IDc1LjljLTE3IDE4LjQtMzQuNCA0NS4xLTM0LjQgNzh2Ni4xYzAgMjYuNS0yMS41IDQ4LTQ4IDQ4Yy0xNy43IDAtMzIgMTQuMy0zMiAzMnMxNC4zIDMyIDMyIDMyYzYxLjkgMCAxMTItNTAuMSAxMTItMTEydi02LjFjMC05LjggNS40LTIxLjcgMTcuNC0zNC43QzM5OC4zIDMyNy45IDQxNiAyODYgNDE2IDI0MGMwLTk3LjItNzguOC0xNzYtMTc2LTE3NmMtOTAuNiAwLTE2NS4yIDY4LjUtMTc0LjkgMTU2LjVjLTEuOSAxNy42IDEwLjcgMzMuNCAyOC4zIDM1LjNzMzMuNC0xMC43IDM1LjMtMjguM00zMiA1MTJhMzIgMzIgMCAxIDAgMC02NGEzMiAzMiAwIDEgMCAwIDY0bTE2MC0xNjBhMzIgMzIgMCAxIDAtNjQgMGEzMiAzMiAwIDEgMCA2NCAwbS0xNTAuNiA5LjRjLTEyLjUgMTIuNS0xMi41IDMyLjggMCA0NS4zbDY0IDY0YzEyLjUgMTIuNSAzMi44IDEyLjUgNDUuMyAwczEyLjUtMzIuOCAwLTQ1LjNsLTY0LTY0Yy0xMi41LTEyLjUtMzIuOC0xMi41LTQ1LjMgME0yMDggMjQwYzAtMTcuNyAxNC4zLTMyIDMyLTMyczMyIDE0LjMgMzIgMzJjMCAxMy4zIDEwLjcgMjQgMjQgMjRzMjQtMTAuNyAyNC0yNGMwLTQ0LjItMzUuOC04MC04MC04MHMtODAgMzUuOC04MCA4MGMwIDEzLjMgMTAuNyAyNCAyNCAyNHMyNC0xMC43IDI0LTI0Ii8+PC9zdmc+"; +// const plusIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4="; +// https://icon-sets.iconify.design + +export const ICON = { + "icon-[material-symbols--upload]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEgMTZWNy44NWwtMi42IDIuNkw3IDlsNS01bDUgNWwtMS40IDEuNDVsLTIuNi0yLjZWMTZ6bS01IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", + alt: "Upload" + }, + "icon-[material-symbols--download]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJtMTIgMTZsLTUtNWwxLjQtMS40NWwyLjYgMi42VjRoMnY4LjE1bDIuNi0yLjZMMTcgMTF6bS02IDRxLS44MjUgMC0xLjQxMi0uNTg3VDQgMTh2LTNoMnYzaDEydi0zaDJ2M3EwIC44MjUtLjU4NyAxLjQxM1QxOCAyMHoiLz48L3N2Zz4=", + alt: "Download" + }, + "icon-[ix--robotic-arm]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0iIzAwMCIgZD0ibTI4My4wMTYgNDguMzhsLTEzOS43MTMgODMuNTRhNTkuOSA1OS45IDAgMCAwLTE3LjU4NS0yLjYzMkM5MS42MzIgMTI5LjI4OCA2NCAxNTcuOTA0IDY0IDE5My4yMDRjMCAxNC41NDYgNC42OTIgMjcuOTU3IDEyLjU5NCAzOC42OTlhNjQgNjQgMCAwIDEtMi45MTgtNC4zMjVsNzUuNDEzIDEzNS4yOTVsLS4xMzcuNTU3YTg4LjMgODguMyAwIDAgMC0yLjQzOSAyMC42NzlsLjAxMyA0Mi42MDdsLTQxLjE1OC4wMDR2NDIuNjExaDI1Ny4yOTl2LTQyLjY2N2wtNTEuNTU5LjA1MmwtLjAxMi00Mi42MDdjMC0zNC4xMDYtMTkuMzQ2LTYzLjUzMy00Ny4yOTQtNzcuMTUzbC00LjQzOC0xLjk5OWwtNjAuMjE3LTEwOC4wMmwxMDUuMTExLTYyLjg1MWw0NS44NDEgODIuMzI1Yy0xLjkxOCA3LjA3Mi0xLjQ5NSAxNC44MTkgMS42MjEgMjEuODg0bDEuMzYzIDIuNzM5YzIuMDY5IDMuNzA5IDQuNzQ3IDYuODI4IDcuODI4IDkuMjk2bC4wMjggNDMuNjQ4bC0uMDM3LjE2NGwzOS43NDMgMTEuMDI5bDUuMzI1LTIwLjU4bC0yNC40NTctNi44MDJsLjAwMi0yMC44MThjNC42ODUtLjI2NSA5LjM4Mi0xLjY0NCAxMy43MjMtNC4yNGM0LjU5My0yLjc0NiA4LjI0Ny02LjUzMSAxMC44NDQtMTAuOTAzbDE4LjAxMyAxMC43NzlsLTYuNDg4IDI1LjE2MWwxOS44NzIgNS41MTRMNDQ4IDI0Mi4zNTdsLS4wNDYtLjAwNWwuMDM0LS4wNjdsLTM3Ljc0OS0yMi41OTFhMzIuNyAzMi43IDAgMCAwLTMuNzA2LTEwLjYxOWMtNC43OTQtOC41OTktMTIuODY0LTE0LjAxOC0yMS42MzUtMTUuNTQybC00Ny4xMy04NC41MTFjOC40NTEtMTMuMDcgOS41NjEtMzAuNTAzIDEuNDU0LTQ1LjA0NWMtMTEuMzYyLTIwLjM4LTM2LjUyNi0yNy4zNjQtNTYuMjA2LTE1LjU5N20tNTQuMjExIDI5My4xMThjMjEuNjkxIDAgMzkuNDYyIDE3LjM4MyA0MS4wMzIgMzkuNDMxbC4xMTMgMy4xOHY0Mi42MTFoLTgyLjI5MXYtNDIuNjExYzAtMjIuNDY0IDE2Ljc4NS00MC44NjcgMzguMDc1LTQyLjQ5NHptLTU4LjM5Ny0xMDQuMjFsMi4yNzgtMi42MTZsMzcuMDg3IDY2LjUwNmwuNzg1LS4xODhjLTEyLjUyNiAyLjkzNi0yMy45NjggOC44MzItMzMuNjA1IDE2LjkzOWwtMy41ODggMy4ybC0zNi4yOTQtNjUuMDg3bC0uNTYxLjEwNWMxMy4yMzItMi40MTcgMjUuMDEtOS4xOTkgMzMuODk4LTE4Ljg1OW0tNDQuNjktNjUuMzg5YzExLjM2MiAwIDIwLjU3MyA5LjUzOSAyMC41NzMgMjEuMzA1cy05LjIxMSAyMS4zMDYtMjAuNTczIDIxLjMwNnMtMjAuNTcyLTkuNTM5LTIwLjU3Mi0yMS4zMDZjMC0xMS43NjYgOS4yMS0yMS4zMDUgMjAuNTcyLTIxLjMwNW0xNzUuMzUtOTYuNzg0YzUuNjgxIDAgMTAuMjg2IDQuNzcgMTAuMjg2IDEwLjY1M3MtNC42MDUgMTAuNjUzLTEwLjI4NiAxMC42NTNzLTEwLjI4Ni00Ljc3LTEwLjI4Ni0xMC42NTNzNC42MDUtMTAuNjUzIDEwLjI4Ni0xMC42NTMiLz48L3N2Zz4=", + alt: "Robotic Arm" + }, + "icon-[mdi--video]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6Ii8+PC9zdmc+", + alt: "Video" + }, + "icon-[mdi--video-off]" : { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMy4yNyAyTDIgMy4yN0w0LjczIDZINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJjLjIgMCAuMzktLjA4LjU0LS4xOEwxOS43MyAyMUwyMSAxOS43M00yMSA2LjVsLTQgNFY3YTEgMSAwIDAgMC0xLTFIOS44MkwyMSAxNy4xOHoiLz48L3N2Zz4=", + alt: "Video off" + }, + "icon-[mdi--video-plus]" : { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6TTE0IDEzaC0zdjNIOXYtM0g2di0yaDNWOGgydjNoM3oiLz48L3N2Zz4=", + alt: "Video Plus" + }, + "icon-[mdi--plus]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=", + alt: "Plus" + }, + "icon-[material-symbols--lock]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNiAyMnEtLjgyNSAwLTEuNDEyLS41ODdUNCAyMFYxMHEwLS44MjUuNTg4LTEuNDEyVDYgOGgxVjZxMC0yLjA3NSAxLjQ2My0zLjUzN1QxMiAxdDMuNTM4IDEuNDYzVDE3IDZ2MmgxcS44MjUgMCAxLjQxMy41ODhUMjAgMTB2MTBxMCAuODI1LS41ODcgMS40MTNUMTggMjJ6bTYtNXEuODI1IDAgMS40MTMtLjU4N1QxNCAxNXQtLjU4Ny0xLjQxMlQxMiAxM3QtMS40MTIuNTg4VDEwIDE1dC41ODggMS40MTNUMTIgMTdNOSA4aDZWNnEwLTEuMjUtLjg3NS0yLjEyNVQxMiAzdC0yLjEyNS44NzVUOSA2eiIvPjwvc3ZnPg==", + alt: "Lock" + }, + "icon-[solar--volume-knob-bold]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTEuMjUgNy4wNTZhNS4wMDEgNS4wMDEgMCAxIDAgMS41IDBWMTFhLjc1Ljc1IDAgMCAxLTEuNSAwek0xMyAzLjVhMSAxIDAgMSAxLTIgMGExIDEgMCAwIDEgMiAwbTcuNSA5LjVhMSAxIDAgMSAxIDAtMmExIDEgMCAwIDEgMCAybS0xNyAwYTEgMSAwIDEgMSAwLTJhMSAxIDAgMCAxIDAgMm0zLjE5Ny03LjcxOGExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTEyLjAyIDEyLjAyMWExIDEgMCAxIDEtMS40MTQgMS40MTVhMSAxIDAgMCAxIDEuNDE0LTEuNDE1bTAtMTAuNjA2YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTVNNi42OTcgMTguNzE4YTEgMSAwIDEgMS0xLjQxNC0xLjQxNWExIDEgMCAwIDEgMS40MTQgMS40MTUiLz48L3N2Zz4=", + alt: "Volume Knob" + }, + "icon-[mingcute--settings-2-fill]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Im0xMi41OTMgMjMuMjU4bC0uMDExLjAwMmwtLjA3MS4wMzVsLS4wMi4wMDRsLS4wMTQtLjAwNGwtLjA3MS0uMDM1cS0uMDE2LS4wMDUtLjAyNC4wMDVsLS4wMDQuMDFsLS4wMTcuNDI4bC4wMDUuMDJsLjAxLjAxM2wuMTA0LjA3NGwuMDE1LjAwNGwuMDEyLS4wMDRsLjEwNC0uMDc0bC4wMTItLjAxNmwuMDA0LS4wMTdsLS4wMTctLjQyN3EtLjAwNC0uMDE2LS4wMTctLjAxOG0uMjY1LS4xMTNsLS4wMTMuMDAybC0uMTg1LjA5M2wtLjAxLjAxbC0uMDAzLjAxMWwuMDE4LjQzbC4wMDUuMDEybC4wMDguMDA3bC4yMDEuMDkzcS4wMTkuMDA1LjAyOS0uMDA4bC4wMDQtLjAxNGwtLjAzNC0uNjE0cS0uMDA1LS4wMTgtLjAyLS4wMjJtLS43MTUuMDAyYS4wMi4wMiAwIDAgMC0uMDI3LjAwNmwtLjAwNi4wMTRsLS4wMzQuNjE0cS4wMDEuMDE4LjAxNy4wMjRsLjAxNS0uMDAybC4yMDEtLjA5M2wuMDEtLjAwOGwuMDA0LS4wMTFsLjAxNy0uNDNsLS4wMDMtLjAxMmwtLjAxLS4wMXoiLz48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTguNSA0YTEuNSAxLjUgMCAwIDAtMyAwdi41SDRhMS41IDEuNSAwIDEgMCAwIDNoMTEuNVY4YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM2gtMS41ek00IDEwLjVhMS41IDEuNSAwIDAgMCAwIDNoMS41di41YTEuNSAxLjUgMCAwIDAgMyAwdi0uNUgyMGExLjUgMS41IDAgMCAwIDAtM0g4LjVWMTBhMS41IDEuNSAwIDEgMC0zIDB2LjV6TTIuNSAxOEExLjUgMS41IDAgMCAxIDQgMTYuNWgxMS41VjE2YTEuNSAxLjUgMCAwIDEgMyAwdi41SDIwYTEuNSAxLjUgMCAwIDEgMCAzaC0xLjV2LjVhMS41IDEuNSAwIDAgMS0zIDB2LS41SDRBMS41IDEuNSAwIDAgMSAyLjUgMTgiLz48L2c+PC9zdmc+", + alt: "Settings" + }, + "icon-[iconamoon--lightning-1-duotone]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48ZyBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJtNiAxNGw3LTEydjhoNWwtNyAxMnYtOHoiIG9wYWNpdHk9IjAuMTYiLz48cGF0aCBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0ibTYgMTRsNy0xMnY4aDVsLTcgMTJ2LTh6Ii8+PC9nPjwvc3ZnPg==", + alt: "Lightning" + }, + "icon-[formkit--arrowright]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEwIDguNWEuNDcuNDcgMCAwIDEtLjM1LS4xNWMtLjItLjItLjItLjUxIDAtLjcxbDMuMTUtMy4xNWwtMy4xNS0zLjE1Yy0uMi0uMi0uMi0uNTEgMC0uNzFzLjUxLS4yLjcxIDBsMy41IDMuNWMuMi4yLjIuNTEgMCAuNzFsLTMuNSAzLjVjLS4xLjEtLjIzLjE1LS4zNS4xNVoiLz48L3N2Zz4=", + alt: "Arrow Right" + }, + "icon-[formkit--arrowup]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDE0Yy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTggNy41YS40Ny40NyAwIDAgMS0uMzUtLjE1TDQuNSA0LjJMMS4zNSA3LjM1Yy0uMi4yLS41MS4yLS43MSAwcy0uMi0uNTEgMC0uNzFsMy41LTMuNWMuMi0uMi41MS0uMi43MSAwbDMuNSAzLjVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTUiLz48L3N2Zz4=", + alt: "Arrow up" + }, + "icon-[formkit--arrowdown]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iLTMuNSAwIDE2IDE2Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNC41IDEzYy0uMjggMC0uNS0uMjItLjUtLjV2LTljMC0uMjguMjItLjUuNS0uNXMuNS4yMi41LjV2OWMwIC4yOC0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNSAxNGEuNDcuNDcgMCAwIDEtLjM1LS4xNWwtMy41LTMuNWMtLjItLjItLjItLjUxIDAtLjcxcy41MS0uMi43MSAwbDMuMTUgMy4xNWwzLjE1LTMuMTVjLjItLjIuNTEtLjIuNzEgMHMuMi41MSAwIC43MWwtMy41IDMuNWMtLjEuMS0uMjMuMTUtLjM1LjE1WiIvPjwvc3ZnPg==", + alt: "Arrow down" + }, + "icon-[formkit--arrowleft]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgMTYgOSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTEyLjUgNWgtOWMtLjI4IDAtLjUtLjIyLS41LS41cy4yMi0uNS41LS41aDljLjI4IDAgLjUuMjIuNS41cy0uMjIuNS0uNS41Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTYgOC41YS40Ny40NyAwIDAgMS0uMzUtLjE1bC0zLjUtMy41Yy0uMi0uMi0uMi0uNTEgMC0uNzFMNS42NS42NWMuMi0uMi41MS0uMi43MSAwcy4yLjUxIDAgLjcxTDMuMjEgNC41MWwzLjE1IDMuMTVjLjIuMi4yLjUxIDAgLjcxYy0uMS4xLS4yMy4xNS0uMzUuMTVaIi8+PC9zdmc+", + alt: "Arrow left" + }, + "icon-[mdi--brain]": { + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMjEuMzMgMTIuOTFjLjA5IDEuNTUtLjYyIDMuMDQtMS44OSAzLjk1bC43NyAxLjQ5Yy4yMy40NS4yNi45OC4wNiAxLjQ1Yy0uMTkuNDctLjU4Ljg0LTEuMDYgMWwtLjc5LjI1YTEuNjkgMS42OSAwIDAgMS0xLjg2LS41NUwxNC40NCAxOGMtLjg5LS4xNS0xLjczLS41My0yLjQ0LTEuMWMtLjUuMTUtMSAuMjMtMS41LjIzYy0uODggMC0xLjc2LS4yNy0yLjUtLjc5Yy0uNTMuMTYtMS4wNy4yMy0xLjYyLjIyYy0uNzkuMDEtMS41Ny0uMTUtMi4zLS40NWE0LjEgNC4xIDAgMCAxLTIuNDMtMy42MWMtLjA4LS43Mi4wNC0xLjQ1LjM1LTIuMTFjLS4yOS0uNzUtLjMyLTEuNTctLjA3LTIuMzNDMi4zIDcuMTEgMyA2LjMyIDMuODcgNS44MmMuNTgtMS42OSAyLjIxLTIuODIgNC0yLjdjMS42LTEuNSA0LjA1LTEuNjYgNS44My0uMzdjLjQyLS4xMS44Ni0uMTcgMS4zLS4xN2MxLjM2LS4wMyAyLjY1LjU3IDMuNSAxLjY0YzIuMDQuNTMgMy41IDIuMzUgMy41OCA0LjQ3Yy4wNSAxLjExLS4yNSAyLjItLjg2IDMuMTNjLjA3LjM2LjExLjcyLjExIDEuMDltLTUtMS40MWMuNTcuMDcgMS4wMi41IDEuMDIgMS4wN2ExIDEgMCAwIDEtMSAxaC0uNjNjLS4zMi45LS44OCAxLjY5LTEuNjIgMi4yOWMuMjUuMDkuNTEuMTQuNzcuMjFjNS4xMy0uMDcgNC41My0zLjIgNC41My0zLjI1YTIuNTkgMi41OSAwIDAgMC0yLjY5LTIuNDlhMSAxIDAgMCAxLTEtMWExIDEgMCAwIDEgMS0xYzEuMjMuMDMgMi40MS40OSAzLjMzIDEuM2MuMDUtLjI5LjA4LS41OS4wOC0uODljLS4wNi0xLjI0LS42Mi0yLjMyLTIuODctMi41M2MtMS4yNS0yLjk2LTQuNC0xLjMyLTQuNC0uNGMtLjAzLjIzLjIxLjcyLjI1Ljc1YTEgMSAwIDAgMSAxIDFjMCAuNTUtLjQ1IDEtMSAxYy0uNTMtLjAyLTEuMDMtLjIyLTEuNDMtLjU2Yy0uNDguMzEtMS4wMy41LTEuNi41NmMtLjU3LjA1LTEuMDQtLjM1LTEuMDctLjlhLjk3Ljk3IDAgMCAxIC44OC0xLjFjLjE2LS4wMi45NC0uMTQuOTQtLjc3YzAtLjY2LjI1LTEuMjkuNjgtMS43OWMtLjkyLS4yNS0xLjkxLjA4LTIuOTEgMS4yOUM2Ljc1IDUgNiA1LjI1IDUuNDUgNy4yQzQuNSA3LjY3IDQgOCAzLjc4IDljMS4wOC0uMjIgMi4xOS0uMTMgMy4yMi4yNWMuNS4xOS43OC43NS41OSAxLjI5Yy0uMTkuNTItLjc3Ljc4LTEuMjkuNTljLS43My0uMzItMS41NS0uMzQtMi4zLS4wNmMtLjMyLjI3LS4zMi44My0uMzIgMS4yN2MwIC43NC4zNyAxLjQzIDEgMS44M2MuNTMuMjcgMS4xMi40MSAxLjcxLjRxLS4yMjUtLjM5LS4zOS0uODFhMS4wMzggMS4wMzggMCAwIDEgMS45Ni0uNjhjLjQgMS4xNCAxLjQyIDEuOTIgMi42MiAyLjA1YzEuMzctLjA3IDIuNTktLjg4IDMuMTktMi4xM2MuMjMtMS4zOCAxLjM0LTEuNSAyLjU2LTEuNW0yIDcuNDdsLS42Mi0xLjNsLS43MS4xNmwxIDEuMjV6bS00LjY1LTguNjFhMSAxIDAgMCAwLS45MS0xLjAzYy0uNzEtLjA0LTEuNC4yLTEuOTMuNjdjLS41Ny41OC0uODcgMS4zOC0uODQgMi4xOWExIDEgMCAwIDAgMSAxYy41NyAwIDEtLjQ1IDEtMWMwLS4yNy4wNy0uNTQuMjMtLjc2Yy4xMi0uMS4yNy0uMTUuNDMtLjE1Yy41NS4wMyAxLjAyLS4zOCAxLjAyLS45MiIvPjwvc3ZnPg==", + alt: "Brain" + }, +} diff --git a/src/lib/utils/positionManager.ts b/src/lib/utils/positionManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd28b9af28b3d1eb5f17ff171f4129f104179a1b --- /dev/null +++ b/src/lib/utils/positionManager.ts @@ -0,0 +1,93 @@ +import type { Position3D } from './positionUtils'; + +/** + * Spiral-based position manager + * Assigns positions in a spiral pattern starting from center to avoid overlapping objects + * + * Pattern: Center -> Right -> Up -> Left -> Down -> Right (outward spiral) + * Example positions: (0,0) -> (1,0) -> (1,-1) -> (0,-1) -> (-1,-1) -> (-1,0) -> (-1,1) -> (0,1) -> (1,1) -> (2,1) ... + */ +export class PositionManager { + private static instance: PositionManager; + private gridSize = 5; // Distance between grid points + private spiralGenerator: Generator<{ x: number; z: number }, never, unknown>; + + static getInstance(): PositionManager { + if (!PositionManager.instance) { + PositionManager.instance = new PositionManager(); + } + return PositionManager.instance; + } + + constructor() { + this.spiralGenerator = this.generateSpiralPositions(); + // Skip the center position since there's already an object there + this.spiralGenerator.next(); + } + + /** + * Get next available position in a spiral pattern + * Starts from center (0,0) and spirals outward + */ + getNextPosition(): Position3D { + const { value: coord } = this.spiralGenerator.next(); + + return { + x: coord.x * this.gridSize, + y: 0, + z: coord.z * this.gridSize + }; + } + + /** + * Generator function that yields spiral positions infinitely + * Uses a simple clockwise spiral starting from origin + */ + private *generateSpiralPositions(): Generator<{ x: number; z: number }, never, unknown> { + let x = 0, z = 0; + let dx = 1, dz = 0; // Start moving right + let steps = 1; + let stepCount = 0; + let changeDirection = 0; + + // Yield center position first + yield { x, z }; + + // Generate spiral positions infinitely + while (true) { + x += dx; + z += dz; + yield { x, z }; + + stepCount++; + + // Change direction when we've completed the required steps + if (stepCount === steps) { + stepCount = 0; + changeDirection++; + + // Rotate 90 degrees clockwise: (dx, dz) -> (dz, -dx) + const temp = dx; + dx = dz; + dz = -temp; + + // Increase step count after every two direction changes + if (changeDirection % 2 === 0) { + steps++; + } + } + } + } + + /** + * Reset position generator (useful for testing) + */ + reset(): void { + this.spiralGenerator = this.generateSpiralPositions(); + // Skip the center position since there's already an object there + this.spiralGenerator.next(); + } +} + +// Global instance +export const positionManager = PositionManager.getInstance(); \ No newline at end of file diff --git a/src/lib/utils/positionUtils.ts b/src/lib/utils/positionUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..deed782527d6b7eabae4e476ed690111f725a619 --- /dev/null +++ b/src/lib/utils/positionUtils.ts @@ -0,0 +1,48 @@ +export interface Position3D { + x: number; + y: number; + z: number; +} + +/** + * Generate a random position within the given ranges + */ +export function generateRandomPosition( + xRange: [number, number], + yRange: [number, number], + zRange: [number, number] +): Position3D { + return { + x: Math.random() * (xRange[1] - xRange[0]) + xRange[0], + y: Math.random() * (yRange[1] - yRange[0]) + yRange[0], + z: Math.random() * (zRange[1] - zRange[0]) + zRange[0] + }; +} + +/** + * Clamp a position within bounds + */ +export function clampPosition( + position: Position3D, + bounds: { + x: [number, number]; + y: [number, number]; + z: [number, number]; + } +): Position3D { + return { + x: Math.max(bounds.x[0], Math.min(bounds.x[1], position.x)), + y: Math.max(bounds.y[0], Math.min(bounds.y[1], position.y)), + z: Math.max(bounds.z[0], Math.min(bounds.z[1], position.z)) + }; +} + +/** + * Calculate distance between two positions + */ +export function getDistance(pos1: Position3D, pos2: Position3D): number { + const dx = pos2.x - pos1.x; + const dy = pos2.y - pos1.y; + const dz = pos2.z - pos1.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4628fc629779c36b2f680953bf1422d4ed9329e5 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,37 @@ + + +
    + {@render children()} +
    + + + + \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b122db0fe1c1722f52f37fbb40df1e6296c8abbf --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,81 @@ + + +{#if workspaceId} + + + + + + + + + + + + + + + + + + + + + + + {#if dev} + + {/if} + +{:else} +
    +
    Loading workspace...
    +
    +{/if} \ No newline at end of file diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..33bd44cad0768e652211564eebc828f73e1566c4 --- /dev/null +++ b/static/favicon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e2404f3256de08f1b12fee023eea07cbf7581ba70a30b6eabf1e15f8eb99ea1 +size 3884 diff --git a/static/favicon_1024.png b/static/favicon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..7d44147dcd21f692033fb0303848fada34593728 --- /dev/null +++ b/static/favicon_1024.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c9fc87e78ff88fcc8a809d77b2e8b1c3cb9cf3117ad1d40e23ba46b28fb9df2 +size 255401 diff --git a/static/favicon_128.png b/static/favicon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..e80468599b05eb182719df600e60714ff608d654 --- /dev/null +++ b/static/favicon_128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bb504fd708855ba1a8b186b972381a3965edf45d86a09428e1638564099cfe0 +size 3794 diff --git a/static/gpu/license.txt b/static/gpu/license.txt new file mode 100644 index 0000000000000000000000000000000000000000..e280e0dce6ed216b3f472392d6dbca390dca7d0f --- /dev/null +++ b/static/gpu/license.txt @@ -0,0 +1,11 @@ +Model Information: +* title: Nvidia GeForce RTX 3090 +* source: https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e +* author: Cem Gürbüz (https://sketchfab.com/cemgurbuzz) + +Model License: +* license type: CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/) +* requirements: Author must be credited. No commercial use. + +If you use this 3D model in your project be sure to copy paste this credit wherever you share it: +This work is based on "Nvidia GeForce RTX 3090" (https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e) by Cem Gürbüz (https://sketchfab.com/cemgurbuzz) licensed under CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/) \ No newline at end of file diff --git a/static/gpu/scene.bin b/static/gpu/scene.bin new file mode 100644 index 0000000000000000000000000000000000000000..f580070724d570f951b0e792014c58a47852ce0b --- /dev/null +++ b/static/gpu/scene.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae5f7724b7e079682c88ab2685541abc29989c2a3fbbfbc02266bb35530ddf82 +size 5694824 diff --git a/static/gpu/scene.gltf b/static/gpu/scene.gltf new file mode 100644 index 0000000000000000000000000000000000000000..8c51a65b308f2a7a52ea2055e7271d7951817554 --- /dev/null +++ b/static/gpu/scene.gltf @@ -0,0 +1,2494 @@ +{ + "accessors": [ + { + "bufferView": 2, + "componentType": 5126, + "count": 18466, + "max": [ + 224.7469940185547, + 25.571533203125, + 88.23554992675781 + ], + "min": [ + -224.74513244628906, + -35.34562683105469, + -88.20793914794922 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 221592, + "componentType": 5126, + "count": 18466, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 18466, + "max": [ + 0.8829709887504578, + 1.0, + 1.0, + 1.0 + ], + "min": [ + -0.8874375224113464, + -1.0, + -1.0, + -1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 18466, + "max": [ + 0.9974566698074341, + 1.0000001192092896 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 147728, + "componentType": 5126, + "count": 18466, + "max": [ + 0.9974566698074341, + 1.0000001192092896 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 295456, + "componentType": 5126, + "count": 18466, + "max": [ + 0.9974566698074341, + 1.0000001192092896 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "componentType": 5125, + "count": 80496, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 443184, + "componentType": 5126, + "count": 14, + "max": [ + 115.76972961425781, + 0.0, + 100.0 + ], + "min": [ + -97.66373443603516, + 0.0, + -100.0 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 443352, + "componentType": 5126, + "count": 14, + "max": [ + 0.0, + 1.0, + 0.0 + ], + "min": [ + 0.0, + 1.0, + 0.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 295456, + "componentType": 5126, + "count": 14, + "max": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "min": [ + -1.4227993005988537e-07, + 0.0, + 1.0, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 443184, + "componentType": 5126, + "count": 14, + "max": [ + 0.5462319254875183, + 0.5829209089279175 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 443296, + "componentType": 5126, + "count": 14, + "max": [ + 0.5462319254875183, + 0.5829209089279175 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 443408, + "componentType": 5126, + "count": 14, + "max": [ + 0.5462319254875183, + 0.5829209089279175 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 321984, + "componentType": 5125, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 443520, + "componentType": 5126, + "count": 1792, + "max": [ + 99.35418701171875, + 3.8504819869995117, + 99.35418701171875 + ], + "min": [ + -99.35418701171875, + -36.769859313964844, + -99.35418701171875 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 465024, + "componentType": 5126, + "count": 1792, + "max": [ + 1.0, + 0.9999999403953552, + 1.0 + ], + "min": [ + -1.0, + -0.9999999403953552, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 295680, + "componentType": 5126, + "count": 1792, + "max": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 443520, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 457856, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 472192, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 322128, + "componentType": 5125, + "count": 9216, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 486528, + "componentType": 5126, + "count": 5519, + "max": [ + 243.8433380126953, + 3.845768690109253, + 245.13909912109375 + ], + "min": [ + -245.8629150390625, + -81.39239501953125, + -244.58486938476563 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 552756, + "componentType": 5126, + "count": 5519, + "max": [ + 0.9999997615814209, + 0.9999998807907104, + 0.9999997615814209 + ], + "min": [ + -0.9999997615814209, + -0.9999996423721313, + -0.9999997615814209 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 324352, + "componentType": 5126, + "count": 5519, + "max": [ + 0.9960846900939941, + 0.9999998211860657, + 0.9974399209022522, + 1.0 + ], + "min": [ + -0.9998785257339478, + -0.9999998211860657, + -0.9908028244972229, + -1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 486528, + "componentType": 5126, + "count": 5519, + "max": [ + 0.8715417385101318, + 1.0 + ], + "min": [ + 0.0001247699256055057, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 530680, + "componentType": 5126, + "count": 5519, + "max": [ + 0.8715417385101318, + 1.0 + ], + "min": [ + 0.0001247699256055057, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 574832, + "componentType": 5126, + "count": 5519, + "max": [ + 0.8715417385101318, + 1.0 + ], + "min": [ + 0.0001247699256055057, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 358992, + "componentType": 5125, + "count": 24528, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 618984, + "componentType": 5126, + "count": 948, + "max": [ + 89.06101989746094, + 3.585411310195923, + 89.0611343383789 + ], + "min": [ + -89.06101989746094, + -81.14639282226563, + -89.06101989746094 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 630360, + "componentType": 5126, + "count": 948, + "max": [ + 0.9999997615814209, + 1.0, + 0.9999997615814209 + ], + "min": [ + -0.9999997615814209, + -1.0, + -0.9999997615814209 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 618984, + "componentType": 5126, + "count": 948, + "max": [ + 0.8170047998428345, + 0.5114779472351074 + ], + "min": [ + 0.7189478874206543, + 0.2939568758010864 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 457104, + "componentType": 5125, + "count": 4224, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 641736, + "componentType": 5126, + "count": 136, + "max": [ + 77.20637512207031, + -1.1121496754640248e-06, + 25.964923858642578 + ], + "min": [ + -77.20637512207031, + -10.26113510131836, + -53.971092224121094 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 643368, + "componentType": 5126, + "count": 136, + "max": [ + 0.995922327041626, + 1.0, + 0.9986786842346191 + ], + "min": [ + -0.995922327041626, + -1.0, + -0.9876721501350403 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 412656, + "componentType": 5126, + "count": 136, + "max": [ + 0.8069314956665039, + 0.6983179450035095, + 1.0, + 1.0 + ], + "min": [ + -0.5064710974693298, + -1.0, + -0.995922327041626, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 626568, + "componentType": 5126, + "count": 136, + "max": [ + 0.9828680753707886, + 0.9973888397216797 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 627656, + "componentType": 5126, + "count": 136, + "max": [ + 0.9828680753707886, + 0.9973888397216797 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 628744, + "componentType": 5126, + "count": 136, + "max": [ + 0.9828680753707886, + 0.9973888397216797 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 474000, + "componentType": 5125, + "count": 348, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 645000, + "componentType": 5126, + "count": 260, + "max": [ + 72.35018920898438, + -1.4828661960564204e-06, + 13.962870597839355 + ], + "min": [ + -82.06255340576172, + -10.26113510131836, + -65.97315216064453 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 648120, + "componentType": 5126, + "count": 260, + "max": [ + 0.9959227442741394, + 1.0, + 0.9986786842346191 + ], + "min": [ + -0.9959227442741394, + -1.0, + -0.9876720309257507 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 414832, + "componentType": 5126, + "count": 260, + "max": [ + 0.850715160369873, + 1.0, + 1.0, + 1.0 + ], + "min": [ + -0.5064702033996582, + -1.0, + -0.9959227442741394, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 629832, + "componentType": 5126, + "count": 260, + "max": [ + 0.7645499110221863, + 1.0000001192092896 + ], + "min": [ + 0.028024811297655106, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 631912, + "componentType": 5126, + "count": 260, + "max": [ + 0.7645499110221863, + 1.0000001192092896 + ], + "min": [ + 0.028024811297655106, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 633992, + "componentType": 5126, + "count": 260, + "max": [ + 0.7645499110221863, + 1.0000001192092896 + ], + "min": [ + 0.028024811297655106, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 475392, + "componentType": 5125, + "count": 708, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 651240, + "componentType": 5126, + "count": 1792, + "max": [ + 99.35418701171875, + 3.8504819869995117, + 99.35418701171875 + ], + "min": [ + -99.35418701171875, + -36.769859313964844, + -99.35418701171875 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 672744, + "componentType": 5126, + "count": 1792, + "max": [ + 1.0, + 0.9999999403953552, + 1.0 + ], + "min": [ + -1.0, + -0.9999999403953552, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 418992, + "componentType": 5126, + "count": 1792, + "max": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 636072, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 650408, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 664744, + "componentType": 5126, + "count": 1792, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 478224, + "componentType": 5125, + "count": 9216, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 694248, + "componentType": 5126, + "count": 568, + "max": [ + 100.02538299560547, + 0.0050859092734754086, + 100.0043716430664 + ], + "min": [ + -102.13546752929688, + -3.877331256866455, + -104.43467712402344 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 701064, + "componentType": 5126, + "count": 568, + "max": [ + 0.9999963641166687, + 1.0, + 0.999996542930603 + ], + "min": [ + -0.999996542930603, + -1.0, + -0.9998822808265686 + ], + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 515088, + "componentType": 5125, + "count": 912, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 707880, + "componentType": 5126, + "count": 568, + "max": [ + 100.02538299560547, + 0.0050859092734754086, + 100.0043716430664 + ], + "min": [ + -102.13546752929688, + -3.877331256866455, + -104.43467712402344 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 714696, + "componentType": 5126, + "count": 568, + "max": [ + 0.9999963641166687, + 1.0, + 0.999996542930603 + ], + "min": [ + -0.999996542930603, + -1.0, + -0.9998822808265686 + ], + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 518736, + "componentType": 5125, + "count": 912, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 721512, + "componentType": 5126, + "count": 92, + "max": [ + 115.76972961425781, + 0.7328627109527588, + 100.0 + ], + "min": [ + -97.66373443603516, + 0.0, + -100.0 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 722616, + "componentType": 5126, + "count": 92, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 679080, + "componentType": 5126, + "count": 92, + "max": [ + 0.4384359121322632, + 0.9677327871322632 + ], + "min": [ + 0.24906408786773682, + 0.7783609628677368 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 522384, + "componentType": 5125, + "count": 156, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 723720, + "componentType": 5126, + "count": 246, + "max": [ + 135.4468536376953, + 1.6171379089355469, + 15.116771697998047 + ], + "min": [ + -8.459820747375488, + 0.0, + -9.43148136138916 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 726672, + "componentType": 5126, + "count": 246, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 679816, + "componentType": 5126, + "count": 246, + "max": [ + 0.4994727373123169, + 0.8554167747497559 + ], + "min": [ + 0.2284148782491684, + 0.7796869277954102 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 523008, + "componentType": 5125, + "count": 702, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 729624, + "componentType": 5126, + "count": 440, + "max": [ + 1.1522057056427002, + 20.3384952545166, + 71.93663024902344 + ], + "min": [ + -16.551036834716797, + -20.3384952545166, + -98.71595764160156 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 734904, + "componentType": 5126, + "count": 440, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 447664, + "componentType": 5126, + "count": 440, + "max": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 681784, + "componentType": 5126, + "count": 440, + "max": [ + 0.7879384160041809, + 0.9999999403953552 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 685304, + "componentType": 5126, + "count": 440, + "max": [ + 0.7879384160041809, + 0.9999999403953552 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 688824, + "componentType": 5126, + "count": 440, + "max": [ + 0.7879384160041809, + 0.9999999403953552 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 525816, + "componentType": 5125, + "count": 1116, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 740184, + "componentType": 5126, + "count": 22938, + "max": [ + 92.23814392089844, + 35.8652458190918, + 79.53164672851563 + ], + "min": [ + -118.7790298461914, + -14.789424896240234, + -79.50471496582031 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1015440, + "componentType": 5126, + "count": 22938, + "max": [ + 1.0, + 1.0, + 0.7779618501663208 + ], + "min": [ + -1.0, + -1.0, + -0.7796278595924377 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 692344, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 875848, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1059352, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 530280, + "componentType": 5125, + "count": 68424, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1290696, + "componentType": 5126, + "count": 22938, + "max": [ + 92.23814392089844, + 35.8652458190918, + 79.53164672851563 + ], + "min": [ + -118.7790298461914, + -14.789424896240234, + -79.50471496582031 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1565952, + "componentType": 5126, + "count": 22938, + "max": [ + 1.0, + 1.0, + 0.7779618501663208 + ], + "min": [ + -1.0, + -1.0, + -0.7796278595924377 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 1242856, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1426360, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1609864, + "componentType": 5126, + "count": 22938, + "max": [ + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 803976, + "componentType": 5125, + "count": 68424, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1841208, + "componentType": 5126, + "count": 5977, + "max": [ + 243.8433380126953, + 3.845768690109253, + 245.13909912109375 + ], + "min": [ + -245.8629150390625, + -81.39239501953125, + -244.58486938476563 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1912932, + "componentType": 5126, + "count": 5977, + "max": [ + 0.9999997615814209, + 0.9999998807907104, + 0.9999997615814209 + ], + "min": [ + -0.9999997615814209, + -0.9999996423721313, + -0.9999997615814209 + ], + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 454704, + "componentType": 5126, + "count": 5977, + "max": [ + 0.999821662902832, + 0.9999991655349731, + 0.9965219497680664, + 1.0 + ], + "min": [ + -0.9999917149543762, + -0.9999999403953552, + -0.9999608397483826, + 1.0 + ], + "type": "VEC4" + }, + { + "bufferView": 1, + "byteOffset": 1793368, + "componentType": 5126, + "count": 5977, + "max": [ + 1.0000001192092896, + 0.9204620122909546 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1841184, + "componentType": 5126, + "count": 5977, + "max": [ + 1.0000001192092896, + 0.9204620122909546 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 1, + "byteOffset": 1889000, + "componentType": 5126, + "count": 5977, + "max": [ + 1.0000001192092896, + 0.9204620122909546 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 1077672, + "componentType": 5125, + "count": 24528, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 1984656, + "componentType": 5126, + "count": 948, + "max": [ + 89.06101989746094, + 3.585411310195923, + 89.0611343383789 + ], + "min": [ + -89.06101989746094, + -81.14639282226563, + -89.06101989746094 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 1996032, + "componentType": 5126, + "count": 948, + "max": [ + 0.9999997615814209, + 1.0, + 0.9999997615814209 + ], + "min": [ + -0.9999997615814209, + -1.0, + -0.9999997615814209 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 1936816, + "componentType": 5126, + "count": 948, + "max": [ + 0.8170047998428345, + 0.5114779472351074 + ], + "min": [ + 0.7189478874206543, + 0.2939568758010864 + ], + "type": "VEC2" + }, + { + "bufferView": 0, + "byteOffset": 1175784, + "componentType": 5125, + "count": 4224, + "type": "SCALAR" + } + ], + "asset": { + "extras": { + "author": "Cem Gürbüz (https://sketchfab.com/cemgurbuzz)", + "license": "CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)", + "source": "https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e", + "title": "Nvidia GeForce RTX 3090" + }, + "generator": "Sketchfab-12.68.0", + "version": "2.0" + }, + "bufferViews": [ + { + "buffer": 0, + "byteLength": 1192680, + "name": "floatBufferViews", + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 1944400, + "byteOffset": 1192680, + "byteStride": 8, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2007408, + "byteOffset": 3137080, + "byteStride": 12, + "name": "floatBufferViews", + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 550336, + "byteOffset": 5144488, + "byteStride": 16, + "name": "floatBufferViews", + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 5694824, + "uri": "scene.bin" + } + ], + "images": [ + { + "uri": "/textures/Metal_baseColor.png" + }, + { + "uri": "/textures/Metal_metallicRoughness.png" + }, + { + "uri": "/textures/Metal_normal.png" + }, + { + "uri": "/textures/Black_baseColor.png" + }, + { + "uri": "/textures/Black_metallicRoughness.png" + }, + { + "uri": "/textures/Black_normal.png" + }, + { + "uri": "/textures/Black_Fan_baseColor.png" + }, + { + "uri": "/textures/Black_Fan_metallicRoughness.png" + }, + { + "uri": "/textures/Black_Fan_normal.png" + }, + { + "uri": "/textures/Slot.1_baseColor.png" + }, + { + "uri": "/textures/Metal_S_baseColor.png" + }, + { + "uri": "/textures/Metal_S_metallicRoughness.png" + }, + { + "uri": "/textures/Metal_S_normal.png" + } + ], + "materials": [ + { + "doubleSided": true, + "name": "Metal", + "normalTexture": { + "index": 2 + }, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.7374110015, + 0.7374110015, + 0.7374110015, + 1.0 + ], + "baseColorTexture": { + "index": 0 + }, + "metallicRoughnessTexture": { + "index": 1 + }, + "roughnessFactor": 0.2692376679 + } + }, + { + "doubleSided": true, + "name": "Black", + "normalTexture": { + "index": 5 + }, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.792132559935307, + 0.792132559935307, + 0.792132559935307, + 1.0 + ], + "baseColorTexture": { + "index": 3 + }, + "metallicRoughnessTexture": { + "index": 4 + } + } + }, + { + "doubleSided": true, + "name": "Black_Fan", + "normalTexture": { + "index": 8 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 6 + }, + "metallicRoughnessTexture": { + "index": 7 + }, + "roughnessFactor": 0.3908411311554153 + } + }, + { + "doubleSided": true, + "name": "Slot.1", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 9 + }, + "metallicFactor": 0.0, + "roughnessFactor": 0.7927045273 + } + }, + { + "doubleSided": true, + "name": "Metal_Black", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.1717234471995979, + 0.14983924486362052, + 0.14983924486362052, + 1.0 + ], + "roughnessFactor": 0.6097273650353562 + } + }, + { + "doubleSided": true, + "name": "Black.001", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.0330691, + 0.0330691, + 0.0330691, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.9918997578746929 + } + }, + { + "doubleSided": true, + "name": "Slot", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 9 + }, + "metallicFactor": 0.0, + "roughnessFactor": 0.8040408206 + } + }, + { + "doubleSided": true, + "name": "Metal_S", + "normalTexture": { + "index": 12 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 10 + }, + "metallicRoughnessTexture": { + "index": 11 + } + } + } + ], + "meshes": [ + { + "name": "Metal Frame_Metal_0", + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 0, + "TANGENT": 2, + "TEXCOORD_0": 3, + "TEXCOORD_1": 4, + "TEXCOORD_2": 5 + }, + "indices": 6, + "material": 0, + "mode": 4 + } + ] + }, + { + "name": "Front Cover_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 8, + "POSITION": 7, + "TANGENT": 9, + "TEXCOORD_0": 10, + "TEXCOORD_1": 11, + "TEXCOORD_2": 12 + }, + "indices": 13, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Fan Circle_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 15, + "POSITION": 14, + "TANGENT": 16, + "TEXCOORD_0": 17, + "TEXCOORD_1": 18, + "TEXCOORD_2": 19 + }, + "indices": 20, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan F_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 22, + "POSITION": 21, + "TANGENT": 23, + "TEXCOORD_0": 24, + "TEXCOORD_1": 25, + "TEXCOORD_2": 26 + }, + "indices": 27, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan F_Slot.1_0", + "primitives": [ + { + "attributes": { + "NORMAL": 29, + "POSITION": 28, + "TEXCOORD_0": 30 + }, + "indices": 31, + "material": 3, + "mode": 4 + } + ] + }, + { + "name": "Front Cover U_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 33, + "POSITION": 32, + "TANGENT": 34, + "TEXCOORD_0": 35, + "TEXCOORD_1": 36, + "TEXCOORD_2": 37 + }, + "indices": 38, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Front Cover T_Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 40, + "POSITION": 39, + "TANGENT": 41, + "TEXCOORD_0": 42, + "TEXCOORD_1": 43, + "TEXCOORD_2": 44 + }, + "indices": 45, + "material": 1, + "mode": 4 + } + ] + }, + { + "name": "Fan Circle B_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 47, + "POSITION": 46, + "TANGENT": 48, + "TEXCOORD_0": 49, + "TEXCOORD_1": 50, + "TEXCOORD_2": 51 + }, + "indices": 52, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Grills U_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 54, + "POSITION": 53 + }, + "indices": 55, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Grills T_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 57, + "POSITION": 56 + }, + "indices": 58, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Plane.010_Black.001_0", + "primitives": [ + { + "attributes": { + "NORMAL": 60, + "POSITION": 59, + "TEXCOORD_0": 61 + }, + "indices": 62, + "material": 5, + "mode": 4 + } + ] + }, + { + "name": "Socket_Slot_0", + "primitives": [ + { + "attributes": { + "NORMAL": 64, + "POSITION": 63, + "TEXCOORD_0": 65 + }, + "indices": 66, + "material": 6, + "mode": 4 + } + ] + }, + { + "name": "Side Metal Part_Metal S_0", + "primitives": [ + { + "attributes": { + "NORMAL": 68, + "POSITION": 67, + "TANGENT": 69, + "TEXCOORD_0": 70, + "TEXCOORD_1": 71, + "TEXCOORD_2": 72 + }, + "indices": 73, + "material": 7, + "mode": 4 + } + ] + }, + { + "name": "Grills F.003_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 75, + "POSITION": 74, + "TEXCOORD_0": 76, + "TEXCOORD_1": 77, + "TEXCOORD_2": 78 + }, + "indices": 79, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Grills F.002_Metal Black_0", + "primitives": [ + { + "attributes": { + "NORMAL": 81, + "POSITION": 80, + "TEXCOORD_0": 82, + "TEXCOORD_1": 83, + "TEXCOORD_2": 84 + }, + "indices": 85, + "material": 4, + "mode": 4 + } + ] + }, + { + "name": "Fan B_Black Fan_0", + "primitives": [ + { + "attributes": { + "NORMAL": 87, + "POSITION": 86, + "TANGENT": 88, + "TEXCOORD_0": 89, + "TEXCOORD_1": 90, + "TEXCOORD_2": 91 + }, + "indices": 92, + "material": 2, + "mode": 4 + } + ] + }, + { + "name": "Fan B_Slot.1_0", + "primitives": [ + { + "attributes": { + "NORMAL": 94, + "POSITION": 93, + "TEXCOORD_0": 95 + }, + "indices": 96, + "material": 3, + "mode": 4 + } + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.220446049250313e-16, + -1.0, + 0.0, + 0.0, + 1.0, + 2.220446049250313e-16, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "name": "Sketchfab_model" + }, + { + "children": [ + 2 + ], + "matrix": [ + 0.009999999776482582, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009999999776482582, + 0.0, + 0.0, + -0.009999999776482582, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "name": "747a3f0779154e25a172cd94a8a85a59.fbx" + }, + { + "children": [ + 3, + 5, + 7, + 9, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32 + ], + "name": "RootNode" + }, + { + "children": [ + 4 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.6292067939183141e-07, + 0.9999999999999868, + 0.0, + 0.0, + -0.9999999999999868, + -1.6292067939183141e-07, + 0.0, + -0.0009551644325256348, + 88.30477905273438, + -8.472945213317871, + 1.0 + ], + "name": "Metal Frame" + }, + { + "mesh": 0, + "name": "Metal Frame_Metal_0" + }, + { + "children": [ + 6 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.6292067939183141e-07, + 0.9999999999999868, + 0.0, + 0.0, + -0.8362743854522594, + -1.362463910358702e-07, + 0.0, + -122.30303192138672, + 89.6927261352539, + 12.111770629882813, + 1.0 + ], + "name": "Front Cover" + }, + { + "mesh": 1, + "name": "Front Cover_Black_0" + }, + { + "children": [ + 8 + ], + "matrix": [ + 0.7936016917228699, + 0.0, + 0.0, + 0.0, + 0.0, + -1.292941267819967e-07, + 0.7936016917228594, + 0.0, + 0.0, + -0.7936016917228594, + -1.292941267819967e-07, + 0.0, + 127.49998474121094, + 88.5121078491211, + 10.287901878356934, + 1.0 + ], + "name": "Fan Circle" + }, + { + "mesh": 2, + "name": "Fan Circle_Black Fan_0" + }, + { + "children": [ + 10, + 11 + ], + "matrix": [ + 0.30334164847183204, + 0.015296130773162211, + 1.9346649197391982e-09, + 0.0, + 2.3832869586734116e-09, + -8.567920238331605e-08, + 0.3037270605563997, + 0.0, + 0.015296130773162149, + -0.30334164847182, + -8.569050622925244e-08, + 0.0, + 127.49998474121094, + 88.5121078491211, + 10.287901878356934, + 1.0 + ], + "name": "Fan F" + }, + { + "mesh": 3, + "name": "Fan F_Black Fan_0" + }, + { + "mesh": 4, + "name": "Fan F_Slot.1_0" + }, + { + "children": [ + 13 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.6292067939183141e-07, + 0.9999999999999868, + 0.0, + 0.0, + -0.9999999999999868, + -1.6292067939183141e-07, + 0.0, + 0.021076202392578125, + 26.082378387451172, + 14.08882999420166, + 1.0 + ], + "name": "Front Cover U" + }, + { + "mesh": 5, + "name": "Front Cover U_Black_0" + }, + { + "children": [ + 15 + ], + "matrix": [ + -0.9999999999999203, + -3.8941437731121037e-07, + 8.742276236262104e-08, + 0.0, + 8.742269891895826e-08, + 1.6292071369772287e-07, + 0.9999999999999828, + 0.0, + -3.894143915541825e-07, + 0.9999999999999108, + -1.6292067961387602e-07, + 0.0, + -4.7524333000183105, + 163.40081787109375, + 14.088836669921875, + 1.0 + ], + "name": "Front Cover T" + }, + { + "mesh": 6, + "name": "Front Cover T_Black_0" + }, + { + "children": [ + 17 + ], + "matrix": [ + -0.7936016917228065, + 6.937887475876209e-08, + 3.090399089678156e-07, + 0.0, + -3.0903990372985424e-07, + 5.99152878920572e-08, + -0.7936016917228074, + 0.0, + -6.937889809063118e-08, + -0.7936016917228645, + -5.991526075495118e-08, + 0.0, + -124.1536636352539, + 88.51213073730469, + -40.17750549316406, + 1.0 + ], + "name": "Fan Circle B" + }, + { + "mesh": 7, + "name": "Fan Circle B_Black Fan_0" + }, + { + "children": [ + 19 + ], + "matrix": [ + 0.38677331740211784, + -0.3867733343085177, + 6.07269882145477e-17, + 0.0, + -3.632659060470012e-07, + -3.632658903889941e-07, + 11.752899169921864, + 0.0, + -0.38677333430851735, + -0.38677331740211757, + -2.3909259764264718e-08, + 0.0, + -0.11941343545913696, + 3.1571507453918457, + 3.0867953300476074, + 1.0 + ], + "name": "Grills U" + }, + { + "mesh": 8, + "name": "Grills U_Metal Black_0" + }, + { + "children": [ + 21 + ], + "matrix": [ + -0.38677332816141635, + 0.3867733235491907, + 1.4909048089098407e-07, + 0.0, + 3.6191709386740606e-06, + -9.112484713068894e-07, + 11.752899169921283, + 0.0, + 0.38677332354918276, + 0.3867733281614427, + -8.911436685577966e-08, + 0.0, + 0.7981881499290466, + 174.49168395996094, + 3.0868005752563477, + 1.0 + ], + "name": "Grills T" + }, + { + "mesh": 9, + "name": "Grills T_Metal Black_0" + }, + { + "children": [ + 23 + ], + "matrix": [ + -0.9999999999999203, + -3.8941437731121037e-07, + 8.742276236262104e-08, + 0.0, + 8.742269891895826e-08, + 1.6292071369772287e-07, + 0.9999999999999828, + 0.0, + -3.2565728098324384e-07, + 0.8362743854521959, + -1.3624639122156042e-07, + 0.0, + 121.83759307861328, + 88.41606140136719, + -34.240440368652344, + 1.0 + ], + "name": "Plane.010" + }, + { + "mesh": 10, + "name": "Plane.010_Black.001_0" + }, + { + "children": [ + 25 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -3.14346242379091e-07, + 1.929443478584264, + 0.0, + 0.0, + -0.9999999999999868, + -1.6292067939183141e-07, + 0.0, + -149.70736694335938, + 187.4652557373047, + -39.00934982299805, + 1.0 + ], + "name": "Socket" + }, + { + "mesh": 11, + "name": "Socket_Slot_0" + }, + { + "children": [ + 27 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.6292067939183141e-07, + 0.9999999999999868, + 0.0, + 0.0, + -0.9999999999999868, + -1.6292067939183141e-07, + 0.0, + -225.87086486816406, + 118.08707427978516, + -12.542006492614746, + 1.0 + ], + "name": "Side Metal Part" + }, + { + "mesh": 12, + "name": "Side Metal Part_Metal S_0" + }, + { + "children": [ + 29 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.6292067939183141e-07, + 0.9999999999999868, + 0.0, + 0.0, + -1.0234513282775743, + -1.667413857274569e-07, + 0.0, + 131.4942626953125, + 88.83919525146484, + -23.017173767089844, + 1.0 + ], + "name": "Grills F.003" + }, + { + "mesh": 13, + "name": "Grills F.003_Metal Black_0" + }, + { + "children": [ + 31 + ], + "matrix": [ + -0.9999999999999241, + 9.892427301959894e-15, + 3.8941437775530107e-07, + 0.0, + -3.7781312610095593e-07, + 7.32487015753169e-08, + -0.9702084660529326, + 0.0, + -4.0213853733746167e-14, + -1.0234513282775848, + -7.72684257400924e-08, + 0.0, + -128.1754608154297, + 88.83919525146484, + -4.171847343444824, + 1.0 + ], + "name": "Grills F.002" + }, + { + "mesh": 14, + "name": "Grills F.002_Metal Black_0" + }, + { + "children": [ + 33, + 34 + ], + "matrix": [ + -0.3033416743311482, + 0.01529620971837044, + 9.933227977876897e-08, + 0.0, + -1.003610534326315e-07, + -1.789911585542887e-08, + -0.3037270605563946, + 0.0, + -0.015296209718363723, + -0.30334167433116394, + 2.29307573058707e-08, + 0.0, + -123.89674377441406, + 88.5121078491211, + -37.82356262207031, + 1.0 + ], + "name": "Fan B" + }, + { + "mesh": 15, + "name": "Fan B_Black Fan_0" + }, + { + "mesh": 16, + "name": "Fan B_Slot.1_0" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "scene": 0, + "scenes": [ + { + "name": "Sketchfab_Scene", + "nodes": [ + 0 + ] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + }, + { + "sampler": 0, + "source": 2 + }, + { + "sampler": 0, + "source": 3 + }, + { + "sampler": 0, + "source": 4 + }, + { + "sampler": 0, + "source": 5 + }, + { + "sampler": 0, + "source": 6 + }, + { + "sampler": 0, + "source": 7 + }, + { + "sampler": 0, + "source": 8 + }, + { + "sampler": 0, + "source": 9 + }, + { + "sampler": 0, + "source": 10 + }, + { + "sampler": 0, + "source": 11 + }, + { + "sampler": 0, + "source": 12 + } + ] +} diff --git a/static/gpu/textures/Black_Fan_baseColor.png b/static/gpu/textures/Black_Fan_baseColor.png new file mode 100644 index 0000000000000000000000000000000000000000..20a5d51532707c7ca0ffaea627fc4e3912c69d3f --- /dev/null +++ b/static/gpu/textures/Black_Fan_baseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bc2b1f39fcc940a6199cc0da14a86e140c52461e44eb2ba55102b3a96b097d0 +size 384537 diff --git a/static/gpu/textures/Black_Fan_metallicRoughness.png b/static/gpu/textures/Black_Fan_metallicRoughness.png new file mode 100644 index 0000000000000000000000000000000000000000..fc3f382e4dac202fb75599b9dcaf71e7fa24817d --- /dev/null +++ b/static/gpu/textures/Black_Fan_metallicRoughness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e21c8681ab4250e6f4beaf88213ff5650917a0923092ec7b0797de4fd0be2881 +size 605 diff --git a/static/gpu/textures/Black_Fan_normal.png b/static/gpu/textures/Black_Fan_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..a3b8ca27ec588f4bd97a1dd69bdb878bbba9d71a --- /dev/null +++ b/static/gpu/textures/Black_Fan_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5c3242f8cbe26d6c30ec04c797996383fc95f6a53f4619c7f6a1cdc7541021c +size 904366 diff --git a/static/gpu/textures/Black_baseColor.png b/static/gpu/textures/Black_baseColor.png new file mode 100644 index 0000000000000000000000000000000000000000..04779aaee2ea008889b1b77f682884dbcb41953c --- /dev/null +++ b/static/gpu/textures/Black_baseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d291827f116a43b02a9b9d88791a3f6e5693d13e51008fc1cf5fbf90a55a07bb +size 172335 diff --git a/static/gpu/textures/Black_metallicRoughness.png b/static/gpu/textures/Black_metallicRoughness.png new file mode 100644 index 0000000000000000000000000000000000000000..e712cf7d42b9e4af401422a8637865a43f031cba --- /dev/null +++ b/static/gpu/textures/Black_metallicRoughness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72b4917decc7337d8b45cd8c374cc34718a6a2af273928adaf0db8696178081a +size 261707 diff --git a/static/gpu/textures/Black_normal.png b/static/gpu/textures/Black_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..25b8141d82e5b6fc9f2121b3af02025f6c9a6df0 --- /dev/null +++ b/static/gpu/textures/Black_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fc0e7128c7ac54b3fd3b776e4fbaae381c640c67cf57b210106671813552f88 +size 347540 diff --git a/static/gpu/textures/Metal_S_baseColor.png b/static/gpu/textures/Metal_S_baseColor.png new file mode 100644 index 0000000000000000000000000000000000000000..94c63da2d341b01dbf37372ec66b9f2e113ee6b4 --- /dev/null +++ b/static/gpu/textures/Metal_S_baseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:482721e58a210225b7d6b84140fc1edf68ef7757c9cf6dc62d11e044b1120016 +size 605 diff --git a/static/gpu/textures/Metal_S_metallicRoughness.png b/static/gpu/textures/Metal_S_metallicRoughness.png new file mode 100644 index 0000000000000000000000000000000000000000..591c6d900a54fefbf52db926787abd3c1ce26824 --- /dev/null +++ b/static/gpu/textures/Metal_S_metallicRoughness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05d21ce09733a5c377ed8b92fab7edc0442dff2b41c5ed0e0dfe95da6d006322 +size 605 diff --git a/static/gpu/textures/Metal_S_normal.png b/static/gpu/textures/Metal_S_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..b687549528e38d5cb9ef66a737e7b28d209a245a --- /dev/null +++ b/static/gpu/textures/Metal_S_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfba44d03e4b112d5d024d654b413a56b536ef9e03955a155f4dda47aa5f66b9 +size 605 diff --git a/static/gpu/textures/Metal_baseColor.png b/static/gpu/textures/Metal_baseColor.png new file mode 100644 index 0000000000000000000000000000000000000000..80d91f98afb278db7674e6a133f3a85ae30216a2 --- /dev/null +++ b/static/gpu/textures/Metal_baseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02c555b58a87ed3b58c836c75bea9092f94973a6ee08394cdec96d97ba49908 +size 6990 diff --git a/static/gpu/textures/Metal_metallicRoughness.png b/static/gpu/textures/Metal_metallicRoughness.png new file mode 100644 index 0000000000000000000000000000000000000000..5c570bddf89135825d83e8810d718a7b255b8a48 --- /dev/null +++ b/static/gpu/textures/Metal_metallicRoughness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6425c78205c0194069822ac6256e5b9ffcaa2dd05ecd642a235dfa0b5ac520d +size 605 diff --git a/static/gpu/textures/Metal_normal.png b/static/gpu/textures/Metal_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..b687549528e38d5cb9ef66a737e7b28d209a245a --- /dev/null +++ b/static/gpu/textures/Metal_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfba44d03e4b112d5d024d654b413a56b536ef9e03955a155f4dda47aa5f66b9 +size 605 diff --git a/static/gpu/textures/Slot.1_baseColor.png b/static/gpu/textures/Slot.1_baseColor.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b122904eda4c56fc4c2e9685929a006b76248c --- /dev/null +++ b/static/gpu/textures/Slot.1_baseColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa99aba8b9d18ca137b6eda7dbf08f6466d458d37722488d47903a987aea92ab +size 533056 diff --git a/static/robots/so-100/meshes/Base.stl b/static/robots/so-100/meshes/Base.stl new file mode 100755 index 0000000000000000000000000000000000000000..03bc1a5d0bbba5330f4833a2ee89fdc8825b91bf --- /dev/null +++ b/static/robots/so-100/meshes/Base.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb8afb0303c0125f265a797625cfaf646ac214d5d2dc9c986348c415755e2a74 +size 443484 diff --git a/static/robots/so-100/meshes/Base_Motor.stl b/static/robots/so-100/meshes/Base_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..bc3f3a63ce1d14560ec72a7fe8cc65c9af5990e2 --- /dev/null +++ b/static/robots/so-100/meshes/Base_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aac4485d448eca418d2a93df3f78f449bd59b21ec1af2a579171819fd44ea576 +size 154284 diff --git a/static/robots/so-100/meshes/Fixed_Jaw.stl b/static/robots/so-100/meshes/Fixed_Jaw.stl new file mode 100755 index 0000000000000000000000000000000000000000..4bb2138be0e72d20b944df51984aeb9fcbe17e03 --- /dev/null +++ b/static/robots/so-100/meshes/Fixed_Jaw.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5528867073033eaa4c27a8087180954f3f0635bd69d517d5cace92f01dd266b2 +size 264584 diff --git a/static/robots/so-100/meshes/Fixed_Jaw_Motor.stl b/static/robots/so-100/meshes/Fixed_Jaw_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..98bb0ad86391d635384ab5cb3e3e1ff84843135b --- /dev/null +++ b/static/robots/so-100/meshes/Fixed_Jaw_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0321bfb902d5a119c4f2a934ab4be7e9ea8c93a02e1685f066f99c24a5ca3ecf +size 151884 diff --git a/static/robots/so-100/meshes/Lower_Arm.stl b/static/robots/so-100/meshes/Lower_Arm.stl new file mode 100755 index 0000000000000000000000000000000000000000..e4d68753a33891eb9b9e56d7ee38961e5109f82e --- /dev/null +++ b/static/robots/so-100/meshes/Lower_Arm.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a04511bc65cbb702b2d6023764de947888b06a09fbf272803f39c22724329a3e +size 347184 diff --git a/static/robots/so-100/meshes/Lower_Arm_Motor.stl b/static/robots/so-100/meshes/Lower_Arm_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..bb7c71a30651879d3f5d8d48ce7e08b7164d1c7d --- /dev/null +++ b/static/robots/so-100/meshes/Lower_Arm_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07368fa4c50dea70e8157accee71072120c0cb374f672c85aba1d081702bc476 +size 151884 diff --git a/static/robots/so-100/meshes/Moving_Jaw.stl b/static/robots/so-100/meshes/Moving_Jaw.stl new file mode 100755 index 0000000000000000000000000000000000000000..20bfe32f7ba11e39dadd6a4c4f51ba6386922e50 --- /dev/null +++ b/static/robots/so-100/meshes/Moving_Jaw.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d9c923024c75e6f0a8d8300317c4d8a17dd49880260cbc60b7fe5e0c6dadfa8 +size 177184 diff --git a/static/robots/so-100/meshes/Rotation_Pitch.stl b/static/robots/so-100/meshes/Rotation_Pitch.stl new file mode 100755 index 0000000000000000000000000000000000000000..1035e7915b034c95306b413804e800e87fed1fd0 --- /dev/null +++ b/static/robots/so-100/meshes/Rotation_Pitch.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a1476f57d28d2c065bc0ede6a674cb3ebacf1193372916ef1e0ff32eeb92cf0 +size 347584 diff --git a/static/robots/so-100/meshes/Rotation_Pitch_Motor.stl b/static/robots/so-100/meshes/Rotation_Pitch_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..61d6fccc7b7ea29f98d227a92de57c8109df7c06 --- /dev/null +++ b/static/robots/so-100/meshes/Rotation_Pitch_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4fbcf18d8f2e91d12a767c1679430140dce0c81ea1478cc153bfce3a162db61 +size 154284 diff --git a/static/robots/so-100/meshes/Upper_Arm.stl b/static/robots/so-100/meshes/Upper_Arm.stl new file mode 100755 index 0000000000000000000000000000000000000000..273e98a4b1a51d5b3aa1724132e2f2e31355816f --- /dev/null +++ b/static/robots/so-100/meshes/Upper_Arm.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d97ad37c90400fe39bb2d5ace8b4290f3d1262e13f1d4f166494421752d5f3b7 +size 215184 diff --git a/static/robots/so-100/meshes/Upper_Arm_Motor.stl b/static/robots/so-100/meshes/Upper_Arm_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..7a195a05c6da71ccbcc0d0e059c071787636e412 --- /dev/null +++ b/static/robots/so-100/meshes/Upper_Arm_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bafdb2cb9e9a884e62b5fbf15b1f706a65771da82764dbe703df128d9f34d723 +size 152284 diff --git a/static/robots/so-100/meshes/Wrist_Pitch_Roll.stl b/static/robots/so-100/meshes/Wrist_Pitch_Roll.stl new file mode 100755 index 0000000000000000000000000000000000000000..90f30820a9f77552dfc7d24147b06323c25b8839 --- /dev/null +++ b/static/robots/so-100/meshes/Wrist_Pitch_Roll.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58e83ce1c8b24e1fa3a79672ea965ea4143986c81a21386dcd6da1cbcfdc94bb +size 341684 diff --git a/static/robots/so-100/meshes/Wrist_Pitch_Roll_Motor.stl b/static/robots/so-100/meshes/Wrist_Pitch_Roll_Motor.stl new file mode 100755 index 0000000000000000000000000000000000000000..d010f37a43799b8f384fdd0198724bf3372b8288 --- /dev/null +++ b/static/robots/so-100/meshes/Wrist_Pitch_Roll_Motor.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ac3fb1d2536fa4f7c40e7449be4b934d7d776f9cd0453c842767f980ff6c18 +size 133684 diff --git a/static/robots/so-100/so_arm100.urdf b/static/robots/so-100/so_arm100.urdf new file mode 100644 index 0000000000000000000000000000000000000000..2bea5e4eddac477535a3c174ffb7ccafea580eb1 --- /dev/null +++ b/static/robots/so-100/so_arm100.urdf @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -1.9 + 1.9 + + + + + + + + + + + 0.1 + 3.4 + + + + + + + + + + + -3.0 + -0.1 + + + + + + + + + + + -2.4 + 1.1 + + + + + + + + + + + -3.0 + 3.0 + + + + + + + + + + + -0.1 + 1.9 + + + + + + + + + diff --git a/static/video.mp4 b/static/video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..b4f49e2537f60c251c274e460e423682a3828ece --- /dev/null +++ b/static/video.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c19b251d8e53e619b9931f375565adcf8ff850b0b02733b8fc6531c186110e0 +size 2155024 diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000000000000000000000000000000000..94d9b73379903189b2e538783c0b8bc5d934623a --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,28 @@ +import adapter from "@sveltejs/adapter-static"; +// import adapter from "@sveltejs/adapter-auto"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }), + alias: { + "@/*": "./src/lib/*" + } + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..0b2d8865f4efe30f84b6516a5de8d6bd6b927fb6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..f93b6b98a4b808ac74a79dd7f88b6ea30a0c872e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +import tailwindcss from "@tailwindcss/vite"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { fs: { allow: ["../backend/client/js", "packages/feetech.js"] } } +});