From b799e2c1c5b551951895f64d22c0ed3340f5320a Mon Sep 17 00:00:00 2001 From: yyda Date: Sat, 21 Mar 2026 10:55:44 +0800 Subject: [PATCH] feat: Implement complete ACT functions for ball pickup and red bucket placement - Implement two core ACT functions: 1. Automatically locate and pick up the ball 2. Find the red bucket and place the ball - Pathfinding and obstacle avoidance: 1. Implement object-finding pathfinding algorithm for the robot car 2. Currently support target finding in obstacle-free environments 3. TODO: Improve dynamic obstacle avoidance logic and fix bugs related to the ball and map center --- .gitignore | 9 + package-lock.json | 771 +++++++++++++++++++++++++++++++++++++++ src/App.tsx | 239 ++++++++++-- src/sim/actExecutor.ts | 386 ++++++++++++++++++++ src/sim/armController.ts | 224 ++++++++++++ src/sim/environment.ts | 11 + src/sim/pathfinding.ts | 227 ++++++++++++ src/sim/vision.ts | 91 +++++ 8 files changed, 1927 insertions(+), 31 deletions(-) create mode 100644 src/sim/actExecutor.ts create mode 100644 src/sim/armController.ts create mode 100644 src/sim/pathfinding.ts create mode 100644 src/sim/vision.ts diff --git a/.gitignore b/.gitignore index 747bbed4..35bc220c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ coverage/ !.env.example *-lock.json *-lock.yaml + +# Spec files +spec.md +tasks.md +checklist.md + +# Development docs +*.md +!README.md diff --git a/package-lock.json b/package-lock.json index 7eb25986..4d5006be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "better-sqlite3": "^12.4.1", "dotenv": "^17.2.3", "express": "^4.21.2", + "jszip": "^3.10.1", "lucide-react": "^0.546.0", "motion": "^12.23.24", "react": "^19.0.0", "react-dom": "^19.0.0", + "socket.io-client": "^4.8.3", "three": "^0.183.2", "vite": "^6.2.0" }, @@ -26,6 +28,7 @@ "@types/express": "^4.17.21", "@types/node": "^22.14.0", "autoprefixer": "^10.4.21", + "gh-pages": "^6.1.1", "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", @@ -796,6 +799,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1201,6 +1242,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -1902,6 +1949,23 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2074,6 +2138,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2346,6 +2423,23 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2399,6 +2493,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2500,6 +2600,19 @@ "node": ">=8" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -2553,6 +2666,13 @@ "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, + "node_modules/email-addresses": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/email-addresses/-/email-addresses-5.0.0.tgz", + "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2577,6 +2697,49 @@ "once": "^1.4.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", @@ -2692,6 +2855,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2777,6 +2950,33 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2823,6 +3023,47 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -2856,6 +3097,38 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2965,6 +3238,21 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3085,6 +3373,29 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/gh-pages": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/gh-pages/-/gh-pages-6.3.0.tgz", + "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "commander": "^13.0.0", + "email-addresses": "^5.0.0", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^11.1.1", + "globby": "^11.1.0" + }, + "bin": { + "gh-pages": "bin/gh-pages.js", + "gh-pages-clean": "bin/gh-pages-clean.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -3112,6 +3423,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "10.6.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", @@ -3269,6 +3614,22 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3290,6 +3651,16 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3299,6 +3670,35 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3368,6 +3768,61 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3389,6 +3844,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -3638,6 +4102,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3671,6 +4148,22 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3698,6 +4191,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -3707,6 +4210,33 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3972,6 +4502,35 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -3985,12 +4544,28 @@ "node": ">=8" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4000,6 +4575,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4037,6 +4622,16 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4055,6 +4650,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -4117,6 +4725,12 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -4179,6 +4793,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4296,6 +4931,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -4355,6 +5001,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4456,6 +5126,12 @@ "node": ">= 0.8.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4612,6 +5288,44 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4750,6 +5464,19 @@ "node": ">=0.10.0" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4831,6 +5558,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4846,6 +5586,19 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4917,6 +5670,16 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5669,6 +6432,14 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/src/App.tsx b/src/App.tsx index fe93810c..1f31fcb4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,8 @@ import { createScene, createCamera, createRenderer, createLights, createFloor } import { createRobot } from './sim/robot'; import { updateEnvironment } from './sim/environment'; import { tr } from 'motion/react-client'; +import { ACTExecutor, ACTTask } from './sim/actExecutor'; +import { ArmController } from './sim/armController'; export default function App() { // State @@ -41,6 +43,8 @@ export default function App() { ]); const [actionChunks, setActionChunks] = useState([]); const [activeKeys, setActiveKeys] = useState>({}); + const [actTask, setActTask] = useState('idle'); + const [actExecutorState, setActExecutorState] = useState<{ task: ACTTask; subTask: string; isComplete: boolean; isExecuting: boolean } | null>(null); // Persist settings useEffect(() => { @@ -80,6 +84,7 @@ export default function App() { episodes: [] as any[], currentEpisode: [] as any[], target: null as THREE.Mesh | null, + bucket: null as THREE.Mesh | null, walls: [] as THREE.Mesh[], environmentGroup: null as THREE.Group | null, dirLight: null as THREE.DirectionalLight | null, @@ -103,11 +108,15 @@ export default function App() { stuckCounter: 0, actionBuffer: [] as any[], lastInferenceLogTime: 0, - lastManualLogTime: 0 + lastManualLogTime: 0, + armController: null as ArmController | null, + actExecutor: null as ACTExecutor | null, + prevIsExecuting: false }); const isAtBottom = useRef(true); const isDragging = useRef(false); + const draggingObject = useRef(null); const raycaster = useRef(new THREE.Raycaster()); const mouse = useRef(new THREE.Vector2()); @@ -187,23 +196,35 @@ export default function App() { // Dragging Logic const onMouseDown = (event: MouseEvent) => { - if (!container || !sim.current.camera || !sim.current.target) return; + if (!container || !sim.current.camera) return; const rect = container.getBoundingClientRect(); mouse.current.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1; mouse.current.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1; raycaster.current.setFromCamera(mouse.current, sim.current.camera); - const intersects = raycaster.current.intersectObject(sim.current.target); - if (intersects.length > 0) { + // Check for both target (ball) and bucket + const objectsToCheck = [sim.current.target, sim.current.bucket].filter(obj => obj !== null) as THREE.Mesh[]; + let draggedObject: THREE.Mesh | null = null; + + for (const obj of objectsToCheck) { + const intersects = raycaster.current.intersectObject(obj); + if (intersects.length > 0) { + draggedObject = obj; + break; + } + } + + if (draggedObject) { isDragging.current = true; + draggingObject.current = draggedObject; if (renderer.domElement) renderer.domElement.style.cursor = 'grabbing'; } }; const onMouseMove = (event: MouseEvent) => { - if (!container || !sim.current.camera || !sim.current.target) return; + if (!container || !sim.current.camera) return; const rect = container.getBoundingClientRect(); mouse.current.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1; @@ -212,26 +233,38 @@ export default function App() { raycaster.current.setFromCamera(mouse.current, sim.current.camera); if (!isDragging.current) { - const intersects = raycaster.current.intersectObject(sim.current.target); + // Check for both target and bucket for cursor hover effect + const objectsToCheck = [sim.current.target, sim.current.bucket].filter(obj => obj !== null) as THREE.Mesh[]; + let hovering = false; + + for (const obj of objectsToCheck) { + const intersects = raycaster.current.intersectObject(obj); + if (intersects.length > 0) { + hovering = true; + break; + } + } + if (renderer.domElement) { - renderer.domElement.style.cursor = intersects.length > 0 ? 'grab' : 'default'; + renderer.domElement.style.cursor = hovering ? 'grab' : 'default'; } return; } - if (!sim.current.plane) return; + if (!sim.current.plane || !draggingObject.current) return; const intersects = raycaster.current.intersectObject(sim.current.plane); if (intersects.length > 0) { const point = intersects[0].point; - sim.current.target.position.x = point.x; - sim.current.target.position.z = point.z; + draggingObject.current.position.x = point.x; + draggingObject.current.position.z = point.z; } }; const onMouseUp = () => { isDragging.current = false; + draggingObject.current = null; if (renderer.domElement) renderer.domElement.style.cursor = 'default'; }; @@ -282,7 +315,32 @@ export default function App() { if (sim.current.armGroup) { sim.current.armGroup.visible = hasArm; } - }, [hasArm]); + + // Initialize ArmController and ACTExecutor if arm is available + if (hasArm && sim.current.arm && sim.current.robot) { + const armCtrl = new ArmController( + sim.current.armGroup!, + sim.current.arm.lowerArm, + sim.current.arm.elbow, + sim.current.arm.wrist, + sim.current.arm.gripper, + sim.current.environmentGroup!, + 0.1 + ); + sim.current.armController = armCtrl; + + const executor = new ACTExecutor( + sim.current.scene!, + sim.current.walls, + addLog, + armCtrl + ); + executor.setRobotRef(sim.current.robot, sim.current.robotState); + sim.current.actExecutor = executor; + + addLog('ACT Executor initialized', 'info'); + } + }, [hasArm, addLog]); useEffect(() => { if (sim.current.dirLight) { @@ -464,7 +522,19 @@ export default function App() { ball.userData = { w: 0.5, d: 0.5 }; group.add(ball); sim.current.walls.push(ball); - sim.current.target = ball; // Use ball as the target since red cube is removed + sim.current.target = ball; + + // Add bucket for ball placement + const bucketGeo = new THREE.CylinderGeometry(1.0, 1.0, 1.6, 32, 1, true); + const bucketMat = new THREE.MeshStandardMaterial({ color: 0xff4444, roughness: 0.8, side: THREE.DoubleSide }); + const bucket = new THREE.Mesh(bucketGeo, bucketMat); + bucket.position.set(2, 0.8, -5); + bucket.castShadow = true; + bucket.receiveShadow = true; + bucket.userData = { w: 1.2, d: 1.2, type: 'bucket' }; + group.add(bucket); + sim.current.walls.push(bucket); + sim.current.bucket = bucket; addLog(`Scene updated: ${sceneType}, Size: ${sceneSize}, Complexity: ${sceneComplexity}`, 'info'); }, [sceneType, sceneSize, sceneComplexity, addLog]); @@ -620,6 +690,9 @@ export default function App() { const updateRobotMovement = useCallback(() => { if (sim.current.isInferencing || sim.current.isTraining) return; + + // Skip if ACT executor is running a task + if (sim.current.actExecutor && sim.current.actExecutor.getState().isExecuting) return; const keys = sim.current.keys; let v = 0; @@ -665,7 +738,7 @@ export default function App() { const nextZ = state.z + Math.cos(state.rotation) * state.velocity; let collided = false; - const robotRadius = 0.9; // Increased to cover camera offset (1.2) and body + const robotRadius = 0.5; // Reduced from 0.9 to allow closer approach to objects const checkAABB = (objX: number, objZ: number, w: number, d: number) => { return (nextX > objX - w / 2 - robotRadius && nextX < objX + w / 2 + robotRadius && @@ -690,6 +763,10 @@ export default function App() { state.z = nextZ; } else { state.velocity = 0; + // Debug log + if (Date.now() % 500 < 50) { + console.log(`[Physics] Collision detected at (${state.x.toFixed(2)}, ${state.z.toFixed(2)}), nextPos=(${nextX.toFixed(2)}, ${nextZ.toFixed(2)})`); + } } state.rotation += state.angularVelocity; @@ -776,6 +853,36 @@ export default function App() { }, []); const updateArm = useCallback(() => { + // Use ACT Executor if available and executing + if (sim.current.actExecutor && sim.current.actExecutor.getState().isExecuting) { + sim.current.actExecutor.update(); + if (sim.current.armController) { + sim.current.armController.update(); + sim.current.armController.advancePhase(addLog); + } + + // Update executor state for UI + const state = sim.current.actExecutor.getState(); + setActExecutorState({ + task: state.task, + subTask: state.subTask, + isComplete: state.isComplete, + isExecuting: state.isExecuting + }); + sim.current.prevIsExecuting = true; + return; + } + + // Sync arm state when executor stops (task completed or manually stopped) + if (sim.current.prevIsExecuting && sim.current.actExecutor && sim.current.armController && sim.current.arm) { + const armCtrlState = sim.current.armController.getState(); + sim.current.arm.state = armCtrlState.task === 'holding' || armCtrlState.task === 'pickUp' ? 'holding' : armCtrlState.task; + sim.current.arm.grabbedObject = armCtrlState.grabbedObject; + sim.current.arm.targetRotations = { ...armCtrlState.targetRotations }; + sim.current.prevIsExecuting = false; + } + + // Fallback to manual arm control if (!sim.current.arm || !hasArm) return; const arm = sim.current.arm; @@ -865,10 +972,10 @@ export default function App() { const animate = useCallback(() => { sim.current.animationFrameId = requestAnimationFrame(animate); + updateArm(); updateRobotMovement(); updatePhysics(); updateOnboardCamera(); - updateArm(); if (sim.current.renderer && sim.current.scene && sim.current.camera) { sim.current.renderer.render(sim.current.scene, sim.current.camera); @@ -919,9 +1026,43 @@ export default function App() { sim.current.target.position.set(0, 0.25, -5); } + // Reset ACT Executor + if (sim.current.actExecutor) { + sim.current.actExecutor.reset(); + setActExecutorState(null); + } + addLog('Robot and target reset', 'warning'); }; + // ACT Task Control Functions + const startACTTask = (task: ACTTask) => { + if (!sim.current.actExecutor) { + addLog('ACT Executor not initialized. Please enable robotic arm first.', 'error'); + return; + } + + if (!hasArm) { + addLog('Please enable robotic arm in robot configuration', 'error'); + return; + } + + // Update walls reference + sim.current.actExecutor.updateWalls(sim.current.walls); + + setActTask(task); + sim.current.actExecutor.startTask(task); + addLog(`Starting ACT task: ${task}`, 'info'); + }; + + const stopACTTask = () => { + if (sim.current.actExecutor) { + sim.current.actExecutor.stop(); + setActTask('idle'); + setActExecutorState(null); + } + }; + const saveDataset = () => { if (trainingMode === 'cloud') { saveCloudDataset(); @@ -1720,12 +1861,19 @@ export default function App() { } try { - if (selectedModel === '跟随网球示例' || selectedModel === '自动避障示例') { + if (selectedModel === '跟随网球示例' || selectedModel === '自动避障示例' || selectedModel === 'findAndPickBall' || selectedModel === 'findBucketAndDrop') { addLog(`Loading ${selectedModel}...`, 'info'); sim.current.isInferencing = true; setIsInferencing(true); - addLog(`Starting Frontend ACT inference with ${selectedModel}...`, 'success'); - runInference(); + + // 启动传统算法任务 + if (selectedModel === 'findAndPickBall' || selectedModel === 'findBucketAndDrop') { + startACTTask(selectedModel as ACTTask); + addLog(`Starting traditional algorithm: ${selectedModel}...`, 'success'); + } else { + addLog(`Starting Frontend ACT inference with ${selectedModel}...`, 'success'); + runInference(); + } } else { addLog(`Loading model ${selectedModel}...`, 'info'); sim.current.model = await tf.loadLayersModel(`indexeddb://${selectedModel}`); @@ -2189,7 +2337,7 @@ export default function App() {
-

