diff --git "a/docs-self/\345\260\217\350\275\246\350\267\237\350\270\252\346\212\223\345\217\226\344\273\273\345\212\241\345\256\236\347\216\260\350\256\276\350\256\241.md" "b/docs-self/\345\260\217\350\275\246\350\267\237\350\270\252\346\212\223\345\217\226\344\273\273\345\212\241\345\256\236\347\216\260\350\256\276\350\256\241.md" new file mode 100644 index 00000000..17094006 --- /dev/null +++ "b/docs-self/\345\260\217\350\275\246\350\267\237\350\270\252\346\212\223\345\217\226\344\273\273\345\212\241\345\256\236\347\216\260\350\256\276\350\256\241.md" @@ -0,0 +1,233 @@ +# 小车跟踪抓取任务实现设计 + +## 1. 任务描述 + +当前 ACT 尚未实现可用的推理,需要基于现有数据集结构实现正确的 ACT 训练和推理。 + +任务分为三个阶段: + +| 阶段 | 目标 | 状态 | +|------|------|------| +| 基础 | 小车跟踪小球 | 已实现(管线已修通,待充分训练验证) | +| 进阶 | 小车跟踪并停下再抓取小球 | 已实现(规则衔接,待端到端验证) | +| 进阶 | 小车跟踪并抓取小球后跟踪红桶并投放 | 未实现 | + +## 2. 现状分析:为什么 ACT 推理不可用 + +排查发现三个阻塞问题,任意一个都会导致推理输出无意义: + +### 2.1 动作格式不匹配(致命) + +录制、训练、推理三个环节对"动作"的定义互相矛盾: + +``` +录制: action = [1, 0, 0, 0, 0] ← 5维 one-hot,按键编码 +训练: label = (action[0], action[1]) ← 取前两维作为 (v, w) 标签 +推理: output → (v, w) 连续值 ← 期望连续速度 +``` + +实际效果: + +``` +按 W 前进 → action = [1, 0, 0, 0, 0] → 训练标签 = (1, 0) ← 速度恒为 1,不真实 +按 A 左转 → action = [0, 0, 1, 0, 0] → 训练标签 = (0, 0) ← 角速度丢失 +停止不动 → action = [0, 0, 0, 0, 1] → 训练标签 = (0, 0) +``` + +`action[1]` 在 one-hot 中是"后退标志位",不是角速度。模型学到的标签是错的。 + +### 2.2 特征版本不兼容 + +早期模型(V1)使用 14 维状态向量,当前版本(V2)使用 16 维。两者不是简单的多/少几个维度,特征语义不同: + +``` +V1 (14维): index 2 = rotation(原始弧度值,范围 [-π, π]) +V2 (16维): index 2 = sin(rotation),index 3 = cos(rotation)(范围 [-1, 1]) +``` + +加载旧模型时如果用新版特征喂入,index 2 的值从弧度变成了 sin 值,语义完全错乱。 + +### 2.3 反应控制器("老师")缺陷 + +反应控制器是采集训练数据的"老师",模型通过模仿它来学习。老师本身有三个问题: + +| 问题 | 原因 | +|------|------| +| 球在右边时仍向左转一整圈 | `targetTurn = turnSpeed` 写死为正值,不看球的方位 | +| 跟踪速度慢、响应迟钝 | 速度上限和转向增益都偏保守 | +| 到达目标后不停车 | 原逻辑是到达后把球传送走,没有停车动作 | + +但注意:推理效果差并非仅仅是"老师"的问题。其他因素同样在影响: +- 低通滤波器(alpha = 0.5)衰减模型输出的峰值 +- 时间集成(Temporal Ensembling)对重叠 chunk 取加权平均,进一步平滑 +- 训练数据量不足(< 200 帧无法覆盖状态空间) +- 模型容量(浅层 CNN + 2 层 Dense 的表达能力有限) + +## 3. 设计方案 + +### 3.1 修复动作格式:discrete 5-dim → continuous 2-dim + +录制时直接记录小车当前的运动学量: + +```typescript +const action = [ + sim.current.robotState.velocity, // 线速度 + sim.current.robotState.angularVelocity // 角速度 +]; +``` + +`[velocity, angularVelocity]` 两个分量足以完整描述平面差速驱动的运动。这样录制、训练、推理三个环节对动作的理解完全一致。 + +### 3.2 多版本特征适配器 + +不能靠截断或补零来兼容不同版本,必须维护独立的特征映射表。 + +**设计选择**:版本路由表——根据模型输入维度自动选择对应的特征映射。 + +``` +FeatureFactory // 纯几何计算 +├── calculateRelativeTarget() // 距离、角度差、可见性 +└── checkOcclusion() // 射线步进遮挡检测 + +V1_FEATURE_MAP (14维) // [x, z, rotation, speed, targetDist, collision, ...] +V2_FEATURE_MAP (16维) // [x, z, sin(θ), cos(θ), speed, targetX, targetZ, ...] +VERSION_MAP = { 14: V1, 16: V2 } // 路由表 + +FeatureAdapter.getFeatures(dim, simData) // 入口:按维度路由 → 选映射表 → 组装向量 +``` + +这样做的好处: +- 新增版本只需追加一张映射表 + 一行路由,不碰已有逻辑 +- 映射表注释本身就是特征文档 +- 数据采集和推理都通过同一入口 `getFeatures()`,消除不一致风险 + +模型加载时自动检测维度: +```typescript +const stateDim = model.inputs[1].shape[1]; // inputs[0]=图像, inputs[1]=状态 +``` + +### 3.3 修复反应控制器 + +| 修改 | 改动 | +|------|------| +| 转向方向 | `angleDiff > 0 ? turnSpeed : -turnSpeed`,根据球的实际方位决定 | +| 跟踪速度 | 速度上限 × 2.0,转向增益 1.0 → 1.5,角度大时减速比 0.5 → 0.3 | +| 减速曲线 | `(distance - 1.0) * 0.8`,更陡的线性减速 | + +### 3.4 停车 + 机械臂衔接 + +导航和抓取是两个性质不同的子任务: +- **导航**:连续控制,状态空间大,适合模型学习 +- **抓取**:确定性操作,只需在固定距离触发预设动作序列 + +因此采用混合架构——模型负责导航,规则负责停车和抓取触发: + +``` +距离 > 1.5 → 模型推理正常控制 +1.0 < 距离 < 1.5 → 停车,清空动作缓冲,继续轮询 +距离 < 1.0 → 终止推理,若机械臂空闲则自动触发抓取 +``` + +不用训练来学习"何时停车"和"何时抓取": +- 停车是简单的距离阈值判断 +- 抓取是确定性状态机 +- 规则衔接可以保证可靠性,不受模型精度影响 + +## 4. 实现说明 + +### 代码位置 + +| 文件 | 职责 | +|------|------| +| `src/services/featureService.ts` | 特征工厂 + 版本适配器(新增) | +| `src/services/actService.ts` | 模型定义、训练、推理(未改动) | +| `src/App.tsx` | 数据采集、推理管线、反应控制器 | + +### 提交记录 + +``` +5d5e095 feat: add multi-version feature adapter for model dimension compatibility +43d7ee1 fix: switch action recording from discrete 5-dim to continuous 2-dim +8f70fce feat: add model dimension auto-detection and FeatureAdapter in inference +a5ea294 feat: improve reactive controller and add stop-at-target with auto arm grab +``` + +## 5. 已实现的效果 + +- 加载 14 维旧模型时自动使用 V1 特征映射,加载 16 维新模型时使用 V2 映射 +- 采集的训练数据中动作标签为真实的 `(v, w)` 连续值,与推理一致 +- 反应控制器能正确跟踪任意方向的目标,可作为采集高质量训练数据的"老师" +- 小车到达目标后停车,自动衔接机械臂抓取 + +## 6. 待实现 / 遗留问题 + +### 6.1 阶段三:跟踪红桶并投放(未实现) + +完整管线应为:跟踪小球 → 停车 → 抓取 → 跟踪红桶 → 停车 → 投放。当前只实现了前三步。后续需要: +- 抓取完成后切换跟踪目标为红桶 +- 红桶到达后触发投放动作 +- 状态机需扩展为多阶段:`navigate_ball → grab → navigate_barrel → drop` + +### 6.2 训练数据质量与数量 + +当前验证用的数据量(约 16 个 episode,每个约 12 帧)远不足以让模型学到有效策略。建议: +- 每个 episode 至少 50-100 帧(5-10 秒连续操作) +- 至少 50 个 episode 以覆盖不同起始位置和目标方位 +- 使用修复后的反应控制器自动采集,而非手动操作 + +### 6.3 低通滤波器过度平滑 + +推理时的低通滤波 `alpha = 0.5` 会将模型输出衰减约一半: +```typescript +robotState.velocity = robotState.velocity * 0.5 + targetV * 0.5; +``` +模型预测 `v = 0.3` 时,实际施加的速度从 0 开始需要多步才能接近 0.3,导致响应滞后。可考虑提高 alpha 到 0.7-0.8。 + +### 6.4 时间集成权重衰减 + +当前 `exp(-0.5 * i)` 使第 5 步的权重衰减到 `e^(-2.5) ≈ 0.08`。数据不足时模型输出本身就弱,加权平均会进一步削弱有效信号。 + +### 6.5 模型容量 + +当前前端模型(2 层 CNN + 2 层 Dense,约 20 万参数)对端到端视觉控制任务可能偏小。充足数据下仍欠拟合时可考虑增加 CNN 深度或 Dense 宽度,但需权衡浏览器端推理延迟。 + +### 6.6 状态向量归一化 + +状态向量各维度量纲差异较大(位置 `[-6, 6]`、角度 `[-1, 1]`、速度 `[0, 0.5]`、布尔 `{0, 1}`),未做归一化可能影响训练稳定性。优先级低于上述问题。 + +### 6.7 文档同步 + +`FRONTEND_TRAINING_DOCS.md` 中的数据定义(14 维状态、5 维动作)已过时,需更新为 16 维状态、2 维动作。 + +## 附录:前期改动记录 + +以下为本次重构之前的迭代改动,按时间顺序排列。 + +### A.1 添加小球运动类型和配置 + +- 新增 `BallMotionMode` 类型(fixed / random / mixed) +- 新增 `BallMotionConfig` 接口,支持配置运动半径、速度、随机强度 + +### A.2 改进神经网络架构 + +- 状态输入维度从 14 调整为 12 +- 状态处理层从 32 单元增加到 64 单元 +- 新增一层 Dense(64) 用于更好的特征提取 + +### A.3 实现小球运动控制系统 + +- 新增 `updateBallMotion()` 函数,支持三种运动模式: + - 固定轨迹(圆周运动) + - 随机运动(随机游走) + - 混合运动(圆周 + 随机扰动) +- 新增小球速度计算用于状态表示 +- 状态向量扩展为 12 维:机器人位姿(sin/cos 编码)、速度、小球位置和速度、相对角度和距离、碰撞标志 +- 录制和推理路径同步更新 +- 新增小球运动控制 UI 面板 + +### A.4 增强球跟踪与反应控制基础 + +- 状态向量从 12 维扩展到 16 维,新增小球加速度、可见性、遮挡特征 +- 新增小球手动控制模式 +- 改进 UI 布局,建立动态场景边界系统 +- 编写反应控制逻辑技术文档 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..c7d9afd5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { CloudModel, CloudDataset, CloudTrainingStatus, SimulationState, RobotCo import { createScene, createCamera, createRenderer, createLights, createFloor } from './sim/scene'; import { createRobot } from './sim/robot'; import { updateEnvironment } from './sim/environment'; +import { FeatureAdapter } from './services/featureService'; import { tr } from 'motion/react-client'; export default function App() { @@ -36,6 +37,10 @@ export default function App() { }); const [speed, setSpeed] = useState(() => Number(localStorage.getItem('speed')) || 0.1); const [turnSpeed, setTurnSpeed] = useState(() => Number(localStorage.getItem('turnSpeed')) || 0.05); + const [ballMotionMode, setBallMotionMode] = useState<'fixed' | 'random' | 'mixed'>(() => (localStorage.getItem('ballMotionMode') as any) || 'fixed'); + const [ballRadius, setBallRadius] = useState(() => Number(localStorage.getItem('ballRadius')) || 5); + const [ballSpeed, setBallSpeed] = useState(() => Number(localStorage.getItem('ballSpeed')) || 0.3); + const [ballRandomIntensity, setBallRandomIntensity] = useState(() => Number(localStorage.getItem('ballRandomIntensity')) || 0.5); const [logs, setLogs] = useState<{ message: string, type: string, time: string }[]>([ { message: 'System initialized. Waiting for commands...', type: 'info', time: new Date().toLocaleTimeString() } ]); @@ -51,7 +56,11 @@ export default function App() { localStorage.setItem('lightPos', JSON.stringify(lightPos)); localStorage.setItem('speed', speed.toString()); localStorage.setItem('turnSpeed', turnSpeed.toString()); - }, [sceneType, sceneSize, sceneComplexity, hasArm, lightPos, speed, turnSpeed]); + localStorage.setItem('ballMotionMode', ballMotionMode); + localStorage.setItem('ballRadius', ballRadius.toString()); + localStorage.setItem('ballSpeed', ballSpeed.toString()); + localStorage.setItem('ballRandomIntensity', ballRandomIntensity.toString()); + }, [sceneType, sceneSize, sceneComplexity, hasArm, lightPos, speed, turnSpeed, ballMotionMode, ballRadius, ballSpeed, ballRandomIntensity]); // Cloud Training State const [trainingMode, setTrainingMode] = useState<'frontend' | 'cloud'>('frontend'); @@ -103,7 +112,19 @@ export default function App() { stuckCounter: 0, actionBuffer: [] as any[], lastInferenceLogTime: 0, - lastManualLogTime: 0 + lastManualLogTime: 0, + ballMotionTime: 0, + ballPrevX: 0, + ballPrevZ: 0, + ballVelX: 0, + ballVelZ: 0, + ballPrevVelX: 0, + ballPrevVelZ: 0, + ballAccelX: 0, + ballAccelZ: 0, + isVisible: false, + isBlocked: false, + sceneBoundary: 10 // 默认值(halfSize) }); const isAtBottom = useRef(true); @@ -225,8 +246,11 @@ export default function App() { if (intersects.length > 0) { const point = intersects[0].point; - sim.current.target.position.x = point.x; - sim.current.target.position.z = point.z; + const boundary = sim.current.sceneBoundary || 9; + + // 限制拖拽范围,不让小球穿墙 + sim.current.target.position.x = Math.max(-boundary, Math.min(boundary, point.x)); + sim.current.target.position.z = Math.max(-boundary, Math.min(boundary, point.z)); } }; @@ -306,6 +330,7 @@ export default function App() { if (sceneSize === 'large') sizeVal = 30; const halfSize = sizeVal / 2; + sim.current.sceneBoundary = halfSize - 1.0; // 留出 1m 的安全距离 const createTexture = (type: string) => { const canvas = document.createElement('canvas'); @@ -525,47 +550,19 @@ export default function App() { // Use small canvas for Base64 - much faster const imageBase64 = smallCanvasRef.current?.toDataURL('image/jpeg', 0.7).split(',')[1]; - // Match reference state structure: - // state: [x, y, angle, speed, ballDist, isColliding, ...zeros] (14 dims) - - const x = sim.current.robotState.x; - const z = sim.current.robotState.z; // treating z as y in 2D - const angle = sim.current.robotState.rotation; - const speed = sim.current.robotState.velocity; - - const targetDist = Math.hypot(sim.current.target.position.x - x, sim.current.target.position.z - z); - - const state = new Array(14).fill(0); - state[0] = x; - state[1] = z; - state[2] = angle; - state[3] = speed; - state[4] = targetDist; - state[5] = sim.current.isColliding ? 1.0 : 0.0; // Explicitly tell model we are colliding - - const envState = new Array(7).fill(0); - envState[0] = x; - envState[1] = z; - envState[2] = angle; - envState[3] = speed; - envState[4] = sim.current.isColliding ? 1 : 0; - envState[5] = 0; // Forward distance placeholder - envState[6] = targetDist; + // 使用特征适配器获取 16 维状态 (当前系统契约) + const state = FeatureAdapter.getFeatures(16, { + ...sim.current, + robotState: sim.current.robotState + }); - // Action: [up, down, left, right, stop] (one-hot or similar) - // Reference uses commandToActionVec which returns 5-dim vector. - // Our current action is [velocity, angularVelocity]. - // We need to map continuous controls to discrete if we want to match reference exactly, - // OR ensure the backend handles continuous. - // The reference metadata says "action_space: discrete_5". - // So we should map our keys to discrete actions. + const envState = [...state]; - let action = [0, 0, 0, 0, 1]; // Default stop - const keys = sim.current.keys; - if (keys['w'] || keys['arrowup']) action = [1, 0, 0, 0, 0]; - else if (keys['s'] || keys['arrowdown']) action = [0, 1, 0, 0, 0]; - else if (keys['a'] || keys['arrowleft']) action = [0, 0, 1, 0, 0]; - else if (keys['d'] || keys['arrowright']) action = [0, 0, 0, 1, 0]; + // 录制实际的连续速度/角速度(兼容手动控制和规则控制器) + const action = [ + sim.current.robotState.velocity, // 实际线速度 + sim.current.robotState.angularVelocity // 实际角速度 + ]; const frame = { state: state, @@ -725,6 +722,71 @@ export default function App() { if (velocityRef.current) velocityRef.current.textContent = (Math.abs(state.velocity) * 10).toFixed(1); }, []); + const updateBallMotion = useCallback(() => { + if (!sim.current.target) return; + + // 手动模式下不自动更新小球位置 + if (ballMotionMode === 'manual') return; + + const dt = 0.016; // ~60fps + sim.current.ballMotionTime += dt; + + const prevX = sim.current.target.position.x; + const prevZ = sim.current.target.position.z; + + let newX = prevX; + let newZ = prevZ; + + const boundary = sim.current.sceneBoundary || 9; + + if (ballMotionMode === 'fixed') { + // 固定航迹:圆形运动 + // 半径不能超过边界 + const effectiveRadius = Math.min(ballRadius, boundary); + const angle = sim.current.ballMotionTime * ballSpeed; + newX = effectiveRadius * Math.cos(angle); + newZ = effectiveRadius * Math.sin(angle); + } else if (ballMotionMode === 'random') { + // 随机运动:随机游走 + const randomX = (Math.random() - 0.5) * ballRandomIntensity * ballSpeed; + const randomZ = (Math.random() - 0.5) * ballRandomIntensity * ballSpeed; + newX = prevX + randomX; + newZ = prevZ + randomZ; + } else if (ballMotionMode === 'mixed') { + // 混合运动:固定航迹 + 随机扰动 + const effectiveRadius = Math.min(ballRadius, boundary); + const angle = sim.current.ballMotionTime * ballSpeed; + const baseX = effectiveRadius * Math.cos(angle); + const baseZ = effectiveRadius * Math.sin(angle); + + const randomX = (Math.random() - 0.5) * ballRandomIntensity * 0.5; + const randomZ = (Math.random() - 0.5) * ballRandomIntensity * 0.5; + + newX = baseX + randomX; + newZ = baseZ + randomZ; + } + + // 统一边界限制(所有模式) + newX = Math.max(-boundary, Math.min(boundary, newX)); + newZ = Math.max(-boundary, Math.min(boundary, newZ)); + + // 计算小球速度 + sim.current.ballVelX = (newX - prevX) / dt; + sim.current.ballVelZ = (newZ - prevZ) / dt; + + // 计算小球加速度 + sim.current.ballAccelX = (sim.current.ballVelX - sim.current.ballPrevVelX) / dt; + sim.current.ballAccelZ = (sim.current.ballVelZ - sim.current.ballPrevVelZ) / dt; + + // 保存前一帧的速度 + sim.current.ballPrevVelX = sim.current.ballVelX; + sim.current.ballPrevVelZ = sim.current.ballVelZ; + + // 更新小球位置 + sim.current.target.position.x = newX; + sim.current.target.position.z = newZ; + }, [ballMotionMode, ballRadius, ballSpeed, ballRandomIntensity]); + const updateOnboardCamera = useCallback(() => { const { onboardCamera, onboardRenderTarget, renderer, scene, robotState } = sim.current; const canvas = cameraCanvasRef.current; @@ -867,13 +929,14 @@ export default function App() { updateRobotMovement(); updatePhysics(); + updateBallMotion(); updateOnboardCamera(); updateArm(); if (sim.current.renderer && sim.current.scene && sim.current.camera) { sim.current.renderer.render(sim.current.scene, sim.current.camera); } - }, [updateRobotMovement, updatePhysics, updateOnboardCamera, updateArm]); + }, [updateRobotMovement, updatePhysics, updateBallMotion, updateOnboardCamera, updateArm]); useEffect(() => { sim.current.animationFrameId = requestAnimationFrame(animate); @@ -933,7 +996,7 @@ export default function App() { const dataset = { metadata: { robot_type: "diff_drive", - action_space: "discrete_5", + action_space: "continuous_2", fps: 10, created: new Date().toISOString() }, @@ -996,7 +1059,11 @@ export default function App() { const loadedModels = Object.keys(models) .filter(key => key.startsWith('indexeddb://')) .map(key => ({ name: key.replace('indexeddb://', '') })); - + + if (loadedModels.length > 0) { + addLog(`Detected ${loadedModels.length} trained models. Multi-version adapter enabled.`, 'info'); + } + setTrainedModels([ { name: '跟随网球示例' }, { name: '自动避障示例' }, @@ -1005,7 +1072,7 @@ export default function App() { } catch (err) { console.error('Failed to load models from IndexedDB', err); } - }, []); + }, [addLog]); const handleExportModels = async () => { const modelsToExport = trainedModels.filter(m => !m.name.endsWith('示例')); @@ -1483,23 +1550,23 @@ export default function App() { targetSpeed = 0; targetTurn = 0; } else if (!isVisible) { - // Ball out of FOV or blocked: rotate in place to search + // Ball out of FOV or blocked: rotate toward the ball's actual direction targetSpeed = 0; - targetTurn = turnSpeed; + // angleDiff 已经包含了方向信息:正=球在左边,负=球在右边 + targetTurn = angleDiff > 0 ? turnSpeed : -turnSpeed; } else { // Ball in FOV: track and approach - // Use a smaller multiplier for smoother turning - targetTurn = angleDiff * 1.0; - + targetTurn = angleDiff * 1.5; + // Cap the turn speed to prevent violent swings - targetTurn = Math.max(-turnSpeed * 1.5, Math.min(turnSpeed * 1.5, targetTurn)); - - // Smooth speed approach - targetSpeed = Math.min(speed, (distance - 1.5) * 0.5); - + targetTurn = Math.max(-turnSpeed * 2.0, Math.min(turnSpeed * 2.0, targetTurn)); + + // Faster speed approach with smooth deceleration near target + targetSpeed = Math.min(speed * 2.0, (distance - 1.0) * 0.8); + // If the angle is too large, slow down to turn if (Math.abs(angleDiff) > Math.PI / 6) { - targetSpeed *= 0.5; + targetSpeed *= 0.3; } } } else if (selectedModel === '自动避障示例') { @@ -1612,16 +1679,18 @@ export default function App() { const image = captureImage(); if (!image) return; - const dx = target.position.x - robotState.x; - const dz = target.position.z - robotState.z; - const targetDist = Math.sqrt(dx * dx + dz * dz); + // 根据加载的模型维度,使用适配器提取特征 + const stateDim = (sim.current as any).modelStateDim || 16; + const state = FeatureAdapter.getFeatures(stateDim, { + ...sim.current, + robotState: sim.current.robotState + }); - const state = new Array(14).fill(0); - state[0] = robotState.x; - state[1] = robotState.z; - state[2] = robotState.rotation; - state[3] = robotState.velocity; - state[4] = targetDist; + // 直接计算距离,不依赖状态向量中的索引(不同版本索引不同) + const actualDist = Math.sqrt( + Math.pow(target.position.x - robotState.x, 2) + + Math.pow(target.position.z - robotState.z, 2) + ); const prediction = actService.predict(model, image, state); @@ -1690,15 +1759,31 @@ export default function App() { return keysChanged ? newActiveKeys : prev; }); - // Check goal - if (targetDist < 1.0) { - addLog('Target reached!', 'success'); - target.position.set( - (Math.random() - 0.5) * 12, - 0.4, - (Math.random() - 0.5) * 12 - ); + // Check goal: stop-at-target + auto arm grab + if (actualDist < 1.5) { + // 接近目标:开始减速 + robotState.velocity = 0; + robotState.angularVelocity = 0; sim.current.actionBuffer = []; + + if (actualDist < 1.0) { + // 到达目标:完全停车,停止推理 + addLog('🎯 Target reached! Car stopped.', 'success'); + sim.current.isInferencing = false; + setIsInferencing(false); + + // 如果有机械臂且处于空闲状态,自动触发抓取 + const hasArm = sim.current.arm && sim.current.arm.state === 'idle'; + if (hasArm) { + addLog('🦾 Auto-triggering arm grab...', 'info'); + sim.current.keys['q'] = true; + } + return; + } + + // 在 1.0-1.5 范围内:已停车,等待更精确的位置调整 + sim.current.inferenceTimeoutId = setTimeout(runInference, 100); + return; } // Match training frequency (10Hz = 100ms) @@ -1728,7 +1813,25 @@ export default function App() { runInference(); } else { addLog(`Loading model ${selectedModel}...`, 'info'); - sim.current.model = await tf.loadLayersModel(`indexeddb://${selectedModel}`); + const model = await tf.loadLayersModel(`indexeddb://${selectedModel}`); + sim.current.model = model; + + // 动态获取模型的输入状态维度 + // ACT 模型有两个输入:[image, state] + // model.inputs[1] 是状态输入层 + try { + const stateDim = model.inputs[1].shape[1]; + (sim.current as any).modelStateDim = stateDim; + if (stateDim === 14) { + addLog(`模型已加载,状态维度: ${stateDim}(V1 旧版特征映射)`, 'warning'); + } else { + addLog(`模型已加载,状态维度: ${stateDim}`, 'success'); + } + } catch (e) { + (sim.current as any).modelStateDim = 16; // 降级默认值用当前版本 + addLog('无法检测模型维度,默认使用 16 维。', 'warning'); + } + sim.current.isInferencing = true; setIsInferencing(true); addLog(`Starting Frontend ACT inference with ${selectedModel}...`, 'success'); @@ -2001,6 +2104,7 @@ export default function App() { {turnSpeed.toFixed(2)} +
{hasArm ? (
+

使用键盘 WASD 或方向键控制

+ +
+
🎾 小球运动控制
+ + + + {(ballMotionMode === 'random' || ballMotionMode === 'mixed') && ( + + )} + {ballMotionMode === 'manual' && ( +
+ 💡 手动模式:用鼠标拖动小球,可以自由创建训练数据 +
+ )} +

使用键盘 WASD 或方向键控制

diff --git a/src/components/SidebarLeft.tsx b/src/components/SidebarLeft.tsx index 05fb7b50..e1fe774a 100644 --- a/src/components/SidebarLeft.tsx +++ b/src/components/SidebarLeft.tsx @@ -59,6 +59,16 @@ interface SidebarLeftProps { isInferencing: boolean; showAttention: boolean; setShowAttention: (show: boolean) => void; + + // Ball Motion Props + ballMotionMode: 'fixed' | 'random' | 'mixed'; + setBallMotionMode: (mode: 'fixed' | 'random' | 'mixed') => void; + ballRadius: number; + setBallRadius: (radius: number) => void; + ballSpeed: number; + setBallSpeed: (speed: number) => void; + ballRandomIntensity: number; + setBallRandomIntensity: (intensity: number) => void; } export const SidebarLeft: React.FC = ({ @@ -70,7 +80,8 @@ export const SidebarLeft: React.FC = ({ speed, setSpeed, turnSpeed, setTurnSpeed, simRef, sendCommand, isRecording, toggleRecording, episodesCount, frameCount, actionCount, saveDataset, startTraining, isTraining, trainingProgress, trainingStatus, - trainedModel, startInference, isInferencing, showAttention, setShowAttention + trainedModel, startInference, isInferencing, showAttention, setShowAttention, + ballMotionMode, setBallMotionMode, ballRadius, setBallRadius, ballSpeed, setBallSpeed, ballRandomIntensity, setBallRandomIntensity }) => { return (