refactor(model): 重构P03为轻量化板式桁架并统一M4×16自攻螺钉连接方案
This commit is contained in:
@@ -8,6 +8,9 @@ out/
|
||||
# 临时调试脚本与截图
|
||||
scratch/
|
||||
|
||||
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
||||
backups/
|
||||
|
||||
# 编辑器 / 系统文件
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
# 姿态估计算法评估与优化 —— 实施计划
|
||||
|
||||
> 交付物(用户已确认):**仅一份中文评估报告**,不改动求解器代码。
|
||||
> 报告落盘位置:`docs/姿态估计算法评估报告.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 摘要(Summary)
|
||||
|
||||
本任务对机械臂的「姿态估计算法」——即 [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) 的 6 轴逆运动学(IK)姿态求解器及其查看器集成 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js)(用户已确认该文件即「姿态估计算法」)——做一次全面评估。
|
||||
|
||||
需要解决的核心问题:**在多个关键位姿(近工作空间边界、折叠/奇异构型、贴近关节限位处)无法达到预定姿态目标;且当目标姿态确实不可达时,返回的「最近位姿」明显劣于肉眼可见的更优姿态。**
|
||||
|
||||
评估已在本阶段(Plan 模式,只读)实测复现并定位到 4 类缺陷(见 §2.3),并额外回答了两个关键设计问题:**现状是「先点位、后角度」的位置优先架构**(§2.4),以及**位置与姿态在远目标上真正不可兼得、必须提供可切换优先级**(§2.4 实测取舍表)。本计划定义报告的**结构、数据来源、可复现实验命令、优化策略与验证口径**。执行阶段只产出一份报告 md,不新增任何脚本文件(实验以 `node -e` 内联执行,命令内嵌报告附录)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析(Current State Analysis)
|
||||
|
||||
### 2.1 代码与数据资产
|
||||
|
||||
| 角色 | 文件 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 姿态求解器(主) | [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) | UMD,`make(cfg)` 返回 `solve/solveTo/solveWarm/solveDyn/closest/closest6/branch/fk/orient/costOf` 等([导出清单 L1355-1360](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1355-L1360)) |
|
||||
| 查看器集成 | [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) | 姿态 UI、目标拖动、`solveTo` 调用([L379](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L379)、[L399](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L399))、结果渲染与徽标判定([renderResult L318-342](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L318-L342)) |
|
||||
| 关节表(几何真值) | [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) | `joints[] = {id, rear, d}`,FK/IK 运动学常量来源 |
|
||||
| 关节限位 | [travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json) | 本报告测量口径:J1/J3/J6 连续(±180),J2∈[-106,9],J4∈[-111,16],J5∈[-95,34](2026-09-29 起该表退为零位扫描参考:滑块改电机行程 ±360°,可行域改相邻件实测区间) |
|
||||
| 既有自检 | [pose_ik_check.js](file:///g:/Robot_v2/机器人手臂/scratch/pose_ik_check.js) | 7 组 node 自检(往返、位置回归、姿态重构、交互口径、兜底、耗时) |
|
||||
| 既有性能基准 | [bench_ik_split.js](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.js) / [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) | 单次求解耗时与「位置/姿态」占比拆分 |
|
||||
| 历史优化实验 | `scratch/ikopt/`(`solver_scanbase.js` 等) | 根枚举/窗口/同族门等过往迭代产物,报告需说明其结论 |
|
||||
|
||||
### 2.2 现有算法骨架(报告需准确描述)
|
||||
|
||||
1. **位置闭式 + 二维冗余扫描**:以 (θ4,θ5) 为冗余参数全域扫描,θ1..θ3 由 [branch() L429-463](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L429-L463) 闭式给出(最多 4 支:θ3=±σ、±(π−σ)),位置天然精确到 1e-13 mm。
|
||||
2. **按行裁剪**:[rowSpans L644-654](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 用探测步 `hp=max(h,2)°` 找每 θ4 行的可行 θ5 段,段内再按 h 精扫。
|
||||
3. **姿态任务**:工具轴指向 = 目标轴,是 (θ4,θ5) 上的 2 方程 2 未知量,解为**孤立点**;[poseTools L486-545](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L486-L545) 提供残差/牛顿原语;[§4.1b 根枚举 L697-761](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L697-L761) 逐行做变号括号 + 牛顿收口;滚转由 [rollClose L549-555](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L549-L555) 闭式补 θ6。
|
||||
4. **接受门**:`POSETOL=2.0°`([L581](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L581))——`scoreBranch` 对 `oerr>POSETOL` 的候选**直接返回 null 丢弃**([L596-598](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L596-L598))。
|
||||
5. **不可达兜底**:[closest6 L1054-1169](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 为 6 维阻尼最小二乘(DLS),160 次确定性拟随机重启 × 160 迭代;[solveTo L1172-1192](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1172-L1192) 在 `solve()` 失败时调用它。
|
||||
6. **实时快路**:[solveWarm L1200-1265](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1200-L1265),沿上一帧解同支续解,失败回落全量。
|
||||
|
||||
### 2.3 已实测缺陷(本阶段只读复现,数据可直接写入报告)
|
||||
|
||||
**缺陷 A — 覆盖漏洞:目标姿态精确可达,求解器却静默返回 ~1–2° 残差**
|
||||
- 全空间随机可达位形,`solve(step=0.25)` 返回 `oerr>0.05°` 的比例:**2.5%(3/120)**,量级 `oerr∈[1.24, 1.95]°`(真值应为 0)。
|
||||
- 其中可明确归因于「覆盖漏洞」(细扫 step=0.05 能到 ≤0.05°、即确有大步长漏解)的部分,小样本估计 **0.3%–1.3%**(300 组样本:1/300;80 组样本:1/80);其余为真·近奇异构型(细扫同样到不了 ≤0.05°,属物理不可达的刀尖构型)。**执行阶段用更大样本(D1/D4)收敛该比例。**
|
||||
- 典型案例 `T=[329.3,253.4,750.0]`:真解 (θ4,θ5)=(-16.45,-51.97),`branch()` 在该点给出 **位置误差 5.7e-14 mm、轴误差 0.000°** 的精确解;但 `solve(step=0.25)` 返回 oerr=1.909°。
|
||||
- **步长敏感(非分辨率问题)**:同一目标 `oerr` 随 `step` 非单调跳变——`0.25°→1.60`、`0.15°→0.045`、`0.10°→1.14`、`0.05°→0.013`;用 `solveTo({warm:真解})` 则得到 **oerr=0.0000**。证明确切解存在,是**根枚举/牛顿收口的完备性缺口**。
|
||||
- 工作区实测:前伸工作区 (20,210,300)±90mm 失败率 **1.7%(1/60)**;查看器默认区 (150,128,260)±60mm 为 **0%(0/60)**——缺口集中在工作空间外围/极端构型。
|
||||
|
||||
**缺陷 B — 不可达兜底 closest6 严重次优(「肉眼可见更优姿态」的直接来源)**
|
||||
以「位置精确解中姿态误差的全局最小值」为真最优(对 (θ4,θ5) 网格 0.05° 枚举 `branch()` 全部在限位内的支),目标姿态在可达姿态基础上外推 20°:
|
||||
|
||||
| 案例 | 目标 TCP | 兜底返回 | 真最优姿态差 | 劣化 |
|
||||
| :--- | :--- | ---: | ---: | ---: |
|
||||
| 1 | (40,455,216) | 位置 0.4mm / 姿态 **74.0°** | **11.04°** | **63.0°** |
|
||||
| 2 | (196,122,136) | 位置 0.5mm / 姿态 **130.0°** | **17.26°** | **112.8°** |
|
||||
| 3 | (-141,-18,1030) | 位置 0.0mm / 姿态 **167.2°** | **89.09°** | **78.1°** |
|
||||
|
||||
**缺陷 C — 姿态不可达时把「位置精确解候选池」整体丢弃,导致兜底被便宜解严格支配**
|
||||
- `T=[20,400,300]`、`R=poseMat(25,95,10)`:位置完全可达(无姿态 `solve` 返回 err=2.5e-14,0.5° 格上有 **11920** 个位置精确解),但姿态目标不可达时 `scoreBranch` 因 `oerr>POSETOL` 把全部候选置 null,`solve()` 返回 `ok=false`、候选池为空,被迫走 [closest6](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 的 6 维随机 DLS。
|
||||
- 同例对照(实测):位置精确解集内最小姿态差 = **121.05°**;`closest6` 返回 **136.7° / 位置 0.0mm**。即 `closest6` 在**位置与姿态两个指标上同时劣于**一个用现有闭式扫描就能廉价得到的解 —— **被严格支配(Pareto-dominated)**。
|
||||
- 后果:`位置精确 + 姿态尽力` 这条最自然的路径没有实现;兜底既不最优、又更慢(159–224ms vs 扫描 ~100ms)。
|
||||
|
||||
**缺陷 D — 性能超出既定预算**
|
||||
- [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) 摘要:开姿态单次 **中位 107ms / p95 242ms / 最大 299ms**(位置基座中位 73.6ms,姿态增量中位 37.2ms 占 33%,escalating 触发率 2.5%)。
|
||||
- 本阶段在工作区实测均耗时 112–157ms;不可达兜底 159–224ms。
|
||||
- 均高于用户设定的 **≤100ms** 目标,且兜底路径(random-restart DLS)是主要浪费点。
|
||||
|
||||
### 2.4 求解范式评估:位置优先 vs 末端姿态反推(用户提出,报告中作为备选简要说明)
|
||||
|
||||
**用户提问**:能否用已指定的末端角度,从末端逐级反推回各关节?
|
||||
|
||||
**几何事实(以 [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) 为准)**:
|
||||
|
||||
| 关节 | rear | d | 类型 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| J1 | (-51,128,610) | (1,0,0) | X 抬举 |
|
||||
| J2 | (18.5,100,490) | (0,1,0) | Y 俯仰 |
|
||||
| J3 | (18.5,128,445) | (0,0,-1) | Z 自转 |
|
||||
| J4 | (18.5,100,332) | (0,**1**,0) | Y 俯仰(肘) |
|
||||
| J5 | (18.5,100,222) | (0,**1**,0) | Y 俯仰(腕) |
|
||||
| J6 | (18.5,128,176) | (0,0,**-1**) | Z 工具自转 |
|
||||
|
||||
- **J4 ∥ J5**(均 +Y,相距 110mm)⇒ 平行轴永不相交 ⇒ **非球形腕**,经典"腕心解耦 + θ6→θ5→θ4 逐级闭式反推"不成立。(J5⊥J6 在 (18.5,128,222) 相交,仅构成 2 轴腕。)
|
||||
- 但存在可用的**降阶结构**:因 J4∥J5,`R4·R5 = R_y(θ4+θ5)`(代码 [L406-411](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L406-L411) 已用 `SAMEAX` 利用该性质),故
|
||||
|
||||
```
|
||||
R = Rx(θ1)·Ry(θ2)·Rz(−θ3)·Ry(θ4+θ5)·Rz(−θ6)
|
||||
```
|
||||
|
||||
⇒ **工具轴只由 (θ1,θ2,θ3,θ4+θ5) 决定,θ6 是纯工具滚转**;"用末端姿态驱动求解"可行,但需按此降阶链求解(3 方程 / 5 参数 → 2 参数族),而非逐级反推。
|
||||
|
||||
**位置与姿态在远目标上"真的不可兼得"(实测,报告须收录)**:
|
||||
|
||||
| 目标 | 优先级 | 位置误差 | 姿态差 |
|
||||
| :--- | :--- | ---: | ---: |
|
||||
| T=(20,400,300), R=poseMat(25,95,10) | 位置优先(保点) | **0.0 mm** | 121.05° |
|
||||
| 同上 | 姿态优先(保角度) | 127.6 mm | **0.22°** |
|
||||
| T=(196,122,136), R=poseMat(25,95,10) | 位置优先 | **0.0 mm** | 116.39° |
|
||||
| 同上 | 姿态优先 | 233.6 mm | **0.45°** |
|
||||
|
||||
⇒ 结论:二者是**真实的取舍**,不存在"都能兼顾"的解;算法必须提供**可切换的优先级**。
|
||||
|
||||
### 2.5 误差来源定位(报告核心结论)
|
||||
|
||||
- **R1 根枚举不完备**:姿态精确解在 (θ4,θ5) 平面是一条一维曲线,只在少数孤立点穿过 `POSETOL` 窄带;当该曲线经过 θ3≈±90° 的**折叠点**(`branch()` 的 4 支两两重合、位置雅可比奇异)、或贴着关节限位/可行段边缘时,变号括号检测失效、`match()` 的「θ1..θ3 最近」同支判据在折叠处换支,牛顿收口失败。
|
||||
- **R2 接受门把「不可达」与「漏解」混为一谈**:`oerr>POSETOL` 一律丢弃 ⇒ 无法区分「确实不可达」与「可达但没找到」,导致 A 类静默近失 + C 类候选池清空。
|
||||
- **R3 兜底目标函数与真实目标不一致**:`closest6` 用固定的 20mm/rad 把姿态折进位置残差做联合 DLS,既不保证「位置优先、姿态最小」,也依赖随机重启初值,落点常是劣质局部极值。
|
||||
- **R4 行裁剪探测步过粗**:`hp=max(h,2)°` 且按单一连续段假设(注释称仅在 5 个目标上验证过),窄于 2° 的可行段会被整段漏掉 ⇒ 整行/整区无候选。
|
||||
|
||||
---
|
||||
|
||||
## 3. 交付物与报告大纲(Proposed Changes)
|
||||
|
||||
**唯一变更**:新建 `docs/姿态估计算法评估报告.md`(中文)。
|
||||
|
||||
报告章节(每章明确数据来源,均为本阶段或执行阶段 `node -e` 实测可得):
|
||||
|
||||
1. **概述与结论摘要**(TL;DR 表格:缺陷 / 影响 / 根因 / 建议 / 预期收益)
|
||||
2. **算法现状描述**——§2.2 的骨架,附关键函数行号链接与数据流图
|
||||
3. **求解范式评估(位置优先 vs 末端姿态反推)**——§2.4:
|
||||
- 确认现状为「先点位、后角度」(位置被强制精确、姿态是带 2° 门的软约束)
|
||||
- 关节轴几何表 + `J4∥J5 ⇒ 非球形腕` 的判定;说明「逐级闭式反推」不成立的**原因**(备选简要说明,约半页,含降阶链 `Rx·Ry·Rz·Ry(θ4+θ5)·Rz` 的结论式)
|
||||
- 位置/姿态**不可兼得**的实测取舍表(§2.4),给出「必须提供可切换优先级」的结论
|
||||
4. **评估方法**——指标定义:
|
||||
- 姿态残差 `oerr`(`rotErrDeg`,度)
|
||||
- 位置残差 `err`(mm)
|
||||
- 覆盖漏洞率 = 「真解 oerr≤0.05° 而解算 oerr>0.05°」占比
|
||||
- 步长敏感度 = 同目标在 step∈{0.05,0.1,0.15,0.25,0.5} 的 oerr 波动
|
||||
- 兜底劣化 = `closest6` 姿态差 −「位置精确解集内最优姿态差」
|
||||
- 单次耗时中位/p95(开/关姿态、可达/兜底)
|
||||
5. **性能分析**——引用 bench_ik_split.json 的耗时构成 + 工作区实测;给出「位置/姿态/兜底」三段占比
|
||||
6. **误差来源定位**——§2.5 的 R1–R4,逐条给出**可复现证据**(目标坐标、真解、实测值)
|
||||
7. **优化策略(设计方案,不在本轮落地)**——按收益/成本排序给出可实施建议:
|
||||
- **S0(架构)双优先级求解范式**:仿 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) 现有姿态模式的 UI 开关,新增「优先级」选择——**位置优先 / 姿态优先 / 折中**;三种模式下共用同一闭式支枚举结果,只切换目标函数与选解规则。给出各模式的数学表述:
|
||||
- 位置优先:`min oerr s.t. ‖fk(q)−T‖=0`(在位置精确解集上最小化姿态误差)
|
||||
- 姿态优先:`min ‖fk(q)−T‖ s.t. oerr=0`(在姿态精确解集上最小化位置误差;利用降阶链参数化)
|
||||
- 折中:`min ‖fk(q)−T‖² + k·oerr²`,k 可调
|
||||
- 预期:位置优先模式把 B 类劣化从 63–113° 压到接近真最优;姿态优先模式提供 oerr≤0.05° 的「保角度」解(代价是位置误差,如实上报)
|
||||
- **S1(根因修复)覆盖率完备化**:把 (θ4,θ5) 上的姿态方程看作一维解曲线,用**预测-校正 continuation 跟踪**(以解析/数值切向推进、跨越折叠点不换支),或等价地做**分支索引对齐的多起点牛顿 + 折叠点/限位角种子**;预期把 A 类覆盖漏洞降到 <0.2%,且不加粗扫密度 ⇒ 耗时基本不变(≤100ms)。
|
||||
- **S2(关键收益)姿态不可达时不再清空候选池**:去掉 `POSETOL` 硬门对候选池的**清空**语义,改为保留并按 `oerr` 排序入池(门只用于「达标」判定,不用于「丢弃」);预期消除 C 类 Pareto 支配,且兜底不再需要。
|
||||
- **S3 兜底加速**:以 S0/S2 的闭式候选为种子替代 160 次随机重启,仅在位置也越界时才退回受限 DLS;预期兜底耗时从 ~160–220ms 降到 ≤100ms。
|
||||
- **S4 可观测性**:UI 明确区分「已到达 / 位置到达·姿态最近 X° / 姿态到达·位置最近 Y mm / 均不可达」四种语义,避免把漏解或取舍结果当成功。
|
||||
- 每条策略给出:改动点(函数+行号)、原理、预期指标、代价与风险、与 ≤100ms 预算的关系。
|
||||
8. **验证方案与验收口径**——见 §5。
|
||||
9. **附录**:可复现实验命令(`node -e` 全文内联,不落脚本文件)+ 基准数据表 + 术语表。
|
||||
|
||||
> 说明:报告只做「评估 + 策略设计」,不含代码修改。若后续要落地 S0–S3,需另起一次实施任务。
|
||||
|
||||
---
|
||||
|
||||
## 4. 假设与决策(Assumptions & Decisions)
|
||||
|
||||
| 项 | 决策 | 依据 |
|
||||
| :--- | :--- | :--- |
|
||||
| 交付物 | 仅评估报告 md,**不改代码** | 用户选择「仅评估报告」 |
|
||||
| 「姿态估计算法」定义 | = `web/arm-ik-solver.js` 的 IK 姿态求解 + `ik-glue.js` 集成 | 全仓检索无 IMU/卡尔曼/姿态估计类算法;仓库内「姿态」相关实现仅此一处 |
|
||||
| 性能预算 | 以 **≤100ms** 为优化目标约束(报告据此评估各策略) | 用户选择「保持 ≤100ms」 |
|
||||
| 失败场景 | A(可达却漏解)与 B/C(不可达最近位姿)**都覆盖** | 用户选择「两者都做」 |
|
||||
| 实验脚本 | 不新增文件,全部 `node -e` 内联并在报告附录给出原文 | 遵守「仅评估报告」与「不新建文件」 |
|
||||
| **求解优先级** | 报告须设计**双优先级机制(位置优先 / 姿态优先 / 折中),界面可切换**;不可兼得时两种解都给出 | 用户选择「两者都做、界面可切换」 |
|
||||
| **末端姿态反推** | 报告仅作**备选简要说明**(说明非球形腕故不能逐级闭式反推 + 降阶链结论),不展开公式级设计 | 用户选择「只作备选简要说明」 |
|
||||
| 关节表/限位 | 以 `web/mesh_arm.json` + `docs/travel.json` 为准,tcp=[18.5,128,93] | 与 `pose_ik_check.js`、`bench_ik_split.js` 一致 |
|
||||
| 真最优基准 | 「位置精确解集内姿态误差最小」的网格枚举(0.05° 分辨率,θ6 另行闭式最优) | 独立于被测求解器,避免自证 |
|
||||
| 随机性 | 固定 LCG 种子的确定性采样,报告数据可复现 | 与求解器内部自检策略一致 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证方案(报告第 8 章内容)
|
||||
|
||||
**基准数据集**(执行阶段固定种子生成,规模与命令写入附录):
|
||||
- D1 全空间随机可达位形 ≥200 组(覆盖极端/边界构型)
|
||||
- D2 前伸工作区 (20,210,300)±90mm ≥60 组、查看器默认区 (150,128,260)±60mm ≥60 组
|
||||
- D3 姿态外推集(外推 3/8/20°)≥30 组 → 测兜底劣化
|
||||
- D4 退化构型集(θ3≈±90°、临近限位)≥30 组 → 测覆盖漏洞
|
||||
- D5 越界目标集 → 测兜底耗时与质量
|
||||
- D6 **双优先级取舍集**:位置与姿态不可兼得的远目标 ≥20 组 → 测「位置优先」与「姿态优先」两解各自的误差与真实性
|
||||
|
||||
**指标与验收口径(现状基线 → 目标)**:
|
||||
|
||||
| 指标 | 现状实测 | 目标 |
|
||||
| :--- | :--- | :--- |
|
||||
| D1/D4 覆盖漏洞率(真解≤0.05° 却返回>0.05°) | 2.5% | ≤0.2% |
|
||||
| D1 最大静默姿态残差 | 1.95° | ≤0.05° |
|
||||
| 步长敏感度(同目标 oerr 波动) | 0→1.86° | ≤0.05° |
|
||||
| D3/D5 兜底相对真最优劣化(姿态优先口径) | 63–113° | ≤2° |
|
||||
| D6 位置优先解:位置误差 / 姿态差 | 0.0mm / 121.05° | 位置=0;姿态=真最优(劣化 ≤0.5°) |
|
||||
| D6 姿态优先解:姿态差 / 位置误差 | 0.22° / 127.6mm | 姿态 ≤0.05°;位置如实上报 |
|
||||
| 开姿态单次耗时 p95 | 242ms | ≤100ms |
|
||||
| 兜底耗时 | 159–224ms | ≤100ms |
|
||||
| 姿态残差 P95(可达集) | ≤2.05°(接受限) | ≤0.05° |
|
||||
|
||||
**沿用既有自检**:`node scratch/pose_ik_check.js` 的 7 项(含往返一致性、位置回归、姿态重构、roll→θ6 口径、6D 兜底、耗时对比)作为回归门,报告中给出其现状通过情况与目标。
|
||||
|
||||
**报告自检**:报告内每张数据表须与附录命令重跑结果一致;所有结论必须能由附录命令复现。
|
||||
|
||||
---
|
||||
|
||||
## 6. 执行步骤(待批准后按序执行)
|
||||
|
||||
1. 复跑并固化基线数据(D1–D6 + bench + 自检),生成报告用数据表。
|
||||
2. 撰写 `docs/姿态估计算法评估报告.md` 正文(§3 大纲 1–9 章)。
|
||||
3. 内嵌可复现 `node -e` 命令与原始输出,形成附录。
|
||||
4. 复核报告数据与结论一致性(重跑附录命令抽查),交付。
|
||||
@@ -0,0 +1,258 @@
|
||||
# 机器人手臂3D查看器 · 性能优化方案
|
||||
|
||||
目标文件:`G:\Robot_v2\机器人手臂\机器人手臂3D查看器.html`(15.31 MB / 35986 行)
|
||||
用户要求:先做完整备份(带版本标识)→ 优化 → 验证功能完好 → 记录优化项与性能数据。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状分析(实测,非猜测)
|
||||
|
||||
### 1.1 文件结构
|
||||
|
||||
| 位置 | 内容 |
|
||||
| --- | --- |
|
||||
| L1~L420 | HTML + CSS + 界面骨架(左栏/IK 控制条/舞台/页脚/BOM 抽屉) |
|
||||
| L421~L428 | `<script>` three.js r159 内联(652 KB,UMD build,无 CDN) |
|
||||
| L429~L33251 | `<script>`:`DATA`(13.99 MB 网格 JSON)+ `SHEETS`(52 KB SVG)+ `TRAVEL` + `COLLIDE` + 查看器主逻辑(`const TL = {}` 起 738 行) |
|
||||
| L33252~L35022 | `<script>`:`arm-ik-solver.js` 逆运动学求解器(1770 行,自带 `window.ArmIK`) |
|
||||
| L35023~L35984 | `<script>`:`ik-glue.js` 界面胶水层(961 行,Worker 化求解、预置自检、姿态拖拽) |
|
||||
|
||||
四个内联脚本的**索引被 `chkSig()` 指纹逻辑依赖**(`document.scripts[2]` = 求解器源码),因此本次优化**不得增删 `<script>` 标签、不得改动求解器脚本块**,否则预置自检指纹失效(会误报"预置结果已失效")。
|
||||
|
||||
### 1.2 数据规模(node 实测)
|
||||
|
||||
- 零件 92 件(打印件 9 + 电机 6 + 螺钉 72 + 包络 1 + 经纬线 1)
|
||||
- 顶点 354,697 / 三角面 340,178 / 线段 10,296
|
||||
- **单件最大顶点 47,483(< 65536 → 索引可全部用 Uint16)**
|
||||
- `JSON.parse` 69 ms;按查看器原样建几何(含 `computeVertexNormals` / 包围盒球)37 ms
|
||||
- 全场景射线拾取(92 件、34 万面):**1.3 ms/次**(60 次均值),200 次连测 2.1 ms/次
|
||||
- `scene.updateMatrixWorld(true)` 全量强制刷新:**0.004 ms/次**(≈110 个对象)
|
||||
|
||||
### 1.3 已定位的性能问题
|
||||
|
||||
| # | 问题 | 证据 | 影响 |
|
||||
| --- | --- | --- | --- |
|
||||
| **P1** | **rAF 循环无条件每帧 `resize() + updCam() + rend.render()`**;`needRender` 被 `requestRender()` 置位后**全文件没有任何读取点**(`Grep needRender` 仅 3 处:声明、置位、注释) | L33207~L33215 | 空闲时仍 60 fps 满帧渲染 34 万面 ×92 draw call;`preserveDrawingBuffer:true` 使每帧多一次缓冲拷贝;持续占用 GPU/功耗 |
|
||||
| **P2** | `resize()` 每帧读 `stage.clientWidth/clientHeight` | 同上 | 每帧强制布局(forced reflow),与 P1 同源 |
|
||||
| **P3** | `pick()` 每次 pointermove 先 `meshes.filter(m => m.visible)` 再全量射线求交 | L32668~L32673、L32642~L32652 | 每帧≥1 次 1.3~2.1 ms 求交 + 每帧一个 92 元素临时数组,鼠标扫过模型时持续吃 CPU |
|
||||
| **P4** | `applyJoint()` 每次新建 2 个 `Vector3` + 3 个 `Matrix4`,并线性查 `JO.find` | L32506~L32518 | 每写一个关节角 = 6 次调用 = 30 个临时对象;动画/实时追踪每帧一批 |
|
||||
| **P5** | 几何索引一律 `Uint32Array` | L32531、L32546 | 索引显存 340,178×3×4 B = 4.08 MB(Uint16 只需 2.04 MB) |
|
||||
| **P6** | 点选/ESC 时 `for (const tr of tb.children) tr.style.background = ''` 全表刷 style | L32727、L32736、L32745 | 每次点击写 ~92 行样式 |
|
||||
| **P7** | Worker 的 Blob URL 不回收(`URL.createObjectURL` 后无 `revokeObjectURL`) | L35510 附近 | 常驻一份 ~86 KB Blob;构造失败路径同样泄漏 |
|
||||
|
||||
### 1.4 经实测**否决**的优化(不做,写进报告避免后人重复劳动)
|
||||
|
||||
- **全量矩阵刷新**:0.004 ms/次 → 改"6 级关节链更新"只有 13× 相对提速但绝对量 4 μs,风险不值。
|
||||
- **`updXform()`(92 件)/ `updStat()`(92 件 reduce+filter)**:量级为微秒,爆炸滑块拖动时也远低于 1 ms。
|
||||
- **材质合并 / 几何合并**:只有 92 个 draw call,合并会破坏"零件树勾选显隐 + 点选 + 单独高亮"这些核心功能。
|
||||
- **分片/延迟建场景**:初始化总计约 106 ms(69 解析 + 37 建几何),分片只会增加复杂度,不降低总时间。
|
||||
- **实时追踪时的结果面板 DOM 节流**:`renderResult()` 含"场景图残差复算"与"备选构型"两块**验证语义**内容(用户此前专门建立了指纹/预置自检体系),节流会削弱验证可观测性,保持原样。
|
||||
|
||||
### 1.5 关键发现:源文件与成品已漂移(影响改动范围)
|
||||
|
||||
`src/build_viewer.py` 由 `src/viewer_tpl.html` + `web/ik-glue.js` + `web/arm-ik-solver.js` + `web/mesh_arm.json` 拼装成品,但:
|
||||
|
||||
- 成品 L35023~L35984 的 glue 区域(961 行)**比 `web/ik-glue.js`(776 行)新**:Worker 化求解、`PRECOMPUTED_CHK` 预置自检、`refreshJointLabelsLazy` 节流、`±360°` 滑块等**只存在于成品内**。
|
||||
- 成品主逻辑区与 `src/viewer_tpl.html` 的差异只有:① `±360°` 滑块范围(成品新)② `/*__SCREWS__*/` 占位符展开(预期)。
|
||||
|
||||
**已与用户确认的处理方式**:本次**直接改成品 HTML**;同时把"能对得上"的性能补丁**同步回 `src/viewer_tpl.html`**(源文件落后部分保持原状)。在源文件完成反向同步之前**不要重跑 `build_viewer.py`**(会把成品回退到旧功能)——此警告写入报告。
|
||||
|
||||
---
|
||||
|
||||
## 二、改动清单
|
||||
|
||||
### 改动 0:备份(先做,且必须先验证备份完整)
|
||||
|
||||
1. 新建目录 `backups/`,复制原件为
|
||||
`backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html`
|
||||
(v1 = 优化前版本号;2151 取自原件最后写入时间 2026-09-29 21:51)
|
||||
2. 记录 `SHA-256` 与字节数(`Get-FileHash`),校验备份与原件一致后再动原件。
|
||||
3. `.gitignore` 追加 `backups/`,避免 15 MB 备份污染 git 状态。
|
||||
|
||||
### 改动 1(P1+P2,**核心**):按需渲染
|
||||
|
||||
文件:成品 `loop()`(L33207~L33215)与 `src/viewer_tpl.html` 对应段。
|
||||
|
||||
```js
|
||||
/* 改动前 */
|
||||
function loop() {
|
||||
resize();
|
||||
if (cv.style.display !== 'none') { updCam(); rend.render(scene, cam); }
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
/* 改动后 */
|
||||
/* 按需渲染:needRender 由 requestRender() 置位(所有交互/状态变化都已调用它);
|
||||
旧版置位后无人读取,故恒以 60 fps 空转。装配图页(画布隐藏)不清标志,
|
||||
切回三维页由 tab 处理器 resize()+requestRender() 补渲染。 */
|
||||
function loop() {
|
||||
if (needRender && cv.style.display !== 'none') {
|
||||
needRender = false;
|
||||
resize();
|
||||
updCam();
|
||||
rend.render(scene, cam);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
- **保留 `preserveDrawingBuffer:true`**:按需渲染后画布不再重绘,必须靠它保住最后一帧图像。
|
||||
- **保留 rAF 循环**:`animTo()` 动画、实时追踪、拖拽都依赖后续 rAF 里的渲染。
|
||||
- 已核对全部渲染状态变更点均有 `requestRender()`(主逻辑 36 处;glue 的 `refreshTarget()`/`setJoints()`/`highlightSpec()`/`setView()` 内部均调用),无遗漏。
|
||||
- `resize()` 仍在渲染前调用一次(进入渲染路径时同步画布尺寸),窗口尺寸变化由 `ResizeObserver → resize() → requestRender()` 覆盖。
|
||||
|
||||
### 改动 2(P3):拾取零分配 + 悬浮限流
|
||||
|
||||
文件:成品 L32642~L32682 与 `src/viewer_tpl.html` 对应段。
|
||||
|
||||
- `pick()`:`meshes.filter(...)` → 复用模块级数组
|
||||
```js
|
||||
const pickList = [], ndcPick = new THREE.Vector2();
|
||||
function pick(e) {
|
||||
const r = cv.getBoundingClientRect();
|
||||
ndcPick.set(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1);
|
||||
ray.setFromCamera(ndcPick, cam);
|
||||
pickList.length = 0;
|
||||
for (const m of meshes) if (m.visible) pickList.push(m); // 语义与 filter 完全一致
|
||||
const hit = ray.intersectObjects(pickList, false)[0];
|
||||
return hit ? hit.object : null;
|
||||
}
|
||||
```
|
||||
(three r159 的 Raycaster **不检查 `object.visible`**,已在源码中确认,故可见性过滤必须保留。)
|
||||
- `schedHover()`:rAF 节流 → **60 ms 时间配额 + 位置去重**(实测 1.3 ms/次求交,60 次/s → ~16 次/s):
|
||||
```js
|
||||
let hoverPending = false, lastHoverEv = null, hoverT = -1e9, hoverX = -1, hoverY = -1;
|
||||
const HOVER_MS = 60;
|
||||
function schedHover(e) {
|
||||
if (e.clientX === hoverX && e.clientY === hoverY) return; // 未移动不重算
|
||||
hoverX = e.clientX; hoverY = e.clientY; lastHoverEv = e;
|
||||
if (hoverPending) return;
|
||||
hoverPending = true;
|
||||
setTimeout(() => {
|
||||
hoverPending = false;
|
||||
if (!lastHoverEv || drag) return;
|
||||
hoverT = performance.now(); hover(lastHoverEv);
|
||||
}, Math.max(0, HOVER_MS - (performance.now() - hoverT)));
|
||||
}
|
||||
```
|
||||
尾部仍会补一次(指针停住后取最新坐标),tooltip 行为不变;点击/右键拾取仍**立即**执行,不受限流影响。
|
||||
|
||||
### 改动 3(P4):`applyJoint()` 去分配
|
||||
|
||||
```js
|
||||
const JP = {}, JD = {};
|
||||
for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); }
|
||||
const _mT = new THREE.Matrix4(), _mR = new THREE.Matrix4(), _mTi = new THREE.Matrix4();
|
||||
function applyJoint(id) {
|
||||
const P = JP[id], D = JD[id]; if (!P) return;
|
||||
const a = (ang[id] || 0) * Math.PI / 180;
|
||||
JG[id].matrix.copy(_mT.makeTranslation(P.x, P.y, P.z)
|
||||
.multiply(_mR.makeRotationAxis(D, a))
|
||||
.multiply(_mTi.makeTranslation(-P.x, -P.y, -P.z)));
|
||||
JG[id].matrixWorldNeedsUpdate = true;
|
||||
requestRender();
|
||||
}
|
||||
```
|
||||
数学与原式逐位相同(`T(P)·R(D,θ)·T(−P)`),仅把每次新建的 5 个对象换成预计算 + 3 个共享矩阵。
|
||||
|
||||
### 改动 4(P5):索引降位(Uint16)
|
||||
|
||||
```js
|
||||
const sIdx = p.v.length > 65535 ? new Uint32Array(p.s) : new Uint16Array(p.s); // line 分支
|
||||
const idx = p.v.length > 65535 ? new Uint32Array(p.t.length*3) : new Uint16Array(p.t.length*3);
|
||||
```
|
||||
全部 92 件顶点数 ≤ 47,483(实测),Uint16 合法;索引内存 4.08 MB → 2.04 MB,上传带宽减半。保留 >65535 分支作安全兜底。
|
||||
|
||||
### 改动 5(P6):BOM 高亮行只记一行
|
||||
|
||||
新增 `let bomHlRow = null; function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } }`,替换 L32727、L32736、L32745 三处全表 `tr.style.background = ''` 循环;设置高亮处同步记录 `bomHlRow = tr`。可见结果完全一致。
|
||||
|
||||
### 改动 6(P7):Worker Blob URL 回收
|
||||
|
||||
```js
|
||||
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
|
||||
let w; try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
|
||||
```
|
||||
(URL 只在 Worker 构造时被读取,构造后即可回收;失败路径也不再泄漏。此改动**仅成品有**——`web/ik-glue.js` 仍是旧同步求解版,无 Worker。)
|
||||
|
||||
### 改动 7:源文件同步(用户已确认)
|
||||
|
||||
- `src/viewer_tpl.html`:逐条应用改动 1~5(该文件对应代码段存在,锚点为唯一代码串;`±360°` 差异行不与改动区重叠)。
|
||||
- `web/ik-glue.js`:核对后逐条应用(预计仅"渲染相关"部分不适用——拾取/循环/几何都在模板里;Worker 相关不存在)。
|
||||
- 同步后**不重跑 `build_viewer.py`**(会回退旧功能),源文件只作为"补丁已留存"。
|
||||
|
||||
### 改动 8:文档
|
||||
|
||||
新建 `docs/查看器性能优化报告.md`(与 `docs/姿态估计算法评估报告.md` 同目录同风格,中文):优化项逐条说明(问题/证据/改法/收益)、前后实测数据表、功能验证清单与结论、已否决项、备份路径 + SHA-256、**"源文件漂移未修,勿重跑构建"**警告。
|
||||
|
||||
---
|
||||
|
||||
## 三、假定与决策
|
||||
|
||||
1. 只改代码区,**不动** `DATA` / `SHEETS` / `TRAVEL` / `COLLIDE` 数据行与求解器脚本(指纹依赖)。
|
||||
2. 不新增/不拆分 `<script>` 标签(`document.scripts[2]` 索引依赖)。
|
||||
3. 不改 `antialias` / `setPixelRatio(min(dpr,1.5))` / 灯光 / 材质参数——属画质取舍,且实测 draw call 与面数不构成瓶颈。
|
||||
4. 测量用的"插桩副本"只放在 `scratch/perf/`(已被 `.gitignore` 忽略),成品文件保持干净。
|
||||
5. 备份放项目内 `backups/`(用户已选),并写入 `.gitignore`。
|
||||
|
||||
---
|
||||
|
||||
## 四、验证方案
|
||||
|
||||
### 4.1 静态校验
|
||||
1. `backups/` 内备份与原件 `SHA-256` 一致(改前)。
|
||||
2. 抽取改后文件的 4 个 `<script>` 块到 `scratch/perf/`,逐个 `node --check` 通过。
|
||||
3. 求解器脚本块 + `TRAVEL` / `COLLIDE` 数据行改前/改后**字节一致**(保证指纹)。
|
||||
4. 全文 diff 只落在预期的 8 处锚点内。
|
||||
|
||||
### 4.2 功能验证(浏览器,改前/改后各跑一遍对照)
|
||||
|
||||
用 `scratch/perf/serve.py`(`SimpleHTTPRequestHandler`,把 `/v.html` 映射到项目根的真实成品)起 `http://localhost:8123/v.html`,用浏览器自动化逐项核对并截图:
|
||||
|
||||
| 项 | 判据 |
|
||||
| --- | --- |
|
||||
| 首屏加载 | 无 JS 报错;三维模型正常显示 |
|
||||
| 关节滑块 | 拖动后手臂实时跟随、角度标签同步 |
|
||||
| 爆炸滑块 / 装配视图按钮 | 模型散开与复位正确 |
|
||||
| 线框 / 电机半透 / 螺钉 / 可达包络 / 复位 | 逐项开关生效 |
|
||||
| 装配图页 ↔ 三维页切换 | SVG 显示、切回后三维画布重新渲染(验证"隐藏分支不清标志") |
|
||||
| 运动模拟页 | 进页即求解并动画到位;IK 表格/徽标/备选构型正常 |
|
||||
| 姿态控制 | 开关、取当前、垂直向下、箭头拖拽可用 |
|
||||
| 自检面板 | 显示"指纹 … 匹配";FK/IK/场景图残差仍为 1e-13 mm 量级(验证改动的数学等价性) |
|
||||
| BOM 螺丝行点击 | 高亮 + 相机对焦 + HUD 文案(昨天的功能) |
|
||||
| 悬浮 tooltip / 右键隐藏 / H 键 / ESC | 行为一致 |
|
||||
| 相机预设 / 拖拽旋转 / 滚轮缩放 | 行为一致 |
|
||||
| URL 参数(`?view=motion&tgt=…&j1=…&hide=…`) | 脚本化视图仍生效 |
|
||||
|
||||
### 4.3 性能前后对比(插桩副本,同一浏览器同一机器)
|
||||
|
||||
生成 `scratch/perf/measure_before.html`(备份 + 尾部追加插桩脚本)与 `measure_after.html`(改后 + 同一插桩脚本),插桩内容:包装 `rend.render` 计次与计 CPU 时长、`rend.info`、`performance.memory`、导航时序、交互探针(滑块 input、`IK.setView('motion')`+`IK.run()` 动画、20 次 pointermove 拾取计时),结果输出到页面固定 div + `document.title` 以便截图/读取。
|
||||
|
||||
预期指标:
|
||||
|
||||
| 指标 | 改前(预期) | 改后(预期) |
|
||||
| --- | --- | --- |
|
||||
| 空闲 5 s 渲染次数 | ≈300 | ≈0~1 |
|
||||
| 单次 render CPU 时长 / draw call / 三角面 | 基线 | 相同(画质不变) |
|
||||
| 20 次 pointermove 拾取总耗时 | ≈26~42 ms | ≈7~11 ms(限流 + 零分配) |
|
||||
| 索引显存 | 4.08 MB | 2.04 MB |
|
||||
| 加载(navigation→首次渲染、DOMContentLoaded→load) | 基线 | 持平或略优 |
|
||||
| 交互后渲染次数(滑块/动画) | — | 每次变化 ≥1 次,无丢帧 |
|
||||
|
||||
### 4.4 回滚
|
||||
`Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html`(并同步回源文件的 `git checkout`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、执行顺序
|
||||
|
||||
1. 备份 + 校验 + `.gitignore`(改动 0)
|
||||
2. 起静态服务器 + 跑改前功能对照与改前性能插桩(拿到基线数据)
|
||||
3. 改成品 HTML(改动 1→6,逐处 Edit,锚点唯一串)
|
||||
4. 静态校验(4.1)
|
||||
5. 改后功能对照 + 改后性能插桩(4.2/4.3)
|
||||
6. 同步源文件补丁(改动 7)
|
||||
7. 写报告(改动 8)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"network": {
|
||||
"httpProxy": "http://127.0.0.1:7897",
|
||||
"noProxy": "localhost,127.0.0.1,192.168.31.10,z.ai,bigmodel.cn,modelscope.cn,sharellm.net,motomoto.lol"
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ python src/sweep_v2.py
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| **P01** | 胸板 / 安装板 | 根基固定 | 12mm 基础基座,预留 4x M4 紧固位与躯干走线窗 |
|
||||
| **P02** | 底脚 / 支撑座 | 根基固定 | 200×320mm 重型防倾覆底盘,提供稳固调试基座 |
|
||||
| **P03** | 肩吊臂 / 肩机舱 | J1 驱动抱紧 | 仿生规整风道机舱,带 7 组轴向散热格栅,抱紧 M1 电机 |
|
||||
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
|
||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
|
||||
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2,后侧刚性抱紧 M3 |
|
||||
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机 |
|
||||
|
||||
+27
-27
@@ -4,30 +4,30 @@
|
||||
"名称": "立柱下段",
|
||||
"类型": "3D打印",
|
||||
"数量": 1,
|
||||
"体积cm3": 402.4,
|
||||
"质量g_PLA": 499.0,
|
||||
"体积cm3": 402.8,
|
||||
"质量g_PLA": 499.4,
|
||||
"包络": "[-148.0 95.0 10.0 .. -82.0 161.0 188.0] (66.0x66.0x178.0)",
|
||||
"说明": "200mm适配分段A:高166mm+16mm阶梯公榫+非对称M4防反装通孔+底部50x50x12榫头+P01-A 竖排TOP刻字"
|
||||
"说明": "200mm适配分段A:高166mm+16mm阶梯公榫+顶端 2x Ø3.9 对锁孔(M4x16 自攻)+底部50x50x12榫头+P01-A 竖排TOP刻字"
|
||||
},
|
||||
{
|
||||
"件号": "P01_B_立柱中段_三角形30%_墙4底5_竖打",
|
||||
"名称": "立柱中段",
|
||||
"类型": "3D打印",
|
||||
"数量": 1,
|
||||
"体积cm3": 353.9,
|
||||
"质量g_PLA": 438.9,
|
||||
"体积cm3": 354.3,
|
||||
"质量g_PLA": 439.3,
|
||||
"包络": "[-148.0 95.0 172.0 .. -82.0 161.0 337.0] (66.0x66.0x165.0)",
|
||||
"说明": "200mm适配分段B:高165mm+双端公母榫+非对称M4防反装通孔+P01-B 竖排TOP刻字"
|
||||
"说明": "200mm适配分段B:高165mm+双端公母榫+两端各 2x Ø3.9 对锁孔(M4x16 自攻,非对称孔位防反装)+P01-B 竖排TOP刻字"
|
||||
},
|
||||
{
|
||||
"件号": "P01_C_立柱上段_三角形30%_墙4底5_竖打",
|
||||
"名称": "立柱上段",
|
||||
"类型": "3D打印",
|
||||
"数量": 1,
|
||||
"体积cm3": 305.3,
|
||||
"质量g_PLA": 378.5,
|
||||
"体积cm3": 305.4,
|
||||
"质量g_PLA": 378.7,
|
||||
"包络": "[-148.0 95.0 321.0 .. -82.0 161.0 470.0] (66.0x66.0x149.0)",
|
||||
"说明": "200mm适配分段C:高149mm+底端母榫+非对称M4防反装通孔+顶面P03安装台+P01-C 竖排TOP刻字"
|
||||
"说明": "200mm适配分段C:高149mm+底端母榫+底端 2x Ø3.9 对锁孔(非对称孔位防反装)+顶面P03安装台(4x Ø3.9 安装孔 + 1x Ø3.9 横向锁紧孔)+P01-C 竖排TOP刻字"
|
||||
},
|
||||
{
|
||||
"件号": "P02_Center_底座中核心_螺旋体30%_墙5底6_平打",
|
||||
@@ -64,20 +64,20 @@
|
||||
"名称": "肩关节安装悬臂",
|
||||
"类型": "3D打印",
|
||||
"数量": 1,
|
||||
"体积cm3": 728.0,
|
||||
"质量g_PLA": 902.8,
|
||||
"包络": "[-148.0 91.0 455.0 .. 8.5 165.0 647.0] (156.5x74.0x192.0)",
|
||||
"说明": "高强多肋机架:10mm后置法兰+双侧8mm大三角加强筋+下腹12mm抗弯龙骨筋+侧开式电机托架(M1从+Y侧滑入装配,前后法兰8xM3双向锁死)+前置止脱锁紧法兰(4xM3止脱螺孔)+大散热窗与对流槽"
|
||||
"体积cm3": 274.4,
|
||||
"质量g_PLA": 340.2,
|
||||
"包络": "[-148.0 91.0 455.0 .. 11.5 165.0 647.0] (159.5x74.0x192.0)",
|
||||
"说明": "轻量化板式桁架版:主干由 66mm 实心块改为双侧 6mm 桁架竖板+帐篷减重窗(斜面≥52°自支撑)+R4内角,外轮廓与旧版并集完全一致,实心 728->286cm³;底部 9mm 承力鞍座(Ø3.9 安装孔)+12mm 中位龙骨筋(自鞍座斜拉至法兰下缘)+40~45°外扩斜撑;保留 10mm 后置法兰、侧开式电机托架、Ø60.7 直通电机舱(平孔无台阶)(M1 从+Y滑入,后端 4xM3x16 锁死)、大散热窗与对流槽;无封闭空腔、免支撑"
|
||||
},
|
||||
{
|
||||
"件号": "P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"名称": "肩叉",
|
||||
"类型": "3D打印",
|
||||
"数量": 1,
|
||||
"体积cm3": 346.5,
|
||||
"质量g_PLA": 429.7,
|
||||
"包络": "[-16.5 91.5 455.0 .. 53.5 164.5 646.5] (70.0x73.0x191.5)",
|
||||
"说明": "加长避让版:头部与身子前移加长至X=10.5,与P03锁紧法兰保持2.0mm轴向安全间隙(不做前移,保持输出端最短悬伸力臂),J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"
|
||||
"体积cm3": 347.4,
|
||||
"质量g_PLA": 430.8,
|
||||
"包络": "[-15.5 91.5 455.0 .. 54.5 164.5 646.5] (70.0x73.0x191.5)",
|
||||
"说明": "齐平插槽版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"
|
||||
},
|
||||
{
|
||||
"件号": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
@@ -86,7 +86,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 159.5,
|
||||
"质量g_PLA": 197.8,
|
||||
"包络": "[-16.5 93.0 389.0 .. 53.5 174.0 525.0] (70.0x81.0x136.0)",
|
||||
"包络": "[-15.5 93.0 389.0 .. 54.5 174.0 525.0] (70.0x81.0x136.0)",
|
||||
"说明": "V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 169.3,
|
||||
"质量g_PLA": 210.0,
|
||||
"包络": "[-16.5 88.8 297.0 .. 53.5 167.2 390.1] (70.0x78.4x93.1)",
|
||||
"包络": "[-15.5 88.8 297.0 .. 54.5 167.2 390.1] (70.0x78.4x93.1)",
|
||||
"说明": "Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体,单实体0干涉"
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 289.1,
|
||||
"质量g_PLA": 358.5,
|
||||
"包络": "[-16.5 92.0 187.0 .. 53.5 174.0 367.0] (70.0x82.0x180.0)",
|
||||
"包络": "[-15.5 92.0 187.0 .. 54.5 174.0 367.0] (70.0x82.0x180.0)",
|
||||
"说明": "V2 L型结构:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉"
|
||||
},
|
||||
{
|
||||
@@ -116,7 +116,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 160.0,
|
||||
"质量g_PLA": 198.4,
|
||||
"包络": "[-16.5 93.0 120.0 .. 53.5 174.0 257.0] (70.0x81.0x137.0)",
|
||||
"包络": "[-15.5 93.0 120.0 .. 54.5 174.0 257.0] (70.0x81.0x137.0)",
|
||||
"说明": "V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉"
|
||||
},
|
||||
{
|
||||
@@ -126,7 +126,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 98.8,
|
||||
"质量g_PLA": 122.5,
|
||||
"包络": "[-16.5 93.0 93.0 .. 53.5 163.0 121.1] (70.0x70.0x28.1)",
|
||||
"包络": "[-15.5 93.0 93.0 .. 54.5 163.0 121.1] (70.0x70.0x28.1)",
|
||||
"说明": "V2圆角:外轮廓R=3mm,法兰面倒角2mm"
|
||||
},
|
||||
{
|
||||
@@ -146,7 +146,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 116.4,
|
||||
"质量g_PLA": 144.4,
|
||||
"包络": "[-13.3 100.0 458.1 .. 50.3 156.0 521.7] (63.6x56.0x63.6)",
|
||||
"包络": "[-12.3 100.0 458.1 .. 51.3 156.0 521.7] (63.6x56.0x63.6)",
|
||||
"说明": "复用 电机.STEP"
|
||||
},
|
||||
{
|
||||
@@ -156,7 +156,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 116.4,
|
||||
"质量g_PLA": 144.4,
|
||||
"包络": "[-13.3 96.1 389.0 .. 50.3 159.7 445.0] (63.6x63.6x56.0)",
|
||||
"包络": "[-12.3 96.1 389.0 .. 51.3 159.7 445.0] (63.6x63.6x56.0)",
|
||||
"说明": "复用 电机.STEP"
|
||||
},
|
||||
{
|
||||
@@ -166,7 +166,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 116.4,
|
||||
"质量g_PLA": 144.4,
|
||||
"包络": "[-13.3 100.0 300.1 .. 50.3 156.0 363.7] (63.6x56.0x63.6)",
|
||||
"包络": "[-12.3 100.0 300.1 .. 51.3 156.0 363.7] (63.6x56.0x63.6)",
|
||||
"说明": "复用 电机.STEP"
|
||||
},
|
||||
{
|
||||
@@ -176,7 +176,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 116.4,
|
||||
"质量g_PLA": 144.4,
|
||||
"包络": "[-13.3 100.0 190.1 .. 50.3 156.0 253.7] (63.6x56.0x63.6)",
|
||||
"包络": "[-12.3 100.0 190.1 .. 51.3 156.0 253.7] (63.6x56.0x63.6)",
|
||||
"说明": "复用 电机.STEP"
|
||||
},
|
||||
{
|
||||
@@ -186,7 +186,7 @@
|
||||
"数量": 1,
|
||||
"体积cm3": 116.4,
|
||||
"质量g_PLA": 144.4,
|
||||
"包络": "[-13.3 96.1 120.0 .. 50.3 159.7 176.0] (63.6x63.6x56.0)",
|
||||
"包络": "[-12.3 96.1 120.0 .. 51.3 159.7 176.0] (63.6x63.6x56.0)",
|
||||
"说明": "复用 电机.STEP"
|
||||
}
|
||||
]
|
||||
+31997
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,13 @@
|
||||
|
||||
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(Modifier) |
|
||||
|---|---|---|---|---|
|
||||
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 顶端榫接区 + Ø3.9 对锁孔周 |
|
||||
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + Ø3.9 对锁孔周 |
|
||||
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 底端榫接区 + Ø3.9 对锁孔周 + 顶面安装孔周 |
|
||||
| P02_Center_底座中核心_螺旋体30%_墙5底6_平打 | 螺旋体 30% | 5 / 6 | 平打 | Ø48 套筒 + 12mm 阶梯榫座 |
|
||||
| P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
||||
| P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
||||
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 10mm 后法兰孔 + J1 电机座接触面 |
|
||||
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 5x Ø3.9 安装孔周 + 后法兰 4xM3 沉头孔周 + 前锁紧法兰孔周 |
|
||||
| P04_肩叉_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | J1 输出凸台贯通区(整段实心) |
|
||||
| P05_大臂旋转座_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | J2 接口孔周 |
|
||||
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
|
||||
@@ -23,9 +23,11 @@
|
||||
## 通用规则
|
||||
|
||||
1. **打印方向**:全部按模型姿态打,弯曲发生在层面内,弯矩交给顶/底实心层和墙,不让层间缝扛拉。
|
||||
2. **螺纹孔一律不切螺纹**:M3/M4/M5 全部用黄铜热熔螺母;孔周按 1.5~2 倍螺栓直径加载实心 Modifier。P02 力臂的 M5 螺母沉槽同此处理。
|
||||
2. **不用热熔铜螺母、不需要攻丝、也不打沉孔/过孔**:全部连接螺钉统一为 **M4×16**,每个螺钉位**只打一个 Ø3.9 直孔**——M4×16 直接用扳手拧入,螺钉自己攻出螺纹(顶部 4 颗穿鞍座 9mm 后吃进立柱顶环 7mm;横向 1 颗穿 11mm 后壁吃进榫头 4.7mm;分段缝 8 颗穿母口壁+公榫壁共 10.5mm)。竖向载荷由承压面承担(顶部 2403mm²、分段缝榫座面),螺钉只防上抬/防滑。孔周按 1.5~2 倍螺栓直径加载实心 Modifier。
|
||||
3. **墙数和顶底壳优先于密度**:抗弯主力是墙+顶底实心层,填充件是剪力腹板。密度只在小区域加(关节毂 30~35%),不要全局堆高。
|
||||
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
|
||||
P03 已改薄壁桁架(双侧 6mm 竖板 + 帐篷减重窗 + 9mm 鞍座 + 12mm 中位龙骨筋):强度由 **墙层数** 决定,
|
||||
35% 只作用在很薄的内部芯上;想再压重量可把 P03 单独降到 螺旋体 20%(≈省 15~20g,强度不受影响)。
|
||||
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
|
||||
6. 立柱 P01 三段用三角形是唯一例外:轴压方向已知,Z 向斜撑跨层传力比螺旋体更对口。
|
||||
|
||||
|
||||
+25
-15
@@ -31,23 +31,23 @@
|
||||
},
|
||||
{
|
||||
"id": "S04",
|
||||
"规格": "M3 x 8",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°);座位为舱孔前段 4 个 Ø10 局部凸台(2026-10-02 平直舱孔方案),M3x10 旋入电机前端面咬合 3.5mm",
|
||||
"头型": "沉头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 前置止脱锁紧法兰 -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°)"
|
||||
"螺纹": "M3",
|
||||
"规格": "M3 x 10",
|
||||
"长度mm": 10
|
||||
},
|
||||
{
|
||||
"id": "S05",
|
||||
"规格": "M4 x 25",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 25,
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗",
|
||||
"备注": "Ø4.4 通孔,非对称偏心孔位物理防反装;需另配 M4 螺母 4 枚"
|
||||
"数量": 8,
|
||||
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
|
||||
"备注": "统一 Ø3.9 直孔(母口壁与公榫壁同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从外面直接拧入自攻成型,螺纹吃在母口壁 3.6 + 公榫壁 6.9 共 10.5mm;孔位 x=-131/-93 非对称物理防反装"
|
||||
},
|
||||
{
|
||||
"id": "S06",
|
||||
@@ -81,13 +81,23 @@
|
||||
},
|
||||
{
|
||||
"id": "S09",
|
||||
"规格": "M5 x 35",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 35,
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 肩吊臂安装台 -> P01_C 立柱顶面",
|
||||
"备注": "Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm"
|
||||
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
|
||||
"备注": "统一 Ø3.9 直孔(鞍座与立柱顶环同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从上方直接拧入自攻成型,穿鞍座 9mm 后吃进立柱顶环 7mm;载荷走 2403mm² 承压面,螺钉只防上抬"
|
||||
},
|
||||
{
|
||||
"id": "S11",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 1,
|
||||
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
|
||||
"备注": "与 S05/S09/S06 同为 M4x16(全机 M4x16 合计 17 颗);Ø3.9 直孔贯穿 11mm 后壁 + 榫头同径:从后方直接拧入自攻成型,榫头内咬合 4.7mm"
|
||||
},
|
||||
{
|
||||
"id": "S10",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1150" height="760" viewBox="0 0 1150 760">
|
||||
<rect width="1150" height="760" fill="#15181f"/>
|
||||
<rect x="400.0" y="154.0" width="396.0" height="396.0" fill="#1d4ed8" stroke="#9dc4ff" stroke-width="2.0"/>
|
||||
<rect x="466.0" y="220.0" width="264.0" height="264.0" fill="#12161d" stroke="#6f86b8" stroke-width="1.4"/>
|
||||
<rect x="488.8" y="484.0" width="26.4" height="66.0" fill="#243043" stroke="#ffd166" stroke-width="1.4"/>
|
||||
<rect x="488.8" y="154.0" width="26.4" height="66.0" fill="#243043" stroke="#96dcff" stroke-width="1.4"/>
|
||||
<rect x="484.0" y="460.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
|
||||
<rect x="484.0" y="220.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
|
||||
<rect x="488.8" y="400.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
|
||||
<rect x="488.8" y="154.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
|
||||
<rect x="481.0" y="550.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
|
||||
<rect x="481.0" y="130.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
|
||||
<rect x="716.8" y="484.0" width="26.4" height="66.0" fill="#243043" stroke="#ffd166" stroke-width="1.4"/>
|
||||
<rect x="716.8" y="154.0" width="26.4" height="66.0" fill="#243043" stroke="#96dcff" stroke-width="1.4"/>
|
||||
<rect x="712.0" y="460.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
|
||||
<rect x="712.0" y="220.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
|
||||
<rect x="716.8" y="400.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
|
||||
<rect x="716.8" y="154.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
|
||||
<rect x="709.0" y="550.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
|
||||
<rect x="709.0" y="130.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
|
||||
<text x="598.0" y="360.0" font-size="22" fill="#5f7396" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">内腔 44x44</text>
|
||||
<text x="598.0" y="558.0" font-size="17" fill="#ffd166" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">前 -Y y=95</text>
|
||||
<text x="598.0" y="158.0" font-size="17" fill="#96dcff" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">后 +Y y=161</text>
|
||||
<path d="M481.0,562.0 L150.0,562.0" stroke="#8fa4c8" stroke-width="1.2"/>
|
||||
<text x="10.0" y="554.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">M4x25 前面一颗</text>
|
||||
<text x="10.0" y="574.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">头 y=91..95 外露 4mm · 杆到 y=120</text>
|
||||
<path d="M484.0,472.0 L150.0,472.0" stroke="#8fa4c8" stroke-width="1.2"/>
|
||||
<text x="10.0" y="464.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">前螺母贴前壁内侧</text>
|
||||
<text x="10.0" y="484.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">y=106..110 · 压紧前侧 10.5mm 搭接壁</text>
|
||||
<path d="M515.2,352.0 L150.0,352.0" stroke="#8fa4c8" stroke-width="1.2"/>
|
||||
<text x="10.0" y="344.0" font-size="16" fill="#96dcff" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">两端杆头净空 16.2</text>
|
||||
<text x="10.0" y="364.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">前 120 / 后 136 · 互不碰头</text>
|
||||
<path d="M484.0,232.0 L150.0,232.0" stroke="#8fa4c8" stroke-width="1.2"/>
|
||||
<text x="10.0" y="224.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">后螺母贴后壁内侧</text>
|
||||
<text x="10.0" y="244.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">y=146..150 · 压紧后侧 10.5mm 搭接壁</text>
|
||||
<path d="M481.0,142.0 L150.0,142.0" stroke="#8fa4c8" stroke-width="1.2"/>
|
||||
<text x="10.0" y="134.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">M4x25 背面一颗</text>
|
||||
<text x="10.0" y="154.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">头 y=161..165 · 杆到 y=136</text>
|
||||
<text x="12.0" y="30.0" font-size="21" fill="#e8eefc" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">P01 立柱分段缝 · 两侧对锁(M4x25 前后各一颗,共 8 颗)· 俯视剖面</text>
|
||||
<text x="12.0" y="52.0" font-size="14" fill="#9fb4d8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">孔位 x=-131/-93 与 Ø4.4 通孔均未改动,与已打印的 A/B 段完全一致;偏心差 6mm 仍作防反装防呆</text>
|
||||
<text x="1138.0" y="746.0" font-size="13" fill="#6f86b8" text-anchor="end" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">单位 mm · 6px/mm · 数据取自 web/mesh_arm.json 的 S05 实例</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,205 @@
|
||||
[
|
||||
{
|
||||
"截面": "吊臂根座 @z=480",
|
||||
"零件": "P03_肩吊臂_螺旋体35%_墙5底5_竖打",
|
||||
"关节": "J1",
|
||||
"A": 975.4800000000001,
|
||||
"I": 391731.4096400291,
|
||||
"c": 37.23299062774025,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
-110.76700947225983,
|
||||
128.00000000000003
|
||||
],
|
||||
"M": 23529.618031505226,
|
||||
"F": 41.71918090692416,
|
||||
"sigma": 2.2364253314442926,
|
||||
"tau": 0.06415177283018231,
|
||||
"SF": 5.365705633574787
|
||||
},
|
||||
{
|
||||
"截面": "吊臂颊板 @z=560",
|
||||
"零件": "P03_肩吊臂_螺旋体35%_墙5底5_竖打",
|
||||
"关节": "J1",
|
||||
"A": 1102.4399999999998,
|
||||
"I": 605072.9254967812,
|
||||
"c": 43.53697862036803,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
-96.71968814629368,
|
||||
128.0
|
||||
],
|
||||
"M": 23529.618031505226,
|
||||
"F": 41.71918090692416,
|
||||
"sigma": 1.6930330775286337,
|
||||
"tau": 0.05676387954027997,
|
||||
"SF": 7.087870969134711
|
||||
},
|
||||
{
|
||||
"截面": "肩叉腹板 @z=560",
|
||||
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 2217.3650000000002,
|
||||
"I": 207369.8226041668,
|
||||
"c": 16.75000010000293,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
27.25,
|
||||
124.95500000000001
|
||||
],
|
||||
"M": 15271.482458030512,
|
||||
"F": 34.395230761329984,
|
||||
"sigma": 1.2335320997379497,
|
||||
"tau": 0.023267638003664246,
|
||||
"SF": 9.728161920187782
|
||||
},
|
||||
{
|
||||
"截面": "肩叉腹板 @z=600",
|
||||
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 2368.7748,
|
||||
"I": 276311.8560102971,
|
||||
"c": 21.62027641094759,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
25.562276310943524,
|
||||
126.94624799284419
|
||||
],
|
||||
"M": 15271.482458030512,
|
||||
"F": 34.395230761329984,
|
||||
"sigma": 1.1949312516479667,
|
||||
"tau": 0.02178039302933946,
|
||||
"SF": 10.042418744551561
|
||||
},
|
||||
{
|
||||
"截面": "大臂旋转座横梁 @z=480",
|
||||
"零件": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 1250.9348,
|
||||
"I": 490359.7093282266,
|
||||
"c": 35.000000100000086,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.5,
|
||||
165.03578024690017
|
||||
],
|
||||
"M": 15271.482458030512,
|
||||
"F": 34.395230761329984,
|
||||
"sigma": 1.090019994282287,
|
||||
"tau": 0.04124343342434392,
|
||||
"SF": 11.008972370182331
|
||||
},
|
||||
{
|
||||
"截面": "大臂旋转座套筒 @z=440",
|
||||
"零件": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
"关节": "J3",
|
||||
"A": 923.6637500000008,
|
||||
"I": 460541.5056708324,
|
||||
"c": 35.00071932511497,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.499280774885154,
|
||||
162.1529414131134
|
||||
],
|
||||
"M": 10944.224641918447,
|
||||
"F": 28.80059116294328,
|
||||
"sigma": 0.831750733009024,
|
||||
"tau": 0.046771226806740965,
|
||||
"SF": 14.42739936830306
|
||||
},
|
||||
{
|
||||
"截面": "大臂骨架中段 @z=350",
|
||||
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J3",
|
||||
"A": 1516.920000000001,
|
||||
"I": 1131945.0546959997,
|
||||
"c": 35.00000010000099,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.499999999999996,
|
||||
119.46110407931855
|
||||
],
|
||||
"M": 10944.224641918447,
|
||||
"F": 28.80059116294328,
|
||||
"sigma": 0.33839793015788383,
|
||||
"tau": 0.028479344160809335,
|
||||
"SF": 35.46120980823154
|
||||
},
|
||||
{
|
||||
"截面": "大臂骨架抱箍 @z=325",
|
||||
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 562.2200000000005,
|
||||
"I": 255126.52178866684,
|
||||
"c": 34.31311712541806,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.5,
|
||||
100.98098075486462
|
||||
],
|
||||
"M": 6102.354966361885,
|
||||
"F": 23.11498093318896,
|
||||
"sigma": 0.820733255145853,
|
||||
"tau": 0.06167064743300382,
|
||||
"SF": 14.621071980161778
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架中段 @z=245",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 1688.4280000000003,
|
||||
"I": 1225116.8186629335,
|
||||
"c": 35.00000010000015,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.5,
|
||||
120.60880570566229
|
||||
],
|
||||
"M": 6102.354966361885,
|
||||
"F": 23.11498093318896,
|
||||
"sigma": 0.17433637444141997,
|
||||
"tau": 0.020535356793291413,
|
||||
"SF": 68.83245127959344
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架抱箍 @z=195",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J5",
|
||||
"A": 381.97200000000026,
|
||||
"I": 75885.39226040001,
|
||||
"c": 22.39173964832456,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.5,
|
||||
103.25743876514508
|
||||
],
|
||||
"M": 2105.5758538864893,
|
||||
"F": 16.322293440980538,
|
||||
"sigma": 0.6212988419199141,
|
||||
"tau": 0.06409747353594188,
|
||||
"SF": 19.31437689939684
|
||||
},
|
||||
{
|
||||
"截面": "腕偏转座根部 @z=150",
|
||||
"零件": "P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J5",
|
||||
"A": 877.5228499999998,
|
||||
"I": 452897.5474791441,
|
||||
"c": 35.000019545458414,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
18.49998055454244,
|
||||
153.23437984777257
|
||||
],
|
||||
"M": 2105.5758538864893,
|
||||
"F": 16.322293440980538,
|
||||
"sigma": 0.1627193533077502,
|
||||
"tau": 0.02790062978014853,
|
||||
"SF": 73.74660577285154
|
||||
},
|
||||
{
|
||||
"截面": "工具法兰 @z=55",
|
||||
"零件": "P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"关节": "J6"
|
||||
}
|
||||
]
|
||||
+1130
-8
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,704 @@
|
||||
# 机械臂姿态估计算法评估报告
|
||||
|
||||
> 评估对象:`web/arm-ik-solver.js`(6 轴逆运动学姿态求解器)+ `web/ik-glue.js`(查看器集成)
|
||||
> 评估日期:2026-09-29 评估方式:静态审查 + 可复现的离线数值实验(`node`,固定随机种子)
|
||||
> 本报告为「评估 + 策略设计」,不含代码修改。
|
||||
>
|
||||
> **勘误(2026-09-29,实施 S0 时发现并已全表修正)**:本报告初版的「真最优」基准在度量姿态差时
|
||||
> 漏掉了 θ6(工具滚转)的闭式最优,即用 `rotErrDeg(orient(q), R_tar)`(θ6=0)代替了
|
||||
> 「θ6 取最优后的全姿态偏差」,系统性高估了可达的最小姿态差(例:`T=(20,400,300)` 初版 121.05°、
|
||||
> 修正后 44.20°)。**修正后缺陷 B 的劣化量更大(更严重),而非更小**:均值 104.2°→110.3°。
|
||||
> 修正涉及 §1、§2.3、§2.4、§3.3、§6.2、§6.3、§8.2 与 §9 附录命令(`posBest` 已改为 θ6 最优口径)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述与结论摘要
|
||||
|
||||
用户提出的问题:**「在多个关键位置无法达到预定姿态目标;肉眼可观察到存在更优姿态,但求解器给出的不是它。」**
|
||||
|
||||
评估结论:问题真实存在,且可稳定复现、可量化。核心原因不是"算不出来",而是**求解器在姿态不可达时丢弃了已经算出的位置精确解,转而使用一个更慢且更差的 6 维随机搜索**;同时在可达但构型退化时,姿态精确解的根枚举会漏解,并**把漏解当成成功**返回。
|
||||
|
||||
| 编号 | 缺陷 | 实测影响 | 根因 | 对应策略 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **A** | 姿态精确可达,却静默返回 1~2° 残差 | 全空间 0.50%(2/400) 触发;近伸直构型 3.3%(1/30);最大 1.934° | 根枚举在退化处漏解(R1) | S1 |
|
||||
| **B** | 姿态不可达时最近的"最近位姿"严重次优 | 平均劣化 **110.3°**(12 目标)/ **106.3°**(38 样本),最大 176.7°,被支配 12/12 | 接受门直接把候选池清空(R2)+ 兜底目标函数不一致(R3) | S0/S2 |
|
||||
| **C** | 位置精确解整体被丢弃,兜底被便宜解严格支配 | 存在 11920 个位置精确解的目标,兜底仍在指标上双输 | `oerr>POSETOL` 一律 `return null` | S2 |
|
||||
| **D** | 耗时超出既定 ≤100 ms 预算 | 开姿态中位 107 ms / p95 242 ms / 最大 299 ms | 兜底路径 160 次随机重启;姿态任务 escalating 重扫 | S1/S3 |
|
||||
|
||||
另外回答了一个架构问题(§3):**现状确实是「先点位、后角度」**;用户设想的"用末端指定角度逐级反推"**在本机构上不可行**——因为 `J4 ∥ J5`,这是**非球形腕**。但由此得到的「姿态优先」求解模式是可行且有价值的,实测可把姿态差压到 0.04°,代价是十几毫米的位置误差。
|
||||
|
||||
> 篇幅说明:第 9 章附录内嵌了本报告**全部数据的可复现命令**,任意数字都可重跑核对。
|
||||
|
||||
---
|
||||
|
||||
## 2. 算法现状描述
|
||||
|
||||
### 2.1 资产与数据流
|
||||
|
||||
| 角色 | 文件 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 姿态求解器 | [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) | UMD 单文件,`make(cfg)` 返回 `solve / solveTo / solveWarm / solveDyn / closest / closest6 / branch / fk / orient / rotErrDeg / LIM / costOf`([导出 L1355-1360](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1355-L1360)) |
|
||||
| 查看器集成 | [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) | 姿态 UI、目标拖动、调用 `solveTo`([L379](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L379)、[L399](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L399))、结果渲染与徽标([L318-342](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L318-L342)) |
|
||||
| 关节表(几何真值) | [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) | `joints[] = {id, rear, d}` |
|
||||
| 关节限位 | [travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json) | 本报告测量口径:J1/J3/J6 连续(±180),J2∈[-106,9],J4∈[-111,16],J5∈[-95,34](2026-09-29 起该表退为零位扫描参考:滑块改电机行程 ±360°,可行域改相邻件实测区间 J2[-107,128]/J4[-112,112]/J5[-96,96]) |
|
||||
| 既有自检 | [pose_ik_check.js](file:///g:/Robot_v2/机器人手臂/scratch/pose_ik_check.js) | 7 组 node 自检(本次全部通过,见 §6.3) |
|
||||
| 性能基准 | [bench_ik_split.js](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.js) / [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) | 单次求解耗时与位置/姿态占比拆分 |
|
||||
| 历史实验 | `scratch/ikopt/`(`solver_scanbase.js` = 改动前快照) | 见 §2.4 |
|
||||
|
||||
### 2.2 求解流程(`solve()`,[L558-944](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L558-L944))
|
||||
|
||||
1. **位置闭式 + 二维冗余扫描**:以 (θ4,θ5) 为冗余参数全域扫描;对每个网格点,[branch() L429-463](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L429-L463) 闭式给出最多 4 支 θ1..θ3(θ3 = ±σ、±(π−σ)),**位置误差天然 ~1e-13 mm**。
|
||||
2. **按行裁剪**:[rowSpans L644-654](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 以探测步 `hp = max(h, 2)°` 找每 θ4 行的可行 θ5 段,段内再按 h 精扫。
|
||||
3. **姿态任务**:工具轴对齐 = (θ4,θ5) 上的 2 方程 2 未知量,解为**孤立点**;[§4.1b 根枚举 L697-761](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L697-L761) 逐行做残差变号括号 + 牛顿收口([poseTools L486-545](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L486-L545));滚转角 θ6 由 [rollClose L549-555](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L549-L555) 闭式补足。
|
||||
4. **接受门**:`POSETOL = 2.0°`;[scoreBranch L592-606](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L592-L606) 对 `oerr > POSETOL` 的候选**直接 `return null`**([L596-598](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L596-L598))。
|
||||
5. **不可达兜底**:[closest6 L1054-1169](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 为 6 维阻尼最小二乘(DLS),160 次确定性伪随机重启 × 160 迭代;由 [solveTo L1172-1192](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1172-L1192) 在 `solve()` 失败时调用。
|
||||
6. **实时快路**:[solveWarm L1200-1265](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1200-L1265),沿上一帧解同支续解,任一项校验不过即回落全量。
|
||||
|
||||
**关键判断(贯穿全文)**:位置是**硬约束**(闭式、精确),姿态是**软约束**(带 2° 门、超门即弃)。
|
||||
|
||||
### 2.3 关节轴几何(判定"能否解耦"的依据)
|
||||
|
||||
| 关节 | rear (CAD) | d | 类型 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| J1 | (-51, 128, 610) | (1,0,0) | X 抬举 |
|
||||
| J2 | (18.5, 100, 490) | (0,**1**,0) | Y 俯仰 |
|
||||
| J3 | (18.5, 128, 445) | (0,0,−1) | Z 自转 |
|
||||
| J4 | (18.5, 100, 332) | (0,**1**,0) | Y 俯仰(肘) |
|
||||
| J5 | (18.5, 100, 222) | (0,**1**,0) | Y 俯仰(腕) |
|
||||
| J6 | (18.5, 128, 176) | (0,0,**−1**) | Z 工具自转 |
|
||||
|
||||
- **J4 ∥ J5**(均 +Y,相距 110 mm)⇒ 平行轴永不相交 ⇒ **非球形腕**。
|
||||
- 代码已利用该性质:`SAMEAX`([L408](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L408))令 `R4·R5 = R_y(θ4+θ5)`。
|
||||
- J5 ⊥ J6 且在 (18.5,128,222) 相交,故末两轴构成 2 轴腕,但肘关节 J4 破坏了球形性。
|
||||
|
||||
### 2.4 历史优化实验(`scratch/ikopt/`)
|
||||
|
||||
按时间:`solver_win` → `solver_fast` → `solver_warm` → `solver_old` → `solver_scanbase`(改动前快照)→ `bench_prune`(行裁剪 A/B)→ `bench_warmfollow`(warm 跟踪 A/B)。配套 `patch_*.py` / `merge_fast.py` / `audit_*.js`。
|
||||
|
||||
**结论**:这些迭代解决的是**耗时、连续性、退化漂移**三类问题(姿态预筛提速 1.41×、warm 窗口跟踪、同族门、行裁剪),**均未触及"覆盖完备性(缺陷 A)"与"兜底解质量(缺陷 B/C)"**。因此 A/B/C 三类缺陷在历次优化中一直存在。
|
||||
|
||||
---
|
||||
|
||||
## 3. 求解范式评估:位置优先 vs 末端姿态反推
|
||||
|
||||
> 本节回答用户提问:**「现在是不是先找点位再找角度?能不能用末端指定角度一级级反推回去?」**
|
||||
|
||||
### 3.1 现状确认:确实是"先点位、后角度"
|
||||
|
||||
`solve()` 先用位置约束闭式定死 θ1..θ3(位置误差 ~1e-13 mm),再把姿态当次级软约束(2° 门)。**所有实验里被牺牲的永远是角度**——§5 的兜底数据、§6.3 自检的 [4] 项("位置残差始终 3.1e-13 mm(姿态未达也不牺牲位置)")都直接印证这一点。
|
||||
|
||||
### 3.2 「末端角度逐级反推」可行性判定:本机构不成立
|
||||
|
||||
经典"末端反推"= 腕心解耦法,前提是**球形腕**(末端 3 轴交于一点):先由目标姿态算出腕心 `p_w = p_tcp − d₆·z_tool`,再由位置解 θ1..θ3,最后解 θ4/θ5/θ6。
|
||||
|
||||
**本机构不满足**:J4 ∥ J5(相距 110 mm 的平行轴),二者永不相交,不存在统一的"腕心",因此**无法**把 θ6→θ5→θ4 一级级闭式反推。
|
||||
|
||||
**但存在可用的降阶结构**(这才是正确的利用方式):
|
||||
|
||||
```
|
||||
R = Rx(θ1) · Ry(θ2) · Rz(−θ3) · Ry(θ4+θ5) · Rz(−θ6)
|
||||
```
|
||||
|
||||
即:**工具轴指向只由 (θ1, θ2, θ3, θ4+θ5) 决定;θ6 是纯工具滚转**。因此"用末端姿态驱动求解"是可行的,但要按这条降阶链解(3 方程 / 5 参数 → 2 参数族),而不是逐级反推。这一点也解释了为什么现有代码用 `axisCol()` 只算到 θ5 为止([L409-424](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L409-L424))。
|
||||
|
||||
### 3.3 更重要的实测结论:位置与姿态"真的不可兼得"
|
||||
|
||||
在可达姿态基础上把目标姿态外推 20°,用同一目标对照两种优先级(复现命令见 §9 A4/A3):
|
||||
|
||||
| 目标 TCP | 兜底 closest6 | 位置优先(保点) | 姿态优先(保角度) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| (308,−182,372) | 90.9° / 0.5 mm | **4.26° / 0.0 mm** | **0.04°** / 3.4 mm |
|
||||
| (180,−111,422) | 138.7° / 0.0 mm | **2.81° / 0.0 mm** | **0.01°** / 6.1 mm |
|
||||
| (83,192,1032) | 179.8° / 0.0 mm | **9.63° / 0.0 mm** | **0.06°** / 23.7 mm |
|
||||
| (−46,48,430) | 129.0° / 0.0 mm | **2.87° / 0.0 mm** | **0.01°** / 6.4 mm |
|
||||
| …(共 12 组,见附录) | 均值 **115.3°** | 均值 **5.7°** / 位置恒 0.0 mm | 均值 **0.04°** / 位置均 11.6 mm |
|
||||
|
||||
**结论**:位置与姿态在远目标是**真实的取舍关系**,不存在"都能兼顾"的解。
|
||||
- 位置优先:位置误差恒为 0,姿态差从 2.81° 到 9.63° 不等;
|
||||
- 姿态优先:姿态差 0.01°~0.10°(等价于精确到达),位置误差 3.4~27.7 mm;
|
||||
- 二者差值可达数十毫米 / 上百度。
|
||||
|
||||
⇒ **求解器必须提供可切换的优先级**,这正是用户诉求"无法精确达到目标姿态时输出最接近的角度解"的正确落点(→ 策略 S0)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 评估方法
|
||||
|
||||
### 4.1 指标定义
|
||||
|
||||
| 指标 | 定义 |
|
||||
| :--- | :--- |
|
||||
| 位置残差 `err` | `‖fk(q) − T‖`,mm |
|
||||
| 姿态残差 `oerr` | `rotErrDeg(orient(q), R_des)`,度(求解器 `rotErrDeg` 口径) |
|
||||
| 达标线 | 位置 ≤ 1e-6 mm(闭式恒满足);姿态 ≤ 0.05° |
|
||||
| **覆盖漏洞率** | 目标姿态精确可达(细扫 step=0.05° 能到 ≤0.05°)而模拟默认档 step=0.25° 返回 >0.05° 的比例 |
|
||||
| **静默残差** | 上述情况下返回的 `oerr`(求解器仍报 `reachable=true`,不报错) |
|
||||
| **步长敏感度** | 同一目标在 step ∈ {0.5,0.4,0.3,0.25,0.2,0.15,0.1,0.05} 下 `oerr` 的波动 |
|
||||
| **兜底劣化** | `closest6` 的姿态差 − 「位置精确解集内能达到的最小姿态差」 |
|
||||
| 耗时 | 单次 `solve` / `solveTo` 墙钟时间,ms |
|
||||
|
||||
### 4.2 独立真值基准(避免自证)
|
||||
|
||||
被测求解器自报的 `err/oerr` 与被测算法同源,不能自证。本报告的"真最优"用**独立于求解器决策路径**的枚举得到:
|
||||
对 (θ4,θ5) 做网格枚举,取 `branch()` 全部在限位内的支(位置天然精确),再对姿态差取最小;并对最优的前 64 个候选**解析优化 θ6**(绕 J6 轴的滚转,闭式取到最优),得到更紧的真值。
|
||||
注:该基准对 (θ4,θ5) 仍是离散网格(0.5°),故给出的真最优是**上界**,据此算出的"劣化"是**下界**——结论偏保守。
|
||||
|
||||
### 4.3 基准数据集与随机性
|
||||
|
||||
- 采样一律使用**固定种子的 mulberry32**(`seed` 已在附录命令中写死),任意数据可逐位复现。
|
||||
- 早期探索阶段曾用 `Math.random()`(不可复现)得到过更高的触发率(最高 2.5%/120 样本);本报告**只采用可复现数据**。
|
||||
- 数据集:D1 全空间随机可达位形 N=400;D2 工作区两区各 N=60;D3 姿态外推 20°(120 目标);D4b 近伸直构型 N=30;D5 越界目标 4 个;D6 双优先级 12 组。
|
||||
|
||||
---
|
||||
|
||||
## 5. 性能分析
|
||||
|
||||
### 5.1 单次求解耗时构成([bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json),40 样本,step 0.25 / tol 1e-6 / 开姿态)
|
||||
|
||||
| 指标 | 数值 |
|
||||
| :--- | ---: |
|
||||
| 冗余维网格 | J4 509 点 × J5 517 点 ≈ 26.3 万粗扫点 |
|
||||
| 开姿态单次 · 中位 | **107.1 ms** |
|
||||
| 开姿态单次 · p95 | **242.5 ms** |
|
||||
| 开姿态单次 · 最大 | **299 ms** |
|
||||
| 其中"位置基座"中位 | 73.6 ms |
|
||||
| 其中"姿态增量"中位 | 37.2 ms(占 **33.3%**) |
|
||||
| escalating 触发率 | 2.5% |
|
||||
| 关姿态同目标中位 | 73.4 ms |
|
||||
|
||||
### 5.2 本次实测耗时
|
||||
|
||||
| 场景 | 耗时 |
|
||||
| :--- | ---: |
|
||||
| D1 全空间可达目标 `solve(0.25)` 均值 | 100 ms |
|
||||
| D2 前伸工作区 / 默认区 均值 | 98 ms / 98 ms |
|
||||
| D5 越界目标 `solveTo`(含兜底) | 85 ~ 125 ms |
|
||||
| 参考:`solve(0.05)` 均值(D1) | ~835 ms |
|
||||
|
||||
### 5.3 结论
|
||||
|
||||
- **已超预算**:用户既定目标为 ≤100 ms,实测中位 107 ms、p95 242 ms、最大 299 ms。
|
||||
- **姿态任务是主要波动源**:占总耗时 33%,且 escalating(×100 权重重扫)在 2.5% 的目标上会额外付出整轮精修。
|
||||
- **兜底路径性价比最低**:160 次随机重启的 6 维 DLS 在 85~125 ms 内给出的是 §6 里"平均劣化 104°"的解——**又慢又差**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 误差来源定位
|
||||
|
||||
### 6.1 R1 根枚举不完备(对应缺陷 A)
|
||||
|
||||
姿态精确解在 (θ4,θ5) 平面上是一条**一维曲线**,只在少数孤立点穿过 `POSETOL` 窄带。当该曲线经过退化构型时,变号括号检测与"θ1..θ3 最近"同支匹配会失效:
|
||||
|
||||
- **退化点 1:θ3 → 0 或 ±180(手臂近伸直)**。此时 `branch()` 中 `Vx → 0`,4 支两两重合、位置雅可比奇异,牛顿收口停摆。
|
||||
- **退化点 2:关节贴限位**(如 θ5 ≈ J5 上限)。
|
||||
|
||||
**可复现证据(见 §9 命令 F,seed=424242)**:
|
||||
|
||||
| 项 | 值 |
|
||||
| :--- | :--- |
|
||||
| 目标 TCP | **(26.386, −3.334, 1019.998)** |
|
||||
| 真实关节角 | (198.03, −46.76, **−180.52**, −92.79, **33.48**, 102.36) ← θ3≈−180 近伸直、θ5≈J5 上限 34 |
|
||||
| 真解在 `branch()` 下 | 位置误差 **1.4e-13 mm** 的精确解存在,且在限位内 |
|
||||
| `solveTo({warm: 真解})` | `oerr = 1.21e-6°`(**精确解确实存在**) |
|
||||
| `solve(step=0.25)` | `oerr = 1.9881°`(**漏解,且报 reachable=true 不报错**) |
|
||||
|
||||
**步长敏感度(同一目标)**——非单调、非收敛,直接证明这不是"精度不够"而是"没找到":
|
||||
|
||||
| step | 0.5 | 0.4 | 0.3 | 0.25 | 0.2 | 0.15 | 0.1 | 0.05 |
|
||||
| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `oerr` (°) | 1.9881 | 1.9119 | **0.0307** | 1.9881 | 1.9881 | 1.9652 | **0.0258** | 0.0491 |
|
||||
|
||||
> **顺带发现**:查看器"扫描精度"下拉框的悬停说明写着「姿态任务的精确解由孤立根枚举给出、**与步长无关**」([viewer_tpl.html L229](file:///g:/Robot_v2/机器人手臂/src/viewer_tpl.html#L229))。上表**直接证伪**了这一说明。
|
||||
|
||||
**发生率(可复现)**:
|
||||
|
||||
| 数据集 | 样本 | `oerr>0.05°` | 其中覆盖漏洞 | 最大静默残差 |
|
||||
| :--- | ---: | ---: | ---: | ---: |
|
||||
| D1 全空间(seed=20260929) | 400 | 2(0.50%) | 1(0.25%) | 1.934° |
|
||||
| D4b 近伸直 θ3≈0/±180(seed=909090) | 30 | 1(3.3%) | 1(3.3%) | 1.860° |
|
||||
| D2 前伸工作区 (20,210,300)±90mm | 60 | 0 | 0 | 0° |
|
||||
| D2 查看器默认区 (150,128,260)±60mm | 60 | 0 | 0 | 0° |
|
||||
|
||||
**结论**:缺陷 A 是**构型相关**的——常规工作区(默认目标附近)几乎不触发,**集中在外围/极端位姿与近伸直、贴限位构型**。这解释了为什么肉眼只在"关键位置"看到问题。
|
||||
|
||||
### 6.2 R2 接受门把"不可达"与"漏解"混为一谈(对应缺陷 C)
|
||||
|
||||
`scoreBranch` 对 `oerr > POSETOL(2°)` 的候选一律 `return null`([L596-598](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L596-L598))。后果是**候选池被整体清空**,`solve()` 报 `ok=false`,转入 `closest6`。
|
||||
|
||||
**可复现证据**:目标 `T=(20,400,300)`、`R=poseMat(25,95,10)`:
|
||||
|
||||
- 位置**完全可达**:无姿态 `solve` 返回 `err = 2.5e-14 mm`;0.5° 网格上有 **11920** 个位置精确解;
|
||||
- 姿态目标不可达(位置精确解集内最优姿态差 **44.20°** > 2°)⇒ 候选池被清空 ⇒ 走 `closest6`;
|
||||
- `closest6` 返回 **136.7° / 位置 0.0 mm** —— **在位置与姿态两个指标上都不优于**一个用现有闭式扫描就能廉价得到的解(劣化 **92.5°**)。**被严格支配(Pareto-dominated)。**
|
||||
|
||||
另一例(离查看器默认目标很近,仅抬高 Z):`T=(150,128,600)` 位置精确可达(0.0 mm,2580 个位置精确解),位置精确解集内最优姿态差 **14.72°**,而 `closest6` 给出 **173.4°** —— 劣化 **158.7°**。
|
||||
|
||||
> 注:`T=(20,400,300)` 这一例另有独立复核——初版基准给的 121.05° 是 θ6=0 口径(见文首勘误),
|
||||
> 按 θ6 最优口径重扫全域 0.5° 网格(258×254 ≈ 6.6 万点)得 **44.200°**,与 θ6 最优口径一致。
|
||||
|
||||
### 6.3 R3 兜底目标函数与真实目标不一致(对应缺陷 B)
|
||||
|
||||
`closest6` 用固定 `OW = 20 mm/rad` 把姿态折进位置残差做联合 DLS([L1055](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1055)),既不保证"位置优先",也不保证"姿态最小",且依赖随机重启初值。
|
||||
|
||||
**严格真最优基准下的兜底劣化(§9 命令 O2,12 目标)**:
|
||||
|
||||
| 目标 TCP | closest6 姿态差 | 位置优先真最优(θ6 最优口径) | 劣化 |
|
||||
| :--- | ---: | ---: | ---: |
|
||||
| (364,336,843) | 114.2° | 5.98° | 108.2° |
|
||||
| (288,−16,595) | 166.8° | 7.94° | 158.9° |
|
||||
| (72,376,161) | 107.2° | 5.30° | 101.9° |
|
||||
| (311,10,701) | 138.2° | 14.72° | 123.4° |
|
||||
| (−52,242,897) | 121.1° | 3.18° | 117.9° |
|
||||
| (4,−218,303) | 156.3° | 13.02° | 143.3° |
|
||||
| (279,−155,765) | 173.0° | 3.15° | **169.9°** |
|
||||
| (402,202,425) | 81.2° | 16.48° | 64.7° |
|
||||
| (−4,472,883) | 136.3° | 4.92° | 131.4° |
|
||||
| (189,−298,766) | 54.4° | 16.61° | 37.8° |
|
||||
| (259,−284,528) | 44.8° | 5.84° | 38.9° |
|
||||
| (137,−315,829) | 133.5° | 6.20° | 127.3° |
|
||||
| **均值** | **118.9°** | **8.6°** | **110.3°** |
|
||||
|
||||
**被支配 12/12**:每一个目标上,`closest6` 的姿态差都大于一个"位置同样精确、只是换了构型"的解。
|
||||
|
||||
**更大样本的兜底劣化(§9 命令 A3,120 目标,38 个不可达样本)**:平均劣化 **106.3°**,最大 **176.7°**,最小 16.1°。
|
||||
|
||||
### 6.4 R4 行裁剪探测步过粗
|
||||
|
||||
[rowSpans](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 以 `hp = max(h,2)°` 探测每 θ4 行的可行 θ5 段,并按"单一连续段"假设(注释称仅在 5 个目标上验证过)。窄于 2° 的可行段会被整段漏掉,导致整行无候选——这是 §6.1 漏解的另一条通路,也与"θ5 贴着限位"的退化构型吻合。
|
||||
|
||||
### 6.5 既有自检为何没有拦住这些缺陷
|
||||
|
||||
`node scratch/pose_ik_check.js` 本次**全部通过**:
|
||||
|
||||
| 项 | 结果 |
|
||||
| :--- | :--- |
|
||||
| [1] poseMat ↔ poseAng 往返 | ✓ 最大偏差 2.4e-6° |
|
||||
| [2] 位置 IK 回归(200 组) | ✓ 200/200,最大残差 3.5e-13 mm,均耗时 68 ms |
|
||||
| [3] 姿态重构(200 组,step 0.5) | ✓ 达标 195/200;**允许 4 个"奇异刀尖"不达标**;最差 0.90° |
|
||||
| [4] 工作区 ±45mm(48 个) | ✓ 48/48 精确,最差 0.02° |
|
||||
| [5] roll 0→90 口径 | ✓ Δθ6 = 90.000° |
|
||||
| [6] 超工作空间 → closest6 | ✓ **只检查"返回了最近位姿",不检查其最优性**(返回 0.0 mm / **136.7°**) |
|
||||
| [7] 耗时对比 | 关 41 ms / 开 15 ms(**开比关还快,异常,说明该计时项不可靠**) |
|
||||
|
||||
**盲区诊断**:
|
||||
1. [3] 把 2° 以内的残差视为"通过",且显式豁免"奇异刀尖"——与缺陷 A 的静默残差(1.2~1.95°)**正好落在盲区内**;
|
||||
2. [4] 只在 `(20,210,300)±45mm` 这一处良性区域采样(与 D2 的 0 失败一致),碰不到外围退化构型;
|
||||
3. [6] 只验证"有返回",不验证"最近"——与缺陷 B/C 完全错开。
|
||||
|
||||
---
|
||||
|
||||
## 7. 优化策略
|
||||
|
||||
按"收益 / 成本"排序。所有策略共用一份闭式支枚举结果,不改变运动学口径。
|
||||
|
||||
> **实施状态**:**S0(位置优先 / 姿态优先切换)已实施并验证**,见 §7.1;
|
||||
> S2 中"不再清空候选池"的部分已随 S0 的位置优先模式一并落地。S1 / S3 / S4 及 S0 的"折中"模式尚未实施。
|
||||
|
||||
### 7.1 S0(已实施)双优先级求解范式
|
||||
|
||||
**问题**:位置与姿态在远目标上不可兼得(§3.3),单一目标函数无法兼顾两种使用意图。
|
||||
|
||||
**已实现**:
|
||||
|
||||
| 项 | 实现 |
|
||||
| :--- | :--- |
|
||||
| 求解器入口 | `solve()/solveWarm()/solveTo()` 新增 `opt.priority ∈ {'pos','ori'}`;**省略该选项时行为与改动前逐字一致**(既有自检/基准不受影响) |
|
||||
| 位置优先 `'pos'` | `min oerr s.t. ‖fk(q)−T‖ = 0`。扫描时额外记录「轴夹角最小」的廉价种子(复用 AXREJ 已算的点积,零额外开销);若 POSETOL 硬门把候选全挡下,则从这些种子出发在 (θ4,θ5) 上模式搜索最小化 oerr(新增 §4.5,每点仍走 `branch()` 闭式支 ⇒ 位置保持精确) |
|
||||
| 姿态优先 `'ori'` | `min ‖fk(q)−T‖ s.t. oerr = 0`。新增 [closestOri](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) §5c:利用降阶链 `w = Rx(θ1)·Ry(θ2)·Rz(−θ3)·Ry(s)·ẑ`(s=θ4+θ5),给定 (θ3,s) 对 (θ1,θ2) 闭式求解,再在 (θ3,s,θ4) 三维上粗扫 + 坐标下降精修;θ6 由 `rollClose` 闭式补足 ⇒ 轴对齐后 oerr 恒为 0 |
|
||||
| 实时快路 | `solveWarm` 在 `'pos'` 下不再要求 oerr ≤ 0.05°(否则拖进"姿态不可达区"后每帧回落全量),改以 oerr 为主键选解 |
|
||||
| 界面 | [viewer_tpl.html](file:///g:/Robot_v2/机器人手臂/src/viewer_tpl.html) 姿态控制框内新增「优先级」下拉(位置优先 / 姿态优先,默认位置优先);[ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) 传入该选项、结果栏标注模式、并对姿态优先的"姿态到达·位置最近 X mm"单独置徽标 |
|
||||
| 未实施 | 「折中」`min ‖fk−T‖² + k·oerr²`(k 可调)——需要 6 维联合优化,留待 S3 一并处理 |
|
||||
|
||||
**验证结果(本次实测)**:
|
||||
|
||||
| 检查 | 结果 |
|
||||
| :--- | :--- |
|
||||
| `axisSolve` 闭式解正确性(20000 组随机位形回代) | 未命中 0,最大角差 **4.99e-9°** |
|
||||
| `fkChain` 独立复核位置精度(位置优先) | 恒 **~1e-13 mm**(如 1.66e-13 mm) |
|
||||
| 姿态可达时与原行为一致 | `'pos'` 59/60、`'ori'` **40/40** 与默认解逐位一致(差 1 例落在 0.05° 达标边界) |
|
||||
| 位置优先 vs θ6 最优 1° 网格基准(10 个不可达目标) | 劣于基准 **0/10**,最大劣化 **0.000°**;姿态差均值 **11.4°** vs 默认 DLS 的 **121.0°**(改善 **109.6°**) |
|
||||
| 姿态优先的精确性(8 个不可达目标) | 姿态差 **0.0000°**(最大 1.21e-6°);位置均值 **24.35 mm** vs 惩罚下降参考的 24.76 mm |
|
||||
| 姿态优先耗时 | 18~100 ms(均值 **52 ms**),低于其替代的 DLS(85~125 ms) |
|
||||
| 既有自检 `pose_ik_check.js` 7 项 | **全部通过**(默认路径零回归) |
|
||||
| 关键目标实测 | `T=(20,400,300)`:默认 DLS 136.7° → 位置优先 **44.19°**(基准 44.20°)、姿态优先 **0.0000°/116.4mm** |
|
||||
|
||||
**收益**:位置优先把 §6.3 的平均劣化 **110.3°** 压到接近 0(实测不劣于基准);姿态优先提供严格的"保角度"解(0.0000°),位置代价如实上报。
|
||||
|
||||
### S1(根因修复)覆盖率完备化,消除缺陷 A
|
||||
|
||||
**方案**(二选一或组合):
|
||||
1. **解曲线 continuation 跟踪**:把 `poseRes(θ4,θ5) = 0` 视为 (θ4,θ5) 平面上的一维解曲线,用预测-校正沿曲线推进,**跨越折叠点时不换支**(对比现状的"变号括号 + 最近 θ1..θ3 匹配",后者在退化处失效);
|
||||
2. **分支索引对齐的多起点牛顿 + 退化种子**:以 `branch()` 的**支下标**(0..3)而非"θ1..θ3 最近"做同支判据;在 θ3∈{0, ±180}、各轴贴限位处、以及 rowSpans 的段端点额外播种。
|
||||
|
||||
同时把 [rowSpans](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 的"单一连续段"假设放宽为多段(探测步 hp 与段数一起记录)。
|
||||
|
||||
**改动点**:`solve()` §4.1b([L697-761](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L697-L761))、`poseTools` 的 `poseQAt` 同支匹配([L495-504](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L495-L504))、`rowSpans`。
|
||||
**预期收益**:覆盖漏洞率 0.25%→<0.05%(D1 口径),近伸直构型 3.3%→<0.5%;最大静默残差 1.934°→≤0.05°。
|
||||
**代价**:**不增加粗扫密度**,只在退化行补一次定向播种/跟踪,预计耗时增量 <10%,仍满足 ≤100 ms。
|
||||
|
||||
### S2(关键收益)姿态不可达时不再清空候选池,消除缺陷 B/C
|
||||
|
||||
**实现方式(随 S0 落地,比上述"放宽门"更省)**:**不动** `scoreBranch` 的 POSETOL 硬门(扫描路径与耗时逐字不变),
|
||||
只在门内额外记录「轴夹角最小」的廉价种子;候选池为空时改由 §4.5 从种子出发做最小残差搜索。
|
||||
这样既拿到"不清空信息"的全部收益,又避免为超门候选逐个算完整滚转(实测那会让粗扫开销翻倍级增长)。
|
||||
|
||||
**改动点**:`solve()` §4.5(新增)、`scoreBranch` 的种子记录、选解段 `poolByOerr` 分支。
|
||||
**预期收益**:`closest6` 被支配的现象**直接消失**(不需要再走兜底),平均劣化 110.3°→≈0(相对真最优,实测不劣于基准)。
|
||||
**代价**:极小(常态零开销,仅在落选路径多一次局部搜索)。这是本次评估中**性价比最高的一条**。
|
||||
|
||||
### S3 兜底路径加速与改造
|
||||
|
||||
**方案**:位置也越界时才需要真正"最近位姿"。此时:
|
||||
1. 用 S2 得到的闭式候选(位置精确解集的最优者)作为 DLS 初值,取代 160 次随机重启;
|
||||
2. 只在位置完全不可达时保留受限 DLS(重启次数降到 16~32),并对 J1/J3/J6 三个连续轴改用最短角差而非硬夹限。
|
||||
|
||||
**预期收益**:兜底耗时 85~125 ms→≤60 ms;位置不可达时结果优于现状(现状 `T=(400,300,200)` 距目标 104.0 mm / 姿态差 172.0°)。
|
||||
**代价**:需保证"位置越界"判定与现有 `tol` 口径一致,风险低。
|
||||
|
||||
### S4 可观测性(防止静默失败再次发生)
|
||||
|
||||
**方案**:把 UI 徽标从两态扩为四态,并同步修正 §6.1 提到的错误悬停说明:
|
||||
|
||||
- `已到达`(位置精确 + 姿态 ≤0.05°)
|
||||
- `位置到达·姿态最近 X.XX°`(位置精确、姿态未达标,**明确说明这是"最近解"而非失败**)
|
||||
- `姿态到达·位置最近 Y.Y mm`(姿态优先模式)
|
||||
- `均不可达·最近位姿(位置 Y mm / 姿态 X°)`
|
||||
|
||||
**改动点**:[renderResult L318-342](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L318-L342);[viewer_tpl.html L229](file:///g:/Robot_v2/机器人手臂/src/viewer_tpl.html#L229) 的 `ikStep` 说明文字。
|
||||
|
||||
### 策略汇总
|
||||
|
||||
| 策略 | 针对缺陷 | 收益 | 成本 | 是否影响 ≤100 ms | 状态 |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| S0 双优先级 | 用户诉求(保点/保角度可切换) | 位置优先劣化 →≈0;姿态优先 0.0000° | 中 | 姿态优先 18~100 ms(均值 52 ms),未超预算 | **已实施** |
|
||||
| S1 覆盖完备化 | A | 漏洞率 →<0.05% | 中 | 增量 <10% | 待实施 |
|
||||
| S2 不清空候选池 | B / C | 兜底劣化 →≈0,常态零额外开销 | **极低** | 无 | **已随 S0 实施** |
|
||||
| S3 兜底加速 + 折中模式 | D + 越界质量 | 85~125 ms→≤60 ms;补上 blend 模式 | 低 | 改善 | 待实施 |
|
||||
| S4 可观测性 | 静默失败 | 漏解/取舍不再被当成"成功" | 低 | 无 | 部分(结果栏已标注模式与"姿态到达·位置最近") |
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证方案与验收口径
|
||||
|
||||
### 8.1 基准数据集(固定种子,命令见附录)
|
||||
|
||||
| 集 | 内容 | 规模 | 用途 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| D1 | 全空间随机可达位形 | 400 | 覆盖率 / 静默残差 / 耗时 |
|
||||
| D2 | 前伸工作区 (20,210,300)±90mm;默认区 (150,128,260)±60mm | 60 + 60 | 常规工作区回归 |
|
||||
| D3 | 目标姿态外推 20° | 120 目标 | 兜底劣化 |
|
||||
| D4b | 近伸直 θ3≈0/±180 | 30 | 退化构型覆盖 |
|
||||
| D5 | 越界目标(4 个定点) | 4 | 兜底耗时与质量 |
|
||||
| D6 | 双优先级不可兼得场景 | 12 | 保点/保角度取舍 |
|
||||
| O2 | 真最优基准(θ6 解析优化) | 12 | 独立真值 |
|
||||
|
||||
### 8.2 验收口径(现状 → 目标)
|
||||
|
||||
| 指标 | 现状实测 | 目标 | 当前状态 |
|
||||
| :--- | ---: | ---: | :--- |
|
||||
| D1 覆盖漏洞率(step 0.25 相对 0.05) | 0.25%(1/400) | ≤0.05% | 待 S1 |
|
||||
| D4b 近伸直覆盖漏洞率 | 3.3%(1/30) | ≤0.5% | 待 S1 |
|
||||
| 最大静默姿态残差 | **1.934°** | ≤0.05° | 待 S1 |
|
||||
| 步长敏感度(同目标 oerr 波动) | **1.9881 − 0.0258 = 1.96°** | ≤0.05° | 待 S1 |
|
||||
| 兜底相对真最优劣化(12 目标均值) | **110.3°** | ≤2° | **已达成**(S0 位置优先:0/10 劣于基准) |
|
||||
| 兜底被支配比例 | **12/12** | 0/12 | **已达成**(S0 位置优先:不劣于基准的解总是存在) |
|
||||
| 位置优先模式姿态差(12 目标均值) | 5.7°(真最优基准) | ≈真最优(劣化 ≤1°) | **已达成** |
|
||||
| 姿态优先模式姿态差 | **0.0000°**(最大 1.21e-6°) | ≤0.05° | **已达成** |
|
||||
| 开姿态单次耗时 · 中位 / p95 | 107 ms / 242 ms | ≤100 ms / ≤150 ms | 待 S1/S3(S0 未使常态变慢) |
|
||||
| 兜底路径耗时 | 85~125 ms | ≤60 ms | 姿态优先降到 18~100 ms;位置越界仍待 S3 |
|
||||
| 位置残差(姿态未达时) | 3.5e-13 mm(不得牺牲) | 保持 ≤1e-6 mm | **已达成**(S0 位置优先实测 ~1e-13 mm) |
|
||||
|
||||
### 8.3 回归门与新增门
|
||||
|
||||
- **沿用**:`node scratch/pose_ik_check.js` 7 项必须继续全绿;
|
||||
- **修正**:[3] 项的"豁免奇异刀尖"应改为"显式区分可达/不可达",[6] 项应增加"最近性"断言,[7] 项计时需先预热并与 `bench_ik_split.js` 对齐(现状"开比关快"不可靠);
|
||||
- **新增**:把 §8.1 的 D1/D3/D4b/D6 固化为可重复的覆盖率与最优性断言(当前仓库缺少这两类断言,正是缺陷 A/B 长期存活的直接原因)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 附录:可复现实验命令
|
||||
|
||||
统一环境:在项目根目录 `g:\Robot_v2\机器人手臂` 下执行;`ArmIK` 由 `web/arm-ik-solver.js` 加载;关节表/限位取自 `web/mesh_arm.json`、`docs/travel.json`;`tcp=[18.5,128,93]`。
|
||||
所有随机采样使用固定种子的 **mulberry32**(`s = seed>>>0`,`s=(s+0x6D2B79F5)>>>0; t=s; t=Math.imul(t^(t>>>15),t|1); t^=t+Math.imul(t^(t>>>7),t|61); return ((t^(t>>>14))>>>0)/4294967296;`),结果逐位可复现。
|
||||
|
||||
### A1 / A2 — 覆盖率、静默残差、工作区回归(对应 §6.1、§6.4)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;
|
||||
const mkseed=v=>{let s=v>>>0;return()=>{s=(s+0x6D2B79F5)>>>0;let t=s;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296;};};
|
||||
const rnd=mkseed(20260929);const rndQ=()=>O.map(id=>{const t=LIM[id];return t.cont?(rnd()*720-360):(t.lo+rnd()*(t.hi-t.lo));});
|
||||
console.log("[A1] D1 全空间采样(seed=20260929) step0.25 两阶段");
|
||||
const N=400;let fails=[],sum=0;
|
||||
for(let k=0;k<N;k++){const q=rndQ(),Tp=S.fk(q,0),R=S.orient(q);const a=S.solve(Tp,{step:0.25,tol:1e-6,pose:{R:R}});sum+=a.ms;
|
||||
const va=a.q?a.oerr:999;if(va>0.05)fails.push({T:Tp,R,oa:va});}
|
||||
console.log(" N="+N+" oerr>0.05 数量="+fails.length+" 比例="+(fails.length/N*100).toFixed(2)+"pct 均耗时="+(sum/N).toFixed(0)+"ms");
|
||||
let gap=0,maxS=0,maxG=0,tbl=[];
|
||||
for(const f of fails){const b=S.solve(f.T,{step:0.05,tol:1e-6,pose:{R:f.R}});const vb=b.q?b.oerr:999;
|
||||
if(f.oa>maxS)maxS=f.oa;const g=f.oa-vb;if(g>0.05){gap++;if(g>maxG)maxG=g;}
|
||||
tbl.push({T:f.T.map(v=>+v.toFixed(1)),oa:+f.oa.toFixed(3),ob:+vb.toFixed(3),gap:+g.toFixed(3)});}
|
||||
console.log(" 其中覆盖漏洞(细扫能达标)="+gap+" 真近奇异="+(fails.length-gap)+" 最大静默="+maxS.toFixed(3)+"deg 最大覆盖差="+maxG.toFixed(3)+"deg");
|
||||
console.log(" 明细="+JSON.stringify(tbl.slice(0,10)));
|
||||
const r2=mkseed(777001);const rq2=()=>O.map(id=>{const t=LIM[id];return t.cont?(r2()*720-360):(t.lo+r2()*(t.hi-t.lo));});
|
||||
function zone(name,T0,rad,nmax){const pts=[];for(let k=0;k<3000000&&pts.length<nmax;k++){const q=rq2();const p=S.fk(q,0);if(Math.hypot(p[0]-T0[0],p[1]-T0[1],p[2]-T0[2])<=rad)pts.push({Tp:p,R:S.orient(q)});}
|
||||
let f2=[],s2=0;for(const p of pts){const a=S.solve(p.Tp,{step:0.25,tol:1e-6,pose:{R:p.R}});s2+=a.ms;const va=a.q?a.oerr:999;if(va>0.05)f2.push({Tp:p.Tp,R:p.R,oa:va});}
|
||||
let g2=0,mS=0,mG=0;for(const f of f2){const b=S.solve(f.Tp,{step:0.05,tol:1e-6,pose:{R:f.R}});const vb=b.q?b.oerr:999;if(f.oa>mS)mS=f.oa;const g=f.oa-vb;if(g>0.05){g2++;if(g>mG)mG=g;}}
|
||||
console.log("["+name+"] N="+pts.length+" 失败="+f2.length+" 覆盖漏洞="+g2+" 最大静默="+mS.toFixed(3)+"deg 均耗时="+(s2/pts.length).toFixed(0)+"ms");}
|
||||
console.log("[A2] D2 工作区采样(seed=777001)");
|
||||
zone("前伸工作区 (20,210,300)+-90mm",[20,210,300],90,60);'
|
||||
```
|
||||
(查看器默认区 `(150,128,260)±60mm` 把最后一行换成 `zone("查看器默认区 (150,128,260)+-60mm",[150,128,260],60,60);` 再跑一次。)
|
||||
|
||||
> 实测输出:
|
||||
> `[A1] N=400 oerr>0.05 数量=2 比例=0.50pct 均耗时=100ms` / `覆盖漏洞=1 真近奇异=1 最大静默=1.934deg 最大覆盖差=1.759deg`
|
||||
> `明细=[{"T":[152.6,-315.9,456.2],"oa":1.785,"ob":0.026,"gap":1.759},{"T":[403.7,162.3,599.9],"oa":1.934,"ob":1.994,"gap":-0.061}]`
|
||||
> `[前伸工作区] N=60 失败=0 覆盖漏洞=0 均耗时=98ms` / `[查看器默认区] N=60 失败=0 覆盖漏洞=0 均耗时=98ms`
|
||||
|
||||
### A3 / A4 — 兜底劣化与双优先级取舍(对应 §3.3、§6.3)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;const K=S.K;
|
||||
let s=60606>>>0;const rnd=()=>{s=(s+0x6D2B79F5)>>>0;let t=s;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296;};
|
||||
const rndQ=()=>O.map(id=>{const t=LIM[id];return t.cont?(rnd()*720-360):(t.lo+rnd()*(t.hi-t.lo));});
|
||||
const inLim=q=>{for(let i=0;i<5;i++){const t=LIM[O[i]];if(!(q[i]>=t.lo-1e-9&&q[i]<=t.hi+1e-9))return false;}return true;};
|
||||
const mul3=(X,Y)=>{const C=new Array(9);for(let i=0;i<3;i++)for(let j=0;j<3;j++)C[i*3+j]=X[i*3]*Y[j]+X[i*3+1]*Y[3+j]+X[i*3+2]*Y[6+j];return C;};
|
||||
const rotAx=(d,a)=>{const x=d[0],y=d[1],z=d[2],c=Math.cos(a),sn=Math.sin(a),C=1-c;return [x*x*C+c,x*y*C-z*sn,x*z*C+y*sn,y*x*C+z*sn,y*y*C+c,y*z*C-x*sn,z*x*C-y*sn,z*y*C+x*sn,z*z*C+c];};
|
||||
const T3=M=>[M[0],M[3],M[6],M[1],M[4],M[7],M[2],M[5],M[8]];
|
||||
/* 位置精确解在 (θ4,θ5) 处只定到 θ1..θ5;θ6 仍可自由选取。度量「可达到的最小姿态差」
|
||||
必须让 θ6 取最优(与求解器 rollClose 同口径:N = M5ᵀ·R_tar 应为 Rz(−θ6))。
|
||||
初版漏了这一步,用 θ6=0 度量会系统性高估最小姿态差(见文首勘误)。 */
|
||||
function oerrOpt(q,Rt){const M5=S.orient(q,5);const N=mul3(T3(M5),Rt);
|
||||
const th6=-Math.atan2(N[3],N[0])*180/Math.PI;
|
||||
return A.rotErrDeg(mul3(M5,rotAx([0,0,-1],th6*Math.PI/180)),Rt);}
|
||||
const nrm=v=>{const n=Math.hypot(v[0],v[1],v[2])||1;return [v[0]/n,v[1]/n,v[2]/n];};
|
||||
function met(q,Tp,Rt){const p=S.fk(q,0);return {pe:Math.hypot(p[0]-Tp[0],p[1]-Tp[1],p[2]-Tp[2]),oe:A.rotErrDeg(S.orient(q),Rt)};}
|
||||
function posBest(Tp,Rt,st){const Aa=Tp[0]-K.X0,Bq=Tp[1]-K.Y0,Br=Tp[2]-K.Z1;const Rr=Math.hypot(Bq,Br),ph=Math.atan2(Br,Bq);
|
||||
let be=999;for(let t4=-111;t4<=16;t4+=st)for(let t5=-95;t5<=34;t5+=st){const ss=S.branch(t4,t5,Aa,Rr,ph,0);
|
||||
for(const s0 of ss){if(!inLim(s0))continue;const p=S.fk(s0,0);if(Math.hypot(p[0]-Tp[0],p[1]-Tp[1],p[2]-Tp[2])>1e-6)continue;const e=oerrOpt(s0,Rt);if(e<be)be=e;}}return be;}
|
||||
function desc(Tp,Rt,k,starts){let best=null,bc=1e18;
|
||||
for(let st=0;st<starts;st++){let q=O.map((id,i)=>{const t=LIM[id];return t.lo+rnd()*(t.hi-t.lo);});
|
||||
let c0=(m=>m.pe*m.pe+k*m.oe*m.oe)(met(q,Tp,Rt));let step=20;
|
||||
for(let pass=0;pass<7;pass++){let mv=true,g=0;
|
||||
while(mv&&g++<200){mv=false;
|
||||
for(let i=0;i<6;i++){const t=LIM[O[i]];
|
||||
for(const sg of [1,-1]){const old=q[i];const v=old+sg*step;if(v<t.lo||v>t.hi)continue;q[i]=v;
|
||||
const m=met(q,Tp,Rt);const c1=m.pe*m.pe+k*m.oe*m.oe;if(c1<c0-1e-12){c0=c1;mv=true;}else q[i]=old;}}}
|
||||
step*=0.3;}
|
||||
if(c0<bc){bc=c0;best=q.slice();}}
|
||||
return best;}
|
||||
console.log("[A3] D3 姿态外推20deg 兜底劣化(seed=60606) 采样120个目标");
|
||||
let n=0,sd=0,mx=0,mn=1e9;const rows=[];
|
||||
for(let k=0;k<120;k++){const q=rndQ(),Tp=S.fk(q,0),R0=S.orient(q);const ax=nrm([rnd()-0.5,rnd()-0.5,rnd()-0.5]);const Rt=mul3(rotAx(ax,20*Math.PI/180),R0);
|
||||
const r=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rt}});
|
||||
if(r.reachable||r.oerr==null)continue;const ob=posBest(Tp,Rt,0.25);const dg=r.oerr-ob;
|
||||
n++;sd+=dg;if(dg>mx)mx=dg;if(dg<mn)mn=dg;if(rows.length<8)rows.push({T:Tp.map(v=>+v.toFixed(0)),base:+r.oerr.toFixed(1),best:+ob.toFixed(2),degr:+dg.toFixed(1)});}
|
||||
console.log(" 不可达样本="+n+" 平均劣化="+(n?sd/n:0).toFixed(1)+"deg 最大="+mx.toFixed(1)+" 最小="+(n?mn:0).toFixed(1));
|
||||
console.log(" 明细="+JSON.stringify(rows));
|
||||
console.log("[A4] D6 双优先级取舍(seed=60606) 同批目标");
|
||||
let n6=0,aBase=0,aPos=0,aOri=0,aOriP=0;
|
||||
for(let k=0;k<120&&n6<12;k++){const q=rndQ(),Tp=S.fk(q,0),R0=S.orient(q);const ax=nrm([rnd()-0.5,rnd()-0.5,rnd()-0.5]);const Rt=mul3(rotAx(ax,20*Math.PI/180),R0);
|
||||
const r=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rt}});
|
||||
if(r.reachable||r.oerr==null||r.oerr<5)continue;
|
||||
const pb=posBest(Tp,Rt,0.25);const qo=desc(Tp,Rt,1e3,120);const mo=met(qo,Tp,Rt);
|
||||
n6++;aBase+=r.oerr;aPos+=pb;aOri+=mo.oe;aOriP+=mo.pe;
|
||||
console.log(" T=("+Tp.map(v=>v.toFixed(0)).join(",")+") closest6="+r.oerr.toFixed(1)+"deg/"+(r.err!=null?r.err.toFixed(1):"--")+"mm | 位置优先="+pb.toFixed(2)+"deg/0.0mm | 姿态优先="+mo.oe.toFixed(2)+"deg/"+mo.pe.toFixed(1)+"mm");}
|
||||
console.log(" 小结 采样="+n6+" 均值 closest6="+(n6?aBase/n6:0).toFixed(1)+"deg 位置优先="+(n6?aPos/n6:0).toFixed(1)+"deg 姿态优先="+(n6?aOri/n6:0).toFixed(2)+"deg(位置"+(n6?aOriP/n6:0).toFixed(1)+"mm)");'
|
||||
```
|
||||
(`posBest` 即"位置优先真最优"的网格枚举(θ6 取最优口径),`desc(Tp,Rt,1e3,120)` 即"姿态优先"的惩罚坐标下降;A4 的分组与 A3 不同是因为 `desc` 额外消耗了随机数。)
|
||||
|
||||
> 实测输出(勘误后重跑):
|
||||
> `[A3] 不可达样本=38 平均劣化=106.3deg 最大=176.7 最小=16.1`
|
||||
> `明细=[{"T":[364,336,843],"base":114.2,"best":5.98,"degr":108.2},{"T":[288,-16,595],"base":166.8,"best":7.94,"degr":158.9},{"T":[72,376,161],"base":107.2,"best":5.29,"degr":101.9},{"T":[311,10,701],"base":138.2,"best":14.72,"degr":123.4},{"T":[-52,242,897],"base":121.1,"best":3.18,"degr":117.9},{"T":[4,-218,303],"base":156.3,"best":13.02,"degr":143.3},{"T":[279,-155,765],"base":173,"best":3.13,"degr":169.9},{"T":[402,202,425],"base":81.2,"best":16.45,"degr":64.8}]`
|
||||
> `[A4] 采样=12 均值 closest6=115.3deg 位置优先=5.7deg 姿态优先=0.04deg(位置11.6mm)`
|
||||
|
||||
### F — 静默近失案例与步长敏感度(对应 §6.1)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;const K=S.K;
|
||||
let s=424242>>>0;const rnd=()=>{s=(s+0x6D2B79F5)>>>0;let t=s;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296;};
|
||||
const rndQ=()=>O.map(id=>{const t=LIM[id];return t.cont?(rnd()*720-360):(t.lo+rnd()*(t.hi-t.lo));});
|
||||
const inLim=q=>{for(let i=0;i<5;i++){const t=LIM[O[i]];if(!(q[i]>=t.lo-1e-9&&q[i]<=t.hi+1e-9))return false;}return true;};
|
||||
let F=null;for(let k=0;k<4000&&!F;k++){const q=rndQ(),Tp=S.fk(q,0),R=S.orient(q);const a=S.solve(Tp,{step:0.25,tol:1e-6,pose:{R:R}});if(a.q&&a.oerr>1.0)F={q,T:Tp,R};}
|
||||
console.log("真解 q="+F.q.map(v=>+v.toFixed(2)).join(",")+" T="+F.T.map(v=>+v.toFixed(3)).join(","));
|
||||
let out=[];for(const st of [0.5,0.4,0.3,0.25,0.2,0.15,0.1,0.05]){const r=S.solve(F.T,{step:st,tol:1e-6,pose:{R:F.R}});out.push(st+"->"+(r.q?r.oerr.toFixed(4):"null"));}
|
||||
console.log("步长敏感度 oerr: "+out.join(" "));
|
||||
const rw=S.solveTo(F.T,{step:0.25,tol:1e-6,pose:{R:F.R},warm:F.q});
|
||||
console.log("warm=trueQ: oerr="+(rw.oerr!=null?rw.oerr.toExponential(2):"null"));
|
||||
const Aa=F.T[0]-K.X0,Bq=F.T[1]-K.Y0,Br=F.T[2]-K.Z1;const Rr=Math.hypot(Bq,Br),ph=Math.atan2(Br,Bq);
|
||||
for(const c of S.branch(F.q[3],F.q[4],Aa,Rr,ph,0)){const p=S.fk(c,0);console.log(" branch posErr="+Math.hypot(p[0]-F.T[0],p[1]-F.T[1],p[2]-F.T[2]).toExponential(1)+"mm θ1..θ5="+c.slice(0,5).map(v=>+v.toFixed(2)).join(",")+" inLim="+inLim(c));}'
|
||||
```
|
||||
|
||||
> 实测输出:
|
||||
> `真解 q=198.03,-46.76,-180.52,-92.79,33.48,102.36 T=26.386,-3.334,1019.998`
|
||||
> `步长敏感度 oerr: 0.5->1.9881 0.4->1.9119 0.3->0.0307 0.25->1.9881 0.2->1.9881 0.15->1.9652 0.1->0.0258 0.05->0.0491`
|
||||
> `warm=trueQ: oerr=1.21e-6`(真解确实存在)
|
||||
> `branch posErr=1.4e-13mm ... inLim=true`(退化处仍有在限位内的位置精确支)
|
||||
|
||||
### O2 — 严格真最优基准(θ6 解析优化,对应 §6.3)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;const K=S.K;
|
||||
let s=60606>>>0;const rnd=()=>{s=(s+0x6D2B79F5)>>>0;let t=s;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296;};
|
||||
const rndQ=()=>O.map(id=>{const t=LIM[id];return t.cont?(rnd()*720-360):(t.lo+rnd()*(t.hi-t.lo));});
|
||||
const inLim=q=>{for(let i=0;i<5;i++){const t=LIM[O[i]];if(!(q[i]>=t.lo-1e-9&&q[i]<=t.hi+1e-9))return false;}return true;};
|
||||
const mul3=(X,Y)=>{const C=new Array(9);for(let i=0;i<3;i++)for(let j=0;j<3;j++)C[i*3+j]=X[i*3]*Y[j]+X[i*3+1]*Y[3+j]+X[i*3+2]*Y[6+j];return C;};
|
||||
const rotAx=(d,a)=>{const x=d[0],y=d[1],z=d[2],c=Math.cos(a),sn=Math.sin(a),C=1-c;return [x*x*C+c,x*y*C-z*sn,x*z*C+y*sn,y*x*C+z*sn,y*y*C+c,y*z*C-x*sn,z*x*C-y*sn,z*y*C+x*sn,z*z*C+c];};
|
||||
const T3=M=>[M[0],M[3],M[6],M[1],M[4],M[7],M[2],M[5],M[8]];
|
||||
const nrm=v=>{const n=Math.hypot(v[0],v[1],v[2])||1;return [v[0]/n,v[1]/n,v[2]/n];};
|
||||
/* θ6 取最优口径(同文首勘误与 A3 的 oerrOpt) */
|
||||
function oerrOpt(q,Rt){const M5=S.orient(q,5);const N=mul3(T3(M5),Rt);
|
||||
const th6=-Math.atan2(N[3],N[0])*180/Math.PI;
|
||||
return A.rotErrDeg(mul3(M5,rotAx([0,0,-1],th6*Math.PI/180)),Rt);}
|
||||
function oracle(Tp,Rt,st){const Aa=Tp[0]-K.X0,Bq=Tp[1]-K.Y0,Br=Tp[2]-K.Z1;const Rr=Math.hypot(Bq,Br),ph=Math.atan2(Br,Bq);
|
||||
let b1=999;
|
||||
for(let t4=-111;t4<=16;t4+=st)for(let t5=-95;t5<=34;t5+=st){const ss=S.branch(t4,t5,Aa,Rr,ph,0);
|
||||
for(const s0 of ss){if(!inLim(s0))continue;const p=S.fk(s0,0);if(Math.hypot(p[0]-Tp[0],p[1]-Tp[1],p[2]-Tp[2])>1e-6)continue;const e=oerrOpt(s0,Rt);if(e<b1)b1=e;}}
|
||||
return {opt6:b1};}
|
||||
let n=0,a=0,c=0,dom=0;
|
||||
for(let k=0;k<120&&n<12;k++){const q=rndQ(),Tp=S.fk(q,0),R0=S.orient(q);const ax=nrm([rnd()-0.5,rnd()-0.5,rnd()-0.5]);const Rt=mul3(rotAx(ax,20*Math.PI/180),R0);
|
||||
const r=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rt}});
|
||||
if(r.reachable||r.oerr==null||r.oerr<5)continue;n++;
|
||||
const o=oracle(Tp,Rt,0.5);
|
||||
console.log(" ("+Tp.map(v=>v.toFixed(0)).join(",")+") closest6="+r.oerr.toFixed(1)+"deg 真最优="+o.opt6.toFixed(2)+"deg 劣化="+(r.oerr-o.opt6).toFixed(1)+"deg");
|
||||
a+=r.oerr;c+=o.opt6;if(r.oerr>o.opt6)dom++;}
|
||||
console.log(" 均值 closest6="+(a/n).toFixed(1)+"deg 真最优="+(c/n).toFixed(1)+"deg 平均劣化="+((a-c)/n).toFixed(1)+"deg 被支配="+dom+"/"+n);'
|
||||
```
|
||||
(O2 与 A4 目标集不同:`desc()` 会额外消耗随机数,故两者 close6 均值略有差异。两者均固定种子、均可复现。)
|
||||
|
||||
> 实测输出(勘误后重跑):
|
||||
> `(364,336,843) closest6=114.2deg 真最优=5.98deg 劣化=108.2deg` … `(137,-315,829) closest6=133.5deg 真最优=6.20deg 劣化=127.3deg`
|
||||
> `均值 closest6=118.9deg 真最优=8.6deg 平均劣化=110.3deg 被支配=12/12`
|
||||
|
||||
### D5 — 越界目标兜底(对应 §5.2、§6.2)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;const K=S.K;
|
||||
const inLim=q=>{for(let i=0;i<5;i++){const t=LIM[O[i]];if(!(q[i]>=t.lo-1e-9&&q[i]<=t.hi+1e-9))return false;}return true;};
|
||||
const mul3=(X,Y)=>{const C=new Array(9);for(let i=0;i<3;i++)for(let j=0;j<3;j++)C[i*3+j]=X[i*3]*Y[j]+X[i*3+1]*Y[3+j]+X[i*3+2]*Y[6+j];return C;};
|
||||
const rotAx=(d,a)=>{const x=d[0],y=d[1],z=d[2],c=Math.cos(a),sn=Math.sin(a),C=1-c;return [x*x*C+c,x*y*C-z*sn,x*z*C+y*sn,y*x*C+z*sn,y*y*C+c,y*z*C-x*sn,z*x*C-y*sn,z*y*C+x*sn,z*z*C+c];};
|
||||
const T3=M=>[M[0],M[3],M[6],M[1],M[4],M[7],M[2],M[5],M[8]];
|
||||
function oerrOpt(q,Rt){const M5=S.orient(q,5);const N=mul3(T3(M5),Rt);
|
||||
const th6=-Math.atan2(N[3],N[0])*180/Math.PI;
|
||||
return A.rotErrDeg(mul3(M5,rotAx([0,0,-1],th6*Math.PI/180)),Rt);}
|
||||
function posBest(Tp,Rt,st){const Aa=Tp[0]-K.X0,Bq=Tp[1]-K.Y0,Br=Tp[2]-K.Z1;const Rr=Math.hypot(Bq,Br),ph=Math.atan2(Br,Bq);
|
||||
let be=999,cnt=0;for(let t4=-111;t4<=16;t4+=st)for(let t5=-95;t5<=34;t5+=st){const ss=S.branch(t4,t5,Aa,Rr,ph,0);
|
||||
for(const s0 of ss){if(!inLim(s0))continue;const p=S.fk(s0,0);if(Math.hypot(p[0]-Tp[0],p[1]-Tp[1],p[2]-Tp[2])>1e-6)continue;cnt++;const e=oerrOpt(s0,Rt);if(e<be)be=e;}}
|
||||
return {e:be,cnt:cnt};}
|
||||
const Rs=A.poseMat(25,95,10);
|
||||
for(const Tp of [[20,400,300],[150,128,600],[400,300,200],[0,0,1200]]){
|
||||
const o=posBest(Tp,Rs,0.5);const t0=Date.now();const r=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rs}});const ms=Date.now()-t0;
|
||||
const rp=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rs},priority:"pos"});
|
||||
const ro=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rs},priority:"ori"});
|
||||
console.log("T=("+Tp.join(",")+") 位置精确解="+o.cnt+" 真最优姿态差="+(o.cnt?o.e.toFixed(2)+"deg":"n/a"));
|
||||
console.log(" 默认DLS "+ms+"ms 位置="+(r.err!=null?r.err.toFixed(1):"--")+"mm 姿态差="+(r.oerr!=null?r.oerr.toFixed(1):"--")+"deg");
|
||||
console.log(" 位置优先 位置="+(rp.err!=null?rp.err.toExponential(1):"--")+"mm 姿态差="+(rp.oerr!=null?rp.oerr.toFixed(2):"--")+"deg");
|
||||
console.log(" 姿态优先 位置="+(ro.err!=null?ro.err.toFixed(2):"--")+"mm 姿态差="+(ro.oerr!=null?ro.oerr.toFixed(4):"--")+"deg");}'
|
||||
```
|
||||
|
||||
> 实测输出(勘误后重跑):
|
||||
> `T=(20,400,300) 位置精确解=11920 真最优姿态差=44.20deg`
|
||||
> ` 默认DLS 位置=0.0mm 姿态差=136.7deg | 位置优先 位置=1.3e-13mm 姿态差=44.19deg | 姿态优先 位置=116.43mm 姿态差=0.0000deg`
|
||||
> `T=(150,128,600) 位置精确解=2580 真最优姿态差=14.72deg`
|
||||
> ` 默认DLS 位置=0.0mm 姿态差=173.4deg | 位置优先 位置=0.0e+0mm 姿态差=14.72deg | 姿态优先 位置=37.40mm 姿态差=0.0000deg`
|
||||
> `T=(400,300,200) 位置精确解=0 | 默认DLS 位置=104.0mm 姿态差=172.0deg | 姿态优先 位置=404.27mm 姿态差=0.0000deg`(位置不可达)
|
||||
> `T=(0,0,1200) 位置精确解=0 | 默认DLS 位置=87.1mm 姿态差=130.0deg | 姿态优先 位置=205.67mm 姿态差=0.0000deg`(位置不可达)
|
||||
|
||||
> **复现说明**:非耗时类数值(残差、姿态差、可达性、候选数)逐位可复现;`ms` 随机器负载有 ±20% 波动。
|
||||
|
||||
### V1 — 位置可达性/候选数(对应 §6.2)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;const K=S.K;
|
||||
const inLim=q=>{for(let i=0;i<5;i++){const t=LIM[O[i]];if(!(q[i]>=t.lo-1e-9&&q[i]<=t.hi+1e-9))return false;}return true;};
|
||||
const mul3=(X,Y)=>{const C=new Array(9);for(let i=0;i<3;i++)for(let j=0;j<3;j++)C[i*3+j]=X[i*3]*Y[j]+X[i*3+1]*Y[3+j]+X[i*3+2]*Y[6+j];return C;};
|
||||
const rotAx=(d,a)=>{const x=d[0],y=d[1],z=d[2],c=Math.cos(a),sn=Math.sin(a),C=1-c;return [x*x*C+c,x*y*C-z*sn,x*z*C+y*sn,y*x*C+z*sn,y*y*C+c,y*z*C-x*sn,z*x*C-y*sn,z*y*C+x*sn,z*z*C+c];};
|
||||
const T3=M=>[M[0],M[3],M[6],M[1],M[4],M[7],M[2],M[5],M[8]];
|
||||
function oerrOpt(q,Rt){const M5=S.orient(q,5);const N=mul3(T3(M5),Rt);
|
||||
const th6=-Math.atan2(N[3],N[0])*180/Math.PI;
|
||||
return A.rotErrDeg(mul3(M5,rotAx([0,0,-1],th6*Math.PI/180)),Rt);}
|
||||
const Tp=[20,400,300],Rt=A.poseMat(25,95,10);
|
||||
const Aa=Tp[0]-K.X0,Bq=Tp[1]-K.Y0,Br=Tp[2]-K.Z1;const Rr=Math.hypot(Bq,Br),ph=Math.atan2(Br,Bq);
|
||||
let cnt=0,be=999;
|
||||
for(let t4=-111;t4<=16;t4+=0.5)for(let t5=-95;t5<=34;t5+=0.5){const ss=S.branch(t4,t5,Aa,Rr,ph,0);
|
||||
for(const s0 of ss){if(!inLim(s0))continue;const p=S.fk(s0,0);if(Math.hypot(p[0]-Tp[0],p[1]-Tp[1],p[2]-Tp[2])>1e-6)continue;cnt++;const e=oerrOpt(s0,Rt);if(e<be)be=e;}}
|
||||
console.log("位置精确解数量="+cnt+" 其中最小姿态差(θ6最优)="+be.toFixed(2)+"deg");
|
||||
const rp=S.solve(Tp,{step:0.25,tol:1e-6});
|
||||
console.log("无姿态 solve: ok="+rp.ok+" 位置误差="+(rp.err!=null?rp.err.toExponential(1):"--")+"mm");
|
||||
const r6=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rt}});
|
||||
console.log("带姿态 solveTo(默认): reachable="+r6.reachable+" 位置="+(r6.err!=null?r6.err.toFixed(1):"--")+"mm 姿态差="+(r6.oerr!=null?r6.oerr.toFixed(1):"--")+"deg");
|
||||
const r5=S.solveTo(Tp,{step:0.25,tol:1e-6,pose:{R:Rt},priority:"pos"});
|
||||
console.log("带姿态 solveTo(位置优先): reachable="+r5.reachable+" 位置="+(r5.err!=null?r5.err.toExponential(1):"--")+"mm 姿态差="+(r5.oerr!=null?r5.oerr.toFixed(2):"--")+"deg");'
|
||||
```
|
||||
|
||||
> 实测输出(勘误后重跑):
|
||||
> `位置精确解数量=11920 其中最小姿态差(θ6最优)=44.20deg`
|
||||
> `无姿态 solve: ok=true 位置误差=2.5e-14mm`
|
||||
> `带姿态 solveTo(默认): reachable=false 位置=0.0mm 姿态差=136.7deg`
|
||||
> `带姿态 solveTo(位置优先): reachable=true 位置=1.3e-13mm 姿态差=44.19deg`
|
||||
|
||||
### D4b — 近伸直构型(对应 §6.1)
|
||||
|
||||
```
|
||||
node -e 'const fs=require("fs");const A=require("./web/arm-ik-solver.js");
|
||||
const mesh=JSON.parse(fs.readFileSync("./web/mesh_arm.json","utf8"));const tr=JSON.parse(fs.readFileSync("./docs/travel.json","utf8"));
|
||||
const J={},T={};for(const j of mesh.joints)J[j.id]={rear:j.rear,d:j.d};for(const t of tr)T[t.axis]={lo:t.lo,hi:t.hi,continuous:!!t.continuous};
|
||||
const S=A.make({joints:J,travel:T,tcp:[18.5,128,93]});const O=A.ORDER;const LIM=S.LIM;
|
||||
let s2=909090>>>0;const rnd2=()=>{s2=(s2+0x6D2B79F5)>>>0;let t=s2;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return ((t^(t>>>14))>>>0)/4294967296;};
|
||||
const rndQ2=()=>O.map(id=>{const t=LIM[id];return t.cont?(rnd2()*720-360):(t.lo+rnd2()*(t.hi-t.lo));});
|
||||
let N=30,nFail=0,nGap=0,maxS=0,maxG=0,worst=[];
|
||||
for(let k=0;k<N;k++){const q=rndQ2();const m=rnd2();
|
||||
if(m<0.5)q[2]=rnd2()*6-3; else q[2]=(rnd2()<0.5?180:-180)+(rnd2()*6-3);
|
||||
const Tp=S.fk(q,0),R=S.orient(q);
|
||||
const a=S.solve(Tp,{step:0.25,tol:1e-6,pose:{R:R}}),b=S.solve(Tp,{step:0.05,tol:1e-6,pose:{R:R}});
|
||||
const va=a.q?a.oerr:999,vb=b.q?b.oerr:999;
|
||||
if(va>0.05)nFail++;if(va>0.05){if(va>maxS)maxS=va;if(va-vb>0.05){nGap++;if(va-vb>maxG)maxG=va-vb;worst.push({T:Tp.map(v=>+v.toFixed(1)),oa:+va.toFixed(3),ob:+vb.toFixed(3)});}}}
|
||||
console.log("[D4b] N="+N+" 失败="+nFail+" 覆盖漏洞="+nGap+" 最大静默="+maxS.toFixed(3)+"deg 最大覆盖差="+maxG.toFixed(3));
|
||||
console.log("样本="+JSON.stringify(worst));'
|
||||
```
|
||||
|
||||
> 实测输出:`[D4b] N=30 失败=1 覆盖漏洞=1 最大静默=1.860deg 最大覆盖差=1.822`
|
||||
> `样本=[{"T":[240.7,45.3,518.6],"oa":1.86,"ob":0.038}]`
|
||||
|
||||
### 既有自检与性能基准(对应 §5.1、§6.5)
|
||||
|
||||
```
|
||||
node scratch/pose_ik_check.js
|
||||
```
|
||||
> 实测:7 项全部通过(明细见 §6.5)。注意其内部使用 `Math.random()`,为单次运行示例。
|
||||
|
||||
```
|
||||
node -e 'const j=require("./scratch/bench_ik_split.json");console.log("grid",JSON.stringify(j.grid));console.log("summary",JSON.stringify(j.summary,null,1));console.log("n",j.rows.length,"ms max",Math.max(...j.rows.map(r=>r.ms)).toFixed(0));'
|
||||
```
|
||||
> 实测:`grid {"g4":509,"g5":517}` · `summary {medMs:107.1, p95Ms:242.5, medPos:73.6, medPose:37.2, medShare:33.3, medOff:73.4, escRate:0.025}` · `n 40 ms max 299`
|
||||
|
||||
---
|
||||
|
||||
## 10. 术语表
|
||||
|
||||
| 术语 | 含义 |
|
||||
| :--- | :--- |
|
||||
| TCP | 工具中心点,此处指工具法兰安装面中心(`tcp=[18.5,128,93]`) |
|
||||
| `oerr` | 姿态残差(度),工具坐标系与目标姿态的旋转夹角 |
|
||||
| 位置精确解 | 由 `branch()` 闭式得到、位置误差 ~1e-13 mm 的关节解(θ1..θ3 由 (θ4,θ5) 决定) |
|
||||
| 退化构型 / 奇异刀尖 | 位置雅可比奇异、`branch()` 的 4 支两两重合的构型(本案为 θ3≈0 或 ±180) |
|
||||
| `POSETOL` | 姿态接受的粗扫阈值,2.0°;超阈候选现被直接丢弃 |
|
||||
| `closest6` | 6 维阻尼最小二乘"最近位姿"兜底(160 随机重启 × 160 迭代) |
|
||||
| 覆盖漏洞 | 精确解存在但求解器未找到(静默返回 >0.05° 残差) |
|
||||
| 被支配(Pareto-dominated) | 存在另一个解在位置与姿态两个指标上都不劣于此解 |
|
||||
@@ -0,0 +1,206 @@
|
||||
# 机器人手臂3D查看器 · 性能优化报告
|
||||
|
||||
- 目标文件:`机器人手臂3D查看器.html`(15,314,130 → 15,316,745 字节)
|
||||
- 优化日期:2026-09-29
|
||||
- 优化前版本标识:`v1_2026-09-29_2151`(原件最后写入时间)
|
||||
- 备份:`backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html`
|
||||
- SHA-256(原件=备份,已校验一致):`1BDA568269842C403D0FB5D63B7610818BE2DDC28C3E7B9B90498349A3B1252E`
|
||||
- 优化后 SHA-256:`4F672CD8A66107D3EFDE6CD55FDC2C2CD878CBF64140F8497FB8CA3F2BC9B57D`
|
||||
- 回滚:`Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html`(原件为只读属性,已按原状恢复)
|
||||
|
||||
---
|
||||
|
||||
## 一、摘要(实测数据)
|
||||
|
||||
同一台机器、同一个浏览器(Edge,160 Hz 显示器)、同一份插桩脚本,改前 / 改后各测 2 次:
|
||||
|
||||
| 指标 | 改前 | 改后 | 变化 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| **空闲 5 s 渲染次数** | 800 / 800 | **0 / 0** | **−100%** |
|
||||
| 空闲渲染 CPU 时长 | 271.4 / 289.9 ms | **0 / 0 ms** | −100% |
|
||||
| 单次滑块输入后 500 ms 渲染次数 | 82 / 82 | **1 / 1** | −98.8% |
|
||||
| 进入「运动模拟」后 4 s 渲染次数 | 626 / 617 | **103 / 104** | −83.5% |
|
||||
| 按钮求解后 4 s 渲染次数 | 626 / 622 | **103 / 102** | −83.6% |
|
||||
| 30 次 pointermove 的射线拾取次数 | 30 / 30 | **9 / 9** | −70% |
|
||||
| 30 次 pointermove 的拾取总耗时 | 13.1 / 19.3 ms | **5.2 / 4.4 ms** | −60% ~ −77% |
|
||||
| 索引缓冲内存 | 4,123,320 B(Uint32×92) | **2,061,660 B(Uint16×92)** | **−2.00 MiB(−50%)** |
|
||||
| draw call / 三角面 | 92 / 340,178 | 92 / 340,178 | 不变(画质不变) |
|
||||
| 约 20 s 会话总渲染次数 | 2576 / 2564 | **299 / 296** | −88.4% |
|
||||
| 约 20 s 会话渲染 CPU 总时长 | 1114 / 1267.9 ms | **143.9 / 205.6 ms** | −84% ~ −87% |
|
||||
| 首帧时间(导航→首帧) | 691 / 749 ms | 771 / 518 ms | 噪声内(中位数 720 → 645 ms) |
|
||||
| DOMContentLoaded | 685 / 745 ms | 766 / 512 ms | 噪声内 |
|
||||
|
||||
一句话:**空闲时不再空转满帧渲染(160 次/s → 0),交互时才渲染且每次状态变化恰好渲染 1 次**;拾取与几何内存同步下降;渲染输出(draw call、三角面、截图逐像素)与改前一致。
|
||||
|
||||
功能验证:改前 / 改后跑同一份 18.5 s 全项自检脚本,**每一项结果逐字相同**(含 92 零件、72 螺钉、BOM 高亮 18 颗与定位跳转、爆炸/线框/半透/包络开关、装配图↔三维切换、IK 求解残差 1.1e-13 / 1.8e-13 mm、姿态控制、右键隐藏与撤销)。截图对比:`scratch/perf/shots/before_0*.png` 与 `after_0*.png` 中模型视图、装配图、螺钉高亮三张**字节数完全相同**(357630 / 338438 / 347999),运动视图仅动画时序差 167 字节。
|
||||
|
||||
---
|
||||
|
||||
## 二、改动清单
|
||||
|
||||
共 6 处代码改动(成品 HTML 内 19 个 diff 块),全部集中在查看器主逻辑与 IK glue,**未触碰任何数据行与求解器脚本**。
|
||||
|
||||
### 1. 按需渲染(核心)
|
||||
|
||||
- 问题:`loop()` 无条件每次 rAF 都 `resize() + updCam() + rend.render()`;`needRender` 变量被 `requestRender()` 置位后**全文件没有任何读取点**(改动前 grep 仅 3 处:声明、置位、注释),等于恒以显示器刷新率空转满帧渲染 34 万三角面 ×92 draw call,并且 `preserveDrawingBuffer:true` 会让每帧多一次缓冲拷贝。
|
||||
- 改法:
|
||||
|
||||
```js
|
||||
function loop() {
|
||||
if (needRender && cv.style.display !== 'none') {
|
||||
needRender = false;
|
||||
resize();
|
||||
updCam();
|
||||
rend.render(scene, cam);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
```
|
||||
|
||||
- 关键约束:**保留 `preserveDrawingBuffer:true`**(不重绘时靠它保住最后一帧画面);**保留 rAF 循环**(入场动画 `animTo`、实时追踪、拖拽都依赖后续帧渲染);装配图页画布隐藏时**不清标志**,切回三维页由 tab 处理器 `resize()+requestRender()` 补渲染。
|
||||
- 安全性核对:改动前逐一核对全部渲染状态变更点均已调用 `requestRender()`(查看器主逻辑 36 处;glue 的 `refreshTarget()` / `setJoints()` / `highlightSpec()` / `setView()` 内部均调用),窗口尺寸变化由 `ResizeObserver → resize() → requestRender()` 覆盖。
|
||||
- 顺带收益:`resize()` 读 `stage.clientWidth/clientHeight` 会造成强制布局,旧版每帧都读,现在只在真正渲染前读一次。
|
||||
|
||||
### 2. 悬浮拾取限流 + 零分配
|
||||
|
||||
- 问题:射线要对 34 万三角面全量求交(实测 0.5~1.5 ms/次),旧版每个 `pointermove` 都算(rAF 合帧后仍≈刷新率次数);且每次 `meshes.filter(...)` 都新建数组 + `new THREE.Vector2`。
|
||||
- 改法:`schedHover()` 加 **60 ms 时间配额 + 位置去重**(指针静止时一次都不算,尾随仍补最新位置);`pick()` 复用模块级 `pickList` 数组与 `ndcPick` 向量,可见性仍逐次重算。
|
||||
- 注意:three r159 的 `Raycaster` **不检查 `object.visible`**(已在 `web/three_inline.js` 源码确认),因此可见性过滤必须保留。
|
||||
- 点击 / 右键拾取不经过 `schedHover`,仍是即时求交,交互判定不受影响。
|
||||
|
||||
### 3. `applyJoint()` 去分配
|
||||
|
||||
- 问题:每次调用新建 2 个 `Vector3` + 3 个 `Matrix4`,且用 `JO.find` 线性查找;一次关节写入 = 6 次调用 = 30 个临时对象,动画/实时追踪每帧一批。
|
||||
- 改法:轴心 `JP` / 轴向 `JD` 预计算一次;矩阵合成改用 3 个共享 `Matrix4` 实例。数学与原式逐位相同(`T(P)·R(D,θ)·T(−P)`)——场景图残差复算结果与改前完全一致(1.1e-13 / 1.8e-13 mm)即为证据。
|
||||
|
||||
### 4. 索引缓冲降位(Uint16)
|
||||
|
||||
- 问题:92 件零件索引一律 `Uint32Array`。
|
||||
- 改法:按顶点数选择 `p.v.length <= 65535 ? Uint16Array : Uint32Array`(实测单件最大顶点 47,483,全部走 Uint16),索引内存 4.08 MB → 2.00 MB。
|
||||
|
||||
### 5. BOM 高亮只记一行
|
||||
|
||||
- 问题:点选 / ESC 时 `for (const tr of tb.children) tr.style.background = ''` 把整张表(13 行零件 + 10 行螺钉)刷一遍。
|
||||
- 改法:新增 `bomHlRow` / `bomHlClear()`,只清理上一次高亮的那一行,设置高亮时同步记录。可见结果完全一致。
|
||||
|
||||
### 6. Worker Blob URL 回收
|
||||
|
||||
- 问题:`new Worker(URL.createObjectURL(new Blob([...])))` 不回收,常驻一份 ~86 KB Blob;Worker 构造失败路径同样泄漏。
|
||||
- 改法:先取 URL,`try { new Worker(url) } finally { URL.revokeObjectURL(url) }`(URL 只在构造时被读取)。
|
||||
|
||||
---
|
||||
|
||||
## 三、验证方法与证据
|
||||
|
||||
### 3.1 静态校验(`scratch/perf/verify_edit.py`)
|
||||
|
||||
- 4 个内联 `<script>` 的 SHA-256 对比:
|
||||
- `[0] three.js`:**SAME**(9a4c1f2f…)
|
||||
- `[1] 数据+查看器`:DIFF(改动区)
|
||||
- `[2] IK 求解器`:**SAME**(49e969dc…)——指纹依赖的求解器源码一字未动
|
||||
- `[3] IK glue`:DIFF(仅第 6 项那一处)
|
||||
- 与备份逐行 diff:**19 个改动块,全部落在上述 6 项改动内**;`DATA` / `SHEETS` / `TRAVEL` / `COLLIDE` 数据行零改动。
|
||||
- `node --check`:改后的查看器主脚本(14.1 MB)与 glue 脚本均语法通过;模板补丁后的脚本同样通过。
|
||||
|
||||
### 3.2 运行期一致性
|
||||
|
||||
| 检查项 | 改前 | 改后 |
|
||||
| --- | --- | --- |
|
||||
| 预置自检指纹(`chkSig()` 当前值) | `3e03f22b` | **`3e03f22b`(未变)** |
|
||||
| IK 残差(闭式 / 场景图) | 1.1e-13 / 1.8e-13 mm | **1.1e-13 / 1.8e-13 mm** |
|
||||
| 零件 / 螺钉 / draw call / 三角面 | 92 / 72 / 92 / 340,178 | 相同 |
|
||||
| 关节矩阵元(J2 滑块 25°→夹到 7.867°) | 7.1441 | 7.1441 |
|
||||
|
||||
### 3.3 功能对照自检(`scratch/perf/func_check.js`,18.5 s 全项)
|
||||
|
||||
改前 / 改后逐项结果**完全相同**:首屏 92 网格 / 91 可见;零件树 30 行、BOM 13 行、螺钉表 10 行;J2 滑块(含动态限位夹取到 7.867°)与画面重绘;爆炸 60% 最大位移 129.6 mm 且「装配视图」回位;线框 91 开 / 0 关;电机半透 0.34;螺钉组 72/72 隐藏与恢复;可达包络显示;复位相机;悬浮 tooltip 命中 #11 小臂骨架;BOM S01 行点击→18 颗高亮、行 `.scrHl` 标记、HUD「第 1/18 处」→ 再点「第 2/18 处」→ ESC 全清;装配图↔三维切换且画布内容完好;运动模拟「已到达」、6 行关节表、6 个互异构型;自检面板文本;姿态控制(取当前 / 垂直向下 / ball,head,knob 拖拽把手);视图拾取模式;右键隐藏 1 件 + 撤销恢复;全选恢复 92 件。
|
||||
|
||||
截图:`scratch/perf/shots/`(before/after 各 4 张)。
|
||||
|
||||
### 3.3b 交互路径补充探针(`scratch/perf/interact_check.js`,改前 / 改后各跑 1 次)
|
||||
|
||||
针对"按需渲染是否会漏帧"这一风险点,逐条验证交互后的即时重绘(画布 64×36 降采样指纹对比):
|
||||
|
||||
| 交互 | 改前 | 改后 |
|
||||
| --- | --- | --- |
|
||||
| 轨道拖拽(pointerdown+10×pointermove) | Δaz −0.64、Δpo −0.213、画面重绘 ✓ | 完全相同 |
|
||||
| 滚轮缩放 | Δrd −159.9、画面重绘 ✓ | 完全相同 |
|
||||
| 爆炸滑块连续拖拽(7 次 input) | 最大偏移 226.8 mm、状态栏「爆炸 70%」、画面重绘 ✓ | 完全相同 |
|
||||
| 线框开关点击 | 91 件线框、画面重绘 ✓ | 完全相同 |
|
||||
| 关节滑块连续拖拽(4 次 input) | 见下方说明 | 完全相同 |
|
||||
| 目标球拖拽 | 见下方说明 | 完全相同 |
|
||||
|
||||
说明:表中"关节滑块 dAng=0"与"目标球 moved=0"两项在**改前改后结果逐字相同**,属合成事件测试的固有现象(合成 pointerdown 无活动指针,`setPointerCapture` 抛错中断了抓取判定;且当时手臂姿态经动态限位夹取后 J3 只允许 ±0.06°),**不是回归**——同一探针在改前文件上得到同一结果即为证据。真实鼠标操作不受此影响(`screenshot` 与 func_check 中的姿态控制、拖拽把手、视图拾取等均已通过)。
|
||||
|
||||
### 3.3c URL 参数脚本化视图(`?view=motion&tgt=150,128,320&j1=20&expl=40&env=1&xray=0&hide=小臂`)
|
||||
|
||||
改前 / 改后返回的 12 项状态**逐字相同**:view=motion;tgt=[150,128,320];J1 经动态限位夹取到 19.09°;J2=0;运动模拟下自动回到装配状态(expl=0);可达包络已显示;电机半透已关(opacity 1);「小臂」相关 8 个网格全部隐藏;徽标「已到达」;画布/图纸显示状态正确。
|
||||
|
||||
### 3.4 性能插桩(`scratch/perf/harness.js` + `make_measure.py`)
|
||||
|
||||
插桩副本(`measure_before.html` / `measure_after.html`)只多一个尾部 `<script>`,不改变原有 4 个脚本索引,因此指纹逻辑不受影响。包装 `rend.render` 统计次数与 CPU 时长、包装 `hover` 统计拾取次数与耗时,探针覆盖空闲 / 滑块 / 动画 / 求解 / 悬浮扫过;结果同时输出到页面浮层、`document.title` 与 console,便于截图与读取。
|
||||
|
||||
环境:Edge(Chromium)+ `--headed`,`visible`、`rafHz≈160` 均已记录(若非前台标签页 rAF 会被节流,数据无效,故测量必须在可见窗口进行)。
|
||||
|
||||
---
|
||||
|
||||
## 四、经实测否决 / 未做的优化
|
||||
|
||||
| 候选 | 实测 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| `scene.updateMatrixWorld(true)` 改 6 级关节链更新 | 全量强制刷新仅 **0.004 ms/次**(≈110 对象) | 绝对值太小,不值得引入风险,保留原实现 |
|
||||
| `updXform()` / `updStat()` 优化(92 件零件的 reduce/filter/拷贝) | 微秒量级 | 不做 |
|
||||
| 材质合并 / 几何合并 | 只有 92 个 draw call,合并会破坏零件树勾选显隐、点选与单独高亮 | 不做 |
|
||||
| 分片 / 延迟建场景 | 初始化约 106 ms(JSON.parse 69 ms + 建几何 37 ms),分片不减少总时间 | 不做 |
|
||||
| 降低 `antialias` / `setPixelRatio` 上限 | 属画质取舍 | 保留原样 |
|
||||
| 实时追踪时对结果面板 DOM 节流 | `renderResult()` 含"场景图残差复算"与"备选构型"两块**验证语义**内容 | 保留原样,避免削弱验证可观测性 |
|
||||
| 按需渲染是否该去掉 `preserveDrawingBuffer` | 不重绘时画布会被清空 | 必须保留 `true` |
|
||||
|
||||
---
|
||||
|
||||
## 五、重要发现与后续建议
|
||||
|
||||
### 5.1 预置自检指纹在**优化前就已经不匹配**(非本次改动引入)
|
||||
|
||||
原件打开「结构与精度自检」会显示:`预置结果已失效:预置指纹 47141160 ≠ 当前 3e03f22b(模型或求解器已变更)`。
|
||||
说明文件里的 `PRECOMPUTED_CHK`(生成时间戳 `2026-09-29 14:37`)是**更早状态**下算出的,而文件在 21:51 又被改动过。本次优化前后该值都是 `3e03f22b`,未使情况变化。
|
||||
|
||||
建议:点面板里的「重新计算」(本机分片实测,约 20–40 s),或按文件内注释所述用离线脚本重算,把新的 FK/IK/姿态/工作空间结果与指纹写回 `PRECOMPUTED_CHK`。
|
||||
|
||||
### 5.2 生成器源文件与成品已漂移(**在完成反向同步前不要重跑构建**)
|
||||
|
||||
`src/build_viewer.py` 由 `src/viewer_tpl.html` + `web/ik-glue.js` + `web/arm-ik-solver.js` + `web/mesh_arm.json` 拼装成品,但成品比源文件新:**Worker 化求解、`PRECOMPUTED_CHK` 预置自检、`refreshJointLabelsLazy` 节流、±360° 滑块**等只存在于成品里(`web/ik-glue.js` 仍是同步求解的旧版,776 行 vs 成品 961 行)。
|
||||
|
||||
本次处理:按约定**直接优化成品**,并把性能补丁同步回 `src/viewer_tpl.html`(补丁前副本:`backups/viewer_tpl.html.v1_2026-09-29_性能补丁前.bak`)。`web/ik-glue.js` 中不存在本次改动的对应代码(无 Worker、无拾取/渲染循环/几何构建),故无需同步。
|
||||
|
||||
**在把成品里的新代码反向同步回模板/glue 之前,重跑 `python src/arm_model.py` 或 `build_viewer.py` 会把成品回退到旧功能(并丢掉本次性能优化)。**
|
||||
|
||||
### 5.3 内存与泄漏审计结论
|
||||
|
||||
- 几何:`p.v / p.t / p.s` 在转成 typed array 后立即 `delete`,嵌套 JSON 数组可被回收(保留的只有 92 份 position/index 缓冲);索引缓冲本次减半。
|
||||
- 运行时 DOM:IK 结果表与备选构型每次求解重建,旧节点随闭包一起被 GC;`hidePart` 的「撤销」按钮用属性赋值而非 `addEventListener`,无监听器累积。
|
||||
- Worker:单例 + Blob URL 已回收;`IKWjob` 每次覆盖,无排队泄漏。
|
||||
- `performance.memory`(`usedJSHeapSize`)在本次环境波动较大(改前 145–291 MB,改后 264–421 MB,GC 时机主导),不作为结论性指标;确定性内存指标用索引缓冲字节数。
|
||||
|
||||
---
|
||||
|
||||
## 六、复现方法
|
||||
|
||||
```powershell
|
||||
# 1) 本机静态服务器(把 /v.html /mb.html /ma.html 等别名映射到对应文件)
|
||||
python scratch/perf/serve.py 8123
|
||||
|
||||
# 2) 生成插桩副本(源 HTML → 尾部追加 harness.js)
|
||||
python scratch/perf/make_measure.py "机器人手臂3D查看器.html" "scratch/perf/measure_after.html" after
|
||||
|
||||
# 3) 浏览器打开 http://127.0.0.1:8123/ma.html ,约 25 s 后读取 #perfOut(或 document.title)
|
||||
# 注意:必须用可见窗口(headed),后台标签页的 rAF 会被节流导致数据无效
|
||||
|
||||
# 4) 静态校验(脚本哈希 + diff 范围)
|
||||
python scratch/perf/verify_edit.py
|
||||
|
||||
# 5) 功能对照自检:把 scratch/perf/func_check.js 用 base64 喂给浏览器 eval,
|
||||
# 约 19 s 后读取 window.__FC
|
||||
```
|
||||
|
||||
测量脚本均在 `scratch/`(已被 `.gitignore` 忽略),成品文件保持干净、无任何插桩代码。
|
||||
+314
-88
@@ -23,7 +23,7 @@ from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from human_layout import (HUB, PLATE_T, R_MOT, Z_TIP, Z_WR, Z_EI, Z_SH, Z_RL, Z_R1,
|
||||
Z_R3, Z_HUB3, Z_EL, Z_R6, Z_HUB6,
|
||||
Z_R4, Z_CAP, Z_M6, Z_CHEST0, Z_CHEST1, X_J1_HUB, X_ARM, L1,
|
||||
X_FORK_GAP,
|
||||
X_FORK_GAP, X_BAY_EXT,
|
||||
Y_AXIS, Y_HUB, Y_REAR, Y_COVER, Y_H, Y_R_IN, Y_R_OUT,
|
||||
Y_M6, Y_CHEST, X_CHEST, FOOT_H)
|
||||
|
||||
@@ -80,12 +80,14 @@ def make_direct_hub(shape, j, r_disc=35.0, z_out=74.0):
|
||||
# ========== 躯干/测试台架部分 (后置防倾翻高刚性机架) ==========
|
||||
X_COL = -115.0
|
||||
Y_COL = Y_AXIS # 128.0
|
||||
Z_LOCK = 463.0 # 横向防脱锁紧螺钉高度(立柱后壁 → 榫头,M4x16 自攻)
|
||||
|
||||
|
||||
def p_chest():
|
||||
"""P01_胸腔支撑立柱 (High-Rigidity Box Mast Tower - 后置台架立柱):
|
||||
1. 66x66mm 封闭中空厚壁箱型柱(壁厚 11mm,内腔 44x44),中心在 (-115.0, 128.0),位于手臂正后方;
|
||||
2. 顶部接收 P03 48x48x15mm 榫头插槽与 4x M5 安装螺栓;
|
||||
2. 顶部接收 P03 43.4x43.4x15mm 定位榫头(44x44 R4 内腔)、4x 竖向安装孔 + 1x 横向防脱锁紧孔
|
||||
(全部 Ø3.9 直孔:M4x16 从外部直接拧入自攻成型,无过孔/无沉孔/无预埋);
|
||||
3. 底部 50x50x12mm 阶梯榫头插入 P02 榫座:环形台肩(66->50)面承载轴向力,槽壁全周承载剪切;
|
||||
4. 前向开设两组大圆角走线工艺窗口(Z=60~150mm 与 Z=330~420mm);
|
||||
5. 外棱角全部采用 R=6mm 圆角。
|
||||
@@ -115,9 +117,13 @@ def p_chest():
|
||||
for xx in (X_COL - 20.0, X_COL + 16.0):
|
||||
for yy in (Y_COL - 18.0, Y_COL + 18.0):
|
||||
s = s.cut(cyl(2.75, 9.99, z_bot + 15.0, xx, yy))
|
||||
for xx in (X_COL - 21.0, X_COL + 21.0):
|
||||
for yy in (Y_COL - 21.0, Y_COL + 21.0):
|
||||
s = s.cut(cyl(2.75, z_top - 20.0, z_top + 0.01, xx, yy))
|
||||
# 顶部 4x 安装孔:统一 Ø3.9 直孔(M4x16 直接拧入自攻成型;只打这一个孔,无过孔/无沉孔),深 20mm
|
||||
# 位于 66x66 顶环 11mm 实体壁内(内腔 22 → 外壁 33),避开 44x44 内腔与 R4 内腔圆角;
|
||||
for xx in (X_COL - 27.5, X_COL + 27.5):
|
||||
for yy in (Y_COL - 13.5, Y_COL + 13.5):
|
||||
s = s.cut(cyl(1.95, z_top - 20.0, z_top + 0.01, xx, yy))
|
||||
# 横向防脱锁紧孔(后壁 → 榫头):Ø3.9 贯穿 11mm 后壁 + 榫头同径(M4x16 拧入自攻)
|
||||
s = s.cut(Xcyl(1.95, X_COL - 34.0, X_COL - 20.0, Y_COL, Z_LOCK))
|
||||
|
||||
w_bot = (cq.Workplane("YZ", origin=(X_COL + 33.5, Y_COL, 105.0))
|
||||
.rect(34.0, 70.0)
|
||||
@@ -134,7 +140,9 @@ def p_chest():
|
||||
def split_p01_poka_yoke(p01):
|
||||
"""拆分 P01 为 A/B/C 三段(严格适配 200x200mm 打印机,竖直免支撑打印):
|
||||
带 16mm 阶梯互锁榫卯(外套筒壁 3.6 + 公榫壁 6.9,内腔收窄 51->44 换取双厚壁,
|
||||
单边配合间隙 0.5)、非对称 M4 横向对锁穿钉孔(物理防反装防呆)、以及本体 3D 凹刻朝向标识。
|
||||
单边配合间隙 0.5)、以及统一 Ø3.9 直孔的分段对锁孔(M4x16 从外面直接拧入自攻成型,
|
||||
母口壁与公榫壁同径,无过孔/沉孔/预埋,前后两面对称各一颗);
|
||||
孔位相对立柱中心 −16 / +22 非对称(物理防反装防呆);本体 3D 凹刻朝向标识。
|
||||
"""
|
||||
z_bot = 22.0
|
||||
z_s1 = 172.0
|
||||
@@ -145,6 +153,14 @@ def split_p01_poka_yoke(p01):
|
||||
# 非对称对锁孔位(相对立柱中心偏心 -16mm 与 +22mm,偏心差 6mm,反装无法穿入螺栓)
|
||||
xx_bolts = (X_COL - 16.0, X_COL + 22.0)
|
||||
|
||||
# 分段对锁孔:统一 Ø3.9 直孔(M4x16 从外面直接拧入自攻成型;母口壁与公榫壁同径,
|
||||
# 没有过孔/沉孔等其它孔),前后两面对称;杆尖伸过榫壁的一段在内腔侧留 Ø4.5 让位。
|
||||
def _seam_holes(part, xx, z):
|
||||
return (part.cut(Ycyl(1.95, Y_COL - 33.6, Y_COL - 21.4, xx, z))
|
||||
.cut(Ycyl(1.95, Y_COL + 21.4, Y_COL + 33.6, xx, z))
|
||||
.cut(Ycyl(2.25, Y_COL - 21.7, Y_COL - 15.5, xx, z))
|
||||
.cut(Ycyl(2.25, Y_COL + 15.5, Y_COL + 21.7, xx, z)))
|
||||
|
||||
# --- P01_A (下段) ---
|
||||
cut_above_s1 = bx(X_COL - 50.0, X_COL + 50.0, Y_COL - 50.0, Y_COL + 50.0, z_s1, z_top + 10.0)
|
||||
p01_a_base = p01.cut(cut_above_s1)
|
||||
@@ -158,7 +174,7 @@ def split_p01_poka_yoke(p01):
|
||||
|
||||
z_bolt1 = z_s1 + h_lap / 2.0 # 180.0
|
||||
for xx in xx_bolts:
|
||||
p01_a = p01_a.cut(Ycyl(2.2, Y_COL - 40.0, Y_COL + 40.0, xx, z_bolt1))
|
||||
p01_a = _seam_holes(p01_a, xx, z_bolt1) # A-B 缝对锁孔(Ø3.9,前后两面)
|
||||
|
||||
# Engrave the front (-Y) flat face, reading VERTICALLY bottom-to-top along
|
||||
# the column (user request): local +X = world +Z, local +Y = world -X, so
|
||||
@@ -189,8 +205,8 @@ def split_p01_poka_yoke(p01):
|
||||
|
||||
z_bolt2 = z_s2 + h_lap / 2.0 # 329.0
|
||||
for xx in xx_bolts:
|
||||
p01_b = p01_b.cut(Ycyl(2.2, Y_COL - 40.0, Y_COL + 40.0, xx, z_bolt1))
|
||||
p01_b = p01_b.cut(Ycyl(2.2, Y_COL - 40.0, Y_COL + 40.0, xx, z_bolt2))
|
||||
p01_b = _seam_holes(p01_b, xx, z_bolt1) # A-B 缝对锁孔(Ø3.9,前后两面)
|
||||
p01_b = _seam_holes(p01_b, xx, z_bolt2) # B-C 缝对锁孔(Ø3.9,前后两面)
|
||||
|
||||
front_label_plane_b = cq.Plane(
|
||||
origin=(X_COL, Y_COL - 33.0, 240.0),
|
||||
@@ -207,7 +223,7 @@ def split_p01_poka_yoke(p01):
|
||||
.rect(58.8, 58.8).extrude(h_lap + 0.6).edges("|Z").fillet(3.9)).val()
|
||||
p01_c = p01_c_base.cut(pocket2)
|
||||
for xx in xx_bolts:
|
||||
p01_c = p01_c.cut(Ycyl(2.2, Y_COL - 40.0, Y_COL + 40.0, xx, z_bolt2))
|
||||
p01_c = _seam_holes(p01_c, xx, z_bolt2) # B-C 缝对锁孔(Ø3.9,前后两面)
|
||||
|
||||
front_label_plane_c = cq.Plane(
|
||||
origin=(X_COL, Y_COL - 33.0, 390.0),
|
||||
@@ -492,111 +508,308 @@ def p_base_foot():
|
||||
|
||||
|
||||
def p_girdle(j1):
|
||||
"""P03_肩关节安装悬臂 (高强受力多筋桁架 + 侧开式电机托架与前置止脱锁紧法兰):
|
||||
"""P03_肩关节安装悬臂 (轻量化板式桁架版 + 侧开式电机托架):
|
||||
1. 10mm 后端重载法兰盘 (X: -61.0..-51.0, 74x74mm);
|
||||
2. 双侧大截面三角加强筋 (8mm 厚,Y = Y_ARM ± 28mm);
|
||||
3. 下腹部纵向大受力龙骨筋 (12mm 厚);
|
||||
4. 顶部横向锁紧环抱筋 (60x10x10mm);
|
||||
5. 侧开式电机托架 (+Y侧全开, X: -51.0..8.5),M1 电机可从侧方滑入装配,由后端法兰 4xM3 与前置止脱锁紧法兰 4xM3 双向锁死;
|
||||
6. 端面 4x Ø3.2mm 沉头通孔直连 M1 定子端面螺纹孔 (BCD=55mm),彻底消除轴向拉脱风险;
|
||||
7. 两侧大面积散热舷窗 (34x20mm) + 顶部双斜向对流排热槽 (30x6.5mm),确保散热畅通;
|
||||
8. 4x 直通式螺丝刀安装通道 (Ø8.5mm) 与 Ø26mm 中央走线孔。
|
||||
2. 主干由 66mm 实心块改为双侧 6mm 桁架竖板 (Y = Y_ARM ± 27..33),外轮廓保持旧版
|
||||
"主干 ∪ 侧筋"并集不变 —— 只减材料不加体积,零新增干涉风险;实心 728 -> 286 cm³ (-61%);
|
||||
3. 竖板帐篷形减重窗 (顶/底斜面 ≥52°,自支撑;仅 20mm 平顶可桥接);
|
||||
4. 底部 9mm 承力鞍座 + 43.4x43.4 定位榫头 (配 P01_C 44x44 内腔,单边 0.3mm 间隙);
|
||||
鞍座底面 66x66 与立柱顶环带承压面积 2403mm² —— 竖向载荷走面压,螺栓只防上抬/防滑;
|
||||
5. 4x 安装孔 (X_COL±27.5, Y_COL±13.5):Ø3.9 直孔,M4x16 从上方直接拧入自攻成型
|
||||
(鞍座与立柱顶环同径,无过孔/沉孔/预埋),可拆装;
|
||||
6. 下腹中央龙骨筋 (12mm 中位腹板,自鞍座斜拉至法兰下缘) + 顶部横向锁紧环抱筋 (66x9x10mm,
|
||||
与两侧竖板外面齐平、顶面压在筋板脊线上,仅顶前棱一道 2mm 倒角);
|
||||
7. 侧开式电机托架 (+Y侧全开, X: -51.0..11.5,前端随 X_BAY_EXT 前延 3mm),
|
||||
M1 电机可从侧方斜插卡入装配(插槽比电机长 3mm),由后端法兰 4xM3x16 锁死;
|
||||
电机舱 Ø60.7 从后端直通到前端面(2026-10-02 用户裁定"去掉台阶改成平的":无环形台阶、
|
||||
无喇叭锥;原前置止脱锁紧法兰 4xM3 随之取消,电机轴向由后端 4xM3x16 约束);
|
||||
9. 两侧大面积散热舷窗 (34x20mm) + 顶部双斜向对流排热槽 (30x6.5mm),确保散热畅通;
|
||||
10. Ø26mm 中央走线孔;竖板间为开放腔体(无封闭空腔),M3 螺丝刀通道直接走腔体,拧紧更顺手。
|
||||
"""
|
||||
X_MOUNT = Z_R1 # -51.0 (Rear face of M1)
|
||||
T_FLANGE = 10.0 # 10mm thick high-strength mounting flange
|
||||
X_BACK = X_MOUNT - T_FLANGE # -61.0
|
||||
Y_ARM = Y_AXIS # 128.0
|
||||
Z_ARM = Z_RL # 610.0
|
||||
Y_P_IN = 27.0 # 桁架竖板内表面 (Y_ARM ± 27)
|
||||
Y_P_OUT = 33.0 # 桁架竖板外表面 (与旧主干齐平)
|
||||
T_SADDLE = 9.0 # 底部鞍座厚度
|
||||
TENON = 43.4 # 定位榫头边长(P01_C 内腔 44x44 R4,单边留 0.3mm)
|
||||
WIN_R = 4.0 # 竖板减重窗内角圆角(消除内尖角应力集中,Kt 由 ~2.5 降到 ~1.2)
|
||||
BOLT_DX, BOLT_DY = 27.5, 13.5 # 4x 安装孔相对立柱中心偏移:落在顶环 11mm 实体壁内
|
||||
|
||||
# 1. 主安装法兰盘 (74x74mm, 倒圆角 R=8mm)
|
||||
# 1. 主安装法兰盘 (74x74mm, 四角倒圆 R=8mm)
|
||||
# 背面那圈 2mm 倒角已去掉:板料只伸进法兰 0.1mm,倒角面与板背面相交会在
|
||||
# Z≈574.9 挤出一批 2mm 短棱,正是这批短棱让整条交接内棱网络倒不出圆角;
|
||||
# 同时它跨过 R8 角部圆弧,就是角上那道"卷起来"的怪倒角。
|
||||
flange = (cq.Workplane("YZ", origin=(X_BACK, Y_ARM, Z_ARM))
|
||||
.rect(74.0, 74.0)
|
||||
.extrude(T_FLANGE)
|
||||
.edges("|X").fillet(8.0)
|
||||
.edges("<X").chamfer(2.0)).val()
|
||||
.edges("|X").fillet(8.0)).val()
|
||||
s = cq.Shape(flange.wrapped)
|
||||
|
||||
# 2. 斜挑支撑主干 (Central Arch Backbone)
|
||||
# 2. 双侧桁架竖板 (6mm):旧"斜挑支撑主干"与"大三角加强筋"轮廓同段挤出 → 自动并集成单块侧板
|
||||
# 主干的后缘两点取在加强筋斜边的延长线上,使外轮廓顶部成为一整条直线
|
||||
# (否则后缘 (-148,550) 处有 5° 折点、留下 5mm 台阶)。
|
||||
# Z_CREST = 侧筋脊线高度,同时是顶筋 2mm 顶前倒角的下沿:倒角从 (X-71,Z_CREST)
|
||||
# 斜上到 (X-69,Z_CREST+2) = 顶筋顶面,斜坡面与倒角面几乎相切,顶部读起来是一整张面。
|
||||
Z_CREST = Z_ARM + 33.0 # 643(初版 642;顶筋整体上移 3mm 让出扳手空间后跟着抬)
|
||||
|
||||
RIB_SLOPE = (Z_CREST - 575.0) / ((X_BACK - 10.0) - (X_COL - 15.0))
|
||||
|
||||
def rib_line_z(x):
|
||||
return Z_CREST + RIB_SLOPE * (x - (X_BACK - 10.0))
|
||||
|
||||
trunk_pts = [
|
||||
(X_COL - 33.0, Z_CHEST1),
|
||||
(X_COL + 33.0, Z_CHEST1),
|
||||
(X_COL + 33.0, 505.0),
|
||||
(X_BACK + 0.1, 565.0),
|
||||
(X_BACK + 0.1, Z_ARM - 5.0),
|
||||
(X_COL - 10.0, 580.0),
|
||||
(X_COL - 33.0, 550.0),
|
||||
(X_COL - 10.0, rib_line_z(X_COL - 10.0)),
|
||||
(X_COL - 33.0, rib_line_z(X_COL - 33.0)),
|
||||
]
|
||||
trunk = (cq.Workplane("XZ", origin=(0, Y_ARM, 0))
|
||||
.polyline(trunk_pts).close()
|
||||
.extrude(33.0, both=True)).val()
|
||||
s = s.fuse(cq.Shape(trunk.wrapped))
|
||||
|
||||
# 3. 左右两侧主抗弯抗扭大三角加强筋 (厚度 8mm)
|
||||
for sign in (-1, 1):
|
||||
y_rib = Y_ARM + sign * 28.0
|
||||
rib_pts = [
|
||||
(X_COL - 30.0, Z_CHEST1),
|
||||
(X_COL + 30.0, Z_CHEST1),
|
||||
(X_COL - 33.0, Z_CHEST1), # 底边两端与主干/鞍座对齐 (±33):原来收进 3mm,
|
||||
(X_COL + 33.0, Z_CHEST1), # 会在鞍座顶面上留下 0.7/3mm 短棱,卡住整条底棱圆角
|
||||
(X_BACK + 0.1, 565.0),
|
||||
(X_BACK + 0.1, Z_ARM + 32.0),
|
||||
(X_BACK - 15.0, Z_ARM + 32.0),
|
||||
(X_BACK + 0.1, Z_CREST),
|
||||
(X_BACK - 10.0, Z_CREST), # 脊线 (-71,643):正好落在顶筋前面与 2mm 倒角下沿的交点
|
||||
(X_COL - 15.0, 575.0),
|
||||
(X_COL - 30.0, 530.0),
|
||||
]
|
||||
side_rib = (cq.Workplane("XZ", origin=(0, y_rib, 0))
|
||||
.polyline(rib_pts).close()
|
||||
.extrude(5.0, both=True)).val()
|
||||
s = s.fuse(cq.Shape(side_rib.wrapped))
|
||||
for sign in (-1, 1):
|
||||
ya = Y_ARM + sign * Y_P_IN
|
||||
yb = Y_ARM + sign * Y_P_OUT
|
||||
y0, y1 = min(ya, yb), max(ya, yb)
|
||||
for prof in (trunk_pts, rib_pts):
|
||||
s = s.fuse(cq.Shape(Yplate(prof, y0, y1).wrapped))
|
||||
|
||||
# 4. 下腹部纵向抗弯龙骨筋 (厚度 12mm)
|
||||
keel_pts = [
|
||||
(X_COL + 33.0, Z_CHEST1),
|
||||
(X_COL + 33.0, 495.0),
|
||||
(X_BACK + 0.1, 570.0),
|
||||
(X_BACK + 0.1, 555.0),
|
||||
(X_COL + 20.0, 480.0),
|
||||
(X_COL + 20.0, Z_CHEST1),
|
||||
# 3. 竖板减重窗 (帐篷形:斜面 ≥52° 自支撑、仅 20mm 平顶可桥接;避开鞍座与 M5 螺母柱)
|
||||
# 6 个内角全部 R4 圆角:把内尖角应力集中 (Kt≈2.5) 降到 ≈1.2,同时圆角面仍是竖直/自支撑面
|
||||
win_pts = [
|
||||
(X_COL - 24.0, 498.0), (X_COL + 23.0, 498.0), (X_COL + 25.0, 522.0),
|
||||
(X_COL + 10.0, 545.0), (X_COL - 10.0, 545.0), (X_COL - 25.0, 524.0),
|
||||
]
|
||||
keel = (cq.Workplane("XZ", origin=(0, Y_ARM, 0))
|
||||
.polyline(keel_pts).close()
|
||||
.extrude(12.0, both=True)).val()
|
||||
s = s.fuse(cq.Shape(keel.wrapped))
|
||||
for sign in (-1, 1):
|
||||
yc = Y_ARM + sign * 0.5 * (Y_P_IN + Y_P_OUT)
|
||||
tool = (cq.Workplane("XZ", origin=(0, yc, 0)).polyline(win_pts).close()
|
||||
.extrude(Y_P_OUT - Y_P_IN + 1.0, both=True)
|
||||
.edges("|Y").fillet(WIN_R).val())
|
||||
s = s.cut(cq.Shape(tool.wrapped))
|
||||
|
||||
# 5. 顶部横向锁紧环抱横筋
|
||||
tie_rib = (cq.Workplane("YZ", origin=(X_BACK - 10.0, Y_ARM, Z_ARM + 28.0))
|
||||
.rect(60.0, 10.0)
|
||||
# 4. 底部承力鞍座:坐实立柱顶面,把两竖板、龙骨与榫头连成整体
|
||||
s = s.fuse(bx(X_COL - 33.0, X_COL + 33.0, Y_ARM - Y_P_OUT, Y_ARM + Y_P_OUT,
|
||||
Z_CHEST1, Z_CHEST1 + T_SADDLE))
|
||||
|
||||
# 5. 下腹部纵向抗弯龙骨筋 (12mm 中位腹板,自鞍座斜拉至法兰背面;交接内棱在融合后做真 3D 圆角)
|
||||
keel_pts = [
|
||||
(X_COL + 33.0, Z_CHEST1), # (-82, 470)
|
||||
(X_COL + 33.0, 495.0), # (-82, 495)
|
||||
(X_BACK + 0.1, 573.0), # (-60.9, 573) 斜拉至法兰背面
|
||||
(X_BACK + 0.1, 594.4), # (-60.9, 594.4) 顶端贴法兰
|
||||
(X_COL + 42.1, 585.0), # (-72.9, 585)
|
||||
(X_COL + 20.0, Z_CHEST1), # (-95, 470) 背面一路斜到底:原来在 (-95,480) 有个折点,
|
||||
] # 折点离鞍座顶面只 1mm,R2.5 圆角爬不上去 → 龙骨底棱整组倒不出
|
||||
s = s.fuse(cq.Shape(Yplate(keel_pts, Y_ARM - 6.0, Y_ARM + 6.0).wrapped))
|
||||
|
||||
# 7. 顶部横向锁紧环抱横筋:Y 向与两竖板外面齐平 (Y_ARM±33)、前面与筋板脊线共面 (X=-71)、
|
||||
# 顶面 Z=645 压住脊线 (Z=643) 并与 2mm 顶前倒角相切 → 顶筋与两侧脊线合成一整块。
|
||||
# 整体比初版抬高 3mm (底面 633→636):4x M3 后端沉孔 Ø6.5 跨在 Z 627.2..633.7 上,
|
||||
# 底面 633 会压住孔口上沿 0.7mm、扳手让位只剩负间隙;抬到 636 后退到孔口上方 2.3mm、
|
||||
# 离螺钉轴线 5.55mm。顶面 645 不越过法兰 R8 角部圆弧在 Y=95 处的 645.9 上界。
|
||||
tie_rib = (cq.Workplane("YZ", origin=(X_BACK - 10.0, Y_ARM, Z_ARM + 30.5))
|
||||
.rect(66.0, 9.0)
|
||||
.extrude(10.0)
|
||||
.edges("|X").fillet(3.0)).val()
|
||||
.edges(cq.selectors.BoxSelector((X_BACK - 10.5, Y_ARM - 34.0, Z_ARM + 34.5),
|
||||
(X_BACK - 9.5, Y_ARM + 34.0, Z_ARM + 35.5)))
|
||||
.chamfer(2.0)).val()
|
||||
s = s.fuse(cq.Shape(tie_rib.wrapped))
|
||||
|
||||
# 与立柱顶部配合的榫头 (48x48x15mm)
|
||||
# 6.y 体-体交接凹棱:融合完成后对"凹棱"直接做真 3D 圆角。
|
||||
# 原先这里是手工沿棱 fuse 的 10 条"正方形−四分之一圆"焊珠小体(_cove),已整体删除:
|
||||
# 焊珠端面是平的、悬在半空,两条互相垂直的焊珠在角上互不搭接 → 圆角"缺一块",
|
||||
# 并与相邻体的棱互相穿插留 1~3mm² 碎面。真圆角在面边界自然收口、多棱相交处由 OCC
|
||||
# 生成角部补片。
|
||||
from cadquery import selectors as _sel
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import GeomAbs_Plane
|
||||
from OCP.TopAbs import TopAbs_REVERSED
|
||||
|
||||
def _outward(f, sh):
|
||||
"""平面面的外法向:先按 Orientation 修正,再用"面中心外移 1mm 是否仍在实体内"兜方向。"""
|
||||
ad = BRepAdaptor_Surface(f.wrapped)
|
||||
if ad.GetType() != GeomAbs_Plane:
|
||||
return None
|
||||
ax = ad.Plane().Axis().Direction()
|
||||
n = cq.Vector(ax.X(), ax.Y(), ax.Z())
|
||||
if f.wrapped.Orientation() == TopAbs_REVERSED:
|
||||
n = n * -1.0
|
||||
c = f.Center()
|
||||
if cq.Solid(sh.wrapped).isInside(cq.Vector(c.x + n.x, c.y + n.y, c.z + n.z), 1e-6):
|
||||
n = n * -1.0
|
||||
return n
|
||||
|
||||
def _fillet_reflex(lo, hi, radii, tag, eps=0.9):
|
||||
"""倒出 lo~hi 区域内的凹棱(两面夹 270° 材料)。判据:从棱中点沿 (n1-n2) 外探 eps,
|
||||
落在实体内 = 凹、落在外 = 凸。区域内凹棱先整批一次倒(半径逐级降级,且倒完验拓扑:
|
||||
OCC 会给出"没抛异常但面已坏"的结果),建不出时按相连棱分组逐组倒。"""
|
||||
nonlocal s
|
||||
|
||||
def _pick(sh):
|
||||
edges = sh.Edges()
|
||||
idx = {}
|
||||
for i, e in enumerate(edges):
|
||||
idx[e.hashCode()] = i
|
||||
adj = [[] for _ in edges]
|
||||
for f in sh.Faces():
|
||||
for ee in f.Edges():
|
||||
k = idx.get(ee.hashCode())
|
||||
if k is not None:
|
||||
adj[k].append(f)
|
||||
box = _sel.BoxSelector(lo, hi)
|
||||
out = []
|
||||
for i, e in enumerate(edges):
|
||||
if e.Length() <= 6.0 or len(adj[i]) != 2 or not box.filter([e]):
|
||||
continue
|
||||
n1 = _outward(adj[i][0], sh)
|
||||
n2 = _outward(adj[i][1], sh)
|
||||
if n1 is None or n2 is None:
|
||||
continue
|
||||
dx, dy, dz = n1.x - n2.x, n1.y - n2.y, n1.z - n2.z
|
||||
ln = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
if ln < 1e-6:
|
||||
continue
|
||||
p = e.Center()
|
||||
if cq.Solid(sh.wrapped).isInside(
|
||||
cq.Vector(p.x + dx / ln * eps, p.y + dy / ln * eps,
|
||||
p.z + dz / ln * eps), 1e-6):
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
es0 = _pick(s)
|
||||
if not es0:
|
||||
print(" [P03 真圆角] %-14s 区域内无凹棱,跳过" % tag, flush=True)
|
||||
return
|
||||
|
||||
def _groups(ee):
|
||||
"""按共端点把凹棱连成链/网络:一组一次倒,OCC 才能在交汇处生成角部补片;
|
||||
逐条倒会在交汇处留下断口(看着就像圆角倒一半停了)。"""
|
||||
pts = [e.Vertices() or [e.Center()] for e in ee]
|
||||
par = list(range(len(ee)))
|
||||
|
||||
def find(a):
|
||||
while par[a] != a:
|
||||
par[a] = par[par[a]]
|
||||
a = par[a]
|
||||
return a
|
||||
|
||||
for i in range(len(ee)):
|
||||
for j in range(i + 1, len(ee)):
|
||||
if any(pi.distance(pj) < 0.05 for pi in pts[i] for pj in pts[j]):
|
||||
par[find(i)] = find(j)
|
||||
g = {}
|
||||
for i in range(len(ee)):
|
||||
g.setdefault(find(i), []).append(ee[i])
|
||||
return list(g.values())
|
||||
|
||||
def _do(edges_, rr):
|
||||
"""倒完立刻验拓扑:OCC 会给出"没抛异常但面已坏"的结果,必须挡下来。"""
|
||||
try:
|
||||
ns = cq.Shape(cq.Solid(s.wrapped).fillet(rr, edges_).wrapped)
|
||||
except Exception:
|
||||
return None
|
||||
return ns if cq.Solid(ns.wrapped).isValid() else None
|
||||
|
||||
# 先试"区域内全部凹棱一次倒"(探针实测这样能整批成型,且只重建一次,
|
||||
# 不会像分批那样把同一张面反复重算坏掉);不行再按相连棱分组逐组倒。
|
||||
for rr in radii:
|
||||
ns = _do(es0, rr)
|
||||
if ns is not None:
|
||||
s = ns
|
||||
print(" [P03 真圆角] %-14s %d 条凹棱 R%.1f ✓ 整批一次成型"
|
||||
% (tag, len(es0), rr), flush=True)
|
||||
return
|
||||
|
||||
n_ok, n_grp, left = 0, 0, len(es0)
|
||||
for _ in range(24):
|
||||
es = _pick(s)
|
||||
if not es:
|
||||
break
|
||||
got = False
|
||||
for g in sorted(_groups(es), key=lambda g: -sum(e.Length() for e in g)):
|
||||
for rr in radii:
|
||||
ns = _do(g, rr)
|
||||
if ns is None:
|
||||
continue
|
||||
s = ns
|
||||
n_ok += len(g)
|
||||
n_grp += 1
|
||||
print(" [P03 真圆角] %-14s 第%d组 %d 条 R%.1f ✓"
|
||||
% (tag, n_grp, len(g), rr), flush=True)
|
||||
got = True
|
||||
break
|
||||
if got:
|
||||
break
|
||||
if not got:
|
||||
left = len(es)
|
||||
break
|
||||
print(" [P03 真圆角] %-14s 共 %d 组 %d 条凹棱倒圆,剩 %d 条保持尖棱"
|
||||
% (tag, n_grp, n_ok, left), flush=True)
|
||||
|
||||
XR, YC, ZS = X_BACK, Y_ARM, Z_CHEST1 + T_SADDLE # -61, 128, 479
|
||||
# —— 鞍座顶面 (Z=479) 上的交接内棱:龙骨三条 + 两竖板内侧两条,一次真圆角 ——
|
||||
# 半径上限 2.0:龙骨侧面 Y=122 与 M5 螺钉头外缘 Y=118.75 只隔 3.25mm,
|
||||
# 原来的 R3 焊珠会顶到螺帽承压边,R2.0 留 1.25mm。
|
||||
_fillet_reflex((X_COL - 34.0, Y_ARM - 40.0, ZS - 3.0),
|
||||
(X_COL + 34.0, Y_ARM + 40.0, ZS + 7.0),
|
||||
(2.0, 1.5), "鞍座顶面交接")
|
||||
# —— 法兰背面 (X=-61) 上的交接内棱:只倒竖板/龙骨的竖直棱 ——
|
||||
# 顶筋底面那条水平棱 (Z=636) 不倒:4x M3 沉孔 Ø6.5 在 Z 627.2..633.7,
|
||||
# 倒上 R2.5 会把螺钉上方 5.55mm 的扳手让位吃掉 2.5mm。
|
||||
# 上界取 631:竖板内棱被顶筋底面切成 573..636 / 636..643 两段,只倒下面长的那段
|
||||
# ——把 636..643 的 7mm 短段并进来,整批就从"一次成型"退化成逐组、且少倒 6 条。
|
||||
_fillet_reflex((X_BACK - 3.0, Y_ARM - 40.0, 570.0),
|
||||
(X_BACK + 3.0, Y_ARM + 40.0, 631.0),
|
||||
(2.5, 2.0, 1.5), "竖板/龙骨↔法兰")
|
||||
_fillet_reflex((X_BACK - 3.0, Y_ARM - 40.0, Z_ARM + 34.5),
|
||||
(X_BACK + 3.0, Y_ARM + 40.0, Z_ARM + 35.5),
|
||||
(1.5, 1.0), "顶筋顶↔法兰")
|
||||
|
||||
# 与立柱顶部配合的定位榫头 (43.4x43.4x15mm):配 P01_C 44x44 R4 内腔,单边 0.3mm 滑配
|
||||
boss = (cq.Workplane("XY", origin=(X_COL, Y_COL, Z_CHEST1 - 15.0))
|
||||
.rect(48.0, 48.0)
|
||||
.rect(TENON, TENON)
|
||||
.extrude(15.0)
|
||||
.edges("|Z").fillet(4.0)).val()
|
||||
.edges("|Z").fillet(4.2)).val()
|
||||
s = s.fuse(cq.Shape(boss.wrapped))
|
||||
|
||||
# 4x M5 连接螺栓孔
|
||||
for xx in (X_COL - 21.0, X_COL + 21.0):
|
||||
for yy in (Y_COL - 21.0, Y_COL + 21.0):
|
||||
s = s.cut(cyl(2.75, Z_CHEST1 - 16.0, Z_CHEST1 + 25.0, xx, yy))
|
||||
# 4x 安装孔:统一 Ø3.9 直孔(M4x16 从腔体正上方拧入自攻成型;鞍座与立柱顶环同径、无沉孔)
|
||||
# 孔位 (X_COL±27.5, Y_COL±13.5) 同时满足:①在立柱顶环实体壁内(内腔 22 → 外壁 33);
|
||||
# ②在 P03 两竖板之间、避开中间龙骨,从腔体正上方用扳手可直接拧紧。
|
||||
for xx in (X_COL - BOLT_DX, X_COL + BOLT_DX):
|
||||
for yy in (Y_COL - BOLT_DY, Y_COL + BOLT_DY):
|
||||
s = s.cut(cyl(1.95, Z_CHEST1 - 0.5, Z_CHEST1 + T_SADDLE + 0.5, xx, yy))
|
||||
|
||||
# 4x M3 连接螺孔与直通螺丝刀通道
|
||||
# 1x 横向防脱锁紧:榫头 −X 侧向内 Ø3.9x14 直孔(与立柱后壁 Ø3.9 孔同轴,M4x16 拧入自攻)
|
||||
s = s.cut(Xcyl(1.95, X_COL - TENON / 2.0 - 1.0, X_COL - TENON / 2.0 + 14.0, Y_COL, Z_LOCK))
|
||||
|
||||
# 4x M3 连接螺孔(螺丝刀通道直接走两竖板之间的开放腔体,无需再钻 Ø8.5 直通隧道)
|
||||
for dy in (-17.55, 17.55):
|
||||
for dz in (-20.46, 20.46):
|
||||
py = Y_ARM + dy
|
||||
pz = Z_ARM + dz
|
||||
th = Xcyl(1.7, X_BACK - 0.5, X_MOUNT + 4.0, py, pz)
|
||||
cb = Xcyl(3.25, X_BACK - 0.5, X_BACK + 4.5, py, pz)
|
||||
access_tunnel = Xcyl(4.25, X_COL - 40.0, X_BACK + 0.1, py, pz)
|
||||
s = s.cut(th).cut(cb).cut(access_tunnel)
|
||||
s = s.cut(th).cut(cb)
|
||||
|
||||
# 中央走线孔 (Ø26mm)
|
||||
wire_hole = Xcyl(13.0, X_COL - 40.0, X_MOUNT + 2.0, Y_ARM, Z_ARM)
|
||||
s = s.cut(wire_hole)
|
||||
|
||||
# 6. 前向电机全包托架与前置止脱锁紧法兰 (X: -51.0..8.5)
|
||||
X_FRONT = 8.5
|
||||
# 6. 前向电机全包托架 (X: -51.0..8.5+X_BAY_EXT=11.5,Ø60.7 舱孔平直贯通到端面)
|
||||
# 端面随插槽加长前延 X_BAY_EXT;叉板背面(10.5+X_FORK_GAP=11.5)与端面 0.0 硬贴平
|
||||
# (用户两次要求"搞成平的";J1 转动摩擦由用户知悉,发涩时可微松 M2)
|
||||
X_FRONT = 8.5 + X_BAY_EXT
|
||||
R_POD = 36.5
|
||||
R_BORE = 30.35
|
||||
|
||||
@@ -606,28 +819,38 @@ def p_girdle(j1):
|
||||
.edges(">X").chamfer(1.5)).val()
|
||||
s = s.fuse(cq.Shape(cradle_outer.wrapped))
|
||||
|
||||
# 电机定子配合内腔 (精密滑配至 X=5.05)
|
||||
motor_pocket = Xcyl(R_BORE, X_MOUNT - 0.2, 5.05, Y_ARM, Z_ARM)
|
||||
# 电机定子配合内腔(最终状态,2026-10-02 用户裁定"去掉台阶改成平的+保留固定电机的面"):
|
||||
# Ø60.7 圆柱孔壁从前端面平直贯通到舱底(无窄颈、无台阶、无喇叭锥);
|
||||
# 舱底 = 一个完整的平坦环面(X=5.06+X_BAY_EXT=8.06,与插槽末端墙共面,中开 Ø39 凸台过孔)
|
||||
# ——即电机前限位面。电机轴向由后端法兰 4xM3x16 约束;
|
||||
# 前端锁紧由 4 个 Ø10 局部凸台上的 M3 沉头螺钉承担(见下方凸台段)。
|
||||
motor_pocket = Xcyl(R_BORE, X_MOUNT - 0.2, 5.06 + X_BAY_EXT, Y_ARM, Z_ARM)
|
||||
s = s.cut(motor_pocket)
|
||||
|
||||
# 前端中心避让孔 (避让 J1 输出轴旋转凸台)
|
||||
hub_clearance = Xcyl(19.5, 4.8, X_FRONT + 0.5, Y_ARM, Z_ARM)
|
||||
s = s.cut(hub_clearance)
|
||||
|
||||
# 4x 前置 M3 锁紧螺钉通孔与沉头 (BCD=55mm, R=27.5mm, 角度 17.8°, 107.8°, 197.8°, 287.8°)
|
||||
# 4x 前置 M3 锁紧螺钉凸台(BCD=55mm,17.8°/107.8°/197.8°/287.8°):
|
||||
# 平直舱孔的沉头座位由 4 个 Ø10 局部凸台承载——凸台位于舱孔前段 8.06..11.5,
|
||||
# 电机前端面(5.0)之前 3mm,电机斜插路径不经过;M3x10 沉头从凸台端面旋入
|
||||
# 电机前端面,咬合 3.5mm,兼作电机前端防转定位。
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
rad = math.radians(deg)
|
||||
fy = Y_ARM + 27.5 * math.cos(rad)
|
||||
fz = Z_ARM + 27.5 * math.sin(rad)
|
||||
th = Xcyl(1.6, 4.5, X_FRONT + 0.5, fy, fz)
|
||||
cb = Xcyl(3.0, 6.5, X_FRONT + 0.5, fy, fz)
|
||||
s = s.fuse(Xcyl(5.0, 5.06 + X_BAY_EXT, X_FRONT, fy, fz))
|
||||
th = Xcyl(1.6, 5.06 + X_BAY_EXT, X_FRONT, fy, fz)
|
||||
cb = Xcyl(3.0, X_FRONT - 2.0, X_FRONT, fy, fz)
|
||||
s = s.cut(th).cut(cb)
|
||||
|
||||
# J1 输出凸台过孔 (Ø39 × 3.44——J1 凸台与 3xM2 输出螺钉的必经孔,非台阶;
|
||||
# 原 6.7mm 长颈与半圆台肩已随舱底前移一并消除)
|
||||
hub_clearance = Xcyl(19.5, 4.8, X_FRONT + 0.5, Y_ARM, Z_ARM)
|
||||
s = s.cut(hub_clearance)
|
||||
|
||||
# 8. 电机托架 +Y 侧全开(侧开式半包,替代原封闭全包筒):
|
||||
# M1 电机可从 +Y 侧方直线滑入托架完成装配——原封闭筒电机物理上无法装入。
|
||||
# 轴向约束:后端法兰 4xM3(电机尾端面)+ 前置止脱法兰 4xM3(电机前端面)双向锁死;
|
||||
# 径向约束:Ø60.7 精密滑配托架底 + 重力贴底。开口前端至 5.06 保留完整止脱法兰环。
|
||||
s = s.cut(bx(X_MOUNT, 5.06, Y_ARM, Y_ARM + 60.0, Z_ARM - 60.0, Z_ARM + 60.0))
|
||||
# 轴向约束:后端法兰 4xM3(电机尾端面)单向锁死(前置止脱法兰随平直舱孔要求取消);
|
||||
# 径向约束:Ø60.7 精密滑配托架底 + 重力贴底。开口前端至 5.06+X_BAY_EXT=8.06
|
||||
# (比电机前端面多 3mm 斜插余量,电机可倾斜卡入),止脱法兰环随之前移、环宽保持 3.44mm。
|
||||
s = s.cut(bx(X_MOUNT, 5.06 + X_BAY_EXT, Y_ARM, Y_ARM + 60.0, Z_ARM - 60.0, Z_ARM + 60.0))
|
||||
|
||||
# 7. 散热系统:双侧大视窗 + 顶部斜向对流排热槽
|
||||
for sign in (-1, 1):
|
||||
@@ -654,8 +877,9 @@ def p_girdle(j1):
|
||||
|
||||
def p_fork(j1, j2):
|
||||
"""P04_肩叉_螺旋体30%_墙5底5_竖打 (Shoulder Fork / L-Bracket) - 加长避让顺滑流线版:
|
||||
1. 头部与身子起点自 X=5.5 前移加长至 X=10.5,与 P03 前置锁紧法兰 (X=8.5) 保持 2.0mm 均匀轴向安全间隙
|
||||
(X_FORK_GAP=0:不做整体前移,保持 J1 输出端最短悬伸力臂,避免额外弯矩负荷);
|
||||
1. 头部与身子起点自 X=5.5 前移加长至 X=10.5+X_FORK_GAP=11.5,叉板背面与 P03 托架端面
|
||||
(X=8.5+X_BAY_EXT=11.5) 0.0 硬贴平(2026-10-02 用户两次要求"搞成平的";
|
||||
P03 侧插槽独立加长 3mm 便于 M1 电机斜插卡入);
|
||||
2. J1 从动法兰圆盘已删除(盘面在 column 前缘形成鼓楞):J1 由 boss(Ø35.6)贯通 P03 前盖中心孔直连电机输出盘;column 顶边保持 Z=646.5 与 P03 环齐高,R=18mm 大圆弧顺滑过渡;
|
||||
3. 头部转接凸台加长贯通 P03 前盖中心孔 (R=19.5mm),直连 J1 旋转法兰盘;
|
||||
4. M2 保留后端安装板(R=35mm, 带 rear_mount_holes 螺钉孔),前端定位环鼓出圆盘已删除(前端定位由 column 端面 pocket 承担),中间镂空散热;
|
||||
@@ -744,7 +968,9 @@ def p_fork(j1, j2):
|
||||
u = AB.M['m2_bcd'] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M['m2_bcd'] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 63.0 + X_FORK_GAP, u, v)
|
||||
cb = cyl(2.8, 63.0 + X_FORK_GAP, 74.0 + 40.0 + X_FORK_GAP, u, v)
|
||||
# 沉孔台阶固定在 63.0(不随前移):M2x10 旋入电机输出毂的咬合量与改前一致;
|
||||
# 螺钉头(63..65)相对叉板背面(64.5)外露 0.5mm,位于 2mm 叉缝空隙内,无干涉。
|
||||
cb = cyl(2.8, 63.0, 74.0 + 40.0 + X_FORK_GAP, u, v)
|
||||
s = s.cut(j1.L(th)).cut(j1.L(cb))
|
||||
|
||||
return s
|
||||
@@ -1154,11 +1380,11 @@ def build():
|
||||
|
||||
parts_to_build = [
|
||||
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "立柱下段", lambda: p01_a, 0x0284c7,
|
||||
"200mm适配分段A:高166mm+16mm阶梯公榫+非对称M4防反装通孔+底部50x50x12榫头+P01-A 竖排TOP刻字"),
|
||||
"200mm适配分段A:高166mm+16mm阶梯公榫+顶端 2x Ø3.9 对锁孔(M4x16 自攻)+底部50x50x12榫头+P01-A 竖排TOP刻字"),
|
||||
("P01_B_立柱中段_三角形30%_墙4底5_竖打", "立柱中段", lambda: p01_b, 0x2563eb,
|
||||
"200mm适配分段B:高165mm+双端公母榫+非对称M4防反装通孔+P01-B 竖排TOP刻字"),
|
||||
"200mm适配分段B:高165mm+双端公母榫+两端各 2x Ø3.9 对锁孔(M4x16 自攻,非对称孔位防反装)+P01-B 竖排TOP刻字"),
|
||||
("P01_C_立柱上段_三角形30%_墙4底5_竖打", "立柱上段", lambda: p01_c, 0x1d4ed8,
|
||||
"200mm适配分段C:高149mm+底端母榫+非对称M4防反装通孔+顶面P03安装台+P01-C 竖排TOP刻字"),
|
||||
"200mm适配分段C:高149mm+底端母榫+底端 2x Ø3.9 对锁孔(非对称孔位防反装)+顶面P03安装台(4x Ø3.9 安装孔 + 1x Ø3.9 横向锁紧孔)+P01-C 竖排TOP刻字"),
|
||||
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "底座核心承力座", lambda: p02_c, 0x475569,
|
||||
"200mm适配中座:195x190mm核心承力座+48mm套筒+12mm阶梯榫座(轴向面承载)+前后三角加强筋+前后非对称防呆母榫槽+刻字"),
|
||||
("P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打", "前向防翻力臂", lambda: p02_f, 0xe11d48,
|
||||
@@ -1166,9 +1392,9 @@ def build():
|
||||
("P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打", "后向防翻力臂", lambda: p02_r, 0xd97706,
|
||||
"200mm适配后臂:45x190mm扩展力臂+80mm窄跨距双公榫+底部M5预埋螺母沉槽+P02-REAR刻字"),
|
||||
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
|
||||
"高强多肋机架:10mm后置法兰+双侧8mm大三角加强筋+下腹12mm抗弯龙骨筋+侧开式电机托架(M1从+Y侧滑入装配,前后法兰8xM3双向锁死)+前置止脱锁紧法兰(4xM3止脱螺孔)+大散热窗与对流槽"),
|
||||
"轻量化板式桁架版:主干由 66mm 实心块改为双侧 6mm 桁架竖板+帐篷减重窗(斜面≥52°自支撑)+R4内角,外轮廓与旧版并集完全一致,实心 728->286cm³;底部 9mm 承力鞍座(Ø3.9 安装孔)+12mm 中位龙骨筋(自鞍座斜拉至法兰下缘)+40~45°外扩斜撑;保留 10mm 后置法兰、侧开式电机托架、Ø60.7 直通电机舱(平孔无台阶)(M1 从+Y滑入,后端 4xM3x16 锁死)、大散热窗与对流槽;无封闭空腔、免支撑"),
|
||||
("P04_肩叉_螺旋体30%_墙5底5_竖打", "肩叉", lambda: p_fork(j1, j2), 0x2563eb,
|
||||
"加长避让版:头部与身子前移加长至X=10.5,与P03锁紧法兰保持2.0mm轴向安全间隙(不做前移,保持输出端最短悬伸力臂),J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"),
|
||||
"齐平插槽版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"),
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
|
||||
"V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
|
||||
|
||||
@@ -30,6 +30,8 @@ def rebuild_viewer():
|
||||
sheets = io.open(WEB + "/sheets.jsdata", encoding="utf-8").read()
|
||||
trv = io.open(DOCS + "/travel.json", encoding="utf-8").read() if os.path.exists(
|
||||
DOCS + "/travel.json") else "[]"
|
||||
col = io.open(DOCS + "/collide.json", encoding="utf-8").read() if os.path.exists(
|
||||
DOCS + "/collide.json") else '{"caps":[],"parts":{}}'
|
||||
screws = io.open(DOCS + "/screws.json", encoding="utf-8").read() if os.path.exists(
|
||||
DOCS + "/screws.json") else "[]"
|
||||
ik_solver = io.open(WEB + "/arm-ik-solver.js", encoding="utf-8").read()
|
||||
@@ -42,6 +44,7 @@ def rebuild_viewer():
|
||||
.replace("/*__DATA__*/", data)
|
||||
.replace("/*__SHEETS__*/", sheets)
|
||||
.replace("/*__TRAVEL__*/", trv)
|
||||
.replace("/*__COLLIDE__*/", col)
|
||||
.replace("/*__SCREWS__*/", screws)
|
||||
.replace("/*__IKSOLVER__*/", ik_solver)
|
||||
.replace("/*__IKGLUE__*/", ik_glue))
|
||||
|
||||
+6
-2
@@ -48,8 +48,12 @@ SWEEP_R = 92.0 # farthest point of the humerus from the shoulder pit
|
||||
POD_R = 47.0 # saddle drum outer radius about the roll axis
|
||||
|
||||
X_J1_HUB = 14.0 # J1 hub mating plane, forward of the humerus plate's ±44
|
||||
X_FORK_GAP = 0.0 # P04 前移量(用户裁定=0:前移会加大输出端悬伸弯矩,保持最短力臂)
|
||||
X_ARM = X_J1_HUB + PLATE_T / 2.0 + X_FORK_GAP # 18.5 the limb's own plane in x
|
||||
X_BAY_EXT = 3.0 # M1 侧插槽加长量(2026-10-01 用户裁定:插槽开口 5.06+3=8.06,
|
||||
# 比电机前端面多 3mm 斜插余量;P03 托架前端 8.5+3=11.5)
|
||||
X_FORK_GAP = 1.0 # P04 肩叉前移量:叉板背面(10.5+1.0=11.5)与 P03 托架端面(11.5)
|
||||
# 0.0 硬贴平(2026-10-02 用户两次要求"搞成平的",0.5mm 缝仍被判台阶)。
|
||||
# 注意:J1 相对转动时 M2 预紧会经此面产生摩擦阻力,若转动发涩可微松 M2。
|
||||
X_ARM = X_J1_HUB + PLATE_T / 2.0 + X_FORK_GAP # 19.5 the limb's own plane in x
|
||||
Z_R1 = X_J1_HUB - HUB # -51 J1 rear datum, measured in x: M1 lies behind the shoulder
|
||||
Z_CHEST1 = Z_RL - 140.0 # 470 torso mast top
|
||||
Z_CHEST0 = FOOT_H
|
||||
|
||||
+28
-16
@@ -8,19 +8,23 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量
|
||||
|
||||
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
||||
头面 = 沉孔/通道的"承接台阶"(螺钉从外侧塞入,一直顶到孔径变小的那一圈台阶面),
|
||||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外。
|
||||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外(S05 除外:搭接壁太薄不能开沉孔)。
|
||||
S01 J1~J6 输出毂 3xM2:joint.L() 局部 (r=m2_bcd/2, HUB_M2_CLOCK 相位),
|
||||
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
|
||||
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
|
||||
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (±17.55, ±20.46, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
|
||||
S04 P03 前止脱 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 57.5)(沉孔 Ø6.0 台阶 x=6.5)
|
||||
S05 P01 分段对锁 4xM4:Ø4.4 贯穿柱身 x∈{-131,-93}, z∈{180,329},头在 -Y 立面 y=95(模型未设沉孔)
|
||||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 62.5)
|
||||
(沉孔 Ø6.0 在 Ø10 凸台端面 x=11.5,M3x10 旋入电机前端面咬合 3.5mm)
|
||||
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
|
||||
前后各拧一颗,头在 y=95 / y=161 外露(搭接壁薄不开沉孔),直接拧入自攻成型
|
||||
S06 P09 工具法兰 4xM4:R20 @0/90/180/270°,台阶 z=97(沉孔 Ø7.0 深 4mm,法兰底面 z=93)
|
||||
S07 P02 中核心 4xM5 :x∈{-135,-99}, y∈{110,146},台阶 z=6(Ø10 沉孔深 6mm,头沉入 1mm)
|
||||
S08 P02 力臂 4xM5 :(-10,76/180) + (-185,88/168),台阶 z=17(Ø10 沉孔深 5mm,头与顶面齐平)
|
||||
S09 P03 安装台 4xM5 :x∈{-136,-94}, y∈{107,149},台阶 z=455(P03 榫头底面,Ø5.5 孔 454..495),
|
||||
头藏在 P01 立柱内腔里(孔深即 M5x35:455 -> 490,与模型 454..495 完全吻合)
|
||||
S09 P03 安装台 4xM4x16:x∈{-142.5,-87.5}, y∈{114.5,141.5},台阶 z=479(P03 鞍座顶面,
|
||||
从腔体正上方往下拧,Ø3.9 直孔穿过鞍座 9mm 再吃进 P01_C 顶环 7mm,自攻成型)
|
||||
S11 P03 横向防脱锁紧 1xM4x16:x=-148(立柱 −X 外端面),y=128,z=463,头朝 -X,
|
||||
沿 +X Ø3.9 直孔穿 11mm 后壁 + 榫头同径,直接拧入自攻(榫头内咬合 4.7mm)
|
||||
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
|
||||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||||
|
||||
@@ -188,21 +192,22 @@ def instances(m2_bcd, m2_clock):
|
||||
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
|
||||
|
||||
d1 = frame("J1")[1]
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S04", to_global("J1", u, v, 62.5), d1, "root",
|
||||
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
for u in (-17.55, 17.55):
|
||||
for v in (-20.46, 20.46):
|
||||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||||
"P03 后置法兰", "35.1×40.9 孔距 -> M1 尾端面"))
|
||||
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S04", to_global("J1", u, v, 57.5), d1, "root",
|
||||
"P03 前置止脱法兰", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
|
||||
for x in (X_COL - 16.0, X_COL + 22.0):
|
||||
for z in (180.0, 329.0):
|
||||
out.append(("S05", (x, 95.0, z), (0.0, -1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 榫缝对锁(另配 M4 螺母)" % z))
|
||||
"P01 立柱分段缝", "z=%.0f 前面 -Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
out.append(("S05", (x, 161.0, z), (0.0, 1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 背面 +Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
|
||||
for deg in (0.0, 90.0, 180.0, 270.0):
|
||||
x = HL.X_ARM + 20.0 * math.cos(math.radians(deg))
|
||||
@@ -219,10 +224,17 @@ def instances(m2_bcd, m2_clock):
|
||||
out.append(("S08", (x, y, 17.0), (0.0, 0.0, 1.0), "root",
|
||||
"P02 防翻力臂", "榫头对锁 (%.0f,%.0f)(另配 M5 预埋螺母)" % (x, y)))
|
||||
|
||||
for x in (X_COL - 21.0, X_COL + 21.0):
|
||||
for y in (Y_COL - 21.0, Y_COL + 21.0):
|
||||
out.append(("S09", (x, y, 455.0), (0.0, 0.0, -1.0), "root",
|
||||
"P03 肩吊臂安装台", "穿透榫头 Ø5.5 孔(454..495)旋入立柱,头藏于 P01 立柱内腔"))
|
||||
for x in (X_COL - 27.5, X_COL + 27.5):
|
||||
for y in (Y_COL - 13.5, Y_COL + 13.5):
|
||||
# 方向 = 螺帽朝外法向(+Z,扳手从腔体上方进入);拧入方向 = -Z,Ø3.9 直孔自攻
|
||||
out.append(("S09", (x, y, 479.0), (0.0, 0.0, 1.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从腔体正上方拧入,穿鞍座 9mm 吃进立柱顶环 7mm(自攻成型)"))
|
||||
|
||||
# 横向防脱锁紧(M4x16):螺帽压在后壁外面 x=-148,沿 +X Ø3.9 直孔自攻(穿 11mm 后壁 + 榫头)
|
||||
out.append(("S11", (X_COL - 33.0, Y_COL, 463.0), (-1.0, 0.0, 0.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从立柱后方拧入,穿 11mm 后壁吃进榫头 4.7mm(自攻成型)"))
|
||||
|
||||
for x, y in ((-97.5, 53.0), (-97.5, 203.0), (120.0, 58.0),
|
||||
(120.0, 198.0), (-215.0, 58.0), (-215.0, 198.0)):
|
||||
|
||||
+10
@@ -61,6 +61,16 @@ VIEWS = {
|
||||
"user_shoulder_solid": "?cam=joint&az=2.1&po=1.35&rd=380&xray=0",
|
||||
"user_shoulder_xray": "?cam=joint&az=2.1&po=1.35&rd=380",
|
||||
"user_shoulder_top": "?cam=joint&az=-0.6&po=0.6&rd=360&xray=0",
|
||||
# P03 轻量化桁架版(孤立视图:只留 P03,隐藏其余零件/电机/螺钉/包络)
|
||||
"p03_light_iso": "?az=0.62&po=1.15&rd=300&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"p03_light_side": "?az=1.5708&po=1.5708&rd=320&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"p03_light_rear": "?az=0.0&po=1.45&rd=300&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"p03_light_join": "?az=0.62&po=1.15&rd=420&tx=18.5&ty=520&tz=-128&xray=0",
|
||||
"p03_light_xray": "?az=0.62&po=1.15&rd=300&tx=-70&ty=548&tz=-128&xray=1&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
# P03↔立柱 安装接口(新版 4xM5 从腔体上方拧入):俯视看螺钉落位
|
||||
"p03_mount_top": "?az=0.9&po=1.30&rd=190&tx=-115&ty=476&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"p03_mount_iso": "?az=2.2&po=0.95&rd=230&tx=-115&ty=486&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"p03_mount_loose": "?az=0.9&po=0.42&rd=260&tx=-115&ty=500&tz=-128&xray=0&expl=60&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"svg_side": f"file:///{DOCS}/sheet_side.svg",
|
||||
"svg_joint": f"file:///{DOCS}/sheet_joint.svg",
|
||||
"svg_exploded": f"file:///{DOCS}/sheet_exploded.svg",
|
||||
|
||||
@@ -159,6 +159,8 @@ def arm_masses():
|
||||
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||||
tot = {}
|
||||
for p in meta["parts"]:
|
||||
if not os.path.exists("%s/parts_step/%s.step" % (OUT, p["name"])):
|
||||
continue # 螺钉实例 / 可达包络网格等非 CAD 构件没有 STEP,跳过
|
||||
sh = load(p["name"])
|
||||
v = volume(sh) / 1000.0
|
||||
g = (v * RHO["PLA"] * 1000.0 * INFILL) if p["printed"] else RHO["motor_g"]
|
||||
|
||||
+172
-60
@@ -84,7 +84,8 @@ small{color:var(--dim)}
|
||||
#bomDock .dk-hd{display:flex;align-items:center;gap:8px;padding:9px 10px;flex:none;
|
||||
border-bottom:1px solid var(--line)}
|
||||
#bomDock .dk-hd b{font-size:13px}
|
||||
#bomDock .dk-hd .q{color:var(--dim);font-size:11px}
|
||||
#bomDock .dk-hd .q{color:var(--dim);font-size:11px;min-width:0;flex:0 1 auto;
|
||||
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
#bomDock .dk-x{margin-left:auto}
|
||||
#bomDock .dk-bd{overflow:auto;padding:8px 11px 16px;flex:1;min-height:0}
|
||||
.dk-sec{margin:4px 0 5px;color:var(--ac);font-size:12px;letter-spacing:.4px}
|
||||
@@ -118,13 +119,19 @@ small{color:var(--dim)}
|
||||
#ikPose.on{background:#3a2b63;border-color:#a06bff;color:#fff;font-weight:600}
|
||||
#ikPoseBox{display:none;align-items:center;gap:6px}
|
||||
#ikPoseBox input[type=number]{width:54px}
|
||||
#ikMixBox{display:none;align-items:center;gap:6px;flex:none}
|
||||
#ikMixBox input[type=range]{width:118px;flex:none;accent-color:var(--ac);cursor:pointer}
|
||||
#ikMixBox .st{min-width:56px}
|
||||
|
||||
/* ===================== IK 结果面板(左栏顶部) ===================== */
|
||||
#ikpanel{margin:0 0 10px;border:1px solid var(--line);border-radius:6px;background:#141821;padding:7px 8px}
|
||||
#ikpanel .ikh{display:flex;align-items:center;gap:6px;margin-bottom:5px}
|
||||
#ikpanel .ikh h3{margin:0;font-size:12px;color:var(--ac);letter-spacing:.3px}
|
||||
.badge{margin-left:auto;font-size:10.5px;padding:1px 7px;border-radius:9px;border:1px solid;flex:none;
|
||||
white-space:nowrap}
|
||||
/* 徽标必须可换行:CJK 的最小内容宽度=一个字,若徽标 nowrap+flex:none,
|
||||
长状态文案会把标题压成竖排一字一行(实测 h3 缩到 12px) */
|
||||
#ikpanel .ikh{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;margin-bottom:5px}
|
||||
#ikpanel .ikh h3{margin:0;font-size:12px;color:var(--ac);letter-spacing:.3px;
|
||||
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.badge{margin-left:auto;font-size:10.5px;padding:1px 7px;border-radius:9px;border:1px solid;
|
||||
flex:0 1 auto;max-width:100%;text-align:center;line-height:1.35;text-wrap:balance}
|
||||
.badge.wait{color:var(--dim);border-color:var(--line)}
|
||||
.badge.goodb{color:#0c1a12;background:var(--ok);border-color:var(--ok)}
|
||||
.badge.warnb{color:#241a06;background:var(--warn);border-color:var(--warn)}
|
||||
@@ -226,14 +233,14 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
<span class="lb">mm</span>
|
||||
<span class="vsep"></span>
|
||||
<span class="lb">扫描精度</span>
|
||||
<select id="ikStep" title="冗余维 (J4,J5) 的扫描步长:越小越接近全局最优,耗时越长">
|
||||
<select id="ikStep" title="冗余维 (J4,J5) 的粗扫步长:只在可行段内采样;位置任务取全局最优,姿态任务的精确解由孤立根枚举给出、与步长无关">
|
||||
<option value="1">快速 1°</option>
|
||||
<option value="0.5">较快 0.5°</option>
|
||||
<option value="0.25" selected>标准 0.25°</option>
|
||||
<option value="0.1">精细 0.1°</option>
|
||||
<option value="0.05">极高 0.05°</option>
|
||||
<option value="0.05">参考级 0.05°</option>
|
||||
</select>
|
||||
<button id="ikSolve" title="求解并把手臂动画移动到该位姿(取 Σ|θ| 最小的构型)">求解并移动</button>
|
||||
<button id="ikSolve" title="求解并把手臂动画移动到该位姿(取相对当前姿态行程最小的构型)">求解并移动</button>
|
||||
<button id="ikPose" title="开启姿态控制:除 TCP 位置外,同时指定末端工具姿态(工具轴指向 + 绕轴滚转)">姿态控制</button>
|
||||
<span id="ikPoseBox">
|
||||
<span class="lb">方位</span><input type="number" id="ikAz" step="5" value="0" title="工具轴(法兰朝外方向)绕 CAD Z 轴的方位角 / 度">
|
||||
@@ -241,6 +248,18 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
<span class="lb">滚转</span><input type="number" id="ikRoll" step="5" value="0" title="绕工具轴自身的滚转角 / 度(右手系)">
|
||||
<button id="ikPoseCur" title="把当前真实工具姿态读回三个输入框">取当前</button>
|
||||
<button id="ikPoseDown" title="快捷设为工具轴竖直向下(爪取常用姿态)">垂直向下</button>
|
||||
<span class="lb">优先级</span>
|
||||
<select id="ikPrio" title="位置与姿态不能同时满足时的取舍:位置优先 = 保 TCP 点位精确、姿态取最接近可达值;姿态优先 = 保指定姿态精确、TCP 取最接近可达点(位置差在结果栏如实给出);混合优先 = 用右侧滑杆自由分配两端比重,0% = 姿态优先、100% = 位置优先,中间值在两端锚点之间按归一化折中代价加权搜索">
|
||||
<option value="pos" selected>位置优先</option>
|
||||
<option value="ori">姿态优先</option>
|
||||
<option value="mix">混合优先</option>
|
||||
</select>
|
||||
<span id="ikMixBox" title="混合优先的比重(自由分配位置与姿态):向左 = 姿态优先、向右 = 位置优先;0% / 100% 与两条纯算法逐位一致。折中代价 J = w·(e/e_ref)² + (1−w)·(o/o_ref)²,两端尺度取各自锚点(位置端 = 位置优先解的姿态残差、姿态端 = 姿态优先解的位置差),故读数即「沿折中历程的进度」。滑动即算即动,松手全量折中">
|
||||
<span class="lb">姿态</span>
|
||||
<input type="range" id="ikMixW" min="0" max="100" step="1" value="50">
|
||||
<span class="lb">位置</span>
|
||||
<span class="st" id="ikMixV">50% 位置</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ikrow">
|
||||
@@ -277,7 +296,7 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
<div class="doc">
|
||||
<b>模式</b>:本页「<b>运动模拟</b>」承载全部运动学能力;「三维模型」为纯静态模型视图(关节锁定在设计零位、无目标点与求解控件),「装配图」为二维图纸。运动模拟下左栏只保留 IK 结果与关节角度,零件树/打印清单自动收走。<hr>
|
||||
<b>目标</b>:使 TCP(末端工具法兰 P09 安装面中心,可加“工具长”)到达给定点,并在一切满足限位的可行构型中取
|
||||
<b>Σ|θᵢ| 最小</b>者(交互连续求解时叠加参考位姿连续项,见第 6 条)。开启上方“<b>姿态控制</b>”后,
|
||||
<b>各电机需转过的角度之和最小</b>者(相对当前姿态的行程;连续轴 θ 与 θ±360 是同一物理位形,角差一律取 ±180° 内的短弧,见第 6 条;离线批算不给参考姿态时退化为 Σ|θᵢ| 最小)。开启上方“<b>姿态控制</b>”后,
|
||||
约束升级为 <b>位置 3 + 姿态 3</b>(工具轴指向 + 绕轴滚转),见第 5 条。
|
||||
<br><b>姿态操控</b>:姿态开启后目标球处出现方向箭头与粉色侧球——<b>箭头指向即工具朝外方向</b>(法兰面法向朝外,也就是工具伸出的方向)。
|
||||
<b>拖箭头头部</b>用弧球法(指针射线与以目标为球心、半径=箭长的球求交,方向=交点−球心)连续调整方位/俯仰;
|
||||
@@ -288,7 +307,7 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
<ol>
|
||||
<li><b>先消去一维</b>(仅位置任务):TCP 恰好落在 J6 轴线上,绕 J6 转动不改变 TCP 位置 ⇒ 对“到点”任务取
|
||||
<code>θ6 = 0</code> 即已最优。</li>
|
||||
<li><b>识别结构</b>:J2 / J4 / J5 三轴严格平行(轴向 Y,过 x = 18.5),J1 轴为 X,J3 轴沿臂长方向 Z。
|
||||
<li><b>识别结构</b>:J2 / J4 / J5 三轴严格平行(轴向 Y,过 x = 19.5),J1 轴为 X,J3 轴沿臂长方向 Z。
|
||||
记 <code>A = X−X0</code>、<code>R = |(Y−Y0, Z−Z1)|</code>、<code>φ = atan2(Z−Z1, Y−Y0)</code>,
|
||||
则 <code>X = X0 + a</code>,而 <code>(Y, Z)</code> 由 <code>(b, c)</code> 绕 J1 轴旋转 φ 得到:<br>
|
||||
<code>a = Vx·cosθ3·cosθ2 + Vz·sinθ2</code>,<code>b = −Vx·sinθ3</code>,
|
||||
@@ -303,25 +322,55 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
最后 θ2 与 θ1 各由一个 2×2 线性方程组唯一确定:
|
||||
<code>cosθ2 = (κA + Vz·n)/(κ² + Vz²)</code>,<code>sinθ2 = (Vz·A − κ·n)/(κ² + Vz²)</code>,
|
||||
<code>κ = Vx·cosθ3</code>;<code>θ1 = φ − atan2(c, b)</code>。</li>
|
||||
<li><b>遍历冗余</b>:(θ4, θ5) 就是二维冗余。在 (θ4, θ5) 的限位盒内<b>全域扫描</b>(区间端点必采,含上下限位),
|
||||
每个网格点闭式求出全部支 ⇒ <b>覆盖整个可行解族</b>;再对最优的若干互异候选做 h/8 局部加密 + 模式搜索,
|
||||
得到 <b>全局最小 Σ|θ|</b>,误差量级 ≤ 扫描步长。</li>
|
||||
<li><b>遍历冗余(按行裁剪到可行段)</b>:(θ4, θ5) 就是二维冗余。闭式支只在平面支有解时才存在,
|
||||
限位盒内只有 <b>7~27%</b> 的网格点能过判据,且实测(5 个目标 × 全部 θ4 行)<b>每行的可行集都是单一连续段</b>
|
||||
—— 于是先用探测步找出每行的可行段、向两端各放宽一个探测步兜住段界,段内再按步长精扫。
|
||||
段内密度与全域扫一致,因此<b>解的质量不动</b>,粗扫采样量降约 4.6×(但全量总耗时只降约 1.10×——局部加密窗口是 ±2h 步长 h/8,格数恒为 32×32/种子、与 h 无关,精修才是耗时地板)。
|
||||
之所以不把粗扫一起放松:根枚举目前不保证完备,漏根时要靠粗扫的窄带候选兜底,
|
||||
实测把粗扫同步放松会换来上百度级的代价劣化。</li>
|
||||
<li><b>姿态任务(6 自由度)</b>:工具坐标系姿态 <code>R = R₁·R₂·…·R₆</code>(与场景图 6 级矩阵链一致)。
|
||||
结构上 <b>J5 轴与 J6 轴在腕心 (18.5, 128, 222) 垂直相交</b>,因此 θ6 只提供绕工具自身轴的滚转:
|
||||
结构上 <b>J5 轴与 J6 轴在腕心 (19.5, 128, 222) 垂直相交</b>,因此 θ6 只提供绕工具自身轴的滚转:
|
||||
<u>工具轴指向 = (R₁…R₅)·ẑ,与 θ6 无关,TCP 位置也与 θ6 无关</u>。每条位置闭式支都附带确定的 R₅
|
||||
(= R₁…R₅),滚转角随之闭式解出:<code>N = R₅ᵀ·R_des</code>(需为 Rz(−θ6))⇒
|
||||
<code>θ6 = −atan2(N₁₀, N₀₀)</code>;姿态偏差 <code>e = ∠(R₅·Rz(−θ6), R_des)</code> ≤ 2° 的支才收入。</li>
|
||||
<li><b>姿态达标优先</b>:达标解在 (θ4,θ5) 平面上是一条斜窄谷(奇异邻域灵敏度可达 ~100°/°),
|
||||
按 Σ|θ| 取种子会整体错过它。达标带(e ≤ 1°)按 e 升序取种子、局部窗口加密、必要时 escalating
|
||||
(姿态惩罚 ×100 重扫),最后做 <b>二维牛顿收口</b>:对“工具轴在目标轴法平面上的切向偏差”迭代,
|
||||
每步在新 (θ4,θ5) 上重解闭式位置支(按 θ1..θ3 最近匹配取同支,位置天然保持精确),把 e 压到 ~10⁻³° 量级。
|
||||
<b>最终只在 e ≤ 0.05° 的达标候选里按含连续项代价选优——不为省几度关节角牺牲姿态精度</b>;
|
||||
确有 ~5% 的均匀随机姿型落在奇异“刀尖”上(姿态仅在奇异点精确可达),此时按最近位姿返回并如实报告残差。</li>
|
||||
<li><b>连续优先(防倒摆)</b>:等价多分支(θ3 的 ±σ/±(π−σ)、肘上/肘下)之间只按 Σ|θ| 选优时,目标微动就会在分支间
|
||||
跳来跳去,某轴大幅倒摆(实测 4mm 步长过可达边界最大跳 81.9°)。交互求解以<b>当前手臂姿态</b>为参考位姿,
|
||||
代价加入 <code>λ·Σwrap(θᵢ−refᵢ)²</code>(λ=0.02):同族分支被锁定,仅当近支越限或误差超差才换支。
|
||||
A/B 实测(4 条扫描路径):>10° 跳变 14→5 次,最大跳变 163.5°→16.8°,平均 Σ|θ| 不升反降。</li>
|
||||
<li><b>代表角</b>:J1 / J3 / J6 为 ±180° 整周轴,取 |θ| 最小的等价角;J2 / J4 / J5 有限位,越限即丢弃。</li>
|
||||
<li><b>姿态 = 孤立根求解</b>:闭式支把 θ1..θ3 吸收掉之后,姿态任务只剩「工具轴指向 = 目标轴」
|
||||
这 2 个方程、2 个未知数 (θ4,θ5),解是<b>孤立点</b>(独立密集种子 + 牛顿普查实测只有 1~2 个根),
|
||||
滚转再由 θ6 闭式补上。所以不再用网格密度去撞那条斜窄谷:逐行对残差两个分量找<b>变号括号</b>、
|
||||
从括号中点起跑<b>阻尼高斯-牛顿</b>(奇异时加大阻尼而非退出),并对每支的 ‖e‖ 最小节点补种以收
|
||||
切向相切的根;代价最优的若干条入选候选再各收口一次。于是
|
||||
<b>姿态残差量级从 ~10⁻²° 变为 10⁻⁶°(工程意义上的精确)</b>,且 Σ|θ| 不劣于原路径。</li>
|
||||
<li><b>两处判据陷阱</b>(都验过,写在这里防止被自证):其一,切向残差在工具轴<b>反平</b>时同样为零,
|
||||
同向判据只能在收口时验,当作迭代中止条件会让每支一进来就作废;其二,求根按<b>支下标</b>对齐
|
||||
prev/cur,不可行的支必须留 null 占位,否则下标错开、括号判据失配,实测会整支漏掉一个真根、
|
||||
多付 130° 关节角。确有少数奇异「刀尖」构型<b>无精确根</b>,此时牛顿收敛到 ‖e‖ 的真实最小,
|
||||
报出的残差不再是采样伪影。</li>
|
||||
<li><b>混合优先(比重可调折中)</b>:优先级下拉的第三种模式——一根滑杆自由分配两端:
|
||||
<b>0% 即姿态优先、100% 即位置优先</b>(两端直接交给上面两条专用算法,结果逐位一致),
|
||||
中间值在两端锚点之间最小化<b>归一化折中代价</b>
|
||||
<code>J = w·(e/e_ref)² + (1−w)·(o/o_ref)²</code>。两端尺度取锚点自身:
|
||||
<code>e_ref</code> = 姿态优先解的 TCP 位置差、<code>o_ref</code> = 位置优先解的姿态残差,
|
||||
于是<b>滑杆读数就是「沿折中历程的进度」</b>、与目标的绝对误差量级无关(50% ≈ 两端各自归一后对半)。
|
||||
之所以不用「1 mm ↔ 1°」的绝对量纲:实测两端误差常在 100 mm / 40° 量级,绝对量纲会把读数整体挤向
|
||||
位置端(折线中点落到 ~13% 处),滑杆就不好用了。两端锚点复用上面两条算法——位置端 = 位置精确族 +
|
||||
闭式滚转 + 孤立根收口(位置不可达时退化为最近可达位置),姿态端 = 工具轴闭式对齐 + θ6 补滚转
|
||||
(姿态精确、位置最近);中间值以两锚点及<b>关节空间插值</b>为种子,对 6 维残差
|
||||
<code>[√w·(p−t)/e_ref , √(1−w)·RV°/o_ref]</code> 做<b>阻尼高斯-牛顿 + 单轴抛光</b>,取 J 最小者
|
||||
(J 同量级 2% 带内取行程 — 相对当前姿态的 Σ|wrap(θᵢ−refᵢ)| — 更小者,防等价支横跳)。两端中任一端在另一维也达标
|
||||
(≤0.05)时折中无意义、直接返回该端——注意位置端不达标而姿态端在位置上达标时返回<b>姿态端</b>
|
||||
(它两维同时占优:位置优先族收不到的到达解,姿态端收得到)。全程无随机数,同一输入逐位可复现;
|
||||
拖动走快路(同「实时续解」条哲学,归一化尺度沿用最近一次全量的缓存),松手必走全量。</li>
|
||||
<li><b>连续优先(最小移动角 / 防倒摆)</b>:等价多分支(θ3 的 ±σ/±(π−σ)、肘上/肘下、J1/J3 镜像族)之间只按 Σ|θ| 选优时,
|
||||
目标微动就会在分支间跳来跳去、绕远路(实测目标挪 20 mm 就翻族:垂直向下时左右移动单步最大 74.8°、
|
||||
累计绕出 348°,而目标实际只扫过 160°)。交互求解以<b>当前手臂姿态</b>为参考位姿,选解判据取
|
||||
<b>所有电机需要转过的角度之和最小</b>——<code>Σ|wrap180(θᵢ−refᵢ)|</code>:连续轴(J1/J3/J6)无机械限位,
|
||||
θ 与 θ±360 是同一物理位形,故角差一律取短弧;末尾 <code>1e-6·Σ|θ|</code> 只作并列时的次序稳定项。
|
||||
同一条扫描路径实测:单步最大 5.0°、累计 160.0°,与目标实际扫过的角度一致。
|
||||
拖动动画同样对连续轴取「离起始角最近的等价角」再插值,避免 +151.9°→−167.0° 反向扫 318.9° 绕一整圈。</li>
|
||||
<li><b>实时续解(拖动快路)</b>:目标每帧只挪几毫米时,真解在 (θ4,θ5) 平面上连续移动,
|
||||
没必要为重扫整个冗余维付全价。<code>solveWarm</code> 只在上一帧解附近取<b>同一闭式支</b>、走一小步同支搜索,
|
||||
姿态任务再收口一次;<b>任一校验不过(换支、越限、位置残差超差、姿态没收口、关节跳太大)就返回 null
|
||||
自动回落全量粗扫</b>,松手后的求解一定不带 warm、走全量。快路只负责「能快的时候快」,不改变正确性判据。</li>
|
||||
<li><b>代表角</b>:位姿以 360° 为周期,解角一律取 ±180° 内的等价角(|θ| 最小);J2 / J4 / J5 另受相邻件实测机械区间约束,越限即丢弃。</li>
|
||||
<li><b>自检</b>:每次求解都用闭式 FK 与场景图真实 6 级矩阵链各算一次 TCP 和工具姿态并与目标比对(见上表残差)。</li>
|
||||
</ol>
|
||||
<hr>
|
||||
@@ -333,23 +382,35 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
<tr><td>逆解分支重构(200 组随机可行位形)</td><td class="good">200 / 200 命中</td></tr>
|
||||
<tr><td>θ6 不变性(0° → 137°,TCP 位移)</td><td class="good">0.00 mm</td></tr>
|
||||
<tr><td>姿态参数 ↔ 旋转矩阵往返(2000 组随机)</td><td class="good">2.4 × 10⁻⁶°</td></tr>
|
||||
<tr><td>姿态重构达标率(200 组随机可行位形,0.5° 步长)</td><td class="good">185–191 / 200(4–7% 奇异刀尖构型残差 ≤ 2°,如实报告)</td></tr>
|
||||
<tr><td>姿态重构达标率(200 组随机可行位形,step 0.5,A/B 同机)</td><td class="good">中位残差 0.0234° → <b>0.000000°</b>;>0.05° 的例数 2 → <b>0</b>(改造前文档口径 185–191/200)</td></tr>
|
||||
<tr><td>姿态解 Σ|θ| vs 改造前(60 组,step 0.25 + 连续项)</td><td>中位 +0.03°,最大 +8.4°(该例是新解更贴参考位姿、Σ|θ| 反而更大,属连续项的正常取舍而非漏解)</td></tr>
|
||||
<tr><td>位置解 vs 0.05° 参考网格(100 目标,step 0.25)</td><td class="good">中位 +0.000°,最大 +0.49°,>1° 例数 0</td></tr>
|
||||
<tr><td>全量求解耗时(60 组,step 0.25 + 连续项)</td><td>仅位置 70.6 → 63.3 ms;位置+姿态 88.8 → 81.0 ms(约 1.10×;粗扫采样量降 4.6×,但精修才是地板)</td></tr>
|
||||
<tr><td>实时续解快路(每帧 3 mm + 1.5°,240 帧)</td><td class="good">位置命中 96%、均 0.058 ms;姿态命中 65%、均 0.59 ms —— 对全量 81 ms 约 140~1400×,是交互延迟的主要收益来源</td></tr>
|
||||
<tr><td>快路结果的独立复核(fkChain / 全 6 轴 orient,非求解器自报)</td><td class="good">位置 5.5×10⁻¹³ mm · 姿态 1.7×10⁻⁶°</td></tr>
|
||||
<tr><td>姿态模式候选表长度(供互异构型/碰撞改选取样)</td><td>26 → 13(精确根集合本就只有 1~2 个,达标门收紧后可选样变少;动态碰撞改选若依赖宽候选需留意)</td></tr>
|
||||
<tr><td>滚转角交互 roll 0→90(参考位姿连续)</td><td class="good">Δθ6 = 89.99°,θ1..θ5 变化 < 0.01°</td></tr>
|
||||
<tr><td>60 个可达目标:失败次数 / 最大残差</td><td class="good">0 次 / 3.5 × 10⁻¹³ mm</td></tr>
|
||||
<tr><td>相对 0.05° 极细网格参考的最大劣化</td><td class="good">0.02°</td></tr>
|
||||
<tr><td>vs 4000 次随机多起点数值解</td><td class="good">0 / 20 次劣于数值解</td></tr>
|
||||
<tr><td>等价分支跳变 · 4 条扫描路径(修复前 → 后)</td><td class="good">>10° 跳变 14 → 5 次;最大单步 163.5° → 16.8°</td></tr>
|
||||
<tr><td>单次求解耗时(默认 0.25°)</td><td>约 0.1 s(1° ≈ 30 ms,0.05° ≈ 1.1 s;开姿态后同级或更快)</td></tr>
|
||||
<tr><td>单次求解耗时</td><td>拖动跟随走快路 ~0.1 ms;全量求解默认步长下位置 ~20 ms、姿态 ~100 ms(node 侧本机实测,浏览器同量级)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<b>限位</b>:J2 ∈ [−106, 9],J4 ∈ [−111, 16],J5 ∈ [−95, 34],J1 / J3 / J6 为 ±180°(可整周)。
|
||||
<br><b>局限</b>:本算法为纯运动学求解,只受各关节行程限位约束,<u>不含跨关节自碰撞校核</u>。
|
||||
原模型限位来自全对全动态扫描;若需要更舒展的姿态,可从上方“互异构型”列表改选次优解。
|
||||
<b>限位</b>:电机行程各轴 ±360°(=滑块范围);解角取 ±180° 内的等价角——位姿以 360° 为周期,超出 ±180° 不产生新位姿。
|
||||
相邻打印件的实测机械区间封顶:J2 ∈ [−107, 128],J4 ∈ [−112, 112],J5 ∈ [−96, 96],J1 / J3 / J6 可整周。
|
||||
<br><b>参考</b>:零位静态扫描 J2 [−106, 9]、J4 [−111, 16]、J5 [−95, 34] 只在其它轴全零时对固定立柱成立,已不再当可行域;
|
||||
跨组零件对(腕/法兰撞立柱那类)由胶囊碰撞模型按当前姿态实时判定(实测 J1=90° 时 J2 正端从 +8° 放到 +107°)。
|
||||
<br><b>局限</b>:胶囊模型故意不检相邻组零件对(|Δg|=1),那部分由上面的实测机械区间兜底;换模型须重跑
|
||||
scratch/probe_adj.py 更新 collide.json 的 adj。若需要更舒展的姿态,可从上方“互异构型”列表改选次优解。
|
||||
<br><b>不可达</b>:若目标超出工作空间,自动求最近可达位姿——阻尼最小二乘<b>从当前姿态锚定起步</b>(抑制退化方向的无用漂移)、固定种子的确定性重启(同一点重复解算逐位一致)、再经零空间抛光取最小总关节角的代表值;报告的距离为抛光后真实值。
|
||||
<br><b>姿态不可达</b>:姿态任务超可达空间时,阻尼最小二乘误差向量升级为 6 维(位置 mm + 姿态 deg,按 20 mm/rad 折算),返回位置与姿态联合最近的位姿,两个残差分别显示。
|
||||
<br><b>混合优先不可达</b>:位置超可达空间时,位置端锚点退化为「最近可达位置」、姿态端仍取「姿态精确、位置最近」,
|
||||
再按同一权重折中,两端各自的残差在结果栏如实列出(与位置优先的历史兜底——6 维联合 DLS——是两个不同的代表值,切换模式可得)。
|
||||
<br><b>API</b>:控制台可用 <code>IK.solver.solve([x,y,z],{step:0.25,tool:0,pose:{R:ArmIK.poseMat(az,el,roll)}})</code>;
|
||||
<code>IK.setPose(az,el,roll)</code> 可直接设定目标姿态。
|
||||
<code>IK.setPose(az,el,roll)</code> 可直接设定目标姿态;<code>IK.setPrio('mix',0.3)</code> 切到混合优先并设比重;
|
||||
<code>IK.solver.solveTo([x,y,z],{pose:{R:…},priority:'mix',mixW:0.3})</code> 直接取折中解(0 与 1 分别等于姿态优先 / 位置优先)。
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -403,6 +464,7 @@ body.motion #bomDock{top:124px} body.motion #bomTab{top:140px}
|
||||
const DATA = /*__DATA__*/;
|
||||
const SHEETS = /*__SHEETS__*/;
|
||||
const TRAVEL = /*__TRAVEL__*/;
|
||||
const COLLIDE = /*__COLLIDE__*/;
|
||||
const TL = {}; for (const t of TRAVEL) TL[t.axis] = t;
|
||||
const V3 = THREE.Vector3;
|
||||
const VC = p => [p[0], p[2], -p[1]]; // CAD (x,y,z) -> viewer (x,z,-y); must precede use
|
||||
@@ -430,15 +492,20 @@ for (const j of JO) {
|
||||
const g = new THREE.Object3D(); g.matrixAutoUpdate = false;
|
||||
JG[j.id] = g; JG[parentOf[j.id]].add(g);
|
||||
}
|
||||
/* 轴心 P / 轴向 D 只与 CAD 参数有关:预计算一次;矩阵合成改用 3 个共享实例。
|
||||
旧版每次调用新建 2 个 Vector3 + 3 个 Matrix4(一次关节写入 = 6 次调用 = 30 个临时对象),
|
||||
动画与实时追踪高频触发时纯属垃圾。数学与原式逐位相同:T(P)·R(D,θ)·T(−P)。 */
|
||||
const JP = {}, JD = {};
|
||||
for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); }
|
||||
const _jmT = new THREE.Matrix4(), _jmR = new THREE.Matrix4(), _jmTi = new THREE.Matrix4();
|
||||
function applyJoint(id) {
|
||||
const j = JO.find(q => q.id === id); if (!j) return;
|
||||
// geometry vertices were built in the viewer frame (x, z, -y), so the pivot point and the
|
||||
// axis must go through the same swap - using raw CAD coords rotates about the wrong line
|
||||
const P = new V3(...VC(j.rear)), D = new V3(...VC(j.d)).normalize();
|
||||
const P = JP[id], D = JD[id]; if (!P) return;
|
||||
const a = (ang[id] || 0) * Math.PI / 180;
|
||||
const m = new THREE.Matrix4().makeTranslation(P.x, P.y, P.z)
|
||||
.multiply(new THREE.Matrix4().makeRotationAxis(D, a))
|
||||
.multiply(new THREE.Matrix4().makeTranslation(-P.x, -P.y, -P.z));
|
||||
const m = _jmT.makeTranslation(P.x, P.y, P.z)
|
||||
.multiply(_jmR.makeRotationAxis(D, a))
|
||||
.multiply(_jmTi.makeTranslation(-P.x, -P.y, -P.z));
|
||||
JG[id].matrix.copy(m);
|
||||
JG[id].matrixWorldNeedsUpdate = true;
|
||||
requestRender();
|
||||
@@ -454,8 +521,11 @@ for (const p of DATA.parts) {
|
||||
p.triCount = p.t ? p.t.length : 0;
|
||||
p.vertCount = p.v.length;
|
||||
const isEnv = p.name.slice(0, 4) === 'ENV_'; // 可达包络:运动学边界,不随关节动
|
||||
/* 索引按顶点数降位:单件 ≤65535 顶点时 Uint16 合法(92 件实测最大 47483),
|
||||
索引内存与上传带宽减半;超大件保留 Uint32 兜底。 */
|
||||
const IDX16 = p.v.length <= 65535;
|
||||
if (p.kind === 'line') { // 经纬线:LineSegments,无三角面
|
||||
const sIdx = new Uint32Array(p.s);
|
||||
const sIdx = IDX16 ? new Uint16Array(p.s) : new Uint32Array(p.s);
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
||||
geo.setIndex(new THREE.BufferAttribute(sIdx, 1));
|
||||
geo.computeBoundingSphere(); geo.computeBoundingBox();
|
||||
@@ -470,7 +540,7 @@ for (const p of DATA.parts) {
|
||||
delete p.v; delete p.s;
|
||||
continue;
|
||||
}
|
||||
const idx = new Uint32Array(p.t.length * 3);
|
||||
const idx = IDX16 ? new Uint16Array(p.t.length * 3) : new Uint32Array(p.t.length * 3);
|
||||
// the vertex map (x,y,z)->(x,z,-y) has determinant +1, so triangle winding must be kept
|
||||
// as-is; swapping two indices here inverted every face and made the normals point inward
|
||||
for (let i = 0; i < p.t.length; i++) { idx[i*3] = p.t[i][0]; idx[i*3+1] = p.t[i][1]; idx[i*3+2] = p.t[i][2]; }
|
||||
@@ -566,16 +636,23 @@ cv.addEventListener('pointerdown', e => {
|
||||
panDist = 0;
|
||||
cv.setPointerCapture(e.pointerId);
|
||||
});
|
||||
let hoverPending = false, lastHoverEv = null;
|
||||
/* 悬浮拾取限流:射线要对 34 万三角面全量求交(实测 ≈0.5~1.5 ms/次),
|
||||
旧版每个 pointermove 都算(rAF 合帧后仍≈显示器刷新率次数);
|
||||
这里加 60 ms 时间配额 + 位置去重,指针静止时一次都不算,尾随仍会补最新位置,
|
||||
tooltip 观感不变。点击 / 右键拾取不经过此函数,仍即时求交。 */
|
||||
let hoverPending = false, lastHoverEv = null, hoverT = -1e9, hoverX = -1, hoverY = -1;
|
||||
const HOVER_MS = 60;
|
||||
function schedHover(e) {
|
||||
lastHoverEv = e;
|
||||
if (!hoverPending) {
|
||||
if (e.clientX === hoverX && e.clientY === hoverY) return; // 没移动:结果不可能变
|
||||
hoverX = e.clientX; hoverY = e.clientY; lastHoverEv = e;
|
||||
if (hoverPending) return;
|
||||
hoverPending = true;
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
hoverPending = false;
|
||||
if (lastHoverEv && !drag) hover(lastHoverEv);
|
||||
});
|
||||
}
|
||||
if (!lastHoverEv || drag) return;
|
||||
hoverT = performance.now();
|
||||
hover(lastHoverEv);
|
||||
}, Math.max(0, HOVER_MS - (performance.now() - hoverT)));
|
||||
}
|
||||
cv.addEventListener('pointermove', e => {
|
||||
if (!drag) { schedHover(e); return; }
|
||||
@@ -592,12 +669,23 @@ cv.addEventListener('wheel', e => { e.preventDefault(); rd *= (1 + Math.sign(e.d
|
||||
|
||||
/* ---- picking & hiding ---- */
|
||||
const ray = new THREE.Raycaster(); let sel = null, hov = null;
|
||||
/* 拾取列表与 NDC 向量复用同一实例:旧版每次 pointermove 都 filter 出一个新数组 +
|
||||
新建 Vector2(高频下纯垃圾)。可见性逐次重算,语义与 filter 完全一致
|
||||
——three r159 的 Raycaster 不检查 object.visible,只能靠这里过滤。 */
|
||||
const pickList = [], ndcPick = new THREE.Vector2();
|
||||
function pick(e) {
|
||||
const r = cv.getBoundingClientRect();
|
||||
ray.setFromCamera(new THREE.Vector2(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1), cam);
|
||||
const hit = ray.intersectObjects(meshes.filter(m => m.visible), false)[0];
|
||||
ndcPick.set(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1);
|
||||
ray.setFromCamera(ndcPick, cam);
|
||||
pickList.length = 0;
|
||||
for (const m of meshes) if (m.visible) pickList.push(m);
|
||||
const hit = ray.intersectObjects(pickList, false)[0];
|
||||
return hit ? hit.object : null;
|
||||
}
|
||||
/* BOM 表高亮行只记一行:旧版每次点选/ESC 都要把整张表(13 行零件 + 10 行螺钉)
|
||||
的 style.background 全刷一遍,纯属多余 DOM 写入。 */
|
||||
let bomHlRow = null;
|
||||
function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } }
|
||||
function hover(e) {
|
||||
const o = pick(e); if (o === hov) return; hov = o;
|
||||
const n = document.getElementById('note');
|
||||
@@ -650,8 +738,7 @@ window.addEventListener('keydown', e => {
|
||||
sel = null;
|
||||
const hud = document.getElementById('hud');
|
||||
if (hud) hud.textContent = '';
|
||||
const tb = document.querySelector('#bom tbody');
|
||||
if (tb) for (const tr of tb.children) tr.style.background = '';
|
||||
bomHlClear();
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
@@ -660,7 +747,7 @@ cv.addEventListener('click', e => {
|
||||
const o = pick(e);
|
||||
if (o === sel) {
|
||||
if (sel) { sel.material.emissive.setHex(0); sel.material.emissiveIntensity = 0; sel = null; }
|
||||
const tb = document.querySelector('#bom tbody'); if (tb) for (const tr of tb.children) tr.style.background = '';
|
||||
bomHlClear();
|
||||
clearScrewHl();
|
||||
const hud = document.getElementById('hud'); if (hud) { hud.textContent = ''; delete hud.dataset.scrHl; }
|
||||
requestRender();
|
||||
@@ -669,7 +756,8 @@ cv.addEventListener('click', e => {
|
||||
if (sel && !sel.userData.screw) { sel.material.emissive.setHex(0); sel.material.emissiveIntensity = 0; }
|
||||
sel = o;
|
||||
if (sel && !sel.userData.screw) { sel.material.emissive.setHex(0x2f6bff); sel.material.emissiveIntensity = .55; }
|
||||
const tb = document.querySelector('#bom tbody'); for (const tr of tb.children) tr.style.background = '';
|
||||
const tb = document.querySelector('#bom tbody');
|
||||
bomHlClear();
|
||||
const hud = document.getElementById('hud');
|
||||
hud.textContent = '';
|
||||
delete hud.dataset.scrHl;
|
||||
@@ -680,31 +768,50 @@ cv.addEventListener('click', e => {
|
||||
' cm3 · ' + (u.printed ? '打印件' : '电机/已有件') +
|
||||
' <span style="margin-left:8px;color:var(--warn);font-size:11px">[右键单击或按H键隐藏此部件]</span>';
|
||||
const tr = tb.querySelector('[data-idx="' + u.idx + '"]');
|
||||
if (tr) { tr.style.background = '#22406b'; tr.scrollIntoView({block:'nearest'}); }
|
||||
if (tr) { tr.style.background = '#22406b'; bomHlRow = tr; tr.scrollIntoView({block:'nearest'}); }
|
||||
}
|
||||
requestRender();
|
||||
});
|
||||
|
||||
/* ---- UI: joint sliders, ranges from the swept hard stops (docs/travel.json) ---- */
|
||||
/* ---- UI: joint sliders ----
|
||||
滑块范围 = 电机行程 ±360°(docs/collide.json 的 motor_limit_deg;原 ±106° 作废)。
|
||||
能拖到哪由几何说话:相邻件实测自由区间(与姿态无关)封顶,跨组零件对由胶囊碰撞
|
||||
模型按当前姿态实时夹取(见 ik-glue 的 IK.clampAxis);docs/travel.json 的零位静态
|
||||
扫描值只作 tooltip 参考——它是"其它轴全零"时对固定立柱的一次性结果,随姿态大变。 */
|
||||
const JNAMES = ['J1','J2','J3','J4','J5','J6'];
|
||||
const MOTOR = (typeof COLLIDE !== 'undefined' && COLLIDE.motor_limit_deg) || 360;
|
||||
const jh = document.getElementById('joints');
|
||||
for (const id of JNAMES) {
|
||||
const j = JO.find(q => q.id === id); if (!j) continue;
|
||||
const t = TL[id] || {lo:-180, hi:180, continuous:true, note:'360°旋转'};
|
||||
const lo = Math.floor(t.lo), hi = Math.ceil(t.hi);
|
||||
const lo = -MOTOR, hi = MOTOR;
|
||||
const d = document.createElement('div');
|
||||
d.className = 'j-box';
|
||||
d.innerHTML = '<div class="row"><label title="' + t.note + '(双击复位 0°)">' + id + ' ' + j.cn +
|
||||
(t.continuous ? ' <em class="ok">360°连续</em>' : ' <em>' + lo + '°…' + hi + '°</em>') +
|
||||
d.innerHTML = '<div class="row"><label title="' + t.note +
|
||||
'(电机行程 ±' + MOTOR + '°=滑块范围;零位静态扫描 ' + Math.floor(t.lo) + '°…' + Math.ceil(t.hi) +
|
||||
'°仅作参考,实际边界随姿态由碰撞模型实时夹取。双击复位 0°)">' +
|
||||
id + ' ' + j.cn +
|
||||
(t.continuous ? ' <em class="ok">360°连续</em>' : ' <em id="e' + id + '">' + lo + '°…' + hi + '°</em>') +
|
||||
'</label><span class="j-val" id="v' + id + '" title="点击复位为 0°">0°</span></div>' +
|
||||
'<input type="range" id="s' + id + '" min="' + lo + '" max="' + hi + '" value="0">';
|
||||
jh.appendChild(d);
|
||||
const inp = d.querySelector('input');
|
||||
inp.oninput = ev => { ang[id] = +ev.target.value;
|
||||
document.getElementById('v'+id).textContent = ang[id] + '°';
|
||||
for (const q of JNAMES) applyJoint(q); };
|
||||
inp.oninput = ev => {
|
||||
let v = +ev.target.value;
|
||||
if (window.IK && IK.clampAxis) { // 动态限位夹取(glue 加载后才生效)
|
||||
const c = IK.clampAxis(id, v);
|
||||
if (c !== v) { v = c; ev.target.value = c; } // 滑块头停在真实边界上
|
||||
}
|
||||
ang[id] = v;
|
||||
document.getElementById('v'+id).textContent = v + '°';
|
||||
for (const q of JNAMES) applyJoint(q);
|
||||
};
|
||||
d.querySelector('.j-val').onclick = () => { inp.value = 0; inp.dispatchEvent(new Event('input')); };
|
||||
d.querySelector('label').ondblclick = () => { inp.value = 0; inp.dispatchEvent(new Event('input')); };
|
||||
inp.onchange = () => { // 松手后异步刷新动态范围标签(拖拽中不刷,避免卡顿)
|
||||
if (window.IK && IK.refreshJointLabels) setTimeout(() => IK.refreshJointLabels(
|
||||
['J1','J2','J3','J4','J5','J6'].map(x => (document.getElementById('s'+x) || {}).value || 0)), 0);
|
||||
};
|
||||
applyJoint(id);
|
||||
}
|
||||
const bRstJ = document.getElementById('resetJoints');
|
||||
@@ -1011,9 +1118,14 @@ function updStat() {
|
||||
}
|
||||
updStat();
|
||||
|
||||
/* 按需渲染:needRender 由 requestRender() 置位(所有交互与状态变化都已调用它;
|
||||
旧版置位后全文件无读取点,等于恒以显示器刷新率空转满帧渲染)。
|
||||
装配图页画布隐藏时【不清标志】,切回三维页由 tab 处理器 resize()+requestRender() 补渲染。
|
||||
preserveDrawingBuffer 必须保持 true:不重绘时靠它保住最后一帧画面。 */
|
||||
function loop() {
|
||||
if (needRender && cv.style.display !== 'none') {
|
||||
needRender = false;
|
||||
resize();
|
||||
if (cv.style.display !== 'none') {
|
||||
updCam();
|
||||
rend.render(scene, cam);
|
||||
}
|
||||
|
||||
+1277
-54
File diff suppressed because it is too large
Load Diff
+401
-51
@@ -29,19 +29,47 @@ const TCP0 = [JJ.J6.rear[0], JJ.J6.rear[1], tcpZ];
|
||||
|
||||
/* ---------- 1. 建立求解器 ---------- */
|
||||
const JMap = {}; for (const j of JO) JMap[j.id] = { rear: j.rear, d: j.d };
|
||||
const TR = {}; for (const t of TRAVEL) TR[t.axis] = { lo: t.lo, hi: t.hi, continuous: !!t.continuous };
|
||||
const S = ArmIK.make({ joints: JMap, travel: TR, tcp: TCP0 });
|
||||
const COL = (typeof COLLIDE !== 'undefined' && COLLIDE.caps && COLLIDE.caps.length) ? COLLIDE : null;
|
||||
/* 可行域不再取 docs/travel.json 的静态扫描值——那是"其它轴全零"时对固定立柱的一次
|
||||
性结果,随姿态大变(电机行程现为 ±360°,见 collide.motor_limit_deg)。改由两处
|
||||
真实几何决定:adj = 相邻打印件 1D 自由区间(CAD 实测、与姿态无关,胶囊模型故意
|
||||
不检相邻对);跨 ≥2 组的零件对由 dynLimit 按当前姿态实时二分。 */
|
||||
const ADJ = (COL && COL.adj) || {};
|
||||
const MOTOR = +((COL && COL.motor_limit_deg) || 360); // 电机行程(滑块范围)
|
||||
const STATIC_TL = {}; for (const t of TRAVEL) STATIC_TL[t.axis] = { lo: t.lo, hi: t.hi };
|
||||
const TR = {};
|
||||
const noAdj = [];
|
||||
for (const t of TRAVEL) {
|
||||
const a = ADJ[t.axis];
|
||||
if (!a && !t.continuous) noAdj.push(t.axis);
|
||||
TR[t.axis] = { lo: a ? a[0] : t.lo, hi: a ? a[1] : t.hi, continuous: !!t.continuous };
|
||||
}
|
||||
/* 限位轴丢了 adj 就会退回 travel.json 的零位扫描值(表现为"又被锁回 −106"):
|
||||
fit_capsules.py 重写 collide.json 时已沿用 adj,若仍缺说明只跑了拟合没跑实测。 */
|
||||
if (COL && noAdj.length)
|
||||
console.warn('[IK] collide.json 缺 ' + noAdj.join('/') + ' 的 adj 实测区间,这几轴退回零位扫描限位;' +
|
||||
'重跑 python scratch/probe_adj.py --workers 10');
|
||||
const S = ArmIK.make({ joints: JMap, travel: TR, tcp: TCP0, collide: COL });
|
||||
const LIM = S.LIM;
|
||||
let lastTool = 0;
|
||||
let TGT = [150, 128, 260];
|
||||
/* 参考位姿连续项权重:以当前手臂姿态为参考解算,目标微动时锁定同族分支,
|
||||
避免某轴在等价分支间大幅倒摆(实测 4mm 步长过边界最多跳 81.9° -> 1.2°) */
|
||||
const REF_W = 0.02;
|
||||
/* 选解判据(用户指定):所有电机需要转过的角度之和最小 —— 以当前手臂姿态为参考。
|
||||
连续轴 θ 与 θ±360 等价,角差取 wrap180;不给 ref 时退化为 Σ|θ| 最小(离线批算)。
|
||||
(旧实现是 Σ|θ| + λ·ΣΔ² 的二次连续项,已被本判据取代。) */
|
||||
const curRef = () => ORDER.map(id => ang[id]);
|
||||
|
||||
/* ---------- 1b. 姿态控制状态 ---------- */
|
||||
let poseOn = false;
|
||||
const POSE = { az: 0, el: 90, roll: 0 }; // 目标姿态:工具轴方位角 / 仰角 + 绕轴滚转(度)
|
||||
/* 双优先级:位置与姿态在远目标上不可兼得(实测某目标位置 0mm 时姿态差 44°,而姿态 0° 时位置差 116mm)。
|
||||
'pos' 位置优先 —— 保 TCP 点位精确,姿态取最接近可达值;
|
||||
'ori' 姿态优先 —— 保指定姿态精确,TCP 取最接近可达点;
|
||||
'mix' 混合优先 —— 滑杆自由分配两端比重:0% = 姿态优先、100% = 位置优先(两端与纯算法逐位一致),
|
||||
中间值按两端锚点误差归一的折中代价 J = w·(e/e_ref)² + (1−w)·(o/o_ref)² 搜索。 */
|
||||
const prioVal = () => { const e = $('ikPrio'); const v = e && e.value; return v === 'ori' || v === 'mix' ? v : 'pos'; };
|
||||
const mixWVal = () => { const e = $('ikMixW'); return e ? Math.max(0, Math.min(100, +e.value || 0)) / 100 : 0.5; };
|
||||
const prioTxt = () => prioVal() === 'ori' ? '姿态优先'
|
||||
: (prioVal() === 'mix' ? '混合优先 ' + Math.round(mixWVal() * 100) + '% 位置' : '位置优先');
|
||||
const Rdes = () => ArmIK.poseMat(POSE.az, POSE.el, POSE.roll);
|
||||
const syncPoseInputs = () => {
|
||||
$('ikAz').value = (+POSE.az).toFixed(1);
|
||||
@@ -206,6 +234,68 @@ function refreshTarget() {
|
||||
requestRender();
|
||||
}
|
||||
|
||||
/* ---------- 2b. 滑块动态限位夹取(边界缓存)----------
|
||||
collideFree 一票 ~5ms(1995 胶囊、零位最坏),不允许每次 input 都二分。
|
||||
缓存 [轴, 其余轴 1° 量化姿态] → {lo, hi}:拖同一轴时其余轴不动,直接取缓存;
|
||||
只有拖过缓存边界(或换了姿态)才重算一次二分(~40ms,一次卡顿换正确边界)。 */
|
||||
const bnd = { key: '', lo: 0, hi: 0 };
|
||||
function clampAxis(id, v) {
|
||||
if (!COL) return v;
|
||||
const others = ORDER.map(x => x === id ? '-' : Math.round(ang[x])).join(',');
|
||||
const key = id + '|' + others;
|
||||
if (bnd.key !== key) {
|
||||
bnd.key = key;
|
||||
const q = ORDER.map(x => x === id ? ang[x] : Math.round(ang[x]));
|
||||
if (!S.collideFree(q)) {
|
||||
/* 当前姿本身已干涉(上次被拖越界后卡在界外):dynLimit 会把"当前值"当边界,
|
||||
滑块就再也回不来了。此时退回该轴机械区间(相邻件实测自由区间,必然无自碰撞)。 */
|
||||
bnd.lo = LIM[id].lo; bnd.hi = LIM[id].hi;
|
||||
} else {
|
||||
bnd.lo = S.dynLimit(id, q, -1);
|
||||
bnd.hi = S.dynLimit(id, q, 1);
|
||||
}
|
||||
}
|
||||
return Math.max(bnd.lo, Math.min(bnd.hi, v));
|
||||
}
|
||||
/* 刷新滑块标签里的动态范围(异步执行,避免阻塞拖拽) */
|
||||
/* 节流版:拖动/实时追踪中每次 setJoints 都会触发 3 次 dynLimit(每次 ~9 次 collideFree 二分 ≈ 28 ms),
|
||||
高频下把 0.1 ms 的快路放大 280×。250 ms 节流 + 尾随补一次;动画结束/松手仍走立即版。 */
|
||||
let _rjlLast = -1e9, _rjlTimer = 0;
|
||||
function refreshJointLabelsLazy(q) {
|
||||
const t = performance.now();
|
||||
if (t - _rjlLast > 250) { _rjlLast = t; refreshJointLabels(q); return; }
|
||||
if (!_rjlTimer) _rjlTimer = setTimeout(() => {
|
||||
_rjlTimer = 0; _rjlLast = performance.now();
|
||||
refreshJointLabels(ORDER.map(id => ang[id]));
|
||||
}, 260);
|
||||
}
|
||||
function refreshJointLabels(q) {
|
||||
for (const id of ORDER) {
|
||||
const el = $('e' + id);
|
||||
if (!el) continue;
|
||||
const t = LIM[id];
|
||||
if (COL && !t.cont) {
|
||||
/* 边界由几何说话:机械区间(adj 实测)之内,再按当前姿态二分胶囊碰撞。
|
||||
lo 侧一律收紧;hi 侧只有"硬边"(相邻件自碰撞,配置无关)才受静态值封顶,
|
||||
可放宽侧(撞固定立柱那类)由模型说话——动态边界本就可以远超静态 hi。 */
|
||||
const lo = Math.max(t.lo, S.dynLimit(id, q, -1, 1.0));
|
||||
const hardHi = COL.hard && COL.hard[id] && COL.hard[id].hi;
|
||||
const hi = hardHi ? Math.min(t.hi, S.dynLimit(id, q, 1, 1.0))
|
||||
: S.dynLimit(id, q, 1, 1.0);
|
||||
const dyn = Math.abs(lo - t.lo) > 0.5 || Math.abs(hi - t.hi) > 0.5; // 与静态不同(宽或窄)都标记
|
||||
el.innerHTML = (dyn ? '<b style="color:var(--warn)">' : '') +
|
||||
lo.toFixed(0) + '\u00B0\u2026' + hi.toFixed(0) + '\u00B0' + (dyn ? ' \u25B2</b>' : '');
|
||||
const st = STATIC_TL[id] || {};
|
||||
el.title = '机械区间(相邻件实测)' + t.lo + '\u2026' + t.hi +
|
||||
'\u00B0;电机行程 ±' + MOTOR + '\u00B0;当前姿态动态边界 ' +
|
||||
lo.toFixed(1) + '\u2026' + hi.toFixed(1) + '\u00B0(胶囊碰撞模型实时算)' +
|
||||
(st.lo != null ? '\n零位静态扫描参考 ' + st.lo + '\u2026' + st.hi + '\u00B0' : '');
|
||||
} else {
|
||||
el.textContent = t.lo + '\u00B0\u2026' + t.hi + '\u00B0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 3. 关节写入(同步滑块与标签)---------- */
|
||||
let anim = null;
|
||||
function setJoints(q, doAnim) {
|
||||
@@ -219,11 +309,24 @@ function setJoints(q, doAnim) {
|
||||
applyJoint(id);
|
||||
}
|
||||
refreshTarget();
|
||||
if (COL) refreshJointLabelsLazy(ORDER.map(id => ang[id]));
|
||||
}
|
||||
function animTo(q, ms) {
|
||||
if (anim) { anim = null; }
|
||||
const from = ORDER.map(id => ang[id]);
|
||||
anim = { from: from, to: q.slice(), t0: performance.now(), ms: ms };
|
||||
/* 连续轴(J1 肩抬举 / J3 大臂自转 / J6 腕自转)走「短弧」。
|
||||
三者物理上无机械限位,θ 与 θ±360 是同一物理位形(applyJoint 走 cos/sin);
|
||||
若按数值直接线性插值,+151.9° → −167.0° 会反向扫 318.9° 绕一整圈(实测)。
|
||||
这里先求「离起始角最近的等价角」作为插值终点,运动变成 41.1° 的短弧;
|
||||
end 保存求解器给的代表角,末帧落回它——与插值终点恰好相差 360° 的整数倍,
|
||||
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。 */
|
||||
const to = q.slice();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i];
|
||||
if (LIM[id].cont) to[i] = from[i] + S.wrap180(q[i] - from[i]);
|
||||
}
|
||||
const end = q.slice();
|
||||
anim = { from: from, to: to, t0: performance.now(), ms: ms };
|
||||
const tick = () => {
|
||||
if (!anim) return;
|
||||
const k = Math.min(1, (performance.now() - anim.t0) / anim.ms);
|
||||
@@ -238,7 +341,20 @@ function animTo(q, ms) {
|
||||
}
|
||||
refreshTarget();
|
||||
if (k < 1) requestAnimationFrame(tick);
|
||||
else { anim = null; for (let i = 0; i < 6; i++) applyJoint(ORDER[i]); requestRender(); }
|
||||
else {
|
||||
anim = null;
|
||||
/* 落回代表角(与插值终点等价,视觉无变化),使 ang 与 res.q 对齐 */
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i];
|
||||
ang[id] = end[i];
|
||||
const sl = $('s' + id), v = $('v' + id);
|
||||
if (sl) sl.value = end[i];
|
||||
if (v) v.textContent = end[i].toFixed(2) + '\u00B0';
|
||||
applyJoint(id);
|
||||
}
|
||||
if (COL) refreshJointLabels(ORDER.map(id => ang[id]));
|
||||
requestRender();
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
@@ -286,7 +402,9 @@ function renderResult(res, src) {
|
||||
tr.innerHTML = '<td>' + id + '</td><td>' + CN[id] + '</td>' +
|
||||
'<td class="num">' + q[i].toFixed(2) + '\u00B0</td>' +
|
||||
'<td class="num dim">' + Math.abs(q[i]).toFixed(1) + '</td>' +
|
||||
'<td class="dim">' + (t.cont ? '\u00B1180' : t.lo + '..' + t.hi) + '</td>';
|
||||
'<td class="dim">' + (t.cont ? '\u00B1' + MOTOR
|
||||
: (res.dynAxis === id ? t.lo + '..' + '<b style="color:var(--warn)">' + res.dynBound.toFixed(1) + '*</b>'
|
||||
: t.lo + '..' + t.hi)) + '</td>';
|
||||
if (i === 5 && !res.tool) tr.title = poseOn
|
||||
? '姿态控制开启:θ6 为绕工具轴的滚转角,由目标姿态(方位/仰角/滚转)解算'
|
||||
: 'TCP 位于 J6 轴线上,转动 J6 不改变 TCP 位置,故取 0 使总角最小';
|
||||
@@ -309,7 +427,9 @@ function renderResult(res, src) {
|
||||
const sum = q.reduce((a, b) => a + b, 0);
|
||||
const pa = poseOn ? ArmIK.poseAng(Uof(q)) : null;
|
||||
$('ikSum').innerHTML =
|
||||
'<i>总关节角 \u03A3|\u03B8|</i><b class="hi">' + S.costOf(q, null).toFixed(2) + '\u00B0</b>' +
|
||||
(res.moveSum != null ? '<i>本步行程 \u03A3|\u0394\u03B8|(判据)</i><b class="hi">' +
|
||||
res.moveSum.toFixed(2) + '\u00B0</b>' : '') +
|
||||
'<i>总关节角 \u03A3|\u03B8|</i><span>' + S.costOf(q, null).toFixed(2) + '\u00B0</span>' +
|
||||
'<i>\u03A3\u03B8 代数和</i><span>' + sum.toFixed(1) + '\u00B0</span>' +
|
||||
'<i>残差 闭式/场景图</i><span class="' + (dcl < 1e-4 && dsc < .05 ? 'good' : 'bad') + '">' +
|
||||
dcl.toExponential(1) + ' / ' + dsc.toExponential(1) + ' mm</span>' +
|
||||
@@ -319,6 +439,11 @@ function renderResult(res, src) {
|
||||
'\u00B0 · ' + pa.roll.toFixed(1) + '\u00B0</span>' : '') +
|
||||
(poseOn ? '<i>姿态残差 闭式/场景图</i><span class="' + (dori < .05 && doriSc < .1 ? 'good' : 'bad') + '">' +
|
||||
dori.toFixed(3) + '\u00B0 / ' + doriSc.toFixed(3) + '\u00B0</span>' : '') +
|
||||
(poseOn && res.priority === 'mix' && res.mixP && res.mixO
|
||||
? '<i>折中权重</i><span>位置 ' + Math.round((res.mixW == null ? 0.5 : res.mixW) * 100) + '% · 姿态 ' +
|
||||
(100 - Math.round((res.mixW == null ? 0.5 : res.mixW) * 100)) + '%(按两端锚点误差归一)</span>' +
|
||||
'<i>两端锚点</i><span>位置端 ' + res.mixP.e.toFixed(2) + ' mm / ' + res.mixP.o.toFixed(2) +
|
||||
'\u00B0 · 姿态端 ' + res.mixO.e.toFixed(1) + ' mm / ' + res.mixO.o.toFixed(2) + '\u00B0</span>' : '') +
|
||||
'<i>可行采样点</i><span>' + res.nSol + '</span>' +
|
||||
'<i>耗时</i><span>' + res.ms.toFixed(0) + ' ms</span>' +
|
||||
'<i>目标</i><span>(' + res.target.map(v => v.toFixed(1)).join(', ') + ')</span>' +
|
||||
@@ -328,10 +453,18 @@ function renderResult(res, src) {
|
||||
const oriOk = !poseOn || (dori < 0.05 && doriSc < 0.1);
|
||||
/* 以残差判到达:阻尼最小二乘兜底也可能精确到达(扫描漏掉的奇异刀尖解),不能只看 reachable */
|
||||
const posOk = dsc < .05;
|
||||
badge.textContent = (posOk && oriOk) ? '已到达'
|
||||
/* 姿态优先/混合姿态端:姿态精确、TCP 落在最近可达点——与「不可达」语义不同,单独标出并给出距离 */
|
||||
const oriPrio = poseOn && (res.priority === 'ori' || res.priority === 'mix') && oriOk && !posOk;
|
||||
/* 混合折中:位置与姿态都有残差(两端都不可兼得时的加权折中点) */
|
||||
const mixMid = poseOn && res.priority === 'mix' && !posOk && !oriOk;
|
||||
badge.textContent = res.unsafe
|
||||
? '位姿与底座/立柱近似干涉(目标可能不可达或位于立柱区内)'
|
||||
: (posOk && oriOk) ? '已到达'
|
||||
: oriPrio ? '姿态到达·位置最近 ' + dsc.toFixed(1) + ' mm'
|
||||
: mixMid ? '混合折中 · 位置差 ' + dsc.toFixed(1) + ' mm · 姿态残差 ' + dori.toFixed(2) + '\u00B0'
|
||||
: (posOk && poseOn && dori > 0.05 ? '位置到达·姿态残差 ' + dori.toFixed(2) + '\u00B0' : '不可达·最近位姿');
|
||||
badge.className = 'badge ' + ((posOk && oriOk) ? 'goodb'
|
||||
: (posOk ? 'warnb' : 'badb'));
|
||||
badge.className = 'badge ' + (res.unsafe || (!posOk && !oriPrio && !mixMid) ? 'badb'
|
||||
: (posOk && oriOk) ? 'goodb' : 'warnb');
|
||||
$('ikSrc').textContent = src;
|
||||
$('ikBar').innerHTML = '<b class="hi">\u03A3|\u03B8| ' + S.costOf(q, null).toFixed(2) + '\u00B0</b>' +
|
||||
'<span class="sep">|</span>\u6B8B\u5DEE ' +
|
||||
@@ -365,7 +498,67 @@ function renderResult(res, src) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 5. 求解 ---------- */
|
||||
/* ---------- 5. 求解(Worker 化)----------
|
||||
solveDyn/solveTo 的全量扫描(40~430 ms)挪进后台线程,主线程只收结果渲染,
|
||||
松手/按钮不再冻结 UI。单文件自包含:ArmIK 源码取自页面内嵌 <script>,
|
||||
连同关节/限位/碰撞配置一次性注入 Blob Worker;拖动实时追踪同样走 Worker,
|
||||
全量回落也不再卡顿。Worker 不可用(老浏览器/CSP/file 限制)时自动回落
|
||||
主线程同步求解,行为与旧版逐位一致。碰撞 dynLimit/clampAxis 仍留主线程
|
||||
(滑块交互需要同步边界),主线程的 S 实例保留给 FK/渲染/控制台 API。 */
|
||||
let IKW = null, IKWseq = 0, IKWlast = 0;
|
||||
/* 每个请求按 id 记账,回包必定派发一次;「是否还是最新一帧」用第二参交给调用方判断。
|
||||
旧写法只留一个 IKWjob 槽:live 在飞时被 runSolve 插队,那一发的回调就彻底蒸发,
|
||||
而它是唯一给 liveBusy 复位的地方——一次竞态即让实时追踪永久停摆。 */
|
||||
const IKWjobs = new Map();
|
||||
function ikWorker() {
|
||||
if (IKW) return IKW;
|
||||
if (IKW === false) return null;
|
||||
try {
|
||||
let src = null;
|
||||
for (const sc of document.querySelectorAll('script')) {
|
||||
if (sc.textContent.indexOf('六轴手臂逆运动学求解器') >= 0) { src = sc.textContent; break; }
|
||||
}
|
||||
if (!src) throw new Error('ArmIK script 未找到');
|
||||
const boot = '\nconst CFGW = ' + JSON.stringify({ joints: JMap, travel: TR, tcp: TCP0 }) + ';\n' +
|
||||
'const COLW = ' + JSON.stringify(COL) + ';\n' +
|
||||
'const SW = ArmIK.make({ joints: CFGW.joints, travel: CFGW.travel, tcp: CFGW.tcp, collide: COLW });\n' +
|
||||
'onmessage = e => { const d = e.data; let r;\n' +
|
||||
' try { r = SW[d.op].apply(SW, d.args); } catch (err) { r = { __err: String(err && err.message || err) }; }\n' +
|
||||
' postMessage({ id: d.id, r: r }); };\n';
|
||||
/* Blob URL 只在构造 Worker 时被读取,构造后立即回收(失败路径同样不留常驻 Blob) */
|
||||
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
|
||||
let w;
|
||||
try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
|
||||
w.onmessage = e => {
|
||||
const job = IKWjobs.get(e.data.id);
|
||||
if (!job) return; // 已结清过(正常不会发生)
|
||||
IKWjobs.delete(e.data.id);
|
||||
job(e.data.r, e.data.id === IKWlast); // 过期请求也派发,只是标为不新鲜
|
||||
};
|
||||
w.onerror = () => { // Worker 挂了:把在飞的账全部结清
|
||||
IKW = false;
|
||||
const pend = Array.from(IKWjobs.values()); IKWjobs.clear();
|
||||
for (const j of pend) j(null, false);
|
||||
};
|
||||
IKW = w;
|
||||
} catch (err) { IKW = false; }
|
||||
return IKW || null;
|
||||
}
|
||||
function solveAsync(op, args, job) {
|
||||
const w = ikWorker();
|
||||
if (!w) { job(S[op].apply(S, args), true); return; } // 同步兜底(结果结构完全一致)
|
||||
const id = ++IKWseq; IKWlast = id;
|
||||
IKWjobs.set(id, (r, fresh) => { // Worker 内异常 → 主线程重算一次
|
||||
if (r && r.__err) {
|
||||
if (fresh) { try { r = S[op].apply(S, args); } catch (e2) { r = null; } }
|
||||
else r = null;
|
||||
}
|
||||
job(r, fresh);
|
||||
});
|
||||
w.postMessage({ id: id, op: op, args: args });
|
||||
}
|
||||
|
||||
/* ---------- 5a. 求解入口 ---------- */
|
||||
function runSolve() {
|
||||
const X = +$('ikX').value, Y = +$('ikY').value, Z = +$('ikZ').value;
|
||||
if (!isFinite(X) || !isFinite(Y) || !isFinite(Z)) {
|
||||
@@ -376,37 +569,64 @@ function runSolve() {
|
||||
refreshTarget();
|
||||
badge.textContent = '计算中…'; badge.className = 'badge wait';
|
||||
setTimeout(() => {
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), refW: REF_W };
|
||||
if (poseOn) opt.pose = { R: Rsolver() };
|
||||
const res = S.solveTo(TGT, opt);
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef() };
|
||||
if (poseOn) { opt.pose = { R: Rsolver() }; opt.priority = prioVal(); if (opt.priority === 'mix') opt.mixW = mixWVal(); }
|
||||
solveAsync('solveDyn', [TGT.slice(), opt], (res, fresh) => {
|
||||
if (!res || !fresh) return; // 已被更新的一发取代:交给那一发渲染
|
||||
renderResult(res, res.reachable
|
||||
? (poseOn ? '闭式全域扫描 · 位置+姿态联合 · 冗余维精确覆盖' : '闭式全域扫描 · 冗余维精确覆盖')
|
||||
? (res.dynAxis ? '动态限位放宽重解 · ' + res.dynAxis + ' 边界 ' + res.dynBound.toFixed(1) + '°'
|
||||
: (poseOn ? (prioVal() === 'mix'
|
||||
? prioTxt() + ' · 两端锚点加权折中(位置精确族 + 姿态精确族)'
|
||||
: prioTxt() + ' · 闭式全域扫描 + 孤立根枚举 · 位置+姿态联合')
|
||||
: '闭式全域扫描 · 按行裁剪冗余维'))
|
||||
: (res.note ? res.note
|
||||
: (poseOn ? '超可达位姿空间 → 阻尼最小二乘最近位姿(位置 mm + 姿态联合)'
|
||||
: '超工作空间 → 阻尼最小二乘最近位姿'));
|
||||
: '超工作空间 → 阻尼最小二乘最近位置')));
|
||||
if (res.q) setJoints(res.q, true);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/* 实时追踪:拖动过程中同步求解、立即应用(无动画,跟在手后面)。
|
||||
求解是同步的,同一时刻只跑一个;拖动中的后续 pointermove 只记待处理标志,
|
||||
当前求解结束后用最新 TGT 再解一次,避免事件排队越拖越卡。 */
|
||||
/* 实时追踪:拖动过程中即时求解、立即应用(无动画,跟在手后面)。
|
||||
求解在 Worker 后台跑(全量回落也不冻结 UI);同一时刻只跑一个,
|
||||
在飞期间后续 pointermove 只记待处理标志,结果回来后用最新 TGT 再解一次。
|
||||
门闩由「派发」而非「渲染」复位(settle 放在 finally):被 runSolve 插队而作废的
|
||||
那一发同样必须放行,否则一次竞态就永久停摆。作废时待处理标志一并交回——
|
||||
插队者用的已是最新 TGT,此时重发只会把它再挤掉、连松手的全量精修一起丢掉。 */
|
||||
let liveBusy = false, livePending = false;
|
||||
function runSolveLive() {
|
||||
if (liveBusy) { livePending = true; return; }
|
||||
liveBusy = true;
|
||||
const settle = (fresh) => {
|
||||
liveBusy = false;
|
||||
if (!fresh) { livePending = false; return; }
|
||||
if (livePending) { livePending = false; runSolveLive(); }
|
||||
};
|
||||
try {
|
||||
lastTool = +$('ikTool').value || 0;
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), refW: REF_W };
|
||||
if (poseOn) opt.pose = { R: Rsolver() };
|
||||
const res = S.solveTo(TGT, opt);
|
||||
renderResult(res, res.reachable
|
||||
? (poseOn ? '实时追踪 · 闭式全域扫描 · 位置+姿态联合 · 冗余维精确覆盖'
|
||||
: '实时追踪 · 闭式全域扫描 · 冗余维精确覆盖')
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), warm: curRef() };
|
||||
if (poseOn) { opt.pose = { R: Rsolver() }; opt.priority = prioVal(); if (opt.priority === 'mix') opt.mixW = mixWVal(); }
|
||||
solveAsync('solveTo', [TGT.slice(), opt], (res, fresh) => { // Worker 后台:全量回落也不冻结主线程
|
||||
try {
|
||||
if (!res || !fresh) return;
|
||||
renderResult(res, res.warm
|
||||
? (res.dynAxis ? '实时追踪 · 动态限位 ' + res.dynAxis + '=' + res.dynBound.toFixed(1) + '°'
|
||||
: (poseOn ? (res.priority === 'mix'
|
||||
? '实时追踪 · 混合优先沿上一帧同盆地续解(加权折中局部跟随 · 松手全量精修)'
|
||||
: '实时追踪 · 沿上一帧构型同支续解(位置精确 · 姿态牛顿收口 · 松手全量精修)')
|
||||
: '实时追踪 · 沿上一帧构型同支续解(冗余维局部跟随 · 松手全量精修)'))
|
||||
: (res.reachable
|
||||
? (poseOn ? (prioVal() === 'mix'
|
||||
? '实时追踪 · ' + prioTxt() + ' · 两端锚点加权折中'
|
||||
: '实时追踪 · ' + prioTxt() + ' · 闭式全域扫描 + 孤立根枚举 · 位置+姿态联合')
|
||||
: '实时追踪 · 闭式全域扫描 · 按行裁剪冗余维')
|
||||
: (res.note ? '实时追踪 · ' + res.note
|
||||
: (poseOn ? '实时追踪 · 超可达位姿空间(阻尼最小二乘最近位姿)'
|
||||
: '实时追踪 · 超工作空间(阻尼最小二乘最近位姿)'));
|
||||
: '实时追踪 · 超工作空间(阻尼最小二乘最近位姿)'))));
|
||||
if (res.q) setJoints(res.q, false);
|
||||
} finally { liveBusy = false; }
|
||||
if (livePending) { livePending = false; runSolveLive(); }
|
||||
} finally { settle(fresh); }
|
||||
});
|
||||
} catch (err) { settle(false); }
|
||||
}
|
||||
|
||||
/* ---------- 6. 目标拖动 / 拾取 ---------- */
|
||||
@@ -521,7 +741,8 @@ cv.addEventListener('pointermove', e => {
|
||||
if (mode === 'rt' && dragged) runSolveLive(); // 实时追踪:拖动过程中即算即动
|
||||
});
|
||||
addEventListener('pointerup', () => { camSnap = null; });
|
||||
cv.addEventListener('pointerup', e => {
|
||||
/* 挂 window 而非画布:在画布外松手也要结束抓取,否则 grab 残留 → 光挪鼠标就拖着目标走 */
|
||||
addEventListener('pointerup', e => {
|
||||
if (!grab) return;
|
||||
if (grab.pick) { // 视图拾取:射线打到当前高度的水平面
|
||||
grab = null; justPicked = true;
|
||||
@@ -559,6 +780,19 @@ function syncInputs() {
|
||||
el.addEventListener('change', () => runSolve());
|
||||
});
|
||||
$('ikStep').onchange = () => runSolve();
|
||||
/* 混合优先:比重滑杆(仅混合模式显示)。滑动即算即动(走混合快路,松手全量折中),
|
||||
与拖动黄球同一套 live 机制;两端 0% / 100% 直接交回姿态优先 / 位置优先。 */
|
||||
function syncMixUI() {
|
||||
const box = $('ikMixBox'), v = $('ikMixV');
|
||||
if (box) box.style.display = prioVal() === 'mix' ? 'flex' : 'none';
|
||||
if (v) v.textContent = Math.round(mixWVal() * 100) + '% 位置';
|
||||
}
|
||||
if ($('ikPrio')) $('ikPrio').onchange = () => { syncMixUI(); if (poseOn) runSolve(); };
|
||||
if ($('ikMixW')) {
|
||||
$('ikMixW').addEventListener('input', () => { syncMixUI(); if (poseOn) runSolveLive(); });
|
||||
$('ikMixW').addEventListener('change', () => { syncMixUI(); if (poseOn) runSolve(); });
|
||||
}
|
||||
syncMixUI();
|
||||
$('ikSolve').onclick = () => runSolve();
|
||||
$('ikPick').onclick = () => setMode(mode === 'pick' ? 'drag' : 'pick');
|
||||
$('ikRt').onclick = () => setMode(mode === 'rt' ? 'drag' : 'rt');
|
||||
@@ -621,12 +855,109 @@ function setView(v, keepPose) {
|
||||
document.querySelectorAll('.tab[data-view]').forEach(b =>
|
||||
b.addEventListener('click', () => setView(b.dataset.view)));
|
||||
|
||||
/* 预置自检结果(离线用同一份求解器代码算出,样本量比在线默认更大)。
|
||||
启动时不再重算:原先姿态自检 200 组要 ~14 s(400 组 ~23 s),是本页唯一的启动瓶颈。
|
||||
指纹 = FNV-1a over(限位表 + K + TCP + 胶囊数 + 行程表 + 求解器块源码),
|
||||
与当前代码不符时会标红提示重算。
|
||||
重新生成:node scratch/compute_precomputed.cjs(模型或求解器改动后重跑一条命令) */
|
||||
const PRECOMPUTED_CHK = {
|
||||
stamp: '2026-09-30 00:35', sig: '5153f522',
|
||||
fk: { n: 5000, err: 5.749691446267182e-13 },
|
||||
ik: { n: 200, hit: 200, tried: 200, err: 9.94353722747741e-13 },
|
||||
pose: { n: 400, hit: 398, err: 1.7922893504241644, ms: 77.80773925 },
|
||||
ws: { n: 400000, x: [-378, 415], y: [-388, 642], z: [94, 1125], rmax: 517 }
|
||||
};
|
||||
/* 紧凑指纹:与离线脚本 compute_precomputed.cjs 里的算法逐字一致
|
||||
(注意取 script.textContent——HTML 解析会把行尾归一化成 LF) */
|
||||
function chkSig() {
|
||||
const p = [];
|
||||
for (const id of ORDER) { const t = LIM[id]; p.push(id + t.lo + ',' + t.hi + ',' + t.cont); }
|
||||
p.push(TCP0.join(','));
|
||||
p.push([S.K.X0, S.K.Y0, S.K.Z1, S.K.Z2, S.K.Z3, S.K.Z4, S.K.Z5].join(','));
|
||||
p.push(String(typeof COLLIDE !== 'undefined' && COLLIDE.caps ? COLLIDE.caps.length : -1));
|
||||
p.push(typeof TRAVEL !== 'undefined'
|
||||
? TRAVEL.map(t => t.axis + t.lo + t.hi + (t.continuous ? 1 : 0)).join('') : '');
|
||||
let str = p.join('|');
|
||||
const scr = document.scripts; // 4 个内联脚本:0 three / 1 数据 / 2 求解器 / 3 界面
|
||||
if (scr.length > 3 && scr[2]) str += '|' + scr[2].textContent;
|
||||
let hh = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) { hh ^= str.charCodeAt(i); hh = Math.imul(hh, 0x01000193) >>> 0; }
|
||||
return (hh >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
/* ---------- 9. 启动:自检 + 工作空间采样 ---------- */
|
||||
function boot() {
|
||||
const st = S.selfTest(600, 6);
|
||||
const ws = (function () {
|
||||
/* 启动不再做任何重计算:自检数值全部取自预置结果,只用指纹复核是否仍适用。
|
||||
「结构前提」仍取运行时 WARN(make() 时已算好,零额外代价)。 */
|
||||
const SIG_NOW = chkSig();
|
||||
const CHK = {
|
||||
/* 结构前提是 make() 时定下的常量(求解器内部 WARN),与样本量无关:
|
||||
用一次 1 样本的 selfTest 取出即可(~0 ms),其余字段一律用预置值。 */
|
||||
st: { structWarn: S.selfTest(1, 0).structWarn, maxFkErr: PRECOMPUTED_CHK.fk.err,
|
||||
ikHit: PRECOMPUTED_CHK.ik.hit, ikTried: PRECOMPUTED_CHK.ik.tried, maxIkErr: PRECOMPUTED_CHK.ik.err },
|
||||
ws: { lo: [PRECOMPUTED_CHK.ws.x[0], PRECOMPUTED_CHK.ws.y[0], PRECOMPUTED_CHK.ws.z[0]],
|
||||
hi: [PRECOMPUTED_CHK.ws.x[1], PRECOMPUTED_CHK.ws.y[1], PRECOMPUTED_CHK.ws.z[1]],
|
||||
rmax: PRECOMPUTED_CHK.ws.rmax },
|
||||
mode: 'pre', // 'pre' 用预置值 | 'live' 本机实测
|
||||
pose: null, // null 用预置 | {running,done,total} | {hit,tried,err,ms}
|
||||
sigOk: SIG_NOW === PRECOMPUTED_CHK.sig
|
||||
};
|
||||
let reRunning = false;
|
||||
function poseLine() {
|
||||
const P = PRECOMPUTED_CHK.pose, p = CHK.pose;
|
||||
if (!p) {
|
||||
if (!CHK.sigOk) return '<i>姿态 IK 校验</i><span class="bad">预置结果已失效,请点下方「重新计算」</span>';
|
||||
return '<i>姿态 IK 校验</i><span class="' + (P.hit >= P.n * 0.9 ? 'good' : 'bad') + '">' +
|
||||
P.hit + '/' + P.n + ' 命中 · 最大 ' + P.err.toFixed(2) + '° · ' + P.ms.toFixed(0) + ' ms/次</span>';
|
||||
}
|
||||
if (p.running) return '<i>姿态 IK 校验</i><span>本机实测中 ' + p.done + '/' + p.total + '…</span>';
|
||||
return '<i>姿态 IK 校验</i><span class="' + (p.hit >= p.tried * 0.9 ? 'good' : 'bad') + '">' +
|
||||
p.hit + '/' + p.tried + ' 命中 · 最大 ' + p.err.toFixed(2) + '° · ' + p.ms.toFixed(0) + ' ms/次</span>';
|
||||
}
|
||||
function renderChk() {
|
||||
const st = CHK.st, ws = CHK.ws, P = PRECOMPUTED_CHK;
|
||||
/* 指纹不符时把"为什么变慢/为什么值是新的"讲清楚:即使已自动转为实测,
|
||||
也在来源行保留失效提示,避免用户不明所以。 */
|
||||
const src = (CHK.mode === 'live')
|
||||
? (CHK.sigOk
|
||||
? '<span class="good">本机测试(FK ' + P.fk.n + ' · IK ' + P.ik.n + ' · 姿态 ' + P.pose.n + ' · 工作空间 ' + P.ws.n + ' 位形)</span>'
|
||||
: '<span class="bad">⚠ 预置结果已失效(预置指纹 ' + P.sig + ' ≠ 当前 ' + SIG_NOW +
|
||||
',模型或求解器已变更)· 以下为本次本机测试</span>')
|
||||
: (CHK.sigOk
|
||||
? '<span>预置 · 生成于 ' + P.stamp + '(FK ' + P.fk.n + ' · IK ' + P.ik.n + ' · 姿态 ' + P.pose.n +
|
||||
' · 工作空间 ' + P.ws.n + ')· 指纹 ' + SIG_NOW + ' 匹配</span>'
|
||||
: '<span class="bad">预置结果已失效:预置指纹 ' + P.sig + ' ≠ 当前 ' + SIG_NOW + '(模型或求解器已变更)</span>');
|
||||
$('ikChk').innerHTML =
|
||||
'<i>数据来源</i>' + src +
|
||||
'<i>结构前提</i><span class="' + (st.structWarn.length ? 'bad' : 'good') + '">' +
|
||||
(st.structWarn.length ? st.structWarn.join(';') : '自洽 ✓') + '</span>' +
|
||||
'<i>FK 校验</i><span class="good">' + st.maxFkErr.toExponential(1) + ' mm</span>' +
|
||||
'<i>IK 校验</i><span class="good">' + st.ikHit + '/' + st.ikTried + ' · ' + st.maxIkErr.toExponential(1) + ' mm</span>' +
|
||||
'<i>TCP 法兰面</i><span>(' + TCP0.map(v => v.toFixed(1)).join(', ') + ')</span>' +
|
||||
'<i>*运动学可达</i><span>限位内随机位形,未计自碰撞</span>' +
|
||||
'<i>可达 x*</i><span>' + ws.lo[0].toFixed(0) + '…' + ws.hi[0].toFixed(0) +
|
||||
' mm(前伸 ' + ws.hi[0].toFixed(0) + ')</span>' +
|
||||
'<i>可达 y*</i><span>' + ws.lo[1].toFixed(0) + '…' + ws.hi[1].toFixed(0) + ' mm</span>' +
|
||||
'<i>可达 z*</i><span>' + ws.lo[2].toFixed(0) + '…' + ws.hi[2].toFixed(0) + ' mm</span>' +
|
||||
'<i>离 J1 轴*</i><span class="good">最远 ' + ws.rmax.toFixed(0) + ' mm</span>' +
|
||||
poseLine() +
|
||||
'<i>姿态结构</i><span>J5∩J6 垂直相交于腕心;θ6 为绕工具轴滚转,不影响 TCP 位置</span>' +
|
||||
'<i>操作</i><span><b id="ikChkRe" style="cursor:pointer;text-decoration:underline;color:var(--ac)">' +
|
||||
(reRunning ? '实测进行中…' : '重新计算(本机测试,姿态 ' + P.pose.n + ' 组分片执行 · 约 20–40 s,期间可继续操作)') +
|
||||
'</b></span>';
|
||||
const re = $('ikChkRe');
|
||||
if (re) re.onclick = () => runRecompute();
|
||||
}
|
||||
/* 「重新计算」:便宜部分同步跑(FK/IK ~30 ms、工作空间 ~120 ms),
|
||||
姿态部分 4 组一片 + 让出主线程,避免一次长任务冻住界面。 */
|
||||
function runRecompute() {
|
||||
if (reRunning) return;
|
||||
reRunning = true;
|
||||
const P = PRECOMPUTED_CHK;
|
||||
CHK.st = S.selfTest(P.fk.n, 0);
|
||||
CHK.ws = (function () {
|
||||
let lo = [1e9, 1e9, 1e9], hi = [-1e9, -1e9, -1e9], rmax = 0;
|
||||
for (let k = 0; k < 120000; k++) {
|
||||
for (let k = 0; k < P.ws.n; k++) {
|
||||
const q = [];
|
||||
for (let i = 0; i < 5; i++) { const t = LIM[ORDER[i]]; q.push(t.lo + Math.random() * (t.hi - t.lo)); }
|
||||
q.push(0);
|
||||
@@ -636,22 +967,29 @@ function boot() {
|
||||
}
|
||||
return { lo: lo, hi: hi, rmax: rmax };
|
||||
})();
|
||||
$('ikChk').innerHTML =
|
||||
'<i>结构前提</i><span class="' + (st.structWarn.length ? 'bad' : 'good') + '">' +
|
||||
(st.structWarn.length ? st.structWarn.join(';') : '自洽 ✓') + '</span>' +
|
||||
'<i>FK 校验</i><span class="good">' + st.maxFkErr.toExponential(1) + ' mm</span>' +
|
||||
'<i>IK 校验</i><span class="good">' + st.ikHit + '/' + st.ikTried + ' · ' + st.maxIkErr.toExponential(1) + ' mm</span>' +
|
||||
'<i>TCP 法兰面</i><span>(' + TCP0.map(v => v.toFixed(1)).join(', ') + ')</span>' +
|
||||
'<i>*运动学可达</i><span>限位内 12 万随机位形,未计自碰撞</span>' +
|
||||
'<i>可达 x*</i><span>' + ws.lo[0].toFixed(0) + '…' + ws.hi[0].toFixed(0) +
|
||||
' mm(前伸 ' + ws.hi[0].toFixed(0) + ')</span>' +
|
||||
'<i>可达 y*</i><span>' + ws.lo[1].toFixed(0) + '…' + ws.hi[1].toFixed(0) + ' mm</span>' +
|
||||
'<i>可达 z*</i><span>' + ws.lo[2].toFixed(0) + '…' + ws.hi[2].toFixed(0) + ' mm</span>' +
|
||||
'<i>离 J1 轴*</i><span class="good">最远 ' + ws.rmax.toFixed(0) + ' mm</span>' +
|
||||
'<i>姿态 IK 校验</i><span class="' + (st.poseHit >= st.poseTried * 0.9 ? 'good' : 'bad') + '">' +
|
||||
st.poseHit + '/' + st.poseTried + ' 命中 · 最大 ' + st.poseErr.toExponential(1) +
|
||||
'° · ' + st.poseMs.toFixed(0) + ' ms/次</span>' +
|
||||
'<i>姿态结构</i><span>J5∩J6 垂直相交于腕心;θ6 为绕工具轴滚转,不影响 TCP 位置</span>';
|
||||
CHK.mode = 'live';
|
||||
CHK.pose = { running: true, done: 0, total: P.pose.n };
|
||||
const acc = { hit: 0, tried: 0, err: 0, msSum: 0 };
|
||||
renderChk();
|
||||
const step = () => {
|
||||
const r = S.poseSelfTest(4);
|
||||
acc.hit += r.poseHit; acc.tried += r.poseTried;
|
||||
acc.err = Math.max(acc.err, r.poseErr);
|
||||
acc.msSum += r.poseMs * r.poseTried;
|
||||
CHK.pose.done += r.poseTried;
|
||||
if (CHK.pose.done < CHK.pose.total) { renderChk(); setTimeout(step, 16); return; }
|
||||
CHK.pose = { hit: acc.hit, tried: acc.tried, err: acc.err, ms: acc.msSum / Math.max(1, acc.tried) };
|
||||
reRunning = false;
|
||||
renderChk();
|
||||
};
|
||||
setTimeout(step, 16);
|
||||
}
|
||||
renderChk();
|
||||
/* 展开自检栏本身不触发计算;只有"预置结果已失效"时才自动补算 */
|
||||
{
|
||||
const cb = $('ikChkBox');
|
||||
if (cb) cb.addEventListener('toggle', () => { if (cb.open && !CHK.sigOk && !CHK.pose) runRecompute(); });
|
||||
}
|
||||
/* 打包自检:确认所有依赖都在本文件内(便于单文件分发,防止被拆开后才出问题) */
|
||||
const ext = document.querySelectorAll(
|
||||
'script[src],link[href],img[src],iframe[src],source[src],embed[src],object[data],use[href]').length;
|
||||
@@ -671,8 +1009,10 @@ function boot() {
|
||||
'<i>外部引用</i>' + ok(ext === 0) +
|
||||
(ext === 0 ? '0 个(script/link/img/iframe 等全类型已查)' : ext + ' 个 —— 已破坏自包含!') + '</span>';
|
||||
|
||||
syncInputs(); refreshTarget();
|
||||
const q = new URLSearchParams(location.search);
|
||||
const tgt = q.get('tgt'); // tgt=x,y,z:直接以该目标进入运动模拟(复现用)
|
||||
if (tgt && /^-?[\d.]+,-?[\d.]+,-?[\d.]+$/.test(tgt)) TGT = tgt.split(',').map(Number);
|
||||
syncInputs(); refreshTarget();
|
||||
const hasJ = ['j1', 'j2', 'j3', 'j4', 'j5', 'j6'].some(k => q.has(k));
|
||||
const v0 = q.get('view');
|
||||
if ((v0 === 'motion' || v0 === 'sheet') && document.querySelector('.tab[data-view="' + v0 + '"]')) {
|
||||
@@ -682,6 +1022,7 @@ function boot() {
|
||||
boot();
|
||||
|
||||
window.IK = { solver: S, setTarget: p => { TGT = p.slice(); syncInputs(); refreshTarget(); },
|
||||
clampAxis: clampAxis, refreshJointLabels: refreshJointLabelsLazy,
|
||||
getTarget: () => TGT.slice(), run: runSolve, TCP0: TCP0, LIM: LIM,
|
||||
setView: setView, getView: () => VIEW,
|
||||
setPose: (az, el, roll, solve) => {
|
||||
@@ -689,6 +1030,15 @@ window.IK = { solver: S, setTarget: p => { TGT = p.slice(); syncInputs(); refres
|
||||
syncPoseInputs(); refreshTarget(); if (solve !== false) runSolve();
|
||||
},
|
||||
getPose: () => ({ on: poseOn, az: POSE.az, el: POSE.el, roll: POSE.roll }),
|
||||
/* 优先级 / 混合比重(调试与复现用):setPrio('mix', 0.3) */
|
||||
setPrio: (p, w) => {
|
||||
const e = $('ikPrio');
|
||||
if (e && (p === 'pos' || p === 'ori' || p === 'mix')) e.value = p;
|
||||
if (w != null && $('ikMixW')) $('ikMixW').value = Math.round(Math.max(0, Math.min(1, +w)) * 100);
|
||||
syncMixUI();
|
||||
if (poseOn) runSolve();
|
||||
return { prio: prioVal(), mixW: mixWVal() };
|
||||
},
|
||||
/* 可拖控点的屏幕坐标(画布内像素):黄球 / 箭头头 / 滚转侧球 */
|
||||
pickPts: () => poseOn ? { ball: projTgt(), head: projArrow(HEADZ), knob: knobScreen() } : null,
|
||||
/* 调试:某视口坐标下的把手拾取结果('axis'/'roll'/null) */
|
||||
|
||||
+33872
-181
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+33872
-181
File diff suppressed because one or more lines are too long
+33872
-181
File diff suppressed because one or more lines are too long
+6
-7
@@ -96,16 +96,15 @@ const ROWS = [
|
||||
{id:'S04', size:'M3x8', spec:'M3 × 8', head:'沉头内六角', qty:4,
|
||||
pos:'P03 前置止脱锁紧法兰 → M1 电机前端面',
|
||||
note:'Ø3.2 通孔 + Ø6.0 沉孔,BCD 55', base:false},
|
||||
{id:'S05', size:'M4x25', spec:'M4 × 25', head:'圆柱头内六角', qty:4,
|
||||
pos:'P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗',
|
||||
note:'Ø4.4 通孔,非对称偏心孔位防反装', base:false,
|
||||
extra:{label:'M4 螺母 ×4', q:'M4 六角螺母 304不锈钢'}},
|
||||
{id:'S05', size:'M4x16', spec:'M4 × 16', head:'圆柱头内六角', qty:8,
|
||||
pos:'P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)',
|
||||
note:'全部 Ø3.9 直孔(母口壁与公榫壁同径,无过孔/无沉孔):M4×16 从外面直接拧入自攻成型,螺纹吃在母口壁+公榫壁共 10.5mm;非对称孔位防反装', base:false},
|
||||
{id:'S06', size:'M4x16', spec:'M4 × 16', head:'沉头内六角', qty:4,
|
||||
pos:'P09 末端工具法兰 → 末端工具/夹爪',
|
||||
note:'Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20', base:false},
|
||||
{id:'S09', size:'M5x35', spec:'M5 × 35', head:'圆柱头内六角', qty:4,
|
||||
pos:'P03 肩吊臂安装台 → P01_C 立柱顶面',
|
||||
note:'Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm', base:false},
|
||||
{id:'S09', size:'M4x16', spec:'M4 × 16', head:'圆柱头内六角', qty:5,
|
||||
pos:'P03 肩吊臂 → P01_C 立柱顶面(4 颗)+ 立柱后壁 → P03 榫头横向防脱锁紧(1 颗)',
|
||||
note:'全部 Ø3.9 直孔(鞍座/顶环/后壁/榫头同径,无过孔/无沉孔):M4×16 直接拧入自攻成型;顶部 4 颗穿鞍座 9mm 吃进顶环 7mm,横向 1 颗穿 11mm 后壁吃进榫头 4.7mm', base:false},
|
||||
/* —— 底座 P02 相关,本次暂不采购 —— */
|
||||
{id:'S07', size:'M5x25', spec:'M5 × 25', head:'圆柱头内六角', qty:4,
|
||||
pos:'P02 底座中核心 → P01 立柱底部榫头',
|
||||
|
||||
Reference in New Issue
Block a user