5. 执行推理

+

5. 执行推理示例

{trainingMode === 'cloud' ? (
@@ -2217,20 +2365,21 @@ export default function App() { value={selectedModel} onChange={e => setSelectedModel(e.target.value)} > - - {trainedModels.map((model, i) => ( - - ))} + + + + {!hasArm ? ( + <> + + + + ) : ( + <> + + + + )} -
- - -
)} @@ -2245,6 +2394,34 @@ export default function App() { setShowAttention(e.target.checked)} className="rounded bg-slate-800 border-slate-600" />
+ + {actExecutorState?.isExecuting && ( +
+
传统算法任务执行中:
+
+
+ 任务: + {actExecutorState.task} +
+
+ 子任务: + {actExecutorState.subTask} +
+
+ 状态: + + {actExecutorState.isComplete ? '✓ 完成' : '⟳ 执行中...'} + +
+
+ +
+ )} diff --git a/src/sim/actExecutor.ts b/src/sim/actExecutor.ts new file mode 100644 index 00000000..f603425c --- /dev/null +++ b/src/sim/actExecutor.ts @@ -0,0 +1,386 @@ +import * as THREE from 'three'; +import { PathFinder } from './pathfinding'; +import { ArmController } from './armController'; +import { detectObjects, DetectedObject } from './vision'; + +export type ACTTask = 'findAndPickBall' | 'findBucketAndDrop' | 'idle'; + +export interface ACTExecutorState { + task: ACTTask; + subTask: string; + path: THREE.Vector3[]; + currentWaypoint: THREE.Vector3 | null; + isComplete: boolean; + isExecuting: boolean; +} + +export class ACTExecutor { + private state: ACTExecutorState; + private pathFinder: PathFinder; + private armController: ArmController | null; + private scene: THREE.Scene; + private robotGroup: THREE.Group | null; + private robotState: { x: number; z: number; rotation: number; velocity: number; angularVelocity: number }; + private addLog: (msg: string, type?: string) => void; + private walls: THREE.Mesh[]; + + // Configuration + private readonly WAYPOINT_THRESHOLD = 0.3; // 更小的阈值 + private readonly GRAB_DISTANCE = 1.0; // 减小接近距离,让机器人更接近小球 + private readonly DROP_DISTANCE = 1.0; + private readonly SPEED = 0.08; // 降低速度 + private readonly TURN_SPEED = 0.04; // 降低转向速度 + + constructor( + scene: THREE.Scene, + walls: THREE.Mesh[], + addLog: (msg: string, type?: string) => void, + armController?: ArmController + ) { + this.scene = scene; + this.walls = walls; + this.addLog = addLog; + this.armController = armController || null; + this.pathFinder = new PathFinder(); + + this.robotState = { + x: 0, + z: 0, + rotation: Math.PI, + velocity: 0, + angularVelocity: 0 + }; + + this.state = { + task: 'idle', + subTask: '', + path: [], + currentWaypoint: null, + isComplete: false, + isExecuting: false + }; + } + + setRobotRef(robot: THREE.Group, state: any): void { + this.robotGroup = robot; + this.robotState = state; + } + + setArmController(armController: ArmController): void { + this.armController = armController; + } + + updateObstacles(): void { + this.pathFinder.updateObstacles(this.walls); + } + + updateWalls(walls: THREE.Mesh[]): void { + this.walls = walls; + } + + private findBallInScene(): THREE.Mesh | null { + let ballMesh: THREE.Mesh | null = null; + + this.scene.traverse((object) => { + if (object instanceof THREE.Mesh && object.geometry.type === 'SphereGeometry') { + const material = object.material as THREE.MeshStandardMaterial; + const color = material.color; + + // Check if it's a green ball + if (color.g > 0.8 && color.r > 0.6 && color.b < 0.2) { + ballMesh = object; + } + } + }); + + return ballMesh; + } + + startTask(task: ACTTask): void { + if (this.state.isExecuting) { + this.addLog('Already executing a task', 'warning'); + return; + } + + this.state = { + task, + subTask: 'searching', + path: [], + currentWaypoint: null, + isComplete: false, + isExecuting: true + }; + + this.updateObstacles(); + this.addLog(`Starting task: ${task}`, 'info'); + } + + stop(): void { + this.state.isExecuting = false; + this.state.isComplete = false; + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + this.addLog('Task stopped', 'warning'); + } + + reset(): void { + this.state = { + task: 'idle', + subTask: '', + path: [], + currentWaypoint: null, + isComplete: false, + isExecuting: false + }; + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + + if (this.armController) { + this.armController.reset(); + } + } + + update(): void { + if (!this.state.isExecuting || this.state.isComplete) { + return; + } + + const robotPos = new THREE.Vector3(this.robotState.x, 0, this.robotState.z); + + // Detect objects + const vision = detectObjects(this.scene, robotPos, this.robotState.rotation); + + // Debug log every second + if (Date.now() % 1000 < 50) { + this.addLog(`Vision: ball=${vision.ball ? vision.ball.distance.toFixed(2) : 'null'}, bucket=${vision.redBucket ? vision.redBucket.distance.toFixed(2) : 'null'}, vel=${this.robotState.velocity.toFixed(2)}, subTask=${this.state.subTask}`, 'info'); + this.addLog(`RobotPos: (${this.robotState.x.toFixed(2)}, ${this.robotState.z.toFixed(2)}), rot=${(this.robotState.rotation * 180 / Math.PI).toFixed(0)}°`, 'info'); + } + + switch (this.state.task) { + case 'findAndPickBall': + this.executeFindAndPickBall(vision, robotPos); + break; + + case 'findBucketAndDrop': + this.executeFindBucketAndDrop(vision, robotPos); + break; + } + + // Update arm controller if exists + if (this.armController) { + this.armController.update(); + this.armController.advancePhase(this.addLog); + } + } + + private executeFindAndPickBall(vision: any, robotPos: THREE.Vector3): void { + if (!this.armController) { + this.addLog('No arm controller available', 'error'); + this.state.isComplete = true; + this.state.isExecuting = false; + return; + } + + switch (this.state.subTask) { + case 'searching': + if (vision.ball) { + this.state.subTask = 'approaching'; + this.addLog(`✓ Ball detected at distance ${vision.ball.distance.toFixed(2)}m`, 'success'); + } else { + // Search pattern: rotate in place + this.robotState.velocity = 0; + this.robotState.angularVelocity = this.TURN_SPEED; + this.addLog(`🔄 Searching... (rotating)`); + } + break; + + case 'approaching': + if (vision.ball) { + // Use vision distance directly - relaxed condition for easier pickup + if (vision.ball.distance < this.GRAB_DISTANCE + 0.5) { + // Close enough, start picking (distance threshold: 1.7m) + this.state.subTask = 'picking'; + + // Find the ball mesh + const ballMesh = this.findBallInScene(); + if (ballMesh) { + // Start pick sequence (exactly like pressing Q) + this.armController!.startPickBall(ballMesh); + this.addLog(`🎯 In position (dist=${vision.ball.distance.toFixed(2)}m), picking up ball...`, 'info'); + } + } else { + // Navigate towards ball + const targetPos = vision.ball.position.clone(); + const approachPos = targetPos.clone().sub(robotPos).normalize().multiplyScalar(this.GRAB_DISTANCE).add(robotPos); + this.navigateTo(approachPos, robotPos, () => { + this.state.subTask = 'picking'; + + // Find the ball mesh + const ballMesh = this.findBallInScene(); + if (ballMesh) { + // Start pick sequence (exactly like pressing Q) + this.armController!.startPickBall(ballMesh); + this.addLog('🎯 Reached target position, picking up ball...', 'info'); + } + }); + } + } else { + this.state.subTask = 'searching'; + } + break; + + case 'picking': + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + + if (this.armController.isAtTarget() && this.armController.getState().phase >= 2) { + this.state.subTask = 'completed'; + this.state.isComplete = true; + this.state.isExecuting = false; + this.addLog('✅ Task completed: Ball picked up', 'success'); + } + break; + + case 'completed': + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + break; + } + } + + private executeFindBucketAndDrop(vision: any, robotPos: THREE.Vector3): void { + if (!this.armController) { + this.addLog('No arm controller available', 'error'); + this.state.isComplete = true; + this.state.isExecuting = false; + return; + } + + if (!this.armController.hasObject()) { + this.addLog('No object to drop, please pick up ball first', 'warning'); + this.state.isComplete = true; + this.state.isExecuting = false; + return; + } + + switch (this.state.subTask) { + case 'searching': + if (vision.redBucket) { + this.state.subTask = 'approaching'; + this.addLog(`Red bucket detected at distance ${vision.redBucket.distance.toFixed(2)}m`, 'info'); + } else { + // Search pattern + this.robotState.velocity = 0; + this.robotState.angularVelocity = this.TURN_SPEED; + } + break; + + case 'approaching': + if (vision.redBucket) { + // Use vision distance directly: when distance to bucket < 2.0, start dropping + if (vision.redBucket.distance < 2.2) { + this.state.subTask = 'dropping'; + this.armController!.startDropInBucket(); + this.addLog('In position, dropping ball...', 'info'); + } else { + // Move towards bucket using simple navigation + const targetPos = vision.redBucket.position.clone(); + const robotPos = new THREE.Vector3(this.robotState.x, 0, this.robotState.z); + this.navigateTo(targetPos, robotPos, () => { + // This callback may not be reached due to collision, + // but we check vision.redBucket.distance < 2.0 above + this.state.subTask = 'dropping'; + this.armController!.startDropInBucket(); + this.addLog('In position, dropping ball...', 'info'); + }); + } + } else { + this.state.subTask = 'searching'; + } + break; + + case 'dropping': + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + + if (this.armController.isAtTarget() && this.armController.getState().phase >= 2) { + this.state.subTask = 'completed'; + this.state.isComplete = true; + this.state.isExecuting = false; + this.addLog('Task completed: Ball dropped in bucket', 'success'); + } + break; + + case 'completed': + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + break; + } + } + + private navigateTo( + targetPos: THREE.Vector3, + robotPos: THREE.Vector3, + onArrive: () => void + ): void { + const distance = robotPos.distanceTo(targetPos); + + if (distance < this.WAYPOINT_THRESHOLD) { + this.robotState.velocity = 0; + this.robotState.angularVelocity = 0; + onArrive(); + return; + } + + // Calculate direction to target + const direction = targetPos.clone().sub(robotPos).normalize(); + + // Calculate desired angle to face the target + const desiredAngle = Math.atan2(direction.x, direction.z); + + // Get current rotation (normalized to [0, 2PI]) + let currentRotation = this.robotState.rotation % (2 * Math.PI); + if (currentRotation < 0) currentRotation += 2 * Math.PI; + + // Calculate angle difference (shortest path) + let angleDiff = desiredAngle - currentRotation; + + // Normalize to [-PI, PI] + while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; + while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; + + // Relaxed angle threshold: allow movement with larger angle error + const ANGLE_THRESHOLD = 0.4; // 放宽到 0.4 弧度(约 23 度) + + // Turn towards target + if (Math.abs(angleDiff) > ANGLE_THRESHOLD) { + // Large angle difference: stop and turn + this.robotState.velocity = 0; + this.robotState.angularVelocity = angleDiff > 0 ? this.TURN_SPEED : -this.TURN_SPEED; + if (Date.now() % 500 < 50) { + this.addLog(`🧭 Turning: angleDiff=${angleDiff.toFixed(2)}rad (${(angleDiff * 180 / Math.PI).toFixed(0)}°)`); + } + } else { + // Small angle difference: move forward with minor correction + this.robotState.velocity = this.SPEED; + // Apply proportional correction while moving + this.robotState.angularVelocity = angleDiff * 0.1; // 温和的角度修正 + if (Date.now() % 500 < 50) { + this.addLog(`⬆️ Moving: dist=${distance.toFixed(2)}, angleDiff=${angleDiff.toFixed(2)}rad`); + } + } + } + + getState(): ACTExecutorState { + return { ...this.state }; + } + + // For data collection: get current action + getCurrentAction(): number[] { + // Return [velocity, angularVelocity] normalized + return [ + this.robotState.velocity / this.SPEED, + this.robotState.angularVelocity / this.TURN_SPEED + ]; + } +} diff --git a/src/sim/armController.ts b/src/sim/armController.ts new file mode 100644 index 00000000..b6ffd66a --- /dev/null +++ b/src/sim/armController.ts @@ -0,0 +1,224 @@ +import * as THREE from 'three'; + +export type ArmTask = 'idle' | 'picking_down' | 'picking_up' | 'holding' | 'dropping_down' | 'dropping_up' | 'pickBall' | 'dropInBucket' | 'pickUp' | 'dropDown'; + +export interface ArmControllerState { + task: ArmTask; + phase: number; + targetRotations: { + lowerArm: number; + elbow: number; + wrist: number; + }; + grabbedObject: THREE.Mesh | null; +} + +export class ArmController { + private state: ArmControllerState; + private armGroup: THREE.Group; + private lowerArm: THREE.Group; + private elbow: THREE.Group; + private wrist: THREE.Group; + private gripper: THREE.Group; + private worldGroup: THREE.Group; + private speed: number; + + constructor( + armGroup: THREE.Group, + lowerArm: THREE.Group, + elbow: THREE.Group, + wrist: THREE.Group, + gripper: THREE.Group, + worldGroup: THREE.Group, + speed: number = 0.1 + ) { + this.armGroup = armGroup; + this.lowerArm = lowerArm; + this.elbow = elbow; + this.wrist = wrist; + this.gripper = gripper; + this.worldGroup = worldGroup; + this.speed = speed; + + this.state = { + task: 'idle', + phase: 0, + targetRotations: { + lowerArm: Math.PI / 4, + elbow: Math.PI / 2.5, + wrist: -Math.PI / 6 + }, + grabbedObject: null + }; + } + + update(): void { + // Smoothly interpolate to target rotations (same as manual control: 0.1) + this.lowerArm.rotation.x += (this.state.targetRotations.lowerArm - this.lowerArm.rotation.x) * 0.1; + this.elbow.rotation.x += (this.state.targetRotations.elbow - this.elbow.rotation.x) * 0.1; + this.wrist.rotation.x += (this.state.targetRotations.wrist - this.wrist.rotation.x) * 0.1; + + // Note: Do NOT modify grabbedObject position here! + // - During picking_down: object stays at its world position until gripper reaches it + // - After added to gripper: Three.js automatically handles relative positioning + // Setting position.set(0, 0.35, 0) here would cause the ball to jump to world origin + } + + isAtTarget(): boolean { + return ( + Math.abs(this.lowerArm.rotation.x - this.state.targetRotations.lowerArm) < 0.05 && + Math.abs(this.elbow.rotation.x - this.state.targetRotations.elbow) < 0.05 && + Math.abs(this.wrist.rotation.x - this.state.targetRotations.wrist) < 0.05 + ); + } + + startPickBall(ballMesh: THREE.Mesh): void { + if (this.state.task !== 'idle') return; + + // Exactly like pressing Q: set grabbedObject FIRST (but don't add to gripper yet) + this.state.grabbedObject = ballMesh; + this.state.task = 'picking_down'; + this.state.phase = 0; + + // Target rotations exactly like manual Q press + this.state.targetRotations = { + lowerArm: Math.PI / 2.2, + elbow: Math.PI / 4, + wrist: Math.PI / 4 + }; + } + + startDropInBucket(): void { + if (this.state.task !== 'holding' && this.state.task !== 'pickUp') return; + + this.state.task = 'dropInBucket'; + this.state.phase = 0; + this.state.targetRotations = { + lowerArm: Math.PI / 2.2, + elbow: Math.PI / 4, + wrist: Math.PI / 4 + }; + } + + grabObject(object: THREE.Mesh): void { + this.state.grabbedObject = object; + this.gripper.add(object); + object.position.set(0, 0.35, 0); + } + + hasObject(): boolean { + return this.state.grabbedObject !== null; + } + + releaseObject(): void { + if (!this.state.grabbedObject) return; + + const worldPos = new THREE.Vector3(); + this.state.grabbedObject.getWorldPosition(worldPos); + + this.worldGroup.add(this.state.grabbedObject); + this.state.grabbedObject.position.copy(worldPos); + this.state.grabbedObject.position.y = 0.25; + this.state.grabbedObject = null; + } + + advancePhase(addLog: (msg: string, type?: string) => void): void { + if (!this.isAtTarget()) return; + + // Debug log + console.log('[ArmController] advancePhase called, task:', this.state.task, 'phase:', this.state.phase); + + // Exactly like manual Q/E press state machine + if (this.state.task === 'picking_down') { + // Reached down position, add object to gripper (exactly like line 902-903 in App.tsx) + if (this.state.grabbedObject) { + this.gripper.add(this.state.grabbedObject); + this.state.grabbedObject.position.set(0, 0.35, 0); + } + + // Lift up + this.state.task = 'picking_up'; + this.state.phase++; + this.state.targetRotations = { + lowerArm: -Math.PI / 6, + elbow: Math.PI / 1.5, + wrist: -Math.PI / 4 + }; + } else if (this.state.task === 'picking_up') { + // Reached up position, task complete + this.state.task = 'holding'; + this.state.phase++; + addLog('Ball picked up', 'success'); + } else if (this.state.task === 'dropInBucket') { + console.log('[ArmController] dropInBucket: releasing object'); + // Reached first down position (like pressing E: dropping_down) + // Release object (exactly like manual E press) + if (this.state.grabbedObject) { + const worldPos = new THREE.Vector3(); + this.state.grabbedObject.getWorldPosition(worldPos); + + this.worldGroup.add(this.state.grabbedObject); + this.state.grabbedObject.position.copy(worldPos); + // Drop to ground level + this.state.grabbedObject.position.y = 0.25; + this.state.grabbedObject = null; + addLog('Object released', 'success'); + console.log('[ArmController] Object released successfully'); + } else { + console.log('[ArmController] No object to release!'); + } + + // Lift up (like dropping_up) + this.state.task = 'dropping_up'; + this.state.phase++; + this.state.targetRotations = { + lowerArm: Math.PI / 4, + elbow: Math.PI / 2.5, + wrist: -Math.PI / 6 + }; + } else if (this.state.task === 'dropping_down') { + // Reached down position for dropping, release object (exactly like manual E press) + if (this.state.grabbedObject) { + const worldPos = new THREE.Vector3(); + this.state.grabbedObject.getWorldPosition(worldPos); + + this.worldGroup.add(this.state.grabbedObject); + this.state.grabbedObject.position.copy(worldPos); + // Drop to ground level + this.state.grabbedObject.position.y = 0.25; + this.state.grabbedObject = null; + } + + // Lift up + this.state.task = 'dropping_up'; + this.state.phase++; + this.state.targetRotations = { + lowerArm: Math.PI / 4, + elbow: Math.PI / 2.5, + wrist: -Math.PI / 6 + }; + } else if (this.state.task === 'dropping_up') { + // Reached up position, task complete + this.state.task = 'idle'; + this.state.phase++; + addLog('Ball dropped', 'success'); + } + } + + reset(): void { + this.state = { + task: 'idle', + phase: 0, + targetRotations: { + lowerArm: Math.PI / 4, + elbow: Math.PI / 2.5, + wrist: -Math.PI / 6 + }, + grabbedObject: null + }; + } + + getState(): ArmControllerState { + return { ...this.state }; + } +} diff --git a/src/sim/environment.ts b/src/sim/environment.ts index 6d71deeb..4131173a 100644 --- a/src/sim/environment.ts +++ b/src/sim/environment.ts @@ -93,5 +93,16 @@ export function updateEnvironment( group.add(ball); walls.push(ball); + // Add bucket for ball placement + const bucketGeo = new THREE.CylinderGeometry(1.0, 1.2, 1.6, 32); + const bucketMat = new THREE.MeshStandardMaterial({ color: 0xff4444, roughness: 0.8 }); + const bucket = new THREE.Mesh(bucketGeo, bucketMat); + bucket.position.set(-halfSize/2 + 3.5, 0.8, halfSize/2 - 1.5); + bucket.castShadow = true; + bucket.receiveShadow = true; + bucket.userData = { w: 1.2, d: 1.2, type: 'bucket' }; + group.add(bucket); + walls.push(bucket); + return { walls, group, target: null }; } diff --git a/src/sim/pathfinding.ts b/src/sim/pathfinding.ts new file mode 100644 index 00000000..8fa961a5 --- /dev/null +++ b/src/sim/pathfinding.ts @@ -0,0 +1,227 @@ +import * as THREE from 'three'; + +export interface PathNode { + x: number; + z: number; + gCost: number; + hCost: number; + fCost: number; + parent: PathNode | null; + walkable: boolean; +} + +export interface PathfindingConfig { + gridSize: number; + cellSize: number; + maxIterations: number; +} + +const defaultConfig: PathfindingConfig = { + gridSize: 40, + cellSize: 0.5, + maxIterations: 1000 +}; + +export class PathFinder { + private grid: PathNode[][]; + private obstacles: THREE.Vector3[] = []; + private config: PathfindingConfig; + + constructor(config: Partial = {}) { + this.config = { ...defaultConfig, ...config }; + this.grid = this.createGrid(); + } + + private createGrid(): PathNode[][] { + const grid: PathNode[][] = []; + const halfSize = (this.config.gridSize * this.config.cellSize) / 2; + + for (let x = 0; x < this.config.gridSize; x++) { + grid[x] = []; + for (let z = 0; z < this.config.gridSize; z++) { + grid[x][z] = { + x: x * this.config.cellSize - halfSize, + z: z * this.config.cellSize - halfSize, + gCost: 0, + hCost: 0, + fCost: 0, + parent: null, + walkable: true + }; + } + } + + return grid; + } + + updateObstacles(walls: THREE.Mesh[], robotRadius: number = 0.5): void { + // Reset grid + for (let x = 0; x < this.config.gridSize; x++) { + for (let z = 0; z < this.config.gridSize; z++) { + this.grid[x][z].walkable = true; + } + } + + // Mark obstacles + walls.forEach(wall => { + const w = wall.userData.w || 1; + const d = wall.userData.d || 1; + const halfW = w / 2 + robotRadius; + const halfD = d / 2 + robotRadius; + + for (let x = 0; x < this.config.gridSize; x++) { + for (let z = 0; z < this.config.gridSize; z++) { + const node = this.grid[x][z]; + if (node.x > wall.position.x - halfW && + node.x < wall.position.x + halfW && + node.z > wall.position.z - halfD && + node.z < wall.position.z + halfD) { + node.walkable = false; + } + } + } + }); + } + + findPath(start: THREE.Vector3, end: THREE.Vector3): THREE.Vector3[] { + const startNode = this.getNodeFromWorldPosition(start); + const endNode = this.getNodeFromWorldPosition(end); + + if (!startNode || !endNode || !startNode.walkable || !endNode.walkable) { + return []; + } + + const openSet: PathNode[] = []; + const closedSet = new Set(); + + openSet.push(startNode); + + let iterations = 0; + + while (openSet.length > 0 && iterations < this.config.maxIterations) { + iterations++; + + // Get node with lowest fCost + let currentNode = openSet.reduce((min, node) => + node.fCost < min.fCost ? node : min + ); + + // Remove from open set + const index = openSet.indexOf(currentNode); + openSet.splice(index, 1); + closedSet.add(currentNode); + + // Reached destination + if (currentNode === endNode) { + return this.retracePath(currentNode); + } + + // Check neighbors + const neighbors = this.getNeighbors(currentNode); + + for (const neighbor of neighbors) { + if (!neighbor.walkable || closedSet.has(neighbor)) { + continue; + } + + const newGCost = currentNode.gCost + this.getDistance(currentNode, neighbor); + + if (newGCost < neighbor.gCost || !openSet.includes(neighbor)) { + neighbor.gCost = newGCost; + neighbor.hCost = this.getDistance(neighbor, endNode); + neighbor.fCost = neighbor.gCost + neighbor.hCost; + neighbor.parent = currentNode; + + if (!openSet.includes(neighbor)) { + openSet.push(neighbor); + } + } + } + } + + // No path found + return []; + } + + private getNodeFromWorldPosition(position: THREE.Vector3): PathNode | null { + const halfSize = (this.config.gridSize * this.config.cellSize) / 2; + + const x = Math.floor((position.x + halfSize) / this.config.cellSize); + const z = Math.floor((position.z + halfSize) / this.config.cellSize); + + if (x >= 0 && x < this.config.gridSize && z >= 0 && z < this.config.gridSize) { + return this.grid[x][z]; + } + + return null; + } + + private getNeighbors(node: PathNode): PathNode[] { + const neighbors: PathNode[] = []; + const halfSize = this.config.gridSize; + + const indices = [ + [1, 0], [-1, 0], [0, 1], [0, -1], // Cardinal directions + [1, 1], [1, -1], [-1, 1], [-1, -1] // Diagonals + ]; + + const nodeX = Math.floor((node.x + halfSize * this.config.cellSize) / this.config.cellSize); + const nodeZ = Math.floor((node.z + halfSize * this.config.cellSize) / this.config.cellSize); + + for (const [dx, dz] of indices) { + const newX = nodeX + dx; + const newZ = nodeZ + dz; + + if (newX >= 0 && newX < this.config.gridSize && newZ >= 0 && newZ < this.config.gridSize) { + neighbors.push(this.grid[newX][newZ]); + } + } + + return neighbors; + } + + private getDistance(a: PathNode, b: PathNode): number { + const distX = Math.abs(a.x - b.x); + const distZ = Math.abs(a.z - b.z); + + if (distX > distZ) { + return 14 * distZ + 10 * (distX - distZ); // Diagonal distance + } + return 14 * distX + 10 * (distZ - distX); + } + + private retracePath(endNode: PathNode): THREE.Vector3[] { + const path: THREE.Vector3[] = []; + let currentNode: PathNode | null = endNode; + + while (currentNode) { + path.push(new THREE.Vector3(currentNode.x, 0, currentNode.z)); + currentNode = currentNode.parent; + } + + path.reverse(); + return path; + } + + // Simple direct path for open spaces + findDirectPath(start: THREE.Vector3, end: THREE.Vector3): THREE.Vector3[] { + return [start, end]; + } + + // Get next waypoint from current position + getNextWaypoint(path: THREE.Vector3[], currentPosition: THREE.Vector3, threshold: number = 0.5): THREE.Vector3 | null { + if (path.length <= 1) return null; + + // Find the first waypoint that's not yet reached + for (let i = 1; i < path.length; i++) { + const waypoint = path[i]; + const distance = currentPosition.distanceTo(waypoint); + + if (distance > threshold) { + return waypoint; + } + } + + return null; + } +} diff --git a/src/sim/vision.ts b/src/sim/vision.ts new file mode 100644 index 00000000..f510de6c --- /dev/null +++ b/src/sim/vision.ts @@ -0,0 +1,91 @@ +import * as THREE from 'three'; + +export interface DetectedObject { + type: 'ball' | 'redBucket'; + position: THREE.Vector3; + distance: number; + angle: number; + confidence: number; +} + +export interface VisionResult { + ball: DetectedObject | null; + redBucket: DetectedObject | null; +} + +export function detectObjects( + scene: THREE.Scene, + robotPosition: THREE.Vector3, + robotRotation: number +): VisionResult { + const result: VisionResult = { + ball: null, + redBucket: null + }; + + // Detect ball (green sphere) + scene.traverse((object) => { + if (object instanceof THREE.Mesh && object.geometry.type === 'SphereGeometry') { + const material = object.material as THREE.MeshStandardMaterial; + const color = material.color; + + // Check if it's a green ball (approximately #ccff00) + // R: 0.8, G: 1.0, B: 0.0 + if (color.g > 0.8 && color.r > 0.6 && color.b < 0.2) { + const worldPos = new THREE.Vector3(); + object.getWorldPosition(worldPos); + + const relativePos = worldPos.clone().sub(robotPosition); + const distance = relativePos.length(); + + result.ball = { + type: 'ball', + position: worldPos, + distance, + angle: 0, + confidence: 1.0 + }; + } + } + + // Detect red bucket (red cylinder) + if (object instanceof THREE.Mesh && object.geometry.type === 'CylinderGeometry') { + const material = object.material as THREE.MeshStandardMaterial; + const color = material.color; + + // Check if it's a red bucket (approximately #ff4444) + // R: 1.0, G: 0.267, B: 0.267 + if (color.r > 0.8 && color.g < 0.6 && color.b < 0.6) { + const worldPos = new THREE.Vector3(); + object.getWorldPosition(worldPos); + + const relativePos = worldPos.clone().sub(robotPosition); + const distance = relativePos.length(); + + result.redBucket = { + type: 'redBucket', + position: worldPos, + distance, + angle: 0, + confidence: 1.0 + }; + } + } + }); + + return result; +} + +export function getClosestObject( + objects: DetectedObject[], + maxDistance: number = 10 +): DetectedObject | null { + if (objects.length === 0) return null; + + const validObjects = objects.filter(obj => obj.distance <= maxDistance); + if (validObjects.length === 0) return null; + + return validObjects.reduce((closest, current) => + current.distance < closest.distance ? current : closest + ); +}