feat(arm): 建模代码拆成每件一模块,查看器新增工程图与装配步骤页
- src/parts/p01~p09 + src/part_tools.py:几何、件号/配色/说明与装配帧归属随各自件文件走,arm_model.py 只留装配流水线并对件级符号做只读转发;P04_CH / P07_CH / P06_WIN_XZ / FENCE_OPEN_CUT 这类开关必须写所在件文件,写 arm_model.X 不生效,A/B 判据为翻转后体积变化
- docs/sheet_{exploded,front,side,joint}.svg + draw_sheet.py:四张工程图进查看器页签,支持图层开关、件号高亮与 BOM/3D 联动跳转
- travel/travel_detail/collide/bom/screws 随最新几何重生成;补线束走线、拆卸定位点与 P04 承压面剖切验证图
- .gitignore 排除 .bak/ 与 *.bak_* 源码快照(旧版本 git 历史里已有)
@@ -11,6 +11,10 @@ scratch/
|
||||
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
||||
backups/
|
||||
|
||||
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
|
||||
.bak/
|
||||
*.bak_*
|
||||
|
||||
# 编辑器 / 系统文件
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
@@ -42,7 +42,18 @@ python src/sweep_v2.py
|
||||
├── 机器人手臂3D查看器.html # 单文件内置 3D 查看器(双击即用)
|
||||
├── README.md # 本说明文档
|
||||
├── src/ # 核心源码目录
|
||||
│ ├── arm_model.py # ⭐ 唯一权威主力建模脚本(含五级圆角与最新几何)
|
||||
│ ├── arm_model.py # ⭐ 装配流水线:载电机模组→按件收集 specs()→导出 STEP/STL/BOM/网格→打包查看器
|
||||
│ ├── part_tools.py # 跨件共用刀法:X 向基础体、减重腔 helper、电机前端压盖、pitch->roll 转接座骨架
|
||||
│ ├── parts/ # ⭐ 每个零部件一个文件:几何 + 件号/配色/说明 + 装配帧归属 JOINT
|
||||
│ │ ├── p01_column.py # P01_A/B/C 立柱三段(含立柱定位 X_COL)
|
||||
│ │ ├── p02_base.py # P02 底座三段(中核心/前向力臂/后向力臂)
|
||||
│ │ ├── p03_girdle.py # P03 肩吊臂
|
||||
│ │ ├── p04_fork.py # P04 肩叉(减重槽开度 P04_CH 在此)
|
||||
│ │ ├── p05_roll_base.py # P05 大臂旋转座
|
||||
│ │ ├── p06_humerus.py # P06 大臂骨架(窗开度 P06_WIN_XZ / P06_HOLLOW_WIN 在此)
|
||||
│ │ ├── p07_forearm.py # P07 小臂骨架(减重槽开度 P07_CH 在此)
|
||||
│ │ ├── p08_wrist.py # P08 腕偏转座
|
||||
│ │ └── p09_flange.py # P09 工具法兰
|
||||
│ ├── sweep_v2.py # ⭐ 权威动态碰撞检测与行程扫描工具
|
||||
│ ├── arm_build.py # 底层几何基础库与通用装配/导出函数
|
||||
│ ├── build_viewer.py # 3D 查看器打包生成脚本
|
||||
@@ -52,6 +63,12 @@ python src/sweep_v2.py
|
||||
│ ├── finalize_travel.py # 行程数据维护(从 sweep 明细重建 travel.json)
|
||||
│ ├── strength_check.py # 刚度与应力分析
|
||||
│ └── torque_balance.py # 关节力矩平衡校核
|
||||
|
||||
> **改哪件就开哪个文件**;`arm_model.py` 里不再有件号字符串和几何代码。
|
||||
> 件级开关(`FENCE_OPEN_CUT`、`P04_CH`、`P07_CH`、`P06_WIN_XZ`)随各自的模块搬走:
|
||||
> 做 A/B 必须写它所在的件文件(如 `parts.p04_fork.P04_CH = cfg`),
|
||||
> 写 `arm_model.P04_CH = cfg` **不生效**(`arm_model` 对件级符号只作只读转发,供旧脚本取值)。
|
||||
> 判据:翻转开关后体积必须变化(`scratch/verify_switches.py` 实测 2.70 / 22.23 cm³ 灵敏度)。
|
||||
├── docs/ # 权威数据与渲染截图(历史报告已清理)
|
||||
│ ├── travel.json # 各关节实测行程与硬限位(sweep_v2.py 扫描生成)
|
||||
│ ├── travel_detail.json # 逐对扫描明细与组合折叠约束
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4x M3 螺纹径向槽(电机.STEP 实测:BCD55、相位 39.9°+90k,槽宽 2.5,径向可调约 ±1.25mm)。【2026-10-07 电机厚度公差】用户要求余量偏大减去 1mm:公差让位由 MOTOR_TOL=2.0 调为 1.0mm,承压面从 s=-2.6 移至 s=-1.6(armcore.MOTOR_TOL / arm_build.REAR_SEAT,厚度膜厚 8-4-1.6=2.4mm 显著加强)。装配时在电机尾端面与承压面之间叠 0~1.6mm 的 M3 平垫(标称电机约 0.6mm),厚度 T = 57.6 - 电机实际长度 L,让前端盖正好落到前端压盖面上(压盖背面与前端盖 0.0 面压)。M3x10 螺钉在电机螺纹槽内的咬合量进一步增加 1.0mm(达到 4.4mm 以上)。【2026-10-07 承压面让位腔】承压面段(z=-1.6..+1.0)用 SEAT_R=35.0(= 外壁 R35)圆腔挖满外壁,承压面为完整 R35 平圆盘,彻底消除凸台与环槽围挡。沉孔位置与深度完全不变(Ø5.5 深 4,z=-8..-4),S02 稳固咬合。"
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),BCD 55;装配时配合平垫吸收电机公差,与前端压盖双向夹死"
|
||||
},
|
||||
{
|
||||
"id": "S03",
|
||||
@@ -27,17 +27,17 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm,头沉入 1.5mm),孔位 BCD55、相位 39.9°+90k(电机.STEP 实测螺纹径向槽),轴向锁死防拉脱;旧 35.1×40.9 矩形孔距对角两颗偏出槽位 ~4.7mm,2026-10-02 修正;头型按用户已采购件用圆柱头内六角(沉孔本为圆柱形)"
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm),BCD 55,轴向锁死防拉脱"
|
||||
},
|
||||
{
|
||||
"id": "S04",
|
||||
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm,头露出 1mm),孔位 BCD55 @17.8°+90k(电机.STEP 实测前端螺纹径向槽);头坐在沉孔底 x=9.5,M3x8 咬合电机前端面 3.5mm;2026-10-02 按用户已采购规格由 M3x10 沉头改回 M3x8 圆柱头",
|
||||
"规格": "M3 x 8",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"螺纹": "M3",
|
||||
"规格": "M3 x 8",
|
||||
"长度mm": 8
|
||||
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm),BCD 55,咬合电机前端面 3.5mm"
|
||||
},
|
||||
{
|
||||
"id": "S05",
|
||||
@@ -47,7 +47,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 8,
|
||||
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
|
||||
"备注": "统一 Ø3.9 直孔(母口壁与公榫壁同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从外面直接拧入自攻成型,螺纹吃在母口壁 3.6 + 公榫壁 6.9 共 10.5mm;孔位 x=-131/-93 非对称物理防反装"
|
||||
"备注": "统一 Ø3.9 直孔自攻成型,非对称孔位防反装"
|
||||
},
|
||||
{
|
||||
"id": "S06",
|
||||
@@ -57,7 +57,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
||||
"备注": "Ø4.4 通孔(贯穿法兰)+ Ø7.0 圆柱沉孔(深 4mm,头与法兰底面齐平),R22 @0/90/180/270°(BCD44)-> 末端工具;头型按用户已采购件用圆柱头内六角。2026-10-02 由 R20 改 R22:原 R20 与 3xS01 输出螺钉 Ø5.6 螺丝刀通道中心距 6.06mm < 所需 6.30mm,0° 孔的 Ø7.0 沉孔壁被啃穿成月牙形(圆柱面 88mm² 裂成 45.7+35.4);相位差仅 8.6°,转相位躲不开,R22 后余量 1.66mm、边壁仍余 9.5mm;同日修正通孔:原 Ø3.7 比 M4 杆 Ø4.0 还细且只到 z=105,而 M4x16 杆需走到 z=113,杆尾 8mm 埋在实心料里(干涉 147.3mm³),改为 Ø4.4 贯穿"
|
||||
"备注": "Ø4.4 通孔 + Ø7.0 圆柱沉孔(深 4mm,头平齐),BCD 44 均布 -> 末端工具/夹爪"
|
||||
},
|
||||
{
|
||||
"id": "S07",
|
||||
@@ -67,7 +67,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装"
|
||||
},
|
||||
{
|
||||
"id": "S08",
|
||||
@@ -87,7 +87,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
|
||||
"备注": "统一 Ø3.9 直孔(鞍座与立柱顶环同径,只有这一个孔,无过孔/沉孔/预埋):M4x16 从上方直接拧入自攻成型,穿鞍座 9mm 后吃进立柱顶环 7mm;载荷走 2403mm² 承压面,螺钉只防上抬"
|
||||
"备注": "统一 Ø3.9 直孔自攻成型(从上方拧入),防上抬"
|
||||
},
|
||||
{
|
||||
"id": "S11",
|
||||
@@ -97,7 +97,7 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 1,
|
||||
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
|
||||
"备注": "与 S05/S09/S06 同为 M4x16(全机 M4x16 合计 17 颗);Ø3.9 直孔贯穿 11mm 后壁 + 榫头同径:从后方直接拧入自攻成型,榫头内咬合 4.7mm"
|
||||
"备注": "Ø3.9 直孔贯穿 11mm 后壁 + 榫头,从后方自攻横向锁紧防脱"
|
||||
},
|
||||
{
|
||||
"id": "S10",
|
||||
@@ -117,6 +117,6 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 前端压盖 -> M2/M3/M4/M5/M6 电机前端盖(J2/J3/J4/J5/J6 各 4 颗)",
|
||||
"备注": "Ø3.4 通孔 + Ø6.0 圆柱沉孔(深 3.0,头 Ø5.5×3.0 全沉入),孔位 BCD55 @17.8°+90k—— 正是电机前端面 4 个 M3 螺纹径向槽的相位(与 S04 同一套,勿改);螺钉头面压在孔口台阶上(Y 轴关节 Y=158;J3 的 Z 轴版本 Z=387),压盖背面与电机前端盖外端面 0.0 间隙面压贴合—— 与后端安装板 S02 的 4xM3x10 一起把电机前后双向夹死,消掉大扭矩下电机绕 J 轴点头/翘起的倾向。J2(P04 夹 M2)/J4(P06 夹 M4)/J5(P07 夹 M5) 三处压盖几何完全相同;【2026-10-03 新增 J3】P05 夹 M3 是同一套配方的 Z 轴版本(roll 电机轴向 -Z,压盖占 Z384.0..389.0,背面 Z=389 与 M3 前端盖面压),沉孔台阶 Z=387,杆穿 2.0mm 压盖 + 3.5mm 端盖后旋入前端螺纹槽约 2.5mm。【2026-10-05d 只切敞口那一圈围挡】用户指出 M3 电机前端那圈围挡(P05 在 Z389.0..395.5 的 r30.25..35 前端定位环 + 骑在它上面的 R35 压盖外圈,Ø70 比电机 Ø63.6 凸出 3.2mm)需要删除。之前一版把整个前端(定位环 + 压盖 + 腔口下沿 389->395)都切了,电机前端没了支撑面 —— 过度;现已回退。现只切敞口(-Y)那一圈(盒切,Y 界限与 cut_open 一致),-Y 半圈从 Z=395.5 一路开到压盖背面 Z=389;背脊(+Y)侧整圈定位环保留,继续定位 M3 前端盖并作为R35 压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带)。压盖本体、Ø37 让位孔、4xM3x8 沉孔、腔口下沿 z_bot=389、P06 毂面 383.0 与直锁凸台 65.0 全部不动,本组 16 颗螺钉的孔位/相位/沉孔台阶/咬合量一概不变(M3 仍是后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压)。不整圈削到 R31.8 的原因:那圈壁与 Loft 在 X=±35 处与 Ø70 柱面相切,整圈切完会脱空出 1.32mm3 碎块破坏单实体;且压盖根部熔接带会从 2.37mm2 缩到 0.78mm2,夹紧力会把压盖拉下来。开关 arm_model.M3_FENCE_OPEN_CUT(仅用于 P05/M3)。【2026-10-07 消除误切缺角】2026-10-05e 曾将该盒切推广到全机,因坐标系差异导致 P04/P06/P07/P08 底壁被切掉 3mm 台阶缺角(平白无故缺了一个角)。现已彻底取消该四件的切除,底壁完全补齐恢复平直完整,该盒切仅保留在 P05/M3 上。本组 20 颗螺钉的安装规格、孔位 BCD55、相位、沉孔台阶深度与拧紧力矩一概不受影响。"
|
||||
"备注": "Ø3.4 通孔 + Ø6.0 圆柱沉孔(深 3.0mm,头全沉入),BCD 55;与后端安装板 S02 配合双向夹紧电机"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 197 KiB |
@@ -5,15 +5,17 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合扫描未发现该轴卡死"
|
||||
},
|
||||
{
|
||||
"axis": "J2",
|
||||
"lo": -105.0,
|
||||
"lo": -92.0,
|
||||
"hi": 9.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴正向 +9° / 反向 -92°,总行程 101°(单关节硬限位,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J3",
|
||||
@@ -21,7 +23,8 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": 162.0,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合折叠姿态下(其余关节同时处于各自极限)约 ±162° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J4",
|
||||
@@ -29,7 +32,8 @@
|
||||
"hi": 16.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +16° / 反向 -111°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": 18.0,
|
||||
"note": "单轴正向 +16° / 反向 -111°,总行程 127°(单关节硬限位,含相邻件与电机);组合折叠姿态下(其余关节同时处于各自极限)约 ±18° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J5",
|
||||
@@ -37,7 +41,8 @@
|
||||
"hi": 34.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": 36.0,
|
||||
"note": "单轴正向 +34° / 反向 -93°,总行程 127°(单关节硬限位,含相邻件与电机);组合折叠姿态下(其余关节同时处于各自极限)约 ±36° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J6",
|
||||
@@ -45,6 +50,7 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合扫描未发现该轴卡死"
|
||||
}
|
||||
]
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"generated": "2026-10-04 02:00:11",
|
||||
"generated": "2026-10-13 22:00:28",
|
||||
"vol_min": 2.0,
|
||||
"step": 2.0,
|
||||
"combo_step": 6.0,
|
||||
"workers": 12,
|
||||
"workers": 20,
|
||||
"fixture_included": true,
|
||||
"singles": {
|
||||
"J1": {
|
||||
@@ -12,30 +12,30 @@
|
||||
"jams": {}
|
||||
},
|
||||
"J2": {
|
||||
"lo": -106.25,
|
||||
"lo": -93.38,
|
||||
"hi": 10.38,
|
||||
"jams": {
|
||||
"12.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
2401.7,
|
||||
2668.7,
|
||||
[
|
||||
-52.9,
|
||||
149.9,
|
||||
149.2
|
||||
137.2,
|
||||
152.9
|
||||
]
|
||||
]
|
||||
],
|
||||
"-108.0": [
|
||||
"-94.0": [
|
||||
[
|
||||
"P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
175.1,
|
||||
83.6,
|
||||
[
|
||||
46.8,
|
||||
141.0,
|
||||
532.9
|
||||
56.7,
|
||||
133.7,
|
||||
523.3
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
-82.2,
|
||||
128.0,
|
||||
116.1
|
||||
]
|
||||
@@ -86,9 +86,9 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
-82.4,
|
||||
128.0,
|
||||
140.7
|
||||
140.6
|
||||
]
|
||||
]
|
||||
],
|
||||
@@ -99,7 +99,7 @@
|
||||
144.8,
|
||||
[
|
||||
55.5,
|
||||
139.7,
|
||||
130.4,
|
||||
257.0
|
||||
]
|
||||
]
|
||||
@@ -129,8 +129,8 @@
|
||||
"hold_at": -180.0,
|
||||
"swept": "J2",
|
||||
"direction": "lo",
|
||||
"old": -106.25,
|
||||
"new": -103.0,
|
||||
"old": -93.375,
|
||||
"new": -91.0,
|
||||
"jams": {}
|
||||
},
|
||||
{
|
||||
@@ -246,7 +246,7 @@
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J1",
|
||||
"direction": "hi",
|
||||
"old": 180.0,
|
||||
@@ -256,7 +256,7 @@
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J3",
|
||||
"direction": "hi",
|
||||
"old": 173.0,
|
||||
@@ -266,7 +266,7 @@
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J4",
|
||||
"direction": "hi",
|
||||
"old": 5.0,
|
||||
@@ -276,7 +276,7 @@
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J5",
|
||||
"direction": "hi",
|
||||
"old": 23.0,
|
||||
@@ -286,7 +286,7 @@
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J6",
|
||||
"direction": "hi",
|
||||
"old": 173.0,
|
||||
@@ -328,7 +328,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
-82.2,
|
||||
128.0,
|
||||
116.1
|
||||
]
|
||||
@@ -351,7 +351,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
-82.2,
|
||||
128.0,
|
||||
116.1
|
||||
]
|
||||
@@ -374,7 +374,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
-82.4,
|
||||
128.0,
|
||||
140.6
|
||||
]
|
||||
@@ -397,7 +397,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
-82.4,
|
||||
128.0,
|
||||
140.6
|
||||
]
|
||||
@@ -450,7 +450,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
186.7,
|
||||
[
|
||||
-83.0,
|
||||
-83.1,
|
||||
104.7,
|
||||
120.8
|
||||
]
|
||||
@@ -473,7 +473,7 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
186.7,
|
||||
[
|
||||
-83.0,
|
||||
-83.1,
|
||||
104.7,
|
||||
120.8
|
||||
]
|
||||
@@ -549,8 +549,8 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
104.8,
|
||||
-82.5,
|
||||
104.7,
|
||||
117.4
|
||||
]
|
||||
]
|
||||
@@ -561,8 +561,8 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
151.2,
|
||||
-82.5,
|
||||
151.3,
|
||||
117.4
|
||||
]
|
||||
]
|
||||
@@ -584,8 +584,8 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
104.8,
|
||||
-82.5,
|
||||
104.7,
|
||||
117.4
|
||||
]
|
||||
]
|
||||
@@ -596,8 +596,8 @@
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
151.2,
|
||||
-82.5,
|
||||
151.3,
|
||||
117.4
|
||||
]
|
||||
]
|
||||
@@ -897,7 +897,7 @@
|
||||
{
|
||||
"pass_": 1,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J1",
|
||||
"direction": "hi",
|
||||
"old": 143.0,
|
||||
@@ -907,7 +907,7 @@
|
||||
{
|
||||
"pass_": 1,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J3",
|
||||
"direction": "hi",
|
||||
"old": 131.0,
|
||||
@@ -917,7 +917,7 @@
|
||||
{
|
||||
"pass_": 1,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J4",
|
||||
"direction": "hi",
|
||||
"old": 0.0,
|
||||
@@ -927,7 +927,7 @@
|
||||
{
|
||||
"pass_": 1,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J5",
|
||||
"direction": "hi",
|
||||
"old": 0.0,
|
||||
@@ -937,7 +937,7 @@
|
||||
{
|
||||
"pass_": 1,
|
||||
"hold": "J2",
|
||||
"hold_at": -103.0,
|
||||
"hold_at": -91.0,
|
||||
"swept": "J6",
|
||||
"direction": "hi",
|
||||
"old": 131.0,
|
||||
@@ -1252,15 +1252,17 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合扫描未发现该轴卡死"
|
||||
},
|
||||
{
|
||||
"axis": "J2",
|
||||
"lo": -105.0,
|
||||
"lo": -92.0,
|
||||
"hi": 9.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴正向 +9° / 反向 -92°,总行程 101°(单关节硬限位,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J3",
|
||||
@@ -1268,7 +1270,8 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": 162.0,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合折叠姿态下(其余关节同时处于各自极限)约 ±162° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J4",
|
||||
@@ -1276,7 +1279,8 @@
|
||||
"hi": 16.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +16° / 反向 -111°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": 18.0,
|
||||
"note": "单轴正向 +16° / 反向 -111°,总行程 127°(单关节硬限位,含相邻件与电机);组合折叠姿态下(其余关节同时处于各自极限)约 ±18° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J5",
|
||||
@@ -1284,7 +1288,8 @@
|
||||
"hi": 34.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"combo_warn_deg": 36.0,
|
||||
"note": "单轴正向 +34° / 反向 -93°,总行程 127°(单关节硬限位,含相邻件与电机);组合折叠姿态下(其余关节同时处于各自极限)约 ±36° 起与躯干占位(台架 P01 立柱)干涉,见 travel_detail.combos,运行期由查看器碰撞模型按姿态夹取"
|
||||
},
|
||||
{
|
||||
"axis": "J6",
|
||||
@@ -1292,7 +1297,8 @@
|
||||
"hi": 180.0,
|
||||
"continuous": true,
|
||||
"zero": 0.0,
|
||||
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
|
||||
"combo_warn_deg": null,
|
||||
"note": "单轴整周无干涉(滑块按单轴限位给出);组合扫描未发现该轴卡死"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -76,28 +76,46 @@ def esc(t):
|
||||
return str(t).replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
def frame(W, H, title, note, scale):
|
||||
return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" '
|
||||
'font-family="Microsoft YaHei,sans-serif">' % (W, H),
|
||||
'<rect width="%d" height="%d" fill="#fff"/>' % (W, H),
|
||||
'<rect x="8" y="8" width="%d" height="%d" fill="none" stroke="#111" stroke-width="1.6"/>' % (W - 16, H - 16),
|
||||
'<rect x="14" y="14" width="%d" height="%d" fill="none" stroke="#111" stroke-width=".5"/>' % (W - 28, H - 28),
|
||||
'<line x1="14" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (H - 44, W - 15, H - 44),
|
||||
'<text x="24" y="%d" font-size="13" font-weight="bold">%s</text>' % (H - 27, esc(title)),
|
||||
'<text x="24" y="%d" font-size="9" fill="#444">%s</text>' % (H - 16, esc(note)),
|
||||
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (W - 268, H - 44, W - 268, H - 15),
|
||||
'<text x="%d" y="%d" font-size="9">比例 1:%s(按图幅适配) 单位 mm</text>' % (W - 260, H - 29, scale),
|
||||
'<text x="%d" y="%d" font-size="9">六电机人形机器人手臂 %s / 迭代 %s</text>' % (W - 260, H - 17, LAY["ver"], LAY["it"])]
|
||||
def frame(W, H, title, note, scale, sheet_id=""):
|
||||
return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d" '
|
||||
'class="assembly-sheet-svg" data-sheet-id="%s" data-sheet-title="%s" data-sheet-note="%s" data-sheet-scale="%s" '
|
||||
'font-family="Microsoft YaHei,sans-serif">' % (W, H, W, H, sheet_id, esc(title), esc(note), scale),
|
||||
'<defs>'
|
||||
'<filter id="partGlow" x="-20%" y="-20%" width="140%" height="140%">'
|
||||
'<feDropShadow dx="0" dy="0" stdDeviation="3.5" flood-color="#2563eb" flood-opacity="0.85"/>'
|
||||
'</filter>'
|
||||
'<filter id="balloonShadow" x="-20%" y="-20%" width="140%" height="140%">'
|
||||
'<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" flood-color="#000" flood-opacity="0.25"/>'
|
||||
'</filter>'
|
||||
'</defs>',
|
||||
'<rect class="sheet-bg" width="%d" height="%d" fill="#fff"/>' % (W, H),
|
||||
'<g class="sheet-border">',
|
||||
'<rect class="sheet-border-outer" x="8" y="8" width="%d" height="%d" fill="none" stroke="#111" stroke-width="1.6"/>' % (W - 16, H - 16),
|
||||
'<rect class="sheet-border-inner" x="14" y="14" width="%d" height="%d" fill="none" stroke="#111" stroke-width=".5"/>' % (W - 28, H - 28),
|
||||
'<line class="sheet-title-line" x1="14" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (H - 44, W - 15, H - 44),
|
||||
'<text class="sheet-title-text" x="24" y="%d" font-size="13" font-weight="bold">%s</text>' % (H - 27, esc(title)),
|
||||
'<text class="sheet-note-text" x="24" y="%d" font-size="9" fill="#444">%s</text>' % (H - 16, esc(note)),
|
||||
'<line class="sheet-meta-line" x1="%d" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (W - 268, H - 44, W - 268, H - 15),
|
||||
'<text class="sheet-scale-text" x="%d" y="%d" font-size="9">比例 1:%s(按图幅适配) 单位 mm</text>' % (W - 260, H - 29, scale),
|
||||
'<text class="sheet-ver-text" x="%d" y="%d" font-size="9">六电机人形机器人手臂 %s / 迭代 %s</text>' % (W - 260, H - 17, LAY["ver"], LAY["it"]),
|
||||
'</g>']
|
||||
|
||||
|
||||
def balloon(x, y, i):
|
||||
return ('<circle cx="%.0f" cy="%.0f" r="10" fill="#fff" stroke="#111" stroke-width="1"/>'
|
||||
'<text x="%.0f" y="%.0f" font-size="9.5" text-anchor="middle">%d</text>' % (x, y, x, y + 3.4, i))
|
||||
def balloon(x, y, p):
|
||||
idx = p.get("idx", 0)
|
||||
name = esc(p.get("name", ""))
|
||||
cn = esc(p.get("cn", ""))
|
||||
return ('<g class="sheet-balloon" data-idx="%d" data-name="%s" data-cn="%s">'
|
||||
'<circle class="balloon-circle" cx="%.0f" cy="%.0f" r="10" fill="#fff" stroke="#111" stroke-width="1"/>'
|
||||
'<text class="balloon-text" x="%.0f" y="%.0f" font-size="9.5" text-anchor="middle">%d</text>'
|
||||
'</g>' % (idx, name, cn, x, y, x, y + 3.4, idx))
|
||||
|
||||
|
||||
def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend=True):
|
||||
def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend=True, sheet_id=""):
|
||||
shp = []
|
||||
for p in P:
|
||||
if p["name"].startswith("ENV_") or p["name"].startswith("S"):
|
||||
continue
|
||||
h = hull(part_pts(p, kind, ex))
|
||||
if len(h) >= 3:
|
||||
shp.append((p, h))
|
||||
@@ -114,9 +132,10 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
|
||||
ox = ML + (aw - (hi_x - lo_x) * sc) / 2 - lo_x * sc
|
||||
oy = 34 + (ah + (hi_y - lo_y) * sc) / 2 + lo_y * sc
|
||||
T = lambda q: (q[0] * sc + ox, oy - q[1] * sc)
|
||||
out = frame(W, H, title, note, round(1 / sc, 2))
|
||||
out = frame(W, H, title, note, round(1 / sc, 2), sheet_id)
|
||||
|
||||
if axes:
|
||||
out.append('<g class="sheet-axes-group">')
|
||||
for jid in ("J1", "J2", "J3", "J4", "J5"):
|
||||
j = JOINTS.get(jid)
|
||||
if not j:
|
||||
@@ -124,48 +143,56 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
|
||||
a = proj(j["rear"], kind)
|
||||
b = proj([j["rear"][k] + j["d"][k] * 90 for k in range(3)], kind)
|
||||
pa, pb = T(a), T(b)
|
||||
out.append('<line x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
out.append('<line class="sheet-axis" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
'stroke-width=".8" stroke-dasharray="14 3 2 3"/>' % (pa[0], pa[1], pb[0], pb[1]))
|
||||
out.append('<text x="%.0f" y="%.0f" font-size="10" fill="#0b57a4">%s</text>'
|
||||
out.append('<text class="sheet-axis-label" x="%.0f" y="%.0f" font-size="10" fill="#0b57a4">%s</text>'
|
||||
% (pb[0] + 3, pb[1] - 3, jid))
|
||||
out.append('</g>')
|
||||
|
||||
out.append('<g class="sheet-parts-group">')
|
||||
for p, h in shp:
|
||||
d = "M" + " L".join("%.1f %.1f" % T(q) for q in h) + "Z"
|
||||
col = "#%06x" % p["color"]
|
||||
idx = p.get("idx", 0)
|
||||
cn = esc(p.get("cn", ""))
|
||||
name = esc(p.get("name", ""))
|
||||
printed = 1 if p.get("printed") else 0
|
||||
attrs = 'class="sheet-part-path" data-idx="%d" data-name="%s" data-cn="%s" data-color="%s" data-printed="%d"' % (
|
||||
idx, name, cn, col, printed)
|
||||
if p["printed"]:
|
||||
out.append('<path d="%s" fill="none" stroke="%s" stroke-width="1.35"/>' % (d, col))
|
||||
out.append('<path d="%s" fill="none" stroke="%s" stroke-width="1.35" %s/>' % (d, col, attrs))
|
||||
else:
|
||||
out.append('<path d="%s" fill="none" stroke="#9aa1ad" stroke-width=".7" '
|
||||
'stroke-dasharray="4 2"/>' % d)
|
||||
'stroke-dasharray="4 2" %s/>' % (d, attrs))
|
||||
out.append('</g>')
|
||||
|
||||
# balloons in two side columns. Sort each column by the ANCHOR height (the nearest point
|
||||
# on the part's own outline), not by centroid - sorting by centroid made the leaders cross
|
||||
# in the middle of the sheet because the anchor can sit far from the centroid.
|
||||
# balloons in two side columns.
|
||||
side = []
|
||||
for p, h in shp:
|
||||
cx = sum(T(q)[0] for q in h) / len(h)
|
||||
cy = sum(T(q)[1] for q in h) / len(h)
|
||||
anchor = T(nearest(h, (cx, cy)))
|
||||
side.append((p, anchor, cx))
|
||||
# A hanging arm projects tall and narrow, so splitting the balloons left/right pushed the
|
||||
# whole left column's leaders across the assembly. When the view is taller than wide every
|
||||
# balloon goes to one column just right of the drawing, sorted by anchor height.
|
||||
tall = (hi_y - lo_y) > 1.25 * (hi_x - lo_x)
|
||||
right = sorted(side, key=lambda s: s[1][1]) if tall else sorted([s for s in side if s[2] >= ox + (hi_x - lo_x) * sc / 2.0], key=lambda s: s[1][1])
|
||||
right = sorted(side, key=lambda s: s[1][1]) if tall else sorted([s for s in side if s[2] >= ox + (hi_x - lo_x) * sc / 2.0], key=lambda s: s[1][1])
|
||||
left = [] if tall else sorted([s for s in side if s[2] < ox + (hi_x - lo_x) * sc / 2.0],
|
||||
key=lambda s: s[1][1])
|
||||
|
||||
out.append('<g class="sheet-balloons-group">')
|
||||
def col(lst, x):
|
||||
n = max(1, len(lst))
|
||||
span = H - 130
|
||||
for i, (p, t, _cx) in enumerate(lst):
|
||||
by = 52 + span * (i + 0.5) / n
|
||||
out.append('<polyline points="%.0f,%.0f %.0f,%.0f %.0f,%.0f" fill="none" '
|
||||
'stroke="#555" stroke-width=".6"/>' % (t[0], t[1], (t[0] + x) / 2, by, x, by))
|
||||
out.append(balloon(x, by, p["idx"]))
|
||||
out.append('<polyline class="sheet-leader" data-idx="%d" points="%.0f,%.0f %.0f,%.0f %.0f,%.0f" fill="none" '
|
||||
'stroke="#555" stroke-width=".6"/>' % (p["idx"], t[0], t[1], (t[0] + x) / 2, by, x, by))
|
||||
out.append(balloon(x, by, p))
|
||||
col(left, ML - 30)
|
||||
col(right, (ox + (hi_x - lo_x) * sc + 52.0) if tall else (W - MR + 44))
|
||||
out.append('</g>')
|
||||
|
||||
if legend:
|
||||
out.append('<g class="sheet-legend-box">')
|
||||
rows = sorted(side, key=lambda s: s[0]["idx"])
|
||||
half = (len(rows) + 1) // 2
|
||||
tx, ty0 = W - MR + 74, 62
|
||||
@@ -173,20 +200,30 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
|
||||
for c in (0, 1):
|
||||
for i, (p, _t, _cx) in enumerate(rows[c * half:(c + 1) * half]):
|
||||
y = ty0 + 12 + i * 12.6
|
||||
out.append('<text x="%d" y="%.0f" font-size="8.5">%d</text>' % (tx + c * 106, y, p["idx"]))
|
||||
nm = esc(p["cn"].replace(" 半联轴器/抱箍", " 联轴器/抱箍").replace("端面齿", "")[:11])
|
||||
out.append('<text x="%d" y="%.0f" font-size="8.5">%s</text>' % (tx + c * 106 + 13, y, nm))
|
||||
out.append('<g class="sheet-legend-row" data-idx="%d" data-name="%s" data-cn="%s">'
|
||||
'<rect class="legend-row-bg" x="%d" y="%.0f" width="102" height="12.5" fill="transparent" rx="2"/>'
|
||||
'<text class="legend-row-idx" x="%d" y="%.0f" font-size="8.5">%d</text>'
|
||||
'<text class="legend-row-name" x="%d" y="%.0f" font-size="8.5">%s</text>'
|
||||
'</g>' % (p["idx"], esc(p.get("name","")), esc(p.get("cn","")),
|
||||
tx + c * 106 - 3, y - 9.5,
|
||||
tx + c * 106, y, p["idx"],
|
||||
tx + c * 106 + 13, y, nm))
|
||||
out.append('</g>')
|
||||
|
||||
for (a, b, lab, off) in (dims or []):
|
||||
pa, pb = T(proj(a, kind)), T(proj(b, kind))
|
||||
y = (pa[1] + pb[1]) / 2 + off
|
||||
out.append('<line x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" stroke-width=".9"/>'
|
||||
% (pa[0], y, pb[0], y))
|
||||
for q in (pa, pb):
|
||||
out.append('<line x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
'stroke-width=".5" stroke-dasharray="2 2"/>' % (q[0], q[1], q[0], y))
|
||||
out.append('<text x="%.0f" y="%.0f" font-size="10" fill="#0b57a4" text-anchor="middle">%s</text>'
|
||||
% ((pa[0] + pb[0]) / 2, y - 3, lab))
|
||||
if dims:
|
||||
out.append('<g class="sheet-dims-group">')
|
||||
for (a, b, lab, off) in dims:
|
||||
pa, pb = T(proj(a, kind)), T(proj(b, kind))
|
||||
y = (pa[1] + pb[1]) / 2 + off
|
||||
out.append('<line class="sheet-dim-line" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" stroke-width=".9"/>'
|
||||
% (pa[0], y, pb[0], y))
|
||||
for q in (pa, pb):
|
||||
out.append('<line class="sheet-dim-ext" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
'stroke-width=".5" stroke-dasharray="2 2"/>' % (q[0], q[1], q[0], y))
|
||||
out.append('<text class="sheet-dim-text" x="%.0f" y="%.0f" font-size="10" fill="#0b57a4" text-anchor="middle">%s</text>'
|
||||
% ((pa[0] + pb[0]) / 2, y - 3, lab))
|
||||
out.append('</g>')
|
||||
out.append('</svg>')
|
||||
return "".join(out)
|
||||
|
||||
@@ -202,32 +239,31 @@ def joint_detail():
|
||||
x_hub = x_cover + CT["gap"] * s
|
||||
out = frame(W, H, "A-A 回转副局部放大 — 全部尺寸为实测值(按数值绘制,非网格投影)",
|
||||
"电机 → 端面齿半联轴器 A/B(12 齿,盘脸贴死为唯一轴向止动)→ 对开抱箍防脱 → "
|
||||
"从动盘(复刻电机输出面)。蓝色为关节轴线。", "1/6(本图放大 6 倍)")
|
||||
out.append('<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#0b57a4" stroke-width="1.1" '
|
||||
"从动盘(复刻电机输出面)。蓝色为关节轴线。", "1/6(本图放大 6 倍)", "joint")
|
||||
out.append('<line class="sheet-axis" x1="%d" y1="%d" x2="%d" y2="%d" stroke="#0b57a4" stroke-width="1.1" '
|
||||
'stroke-dasharray="22 5 4 5"/>' % (x0 - 60, cy, x_hub + 190, cy))
|
||||
g = ['<g stroke="#111" fill="none" stroke-width="1.5">']
|
||||
g = ['<g class="joint-geom-group" stroke="#111" fill="none" stroke-width="1.5">']
|
||||
# motor: housing Ø60 x 42, rear plate, cover Ø60.3 x 3.5
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
g.append('<rect class="joint-geom-part" data-part="电机机座" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x0 + 5.5 * s, cy - 30 * s, 47 * s, 60 * s))
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
g.append('<rect class="joint-geom-part" data-part="尾部PCBA" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x0, cy - MT["pcba_w"] / 2.0 * s, MT["pcba_t"] * s, MT["pcba_w"] * s))
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
g.append('<rect class="joint-geom-part" data-part="电机输出端盖" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x_cover - 3.5 * s, cy - 30.15 * s, 3.5 * s, 60.3 * s))
|
||||
# coupling halves (4.5 each) and the driven hub
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover + 4.5 * s, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_hub, cy - 41 * s, 24 * s, 82 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="半联轴器A" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="半联轴器B" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover + 4.5 * s, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="从动盘" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_hub, cy - 41 * s, 24 * s, 82 * s))
|
||||
# clamp band over the closed pair
|
||||
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" stroke-dasharray="6 3"/>'
|
||||
g.append('<rect class="joint-geom-part" data-part="对开抱箍" x="%.1f" y="%.1f" width="%.1f" height="%.1f" stroke-dasharray="6 3"/>'
|
||||
% (x_cover + 0.6 * s, cy - 30 * s, 8.4 * s, 60 * s))
|
||||
g.append('</g>')
|
||||
out.extend(g)
|
||||
|
||||
def dim(y, xa, xb, txt, col="#c00", lx=None):
|
||||
out.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>' % (xa, y, xb, y, col))
|
||||
out.append('<g class="sheet-dim"><line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>' % (xa, y, xb, y, col))
|
||||
for x in (xa, xb):
|
||||
out.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width=".6"/>' % (x, y - 6, x, y + 6, col))
|
||||
# a short span cannot hold a long label centred on it - hang the text off to the left
|
||||
if lx is None:
|
||||
out.append('<text x="%.1f" y="%d" font-size="11" fill="%s" text-anchor="middle">%s</text>'
|
||||
% ((xa + xb) / 2, y - 8, col, txt))
|
||||
@@ -236,19 +272,16 @@ def joint_detail():
|
||||
% (lx, y - 8, col, txt))
|
||||
out.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width=".6"/>'
|
||||
% (lx + 4, y - 11, xa, y, col))
|
||||
out.append('</g>')
|
||||
dim(cy - 240, x0, x_cover, "整机长 %.1f" % MT["cover_z"])
|
||||
dim(cy - 215, x_cover, x_hub, "两盖板面跨距 %.2f" % CT["gap"], lx=620)
|
||||
dim(cy + 250, x0, x_hub, "电机尾基准 → 从动盘贴合面 = %.3f ← 全臂节距的唯一来源" % MS["HUB_FACE_Z"])
|
||||
dim(cy + 215, x_cover + 0.6 * s, x_cover + 9.0 * s, "抱箍轴向 %.3f~%.3f" % tuple(MS["CLAMP_SPAN"]), lx=470)
|
||||
|
||||
def note(x, y, tx, ty, txt, anchor="start"):
|
||||
"""Leader + label. Labels go in one of the two gutters (x=235 right-aligned, x=900
|
||||
left-aligned) - the first version dropped them on top of the geometry they describe.
|
||||
The leader runs vertical off the anchor first, then horizontal: a straight diagonal
|
||||
from anchor to gutter cut across the section it is describing."""
|
||||
out.append('<polyline points="%.1f,%.1f %.1f,%.1f %.1f,%.1f" fill="none" stroke="#555" stroke-width=".7"/>'
|
||||
out.append('<g class="sheet-note"><polyline points="%.1f,%.1f %.1f,%.1f %.1f,%.1f" fill="none" stroke="#555" stroke-width=".7"/>'
|
||||
% (x, y, x, ty, tx, ty))
|
||||
out.append('<text x="%.1f" y="%.1f" font-size="10.5" text-anchor="%s">%s</text>'
|
||||
out.append('<text x="%.1f" y="%.1f" font-size="10.5" text-anchor="%s">%s</text></g>'
|
||||
% (tx + (3 if anchor == "start" else -3), ty + 3.5, anchor, txt))
|
||||
note(x0 + 28 * s, cy - 30 * s, 235, cy - 190, "机座 Ø%.0f x %.0f,滑配孔按 Ø%.1f"
|
||||
% (MT["hous_d"], MT["hous_len"], 60.3), "end")
|
||||
@@ -271,7 +304,10 @@ def joint_detail():
|
||||
return "".join(out)
|
||||
|
||||
|
||||
SH, EL, WR = LAY["SH"], LAY["EL"], LAY["WR"]
|
||||
j2 = JOINTS["J2"]["rear"]
|
||||
j4 = JOINTS["J4"]["rear"]
|
||||
j5 = JOINTS["J5"]["rear"]
|
||||
SH, EL, WR = (j2[1], j2[2]), (j4[1], j4[2]), (j5[1], j5[2])
|
||||
DIMS_ARM = [((0, SH[0], SH[1]), (0, EL[0], EL[1]), "上臂 %.0f" % LAY["L1"], 74),
|
||||
((0, EL[0], EL[1]), (0, WR[0], WR[1]), "前臂 %.0f" % LAY["L2"], 112),
|
||||
((0, 0, 0), (0, WR[0], WR[1]), "腕轴离地 %.0f" % WR[1], -128)]
|
||||
@@ -279,13 +315,13 @@ DIMS_ARM = [((0, SH[0], SH[1]), (0, EL[0], EL[1]), "上臂 %.0f" % LAY["L1"], 74
|
||||
sheets = [
|
||||
build_sheet("front", "总装图 - 正视(沿 -X 投影;肢体在纸面内,显示左右交替堆叠)", 1200, 800, 0.0,
|
||||
"实线=打印件,虚线=电机/已有联轴器;点划线为关节轴线;蓝色为节距链。"
|
||||
"俯仰电机一内一外交替,全部贴合面共用 Y=157 一个平面", True, DIMS_ARM),
|
||||
"俯仰电机一内一外交替,全部贴合面共用 Y=157 一个平面", True, DIMS_ARM, sheet_id="front"),
|
||||
joint_detail(),
|
||||
build_sheet("iso", "爆炸装配图(轴测)", 1200, 800, 1.0,
|
||||
"爆炸量 100%:每个关节沿肢体方向逐级错开一节,电机与端面齿组件再沿各自轴线抽出 —— 自上而下即装配顺序", False, None),
|
||||
"爆炸量 100%:每个关节沿肢体方向逐级错开一节,电机与端面齿组件再沿各自轴线抽出 —— 自上而下即装配顺序", False, None, sheet_id="exploded"),
|
||||
build_sheet("side", "总装图 - 侧视(沿 +Y 投影;俯仰轴垂直纸面)", 900, 800, 0.0,
|
||||
"肩抬举(J1)轴沿 X 前后布置,与俯仰轴平行偏置 92 mm;"
|
||||
"肩吊臂侧板在 |x| ≥ 64,肢体包络 |x| ≤ 60.5,故整周不碰", True, None),
|
||||
"肩吊臂侧板在 |x| ≥ 64,肢体包络 |x| ≤ 60.5,故整周不碰", True, None, sheet_id="side"),
|
||||
]
|
||||
os.makedirs(DOCS, exist_ok=True)
|
||||
for n, s in zip(("sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"), sheets):
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""跨件共用的刀法与统一配方:X 向基础体、减重腔 helper、电机前端压盖、止口 hub、pitch->roll 转接座骨架。"""
|
||||
import arm_build as AB
|
||||
import cadquery as cq
|
||||
import human_layout as HL
|
||||
import math
|
||||
from armcore import cyl, place
|
||||
from arm_build import COVER_Z, R, T, Ycyl, bx, prism
|
||||
from human_layout import X_ARM, Y_AXIS, Y_COVER
|
||||
|
||||
|
||||
Y_MID = Y_AXIS
|
||||
|
||||
|
||||
BOSS = COVER_Z + 0.2
|
||||
|
||||
|
||||
DISC_R = 40.0
|
||||
|
||||
|
||||
SLEEVE_R = 35.0
|
||||
|
||||
|
||||
# 深度圆角优化参数
|
||||
CLEARANCE_GAP = 2.0 # 关节间隙 2.0mm
|
||||
|
||||
|
||||
FILLET_TINY = 1.5 # 微小圆角(螺孔边缘)
|
||||
|
||||
|
||||
FILLET_SMALL = 3.0 # 小圆角(功能面)
|
||||
|
||||
|
||||
FILLET_MEDIUM = 6.0 # 中圆角(结构过渡)
|
||||
|
||||
|
||||
FILLET_LARGE = 10.0 # 大圆角(主要轮廓)
|
||||
|
||||
|
||||
FILLET_XLARGE = 15.0 # 超大圆角(视觉主导区域)
|
||||
|
||||
|
||||
CHAMFER_EDGE = 2.0 # 边缘倒角
|
||||
|
||||
|
||||
def Xcyl(r, x0, x1, py, pz):
|
||||
return place(place(cyl(r, 0.0, x1 - x0, 0.0, 0.0), R(90, 0, 1, 0)), T(x0, py, pz))
|
||||
|
||||
|
||||
def Xplate(pts_yz, x0, x1):
|
||||
prof = [(-p[1], p[0]) for p in pts_yz]
|
||||
return place(place(prism(prof, 0.0, x1 - x0), R(90, 0, 1, 0)), T(x0, 0.0, 0.0))
|
||||
|
||||
|
||||
def XB(a0, a1, y0, y1, z0, z1):
|
||||
return bx(X_ARM + a0, X_ARM + a1, y0, y1, z0, z1)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 减重(镂空)--------
|
||||
# 三个手法件(P04/P06/P07)在关节根部都留着一大块"实心过渡料":壁厚最深到 15~17.5mm,
|
||||
# 而 gyroid 填充只有 25~35%,等于用整块实心去抗一个安全系数 19~122 的截面。
|
||||
# 下面两个 helper 把这块料掏掉,规则与 P03 的"帐篷形减重窗"一致:
|
||||
# 1) 不留封闭空腔 —— 减重腔要么朝上敞开,要么是有进出口的窗,打印不憋气、不积料;
|
||||
# 2) 斜面自支撑 —— 斜面与水平面夹角 >= 52°(与竖直面夹角 <= 38°),免支撑;
|
||||
# 3) 内角 R4 圆角 —— 挖出来的洞会把圆截面变成尖角,Kt 从 ~1.2 抬到 ~2.5,
|
||||
# 圆角把应力集中压回去;
|
||||
# 4) 只吃"距表面 > 8mm"的厚料,四周至少留 8mm 壁 —— 受力纤维一根不碰。
|
||||
|
||||
HOLLOW_WALL = 8.0 # 减重腔与原外表面的最小间距(= 留下的最小壁厚)
|
||||
|
||||
|
||||
HOLLOW_FILLET = 4.0 # 减重腔内角圆角,与 P03 竖板减重窗一致
|
||||
|
||||
|
||||
def hollow_channel(x0, x1, y0, y1, z_floor, z_open, tag="", fil=HOLLOW_FILLET):
|
||||
"""朝上敞开的减重槽:从 z_open 往下切到 z_floor,槽底水平(朝上受支撑)、侧壁竖直。
|
||||
|
||||
竖打(模型 Z = 打印 Z)下这个形状是零支撑的:槽底是水平面、槽壁全是竖直面,
|
||||
顶部完全敞开因而不构成封闭空腔。切完该件变成 U 形槽钢而不是实心料:
|
||||
抗弯靠两侧 X 向腹板(正好落在弯矩拉压区),中间中性层掏空几乎不损失 I。
|
||||
|
||||
x0/x1/y0/y1 用世界坐标;z_open 取一个保证贯通顶面的值。
|
||||
"""
|
||||
tool = bx(x0, x1, y0, y1, z_floor, z_open)
|
||||
if fil > 0:
|
||||
try:
|
||||
tool = cq.Workplane(obj=tool).edges("|Z").fillet(fil).val()
|
||||
except Exception as e:
|
||||
print(" [减重槽] %s 内角圆角跳过: %s" % (tag, e), flush=True)
|
||||
print(" [减重槽] %s: X%.1f..%.1f Y%.1f..%.1f 自 Z%.1f 向上敞开 (四周留壁 >=%.0fmm)"
|
||||
% (tag, x0, x1, y0, y1, z_floor, HOLLOW_WALL), flush=True)
|
||||
return tool
|
||||
|
||||
|
||||
def hollow_tent_window(pts_xz, y0, y1, tag="", fil=HOLLOW_FILLET):
|
||||
"""帐篷形减重窗:XZ 平面画轮廓,沿 Y 双向切穿。
|
||||
|
||||
轮廓下宽上窄(拱形)——窗顶上方的料形成一个尖顶,斜面与水平夹角 >= 52° 自支撑,
|
||||
只有顶部一小段平顶需要桥接(<=20mm,与 P03 同)。轮廓在 X 方向不能吃满整个截面,
|
||||
两侧必须留墙,否则该高度上零件会被切断、失去单实体。
|
||||
"""
|
||||
# XZ 工作平面的法线是 -Y,extrude 会朝 -Y 走,所以起点放在 y1 才能覆盖 y0..y1
|
||||
tool = (cq.Workplane("XZ", origin=(0, y1, 0)).polyline(pts_xz).close()
|
||||
.extrude(y1 - y0).val())
|
||||
if fil > 0:
|
||||
try:
|
||||
tool = cq.Workplane(obj=tool).edges("|Y").fillet(fil).val()
|
||||
except Exception as e:
|
||||
print(" [减重窗] %s 内角圆角跳过: %s" % (tag, e), flush=True)
|
||||
print(" [减重窗] %s: XZ 轮廓 %d 点, 沿 Y %.1f..%.1f 切穿 (帐篷形, 斜面自支撑)"
|
||||
% (tag, len(pts_xz), y0, y1), flush=True)
|
||||
return cq.Shape(tool.wrapped)
|
||||
|
||||
|
||||
# 各关节"电机前端夹紧"统一配方(J2/J3/J4/J5 通用,J1 由 P03 自己那套 S04 凸台实现)。
|
||||
# 电机前端盖外端面:Y 轴关节(J2/J4/J5)在 Y_COVER=156,压盖朝 +Y 生长;
|
||||
# Z 轴关节(J3,roll 电机,轴向 -Z)在 Z_R-56,压盖朝 -Z 生长(-Z 侧是从动件)。
|
||||
# 压盖背面与之 0.0 间隙面压,螺钉只提供压紧力。
|
||||
# 沉孔相位按【关节局部相位】定义(BCD55,72.2°+90k):换算到全局后 Y 轴关节是
|
||||
# 17.8°+90k(与 P03→M1 的 S04 一致),Z 轴关节是同一组数落在全局 (x,y) 平面,
|
||||
# 两种轴向的物理指向完全一致 —— 全机同一套孔位约定,改一处必须同步另一处。
|
||||
CLAMP_BCD = 27.5
|
||||
|
||||
|
||||
CLAMP_CLOCK = (17.8, 107.8, 197.8, 287.8) # Y 轴关节的全局 (x,z) 相位
|
||||
|
||||
|
||||
CLAMP_CLOCK_LOCAL = tuple((360.0 - a) % 360.0 for a in CLAMP_CLOCK) # = 72.2+90k
|
||||
|
||||
|
||||
CLAMP_ROOT = 0.5 # 根部薄环厚度:与前端定位环前端面共面,保证熔接成一体
|
||||
|
||||
|
||||
CLAMP_T = 5.0 # 压盖本体厚度
|
||||
|
||||
|
||||
CLAMP_CLR = 1.0 # 从动件让位间隙:与 Y 轴关节 hub_gap=6.0 同一个道理
|
||||
|
||||
|
||||
CLAMP_BACKOFF = CLAMP_T + CLAMP_CLR - CLAMP_ROOT # =5.5 从动件毂面整体后移量
|
||||
|
||||
|
||||
# 【2026-10-05e】"电机前端围挡"统一整改开关,作用于全机 5 处电机前端(J2/J3/J4/J5/J6)。
|
||||
# 围挡本体 = 各零件前端的 **前端定位环**(`front_collar_pocket` 把座孔从 R30.15 扩到
|
||||
# R30.25 时留下的那圈 r30.25..35、6mm 高的环墙)。它比电机前端盖 Ø60.3 凸出 4.75mm,
|
||||
# 把电机前端盖整圈围住;J2/J3/J4/J5 上还骑着一枚 R35 前端压盖(Ø70 再凸 3.2mm)。
|
||||
# 用户 2026-10-05 先在 J3/M3 尾部指出,2026-10-05d 只切敞口那一圈解决;本次把同一刀
|
||||
# 推广到其余四处(J2/M2、J4/M4、J5/M5、J6/M6),几何完全同构。
|
||||
# 为什么不整圈削到与电机齐平(R35 -> R31.8):
|
||||
# 1. 那圈壁与本体在两个方位与外壁**相切**,整圈切完那两片切向楔形会脱空成孤立碎块
|
||||
# (实测 1.32mm3),破坏单实体拓扑;
|
||||
# 2. 压盖根部只有 0.5mm 熔接带,环削窄后连接面从 2.37mm2 掉到 0.78mm2,
|
||||
# 4xM3x8 的夹紧力会把压盖从本体上拉下来。
|
||||
# 所以只切**敞口那一半**:背脊侧定位环继续定位电机前端盖、并作为压盖唯一且足够强的
|
||||
# 支撑/熔接面;压盖本体、Ø37 让位孔、4xM3x8 沉孔、腔口下沿全部不动。
|
||||
# 【2026-10-05e / 2026-10-07 修复:电机装配敞口通道贯通,消除两端阻碍围挡】
|
||||
# 参考 P05 大臂旋转座设计,电机两头都不要有围挡挡住,电机可顺畅装配入座:
|
||||
# 1. 承压面段通过 R35 圆腔切平,彻底消除尾端阻碍电机的环形围挡;
|
||||
# 2. 前端定位环通过 cut_motor_fence 切除敞口那一半,消除前端阻碍装配的围挡;
|
||||
# 3. 背脊侧整圈定位环完整保留,继续定位电机前端盖并作为压盖唯一且足够强的支撑/熔接面。
|
||||
FENCE_OPEN_CUT = True
|
||||
|
||||
|
||||
def cut_motor_fence(shape, j, tag, name3, z0=49.4, z1=55.5):
|
||||
"""只切掉电机前端定位环在敞口侧那一圈环墙。"""
|
||||
if not FENCE_OPEN_CUT:
|
||||
return shape
|
||||
"""【2026-10-05e / 2026-10-07 整改】只切掉电机前端围挡(前端定位环)在**敞口那一半**的环墙。
|
||||
z1 设为 55.5(等于前端定位环外缘 Y_COVER-0.5,不啃压盖本体):
|
||||
原取 56.1 导致切削盒切入前端压盖内侧贴合面 0.1mm(本应在 56.0/Y156.0),
|
||||
造成敞口侧下陷 0.1mm,与背脊侧在关节中心面产生错位台阶与分界线。
|
||||
取 55.5 后定位环完整切除,压盖贴合面在整圈 360° 完整保留为共面的平圆环(Y156.0),分界线彻底消除。
|
||||
|
||||
与 2026-10-05d 给 P05/M3 那一刀完全同源 —— 当时那记全局盒切
|
||||
(X_ARM±40, Y_AXIS-40..Y_AXIS+3, Z388.9..395.6) 换算到 J3 局部系正好是
|
||||
x±40 / y -3..+40 / z 49.4..56.1。本函数把它从"P05 专用全局盒"泛化成
|
||||
"按关节局部坐标切半个环",J2/J4/J5/J6 于是同一刀通用。
|
||||
|
||||
局部坐标约定(见 trs/place):z 沿电机轴、指向电机前端盖(外端面在 z=COVER_Z=56),
|
||||
x/y 在垂直于轴的平面内。**局部 +y 一律是半包敞口侧** —— 实测 5 处电机舱的
|
||||
本体(环外 r=37/40 有料处 = 背脊侧)方位角全部落在局部角 180..360 = 局部 -y 侧:
|
||||
J2/M2 背脊 240..315 J3/M3 背脊 225..315 J4/M4 背脊 210..330
|
||||
J5/M5 背脊 210..330 J6/M6 背脊 195..345
|
||||
故敞口方向无需逐件配置,统一取局部 +y。
|
||||
|
||||
为什么一对 z 参数能覆盖全机:5 处电机的前端定位环都恰好落在**局部 z 49.5..56**
|
||||
(Y 轴电机前端环 Y150..156 -> z50..56;roll 电机 Z395.5..389 -> z49.5..56)。
|
||||
|
||||
被切掉的只有 r30.25..35 那一圈环墙在敞口侧的部分;压盖本体(4xM3x8 沉孔段)、
|
||||
Ø37 让位孔、腔口下沿、背脊侧整圈定位环全部不动。
|
||||
切口边界取在局部 y=-3.0 而不是 0:环壁在局部 y=-3 处仍与背脊侧实体连通,
|
||||
切出来的是真正的缺口而不是零厚度缝,单实体拓扑天然保持
|
||||
(P05 实测切完 solids=1;2026-10-05c 整圈削 R31.8 那版会掉出 1.32mm3 碎块)。
|
||||
"""
|
||||
if not FENCE_OPEN_CUT:
|
||||
return shape
|
||||
ymin = -3.0 if name3 in ('P05', 'P08') else 0.0
|
||||
shape = shape.cut(j.L(bx(-40.0, 40.0, ymin, 40.0, z0, z1)))
|
||||
print(" [围挡] %s: 只切敞口侧那一圈前端定位环(局部 y %.1f..+40, z %.1f..%.1f);"
|
||||
"背脊侧整圈保留定位前端盖并支撑压盖,压盖本体/沉孔/腔口下沿不变"
|
||||
% (name3, ymin, z0, z1), flush=True)
|
||||
return shape
|
||||
|
||||
|
||||
def _clamp_cyl(axis, px, pz, py, z_face):
|
||||
"""压盖刀具坐标系:C(r, s0, s1, du, dv) = 沿电机轴 s 区间、偏离轴线 (du,dv) 的圆柱。
|
||||
s>0 一律"背离电机",两个轴向同号(s=0 = 电机前端盖外端面)。"""
|
||||
if axis == "Y":
|
||||
return lambda r, s0, s1, du=0.0, dv=0.0: Ycyl(r, Y_COVER + s0, Y_COVER + s1,
|
||||
px + du, pz + dv)
|
||||
if axis == "Z":
|
||||
return lambda r, s0, s1, du=0.0, dv=0.0: cyl(r, z_face - s1, z_face - s0,
|
||||
px + du, py + dv)
|
||||
raise ValueError("motor_front_clamp: axis 只能是 Y 或 Z,收到 %r" % (axis,))
|
||||
|
||||
|
||||
def clamp_hole_drills(px, pz, plate_t=CLAMP_T, axis="Y", py=0.0, z_face=None):
|
||||
"""4xM3x8 压盖钻模:Ø6.0 沉孔(s=2..plate_t,深 3.0)+ Ø3.4 通孔(s=-1.0..2)。
|
||||
|
||||
与 motor_front_clamp 共用同一组半径/相位/深度,孔位只有这一个事实源。
|
||||
单独导出是因为**压盖上的孔在预钻阶段可能被本体填回来**:本体若有料占住沉孔方位,
|
||||
`fuse(压盖)` 取的是并集,不会把本体那部分料挖掉,于是并集上孔就消失了
|
||||
(P04 立柱整体化后 Y158..161 的 +Z 半侧被填成实心,4 个孔只剩 2 个)。
|
||||
所以对"熔接之后的最终并集"必须再钻一次;本体没料的方位上这一刀是空操作。
|
||||
"""
|
||||
C = _clamp_cyl(axis, px, pz, py, z_face)
|
||||
s0, s1 = -CLAMP_ROOT, plate_t
|
||||
drills = []
|
||||
for deg in CLAMP_CLOCK_LOCAL:
|
||||
rad = math.radians(-deg) # 全局相位 = -局部相位
|
||||
fu, fv = CLAMP_BCD * math.cos(rad), CLAMP_BCD * math.sin(rad)
|
||||
drills.append(C(3.0, 2.0, s1 + 0.5, fu, fv)) # Ø6.0 沉孔
|
||||
drills.append(C(1.7, s0 - 0.5, 2.0, fu, fv)) # Ø3.4 通孔
|
||||
return cq.Compound.makeCompound(drills)
|
||||
|
||||
|
||||
def motor_front_clamp(px, pz, plate_t=CLAMP_T, tag="", axis="Y", py=0.0, z_face=None):
|
||||
"""电机前端压盖:把"只被后端安装板单面锁死"改成前后双向面压夹紧。
|
||||
|
||||
原设计里每个关节的电机都只有后端板 4xM3x10(S02)锁在尾端面上,前端盖完全裸露,
|
||||
大扭矩下电机绕自身轴在 Ø60.3 座孔里点头/翘起。这里在电机前端盖外端面加一枚
|
||||
与后端板同径的 R35 压盖,用 4xM3x8(BCD55)在前端盖上再夹一次 —— 电机被前后两块
|
||||
同心 Ø60.3 端面双向夹死,倾斜自由度归零;夹紧力全部走面压,螺钉不参与定位。
|
||||
|
||||
两个轴向共用同一套尺寸,只换刀具体系(不重复实现任何半径/相位):
|
||||
axis="Y"(J2/J4/J5):关节轴过 (px, pz),前端盖外端面在 Y_COVER,压盖朝 +Y 长;
|
||||
axis="Z"(J3 roll,轴向 -Z):关节轴过 (px, py),前端盖外端面在 z_face,压盖朝 -Z 长。
|
||||
|
||||
局部坐标(s 从前端盖外端面起算,s>0 一律"背离电机",两个轴向同号):
|
||||
- 压盖 R35,s 从 -CLAMP_ROOT(与前端定位环前端面共面,保证熔接成一体)到 plate_t;
|
||||
- 背面精确落在 s=0,不让根部那 0.5mm 啃到端盖 R30.15 的外圆;
|
||||
- Ø37 中央让位孔(R18.5)让开从动件的直锁凸台 R17.8 与 3xS01 输出螺钉头;
|
||||
- 4xM3x8:Ø3.4 通孔 + Ø6.0 沉孔深 3.0(自前端面沉到 s=2),圆柱头全沉入,
|
||||
头面压孔口台阶,中间 2.0mm 压盖本体受压。
|
||||
"""
|
||||
C = _clamp_cyl(axis, px, pz, py, z_face)
|
||||
s0, s1 = -CLAMP_ROOT, plate_t
|
||||
fp = C(35.0, s0, s1)
|
||||
fp = fp.cut(C(18.5, s0 - 0.5, s1 + 0.5)) # Ø37 中央让位孔
|
||||
# 【2026-10-03h 接缝整改】根部让位槽原来切 R30.15,比腔壁 BAY_R=30.50 小 0.35,
|
||||
# 于是压盖根部那圈 r30.15..35 的定位唇内壁比腔壁**内凸 0.35**,从腔里看就是
|
||||
# 腔口一圈 0.5mm 宽、0.35mm 深的环槽(P04/P05/P06/P07 各有一圈,实测台阶面
|
||||
# 31.2mm2 + R30.15 环带 44.4mm2,四件完全相同)。这刀是腔还是 R30.15/R30.25
|
||||
# 那个年代留下的:当时必须让到电机实测外圆,压盖根部才啃不到电机。
|
||||
# 现在主腔已统一到 BAY_R,电机实测 R30.15,让位槽取 BAY_R 即可:
|
||||
# 定位唇内壁与腔壁同一根正圆,两张圆柱面首尾相接,中间不再有任何台阶,
|
||||
# 对电机的单边间隙仍是 0.35(唇内径 Ø61.0 > 电机 Ø60.3)。
|
||||
fp = fp.cut(C(AB.BAY_R, s0 - 0.5, 0.0)) # Ø61.0 根部让位槽(= 腔径)
|
||||
fp = fp.cut(clamp_hole_drills(px, pz, plate_t, axis, py, z_face))
|
||||
where = ("Y%.1f..%.1f" % (Y_COVER - CLAMP_ROOT, Y_COVER + plate_t)) if axis == "Y" \
|
||||
else ("Z%.1f..%.1f" % (z_face - plate_t, z_face + CLAMP_ROOT))
|
||||
print(" [前端压盖] %s: R35 %s, 4xM3x8 BCD55@%s (%s 轴)"
|
||||
% (tag, where, "/".join("%.1f" % d for d in CLAMP_CLOCK), axis), flush=True)
|
||||
return fp
|
||||
|
||||
|
||||
def hub_m2_holes(shape, j, cb_z1, th_z1=58.0, th_z0=54.0, cb_z0=58.0,
|
||||
th_r=1.4, cb_r=2.8, clock=None):
|
||||
"""在从动毂上开 3x M2 直锁螺钉孔:Ø2.8 通孔 + Ø5.6 沉孔,BCD 见 AB.M["m2_bcd"]。
|
||||
|
||||
全机 5 处(J1 叉板 / J2+J3 直连毂 / J4+J5 直连毂)统一走这里,各调用点只传自己那两个 z。
|
||||
【2026-10-07 修复】J1 直连电机输出转子,使用转子真实孔位 AB.M["m2_clock"](随 j1.t 旋转),
|
||||
确保肩叉 P04 保持垂直位姿且沉孔与 M1 转子螺孔完全同心。
|
||||
"""
|
||||
if clock is None:
|
||||
clock = AB.M["m2_clock"] if j.jid == "J1" else AB.HUB_M2_CLOCK
|
||||
for a in clock:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
shape = shape.cut(j.L(cyl(th_r, th_z0, th_z1, u, v)))
|
||||
shape = shape.cut(j.L(cyl(cb_r, cb_z0, cb_z1, u, v)))
|
||||
return shape
|
||||
|
||||
|
||||
def make_direct_hub(shape, j, r_disc=35.0, z_out=74.0, backoff=0.0):
|
||||
"""优化直连法兰:增加导向倒角
|
||||
|
||||
backoff = 毂面沿电机轴**远离电机**的让位量(2026-10-03 新增,目前只有 P09 用)。
|
||||
|
||||
场景:J6/M6 补上前端压盖后(见 p_wrist_bracket 的 roll_clamp=True),压盖占
|
||||
局部 z 55.5..61.0(= 全局 Z 120.5..115),而毂面原来在局部 56.5,两者直接打架。
|
||||
按 P06 给 J3 让位的同一套办法,把毂面整体后移 CLAMP_BACKOFF=5.5:
|
||||
毂面 56.5 -> 62.0,压盖前端面在 61.0,中间正好留 CLAMP_CLR=1.0。
|
||||
|
||||
只动"外端"坐标,不动"电机端"坐标,否则咬合量就变了:
|
||||
直锁凸台起点 54.942 不动、长度 54.942..56.5 -> ..62.0 加长 5.5,
|
||||
进入电机输出毂的深度与改前完全一致(与 p_humerus_bone 的 J3 处理同源);
|
||||
导向锥 54.5..54.942 不动;
|
||||
3xS01 输出螺钉让位通道与 4xM2 直锁沉孔的**内**端(54.0 / 58.0)不动,
|
||||
只把外端(z_out+30 / z_out+40)跟着放长。
|
||||
"""
|
||||
disc = cyl(r_disc, 56.5 + backoff, z_out + backoff)
|
||||
boss = cyl(17.8, 54.942, 56.5 + backoff)
|
||||
chamfer_cone = cyl(18.5, 54.5, 54.942)
|
||||
shape = shape.fuse(j.L(disc)).fuse(j.L(boss)).cut(j.L(chamfer_cone))
|
||||
shape = shape.cut(j.L(cyl(3.5, 54.0, z_out + 30.0 + backoff)))
|
||||
return hub_m2_holes(shape, j, cb_z1=z_out + 40.0 + backoff)
|
||||
|
||||
|
||||
def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hub_gap=1.0,
|
||||
roll_clamp=False, fillet_r=1.5, flank_fillet_r=3.0):
|
||||
"""Half-wrapped (半包) 双曲面有机流线型 Pitch-to-Roll 结构座 (P05 & P08):
|
||||
1. 顶部从动法兰与前侧 Loft 自然相切顺接;
|
||||
2. 严格清除 z_plate 顶面以上与上级旋转件贴面的多余材料 (cut_top_solid),保证关节大范围摆动动态 0 干涉;
|
||||
3. 下方电机实行半包散热设计,-Y侧敞开,+Y侧背脊与圆柱一体化融合;
|
||||
4. 严格满足 single-solid 流形拓扑与零干涉 (COLLISION pairs = 0)。
|
||||
|
||||
hub_gap = 从动法兰盘背面与上级电机前端盖之间的轴向让位间隙。
|
||||
默认 1.0(贴合防磨)。P05 传 6.0:J2 处 P04 新增了 M2 前端压盖
|
||||
(Y 155.5..161.0),必须让出这 6.0mm 才能装下;见 p_shoulder_roll_base。
|
||||
|
||||
roll_clamp = 本件作为 roll 电机【前端夹紧件】时为 True(J3/P05 夹 M3)。
|
||||
roll 电机(J3/J6)轴向是 -Z,前端盖外端面在 z_r-56,-Z 侧是从动件:
|
||||
压盖朝 -Z 长(Z_COVER-5..Z_COVER),从动件毂面必须整体后移 CLAMP_BACKOFF=5.5
|
||||
让位(与 Y 轴关节 hub_gap=6.0 同一个道理,只是基准间隙 0.5 而非 1.0)。
|
||||
P08 自 2026-10-03 起也传 True:M6 前端压盖补齐,对侧 P09 让位 CLAMP_BACKOFF
|
||||
(见 make_direct_hub 的 backoff),五处电机腔至此全部是「后端板 + 前端压盖」
|
||||
双向面压,配方同源。
|
||||
|
||||
fillet_r = 立柱顶面与竖直臂交汇处的 L 型内转角过渡圆角半径(mm,默认 1.5)。
|
||||
半径调小为 1.5mm 确保在 OCC 中稳定边倒圆成型(小于临界高度 2.0mm,彻底避免算法异常),
|
||||
同时不生成多余外凸台阶,平滑消除 90° 锐角应力集中。
|
||||
|
||||
flank_fillet_r = 侧壁平直立面与正面 Loft 放样曲面交汇处空间斜向折棱圆角半径(mm,默认 3.0)。
|
||||
【2026-10-07】消除侧壁与正面交界处长约 72mm 的尖锐硬折棱,使侧腹部外轮廓更加饱满圆润。
|
||||
"""
|
||||
z_bot = z_r - 56.0 # roll 电机底端 (front collar)
|
||||
z_plate = z_r + 8.0 # roll 电机顶端安装板 (rear plate top);见第 8 步 t_plate 说明
|
||||
y_out = HL.Y_HUB + HL.PLATE_T # 174.0 (俯仰直连法兰外侧面)
|
||||
y_hub_in = HL.Y_COVER + hub_gap # 157.0 / (P05) 162.0 从动法兰内侧面
|
||||
z_hub_in = y_hub_in - HL.Y_REAR # 法兰盘背面的电机局部 z(凸台长度跟着它走)
|
||||
y_cyl_max = HL.Y_AXIS + 35.0 # 163.0 (下方电机外轮廓极值)
|
||||
|
||||
# 1. 基础立柱:Ø70mm 实心圆柱 (Z: z_bot .. z_plate, R=35mm)
|
||||
s = cyl(35.0, z_bot, z_plate, HL.X_ARM, HL.Y_AXIS)
|
||||
|
||||
# 2. 俯仰轴直连法兰盘:全圆柱实体 (Y: y_hub_in .. y_out, R=35mm)
|
||||
disc = Ycyl(35.0, y_hub_in, y_out, HL.X_ARM, z_pitch)
|
||||
s = s.fuse(disc)
|
||||
|
||||
# 3. 前侧从底端 Ø70mm 圆柱到顶端法兰的有机相切平滑渐变过渡体 (Loft Morph)
|
||||
Z_top = z_pitch - 35.0
|
||||
z_start = z_bot
|
||||
N = 16
|
||||
wp = cq.Workplane("XY")
|
||||
for i in range(N + 1):
|
||||
t = i / float(N)
|
||||
z_curr = z_start + t * (Z_top - z_start)
|
||||
st = 3.0 * (t**2) - 2.0 * (t**3)
|
||||
y_center = y_cyl_max + st * (y_out - y_cyl_max)
|
||||
y_side = HL.Y_AXIS + st * (y_out - HL.Y_AXIS)
|
||||
v_c = y_center - HL.Y_AXIS
|
||||
v_s = y_side - HL.Y_AXIS
|
||||
v_mid = max(v_c, v_s + 0.02)
|
||||
# 后缘两角 R2 圆角:把 y=123 凸包与 R35 柱面交汇的两条 90° 竖棱圆润化
|
||||
# (磨掉"凸起来的"尖角,不动后缘高度/位置,打印无悬挂)
|
||||
FR = 2.0
|
||||
w = (cq.Workplane("XY", origin=(HL.X_ARM, HL.Y_AXIS, z_curr))
|
||||
.moveTo(-35.0, v_s)
|
||||
.threePointArc((0.0, v_mid), (35.0, v_s))
|
||||
.lineTo(35.0, -5.0 + FR)
|
||||
.radiusArc((35.0 - FR, -5.0), FR)
|
||||
.lineTo(-35.0 + FR, -5.0)
|
||||
.radiusArc((-35.0, -5.0 + FR), FR)
|
||||
.lineTo(-35.0, v_s)
|
||||
.close())
|
||||
wp = wp.add(w)
|
||||
|
||||
loft = wp.toPending().loft()
|
||||
s = s.fuse(loft.val())
|
||||
|
||||
# 4. 顶部填充(仅保留 y >= y_hub_in 与法兰盘对齐的部分,避免侵入上级关节旋转扫掠区域)
|
||||
top_fill = (cq.Workplane("YZ", origin=(HL.X_ARM, 0, 0))
|
||||
.moveTo(y_hub_in, Z_top)
|
||||
.lineTo(y_out, Z_top)
|
||||
.lineTo(y_out, z_pitch)
|
||||
.lineTo(y_hub_in, z_pitch)
|
||||
.close()
|
||||
.extrude(35.0, both=True))
|
||||
s = s.fuse(top_fill.val())
|
||||
|
||||
# 5. 旋转清让切除:彻底清除 z >= z_plate 且 y <= y_hub_in 范围内的多余材料,
|
||||
# 消除导致与上游旋转件(P04/P07)严重干涉的立壁与尖角,保持顶面平整规整!
|
||||
cut_top_solid = (cq.Workplane("YZ", origin=(HL.X_ARM, 0, 0))
|
||||
.moveTo(-999.0, z_plate)
|
||||
.lineTo(y_hub_in, z_plate)
|
||||
.lineTo(y_hub_in, z_pitch + 50.0)
|
||||
.lineTo(-999.0, z_pitch + 50.0)
|
||||
.close()
|
||||
.extrude(60.0, both=True))
|
||||
s = s.cut(cut_top_solid.val())
|
||||
|
||||
# 6. 电机半包处理 (Half-Enclosure):-Y侧敞开散热,+Y侧背脊壳体加强
|
||||
# 切削高度上限直通承压面(z_r + AB.REAR_SEAT),消除侧边悬垂突耳,使侧壁边缘与下方完全平直对齐
|
||||
cut_open = bx(HL.X_ARM - 40.0, HL.X_ARM + 40.0, HL.Y_AXIS - 40.0, HL.Y_AXIS + 3.0, z_bot + 6.0, z_r + AB.REAR_SEAT)
|
||||
s = s.cut(cut_open)
|
||||
|
||||
# 7. 上级电机同心圆弧避让
|
||||
clearance = Ycyl(35.5, HL.Y_AXIS - 40.0, HL.Y_COVER + 0.1, HL.X_ARM, z_pitch)
|
||||
s = s.cut(clearance)
|
||||
|
||||
# 8. 下方电机安装孔位与定位止口
|
||||
# t_plate=8.0:Joint.pocket 的尾端面承压面让空了 MOTOR_TOL=1.0,承压面到螺钉沉孔
|
||||
# 台阶为 t_plate-cb_depth-REAR_SEAT = 8-4-1.6 = 2.4mm 膜厚(结构强度大幅提升)。
|
||||
s = j_roll.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
# 【2026-10-03i 接缝整改】front_collar_pocket 已删。
|
||||
# 它切的是 cyl(BAY_R, z0=50.0, z1=56.5),而紧接着的 Joint.pocket 切的是
|
||||
# cyl(BAY_R, -REAR_SEAT, COVER_Z+front_pad) —— **同一根圆柱、同一个半径**,
|
||||
# 前者完全是后者的子集。两次半径相同的圆柱布尔相减,等于让 OCC 在腔壁上
|
||||
# 留下一条 s=50.0 的零宽棱:实测 P05 的 R30.500 腔壁被切成 5090.5 + 493.9
|
||||
# 两张面(-2.6..50.0 与 50.0..55.5),P06 同样在 50.0 处裂开。着色时那条
|
||||
# 棱就是腔里横穿的一道线。P04/P08 本来就没调它,腔壁是完整一张面。
|
||||
s = j_roll.pocket(s)
|
||||
|
||||
# 9. 电机定子配合空腔
|
||||
# 【2026-10-05f】原 cyl(30.5, ...) 定子配合空腔已删,同上:已被主腔 BAY_R 覆盖。
|
||||
|
||||
# 10. 俯仰轴直连法兰结构(凸台延伸至法兰盘背面之后 2.5mm,跨过间隙与法兰盘牢固熔接,
|
||||
# 保持单实体)。凸台长度跟着 y_hub_in 走:法兰盘后移时凸台同步加长,
|
||||
# 3xS01 输出螺钉的头面台阶(局部 z=58)、杆长、咬合量与改前完全一致。
|
||||
boss = cyl(17.8, 54.942, z_hub_in + 2.5)
|
||||
s = s.fuse(j_pitch.L(boss))
|
||||
s = s.cut(j_pitch.L(cyl(3.5, 54.0, 74.0 + 30.0)))
|
||||
s = hub_m2_holes(s, j_pitch, cb_z1=74.0 + 40.0)
|
||||
|
||||
# 11. L 型转角连接处圆角过渡(圆角半径调小为 1.5mm):
|
||||
# 平滑消除立柱顶面与竖直臂交汇处的 90° 尖角与应力集中,
|
||||
# 半径调小至 1.5mm 确保在 OCC 中稳定成型(小于临界高度 2.0mm,彻底避免算法异常),
|
||||
# 局部圆角不切入两侧轮廓,完全避免任何外凸台阶,流线自然过渡。
|
||||
if fillet_r > 0:
|
||||
try:
|
||||
wp_s = cq.Workplane(obj=s)
|
||||
sel_l = cq.NearestToPointSelector((HL.X_ARM, y_hub_in, z_plate))
|
||||
s = wp_s.edges(sel_l).fillet(fillet_r).val()
|
||||
except Exception as e:
|
||||
print(f"[{name}] L型转角圆角异常: {e}")
|
||||
|
||||
# 12. roll 电机(M3)前端压盖:与 P04/P06/P07 的 M2/M4/M5 压盖同一套配方,
|
||||
# 只是 roll 电机轴向为 -Z,压盖朝 -Z 长(见 motor_front_clamp 的 axis="Z")。
|
||||
# 熔接必须放在最后:第 6/7/8/9 步所有 R30.15~30.5 圆柱刀具都走完之后才接,
|
||||
# 否则压盖根部那 0.5mm 薄环会被 front_collar_pocket / 定子孔啃掉一圈。
|
||||
if roll_clamp:
|
||||
# tag 里的电机号从 j_roll.jid 取(P05->M3 / P08->M6),别再写死 M3 ——
|
||||
# 写死的话 P08 的构建日志会谎报自己夹的是 M3,排查时容易被带偏。
|
||||
s = s.fuse(motor_front_clamp(HL.X_ARM, 0.0,
|
||||
tag="%s/M%d" % (name[:3], int(j_roll.jid[1])),
|
||||
axis="Z", py=HL.Y_AXIS, z_face=z_r - COVER_Z))
|
||||
|
||||
# 13.【2026-10-05d】只切"围挡"那一圈:敞口(-Y)侧的前端定位环 + 压盖根部薄环。
|
||||
# 用户 2026-10-05 指出这圈环墙把 M3 前端盖和 P06 的 J3 凸台围在中间;我第一版
|
||||
# 把整个前端(定位环 + 压盖 + 腔口下沿 389->395)都切了,电机前端没了支撑面 ——
|
||||
# 过度。现在只切敞口这一圈,Y 界限与第 6 步 cut_open 完全一致
|
||||
# (Y_AXIS-40 .. Y_AXIS+3),保证 -Y 半圈从 Z=395.5 一路开到压盖背面 Z=389;
|
||||
# 背脊(+Y)侧整圈定位环保留:
|
||||
# - 继续定位 M3 前端盖(内孔 r30.25,前端盖 Ø60.3 -> 单边 0.10mm);
|
||||
# - 继续当 R35 压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带,本次改动
|
||||
# 完全不碰)→ M3 仍是后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压夹紧;
|
||||
# - 压盖本体 / Ø37 让位孔 / 4xM3x8 沉孔 / 腔口下沿 z_bot=389 全部不动,
|
||||
# 压盖敞口侧的端面从 Z=389.5 降到 389 与背面齐平,视觉上不再凸出。
|
||||
# 为什么不能整圈削到与电机齐平(R35->R31.8,见 FENCE_OPEN_CUT 处的说明):
|
||||
# ① 这圈壁与第 3 步 Loft 在 X=±35 处与 Ø70 柱面**相切**,整圈切完那两片切向楔形
|
||||
# (实测 1.32mm3)会脱空成孤立碎块,破坏单实体拓扑;
|
||||
# ② 压盖根部只有 0.5mm 熔接带,环削到 1.55mm 宽后连接面从 2.37mm2 掉到 0.78mm2,
|
||||
# 4xM3x8 的夹紧力会把压盖从本体上拉下来。
|
||||
# 附带效果:盒切把 Loft 在 X=±35 与 Ø70 柱面相切的那两片切向楔形一起带走
|
||||
# (楔形位于 Y_AXIS 附近 < Y_AXIS+3),不会留下孤立碎块 -> 单实体拓扑保持。
|
||||
# 13.【2026-10-05d / 2026-10-07】删掉 P05/P08 前端定位环在敞口侧那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
if (roll_clamp or FENCE_OPEN_CUT) and name.startswith(('P05', 'P08')):
|
||||
s = cut_motor_fence(s, j_roll, "%s/M%d" % (name[:3], int(j_roll.jid[1])), name[:3])
|
||||
|
||||
# 14.【2026-10-07 表面流线优化】侧壁与有机放样曲面交汇处斜向脊线圆角(flank_fillet_r):
|
||||
# 平滑消除侧壁平直立面与正面放样弧面交汇处的 71.6mm 空间硬折棱,
|
||||
# 沿双侧各倒 R3.0 平滑过渡圆角,使大臂旋转座/腕偏转座外表面流线浑然一体、手感圆润饱满
|
||||
if flank_fillet_r > 0:
|
||||
try:
|
||||
wp_s = cq.Workplane(obj=s)
|
||||
e_flank = [
|
||||
e for e in s.Edges()
|
||||
if e.geomType() == 'BSPLINE'
|
||||
and (abs(e.BoundingBox().xmin - (HL.X_ARM - 35.0)) < 0.2 or abs(e.BoundingBox().xmax - (HL.X_ARM + 35.0)) < 0.2)
|
||||
and (e.BoundingBox().ymax - e.BoundingBox().ymin) > 20.0
|
||||
and (e.BoundingBox().zmax - e.BoundingBox().zmin) > 30.0
|
||||
]
|
||||
if len(e_flank) == 2:
|
||||
s = wp_s.newObject(e_flank).fillet(flank_fillet_r).val()
|
||||
except Exception as e:
|
||||
print(f"[{name}] 侧壁放样斜棱圆角异常: {e}")
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def make_surface_tie_bridge(x, y_surf, z, w_x=10.0, l_z=8.0, h_y=3.2, slot_w=3.2, slot_h=1.8, tunnel_dir='X'):
|
||||
"""实体表面外凸式扎带拱桥 (Cable Tie Saddle Bridge)。
|
||||
在 3D 打印件外壁表面一体化成型微型拱桥,扎带完全在外部穿通锁紧,不挖穿外壳壁厚、不进入内部空腔。
|
||||
"""
|
||||
total_y = h_y + 1.0
|
||||
cy = (y_surf - h_y) + total_y / 2.0
|
||||
body = cq.Workplane('XY', origin=(x, cy, z)).box(w_x, total_y, l_z)
|
||||
try:
|
||||
body = body.edges('|X and <Y').chamfer(0.8)
|
||||
except Exception:
|
||||
pass
|
||||
ty = y_surf - 0.2 - slot_h / 2.0
|
||||
if tunnel_dir == 'X':
|
||||
tunnel = cq.Workplane('YZ', origin=(x, ty, z)).box(slot_h, slot_w, w_x + 4.0)
|
||||
else:
|
||||
tunnel = cq.Workplane('XY', origin=(x, ty, z)).box(slot_w, slot_h, l_z + 4.0)
|
||||
return body.cut(tunnel).val()
|
||||
|
||||
|
||||
def make_surface_tie_bridge_neg_x(x_surf, y, z, w_y=10.0, l_z=8.0, h_x=3.2, slot_w=3.2, slot_h=1.8):
|
||||
"""实体表面外凸式扎带拱桥 (-X 侧向立壁专用,线束沿 Z 竖直走)。
|
||||
|
||||
穿带隧道必须沿 Y 贯通:扎带环的平面由"孔轴 + 壁法向"张成,线束要垂直穿过这个环,
|
||||
所以孔轴只能取 Y(线沿 Z、壁法向 X)。若照抄 Y 壁版把隧道做成沿 Z,扎带就会变成
|
||||
顺着一根线往下套、而不是横抱它 —— 抱不住。
|
||||
"""
|
||||
total_x = h_x + 1.0
|
||||
cx = (x_surf - h_x) + total_x / 2.0
|
||||
body = cq.Workplane('YZ', origin=(cx, y, z)).box(w_y, l_z, total_x)
|
||||
try:
|
||||
body = body.edges('|Y and <X').chamfer(0.8)
|
||||
except Exception:
|
||||
pass
|
||||
tx = x_surf - 0.2 - slot_h / 2.0
|
||||
tunnel = cq.Workplane('XY', origin=(tx, y, z)).box(slot_h, w_y + 4.0, slot_w)
|
||||
return body.cut(tunnel).val()
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""每个零部件一个模块:几何、件号/配色/说明、装配帧归属都在这里。
|
||||
|
||||
arm_model.build() 只按 PART_MODULES 顺序收集 specs();改哪件就开哪个文件。
|
||||
"""
|
||||
from parts import p01_column, p02_base, p03_girdle, p04_fork, p05_roll_base, p06_humerus, p07_forearm, p08_wrist, p09_flange
|
||||
|
||||
PART_MODULES = [p01_column, p02_base, p03_girdle, p04_fork, p05_roll_base, p06_humerus, p07_forearm, p08_wrist, p09_flange]
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P01 立柱三段(A/B/C):躯干承力柱 + 阶梯榫卯与防反装对锁孔。"""
|
||||
import cadquery as cq
|
||||
from armcore import cyl
|
||||
from arm_build import Ycyl, bx
|
||||
from human_layout import Y_AXIS, Z_CHEST1
|
||||
from part_tools import Xcyl
|
||||
|
||||
|
||||
# ========== 躯干部分 ==========
|
||||
# ========== 躯干/测试台架部分 (后置防倾翻高刚性机架) ==========
|
||||
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 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 圆角。
|
||||
"""
|
||||
z_bot = 22.0
|
||||
z_top = Z_CHEST1 # 470.0
|
||||
|
||||
col = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_bot))
|
||||
.rect(66.0, 66.0)
|
||||
.extrude(z_top - z_bot)
|
||||
.edges("|Z").fillet(6.0)).val()
|
||||
s = cq.Shape(col.wrapped)
|
||||
|
||||
cav = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_bot + 10.0))
|
||||
.rect(44.0, 44.0)
|
||||
.extrude(z_top - z_bot + 1.0)
|
||||
.edges("|Z").fillet(4.0)).val()
|
||||
s = s.cut(cav)
|
||||
|
||||
# 底部阶梯榫头:插入 P02 榫座后环形台肩坐实,轴向力走面压而非 M5 螺纹
|
||||
tenon = (cq.Workplane("XY", origin=(X_COL, Y_COL, 10.0))
|
||||
.rect(50.0, 50.0).extrude(z_bot - 10.0)
|
||||
.edges("|Z").fillet(4.0)).val()
|
||||
s = s.fuse(tenon)
|
||||
|
||||
# 底部 M5 夹紧孔(贯通榫头):X 向非对称 (−20/+16) 与榫座孔位配对防反装/防转 90°
|
||||
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))
|
||||
# 顶部 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)
|
||||
.extrude(-20.0)).val()
|
||||
s = s.cut(w_bot)
|
||||
|
||||
w_top = (cq.Workplane("YZ", origin=(X_COL + 33.5, Y_COL, 375.0))
|
||||
.rect(34.0, 70.0)
|
||||
.extrude(-20.0)).val()
|
||||
s = s.cut(w_top)
|
||||
return s
|
||||
|
||||
|
||||
def split_p01_poka_yoke(p01):
|
||||
"""拆分 P01 为 A/B/C 三段(严格适配 200x200mm 打印机,竖直免支撑打印):
|
||||
带 16mm 阶梯互锁榫卯(外套筒壁 3.6 + 公榫壁 6.9,内腔收窄 51->44 换取双厚壁,
|
||||
单边配合间隙 0.5)、以及统一 Ø3.9 直孔的分段对锁孔(M4x16 从外面直接拧入自攻成型,
|
||||
母口壁与公榫壁同径,无过孔/沉孔/预埋,前后两面对称各一颗);
|
||||
孔位相对立柱中心 −16 / +22 非对称(物理防反装防呆);本体 3D 凹刻朝向标识。
|
||||
"""
|
||||
z_bot = 22.0
|
||||
z_s1 = 172.0
|
||||
z_s2 = 321.0
|
||||
z_top = 470.0
|
||||
h_lap = 16.0
|
||||
|
||||
# 非对称对锁孔位(相对立柱中心偏心 -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)
|
||||
tongue1_outer = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s1))
|
||||
.rect(57.8, 57.8).extrude(h_lap)
|
||||
.edges("|Z").fillet(3.4).edges(">Z").chamfer(1.5)).val()
|
||||
tongue1_cav = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s1 - 1.0))
|
||||
.rect(44.0, 44.0).extrude(h_lap + 2.0)).val()
|
||||
tongue1 = cq.Shape(tongue1_outer.wrapped).cut(tongue1_cav)
|
||||
p01_a = p01_a_base.fuse(tongue1)
|
||||
|
||||
z_bolt1 = z_s1 + h_lap / 2.0 # 180.0
|
||||
for xx in xx_bolts:
|
||||
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
|
||||
# the trailing "▶" glyph renders pointing world-up (+Z).
|
||||
front_label_plane_a = cq.Plane(
|
||||
origin=(X_COL, Y_COL - 33.0, 120.0),
|
||||
xDir=(0, 0, 1), normal=(0, -1, 0),
|
||||
)
|
||||
label_a = (cq.Workplane(front_label_plane_a)
|
||||
.text("P01-A TOP ▶", 9.0, -0.8)).val()
|
||||
p01_a = p01_a.cut(cq.Shape(label_a.wrapped))
|
||||
|
||||
# --- P01_B (中段) ---
|
||||
cut_below_s1 = bx(X_COL - 50.0, X_COL + 50.0, Y_COL - 50.0, Y_COL + 50.0, z_bot - 14.0, z_s1)
|
||||
cut_above_s2 = bx(X_COL - 50.0, X_COL + 50.0, Y_COL - 50.0, Y_COL + 50.0, z_s2, z_top + 10.0)
|
||||
p01_b_base = p01.cut(cut_below_s1).cut(cut_above_s2)
|
||||
pocket1 = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s1 - 0.1))
|
||||
.rect(58.8, 58.8).extrude(h_lap + 0.6).edges("|Z").fillet(3.9)).val()
|
||||
p01_b_base = p01_b_base.cut(pocket1)
|
||||
|
||||
tongue2_outer = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s2))
|
||||
.rect(57.8, 57.8).extrude(h_lap)
|
||||
.edges("|Z").fillet(3.4).edges(">Z").chamfer(1.5)).val()
|
||||
tongue2_cav = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s2 - 1.0))
|
||||
.rect(44.0, 44.0).extrude(h_lap + 2.0)).val()
|
||||
tongue2 = cq.Shape(tongue2_outer.wrapped).cut(tongue2_cav)
|
||||
p01_b = p01_b_base.fuse(tongue2)
|
||||
|
||||
z_bolt2 = z_s2 + h_lap / 2.0 # 329.0
|
||||
for xx in xx_bolts:
|
||||
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),
|
||||
xDir=(0, 0, 1), normal=(0, -1, 0),
|
||||
)
|
||||
label_b = (cq.Workplane(front_label_plane_b)
|
||||
.text("P01-B TOP ▶", 9.0, -0.8)).val()
|
||||
p01_b = p01_b.cut(cq.Shape(label_b.wrapped))
|
||||
|
||||
# --- P01_C (上段) ---
|
||||
cut_below_s2 = bx(X_COL - 50.0, X_COL + 50.0, Y_COL - 50.0, Y_COL + 50.0, z_bot - 14.0, z_s2)
|
||||
p01_c_base = p01.cut(cut_below_s2)
|
||||
pocket2 = (cq.Workplane("XY", origin=(X_COL, Y_COL, z_s2 - 0.1))
|
||||
.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 = _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),
|
||||
xDir=(0, 0, 1), normal=(0, -1, 0),
|
||||
)
|
||||
label_c = (cq.Workplane(front_label_plane_c)
|
||||
.text("P01-C TOP ▶", 9.0, -0.8)).val()
|
||||
p01_c = p01_c.cut(cq.Shape(label_c.wrapped))
|
||||
|
||||
return p01_a, p01_b, p01_c
|
||||
|
||||
|
||||
JOINT = 'root'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
p01_a, p01_b, p01_c = split_p01_poka_yoke(p_chest())
|
||||
|
||||
return [
|
||||
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "立柱下段", lambda: p01_a, 0x0284c7,
|
||||
"200mm适配分段A:高166mm+16mm阶梯公榫+顶端 2x Ø3.9 对锁孔(M4x16 自攻)+底部50x50x12榫头+P01-A 竖排TOP刻字"),
|
||||
("P01_B_立柱中段_三角形30%_墙4底5_竖打", "立柱中段", lambda: p01_b, 0x2563eb,
|
||||
"200mm适配分段B:高165mm+双端公母榫+两端各 2x Ø3.9 对锁孔(M4x16 自攻,非对称孔位防反装)+P01-B 竖排TOP刻字"),
|
||||
("P01_C_立柱上段_三角形30%_墙4底5_竖打", "立柱上段", lambda: p01_c, 0x1d4ed8,
|
||||
"200mm适配分段C:高149mm+底端母榫+底端 2x Ø3.9 对锁孔(非对称孔位防反装)+顶面P03安装台(4x Ø3.9 安装孔 + 1x Ø3.9 横向锁紧孔)+P01-C 竖排TOP刻字"),
|
||||
]
|
||||
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P02 底座三段(中核心 / 前向力臂 / 后向力臂):后置防倾翻机架与防呆母榫。"""
|
||||
import cadquery as cq
|
||||
from armcore import cyl
|
||||
from parts.p01_column import X_COL, Y_COL
|
||||
|
||||
|
||||
def make_p02_poka_yoke():
|
||||
"""将 P02 拆分为三件模块化组件(均严格 <= 195x190mm,适配 200x200mm 打印床):
|
||||
1. P02_Center (底座中核心): 195x190x70mm,完整容纳立柱套筒与前后三角斜撑筋,核心应力区无接缝;
|
||||
2. P02_Front (前向力臂): 145x190x22mm,宽跨距 104mm 双榫头 + 预埋 M5 螺母槽;
|
||||
3. P02_Rear (后向力臂): 45x190x22mm,窄跨距 80mm 双榫头(跨距差达 24mm 物理防呆,绝不混装)。
|
||||
"""
|
||||
z0 = 0.0
|
||||
z1 = 22.0
|
||||
y_width = 190.0
|
||||
|
||||
f_tenon_w = 44.0
|
||||
f_tenon_y_span = 52.0
|
||||
r_tenon_w = 32.0
|
||||
r_tenon_y_span = 40.0
|
||||
|
||||
# 1. P02_Center (X = -195.0 .. 0.0, 195x190mm)
|
||||
x_c_min = -195.0
|
||||
x_c_max = 0.0
|
||||
x_c_len = x_c_max - x_c_min
|
||||
x_c_mid = (x_c_min + x_c_max) / 2.0
|
||||
|
||||
c_plate = (cq.Workplane("XY", origin=(x_c_mid, Y_COL, z0))
|
||||
.rect(x_c_len, y_width).extrude(z1)
|
||||
.edges("|Z and <X").fillet(8.0).edges("|Z and >X").fillet(8.0)
|
||||
.edges(">Z").fillet(2.0)).val()
|
||||
p02_c = cq.Shape(c_plate.wrapped)
|
||||
|
||||
# Socket collar (Z: 22..70mm)
|
||||
z_collar_top = 70.0
|
||||
collar_outer = (cq.Workplane("XY", origin=(X_COL, Y_COL, z1))
|
||||
.rect(80.0, 80.0).extrude(z_collar_top - z1)
|
||||
.edges("|Z").fillet(8.0).edges(">Z").chamfer(2.0)).val()
|
||||
p02_c = p02_c.fuse(cq.Shape(collar_outer.wrapped))
|
||||
|
||||
# Front & Rear triangular gusset ribs
|
||||
f_pts = [(-75.0, z1), (-15.0, z1), (-15.0, z1 + 4.0), (-75.0, 68.0)]
|
||||
f_rib = (cq.Workplane("XZ", origin=(0, Y_COL, 0)).polyline(f_pts).close().extrude(19.0, both=True)).val()
|
||||
p02_c = p02_c.fuse(cq.Shape(f_rib.wrapped))
|
||||
|
||||
r_pts = [(-155.0, z1), (-195.0, z1), (-195.0, z1 + 14.0), (-155.0, 70.0)]
|
||||
r_rib = (cq.Workplane("XZ", origin=(0, Y_COL, 0)).polyline(r_pts).close().extrude(19.0, both=True)).val()
|
||||
p02_c = p02_c.fuse(cq.Shape(r_rib.wrapped))
|
||||
|
||||
# Front wire guide
|
||||
wg_pts = [(-75.5, z1 + 1.0), (-14.0, z1 + 1.0), (-14.0, z1 + 4.0), (-75.5, 68.5)]
|
||||
wire_guide = (cq.Workplane("XZ", origin=(0, Y_COL, 0)).polyline(wg_pts).close().extrude(9.0, both=True)).val()
|
||||
p02_c = p02_c.cut(wire_guide)
|
||||
|
||||
# Column socket — stepped mortise: upper 66.6 bore for the 66 box walls,
|
||||
# lower 50.6 recess (floor 0.6 low) for the 50x50 bottom tenon; the recess
|
||||
# rim at z=22 is the axial seat, so column weight bears on face, not threads.
|
||||
socket_upper = (cq.Workplane("XY", origin=(X_COL, Y_COL, z1))
|
||||
.rect(66.6, 66.6).extrude(z_collar_top - z1 + 1.0)
|
||||
.edges("|Z").fillet(6.3)).val()
|
||||
p02_c = p02_c.cut(socket_upper)
|
||||
socket_lower = (cq.Workplane("XY", origin=(X_COL, Y_COL, 9.4))
|
||||
.rect(50.6, 50.6).extrude(z1 - 9.4)
|
||||
.edges("|Z").fillet(4.3)).val()
|
||||
p02_c = p02_c.cut(socket_lower)
|
||||
|
||||
# Vertical M5 clamp bolts (offsets match the asymmetric column tenon holes:
|
||||
# X −20/+16 blocks 180° flip, the ±18 Y span blocks 90° rotation).
|
||||
# Threads engage the tenon 15mm — clamp only, axial load rides on the seat.
|
||||
for xx in (X_COL - 20.0, X_COL + 16.0):
|
||||
for yy in (Y_COL - 18.0, Y_COL + 18.0):
|
||||
p02_c = p02_c.cut(cyl(2.75, -0.01, 10.5, xx, yy)).cut(cyl(5.0, -0.01, 6.0, xx, yy))
|
||||
|
||||
# Center clamp slots
|
||||
for cy in (Y_COL - 75.0, Y_COL + 75.0):
|
||||
plane = cq.Plane(origin=(x_c_mid, cy, -0.01), xDir=(0, 1, 0), normal=(0, 0, 1))
|
||||
slot = cq.Workplane(plane).slot2D(28.0, 7.0).extrude(z1 + 1.0).val()
|
||||
p02_c = p02_c.cut(slot)
|
||||
|
||||
p02_c = p02_c.cut((cq.Workplane("XY", origin=(X_COL, Y_COL - 70.0, -0.01)).rect(28.0, 80.0).extrude(6.0)).val())
|
||||
|
||||
# Front mortise pockets
|
||||
for sign in (-1, 1):
|
||||
yy = Y_COL + sign * f_tenon_y_span
|
||||
front_mortise = (cq.Workplane("XY", origin=(-10.0, yy, 4.8))
|
||||
.rect(21.0, f_tenon_w + 0.8).extrude(12.6).edges("|Z and <X").fillet(3.0)).val()
|
||||
p02_c = p02_c.cut(front_mortise)
|
||||
p02_c = p02_c.cut(cyl(2.75, -0.01, z1 + 1.0, -10.0, yy))
|
||||
p02_c = p02_c.cut(cyl(5.0, z1 - 5.0, z1 + 1.0, -10.0, yy))
|
||||
|
||||
# Rear mortise pockets
|
||||
for sign in (-1, 1):
|
||||
yy = Y_COL + sign * r_tenon_y_span
|
||||
rear_mortise = (cq.Workplane("XY", origin=(-185.0, yy, 4.8))
|
||||
.rect(21.0, r_tenon_w + 0.8).extrude(12.6).edges("|Z and >X").fillet(3.0)).val()
|
||||
p02_c = p02_c.cut(rear_mortise)
|
||||
p02_c = p02_c.cut(cyl(2.75, -0.01, z1 + 1.0, -185.0, yy))
|
||||
p02_c = p02_c.cut(cyl(5.0, z1 - 5.0, z1 + 1.0, -185.0, yy))
|
||||
|
||||
# Debossed text on P02_Center top face. The mid-plate line (y=Y_COL) is
|
||||
# hidden behind the column collar and the gusset ribs, so both labels sit on
|
||||
# the flat -Y side strip (y = Y_COL-68) where they stay in full view from the
|
||||
# label side: REAR on the rear half, FRONT on the front half, clear of the
|
||||
# clamp slot (x -101..-94) and the M5 screw counterbores.
|
||||
txt_c_f = (cq.Workplane("XY", origin=(-49.0, Y_COL - 68.0, z1)).text("▲ FRONT (前向) ▲", 9.0, -0.8)).val()
|
||||
txt_c_r = (cq.Workplane("XY", origin=(-146.0, Y_COL - 68.0, z1)).text("▲ REAR (后向) ▲", 9.0, -0.8)).val()
|
||||
p02_c = p02_c.cut(cq.Shape(txt_c_f.wrapped)).cut(cq.Shape(txt_c_r.wrapped))
|
||||
|
||||
# 2. P02_Front (X = 0.0 .. 145.0, 145x190mm)
|
||||
x_f_len = 145.0
|
||||
x_f_mid = 72.5
|
||||
f_plate = (cq.Workplane("XY", origin=(x_f_mid, Y_COL, z0))
|
||||
.rect(x_f_len, y_width).extrude(z1)
|
||||
.edges("|Z and >X").fillet(16.0).edges(">Z").fillet(2.0)).val()
|
||||
p02_f = cq.Shape(f_plate.wrapped)
|
||||
|
||||
for cy in (Y_COL - 70.0, Y_COL + 70.0):
|
||||
plane = cq.Plane(origin=(120.0, cy, -0.01), xDir=(0, 1, 0), normal=(0, 0, 1))
|
||||
slot = cq.Workplane(plane).slot2D(28.0, 7.0).extrude(z1 + 1.0).val()
|
||||
p02_f = p02_f.cut(slot)
|
||||
|
||||
for sign in (-1, 1):
|
||||
yy = Y_COL + sign * f_tenon_y_span
|
||||
tongue = (cq.Workplane("XY", origin=(-9.5, yy, 5.25))
|
||||
.rect(20.0, f_tenon_w - 0.2).extrude(11.5).edges("|Z and <X").fillet(2.5).edges("<X").chamfer(1.5)).val()
|
||||
p02_f = p02_f.fuse(cq.Shape(tongue.wrapped))
|
||||
p02_f = p02_f.cut(cyl(2.75, -0.01, z1 + 1.0, -10.0, yy))
|
||||
nut_pocket = (cq.Workplane("XY", origin=(-10.0, yy, -0.01)).polygon(6, 9.2).extrude(5.0)).val()
|
||||
p02_f = p02_f.cut(nut_pocket)
|
||||
|
||||
# Top-face engraving rotated 90 deg to run across the plate along +Y (user
|
||||
# request): letter tops face -X, so it reads upright from the front (+X).
|
||||
# "▲" points local +Y = world -X, i.e. toward the centre joint.
|
||||
txt_f = (cq.Workplane(cq.Plane(origin=(x_f_mid, Y_COL, z1), xDir=(0, 1, 0),
|
||||
normal=(0, 0, 1)))
|
||||
.text("P02-FRONT ▲ JOIN", 8.0, -0.8)).val()
|
||||
p02_f = p02_f.cut(cq.Shape(txt_f.wrapped))
|
||||
|
||||
# 3. P02_Rear (X = -240.0 .. -195.0, 45x190mm)
|
||||
x_r_len = 45.0
|
||||
x_r_mid = -217.5
|
||||
r_plate = (cq.Workplane("XY", origin=(x_r_mid, Y_COL, z0))
|
||||
.rect(x_r_len, y_width).extrude(z1)
|
||||
.edges("|Z and <X").fillet(16.0).edges(">Z").fillet(2.0)).val()
|
||||
p02_r = cq.Shape(r_plate.wrapped)
|
||||
|
||||
for cy in (Y_COL - 70.0, Y_COL + 70.0):
|
||||
plane = cq.Plane(origin=(-215.0, cy, -0.01), xDir=(0, 1, 0), normal=(0, 0, 1))
|
||||
slot = cq.Workplane(plane).slot2D(28.0, 7.0).extrude(z1 + 1.0).val()
|
||||
p02_r = p02_r.cut(slot)
|
||||
|
||||
for sign in (-1, 1):
|
||||
yy = Y_COL + sign * r_tenon_y_span
|
||||
tongue = (cq.Workplane("XY", origin=(-185.5, yy, 5.25))
|
||||
.rect(20.0, r_tenon_w - 0.2).extrude(11.5).edges("|Z and >X").fillet(2.5).edges(">X").chamfer(1.5)).val()
|
||||
p02_r = p02_r.fuse(cq.Shape(tongue.wrapped))
|
||||
p02_r = p02_r.cut(cyl(2.75, -0.01, z1 + 1.0, -185.0, yy))
|
||||
nut_pocket = (cq.Workplane("XY", origin=(-185.0, yy, -0.01)).polygon(6, 9.2).extrude(5.0)).val()
|
||||
p02_r = p02_r.cut(nut_pocket)
|
||||
|
||||
# Same +Y rotation as the front plate; "▼" points local -Y = world +X,
|
||||
# i.e. toward the centre joint. Along +Y the ~73 mm text fits the 190 mm
|
||||
# plate width instead of overhanging its 45 mm X span.
|
||||
txt_r = (cq.Workplane(cq.Plane(origin=(x_r_mid, Y_COL, z1), xDir=(0, 1, 0),
|
||||
normal=(0, 0, 1)))
|
||||
.text("P02-REAR ▼ JOIN", 7.5, -0.8)).val()
|
||||
p02_r = p02_r.cut(cq.Shape(txt_r.wrapped))
|
||||
|
||||
return p02_c, p02_f, p02_r
|
||||
|
||||
|
||||
def p_base_foot():
|
||||
"""P02_底脚 (重型防倾翻超长力臂底座 + 双向大截面三角加强斜撑筋):
|
||||
1. 底盘尺寸大幅扩展为 385x240x22mm (X: -240.0..+145.0, Y: 8.0..248.0);
|
||||
- 前向延伸至 X = +145.0mm (在手臂垂直投影前方 91.5mm),彻底杜绝手臂前伸与抓握重物时的前倾翻;
|
||||
- 后向延伸至 X = -240.0mm (在立柱后方 92.0mm),抵御大动态回程后仰力矩;
|
||||
2. 立柱插接套筒由 12mm 浅槽大幅升级为 48mm 高加固套筒 (Z: 22.0..70.0mm,外壁厚 7mm);
|
||||
3. 前向大三角加强斜撑筋 (Z: 68.0->22.0mm, X: -75.0->-15.0mm, 宽 38mm),抗弯力臂长达 60mm,留足安全间隙不干涉手臂运动;
|
||||
4. 后向大三角加强斜撑筋 (Z: 70.0->22.0mm, X: -155.0->-225.0mm, 宽 38mm),抗后仰力臂长达 70mm;
|
||||
5. 前斜撑表面设 18mm 宽平滑走线导槽,与 P01 下部走线窗口自然贯通;
|
||||
6. 6 组 M6 工业工作台紧固腰槽 (28x7mm) + 底部隐蔽走线槽;
|
||||
7. 四周 R=18mm 大圆角过渡与 2.5mm 上边缘倒角。
|
||||
"""
|
||||
z0 = 0.0
|
||||
z1 = 22.0
|
||||
x_rear = -240.0
|
||||
x_front = 145.0
|
||||
x_len = x_front - x_rear # 385.0
|
||||
x_mid = (x_rear + x_front) / 2.0 # -47.5
|
||||
y_width = 240.0
|
||||
|
||||
# 1. 主底盘 (385 x 240 x 22 mm)
|
||||
base_plate = (cq.Workplane("XY", origin=(x_mid, Y_COL, z0))
|
||||
.rect(x_len, y_width)
|
||||
.extrude(z1)
|
||||
.edges("|Z").fillet(18.0)
|
||||
.edges(">Z").fillet(2.5)).val()
|
||||
s = cq.Shape(base_plate.wrapped)
|
||||
|
||||
# 2. 立柱外包加固套筒 (Z: 22.0..70.0 mm, 外廓 80x80mm)
|
||||
z_collar_top = 70.0
|
||||
collar_outer = (cq.Workplane("XY", origin=(X_COL, Y_COL, z1))
|
||||
.rect(80.0, 80.0)
|
||||
.extrude(z_collar_top - z1)
|
||||
.edges("|Z").fillet(8.0)
|
||||
.edges(">Z").chamfer(2.0)).val()
|
||||
s = s.fuse(cq.Shape(collar_outer.wrapped))
|
||||
|
||||
# 3. 前向抗倾翻三角加强斜撑筋 (Front Triangular Gusset Rib)
|
||||
f_pts = [
|
||||
(-75.0, z1),
|
||||
(-15.0, z1),
|
||||
(-15.0, z1 + 4.0),
|
||||
(-75.0, 68.0),
|
||||
]
|
||||
f_rib = (cq.Workplane("XZ", origin=(0, Y_COL, 0))
|
||||
.polyline(f_pts).close()
|
||||
.extrude(19.0, both=True)).val()
|
||||
s = s.fuse(cq.Shape(f_rib.wrapped))
|
||||
|
||||
# 4. 后向抗倾翻三角加强斜撑筋 (Rear Triangular Gusset Rib)
|
||||
r_pts = [
|
||||
(-155.0, z1),
|
||||
(-225.0, z1),
|
||||
(-225.0, z1 + 4.0),
|
||||
(-155.0, 70.0),
|
||||
]
|
||||
r_rib = (cq.Workplane("XZ", origin=(0, Y_COL, 0))
|
||||
.polyline(r_pts).close()
|
||||
.extrude(19.0, both=True)).val()
|
||||
s = s.fuse(cq.Shape(r_rib.wrapped))
|
||||
|
||||
# 5. 前斜撑表面走线导槽 (与 P01 前走线窗 Z=70..140mm 自然顺接)
|
||||
wire_guide_pts = [
|
||||
(-75.5, z1 + 1.0),
|
||||
(-14.0, z1 + 1.0),
|
||||
(-14.0, z1 + 4.0),
|
||||
(-75.5, 68.5),
|
||||
]
|
||||
wire_guide = (cq.Workplane("XZ", origin=(0, Y_COL, 0))
|
||||
.polyline(wire_guide_pts).close()
|
||||
.extrude(9.0, both=True)).val()
|
||||
s = s.cut(wire_guide)
|
||||
|
||||
# 6. 立柱深插槽内腔 (Pocket: 66.6 x 66.6 mm, 留 0.3mm 精密滑配间隙)
|
||||
socket_box = (cq.Workplane("XY", origin=(X_COL, Y_COL, 10.0))
|
||||
.rect(66.6, 66.6)
|
||||
.extrude(z_collar_top - 10.0 + 1.0)
|
||||
.edges("|Z").fillet(6.3)).val()
|
||||
s = s.cut(socket_box)
|
||||
|
||||
# 7. 4x M5 底部垂直紧固螺栓孔与沉孔
|
||||
for xx in (X_COL - 21.0, X_COL + 21.0):
|
||||
for yy in (Y_COL - 21.0, Y_COL + 21.0):
|
||||
th = cyl(2.75, -0.01, 10.5, xx, yy)
|
||||
cb = cyl(5.0, -0.01, 6.0, xx, yy)
|
||||
s = s.cut(th).cut(cb)
|
||||
|
||||
# 8. 6 组 M6 工业工作台紧固腰槽 (28 x 7 mm)
|
||||
slot_positions = [
|
||||
(-215.0, Y_COL - 85.0), (-215.0, Y_COL + 85.0), # 后部两角
|
||||
(-47.5, Y_COL - 95.0), (-47.5, Y_COL + 95.0), # 中部两侧
|
||||
(120.0, Y_COL - 85.0), (120.0, Y_COL + 85.0), # 前部两角
|
||||
]
|
||||
for cx, cy in slot_positions:
|
||||
plane = cq.Plane(origin=(cx, cy, -0.01), xDir=(0, 1, 0), normal=(0, 0, 1))
|
||||
slot = cq.Workplane(plane).slot2D(28.0, 7.0).extrude(z1 + 1.0).val()
|
||||
s = s.cut(slot)
|
||||
|
||||
# 9. 底部隐蔽走线通槽
|
||||
wire_ch = (cq.Workplane("XY", origin=(X_COL, Y_COL - 80.0, -0.01))
|
||||
.rect(28.0, 100.0)
|
||||
.extrude(6.0)).val()
|
||||
s = s.cut(wire_ch)
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'root'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
p02_c, p02_f, p02_r = make_p02_poka_yoke()
|
||||
|
||||
return [
|
||||
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "底座核心承力座", lambda: p02_c, 0x475569,
|
||||
"200mm适配中座:195x190mm核心承力座+48mm套筒+12mm阶梯榫座(轴向面承载)+前后三角加强筋+前后非对称防呆母榫槽+刻字"),
|
||||
("P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打", "前向防翻力臂", lambda: p02_f, 0xe11d48,
|
||||
"200mm适配前臂:145x190mm扩展力臂+104mm宽跨距双公榫+底部M5预埋螺母沉槽+P02-FRONT刻字"),
|
||||
("P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打", "后向防翻力臂", lambda: p02_r, 0xd97706,
|
||||
"200mm适配后臂:45x190mm扩展力臂+80mm窄跨距双公榫+底部M5预埋螺母沉槽+P02-REAR刻字"),
|
||||
]
|
||||
@@ -0,0 +1,417 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P03 肩吊臂:板式桁架 + 侧开式电机托架 + M1 后置法兰。"""
|
||||
import cadquery as cq
|
||||
import math
|
||||
from armcore import cyl
|
||||
from arm_build import Yplate, bx
|
||||
from human_layout import X_BAY_EXT, Y_AXIS, Z_CHEST1, Z_R1, Z_RL
|
||||
from part_tools import Xcyl, make_surface_tie_bridge
|
||||
from parts.p01_column import X_COL, Y_COL, Z_LOCK
|
||||
|
||||
|
||||
def p_girdle(j1):
|
||||
"""P03_肩关节安装悬臂 (轻量化板式桁架版 + 侧开式电机托架):
|
||||
1. 10mm 后端重载法兰盘 (X: -61.0..-51.0, 74x74mm);
|
||||
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 螺丝刀通道直接走腔体,拧紧更顺手。
|
||||
11. 三处外凸扎带拱桥(10x8mm、站出 3.2mm、桥下 3.2x1.8 穿带隧道):①桁架竖板面 Y=95、
|
||||
②后置法兰面 Y=91、③Ø73 舱体 −Y 母线 Y=91.5 —— 安装面按各处真实表面取;原来统一按
|
||||
Y=95 放,②③ 被筒壁吞成露出 0mm、且压在 34x20 散热舷窗的贯通孔上,等于没有固定点。
|
||||
"""
|
||||
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)
|
||||
# 背面那圈 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)).val()
|
||||
s = cq.Shape(flange.wrapped)
|
||||
|
||||
# 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, rib_line_z(X_COL - 10.0)),
|
||||
(X_COL - 33.0, rib_line_z(X_COL - 33.0)),
|
||||
]
|
||||
rib_pts = [
|
||||
(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_CREST),
|
||||
(X_BACK - 10.0, Z_CREST), # 脊线 (-71,643):正好落在顶筋前面与 2mm 倒角下沿的交点
|
||||
(X_COL - 15.0, 575.0),
|
||||
(X_COL - 30.0, 530.0),
|
||||
]
|
||||
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))
|
||||
|
||||
# 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),
|
||||
]
|
||||
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))
|
||||
|
||||
# 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(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))
|
||||
|
||||
# 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(TENON, TENON)
|
||||
.extrude(15.0)
|
||||
.edges("|Z").fillet(4.2)).val()
|
||||
s = s.fuse(cq.Shape(boss.wrapped))
|
||||
|
||||
# 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))
|
||||
|
||||
# 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))
|
||||
|
||||
# 【线束过线孔】榫头/鞍座中央 22x22mm 竖向贯通方孔 (直通 P01 立柱 44x44mm 内腔,容纳 20x20mm 接插件)
|
||||
saddle_wire = (cq.Workplane("XY", origin=(X_COL, Y_COL, Z_CHEST1 - 16.0))
|
||||
.rect(22.0, 22.0)
|
||||
.extrude(T_SADDLE + 17.0)
|
||||
.edges("|Z").fillet(3.0)).val()
|
||||
s = s.cut(cq.Shape(saddle_wire.wrapped))
|
||||
|
||||
# 4x M3 连接螺孔(螺丝刀通道直接走两竖板之间的开放腔体,无需再钻 Ø8.5 直通隧道)
|
||||
# 4x M3 连接螺孔(螺丝刀通道直接走两竖板之间的开放腔体,无需再钻 Ø8.5 直通隧道)
|
||||
# 孔位 = 电机尾端 4x M3 螺纹径向槽实测位(电机.STEP):BCD55。
|
||||
# 【2026-10-07 整改】M1 电机沿 +X 轴旋转 5.35° 后,4x M3 后端孔完全规整对齐 45°/135°/225°/315° 正四角
|
||||
# (dy, dz = ±19.45, ±19.45),水平与竖直完全齐平规整,且与旋转后的 M1 尾端 4xM3 槽完全同心
|
||||
for dy, dz in ((19.45, 19.45), (-19.45, 19.45), (-19.45, -19.45), (19.45, -19.45)):
|
||||
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)
|
||||
s = s.cut(th).cut(cb)
|
||||
|
||||
# 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
|
||||
|
||||
cradle_outer = (cq.Workplane("YZ", origin=(X_MOUNT - 0.1, Y_ARM, Z_ARM))
|
||||
.circle(R_POD)
|
||||
.extrude(X_FRONT - X_MOUNT + 0.1)
|
||||
.edges(">X").chamfer(1.5)).val()
|
||||
s = s.fuse(cq.Shape(cradle_outer.wrapped))
|
||||
|
||||
# 电机定子配合内腔(最终状态,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)
|
||||
|
||||
# 4x 前置 M3 锁紧螺钉凸台(BCD=55mm,跟随 M1 电机旋转 5.35° -> 22.82°/112.82°/202.82°/292.82°):
|
||||
# 平直舱孔的沉头座位由 4 个 Ø10 局部凸台承载——凸台位于舱孔前段 8.06..11.5,
|
||||
# 电机前端面(5.0)之前 3mm,电机斜插路径不经过;M3x8 圆柱头坐在凸台沉孔
|
||||
# 底(x=9.5)旋入电机前端面,咬合 3.5mm,与旋转后的 M1 前端盖实测 4xM3 螺孔精确同心
|
||||
for deg in (22.82, 112.82, 202.82, 292.82):
|
||||
rad = math.radians(deg)
|
||||
fy = Y_ARM + 27.5 * math.cos(rad)
|
||||
fz = Z_ARM + 27.5 * math.sin(rad)
|
||||
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(电机尾端面)单向锁死(前置止脱法兰随平直舱孔要求取消);
|
||||
# 径向约束:Ø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):
|
||||
win_plane = cq.Plane(
|
||||
origin=(-24.5, Y_ARM + sign * 35.0, Z_ARM),
|
||||
xDir=(1, 0, 0),
|
||||
normal=(0, sign, 0)
|
||||
)
|
||||
win_cut = cq.Workplane(win_plane).slot2D(34.0, 20.0).extrude(16.0, both=True).val()
|
||||
s = s.cut(win_cut)
|
||||
|
||||
for deg in (45, 135):
|
||||
rad = math.radians(deg)
|
||||
slot_plane = cq.Plane(
|
||||
origin=(-24.5, Y_ARM + 33.0 * math.cos(rad), Z_ARM + 33.0 * math.sin(rad)),
|
||||
xDir=(1, 0, 0),
|
||||
normal=(0, math.cos(rad), math.sin(rad))
|
||||
)
|
||||
slot_cut = cq.Workplane(slot_plane).slot2D(30.0, 6.5).extrude(15.0, both=True).val()
|
||||
s = s.cut(slot_cut)
|
||||
|
||||
# 8. 实体表面外凸式扎带拱桥 (10x8mm 拱形穿带座,一体成型,扎带从桥下 3.2x1.8 隧道穿过):
|
||||
# 安装面一律取"该处的真实表面"——原来三处全按竖板面 Y=95 放,而 ②③ 落在 Ø73 舱体上,
|
||||
# 舱面母线只到 Y=91.5,桥体 3.2mm 被筒壁整段吞掉(露出 0.0mm、穿带孔被壁填死)= 没有固定点。
|
||||
# 组①:桁架竖板平面段 X=-85, Z=570, 贴 Y=95 平面,竖直穿带 —— 与 P04 三处同配方,不动
|
||||
# 组②:后置法兰 −Y 平面 X=-56, Z=610, 贴 Y=91 (法兰 74 宽 → 面在 128-37)
|
||||
# 选法兰面而不是舱面:法兰是 10mm 整实料;且线束过了 X=-61 必须绕开 Y=91 这道 4mm 台阶,
|
||||
# 贴法兰面后法兰带内线皮净空 2.78mm,走舱面径向则线要外拱、只剩 0.33mm。
|
||||
# X 向留 1.0mm 不越法兰前后脸,Z 向 606..614 在 R8 圆角之间 (581..639) 全平面。
|
||||
# 组③:舱体 −Y 母线实料段 X=+5.5, Z=610, 贴 Y=91.5 切点,动静接缝前锚固点
|
||||
# 离舷窗前缘 (最外到 X=-8.3) 10.3mm、离前端 1.5 倒角 (X=10 起) 1.0mm;
|
||||
# 穿带孔由沿 X 改成沿 Z —— 此处线束沿 X 走,孔轴必须垂直于线才抱得住。
|
||||
Y_FACE_FLANGE = Y_ARM - 37.0 # 91.0 法兰 −Y 承面
|
||||
Y_TAN = Y_ARM - R_POD # 91.5 舱体最外的 −Y 母线(切点)
|
||||
for tx, ys, tz, wx in ((-85.0, Y_ARM - Y_P_OUT, 570.0, 10.0),
|
||||
(-56.0, Y_FACE_FLANGE, 610.0, 8.0),
|
||||
(5.5, Y_TAN, 610.0, 7.0)):
|
||||
s = s.fuse(make_surface_tie_bridge(tx, ys, tz, w_x=wx, l_z=8.0, h_y=3.2,
|
||||
slot_w=3.2, slot_h=1.8, tunnel_dir='Z'))
|
||||
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'root'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j1 = JS["J1"]
|
||||
return [
|
||||
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
|
||||
"轻量化板式桁架版:主干由 66mm 实心块改为双侧 6mm 桁架竖板+帐篷减重窗(斜面≥52°自支撑)+R4内角,外轮廓与旧版并集完全一致,实心 728->286cm³;底部 9mm 承力鞍座(Ø3.9 安装孔)+12mm 中位龙骨筋(自鞍座斜拉至法兰下缘)+40~45°外扩斜撑;保留 10mm 后置法兰、侧开式电机托架、Ø60.7 直通电机舱(平孔无台阶)(M1 从+Y滑入,后端 4xM3x16 锁死)、大散热窗与对流槽;【2026-10-07 电机旋转整改】M1 电机旋转 5.35°:后置法兰 4xM3 连接孔完全规整对齐 45°/135°/225°/315° 正四角(dy,dz=±19.45mm),前端 4xM3 锁紧凸台同步旋转至 22.82°+90k,消除原有倾斜,与 M1 前后端盖螺孔精确同心;无封闭空腔、免支撑"),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P05 大臂旋转座:J2 俯仰 -> J3 滚转(复用 part_tools 的 pitch->roll 骨架)。"""
|
||||
import cadquery as cq
|
||||
from human_layout import Z_R3, Z_SH
|
||||
from part_tools import make_pitch_to_roll_bracket
|
||||
|
||||
|
||||
def p_shoulder_roll_base(j2, j3, half=None):
|
||||
"""P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版)
|
||||
|
||||
本件在链路上有两重身份,两处让位都是为前端压盖:
|
||||
1. hub_gap=6.0(从动侧):J2 处 P04 新增了 M2 前端压盖(前端夹紧电机,消除大扭矩下的
|
||||
倾斜),压盖占 Y 155.5..161.0,所以从动法兰盘背面必须从 Y=157 后移到 Y=162
|
||||
(留 1.0mm 动间隙)。
|
||||
2. roll_clamp=True(前端夹紧侧):P05 自己抱着 M3(J3,-Z 轴),原来也只有后端板
|
||||
4xM3x10 单面锁死;现在按 motor_front_clamp 的统一配方在 M3 前端盖外端面
|
||||
(Z=389)加一枚 R35 压盖(Z 384..389,4xM3x8 BCD55),与后端板前后双向面压,
|
||||
倾斜自由度归零。代价是对侧从动件 P06 的 J3 毂面整体后移 CLAMP_BACKOFF=5.5
|
||||
(Z 388.5→383.0),见 p_humerus_bone。
|
||||
P08 保持默认 —— J5 处 P07 的前端压盖已做(只需 6.0 的 hub_gap),而 J6 的 M6
|
||||
前端压盖尚未做,故 roll_clamp 传 False。
|
||||
"""
|
||||
s = make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3,
|
||||
name="P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
hub_gap=6.0, roll_clamp=True)
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'J2'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j2 = JS["J2"]
|
||||
j3 = JS["J3"]
|
||||
return [
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
|
||||
"V3圆角+ J2前端夹紧配合:Loft外轮廓R=10mm,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂);【2026-10-07 侧壁放样斜棱圆角】侧壁与有机放样交汇处斜向硬折棱倒R3圆角,消除侧立面与正面放样弧面接缝硬棱,表面光影与触感圆润流线;【2026-10-07】L型内转角R1.5精细圆角(将圆角半径精调至1.5mm,完全杜绝算法异常与外凸台阶,平滑过渡并消除直角应力集中,与螺钉沉孔/运动间隙保持充裕余量);【2026-10-07 电机座内凹槽与边缘突起消除】彻底消除承压面内侧 4.5mm 环形内凹槽(全腔单一 BAY_R 贯通无接缝),同时 cut_open 敞口高度上限延伸至承压面 (z_r + REAR_SEAT),侧壁完全平直对齐下方壁面,彻底消除侧沿悬垂突耳;J2 从动法兰盘背面由 Y=157 后移至 Y=162(hub_gap=6.0,给 P04 新增的 M2 前端压盖让位,J2 直锁凸台同步加长到 Y=165.5,3xS01 输出螺钉咬合量不变);保证 J2 全角度 0 干涉;【2026-10-05d 只切敞口那一圈围挡】用户指出 M3 电机前端(另一头)也有一圈围挡:Z389.0~395.5 的 r30.25~35 前端定位环(6mm 高环墙) + 骑在它上面的 R35 压盖外圈(Ø70 比电机 Ø63.6 凸 3.2mm),把 M3 前端盖连同穿过 Ø37 孔的 P06 J3 凸台围在中间。第一版把整个前端(定位环+压盖+腔口下沿 389->395)都切了属过度(电机前端失去支撑面、M3 退回单面锁死),已回退;现只切敞口(-Y)那一圈(盒切,Y 界限与 cut_open 一致),-Y 半圈从 Z=395.5 一路开到压盖背面 Z=389,背脊(+Y)侧整圈保留作前端盖定位与压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带);压盖本体/Ø37 孔/4xM3x8 沉孔/腔口下沿 389.0 全部不动,P05 只少 3.2cm3(154.8->151.6),仍单实体。不整圈削到 R31.8 的原因:那圈壁与 Loft 在 X=±35 处与 Ø70 柱面相切,整圈切完会脱空出 1.32mm3 碎块破坏单实体;且压盖根部熔接面会从 2.37mm2 缩到 0.78mm2,4xM3x8 夹紧力会把压盖拉下来。开关 FENCE_OPEN_CUT(全机 5 处电机前端定位环敞口侧统一切除,无围挡阻碍装配)。【2026-10-03】M3 前端压盖(R35, Z384.0..389.0):J3 roll 电机原来也只有后端板 4xM3x10 单面锁死(S02),大扭矩下绕 J3 轴在 Ø60.3 座孔里点头;现按 P04/P06/P07 同一配方在 M3 前端盖外端面(Z=389)加 4xM3x8(BCD55)沉孔压盖,与后端板前后双向面压,倾斜自由度归零(配方唯一差别是 roll 电机轴向 -Z,压盖朝 -Z 长,见 motor_front_clamp axis=\"Z\");代价是对侧 P06 的 J3 毂面整体后移 5.5mm 让位;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
]
|
||||
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P06 大臂骨架:J3 滚转 -> J4 俯仰的 Loft 空心梁。"""
|
||||
import arm_build as AB
|
||||
import cadquery as cq
|
||||
from armcore import cyl
|
||||
from arm_build import COVER_Z, Ycyl, bx
|
||||
from human_layout import X_ARM, Y_AXIS, Y_COVER, Y_REAR, Z_EL, Z_R3
|
||||
from part_tools import (
|
||||
CLAMP_BACKOFF, CLAMP_CLR, CLAMP_T, cut_motor_fence, hub_m2_holes,
|
||||
make_surface_tie_bridge_neg_x, motor_front_clamp,
|
||||
)
|
||||
|
||||
|
||||
P06_WIN_XZ = [(3.0, 362.0), (37.0, 362.0), (37.0, 371.0),
|
||||
(28.0, 374.0), (12.0, 374.0), (3.0, 371.0)] # 大臂过渡块
|
||||
|
||||
|
||||
P06_HOLLOW_WIN = False # 2026-10-03 起关闭:拱形窗正面看是贯通黑洞,不雅观(详见 p_upper_bone 5c)
|
||||
|
||||
|
||||
def p_humerus_bone(j3, j4):
|
||||
"""P06_大臂骨架_螺旋体25%_墙4底5_竖打 (Humerus Bone - Loft 圆弧转方有机过渡版):
|
||||
受 J3 (大臂自转, -Z轴, hub@Z_HUB3=380) 驱动,
|
||||
向下延伸构成大臂主体骨架,底端抱紧 M4 (肘部俯仰电机, +Y轴, 轴线位于 Z_EL=332.0)。
|
||||
1. 借鉴 P05 大臂旋转座几何放样算法:
|
||||
- 顶端结合面 (Z = 383.0mm):与 J3 轴径严格相切的 Ø70mm 正圆形轮廓,彻底消除原先外突的方形水平阶梯;
|
||||
- 底端结合面 (Z = 367.0mm):平滑展开融入 M4 电机套筒的 70x63.5mm 方形截面;
|
||||
- 过渡区 (Z: 367.0..383.0mm):采用 Hermite s(t) = 3t² - 2t³ 驱动 20 层放样切片连续渐变;
|
||||
2. 下部抱紧套筒与安装板 (R=35mm),电机定子 100% 裸露散热开窗;
|
||||
3.【2026-10-03】J3 毂面整体后移 CLAMP_BACKOFF=5.5(顶端 388.5→383.0,直锁凸台
|
||||
59.5→65.0):P05 新增 M3 前端压盖,占 Z 384..389,原先 388.5 的毂面只剩 0.5mm
|
||||
让位放不下压盖。配方与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源:
|
||||
压盖 5.0 + 动间隙 1.0 - 原基准 0.5 = 5.5。凸台同步加长,3xS01 输出螺钉的头面
|
||||
台阶(局部 s=58)、杆长、咬合量与改前完全一致,J3 直锁与零干涉不受影响。
|
||||
压盖中央 Ø37 让位孔(R18.5)也让开 R17.8 凸台,单边余 0.7mm。
|
||||
4.【2026-10-09】−X 侧线束走廊加一座外凸扎带拱桥 (y=102.5, Z=345):站出 3.2mm、桥下
|
||||
3.2×1.8 穿带隧道沿 Y 贯通(与 P03/P04 同配方)。走廊线心随之由 x=-17.5 提到 -20.7:
|
||||
原线皮离本件 −X 面只剩 0.2mm,扎带没有容身处。
|
||||
5. 严格单实体 (solids = 1) 且与上下级旋转构件及电机实现动态 0 碰撞干涉。
|
||||
"""
|
||||
# 1. M4 底部抱紧套筒与后端安装板 (R=35mm)
|
||||
rear_m4 = Ycyl(35.0, Y_REAR - 8.0, Y_REAR, X_ARM, Z_EL)
|
||||
front_m4 = Ycyl(35.0, Y_COVER - 6.0, Y_COVER - 0.5, X_ARM, Z_EL)
|
||||
s = rear_m4.fuse(front_m4)
|
||||
|
||||
# 顶端结合面与过渡高度
|
||||
# z_top 由 M3 前端盖外端面反推:压盖占 [Z_COVER-CLAMP_T, Z_COVER],再留 CLAMP_CLR
|
||||
# 动间隙 -> 383.0(改前 388.5,与前端盖只有 0.5mm,放不下压盖)。
|
||||
z_cover3 = Z_R3 - COVER_Z # 389.0 M3 前端盖外端面
|
||||
z_top = z_cover3 - CLAMP_T - CLAMP_CLR # 383.0 J3 毂面(= 388.5 - CLAMP_BACKOFF)
|
||||
z_bot = Z_EL + 35.0 # 367.0
|
||||
|
||||
# 2. 下部实心基座块 (Z: Z_EL..z_bot + 0.5)
|
||||
lower_block = bx(X_ARM - 35.0, X_ARM + 35.0, Y_REAR - 8.0, Y_COVER - 0.5, Z_EL, z_bot + 0.5)
|
||||
s = s.fuse(lower_block)
|
||||
|
||||
# 3. 借鉴 P05 的 Loft 放样过渡体:圆弧变方形(20 层平滑放样,保持严格对称 4 段式截面拓扑)
|
||||
N = 20
|
||||
wp = cq.Workplane("XY")
|
||||
for i in range(N + 1):
|
||||
t = i / float(N)
|
||||
z_curr = z_bot + t * (z_top - z_bot)
|
||||
st = 3.0 * (t**2) - 2.0 * (t**3)
|
||||
|
||||
# 前侧:t=0 为平直面 (Y=155.5, v=27.5),t=1 渐变为与 Ø70 正切的圆弧顶点 (Y=163.0, v=35.0)
|
||||
v_sf = 27.5 * (1.0 - st) + 0.05 * st
|
||||
v_cf = max(27.5 * (1.0 - st) + 35.0 * st, v_sf + 0.05)
|
||||
|
||||
# 后侧:从 t=0 的平直面 (Y=92.0, v=-36.0) 渐变到 t=1 的圆弧 (Y=93.0, v=-35.0)
|
||||
v_sr = -36.0 * (1.0 - st) - 0.05 * st
|
||||
v_cr = min(-36.0 * (1.0 - st) - 35.0 * st, v_sr - 0.05)
|
||||
|
||||
w = (cq.Workplane("XY", origin=(X_ARM, Y_AXIS, z_curr))
|
||||
.moveTo(-35.0, v_sf)
|
||||
.threePointArc((0.0, v_cf), (35.0, v_sf))
|
||||
.lineTo(35.0, v_sr)
|
||||
.threePointArc((0.0, v_cr), (-35.0, v_sr))
|
||||
.close())
|
||||
wp = wp.add(w)
|
||||
|
||||
loft = wp.toPending().loft()
|
||||
s = s.fuse(loft.val())
|
||||
|
||||
# 4. 电机中间定子散热裸露切除 (100% 裸露)
|
||||
# 【2026-10-05f】原 Ycyl(30.5, ...) 定子散热切除已删:主腔统一 BAY_R=30.50
|
||||
# 且从承压面挖到前端,这刀完全落在主腔内,属同段腔被两个半径各挖一次的残留。
|
||||
# 通风由敞口盒切实现,不依赖它。
|
||||
|
||||
# 5. M4 接口成型
|
||||
# 【2026-10-03i 接缝整改】front_collar_pocket 已删,理由同 p_shoulder_roll_base:
|
||||
# 它与 Joint.pocket 切的是同一根 BAY_R 圆柱,是后者的子集,两次同半径布尔
|
||||
# 相减会在腔壁上留下 s=50.0 的零宽棱(本件实测 R30.500 裂成 5168.4 + 493.9 两张面)。
|
||||
s = j4.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
s = j4.pocket(s)
|
||||
# 【2026-10-07 修复:仅切除敞口侧承压面环墙,保护侧壁杜绝缺口漏螺丝】
|
||||
# 仿照 P05 设计,只在敞口半侧(局部 y>=0)将承压面段(z: -REAR_SEAT..0)切平,
|
||||
# 彻底消除阻碍电机装配的凸起围挡;臂身侧壁(局部 y<0)绝对不动,杜绝切穿侧壁留下缺口!
|
||||
s = s.cut(j4.L(bx(-40.0, 40.0, 0.0, 40.0, -AB.REAR_SEAT, 0.0)))
|
||||
|
||||
# 5b. 【2026-10-02 新增】M4 前端压盖 4xM3x8(BCD55):M4 原来只被后端板单面锁死,
|
||||
# 大扭矩下会绕 J4 轴在 Ø60.3 座孔里点头。现在前端也面压夹一次(见 motor_front_clamp)。
|
||||
# 熔接放在所有 R30.15~30.5 圆柱刀具之后,理由同 p_fork 7b。P07 的 J4 从动侧
|
||||
# 已相应后移到 Y>=162(见 p_forearm_bone 的 J4_CLEAR),与压盖前端面留 1.0mm。
|
||||
s = s.fuse(motor_front_clamp(X_ARM, Z_EL, tag="P06/M4"))
|
||||
|
||||
# 5b2.【2026-10-05e / 2026-10-07 修复】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
s = cut_motor_fence(s, j4, "P06/M4", "P06")
|
||||
|
||||
# 5c. 【2026-10-02 新增】电机上方过渡块减重窗(见 hollow_tent_window)。
|
||||
# Z360..385 这段 Loft 过渡块壁厚最深 15.8mm,是 P06 唯一一块可掏的厚料
|
||||
# (Z332..367 的下部本体已经被 Ø61 定子孔掏成 4.5mm 薄壁,没得再掏)。
|
||||
# 但它正压在 J3 毂下面,所以三条红线:
|
||||
# 1) 窗顶最高只到 Z374 —— J3 的 Ø70 毂面在 Z380..388.5,窗顶留 6mm 整块
|
||||
# 实心托住毂面承压,绝不碰毂面和它后面的 3xS01 直锁螺钉;
|
||||
# 2) 窗的 X 范围 3..37 —— 截面本体 X-15..55,两侧各留 18mm 竖墙,
|
||||
# 该高度上零件才不会被切成两半(必须单实体);
|
||||
# 3) 拱形(下宽上窄)轮廓,斜面与水平 >= 52° 自支撑,只有顶部 X12..28 的
|
||||
# 16mm 平顶需要桥接(与 P03 同量级)。
|
||||
# 窗底取 Z362 而不是更低:Z360 附近是全段安全系数最低的截面,
|
||||
# 抬高窗底 4mm 比横向收窄 8mm 更划算(减重量只少 3cm3,安全系数从 92% 回到 100%)。
|
||||
# 窗沿 Y 双向切穿(Y87..172 超出本体,前后都贯通),顺带把这段的定子散热口
|
||||
# 一起打开。
|
||||
# 【2026-10-03 关闭】用户实机视点确认这个拱形窗在正面看就是一个贯通的矩形黑洞,
|
||||
# 位于J4 关节正上方、整机最显眼的落脚点,视觉上像"零件破了个洞",非常不雅观。
|
||||
# 权衡后恢复实心:代价只有 161->183cm3(+22cm3 / +27g PLA),而强度本来就已经是
|
||||
# 无窗基线的 100%(这段本来就不缺强度),减重收益纯属锦上添花。
|
||||
# 6. J3 直连法兰结构(凸台延伸至 65.0 跨过结合面牢固熔接,保持单实体与零干涉)
|
||||
# 凸台随 J3 毂面一起后移 CLAMP_BACKOFF=5.5(59.5→65.0):进入电机输出毂的深度
|
||||
# 仍是 s54.942..56.0 那 1.06mm,3xS01 的头面台阶(s=58)/杆长/咬合量一律不变。
|
||||
boss = cyl(17.8, 54.942, 59.5 + CLAMP_BACKOFF)
|
||||
chamfer_cone = cyl(18.5, 54.5, 54.942)
|
||||
s = s.fuse(j3.L(boss)).cut(j3.L(chamfer_cone))
|
||||
s = s.cut(j3.L(cyl(3.5, 54.0, 58.0 + 30.0)))
|
||||
s = hub_m2_holes(s, j3, cb_z1=58.0 + 40.0)
|
||||
|
||||
# 9. −X 侧线束走廊扎带拱桥(与 P03/P04 同一配方:站出 3.2mm、桥下 3.2×1.8 穿带隧道、孔沿 Y)
|
||||
# 走廊原来贴 x=-17.5 走,离本件 −X 面 (X_ARM-35 = -15.5) 只剩 2.0mm —— 线皮净空 0.2mm,
|
||||
# 扎带没有容身处。桥站出后线心改到 x=-20.5(桥顶 -18.7 + 线半径 1.8),与 P03 舱段同一处理。
|
||||
# 落点 Z=345 在 lower_block 的 −X 平面上 (Z332..367.5 / Y92..155.5):足迹 Z341..349 离块体
|
||||
# 下缘 9.0mm、Y97.5..107.5 离 Y 边 5.5mm,实测平面度 0.00mm、背后 10.35mm 整实料(未打穿内腔)。
|
||||
s = s.fuse(make_surface_tie_bridge_neg_x(X_ARM - 35.0, 102.5, 345.0,
|
||||
w_y=10.0, l_z=8.0, h_x=3.2))
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'J3'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j3 = JS["J3"]
|
||||
j4 = JS["J4"]
|
||||
return [
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
|
||||
"Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体;【2026-10-03】取消电机上方Loft过渡块(Z360~385)的拱形减重窗,恢复实心:该窗沿Y贯通,正面看是整机最显眼处的矩形黑洞(用户视觉整改),代价仅 +22cm3/+27g,强度仍为无窗基线100%;【2026-10-03】J3 毂面整体后移 5.5mm(顶端 388.5→383.0,直锁凸台 59.5→65.0):给 P05 新增的 M3 前端压盖(Z384..389)让位,留 1.0mm 动间隙——与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源,换算式同为「压盖 5.0 + 动间隙 1.0 - 原基准 0.5」;凸台同步加长,3xS01 头面台阶(s=58)/杆长/咬合量与 J3 直锁关系一律不变;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
]
|
||||
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P07 小臂骨架:J4 -> J5 的 L 型结构 + 顶板减重槽。"""
|
||||
import arm_build as AB
|
||||
import cadquery as cq
|
||||
import human_layout as HL
|
||||
import math
|
||||
from armcore import cyl
|
||||
from arm_build import Ycyl, bx
|
||||
from human_layout import PLATE_T, X_ARM, Y_COVER, Y_HUB, Y_REAR, Z_EL, Z_WR
|
||||
from part_tools import (
|
||||
FILLET_SMALL, cut_motor_fence, hollow_channel, hub_m2_holes,
|
||||
make_surface_tie_bridge_neg_x, motor_front_clamp,
|
||||
)
|
||||
|
||||
|
||||
P07_CH = dict(x0=-3.0, x1=43.0, y0=103.0, y1=153.0, z_floor=255.0) # 小臂顶板
|
||||
|
||||
|
||||
def p_forearm_bone(j4, j5, half=None):
|
||||
"""P07_小臂骨架_螺旋体30%_墙5底5_竖打 L型高刚度无干涉版(深度圆角优化版)
|
||||
设计要点:
|
||||
1. 改为与 P05/P08 统一的 L 型结构(L-bracket):
|
||||
- +Y侧垂直背脊立柱(Y: y_hub_in..y_out),避开上级大臂骨架 P06(Y <= 155.5)
|
||||
- 彻底消除原全深S弯在J4轴下方向两边突出的干涉结构(两边突出的结构全部去除)
|
||||
- 垂直立柱与下方 M5 电机水平座之间采用规则折角(Corner Chamfer/Gusset)刚性连接
|
||||
2. 下方 M5 电机实行半包散热结构,底部与内腔开窗散热
|
||||
3. J4 轴俯仰动态行程从受限的 ±54° 大幅释放至无死角大范围摆动(零干涉)
|
||||
4. 满足单实体 (solids=1) 与装配零碰撞。
|
||||
5.【2026-10-02】M5 前端压盖(4xM3x8 BCD55)消除"电机只被后端板单面锁死"的倾斜隐患;
|
||||
J4 从动侧整体让位:y_hub_in 由 158.5 后移到 J4_CLEAR=162.0,让出 P06 的 M4 前端压盖
|
||||
(前端面 Y=161.0),留 1.0mm 动间隙。J4 直连凸台同步加长,3xS01 输出螺钉咬合量不变。
|
||||
6.【2026-10-09】−X 侧线束走廊加一座外凸扎带拱桥 (y=102.5, Z=235):站出 3.2mm、桥下
|
||||
3.2×1.8 穿带隧道沿 Y 贯通(与 P06/P03/P04 同配方)。走廊线心随之由 x=-17.5 提到 -20.7:
|
||||
原线皮离本件 −X 面只剩 0.2mm,扎带没有容身处。
|
||||
"""
|
||||
y_out = Y_HUB + PLATE_T # 174.0
|
||||
# J4 从动侧让位基准:必须 >= P06 的 M4 前端压盖前端面(Y_COVER+5.0=161.0) + 1.0mm
|
||||
J4_CLEAR = Y_COVER + 6.0 # 162.0
|
||||
y_hub_in = J4_CLEAR
|
||||
y_front = Y_COVER - 0.5 # 155.5
|
||||
y_rear = Y_REAR - 8.0 # 92.0
|
||||
z_top_housing = Z_WR + 39.0 # 261.0 (顶面加厚 4.0mm,消除过薄破面问题)
|
||||
z_roof_top = z_top_housing + 19.0 # 280.0 (顶板整条加高 19mm,用户确认方案4)
|
||||
|
||||
# 1. J5 电机前后安装环
|
||||
collar_front = Ycyl(35.0, HL.Y_COVER - 6.0, HL.Y_COVER - 0.5, X_ARM, Z_WR)
|
||||
rear_plate = Ycyl(35.0, HL.Y_REAR - 8.0, HL.Y_REAR, X_ARM, Z_WR)
|
||||
s = collar_front.fuse(rear_plate)
|
||||
|
||||
# 2. J4 顶部从动法兰盘 (Y: y_hub_in..y_out, R=35mm, Z: Z_EL)
|
||||
disc = Ycyl(35.0, y_hub_in, y_out, X_ARM, Z_EL)
|
||||
s = s.fuse(disc)
|
||||
|
||||
# 3. M5 电机壳体水平顶部顶板 (Z: Z_WR..z_top_housing, Y: y_rear..y_front)
|
||||
roof_w = (cq.Workplane("YZ", origin=(X_ARM, 0, 0))
|
||||
.moveTo(y_rear, Z_WR)
|
||||
.lineTo(y_rear, z_roof_top)
|
||||
.lineTo(y_front - 3.0, z_roof_top)
|
||||
.threePointArc((y_front - 3.0 + 3.0 * math.cos(math.pi / 4),
|
||||
z_roof_top - 3.0 + 3.0 * math.sin(math.pi / 4)),
|
||||
(y_front, z_roof_top - 3.0))
|
||||
.lineTo(y_front, Z_WR)
|
||||
.close()
|
||||
.extrude(35.0, both=True))
|
||||
s = s.fuse(roof_w.val())
|
||||
|
||||
# 4. L型垂直立柱臂 (+Y侧,Y: y_hub_in..y_out,从 Z_EL 顶端延伸至下方电机座)
|
||||
vert_leg = (cq.Workplane("YZ", origin=(X_ARM, 0, 0))
|
||||
.moveTo(y_hub_in, z_top_housing)
|
||||
.lineTo(y_out, z_top_housing)
|
||||
.lineTo(y_out, Z_EL)
|
||||
.lineTo(y_hub_in, Z_EL)
|
||||
.close()
|
||||
.extrude(35.0, both=True))
|
||||
s = s.fuse(vert_leg.val())
|
||||
|
||||
# 5. L型内侧圆弧加强过渡(Circular Arc Gusset,用户要求"改成圆弧而不是折角,消掉悬空小块")
|
||||
# 彻底取代直角折角与直斜线折角,采用与顶面和立柱双相切的平滑内凹大圆弧 (R=28.0mm)
|
||||
# 起点在顶板平整面 (y_arc_start, z_roof_top),精确相切至立柱内侧面 (y_hub_in, z_arc_end)
|
||||
# 严控全域 Z >= z_top_housing,彻底消除侵入下方旋转副的悬空小方块与多余突起!
|
||||
# 注:顶板整条加高19mm(z_roof_top=280)后,R28改R14使弧仍与顶板面光滑相切,不出现折角
|
||||
R_arc = 14.0 # 随顶板加高联动重定:相切起点(144.5,280)->终点(158.5,294),避开P06环底z=297留3mm
|
||||
y_arc_start = y_hub_in - R_arc
|
||||
z_arc_end = z_roof_top + R_arc
|
||||
p_mid_arc = (y_arc_start + R_arc * math.cos(math.pi / 4),
|
||||
z_arc_end - R_arc * math.sin(math.pi / 4))
|
||||
|
||||
gusset_w = (cq.Workplane("YZ", origin=(X_ARM, 0, 0))
|
||||
.moveTo(y_arc_start, z_roof_top)
|
||||
.threePointArc(p_mid_arc, (y_hub_in, z_arc_end))
|
||||
.lineTo(y_out, z_arc_end)
|
||||
.lineTo(y_out, z_top_housing)
|
||||
.lineTo(y_arc_start, z_top_housing)
|
||||
.close()
|
||||
.extrude(35.0, both=True))
|
||||
s = s.fuse(gusset_w.val())
|
||||
|
||||
# 6. 下级 P08 旋转圆盘动态避让切除(R=35.5mm,预留 0.5mm 间隙)
|
||||
p08_clearance = Ycyl(35.5, 156.0, y_out + 2.0, X_ARM, Z_WR)
|
||||
s = s.cut(p08_clearance)
|
||||
|
||||
# 6b. 【2026-10-02 新增】顶板根部减重槽(见 hollow_channel)。
|
||||
# 顶板在 Z255..280 一整段是壁厚最深 15mm 的实心料,而 J4 在 z=245 的安全系数
|
||||
# 高达 122、z=195 也有 24。挖成朝上敞开的 U 形槽,顶板变成槽钢:
|
||||
# 槽底 Z255 以下保留整块实心到电机座,抗弯的两片 X 向腹板(X-15..-7 / X47..55)
|
||||
# 留在弯矩拉压区。Y 上限取 156 而非核心的 166,是为了避开 y_hub_in=158.5 起的
|
||||
# 竖直立柱本体(立柱是 J4 从动法兰的承力柱,不能掏)。
|
||||
# 槽顶 Z280 就是顶板上表面,敞口向上,免支撑、非封闭腔。
|
||||
s = s.cut(hollow_channel(P07_CH["x0"], P07_CH["x1"], P07_CH["y0"], P07_CH["y1"],
|
||||
P07_CH["z_floor"], 300.0, tag="P07顶板"))
|
||||
|
||||
# 7. M5 电机安装接口与定子空腔
|
||||
# 【2026-10-05f 接缝整改】本件原先是全机唯一给腔径加 CLEARANCE_GAP(2.0) 的地方:
|
||||
# front_collar_pocket(r_bore=32.25) + 一刀 Ycyl(32.5, Y_REAR-0.5, y_front-6)。
|
||||
# 结果内壁是 35 -> 32.5 -> 30.2 -> 32.3 -> 30.2,比兄弟件多两道台阶
|
||||
# (实测 scratch/bay_step_profile.py:z=5.5 处 32.5、z=49.5 处掉到 30.2、
|
||||
# z=50.25 又回到 32.3)。
|
||||
# 这 2.0mm 是「关节间隙」,但关节间隙是**轴向**让位(见 hub_gap / CLAMP_CLR),
|
||||
# 径向把腔径撑大 2mm 只会让电机在腔里晃,且与 P05/P06/P08 不一致。
|
||||
# 现统一走 BAY_R(30.50),与兄弟件同径,接缝归零。
|
||||
# 【2026-10-03i】front_collar_pocket 随之删除:它切 cyl(BAY_R, 50.0..56.5),
|
||||
# 是 Joint.pocket 那根 cyl(BAY_R, -2.6..61.6) 的子集,同半径重复相减
|
||||
# 只会在腔壁上多出一条 s=50.0 的零宽棱。
|
||||
s = j5.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
s = j5.pocket(s)
|
||||
# 【2026-10-07 修复:仅切除敞口侧承压面环墙,消除用户圈出的围挡】
|
||||
# 仿照 P05 设计,只在敞口半侧(局部 y>=0)将承压面段(z: -REAR_SEAT..0)切平,
|
||||
# 彻底消除用户红圈指出的承压面凸起围挡;顶板背脊侧(局部 y<0)绝对不动,杜绝切穿本体!
|
||||
s = s.cut(j5.L(bx(-40.0, 40.0, 0.0, 40.0, -AB.REAR_SEAT, 0.0)))
|
||||
# 【2026-10-05f】原 Ycyl(30.5+CLEARANCE_GAP=32.5, ...) 整刀删除:
|
||||
# 它既撑大了腔径(接缝主因),又已被主腔 BAY_R 完全覆盖(纯冗余)。
|
||||
|
||||
# 半包散热设计:-Z底部电机定子通风口
|
||||
vent_cut = bx(X_ARM - 40.0, X_ARM + 40.0, y_rear + 8.0, y_front - 6.0, Z_WR - 40.0, Z_WR - 25.0)
|
||||
s = s.cut(vent_cut)
|
||||
|
||||
# 7b. 【2026-10-02 新增】M5 前端压盖 4xM3x8(BCD55):M5 原来只被后端板单面锁死,
|
||||
# 大扭矩下会绕 J5 轴在座孔里点头。现在前端也面压夹一次(见 motor_front_clamp)。
|
||||
# 必须在第 6 步 p08_clearance(R35.5 让位孔,Y>=156)之后熔接,否则压盖会被削掉。
|
||||
# P08 的 J5 从动侧已相应后移到 Y>=162(make_pitch_to_roll_bracket 的 hub_gap=6.0)。
|
||||
s = s.fuse(motor_front_clamp(X_ARM, Z_WR, tag="P07/M5"))
|
||||
|
||||
# 7c2.【2026-10-05e / 2026-10-07 修复】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
s = cut_motor_fence(s, j5, "P07/M5", "P07")
|
||||
|
||||
# 8. J4 直连法兰结构(凸台延伸到法兰盘背面之后 2.0mm 跨过间隙与法兰盘牢固熔接,
|
||||
# 保持单实体)。凸台长度跟着 y_hub_in 走:法兰盘后移时凸台同步加长,
|
||||
# 3xS01 输出螺钉的头面台阶(局部 z=58)、杆长、咬合量与改前完全一致。
|
||||
boss = cyl(17.8, 54.942, (y_hub_in - HL.Y_REAR) + 2.0)
|
||||
s = s.fuse(j4.L(boss))
|
||||
s = s.cut(j4.L(cyl(3.5, 54.0, 74.0 + 30.0)))
|
||||
s = hub_m2_holes(s, j4, cb_z1=74.0 + 30.0)
|
||||
|
||||
# ========== 关键圆角优化 ==========
|
||||
try:
|
||||
# 后侧顶板外边缘圆角(平滑消除水平顶板与后部垂直端面交汇处的 90° 尖角,用户要求倒角改圆角)
|
||||
wp_s = cq.Workplane(obj=s)
|
||||
sel_rear = cq.selectors.BoxSelector(
|
||||
(X_ARM - 40, y_rear - 1.0, z_roof_top - 1.0),
|
||||
(X_ARM + 40, y_rear + 1.0, z_roof_top + 1.0)
|
||||
)
|
||||
s = wp_s.edges(sel_rear).fillet(FILLET_SMALL).val()
|
||||
except Exception as e:
|
||||
print(f"P07 顶板后边缘圆角异常: {e}")
|
||||
|
||||
try:
|
||||
# 垂直立柱下部外缘圆角(消除立柱垂直外壁与下方悬空水平底面交汇处的 90° 尖角,用户截图红圈指定)
|
||||
wp_s = cq.Workplane(obj=s)
|
||||
sel_leg_bot = cq.selectors.BoxSelector(
|
||||
(X_ARM - 40, y_out - 1.0, z_top_housing - 1.0),
|
||||
(X_ARM + 40, y_out + 1.0, z_top_housing + 1.0)
|
||||
)
|
||||
s = wp_s.edges(sel_leg_bot).fillet(FILLET_SMALL).val()
|
||||
except Exception as e:
|
||||
print(f"P07 立柱下底外缘圆角异常: {e}")
|
||||
|
||||
# 9. −X 侧线束走廊扎带拱桥(与 P06/P03/P04 同一配方:站出 3.2mm、桥下 3.2×1.8 隧道、孔沿 Y)
|
||||
# 线心随桥由 x=-17.5 改到 -20.5(桥顶 -18.7 + 线半径 1.8);本件 −X 面离走廊同样只有
|
||||
# 2.0mm,原来线皮净空 0.2mm,扎带无从下手。
|
||||
# 落点 Z=235 在 M5 壳体顶板的 −X 平面 (x=-15.5, Z222..280) 上:足迹 Z231..239 离顶板下缘
|
||||
# 9.0mm、Y97.5..107.5 离 Y 边 5.5mm,实测平面度 0.00mm、背后 10.35mm 整实料;顶板减重槽
|
||||
# 在 X≥-3 / Z≥255,与本桥不相交。
|
||||
s = s.fuse(make_surface_tie_bridge_neg_x(X_ARM - 35.0, 102.5, 235.0,
|
||||
w_y=10.0, l_z=8.0, h_x=3.2))
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'J4'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j4 = JS["J4"]
|
||||
j5 = JS["J5"]
|
||||
return [
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "小臂骨架", lambda: p_forearm_bone(j4, j5, None), 0x7c3aed,
|
||||
"V2 L型结构 + 顶板减重槽:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,立柱底端外下缘90°直棱倒R3圆角(平滑消除立柱外壁与水平底面交汇处直角尖角);顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉;【2026-10-02】顶板根部Z255~280开朝上敞开的减重槽(X-3~43 / Y103~153, Y上限取153避开y_hub_in=158.5起的竖直立柱本体):顶板由实心变槽钢,288->230cm3(-20%),槽底以下保留整块实心到电机座,抗弯靠两侧各10mm的X向腹板;槽底水平朝上、侧壁竖直、顶部敞开故零支撑且非封闭腔;开度按「减重区最差安全系数 >= 无槽基线 95%」选定(X-3~43 比最初的 X-7~47 收窄 4mm,强度从 88% 回到 96%);【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P08 腕偏转座:J5 俯仰 -> J6 滚转(复用 part_tools 的 pitch->roll 骨架)。"""
|
||||
from human_layout import Z_R6, Z_WR
|
||||
from part_tools import make_pitch_to_roll_bracket
|
||||
|
||||
|
||||
def p_wrist_bracket(j5, j6):
|
||||
"""P08_腕偏转座_螺旋体25%_墙4底5_竖打(深度圆角优化版)
|
||||
|
||||
hub_gap=6.0:J5 处 P07 新增了 M5 前端压盖(Y 155.5..161.0),从动法兰盘背面必须
|
||||
从 Y=157 后移到 Y=162 让位;J5 直锁凸台同步加长,3xS01 输出螺钉咬合量不变。
|
||||
|
||||
roll_clamp=True【2026-10-03】:M6 补上前端压盖,与 P04/P05/P06/P07 的
|
||||
M2/M3/M4/M5 同一套配方(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔、Ø61.0 根部
|
||||
让位槽),轴向 -Z,压盖占全局 Z 120.5..115。此前本件是五处电机腔里**唯一**
|
||||
只被后端板 4xM3x10 单面锁死的,电机大扭矩下会在 Ø60.3 座孔里点头;对侧
|
||||
P09 让位 CLAMP_BACKOFF=5.5(见 make_direct_hub 的 backoff),工具端点不动。
|
||||
"""
|
||||
return make_pitch_to_roll_bracket(j5, j6, Z_WR, Z_R6,
|
||||
name="P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
hub_gap=6.0, roll_clamp=True)
|
||||
|
||||
|
||||
JOINT = 'J5'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j5 = JS["J5"]
|
||||
j6 = JS["J6"]
|
||||
return [
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "腕部转接座", lambda: p_wrist_bracket(j5, j6), 0xd97706,
|
||||
"V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
]
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P09 工具法兰:末端法兰盘与 J6 直锁 hub。"""
|
||||
import math
|
||||
from armcore import cyl
|
||||
from arm_build import D2R
|
||||
from human_layout import PLATE_T, X_ARM, Y_AXIS, Z_HUB6
|
||||
from part_tools import CHAMFER_EDGE, CLAMP_BACKOFF, FILLET_SMALL, FILLET_TINY, make_direct_hub
|
||||
|
||||
|
||||
def p_tool_flange(j6):
|
||||
"""P09_工具法兰_螺旋体30%_墙4底5_竖打 深度圆角优化版
|
||||
关键改进:
|
||||
1. 外轮廓全圆角(R=4mm)
|
||||
2. 法兰面倒角(2mm)
|
||||
3. 螺孔边缘倒角(1.5mm)
|
||||
"""
|
||||
z_bot = Z_HUB6 - PLATE_T - 9.0
|
||||
s = cyl(35.0, z_bot, Z_HUB6, X_ARM, Y_AXIS)
|
||||
P09_M4 = []
|
||||
|
||||
# 4x M4螺栓孔
|
||||
#
|
||||
# 分布圆半径 20 -> 22(BCD 40 -> 44):R20 与 3xS01 输出螺钉的 Ø5.6 螺丝刀通道
|
||||
# (make_direct_hub 里 cyl(2.8, 58.0, z_out+40) 在 BCD29=R14.5、相位 351.4/232.3/112.1°)
|
||||
# 咬穿沉孔。0° 那个孔中心 (39.5,128) 到 351.4° 通道中心 (33.84,125.83) 只有 6.06mm,
|
||||
# 而两孔半径之和是 3.5+2.8=6.30 —— 差 0.24mm,Ø7.0 沉孔壁被啃掉一块,
|
||||
# 完整圆柱面(88mm²)裂成 45.7+35.4 两片,即用户截图里的月牙孔。
|
||||
# 该相位差只有 8.6°,转动 M4 孔相位无法躲开(0/90/180/270 与 45 度错开全部仍 <6.30),
|
||||
# 只能加大分布圆半径:R22 时最小中心距 7.96mm,余量 1.66mm。
|
||||
# 边壁仍充足:R35 法兰外径 - (R22 + 沉孔 3.5) = 9.5mm。
|
||||
for a in (0, 90, 180, 270):
|
||||
rad = 22.0
|
||||
u = rad * math.cos(a * D2R)
|
||||
v = rad * math.sin(a * D2R)
|
||||
# 实际切割推迟到 make_direct_hub 之后(见下)。这里只算出 4 个孔位。
|
||||
P09_M4.append((X_ARM + u, Y_AXIS + v))
|
||||
|
||||
# 定位止口 + 走线孔
|
||||
pilot = cyl(10.0, z_bot - 1.0, z_bot + 2.5, X_ARM, Y_AXIS)
|
||||
thru = cyl(5.0, z_bot - 1.0, Z_HUB6 + 1.0, X_ARM, Y_AXIS)
|
||||
s = s.cut(pilot).cut(thru)
|
||||
|
||||
# 直连法兰
|
||||
# 【2026-10-03】M6 补上前端压盖后,J6 毂面必须让位 CLAMP_BACKOFF=5.5
|
||||
# (详见 make_direct_hub 的 backoff)。P09 的法兰本体 z_bot/Z_HUB6 一个字没动,
|
||||
# 让位的只是伸进 J6 腔里的那圈毂面 —— 圆盘下移后仍与法兰本体在局部 65..79.5
|
||||
# 大面积交叠,熔接不受影响;工具端点 Z_TIP=93 也随之不变。
|
||||
s = make_direct_hub(s, j6, r_disc=35.0, z_out=74.0, backoff=CLAMP_BACKOFF)
|
||||
|
||||
# 4x M4 孔(必须在 make_direct_hub 之后切)
|
||||
#
|
||||
# 直连法兰那步 fuse 了 R35 圆盘(局部 z=56.5..74 -> 全局 z=102..121 一侧),
|
||||
# 会把先切的孔重新填死:分段求交显示 z=97..102 无干涉、z=102 以上整根杆都埋在料里
|
||||
# (每段 12.57 mm^3 = pi*2^2*1,全截面被埋)。
|
||||
#
|
||||
# 孔径与深度同时修正:旧值 Ø3.7 比 M4 杆 Ø4.0 还细,且只到 z=105,
|
||||
# 而 M4x16 头面在 z=97、杆要走到 z=113 —— 杆尾 8mm 无处可去(干涉 147.3 mm^3)。
|
||||
# Ø4.4 是 M4 标准间隙孔(ISO 273 medium ≈ Ø4.3)。
|
||||
# 孔顶要盖过 M4x16 的杆尾:头面 z=97 + 杆长 16 = z=113,故开到 Z_HUB6+4=115,
|
||||
# 比杆尾多留 2mm(实测开到 112 时最后 1mm 杆尾仍撞料 37.7mm³)。
|
||||
for (fx, fy) in P09_M4:
|
||||
s = s.cut(cyl(2.2, z_bot - 1.0, Z_HUB6 + 4.0, fx, fy))
|
||||
s = s.cut(cyl(3.5, z_bot - 1.0, z_bot + 4.0, fx, fy))
|
||||
|
||||
# ========== 关键圆角优化 ==========
|
||||
try:
|
||||
# 外轮廓圆角
|
||||
s = s.edges("|Z").fillet(FILLET_SMALL)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# 法兰面倒角
|
||||
s = s.edges("<Z").chamfer(CHAMFER_EDGE)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
# 顶部边缘圆角
|
||||
s = s.edges(">Z").fillet(FILLET_TINY)
|
||||
except:
|
||||
pass
|
||||
|
||||
return s
|
||||
|
||||
|
||||
JOINT = 'J6'
|
||||
|
||||
|
||||
def specs(JS):
|
||||
"""本件的 (件号, 中文名, 构建 thunk, 配色, 说明) 清单;顺序即 BOM 顺序。"""
|
||||
j6 = JS["J6"]
|
||||
return [
|
||||
("P09_工具法兰_螺旋体30%_墙4底5_竖打", "末端工具法兰", lambda: p_tool_flange(j6), 0xdc2626,
|
||||
"V2圆角:外轮廓R=3mm,法兰面倒角2mm;【2026-10-02】打印填充由100%降到30%:该截面安全系数288是全机裕度最大的件,满填充纯属浪费(件号同步改为P09_工具法兰_螺旋体30%)"),
|
||||
]
|
||||
@@ -31,7 +31,7 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量
|
||||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||||
S12 电机前端压盖 4xM3x8:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) / J5(P07 夹 M5)
|
||||
/ J6(P08 夹 M6) 各 4 颗。
|
||||
配方唯一事实源 = arm_model.motor_front_clamp(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔
|
||||
配方唯一事实源 = part_tools.motor_front_clamp(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔
|
||||
深 3.0、沉孔台阶 = 前端盖外端面 +2.0mm、压盖背面 0.0 面压);J3 是同一套配方的 Z 轴版本
|
||||
(台阶落在 Z=(z_r-56)-2=387),半径/相位直接取 arm_model 的 CLAMP_BCD/CLAMP_CLOCK。
|
||||
孔位必须显式算全局 (x,z)/(x,y) 而不能用 to_global,原因见 instances() 内的注释。
|
||||
@@ -58,9 +58,9 @@ _ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import human_layout as HL # noqa: E402
|
||||
from arm_model import CLAMP_BCD, CLAMP_CLOCK # noqa: E402 压盖孔位唯一事实源
|
||||
from part_tools import CLAMP_BCD, CLAMP_CLOCK # noqa: E402 压盖孔位唯一事实源
|
||||
|
||||
X_COL = -115.0 # 立柱中心(与 arm_model.py 一致)
|
||||
X_COL = -115.0 # 立柱中心(与 parts/p01_column.py 的 X_COL 一致)
|
||||
Y_COL = HL.Y_AXIS # 128.0
|
||||
GRP_PAR = {"J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"}
|
||||
DRIVEN = {"J1": "P04 肩叉", "J2": "P05 大臂旋转座", "J3": "P06 大臂骨架",
|
||||
|
||||
@@ -28,6 +28,13 @@ if not EDGE:
|
||||
VIEWS = {
|
||||
"01_iso": "?cam=iso",
|
||||
"zoom_user": "?cam=iso&az=-0.9&po=1.15&rd=470&tx=30&ty=320&tz=-80&xray=0",
|
||||
"user_harness_view": "?az=-1.85&po=1.32&rd=430&tx=0&ty=330&tz=-115&xray=0",
|
||||
"user_harness_zoom": "?az=-1.85&po=1.32&rd=280&tx=-5&ty=360&tz=-115&xray=0",
|
||||
"user_m1_rear": "?az=-1.25&po=1.35&rd=350&tx=-70&ty=570&tz=-128&xray=0",
|
||||
"user_m1_exact": "?az=-1.5708&po=1.5708&rd=260&tx=0&ty=610&tz=-128&xray=0",
|
||||
"user_m1_bypass_oblique": "?az=-1.0&po=1.15&rd=300&tx=-40&ty=580&tz=-140&xray=0",
|
||||
"user_smooth_harness": "?az=1.50&po=1.45&rd=340&tx=-15&ty=560&tz=-128&xray=0",
|
||||
"user_contour_harness": "?az=1.5708&po=1.50&rd=360&tx=-15&ty=320&tz=-128&xray=0",
|
||||
"02_front": "?cam=front",
|
||||
"03_side": "?cam=side",
|
||||
"04_top": "?cam=top",
|
||||
@@ -118,6 +125,15 @@ VIEWS = {
|
||||
"harness_p04_p05_xray": "?az=0.8&po=1.35&rd=280&tx=25&ty=470&tz=-128&alpha=0.35&xray=1",
|
||||
"harness_p06_window": "?az=-0.65&po=1.25&rd=240&tx=25&ty=355&tz=-128&xray=0",
|
||||
"harness_xray_full": "?cam=iso&xray=1",
|
||||
# 扎带拱桥核验:沿 ±Y 正视肩部,确认三个拱桥各自露出多少、线束是否压在桥上
|
||||
"tie_probe_negY": "?az=1.5708&po=1.5708&rd=460&tx=-30&ty=590&tz=-128&xray=0",
|
||||
"tie_probe_posY": "?az=-1.5708&po=1.5708&rd=460&tx=-30&ty=590&tz=-128&xray=0",
|
||||
"tie_after_negY": "?az=1.5708&po=1.5708&rd=300&tx=-25&ty=600&tz=-128&xray=0",
|
||||
"tie_after_oblique": "?az=2.05&po=1.30&rd=330&tx=-25&ty=600&tz=-128&xray=0",
|
||||
"tie_after_close": "?az=1.90&po=1.50&rd=180&tx=-25&ty=605&tz=-100&xray=0",
|
||||
# P06/P07 −X 侧线束走廊:从 −X、−Y 斜看(纯 −X 会被蓝色立柱挡住)
|
||||
"tie_after_p0607": "?az=2.35&po=1.5708&rd=300&tx=0&ty=290&tz=-102.5&xray=0",
|
||||
"tie_after_p0607_obl": "?az=2.60&po=1.35&rd=260&tx=-5&ty=290&tz=-110&xray=0",
|
||||
}
|
||||
os.makedirs(SHOTS, exist_ok=True)
|
||||
want = sys.argv[1:] or list(VIEWS)
|
||||
|
||||
@@ -30,19 +30,18 @@ def catmull_rom_sample(pts, num_samples_per_seg=25):
|
||||
|
||||
def load_solids():
|
||||
clfs = {}
|
||||
for prefix in ['P01_C', 'P03', 'P04', 'P05', 'P06', 'P07', 'P08']:
|
||||
for f in os.listdir('out/parts_step'):
|
||||
if f.startswith(prefix):
|
||||
print(f"Loading {prefix} ({f})...", flush=True)
|
||||
shape = cq.importers.importStep(os.path.join('out/parts_step', f)).val()
|
||||
clfs[prefix] = BRepClass3d_SolidClassifier(shape.wrapped)
|
||||
break
|
||||
for f in os.listdir('out/parts_step'):
|
||||
if f.endswith('.step'):
|
||||
tag = f.split('.')[0]
|
||||
print(f"Loading {tag}...", flush=True)
|
||||
shape = cq.importers.importStep(os.path.join('out/parts_step', f)).val()
|
||||
clfs[tag] = BRepClass3d_SolidClassifier(shape.wrapped)
|
||||
return clfs
|
||||
|
||||
def main():
|
||||
clfs = load_solids()
|
||||
print("=" * 80)
|
||||
print("【线束三维物理干涉检测】全通道与 22x22mm 接口零碰撞验证")
|
||||
print("【线束三维物理干涉检测】全通道、外侧走线与电机全固态零碰撞验证")
|
||||
print("=" * 80)
|
||||
|
||||
with open('src/viewer_tpl.html', 'r', encoding='utf-8') as f:
|
||||
@@ -67,42 +66,42 @@ def main():
|
||||
checked_count += len(sampled)
|
||||
if hits:
|
||||
total_hits += len(hits)
|
||||
print(f" ❌ FAIL: {name} (发现 {len(hits)} 处实体穿透! 首击穿部件: {hits[0][0]} 坐标: {hits[0][1]})")
|
||||
print(f" ❌ FAIL: {name} (发现 {len(hits)} 处实体穿透! 首击穿部件: {hits[0][0]} 坐标: {hits[0][1]})", flush=True)
|
||||
else:
|
||||
print(f" ✓ PASS: {name} ({len(sampled)} 采样点零干涉)")
|
||||
print(f" ✓ PASS: {name} ({len(sampled)} 采样点零干涉)", flush=True)
|
||||
|
||||
# Test dynamic bridges at zero pose
|
||||
bridge_specs = [
|
||||
('Bridge 1 (J1 轴向扭转段)', [[-15.0, 128.0, 610.0], [0.0, 128.0, 610.0], [15.0, 128.0, 610.0]], ['P03', 'P04']),
|
||||
('Bridge 2 (J2 俯仰跨越段)', [[28.0, 115.0, 480.0], [26.5, 115.0, 467.5], [25.0, 115.0, 455.0]], ['P04', 'P05']),
|
||||
('Bridge 3 (J3 旋转过渡段)', [[25.0, 90.0, 390.0], [25.0, 90.0, 381.0], [25.0, 90.0, 372.0]], ['P05', 'P06']),
|
||||
('Bridge 4 (J4 俯仰跨越段)', [[25.0, 115.0, 294.0], [22.0, 140.0, 294.0], [19.5, 160.0, 296.5]], ['P06', 'P07']),
|
||||
('Bridge 5 (J5 俯仰跨越段)', [[25.0, 115.0, 190.0], [25.0, 115.0, 187.5], [25.0, 115.0, 185.0]], ['P07', 'P08']),
|
||||
('Bridge 1 (J1 Ω-Loop 柔性缓冲段)', [[5.5, 86.1, 610.0], [9.13, 79.4, 611.9], [12.75, 76.78, 613.5], [16.38, 79.85, 614.4], [20.0, 87.0, 615.0]]),
|
||||
('Bridge 2 (J2 U-Loop 柔性缓冲段)', [[-17.5, 102.5, 485.0], [-25.0, 102.5, 475.0], [-30.0, 102.5, 465.0], [-25.0, 102.5, 455.0], [-17.5, 102.5, 445.0]]),
|
||||
('Bridge 3 (J3 螺旋柔性过渡段)', [[-17.5, 102.5, 400.0], [-24.0, 96.0, 390.0], [-27.0, 93.0, 380.0], [-23.0, 96.0, 370.0], [-17.5, 102.5, 360.0]]),
|
||||
('Bridge 4 (J4 肘部U-Loop柔性缓冲段)', [[-17.5, 102.5, 305.0], [-24.0, 102.5, 290.0], [-30.0, 102.5, 280.0], [-24.0, 102.5, 270.0], [-17.5, 102.5, 255.0]]),
|
||||
('Bridge 5 (J5 腕部U-Loop柔性缓冲段)', [[-17.5, 102.5, 205.0], [-22.0, 102.5, 195.0], [-26.0, 102.5, 190.0], [-22.0, 102.5, 182.0], [-17.5, 102.5, 175.0]]),
|
||||
]
|
||||
|
||||
for name, pts, parts in bridge_specs:
|
||||
for name, pts in bridge_specs:
|
||||
sampled = catmull_rom_sample(pts, num_samples_per_seg=25)
|
||||
hits = []
|
||||
for p in sampled:
|
||||
pt = gp_Pnt(float(p[0]), float(p[1]), float(p[2]))
|
||||
for part in parts:
|
||||
clf = clfs.get(part)
|
||||
if not clf: continue
|
||||
for part, clf in clfs.items():
|
||||
clf.Perform(pt, 1e-3)
|
||||
if clf.State() == TopAbs_IN:
|
||||
hits.append((part, p))
|
||||
checked_count += len(sampled)
|
||||
if hits:
|
||||
total_hits += len(hits)
|
||||
print(f" ❌ FAIL: {name} (发现 {len(hits)} 处实体穿透!)")
|
||||
print(f" ❌ FAIL: {name} (发现 {len(hits)} 处实体穿透!)", flush=True)
|
||||
else:
|
||||
print(f" ✓ PASS: {name} ({len(sampled)} 采样点零干涉)")
|
||||
print(f" ✓ PASS: {name} ({len(sampled)} 采样点零干涉)", flush=True)
|
||||
|
||||
print("=" * 80)
|
||||
print(f"【最终结论】共评估 {checked_count} 个空间采样点,固体穿透总数: {total_hits}")
|
||||
print("=" * 80, flush=True)
|
||||
print(f"【最终结论】共评估 {checked_count} 个空间采样点,固体穿透总数: {total_hits}", flush=True)
|
||||
if total_hits == 0:
|
||||
print("🎉 恭喜!线束系统已实现 100% 结构腔体内规范走线与零碰撞干涉!")
|
||||
print("=" * 80)
|
||||
print("🎉 恭喜!线束系统已实现 100% 结构腔体内规范走线与零碰撞干涉!", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
import os
|
||||
os._exit(0 if total_hits == 0 else 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -388,22 +388,45 @@ function make(cfg) {
|
||||
const side = dir > 0 ? 'hi' : 'lo';
|
||||
const hard = HARD[id] && HARD[id][side];
|
||||
const hi0 = dir > 0 ? LIM[id].hi : LIM[id].lo;
|
||||
if (hard) return hi0; // 硬边:配置无关,静态值即真值
|
||||
let hi = sign * (LIM[id].cont ? 360 : Math.min(MOTOR || 180, 180)); // 连续轴(J1/J3/J6)无机械限位:放到 ±360°,不受 MOTOR 封顶
|
||||
/* 连续轴的搜索端点取 ±180:转过的角度以 360 为周期,360° 与 0° 是同一个位姿,
|
||||
拿它当端点探一次必然"安全",二分会把区间内真实存在的撞点整段跳过(实测 J3 在
|
||||
J4=-17.875 的折肘位形下首撞 148°,而旧口径直接返回 360)。 */
|
||||
let hi = sign * (LIM[id].cont ? 180 : Math.min(MOTOR || 180, 180));
|
||||
/* 相邻打印件对的 1D 自由区间(adj 表,精确 CAD 实测、与姿态无关)封顶:
|
||||
典型漏判场景——J4=-100 时 J5+ 被 P07×P08 卡在 96°,而该对落在静态扫描的
|
||||
视野外(零位时立柱先把 J5 卡在 34°),动态胶囊模型也看不见相邻对。 */
|
||||
const adj = ADJ[id];
|
||||
if (adj && dir > 0) hi = Math.min(hi, adj[1]);
|
||||
if (adj && dir < 0) hi = Math.max(hi, adj[0]);
|
||||
/* 硬边侧(静态限位由相邻打印件对决定、与姿态无关,胶囊模型看不见那一对):
|
||||
静态值只当区间上限,区间内仍要按当前姿态二分——否则折肘位形下动态检查被整个
|
||||
跳过,滑块放到静态 -93°,而实测 J5 在 J3=-180° 时 -28° 末端法兰已撞立柱下段。 */
|
||||
if (hard && Math.abs(hi0) < Math.abs(hi)) hi = hi0;
|
||||
if (hi === q[k]) return q[k];
|
||||
const at = v => { const qq = q.slice(); qq[k] = v; return collideFree(qq); };
|
||||
if (!at(q[k])) return q[k]; // 当前姿已干涉:不放大
|
||||
if (at(hi)) return hi;
|
||||
let lo = q[k];
|
||||
while (Math.abs(hi - lo) > tol) {
|
||||
const mid = 0.5 * (lo + hi);
|
||||
if (at(mid)) lo = mid; else hi = mid;
|
||||
if (!LIM[id].cont) { // 限位轴:折过去就是折过去,区间内单峰,二分即可
|
||||
if (at(hi)) return hi;
|
||||
while (Math.abs(hi - lo) > tol) {
|
||||
const mid = 0.5 * (lo + hi);
|
||||
if (at(mid)) lo = mid; else hi = mid;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
/* 连续轴(J1/J3/J6):整周内可行集不连续——末段绕过立柱后会重新变安全,
|
||||
"端点安全"根本不代表中间没撞(实测 J1=-81 位形向 + 方向 180° 端点安全,
|
||||
但 -26° 起撞立柱中段)。所以先按固定步长粗扫出首个干涉角,再在该段内二分。
|
||||
步长 4° 起(区间 360° 时约 40 次 collideFree ≈ 150 ms,只在滑块缓存未命中时发生)。 */
|
||||
const step = (hi > q[k] ? 1 : -1) * Math.max(4, Math.abs(hi - q[k]) / 40);
|
||||
let jam = null;
|
||||
for (let v = lo + step; (hi > q[k] ? v < hi : v > hi); v += step) {
|
||||
if (at(v)) lo = v; else { jam = v; break; }
|
||||
}
|
||||
if (jam === null) { if (at(hi)) return hi; jam = hi; }
|
||||
while (Math.abs(jam - lo) > tol) {
|
||||
const mid = 0.5 * (lo + jam);
|
||||
if (at(mid)) lo = mid; else jam = mid;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
@@ -1908,6 +1931,277 @@ function make(cfg) {
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- 6a5. 路径安全路由:端点安全 ≠ 过渡安全 ----------
|
||||
查看器的动画是逐轴线性插值(ik-glue.js animTo,连续轴走短弧),所以"移动路径"
|
||||
就是关节空间里 from→to 的一条直线段。实测把目标球从臂的一侧横拖到另一侧:
|
||||
1300 帧里 870 帧(66.9%)处于穿模状态,主要是 立柱上段×J5 电机模组(189 帧)、
|
||||
立柱中段×J6 电机模组(186 帧)——端点解本身也撞,而 live 拖拽走的 solveTo
|
||||
一个碰撞判定都没做。这里把"这条直线能不能走"当一等判据:
|
||||
1) 候选表按代价升序,取第一个"端点安全 + 直线过渡安全"的解;
|
||||
2) 一个都没有 → 沿最优候选方向二分退回最近的可安全抵达位形(跟手但停在障碍前),标 blocked;
|
||||
3) 当前位形本身已干涉(滑块被拖越界)→ 退回旧的"只看端点"口径,至少把臂解出来。
|
||||
开销:首个候选通常一次就过(1 次端点 + PATH_N-1 次过渡 ≈ 8 次 collideFree);
|
||||
最坏 CAND_MAX 个候选全查 ≈ 60 次,实测在 Worker 里跑,不冻结主线程。 */
|
||||
const PATH_N = 9, CAND_MAX = 24, SHORT_DEG = 6, FINE_DEG = 3;
|
||||
/* 关节空间步长(连续轴按短弧折算后的最大单轴变化量) */
|
||||
function stepDeg(from, to) {
|
||||
let m = 0;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const d = LIM[ORDER[i]] && LIM[ORDER[i]].cont ? wrap180(to[i] - from[i]) : to[i] - from[i];
|
||||
if (Math.abs(d) > m) m = Math.abs(d);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
function transitPt(from, to, k) {
|
||||
return from.map((v, i) => {
|
||||
const d = LIM[ORDER[i]] && LIM[ORDER[i]].cont ? wrap180(to[i] - v) : to[i] - v;
|
||||
return v + d * k;
|
||||
});
|
||||
}
|
||||
/* 直线段过渡是否全程无干涉(不含端点:起点由调用侧保证,终点已单独查过)。
|
||||
步长 ≤SHORT_DEG° 的跟随帧只查中点:实测拖拽中相邻解的步长多在几度以内,
|
||||
9 点全扫会把 live 从 1~44ms 放大到 441ms/步(≈10×),而这么短的线段
|
||||
两端安全 + 中点安全已经覆盖了实际风险(穿模体是厘米级,不是度级)。 */
|
||||
function pathFree(from, to, n) {
|
||||
n = n || PATH_N;
|
||||
for (let i = 1; i < n; i++) if (!collideFree(transitPt(from, to, i / n))) return false;
|
||||
return true;
|
||||
}
|
||||
function findSafeTarget(cur, from) {
|
||||
if (cur.q && collideFree(cur.q)) return { q: cur.q, res: cur };
|
||||
let best = null, bestCost = Infinity;
|
||||
const n = Math.min((cur.list || []).length, 120);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = cur.list[i];
|
||||
if (s && s.q && collideFree(s.q)) {
|
||||
const cst = costOf(s.q, null, from);
|
||||
if (cst < bestCost) {
|
||||
bestCost = cst;
|
||||
best = { q: s.q, res: Object.assign({}, cur, { q: s.q.slice(), cost: s.cost, err: s.err, oerr: s.oerr }) };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best) return best;
|
||||
const len = (cur.list || []).length;
|
||||
if (len > 120) {
|
||||
const stride = Math.max(1, Math.floor((len - 120) / 40));
|
||||
for (let i = 120; i < len; i += stride) {
|
||||
const s = cur.list[i];
|
||||
if (s && s.q && collideFree(s.q)) {
|
||||
return { q: s.q, res: Object.assign({}, cur, { q: s.q.slice(), cost: s.cost, err: s.err, oerr: s.oerr }) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function routeFrom(from, cur) {
|
||||
if (!COL || !cur || !cur.q || !from) return cur;
|
||||
if (!collideFree(from)) return cur; // 起点已在干涉里:先把自己拔出来再说
|
||||
const seen = new Set(), cand = [];
|
||||
const push = q => {
|
||||
if (!q || q.length !== 6) return;
|
||||
const key = q.map(v => v.toFixed(2)).join(',');
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key); cand.push(q.slice());
|
||||
};
|
||||
push(cur.q);
|
||||
for (const s of (cur.list || [])) push(s.q);
|
||||
// 优先验证与起点 from 关节距离最近的候选解
|
||||
cand.sort((a, b) => costOf(a, null, from) - costOf(b, null, from));
|
||||
const cands = cand.slice(0, CAND_MAX);
|
||||
let fineLeft = 4; // 细验预算:全通过的候选才会走满 n 次探测
|
||||
for (const q of cands) {
|
||||
if (!collideFree(q)) continue;
|
||||
// 快速二分初筛:若中点干涉直接短路(1 次判定否决碰撞射线),节省大半无谓细验开销
|
||||
if (!collideFree(transitPt(from, q, 0.5))) continue;
|
||||
/* 粗筛(≤13 点)先廉价否掉明显穿模的候选,再用与审计同分辨率的 3° 细验定案 */
|
||||
if (!pathFree(from, q, Math.min(13, pathN(from, q, SHORT_DEG)))) continue;
|
||||
if (fineLeft <= 0) break; // 预算用尽就不接受没细验过的候选(宁可贴边停,不赌)
|
||||
fineLeft--;
|
||||
if (pathFree(from, q, pathN(from, q, FINE_DEG))) {
|
||||
const same = q.every((v, i) => Math.abs(v - cur.q[i]) < 1e-9);
|
||||
return Object.assign({}, cur, { q: q, routed: true, blocked: false, kept: same });
|
||||
}
|
||||
}
|
||||
/* 没有一条直线能干净抵达:沿最优候选(优先端点安全的那个)方向二分退回安全边界,
|
||||
让臂贴着障碍停住,而不是穿过去。 */
|
||||
const safeT = findSafeTarget(cur, from);
|
||||
let bestQ = safeT ? safeT.q : null;
|
||||
if (!bestQ) {
|
||||
for (const q of cands) if (collideFree(q)) { bestQ = q; break; }
|
||||
}
|
||||
if (!bestQ) bestQ = cands[0] || cur.q;
|
||||
let lo = 0, hi = 1; // lo=起点(已确认安全),hi=整跳(上面已证明不干净)
|
||||
/* 先按端点二分找到"射线上的第一个撞点"(便宜:每次 1 个探测),
|
||||
再对 from→q 整段做 3° 细验;细验不过就按 0.6 回缩重验。
|
||||
早先直接用 9 点粗扫当谓词,等于把 19° 的采样间隔当成安全边界,实测漏过一帧。 */
|
||||
while (hi - lo > 0.02) {
|
||||
const mid = 0.5 * (lo + hi);
|
||||
if (collideFree(transitPt(from, bestQ, mid))) lo = mid; else hi = mid;
|
||||
}
|
||||
let q = transitPt(from, bestQ, lo);
|
||||
for (let t = 0; t < 5 && lo > 0.02 && !pathFree(from, q, pathN(from, q, FINE_DEG)); t++) {
|
||||
lo *= 0.6; q = transitPt(from, bestQ, lo);
|
||||
}
|
||||
return Object.assign({}, cur, { q: q, safeT: safeT, routed: true, blocked: lo < 0.98, kept: false });
|
||||
}
|
||||
/* 拖拽/松手统一入口:solveTo + 路径路由(opt.from = 当前位形) */
|
||||
function solveRoute(target, opt) {
|
||||
opt = opt || {};
|
||||
if (opt.from && !opt.ref) opt.ref = opt.from;
|
||||
return routeOrVia(opt.from, target, solveTo(target, opt), opt);
|
||||
}
|
||||
/* 两段式绕行:直线被立柱挡住、但目标本身有安全解时,先解一个"抬高版"的中间位形
|
||||
(目标正上方 VIA_DZ),走直线过去,再从那里落回目标。实测 100 个拖拽步里
|
||||
14 步属于这一类(有安全端点、直线到不了)。只在松手全量求解时用:
|
||||
每多一个候选要多解一次 IK + 两段直线检查,实时拖拽扛不住。 */
|
||||
const VIA_DZ = [260, 520];
|
||||
function routeVia(from, target, opt) {
|
||||
for (const dz of VIA_DZ) {
|
||||
const Tw = [target[0], target[1], target[2] + dz];
|
||||
const rw = solveRoute(Tw, Object.assign({}, opt, { noVia: true, noAround: true }));
|
||||
if (!rw || !rw.q || rw.blocked || rw.unsafe || !collideFree(rw.q)) continue;
|
||||
const r2 = solveRoute(target, Object.assign({}, opt, { from: rw.q.slice(), noVia: true, noAround: true, ref: rw.q }));
|
||||
if (!r2 || !r2.q || r2.blocked || r2.unsafe || !collideFree(r2.q)) continue;
|
||||
/* 中间位形表必须把两段各自的子绕行都接上:from→(rw.via)→rw.q→(r2.via)→q。
|
||||
早先只放 [rw.q] 会把 rw/r2 自己的 via 覆盖掉,于是动画走的那条线并不是校验过的那条,
|
||||
实测松手口径因此在离起点 3° 处穿模 6 帧(立柱中段×末端工具法兰)。 */
|
||||
return Object.assign({}, r2, {
|
||||
via: (rw.via || []).concat([rw.q.slice()], r2.via || []), detourDz: dz, detour: 'via'
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/* 绕行使者:收臂 → 转 J1 → 再伸展。
|
||||
CRUISE 表由 scratch/find_cruise_poses.js 搜出并校验:那组 J2/J4/J5 组合在
|
||||
J1 整周每 10° 采样下 collideFree 全通过(280 组枚举里 243 组满足,取半径最贴身
|
||||
且收法不同的 4 组)。几何一改必须重跑该脚本,否则巡航位形可能已经会撞。
|
||||
三段分别是:当前位形→原地收臂(J1 不变)、收臂态把 J1 转到目标解的肩角、
|
||||
再伸展到目标解。每段都过 pathFree,段内 J1 按 ≤10° 采样(与搜索口径一致)。 */
|
||||
/* 巡航参考位形:按收缩幅度从浅到深排序。
|
||||
[0, -15, ...] 为轻微避让,[0, -75, ...] 为深折叠。优先选用微调姿态,避免激进蜷缩。 */
|
||||
const CRUISE = [
|
||||
[0, -15, 0, -95, -60, 0], [0, -45, 0, -95, -60, 0],
|
||||
[0, -75, 0, -95, -60, 0], [0, -90, 0, -65, -60, 0]
|
||||
];
|
||||
/* 转 J1 那一段按 5° 网格查表(懒算 + 记忆化) */
|
||||
const CRUISE_OK = CRUISE.map(() => new Map());
|
||||
function cruiseFree(ci, j1) {
|
||||
const key = Math.round(j1 / 5) * 5;
|
||||
const m = CRUISE_OK[ci];
|
||||
let v = m.get(key);
|
||||
if (v === undefined) { const q = CRUISE[ci].slice(); q[0] = key; v = collideFree(q); m.set(key, v); }
|
||||
return v;
|
||||
}
|
||||
function turnFree(ci, j1a, j1b) {
|
||||
const n = Math.max(1, Math.ceil(Math.abs(wrap180(j1b - j1a)) / 5));
|
||||
for (let i = 0; i <= n; i++) if (!cruiseFree(ci, j1a + (wrap180(j1b - j1a)) * i / n)) return false;
|
||||
return true;
|
||||
}
|
||||
/* 采样点数按线段在关节空间的步长自适应:每 SHORT_DEG(6°) 一个点,最少 3 点、最多 40 点 */
|
||||
function pathN(from, to, deg) {
|
||||
return Math.max(3, Math.min(40, Math.round(stepDeg(from, to) / (deg || SHORT_DEG)) + 1));
|
||||
}
|
||||
/* 轻量偏移量集合:用于避开障碍物边缘(抬肩/微收肘),避免整臂蜷缩 */
|
||||
const GENTLE_OFFSETS = [
|
||||
[-10, 0], [-20, 0], [-15, -10], [0, -15], [-20, -15],
|
||||
[10, 0], [20, 0], [-30, 0], [-10, 10]
|
||||
];
|
||||
/* 智能平滑协同避障规划(模拟人类避障直觉):
|
||||
1) 微调避让:针对贴边擦碰,在直线弦中点微调 J2/J4,各轴保持平滑协同,绝不缩臂;
|
||||
2) 协同单中间点 + 二分松弛:J1 全程平滑旋转,中间位形只做必要的外展/微收,并向直线插值松弛;
|
||||
3) 模拟人类自然避障(回退一点 -> 绕行过去 -> 伸展到位):
|
||||
当手臂靠近立柱或跨度大时,在起点方位先原位/微转收臂回退,跨过立柱后再展开到位;
|
||||
4) 保底安全回退:极端奇异位形下确保无碰撞。 */
|
||||
function routeAroundPlan(from, cur, quick, safeT) {
|
||||
let qT = safeT ? safeT.q : null, resT = safeT ? safeT.res : cur;
|
||||
if (!qT) {
|
||||
const found = findSafeTarget(cur, from);
|
||||
if (found) { qT = found.q; resT = found.res; }
|
||||
}
|
||||
if (!qT) return null;
|
||||
|
||||
const chordMid = transitPt(from, qT, 0.5);
|
||||
const dj1 = wrap180(qT[0] - from[0]);
|
||||
const j1Mid = from[0] + dj1 * 0.5;
|
||||
|
||||
// 阶段 1:微调避障(针对贴边穿模:直接在弦中点微调 J2/J4,完全不蜷缩手臂)
|
||||
for (const [dj2, dj4] of GENTLE_OFFSETS) {
|
||||
const testQ = chordMid.slice();
|
||||
testQ[1] += dj2; testQ[3] += dj4;
|
||||
if (!collideFree(testQ)) continue;
|
||||
const nA = Math.min(8, pathN(from, testQ, FINE_DEG));
|
||||
const nB = Math.min(8, pathN(testQ, qT, FINE_DEG));
|
||||
if (pathFree(from, testQ, nA) && pathFree(testQ, qT, nB)) {
|
||||
return { res: resT, via: [testQ], detour: 'gentle' };
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 2:连续协同单中间点避障(J1 保持平滑转动,避障姿态向自然弦二分松弛)
|
||||
const use = quick ? [0, 1, 2] : [0, 1, 2, 3];
|
||||
for (const ci of use) {
|
||||
const c = CRUISE[ci];
|
||||
const qMidSafe = c.slice();
|
||||
qMidSafe[0] = j1Mid;
|
||||
if (!collideFree(qMidSafe)) continue;
|
||||
const nA = Math.min(8, pathN(from, qMidSafe, FINE_DEG));
|
||||
const nB = Math.min(8, pathN(qMidSafe, qT, FINE_DEG));
|
||||
if (pathFree(from, qMidSafe, nA) && pathFree(qMidSafe, qT, nB)) {
|
||||
// 二分松弛:寻找能安全通过的最小偏离量(靠近弦中点)
|
||||
let lo = 0, hi = 1;
|
||||
for (let it = 0; it < 4; it++) {
|
||||
const midA = (lo + hi) * 0.5;
|
||||
const testQ = transitPt(chordMid, qMidSafe, midA);
|
||||
if (collideFree(testQ) && pathFree(from, testQ, 6) && pathFree(testQ, qT, 6)) {
|
||||
hi = midA;
|
||||
} else {
|
||||
lo = midA;
|
||||
}
|
||||
}
|
||||
const optMid = transitPt(chordMid, qMidSafe, hi);
|
||||
return { res: resT, via: [optMid], detour: 'coordinated' };
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 3:模拟人类自然避障(回退一点 -> 绕行过去 -> 伸展到位)
|
||||
// 用户需求:"模拟人类,哪怕中间有东西挡了,应该回退一点然后再移动过去,而不是说挡着了就不计算了"
|
||||
// 第一中间点在起点方位(微转 frac)处收臂回退,第二中间点在避开立柱后跨过,再伸展入目标
|
||||
for (const ci of use) {
|
||||
const c = CRUISE[ci];
|
||||
for (const frac of [0.08, 0.0, 0.15, 0.25]) {
|
||||
const w1 = c.slice(); w1[0] = from[0] + dj1 * frac;
|
||||
const w2 = c.slice(); w2[0] = qT[0] - dj1 * frac;
|
||||
if (!collideFree(w1) || !collideFree(w2)) continue;
|
||||
const n1 = Math.min(8, pathN(from, w1, FINE_DEG));
|
||||
const n2 = Math.min(8, pathN(w1, w2, FINE_DEG));
|
||||
const n3 = Math.min(8, pathN(w2, qT, FINE_DEG));
|
||||
if (pathFree(from, w1, n1) && pathFree(w1, w2, n2) && pathFree(w2, qT, n3)) {
|
||||
return { res: resT, via: [w1, w2], detour: 'retract_pass' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 阶段 4:保底三段式(仅在所有平滑协同均被阻挡的极端奇异位形启用)
|
||||
for (const ci of use) {
|
||||
const c = CRUISE[ci];
|
||||
const fold = c.slice(); fold[0] = from[0];
|
||||
const turn = c.slice(); turn[0] = qT[0];
|
||||
const ok = pathFree(from, fold, Math.min(8, pathN(from, fold, FINE_DEG))) &&
|
||||
turnFree(ci, fold[0], turn[0]) &&
|
||||
pathFree(turn, qT, Math.min(8, pathN(turn, qT, FINE_DEG)));
|
||||
if (ok) return { res: resT, via: [fold, turn], detour: 'around' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function routeOrVia(from, target, cur, opt) {
|
||||
const r = routeFrom(from, cur);
|
||||
if (!r.blocked || !from) return r;
|
||||
const around = opt.noAround ? null : routeAroundPlan(from, cur, opt.quick, r.safeT);
|
||||
if (around) return Object.assign({}, around.res, { via: around.via, routed: true, blocked: false, detour: around.detour || 'around' });
|
||||
if (opt.noVia) return r;
|
||||
return routeVia(from, target, opt) || r;
|
||||
}
|
||||
|
||||
/* ---------- 6b. 动态限位求解:静态解 + 贴边轴放宽重解 ----------
|
||||
静态限位来自"其它轴全零"的单轴扫描,对非相邻碰撞对(典型:腕/法兰撞
|
||||
固定立柱)系统性过紧——实测 J2 正端随 J1 从 +9.75°(垂臂零位)放到
|
||||
@@ -1916,18 +2210,15 @@ function make(cfg) {
|
||||
且总关节角不升才采用,否则原样返回(最坏即静态行为,无回退风险)。 */
|
||||
function solveDyn(target, opt) {
|
||||
opt = opt || {};
|
||||
if (opt.from && !opt.ref) opt.ref = opt.from;
|
||||
let cur = solveTo(target, opt);
|
||||
if (!COL || opt.noDyn || !cur.q) return cur;
|
||||
/* 静态限位盒不保证组合姿态无干涉(单轴扫描各轴独立取限位,组合折叠会撞——
|
||||
sweep_v2 的组合扫描只记录不收紧滑块)。这里对最终解做全姿态碰撞校验:
|
||||
撞了就沿候选表(按代价升序)找第一个安全的,代价容忍 1.5 倍 + 20°。 */
|
||||
撞了就沿候选表找安全的解,优先采用最贴近起点的安全位形。 */
|
||||
if (!collideFree(cur.q)) {
|
||||
let pick = null;
|
||||
for (let i = 0; i < cur.list.length && i < 40; i++) {
|
||||
const s = cur.list[i];
|
||||
if (s.cost <= cur.cost * 1.5 + 20 && collideFree(s.q)) { pick = s; break; }
|
||||
}
|
||||
if (pick) cur = Object.assign({}, cur, { q: pick.q.slice(), cost: pick.cost, swaped: true });
|
||||
const safeT = findSafeTarget(cur, opt.from);
|
||||
if (safeT) cur = Object.assign({}, safeT.res, { swaped: true });
|
||||
else cur = Object.assign({}, cur, { unsafe: true });
|
||||
}
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
@@ -1968,7 +2259,7 @@ function make(cfg) {
|
||||
if (!picked) break;
|
||||
cur = Object.assign({}, picked, { dynAxis: axis, dynBound: bound });
|
||||
}
|
||||
return cur;
|
||||
return opt.from ? routeOrVia(opt.from, target, cur, opt) : cur;
|
||||
}
|
||||
|
||||
/* ---------- 7. 自检:闭式 FK ↔ 矩阵链;逆解重构;姿态重构 ---------- */
|
||||
@@ -2022,6 +2313,7 @@ function make(cfg) {
|
||||
}
|
||||
|
||||
return { fk: fk, fkChain: fkChain, solve: solve, solveTo: solveTo, solveWarm: solveWarm, solveDyn: solveDyn,
|
||||
solveRoute: solveRoute, routeFrom: routeFrom, transitPt: transitPt,
|
||||
mixSolve: mixSolve, closest: closest, closest6: closest6, closestOri: closestOri, orient: orient,
|
||||
branch: branch, selfTest: selfTest, poseSelfTest: poseSelfTest,
|
||||
collideFree: collideFree, collideDetail: collideDetail,
|
||||
|
||||
@@ -269,24 +269,43 @@ function refreshJointLabelsLazy(q) {
|
||||
refreshJointLabels(ORDER.map(id => ang[id]));
|
||||
}, 260);
|
||||
}
|
||||
/* 六轴动态边界改到 Worker 里算:连续轴现在必须整周粗扫(实测 6 轴×2 向一次
|
||||
175ms → 1131ms),留在主线程会把拖拽冻住。dynAsync 只记账、不写 IKWlast,
|
||||
否则会把在飞的 solveTo/solveDyn 结果误标成过期(历史上丢过回调导致实时追踪停摆)。 */
|
||||
function dynAsync(q, tol, job) {
|
||||
const w = ikWorker();
|
||||
if (!w) { job(S.dynLimits(q, tol)); return; } // Worker 不可用:退回主线程同步算
|
||||
const id = ++IKWseq;
|
||||
IKWjobs.set(id, r => { job(r && !r.__err ? r : S.dynLimits(q, tol)); });
|
||||
w.postMessage({ id: id, op: 'dynLimits', args: [q, tol] });
|
||||
}
|
||||
function refreshJointLabels(q) {
|
||||
dynAsync(q.slice(), 1.0, D => paintJointLabels(D));
|
||||
}
|
||||
function paintJointLabels(D) {
|
||||
D = D || {};
|
||||
for (const id of ORDER) {
|
||||
const el = $('e' + id);
|
||||
if (!el) continue;
|
||||
const t = LIM[id];
|
||||
if (COL && !t.cont) {
|
||||
if (COL && D[id]) {
|
||||
/* 边界由几何说话:机械区间(adj 实测)之内,再按当前姿态二分胶囊碰撞。
|
||||
lo 侧一律收紧;hi 侧只有"硬边"(相邻件自碰撞,配置无关)才受静态值封顶,
|
||||
可放宽侧(撞固定立柱那类)由模型说话——动态边界本就可以远超静态 hi。 */
|
||||
const lo = Math.max(t.lo, S.dynLimit(id, q, -1, 1.0));
|
||||
可放宽侧(撞固定立柱那类)由模型说话——动态边界本就可以远超静态 hi。
|
||||
连续轴(J1/J3/J6)同样要查:折肘/折腕位形下末段会扫到固定立柱,
|
||||
旧口径因为跳过连续轴而把它显示成整周可用。 */
|
||||
const lo = Math.max(t.cont ? -180 : t.lo, D[id].lo);
|
||||
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 hi = hardHi ? Math.min(t.hi, D[id].hi) : D[id].hi;
|
||||
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>' : '');
|
||||
/* 连续轴没收紧时显示"整周可用",收紧了就换成区间:同一个 <em> 是唯一写手,
|
||||
别让静态的 360 度连续徽标和实时夹取区间并存(状态灯与数字会打架)。 */
|
||||
el.className = dyn ? '' : (t.cont ? 'ok' : '');
|
||||
el.innerHTML = (t.cont && !dyn) ? '整周可用' :
|
||||
((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 +
|
||||
el.title = (t.cont ? '连续轴(整周)' : '机械区间(相邻件实测)') + 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' : '');
|
||||
@@ -298,8 +317,8 @@ function refreshJointLabels(q) {
|
||||
|
||||
/* ---------- 3. 关节写入(同步滑块与标签)---------- */
|
||||
let anim = null;
|
||||
function setJoints(q, doAnim) {
|
||||
if (doAnim) { animTo(q, 620); return; }
|
||||
function setJoints(q, doAnim, via, ms) {
|
||||
if (doAnim) { animTo(q, ms || 620, via); return; }
|
||||
anim = null; // 直接写入必须取消在飞动画,否则入场动画会覆写实时解
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i]; ang[id] = q[i];
|
||||
@@ -311,7 +330,7 @@ function setJoints(q, doAnim) {
|
||||
refreshTarget();
|
||||
if (COL) refreshJointLabelsLazy(ORDER.map(id => ang[id]));
|
||||
}
|
||||
function animTo(q, ms) {
|
||||
function animTo(q, ms, via) {
|
||||
if (anim) { anim = null; }
|
||||
const from = ORDER.map(id => ang[id]);
|
||||
/* 连续轴(J1 肩抬举 / J3 大臂自转 / J6 腕自转)走「短弧」。
|
||||
@@ -319,20 +338,31 @@ function animTo(q, ms) {
|
||||
若按数值直接线性插值,+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]);
|
||||
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。
|
||||
via:绕行中间位形(solveRoute 判直线被挡时给的两段式路径),逐段同样按短弧折算,
|
||||
每段各 ms 毫秒——渲染走的段与碰撞校验走的段是同一批,不存在"校验过了但动画穿模"。 */
|
||||
const segs = [];
|
||||
let p = from;
|
||||
for (const raw of (via || []).concat([q])) {
|
||||
const t = raw.slice();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i];
|
||||
if (LIM[id].cont) t[i] = p[i] + S.wrap180(raw[i] - p[i]);
|
||||
}
|
||||
segs.push(t); p = t;
|
||||
}
|
||||
const end = q.slice();
|
||||
anim = { from: from, to: to, t0: performance.now(), ms: ms };
|
||||
const total = ms * segs.length;
|
||||
anim = { from: from, to: segs[segs.length - 1], t0: performance.now(), ms: total };
|
||||
const tick = () => {
|
||||
if (!anim) return;
|
||||
const k = Math.min(1, (performance.now() - anim.t0) / anim.ms);
|
||||
const el = performance.now() - anim.t0;
|
||||
const si = Math.min(segs.length - 1, Math.floor(el / ms));
|
||||
const k = Math.min(1, (el - si * ms) / ms);
|
||||
const e = k * k * (3 - 2 * k);
|
||||
const a0 = si === 0 ? from : segs[si - 1], a1 = segs[si];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i], a = anim.from[i] + (anim.to[i] - anim.from[i]) * e;
|
||||
const id = ORDER[i], a = a0[i] + (a1[i] - a0[i]) * e;
|
||||
ang[id] = a;
|
||||
const sl = $('s' + id), v = $('v' + id);
|
||||
if (sl) sl.value = a;
|
||||
@@ -459,6 +489,13 @@ function renderResult(res, src) {
|
||||
const mixMid = poseOn && res.priority === 'mix' && !posOk && !oriOk;
|
||||
badge.textContent = res.unsafe
|
||||
? '位姿与底座/立柱近似干涉(目标可能不可达或位于立柱区内)'
|
||||
: res.blocked
|
||||
? '路径被立柱挡住 · 已停在边界(目标差 ' + dsc.toFixed(1) + ' mm)'
|
||||
: res.detour
|
||||
? (res.detour === 'retract_pass' ? '避障到达 · 模拟人类回退绕过立柱'
|
||||
: (res.detour === 'gentle' ? '避障到达 · 微调协同避开立柱'
|
||||
: (res.detour === 'coordinated' ? '避障到达 · 平滑协同跨越立柱'
|
||||
: '绕行立柱后到达')))
|
||||
: (posOk && oriOk) ? '已到达'
|
||||
: oriPrio ? '姿态到达·位置最近 ' + dsc.toFixed(1) + ' mm'
|
||||
: mixMid ? '混合折中 · 位置差 ' + dsc.toFixed(1) + ' mm · 姿态残差 ' + dori.toFixed(2) + '\u00B0'
|
||||
@@ -569,7 +606,8 @@ function runSolve() {
|
||||
refreshTarget();
|
||||
badge.textContent = '计算中…'; badge.className = 'badge wait';
|
||||
setTimeout(() => {
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef() };
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(),
|
||||
from: ORDER.map(id => ang[id]) }; // 松手求解同样按"直线能不能走"挑解
|
||||
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; // 已被更新的一发取代:交给那一发渲染
|
||||
@@ -582,7 +620,7 @@ function runSolve() {
|
||||
: (res.note ? res.note
|
||||
: (poseOn ? '超可达位姿空间 → 阻尼最小二乘最近位姿(位置 mm + 姿态联合)'
|
||||
: '超工作空间 → 阻尼最小二乘最近位置')));
|
||||
if (res.q) setJoints(res.q, true);
|
||||
if (res.q) setJoints(res.q, true, res.via);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
@@ -604,9 +642,13 @@ function runSolveLive() {
|
||||
};
|
||||
try {
|
||||
lastTool = +$('ikTool').value || 0;
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), warm: curRef() };
|
||||
/* 实时追踪原来走 solveTo —— 一个碰撞判定都不做:实测把目标球横拖过立柱,
|
||||
1300 帧里 870 帧处于穿模状态。改用 solveRoute:按"从当前位形这条直线能不能走"
|
||||
挑解,走不通就贴着障碍停(blocked),不再把手势直接映射进立柱里。 */
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), warm: curRef(),
|
||||
from: ORDER.map(id => ang[id]), noVia: true, quick: true }; // 拖拽中不做抬高版绕行(要再解两次全量 IK)
|
||||
if (poseOn) { opt.pose = { R: Rsolver() }; opt.priority = prioVal(); if (opt.priority === 'mix') opt.mixW = mixWVal(); }
|
||||
solveAsync('solveTo', [TGT.slice(), opt], (res, fresh) => { // Worker 后台:全量回落也不冻结主线程
|
||||
solveAsync('solveRoute', [TGT.slice(), opt], (res, fresh) => { // Worker 后台:全量回落也不冻结主线程
|
||||
try {
|
||||
if (!res || !fresh) return;
|
||||
renderResult(res, res.warm
|
||||
@@ -623,7 +665,9 @@ function runSolveLive() {
|
||||
: (res.note ? '实时追踪 · ' + res.note
|
||||
: (poseOn ? '实时追踪 · 超可达位姿空间(阻尼最小二乘最近位姿)'
|
||||
: '实时追踪 · 超工作空间(阻尼最小二乘最近位姿)'))));
|
||||
if (res.q) setJoints(res.q, false);
|
||||
/* 无绕行时立即落位(跟手);一旦给出绕行中间位形,就必须分段动画——
|
||||
直接跳到 res.q 等于把"收臂→转 J1→伸展"三段压成一条直线,正是要避免的穿模。 */
|
||||
if (res.q) setJoints(res.q, !!(res.via && res.via.length), res.via, 300);
|
||||
} finally { settle(fresh); }
|
||||
});
|
||||
} catch (err) { settle(false); }
|
||||
@@ -835,7 +879,9 @@ const MOTION_UI = ['ikbar', 'ikpanel', 'joints', 'jointsHead', 'footIk'];
|
||||
function setView(v, keepPose) {
|
||||
VIEW = v;
|
||||
const isMotion = v === 'motion';
|
||||
const isSheet = v === 'sheet';
|
||||
document.body.classList.toggle('motion', isMotion);
|
||||
document.body.classList.toggle('sheet-mode', isSheet);
|
||||
for (const id of MOTION_UI) {
|
||||
const e = $(id);
|
||||
if (e) e.style.display = isMotion ? '' : 'none';
|
||||
@@ -849,6 +895,7 @@ function setView(v, keepPose) {
|
||||
} else {
|
||||
if (mode !== 'drag') setMode('drag'); // 离开运动模拟:交互模式复位
|
||||
if (v === 'model' && !keepPose) setJoints([0, 0, 0, 0, 0, 0], true); // 静态模型:关节锁定零位
|
||||
if (isSheet && typeof openSheetView === 'function') openSheetView();
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
@@ -861,11 +908,11 @@ document.querySelectorAll('.tab[data-view]').forEach(b =>
|
||||
与当前代码不符时会标红提示重算。
|
||||
重新生成: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 }
|
||||
stamp: '2026-10-14 04:02', sig: 'fd1f8bff',
|
||||
fk: { n: 5000, err: 5.684341886080801e-13 },
|
||||
ik: { n: 200, hit: 200, tried: 200, err: 8.320331044333469e-13 },
|
||||
pose: { n: 400, hit: 398, err: 1.5596676084670544, ms: 86.40111575000003 },
|
||||
ws: { n: 400000, x: [-377, 416], y: [-388, 643], z: [96, 1126], rmax: 517 }
|
||||
};
|
||||
/* 紧凑指纹:与离线脚本 compute_precomputed.cjs 里的算法逐字一致
|
||||
(注意取 script.textContent——HTML 解析会把行尾归一化成 LF) */
|
||||
|
||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 41 KiB |