refactor(model): 重构P03为轻量化板式桁架并统一M4×16自攻螺钉连接方案

This commit is contained in:
cbb
2026-10-02 04:39:12 +08:00
parent 208b37048b
commit 6fe97fc643
29 changed files with 139069 additions and 1296 deletions
+27 -27
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -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 向斜撑跨层传力比螺旋体更对口。
+110 -100
View File
@@ -1,102 +1,112 @@
[
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
},
{
"id": "S04",
"规格": "M3 x 8",
"螺纹": "M3",
"长度mm": 8,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 前置止脱锁紧法兰 -> M1 电机前端面",
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°)"
},
{
"id": "S05",
"规格": "M4 x 25",
"螺纹": "M4",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗",
"备注": "Ø4.4 通孔,非对称偏心孔位物理防反装;需另配 M4 螺母 4 枚"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M5 x 35",
"螺纹": "M5",
"长度mm": 35,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 肩吊臂安装台 -> P01_C 立柱顶面",
"备注": "Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm"
},
{
"id": "S10",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
}
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
},
{
"id": "S04",
"位置": "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,
"螺纹": "M3",
"规格": "M3 x 10",
"长度mm": 10
},
{
"id": "S05",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 8,
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
"备注": "统一 Ø3.9 直孔(母口壁与公榫壁同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从外面直接拧入自攻成型,螺纹吃在母口壁 3.6 + 公榫壁 6.9 共 10.5mm;孔位 x=-131/-93 非对称物理防反装"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "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",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
}
]
+42
View File
@@ -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

+205
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+704
View File
@@ -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) | 存在另一个解在位置与姿态两个指标上都不劣于此解 |
+206
View File
@@ -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` 忽略),成品文件保持干净、无任何插桩代码。