Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217a70b484 | ||
|
|
fc17ae10d0 | ||
|
|
2f5a72a6ab | ||
|
|
7a194dd4a7 | ||
|
|
a41a5440c6 | ||
|
|
864b21f894 | ||
|
|
b396298819 | ||
|
|
53a50ecab9 | ||
|
|
425229f412 | ||
|
|
7e063eb3d5 | ||
|
|
406f180cab | ||
|
|
86323f1691 | ||
|
|
0f5b84b919 | ||
|
|
0a19b29fd3 | ||
|
|
1bf59e7175 | ||
|
|
cafa038110 | ||
|
|
527bc25c11 | ||
|
|
9a7404856e | ||
|
|
1f2f2a2bb1 | ||
|
|
0653cf0d8d | ||
|
|
95ba43a1c7 | ||
|
|
5365add657 | ||
|
|
573082df26 | ||
|
|
5a87c0655a | ||
|
|
0b47905a1c | ||
|
|
0d522dd782 | ||
|
|
d60cbcdb3c | ||
|
|
979a5814c1 | ||
|
|
c2732fb3cd | ||
|
|
7fe1e5e0b3 | ||
|
|
3c4bc6c8ab | ||
|
|
4a59ca84f6 | ||
|
|
faf6dd2748 | ||
|
|
b1cf4126fd | ||
|
|
e431862793 | ||
|
|
c65b9986ca | ||
|
|
db0bbbc15d | ||
|
|
21ad209747 | ||
|
|
c143b56232 | ||
|
|
83dcc1c3ee | ||
|
|
9ed213417b | ||
|
|
e6418da084 | ||
|
|
241333c213 | ||
|
|
c128288c1b | ||
|
|
b25a7a8bfa | ||
|
|
c175357b72 | ||
|
|
d03f34822c | ||
|
|
8ad6b58fca | ||
|
|
591678703a | ||
|
|
b6141d237c | ||
|
|
a1ee7d1a4d | ||
|
|
f40a40c56b | ||
|
|
3675fbbd9b | ||
|
|
5591adb39a | ||
|
|
5348bc6f6f | ||
|
|
f8cdabb781 | ||
|
|
8800a68cad | ||
|
|
2a0a2e55ad | ||
|
|
cc43b3d3be | ||
|
|
67964496be | ||
|
|
46bdb98dd8 | ||
|
|
318ad0e264 | ||
|
|
1dfdd48792 | ||
|
|
11205f81c7 | ||
|
|
8cf830493e | ||
|
|
e79cfd8e8f | ||
|
|
f713355351 | ||
|
|
531a293688 | ||
|
|
649bd69810 | ||
|
|
b32d44efe0 | ||
|
|
7e040fc766 | ||
|
|
b67d3ce56e | ||
|
|
b0efb44368 | ||
|
|
c754521bde | ||
|
|
d8f6c69406 | ||
|
|
e3b25452e1 | ||
|
|
b593e47c76 | ||
|
|
6fe97fc643 | ||
|
|
208b37048b | ||
|
|
4fdc37da9f | ||
|
|
a70724e8a9 | ||
|
|
c0c78251b5 |
@@ -1,13 +1,51 @@
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# 构建产物(可用 src/ 下的脚本重新生成)
|
||||
out/
|
||||
|
||||
# flaky/ 是那 13 张的参考像素 + thresholds.json 阈值;PNG 本身不需要入库。
|
||||
# 临时调试脚本与截图
|
||||
scratch/
|
||||
|
||||
# 关卡截图落点 = 纯生成物:每次 `python -m tests.verify --shots` 由 shot.py 重写
|
||||
# 关卡比对的权威物是 tests/baselines/(md5 名单 + flaky 参考像素),不在这里。
|
||||
# 其它编辑器的本地配置/会话残留(不是工程产物,也不该跟着版本走)
|
||||
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
||||
# 定位图/ 只是"给用户看的核验图落点",一次性、会反复清空;
|
||||
# 旧 web/ 单文件产物目录(已删,源码在 webapp/)
|
||||
# 构建产物(可用 python -m arm.* 重新生成)
|
||||
# 构建产物:由 python -m arm.* 重新生成,体积大(mesh 14MB + 单文件查看器 16MB×3),
|
||||
# 比对的权威物在 tests/baselines/ —— shots_stable.md5 是 92 张的 md5 名单,
|
||||
# 编辑器 / 系统文件
|
||||
Thumbs.db
|
||||
# 过去被跟踪导致仓库背着 47MB 生成物;取消跟踪后重建不再脏 git status。
|
||||
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
|
||||
# (其中有 13 张在同一份构建下也不逐字节复现,抗锯齿/量化噪声)。
|
||||
*.bak_*
|
||||
*.pyc
|
||||
.bak/
|
||||
.trae/
|
||||
.workbuddy/
|
||||
.zcode/
|
||||
.zcodeignore
|
||||
/机器人手臂3D查看器.html
|
||||
Desktop.ini
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
backups/
|
||||
data/cache/
|
||||
dist/
|
||||
docs/shots/
|
||||
ml/data/
|
||||
ml/node_modules/
|
||||
ml/runs/
|
||||
out/
|
||||
scratch/
|
||||
web/
|
||||
webapp/data/
|
||||
webapp/generated/
|
||||
定位图/
|
||||
|
||||
# 策略模型产物(由 python -m ml.export_onnx 生成,20 MB 级,不入库)
|
||||
webapp/models/*.onnx
|
||||
webapp/models/*.meta.json
|
||||
|
||||
# 从头重跑时旧产物挪到这里(不入库)
|
||||
ml/archive/
|
||||
|
||||
# onnxruntime-web 本地副本(约 33 MB):**入库**,离线/无 npm 时直接可用,不再依赖 CDN(2026-10-18 用户要求)
|
||||
# 来源 onnxruntime-web@1.20.1 dist,六个文件 + VERSION;ml/prep/vendor_ort.sh 在缺失时仍可补齐
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# ZCode 工作区文件搜索忽略规则(.zcodeignore)
|
||||
# 语法与 .gitignore 一致,只影响 ZCode 的 @ 文件候选 / Command Center / 文件树搜索,
|
||||
# 不影响文件树浏览、上传或 Agent 文件访问。
|
||||
# 修改 .gitignore 不会自动同步到本文件;可在设置页「从 .gitignore 同步」。
|
||||
|
||||
|
||||
# ===== ↑ 以上同步自 .gitignore(「从 .gitignore 同步」只重写以上部分)=====
|
||||
.git/
|
||||
.hg/
|
||||
.svn/
|
||||
node_modules/
|
||||
bower_components/
|
||||
jspm_packages/
|
||||
__pycache__/
|
||||
site-packages/
|
||||
venv/
|
||||
coverage/
|
||||
htmlcov/
|
||||
lcov-report/
|
||||
cmakefiles/
|
||||
cmake-build-*/
|
||||
bazel-*/
|
||||
pods/
|
||||
deriveddata/
|
||||
storybook-static/
|
||||
playwright-report/
|
||||
test-results/
|
||||
allure-results/
|
||||
allure-report/
|
||||
cdk.out/
|
||||
*.egg-info/
|
||||
*.dist-info/
|
||||
eggs/
|
||||
pip-wheel-metadata/
|
||||
wheels/
|
||||
# ----- ↑ 以上为 ZCode 默认排除规则(自定义规则请写在本行下方,不会被同步/恢复改动)-----
|
||||
# 自定义规则写在下方(本行提示可删除)
|
||||
@@ -7,9 +7,10 @@
|
||||
## 🌟 核心设计特性
|
||||
|
||||
1. **一体化直锁电机**:全面取消外挂独立端面齿联轴器,所有关节由 3×M2 沉头螺钉直锁电机输出轴,轴向尺寸大幅压缩,刚度显著提升。
|
||||
2. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
||||
3. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
||||
4. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
||||
2. **电机前端双向夹紧 + 2mm 厚度公差**:每个关节的电机由「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」前后两面夹死;后端板承压面按 `MOTOR_TOL=2.0` 让空成**尾端垫片位**(`armcore.MOTOR_TOL` / `arm_build.REAR_SEAT`),装配时在电机尾端面与承压面之间叠 0~2.6mm 的 M3 平垫,**逐台电机按实测长度配垫**(T = 58.6 − 电机实际长度 L),让前端盖正好落到压盖面上。
|
||||
3. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
||||
4. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
||||
5. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
||||
|
||||
---
|
||||
|
||||
@@ -41,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 查看器打包生成脚本
|
||||
@@ -51,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 # 逐对扫描明细与组合折叠约束
|
||||
@@ -69,15 +87,89 @@ python src/sweep_v2.py
|
||||
|
||||
| 构件编号 | 构件名称 | 关联驱动关节 | 设计说明 |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| **P01** | 胸板 / 安装板 | 根基固定 | 12mm 基础基座,预留 4x M4 紧固位与躯干走线窗 |
|
||||
| **P02** | 底脚 / 支撑座 | 根基固定 | 200×320mm 重型防倾覆底盘,提供稳固调试基座 |
|
||||
| **P03** | 肩吊臂 / 肩机舱 | J1 驱动抱紧 | 仿生规整风道机舱,带 7 组轴向散热格栅,抱紧 M1 电机 |
|
||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
|
||||
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2,后侧刚性抱紧 M3 |
|
||||
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机 |
|
||||
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5 |
|
||||
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5,立式抱紧 M6 腕旋转电机 |
|
||||
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 40mm) |
|
||||
| **P01** | 立柱(三段 A/B/C) | 根基固定 | 66×66mm 三角截面立柱,分段榫接 + M4 自攻对锁孔防反装;顶端为 P03 安装台 |
|
||||
| **P02** | 底盘(核心座 + 前/后防翻力臂) | 根基固定 | 195×190mm 核心承力座 + 48mm 套筒 + 前后三角加强筋;前后防翻力臂带 M5 预埋螺母 |
|
||||
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
|
||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2;**M2 前端压盖**(R35 / Y155.5~161):电机前端盖再加 4×M3x8(BCD55)夹一次,与后端板 4×M3x10 前后双向面压,消掉大扭矩下电机在座孔里的倾斜;**前端围挡只切敞口那半圈**(见下);**立柱减重槽**(X23~35 / Y102~154 / Z525↑ 敞开):立柱由实心变 U 形槽钢,361→289cm³,抗弯靠两侧 X 向腹板(z=560 截面 I 207370→199777,降 3.6%,而面积降 28%) |
|
||||
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2(J2 法兰盘背面后移到 Y=162,给 P04 前端压盖让位,J2 凸台同步加长以保住 S01 咬合),后侧刚性抱紧 M3;**M3 前端压盖**(R35 / Z 384.0~389.5,4×M3x8 BCD55):M3 是 roll 电机(轴向 -Z),压盖与后端板 4×M3x10 前后双向面压;代价是对侧 P06 的 J3 毂面后移 5.5mm 让位 |
|
||||
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机;**M4 前端压盖** 4×M3x8(BCD55),同 P04 配方;**前端围挡只切敞口那半圈**(见下);**J3 毂面后移 5.5mm**(顶端 388.5→383.0,直锁凸台 59.5→65.0):给 P05 的 M3 前端压盖让位,留 1.0mm 动间隙——与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源,换算式同为「压盖 5.0 + 动间隙 1.0 − 原基准 0.5」;凸台同步加长,3×S01 输出螺钉咬合量与 J3 直锁关系一律不变 |
|
||||
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5;**M5 前端压盖** 4×M3x8(BCD55);**前端围挡只切敞口那半圈**(本件环壁 r32.25~35,因 2.0mm 关节间隙而更薄,见下);J4 从动侧(法兰盘/立柱/圆弧加强筋)整体后移到 Y=162 给 P06 压盖让位,J4 凸台同步加长;**顶板减重槽**(X−3~43 / Y103~153 / Z255↑ 敞开):288→230cm³,Y 上限取 153 以避开 y_hub_in=158.5 起的竖直立柱本体(那是 J4 从动法兰的承力柱) |
|
||||
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5(J5 法兰盘背面后移到 Y=162,给 P07 前端压盖让位,J5 凸台同步加长),立式抱紧 M6 腕旋转电机;**M6 前端定位环只切敞口那半圈**(尚无 M6 前端压盖,环是光板,见下)。壁厚峰值 8.3mm,本就是薄壳 |
|
||||
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 44mm);**填充 100%→30%**:该截面安全系数 288,是全机裕度最大的件,满填充纯属浪费 |
|
||||
| **M1~M6** | 关节电机模组 ×6 | 各关节驱动 | Ø60 方形标准减速电机模组(63.6mm 方形尾盖),210 g/台,整机 1260 g |
|
||||
|
||||
> ⚠️ **电机装配必须配垫片(厚度公差 2mm)**:各台 Ø60 电机的轴向长度实测差可达 2mm。
|
||||
> 这条轴向链是「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」两个刚性夹持面,螺钉头一旦坐死
|
||||
> 在沉孔台阶上就不能再进给,所以公差**不能靠拧紧消化**,只能由后端板的承压面让空出来的
|
||||
> 垫片位吸收(`armcore.MOTOR_TOL = 2.0`,`arm_build.REAR_SEAT = 0.6 + 2.0 = 2.6`)。
|
||||
> 装配顺序:① 后端板承压面上叠 **T = 58.6 − 电机实测长度 L** 的 M3 平垫(0.5/1.0/1.5 组合,
|
||||
> 标称电机约 0.6mm,量程 0~2.6mm);② 电机推入到尾端面贴住垫片;③ 前端压盖 4×M3x8 拧紧,
|
||||
> 此时前端盖正好落在压盖面上(0.0 面压);④ 后端板 4×M3x10 拧紧夹死垫片。
|
||||
> **不做第①步的后果**:电机长 1mm 以上就装不进去(前端盖越位 1mm 撞压盖本体),
|
||||
> 电机短 1mm 以上压盖就压不上、只剩后端板单面锁死,大扭矩下电机照样点头。
|
||||
> 校核脚本:`scratch/motor_tol_window.py`(窗口换算)+ `scratch/motor_tol_check.py`
|
||||
> (真实电机包络布尔验证 5 个夹紧关节 × ±2mm,全部零干涉)。
|
||||
> 另:J1/M1(P03 侧开式电机舱)本身就是 Ø60.7 直通长孔、比电机长 3mm,天然覆盖该公差,无需配垫。
|
||||
>
|
||||
> **承压面为什么是 R35 圆面而不是 64.4 方腔**(2026-10-04 整改):承压面下沉 2.6mm 后,
|
||||
> 如果承压面段仍用 64.4 **方**腔,方腔壁以外 `z=-2.6..0` 那圈 R35 料就相对新承压面
|
||||
> 凸起 2.6mm —— P04~P08 后端整圈「悬空凸台」,3D 打印时是悬空薄壁(要支撑、公差难保)。
|
||||
> 现在承压面段用 **R35 圆腔**(`arm_build.SEAT_R`),整圈一次挖到承压面高度,凸台零残留。
|
||||
> `SEAT_R` 必须**等于**外壁 R35:只要小于 R35,刀半径与外壁之间那圈环墙就必然保留并相对
|
||||
> 承压面凸起(P08 实测 1536.9mm³)—— 这就是用户 2026-10-05 在**尾端**指出的"围挡",
|
||||
> 是取值的必然产物而不是残留。连带好处:承压环从「1.95mm 窄环」变成「完整 R35 圆环 4.75mm」,
|
||||
> **受压面积 383 → 974 mm²(2.5 倍)**,夹紧更实。`SEAT_TOP=+1.0` 越过电机尾端面 1mm,
|
||||
> 吃掉半包开口处的 0.052cm³ 残片(取 0 时该残片会脱空成独立碎块)。
|
||||
>
|
||||
> **前端那一圈"围挡"(2026-10-05e,全机 5 处)**:用户 2026-10-05 先在 J3/M3 指出**电机
|
||||
> 另一头(前端)也有一圈围挡**,随后确认其余四处(M2/M4/M5/M6)完全同构。围挡本体不是残留料,
|
||||
> 而是 `front_collar_pocket` 的必然产物:它把座孔从 R30.15 扩到 R30.25,于是留下那圈
|
||||
> `r30.25..35`、6mm 高的环墙,比电机前端盖 Ø60.3 凸出 4.75mm;J2/J3/J4/J5 上还骑着 R35 前端
|
||||
> 压盖(Ø70 再凸 3.2mm)。我第一版把整个前端(定位环 + 压盖 + 腔口下沿)都切了 → **过度**:
|
||||
> 电机前端没了支撑面、M3 退回单面锁死,用户当场驳回。
|
||||
>
|
||||
> **【2026-10-07 修复:消除 P04/P06/P07/P08 误切缺角,完整补齐底壁】**:
|
||||
> 2026-10-05e 曾将 P05(M3) 的定位环敞口盒切推广到全机。但俯仰关节(J2/J4/J5)的局部 y=-3..40 坐标导致盒切向上侵入底壁实体 3mm(Z_axis+3),造成 P04、P06、P07、P08 的底壁和侧耳平白无故缺了一个 3mm x 6.1mm 的台阶缺角。现已彻底取消 P04/P06/P07/P08 处的切除,完整补齐底壁;该盒切仅保留于原本专用的 P05(M3) 敞口侧定位环(开关 `arm_model.M3_FENCE_OPEN_CUT`)。
|
||||
|
||||
> **原 P05(M3) 前端定位环敞口切**(开关 `arm_model.M3_FENCE_OPEN_CUT`,切刀 `cut_motor_fence()`):
|
||||
> 按各关节自己的局部坐标切 —— 局部 y 从 -3 到 +40、z 49.4~56.1。5 处的定位环都恰好落在
|
||||
> 局部 z 49.5~56(Y 轴电机 Y150~156、roll 电机 Z395.5~389),故一对参数通吃;敞口方向恒为
|
||||
> 局部 +y(实测 5 处背脊方位角全部落在局部角 180~360,无需逐件配置)。切口边界取 y=-3.0 而非 0:
|
||||
> 环壁在该处仍与背脊侧实体连通,切出来是真正的缺口而不是零厚度缝,单实体拓扑天然保持(5/5 实测
|
||||
> solids=1)。**背脊侧整圈定位环保留** —— 继续定位前端盖,并作为压盖唯一且足够强的支撑面
|
||||
> (4.75mm 宽 × 0.5mm 熔接带,本次完全不碰);压盖本体 / Ø37 让位孔 / 4×M3x8 沉孔 / 腔口下沿
|
||||
> 全部不动,电机仍是「后端板 4×M3x10 + 前端压盖 4×M3x8」前后双向面压。
|
||||
> 各件减重:P04 −3.4 / P05 −3.2 / P06 −3.2 / P07 −2.1 / P08 −3.0 cm³(P07 环壁因 2.0mm 关节
|
||||
> 间隙而较薄 r32.25~35,故减得最少)。
|
||||
>
|
||||
> **为什么不整圈削到与电机齐平(R35→R31.8)**:① 那圈壁与本体在两个方位与外壁**相切**,整圈
|
||||
> 切完那两片切向楔形(P05 实测 1.32mm³)会脱空成孤立碎块,破坏单实体拓扑;② 压盖根部只有
|
||||
> 0.5mm 熔接带,环削窄后连接面从 2.37mm² 掉到 0.78mm²,4×M3x8 的夹紧力会把压盖从本体上拉下来。
|
||||
|
||||
> ⚠️ **P01 / P02 是固定测试台架,不是机器人本体**。它在扫描中代表机械臂与躯干的连接
|
||||
> 界面——**臂撞立柱 = 臂撞躯干,是真约束,碰撞计算必须包含它**。但台架的位置与截面
|
||||
> 是一个假设,所以躯干侧的工作空间只和这个假设一样准。要改的是"真机躯干占位"这个
|
||||
> 输入,而不是改台架(改台架是改测试工装)。`sweep_v2.py --no-stand` 是诊断开关:
|
||||
> 排除台架重扫,给出不含躯干约束的自由空间行程,用来分辨某个限位是臂自身折叠产生的
|
||||
> 还是躯干挡的。**默认包含台架。**
|
||||
|
||||
> ⚠️ **关节电机力矩现状**:按 `torque_balance.py` 的实测(零件真实质心 + 可达姿态搜索),
|
||||
> **J1 肩抬举仅悬停自身臂身即需 3.84 N·m、J2 需 2.48 N·m**,已超出假定许用连续力矩
|
||||
> 2.0 N·m;带 0.5 kg 末端负载时 J1 达 6.14 N·m(307%)。详见 `docs/torque_report.md`。
|
||||
> 结论对电机额定参数敏感,该参数目前为工程假定(无数据手册),可用 `ARMT_CONT` 覆盖重算。
|
||||
>
|
||||
> 相反地,**结构本身不是瓶颈**:12 个剖面全部合格,最差的 P03 吊臂根座安全系数 9.25,
|
||||
> 最大挠度 1.6mm。减重救不了电机——J1 的 distal 载荷里 5 台电机占 65%(1.05kg),
|
||||
> 打印件只占 0.58kg,把 P07/P08/P09 全部减掉 J1 也只降到 2.54 N·m。
|
||||
>
|
||||
> **2026-10-02 手臂件减重**:按厚度场扫描(`scratch/thickness.py`)定位厚料后,
|
||||
> P04 立柱 / P06 过渡块 / P07 顶板改为薄壁槽与减重窗,P09 填充 100%→30%,
|
||||
> 手臂 6 件实心 1214→1061cm³、BOM 1506→1316g。按 gyroid 实际打印口径(墙 5×0.42mm
|
||||
> 以内 100%、深处 25~30%)估,手臂打印件 906→817g,**净省约 90g**。
|
||||
> 减重开度由 `scratch/tune_hollow.py` 按「减重区最差安全系数 ≥ 无槽基线 95%」选定
|
||||
> (第一版开太大,P06 曾掉到基线的 66%,收窄后全部回到 95~100%)。
|
||||
> 代价只有惯性矩小幅下降(P04 截面 I 降 3.6%、P06/P07 减重区强度回到满值),
|
||||
> 行程范围与碰撞检测完全不变,13/13 零件仍是单实体。
|
||||
|
||||
---
|
||||
|
||||
@@ -85,8 +177,20 @@ python src/sweep_v2.py
|
||||
|
||||
- 📘 **权威数据(工具自动生成)**:
|
||||
- [各关节行程与硬限位 travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json)(sweep_v2.py 扫描生成,修改几何后必须重扫)
|
||||
> 滑块范围 = **单关节**硬限位。组合折叠姿态下另有干涉约束,见 `travel_detail.json` 的
|
||||
> `combos` 字段(目前全部是工具法兰 P09 撞躯干占位 P01_A,属真约束);查看器运行期
|
||||
> 另有胶囊碰撞模型按姿态夹取。
|
||||
- [零件明细与质量属性 bom.json](file:///g:/Robot_v2/机器人手臂/docs/bom.json)(构建自动生成)
|
||||
- [静态干涉检测结果 collision.json](file:///g:/Robot_v2/机器人手臂/docs/collision.json)(构建自动生成)
|
||||
- [截面刚度与应力 strength.json](file:///g:/Robot_v2/机器人手臂/docs/strength.json)(strength_check.py 生成)
|
||||
- [关节力矩与负载能力 torque_report.md](file:///g:/Robot_v2/机器人手臂/docs/torque_report.md)(torque_balance.py 生成)
|
||||
- 🔬 **测试与校验工具**(src/ 目录):
|
||||
- sweep_v2.py — 权威动态碰撞检测与行程扫描
|
||||
- strength_check.py / torque_balance.py — 刚度、应力与力矩平衡校核(按需重新生成报告)
|
||||
- strength_check.py — 剖面刚度/应力校核(零件真实质心 + 可达姿态搜索,输出含挠度估算)
|
||||
- torque_balance.py — 关节电机力矩、惯量与负载能力校核(拓扑取自 human_layout,不手抄)
|
||||
- finalize_travel.py — 从 travel_detail 重建 travel.json(含组合卡死阈值提示)
|
||||
|
||||
> 三个校验脚本共用同一套零件表与运动学链(`strength_check.part_table / chain`,
|
||||
> 关节轴来自 `human_layout.py`),改几何后不存在"报告与模型不同步"的问题。
|
||||
> 环境变量:`ARMPAY`(末端负载 kg)、`ARMDYN`(动载系数)、`ARMINFILL`(实体率)、
|
||||
> `ARMMAT`(材料)、`ARMT_CONT`/`ARMT_PEAK`/`ARMT_ALLOW`(电机力矩)、`ARMT_ALPHA`(角加速度)。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""6 自由度人形机械臂:CadQuery 参数化建模、行程/干涉扫描、制造文件与查看器数据。
|
||||
|
||||
入口一律从仓根以 `python -m arm.xxx` 运行:`python -m` 会把仓根放进 sys.path,
|
||||
包内 import 因此直接可解析,不需要 pip install。
|
||||
"""
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""行程与干涉扫描、强度与力矩核算、可达包络采样与网格注入。
|
||||
"""
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""从 travel_detail.json 的单关节硬限位重新生成 docs/travel.json。
|
||||
|
||||
滑块范围 = 单关节硬限位(任意单关节推满零干涉,含 1° 安全余量);
|
||||
组合折叠约束保留在 travel_detail.json 的 combos 字段里,供报告引用。
|
||||
|
||||
注意 note 的措辞:单轴整周无干涉 **不等于** 任意组合姿态无干涉。sweep_v2 的组合扫描
|
||||
已经在 J1/J3/J6 上找到了真实卡死(工具法兰 P09 撞固定立柱 P01_A),例如 J3≈±162°。
|
||||
所以连续轴的 note 必须写明"单轴"并指向 combos,否则这份文件会误导人——查看器运行期
|
||||
另有胶囊碰撞模型按姿态夹取(ik-glue.js 的 dynLimit),但静态滑块范围按设计不压缩。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
|
||||
import os
|
||||
import sys
|
||||
from arm.analysis.sweep_v2 import CHAIN
|
||||
from arm.paths import DOCS
|
||||
|
||||
# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物
|
||||
# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。
|
||||
if __name__ != "__main__":
|
||||
raise RuntimeError(
|
||||
"finalize_travel 是脚本:顶层代码一执行就重写产物,不能 import。"
|
||||
"要用就跑 python -m arm.analysis.finalize_travel。")
|
||||
|
||||
d = json.load(open(DOCS + "/travel_detail.json", encoding="utf-8"))
|
||||
|
||||
# 组合扫描中,每个被扫轴真实出现卡死的最小绝对角
|
||||
combo_warn = {}
|
||||
for c in d.get("combos", []):
|
||||
if not c.get("jams"):
|
||||
continue
|
||||
ax = c["swept"]
|
||||
for a in c["jams"]:
|
||||
try:
|
||||
v = abs(float(a))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
combo_warn[ax] = min(combo_warn.get(ax, 1e9), v)
|
||||
|
||||
out = []
|
||||
for t in CHAIN[1:]:
|
||||
s = d["singles"][t]
|
||||
lo, hi = s["lo"], s["hi"]
|
||||
warn = combo_warn.get(t)
|
||||
warn_txt = (";组合折叠姿态下(其余关节同时处于各自极限)约 ±%.0f° 起与躯干占位"
|
||||
"(台架 P01 立柱)干涉,见 travel_detail.combos,"
|
||||
"运行期由查看器碰撞模型按姿态夹取" % warn) if warn is not None else ""
|
||||
if lo <= -179.9 and hi >= 179.9:
|
||||
out.append(dict(axis=t, lo=-180.0, hi=180.0, continuous=True, zero=0.0,
|
||||
combo_warn_deg=warn,
|
||||
note="单轴整周无干涉(滑块按单轴限位给出)%s" % (warn_txt or
|
||||
";组合扫描未发现该轴卡死")))
|
||||
else:
|
||||
lo_out = float(math.ceil(lo + 1.0)) if lo < -0.5 else 0.0
|
||||
hi_out = float(math.floor(hi - 1.0)) if hi > 0.5 else 0.0
|
||||
out.append(dict(axis=t, lo=lo_out, hi=hi_out, continuous=False, zero=0.0,
|
||||
combo_warn_deg=warn,
|
||||
note="单轴正向 +%d° / 反向 %d°,总行程 %d°(单关节硬限位,含相邻件与电机)%s"
|
||||
% (hi_out, lo_out, hi_out - lo_out, warn_txt)))
|
||||
print("%-3s %7.1f .. %7.1f -> %s .. %s%s"
|
||||
% (t, lo, hi, out[-1]["lo"], out[-1]["hi"],
|
||||
(" 组合卡死阈值 ±%.0f°" % warn) if warn is not None else ""))
|
||||
|
||||
json.dump(out, open(DOCS + "/travel.json", "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=1)
|
||||
d["final"] = out
|
||||
json.dump(d, open(DOCS + "/travel_detail.json", "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=1)
|
||||
print("rewrote docs/travel.json (single-joint hard stops) + updated travel_detail.json")
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成可达外包络的 ENV 零件(经纬网单件)追加进 web/mesh_arm.json:
|
||||
ENV_WL_包络经纬网 —— kind=line,LineSegments 渲染(经向 10°/条、纬向 8mm/环)
|
||||
包络 = rmax(x) 母线绕 J1 轴(平行 X,过 (y=128, z=610))整周回转;grp=root 不随关节动。"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from arm.paths import DATA, CACHE
|
||||
from arm import kinematics as HL
|
||||
|
||||
Y_AX = HL.Y_AXIS # 128
|
||||
Z_AX = HL.Z_RL # 610
|
||||
|
||||
|
||||
def main():
|
||||
d = np.load(CACHE + "/ws_cloud.npz")
|
||||
xs, rmax = d["xs"], d["rmax"]
|
||||
ok = ~np.isnan(rmax)
|
||||
xs, rmax = xs[ok], rmax[ok]
|
||||
|
||||
def grid(step_x, dphi_deg):
|
||||
"""经纬参数网格 -> verts(NX*NP,3), segs(扁平索引对), tris。"""
|
||||
xs_e = np.arange(xs.min(), xs.max() + 1e-9, step_x)
|
||||
rm_e = np.interp(xs_e, xs, rmax)
|
||||
nph = int(round(360.0 / dphi_deg))
|
||||
phg = np.linspace(0.0, 2.0 * math.pi, nph, endpoint=False)
|
||||
nx = len(xs_e)
|
||||
V = np.empty((nx * nph, 3))
|
||||
for i, x in enumerate(xs_e):
|
||||
r = rm_e[i]
|
||||
for j, ph in enumerate(phg):
|
||||
V[i * nph + j] = (x, Y_AX + r * math.cos(ph), Z_AX + r * math.sin(ph))
|
||||
segs = []
|
||||
for i in range(nx - 1): # 经线(沿 x)
|
||||
for j in range(nph):
|
||||
segs += [i * nph + j, (i + 1) * nph + j]
|
||||
for i in range(nx): # 纬线(沿 φ)
|
||||
for j in range(nph):
|
||||
j2 = (j + 1) % nph
|
||||
segs += [i * nph + j, i * nph + j2]
|
||||
tris = []
|
||||
for i in range(nx - 1):
|
||||
for j in range(nph):
|
||||
j2 = (j + 1) % nph
|
||||
a, b = i * nph + j, i * nph + j2
|
||||
c, e = (i + 1) * nph + j, (i + 1) * nph + j2
|
||||
tris += [a, c, b, b, c, e]
|
||||
return V, segs, tris
|
||||
|
||||
# 经纬网单件:经向 10°一条、纬向 8mm一环(无需贴片面,线自带罩体感)
|
||||
Vl, sl, _tl = grid(8.0, 10.0)
|
||||
|
||||
# 自证:包络尺寸应等于 ws_metrics 极值。
|
||||
# 【2026-10-05c 修正】原来是 <1.0 的硬比较,但网格 x 是从 xs.min() 起按 step_x
|
||||
# 对齐的(np.arange),最后一个采样点最多比真极值短一个步长 —— 一旦
|
||||
# (x_fwd - xs.min()) 不是 step_x 的整数倍,断言就会假失败(实测差 6.46mm,
|
||||
# 正好 < 8.0),导致包络注不进去、查看器里"可达包络"整个消失。改为
|
||||
# "不得超出、且至多少一个步长"这个真正成立的判据。
|
||||
step_x = 8.0
|
||||
met = json.load(open(CACHE + "/ws_metrics.json", encoding="utf-8"))
|
||||
assert Vl[:, 0].min() >= met["x_back"] - 1e-6, (Vl[:, 0].min(), met["x_back"])
|
||||
assert Vl[:, 0].min() <= met["x_back"] + step_x + 1e-6, (Vl[:, 0].min(), met["x_back"])
|
||||
assert Vl[:, 0].max() <= met["x_fwd"] + 1e-6, (Vl[:, 0].max(), met["x_fwd"])
|
||||
assert Vl[:, 0].max() >= met["x_fwd"] - step_x - 1e-6, (Vl[:, 0].max(), met["x_fwd"])
|
||||
rl = np.hypot(Vl[:, 1] - Y_AX, Vl[:, 2] - Z_AX)
|
||||
# rl.max() 就是 rmax(x) 网格插值后的峰值,与 met["r_max"](原始点云极值)之间
|
||||
# 只允许有插值/取整量级的差(实测 0.044mm),故给 1.0mm 容差。
|
||||
assert abs(rl.max() - met["r_max"]) < 1.0, (rl.max(), met["r_max"])
|
||||
assert np.all((Vl[:, 1] - Y_AX) ** 2 + (Vl[:, 2] - Z_AX) ** 2
|
||||
<= (np.interp(Vl[:, 0], xs, rmax) + 1e-6) ** 2), "line"
|
||||
print("包络经纬网: 顶点%d 线段%d(经10° x 纬8mm)" % (len(Vl), len(sl) // 2))
|
||||
|
||||
# ---- 追加进 mesh_arm.json ----
|
||||
path = DATA + "/mesh_arm.json"
|
||||
data = json.load(open(path, encoding="utf-8"))
|
||||
data["parts"] = [p for p in data["parts"] if not p["name"].startswith("ENV_")]
|
||||
mx = max(p["idx"] for p in data["parts"])
|
||||
note_ws = ("可达外包络(运动学,docs/travel.json 滑块限位下的全组合 FK 包络):"
|
||||
"r=rmax(x) 绕 J1 轴整周回转。高 %.0f~%.0f mm,前伸 %.0f / 后伸 %.0f mm,"
|
||||
"离 J1 轴最远 %.0f mm;包络内含死区(肩下折不进),多关节同极位由控制器避碰。"
|
||||
% (met["z_min"], met["z_max"], met["x_fwd"], met["x_back"], met["r_max"]))
|
||||
line = dict(name="ENV_WL_包络经纬网", cn="包络经纬网", v=[list(p) for p in Vl],
|
||||
s=sl, color=0x60a5fa, printed=False, vol=round(met["vol_dm3"] * 1e6, 1),
|
||||
note=note_ws + " 经纬线:经向 10°/条,纬向 8mm/环。",
|
||||
grp="root", jaw=0.0, ex=[0.0, 0.0, 0.0], idx=mx + 1,
|
||||
kind="line")
|
||||
data["parts"] += [line]
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
||||
os.replace(tmp, path)
|
||||
print("已写入", path, "大小 %.1f MB" % (os.path.getsize(path) / 1e6))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,494 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Structural check of the printed arm, using section properties measured from the SOLIDS.
|
||||
|
||||
Nothing here is hand-waved from a sketch dimension: for each critical cut plane the part is
|
||||
boolean-cut and the freshly created planar face is taken and integrated with
|
||||
BRepGProp.SurfaceProperties_s, so area, centroid and the second moments are the real
|
||||
section of the real printed part (including whatever the lightening windows left behind).
|
||||
|
||||
Why the allowables are so low: a printed part is not the material datasheet. Bending
|
||||
across layers, an un-filleted internal corner (Kt ~ 2.5 measured on square corners), and
|
||||
PLA creep under permanent static load together cut 35-50 MPa down to single digits, which
|
||||
is exactly the failure mode to design against. See docs/strength.md.
|
||||
|
||||
Run: python strength.py -> docs/strength.md (+ console table)
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
import cadquery as cq
|
||||
from arm.paths import OUT, DATA, DOCS
|
||||
from arm.cad.helpers import solids_of, bb, mesh_of
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from OCP.BRepGProp import BRepGProp
|
||||
from OCP.GProp import GProp_GProps
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2, gp_Vec
|
||||
from arm import kinematics as HL
|
||||
RHO = {"PLA": 1.24e-3, "motor_g": 210.0} # g/mm3 for printed, g per module
|
||||
# a slicer does not print solid: 3 perimeters + 30% infill lands near this fraction of
|
||||
# the CAD envelope, and using the solid mass would blame the part for plastic that is not there
|
||||
INFILL = float(os.environ.get("ARMINFILL", "0.38"))
|
||||
# allowable bending stress across the print layers, after notch + creep derating
|
||||
ALLOW = {"PLA": 12.0, "PETG": 20.0, "PA_CF": 30.0} # MPa
|
||||
# Young's modulus of the printed lattice, for the deflection estimate only
|
||||
EMOD = {"PLA": 3000.0, "PETG": 2100.0, "PA_CF": 8000.0} # MPa
|
||||
MAT = os.environ.get("ARMMAT", "PLA")
|
||||
PAYLOAD = float(os.environ.get("ARMPAY", "0.5")) # kg held at the fingertip
|
||||
DYN = float(os.environ.get("ARMDYN", "2.0")) # dynamic factor on the moving mass
|
||||
|
||||
G981 = 9.81
|
||||
GRIP_T = 12.0 # N*mm per motor is far too small to matter; the grip force
|
||||
# case is carried separately below as a normal force at the pad
|
||||
_cache = {}
|
||||
GRP = {"root": 0, "J1": 1, "J2": 2, "J3": 3, "J4": 4, "J5": 5, "J6": 6}
|
||||
|
||||
|
||||
def load(name):
|
||||
if name not in _cache:
|
||||
_cache[name] = cq.importers.importStep("%s/parts_step/%s.step" % (OUT, name)).val()
|
||||
return _cache[name]
|
||||
|
||||
|
||||
def volume(sh):
|
||||
p = GProp_GProps()
|
||||
BRepGProp.VolumeProperties_s(sh.wrapped, p)
|
||||
return p.Mass()
|
||||
|
||||
|
||||
def _caps_at(sh, n, o, t):
|
||||
"""The section faces of (solid ∩ thin slab), taken on the +normal side of the slab.
|
||||
|
||||
Two things this replaces. (1) 'Zero extent along the normal' on a HALF-SPACE cut also
|
||||
matches the part's own horizontal faces (top chord, gusset crowns) sitting anywhere above
|
||||
the plane - that over-read the shoulder fork by 2.4x (480 mm2 true vs 1155 reported).
|
||||
(2) The half-space Common outright fails on the filleted fork at z=600 (returns an empty
|
||||
shape) while a thin slab of the same solid cuts fine.
|
||||
A slab is also self-checking: its volume / t must equal the integrated area.
|
||||
"""
|
||||
c0 = sum(n[k] * o[k] for k in range(3))
|
||||
lo, hi = [], []
|
||||
for k in range(3):
|
||||
if n[k] > 0:
|
||||
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
|
||||
elif n[k] < 0:
|
||||
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
|
||||
else:
|
||||
lo.append(o[k] - 400.0); hi.append(o[k] + 400.0)
|
||||
box = BRepPrimAPI_MakeBox(gp_Pnt(*lo), gp_Pnt(*hi)).Shape()
|
||||
cut = BRepAlgoAPI_Common(sh.wrapped, box)
|
||||
cut.Build()
|
||||
if not cut.IsDone():
|
||||
return []
|
||||
off = c0 + t / 2.0
|
||||
out = []
|
||||
for f in cq.Shape(cut.Shape()).Faces():
|
||||
if f.geomType() != "PLANE" or f.Area() <= 1e-6:
|
||||
continue
|
||||
cx, cy, cz = f.Center().toTuple()
|
||||
if abs(n[0] * cx + n[1] * cy + n[2] * cz - off) > 1e-4:
|
||||
continue
|
||||
out.append(f)
|
||||
return out
|
||||
|
||||
|
||||
def section_props(sh, normal, origin, bend=2, size=400.0):
|
||||
"""Cut the solid with a thin slab and integrate the section face.
|
||||
|
||||
Returns (area mm2, Ixx mm4, Iyy mm4, cmax mm, bbox) about the face centroid, with the
|
||||
in-plane axes taken as (u, v) where u x v = normal.
|
||||
"""
|
||||
n = [float(x) for x in normal]
|
||||
o = [float(x) for x in origin]
|
||||
caps = []
|
||||
for t in (0.2, 0.05, 1.0):
|
||||
for nudge in (0.0, 0.05, -0.05, 0.25, -0.25):
|
||||
oo = [o[k] + nudge * n[k] for k in range(3)]
|
||||
caps = _caps_at(sh, n, oo, t)
|
||||
if caps:
|
||||
break
|
||||
if caps:
|
||||
break
|
||||
if not caps:
|
||||
return None
|
||||
plane_ax = [k for k in (0, 1, 2) if abs(n[k]) < 0.5]
|
||||
p_ax, q_ax = plane_ax
|
||||
bend_ax = bend if bend in plane_ax else q_ax
|
||||
far_ax = p_ax if bend_ax == q_ax else q_ax
|
||||
A = Sv = Suu = Svv = S = 0.0
|
||||
for f in caps:
|
||||
verts, tris = mesh_of(f, 0.02)
|
||||
fa = fu = fv = fuu = fvv = 0.0
|
||||
for t in tris:
|
||||
(u1, v1), (u2, v2), (u3, v3) = [(verts[k][p_ax], verts[k][q_ax]) for k in t]
|
||||
ar = ((u2 - u1) * (v3 - v1) - (u3 - u1) * (v2 - v1)) / 2.0
|
||||
fa += ar # SIGNED: inner wires (holes) cancel
|
||||
fu += ar * (u1 + u2 + u3) / 3.0
|
||||
fv += ar * (v1 + v2 + v3) / 3.0
|
||||
fvv += ar * (v1**2 + v2**2 + v3**2 + v1*v2 + v2*v3 + v3*v1) / 6.0
|
||||
fuu += ar * (u1**2 + u2**2 + u3**2 + u1*u2 + u2*u3 + u3*u1) / 6.0
|
||||
if fa < 0: # face normal opposed to the cut normal
|
||||
fa, fu, fv, fvv, fuu = -fa, -fu, -fv, -fvv, -fuu
|
||||
A += fa; S += fu; Sv += fv; Svv += fvv; Suu += fuu
|
||||
if A <= 1e-9:
|
||||
return None
|
||||
uc, vc = S / A, Sv / A
|
||||
best = max(caps, key=lambda f: f.Area()) # only for the bounding box below
|
||||
allb = [f.BoundingBox() for f in caps]
|
||||
lo = [min(getattr(b, ("xmin", "ymin", "zmin")[k]) for b in allb) for k in range(3)]
|
||||
hi = [max(getattr(b, ("xmax", "ymax", "zmax")[k]) for b in allb) for k in range(3)]
|
||||
cmax = max(hi[far_ax] - uc if far_ax == p_ax else hi[far_ax] - vc,
|
||||
uc - lo[far_ax] if far_ax == p_ax else vc - lo[far_ax]) / 1.0
|
||||
# distance from the neutral axis to the extreme fibre, measured in the bending direction
|
||||
ext = (hi[far_ax] - lo[far_ax])
|
||||
cen = uc if far_ax == p_ax else vc
|
||||
cmax = max(hi[far_ax] - cen, cen - lo[far_ax])
|
||||
I = (Svv - A * vc * vc) if far_ax == q_ax else (Suu - A * uc * uc)
|
||||
return dict(A=A, I=I, c=cmax, bend=bend_ax, centroid=(uc, vc))
|
||||
|
||||
|
||||
def arm_masses():
|
||||
"""Everything distal to each joint, in grams, from the model itself."""
|
||||
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||||
tot = {}
|
||||
for p in meta["parts"]:
|
||||
if not os.path.exists("%s/parts_step/%s.step" % (OUT, p["name"])):
|
||||
continue # 螺钉实例 / 可达包络网格等非 CAD 构件没有 STEP,跳过
|
||||
sh = load(p["name"])
|
||||
v = volume(sh) / 1000.0
|
||||
g = (v * RHO["PLA"] * 1000.0 * INFILL) if p["printed"] else RHO["motor_g"]
|
||||
tot[p["name"]] = (g, p["grp"])
|
||||
out = {}
|
||||
for k, jid in enumerate(TREE[1:], 1):
|
||||
down = set(TREE[k:])
|
||||
out[jid] = sum(g for g, grp in tot.values() if grp in down)
|
||||
out["all"] = sum(g for g, _ in tot.values())
|
||||
return out, tot
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 零件表:质量 + 零位世界质心
|
||||
def _mesh_vol_cg(v, t):
|
||||
"""四面体法求闭合三角网格的体积与体积加权质心。
|
||||
|
||||
网格顶点已在零位世界系(mesh_arm.json 由 arm_model 直接落盘),所以质心就是零件
|
||||
在装配中的真实位置。实测体积与 STEP/BOM 差 <0.1%(电机包络 1.7%),可作载荷输入。
|
||||
"""
|
||||
vol = mx = my = mz = 0.0
|
||||
for a, b, c in t:
|
||||
x1, y1, z1 = v[a]
|
||||
x2, y2, z2 = v[b]
|
||||
x3, y3, z3 = v[c]
|
||||
det = x1 * (y2 * z3 - y3 * z2) - x2 * (y1 * z3 - y3 * z1) + x3 * (y1 * z2 - y2 * z1)
|
||||
vol += det
|
||||
mx += (x1 + x2 + x3) * det
|
||||
my += (y1 + y2 + y3) * det
|
||||
mz += (z1 + z2 + z3) * det
|
||||
if vol <= 0.0:
|
||||
return 0.0, None
|
||||
V = vol / 6.0
|
||||
return V, (mx / (24.0 * V), my / (24.0 * V), mz / (24.0 * V))
|
||||
|
||||
|
||||
def part_table(payload_kg=None):
|
||||
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
|
||||
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||
rows = []
|
||||
for p in meta["parts"]:
|
||||
nm = p["name"]
|
||||
if nm.startswith("ENV_") or not p.get("v") or not p.get("t") or p.get("vol", 0) <= 0:
|
||||
continue # 可达包络网格 / 无实体螺钉实例,不承载
|
||||
V, cg = _mesh_vol_cg(p["v"], p["t"])
|
||||
if V <= 0.0 or cg is None:
|
||||
continue
|
||||
if p.get("printed"):
|
||||
m = V * RHO["PLA"] * INFILL
|
||||
elif re.match(r"^M\d", nm):
|
||||
m = RHO["motor_g"]
|
||||
else:
|
||||
continue
|
||||
rows.append([nm, m, cg, GRP.get(p["grp"], 0)])
|
||||
if payload_kg:
|
||||
rows.append(["PAYLOAD", payload_kg * 1000.0,
|
||||
(HL.X_ARM, HL.Y_AXIS, HL.Z_TIP), 6])
|
||||
return rows
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 运动学:产品外参链
|
||||
def _rodrigues(deg, d, p):
|
||||
"""绕世界轴 d 过点 p 转 deg -> (R, t),使 x -> R x + t。"""
|
||||
n = math.sqrt(sum(c * c for c in d)) or 1.0
|
||||
x, y, z = d[0] / n, d[1] / n, d[2] / n
|
||||
a = math.radians(deg)
|
||||
c, s = math.cos(a), math.sin(a)
|
||||
t1 = 1.0 - c
|
||||
R = [[t1 * x * x + c, t1 * x * y - s * z, t1 * x * z + s * y],
|
||||
[t1 * x * y + s * z, t1 * y * y + c, t1 * y * z - s * x],
|
||||
[t1 * x * z - s * y, t1 * y * z + s * x, t1 * z * z + c]]
|
||||
t = [p[i] - (R[i][0] * p[0] + R[i][1] * p[1] + R[i][2] * p[2]) for i in range(3)]
|
||||
return R, t
|
||||
|
||||
|
||||
def chain(ang):
|
||||
"""acc[g] = (R, t):第 g 组零件的刚体变换。
|
||||
|
||||
acc[g] = acc[g-1] o R_g(产品外参,右侧先作用),与 sweep_v2.accum_trsf、
|
||||
查看器嵌套 pivot 三者一致。
|
||||
"""
|
||||
R = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
|
||||
t = [0.0, 0.0, 0.0]
|
||||
out = [(R, t)]
|
||||
for j in range(1, 7):
|
||||
ax = HL.AXIS["J%d" % j]
|
||||
Rj, tj = _rodrigues(ang.get(j, 0.0), ax["d"], ax["rear"])
|
||||
Rn = [[sum(R[i][k] * Rj[k][j2] for k in range(3)) for j2 in range(3)] for i in range(3)]
|
||||
tn = [sum(R[i][k] * tj[k] for k in range(3)) + t[i] for i in range(3)]
|
||||
R, t = Rn, tn
|
||||
out.append((R, t))
|
||||
return out
|
||||
|
||||
|
||||
def _xf(T, c):
|
||||
R, t = T
|
||||
return [sum(R[i][k] * c[k] for k in range(3)) + t[i] for i in range(3)]
|
||||
|
||||
|
||||
def _dir(T, v):
|
||||
"""只取旋转部分——方向向量绝不能带平移,否则投影出的"力矩"会被平移量放大。"""
|
||||
R = T[0]
|
||||
return [sum(R[i][k] * v[k] for k in range(3)) for i in range(3)]
|
||||
|
||||
|
||||
def _cross(a, b):
|
||||
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
|
||||
|
||||
def joint_loads(parts, payload_kg=None):
|
||||
"""在可达姿态网格上搜索,返回每个关节的:
|
||||
|
||||
F 该关节以下的总重力 (N)(与姿态无关)
|
||||
Mxyz 重力对关节轴点取矩后,三个全局分量各自的最大绝对值 (N*mm)
|
||||
tau 重力绕该关节自身轴的最大力矩 (N*mm) —— 电机真正要出的力
|
||||
Lp 出现 tau 最大值时,TCP 到该关节轴点的距离 (mm)
|
||||
|
||||
为什么搜索姿态:力臂不是常数。J1 是绕 X 的自转轴,手臂在轴正下方时重力对它
|
||||
只产生绕 X 的扭矩(不产生弯曲),手臂摆到侧方时扭矩最大。旧算法把全部 distal
|
||||
质量当作集中在指尖,量级完全错掉。
|
||||
"""
|
||||
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
|
||||
|
||||
def picks(j):
|
||||
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
|
||||
if t.get("continuous"):
|
||||
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
|
||||
lo, hi = t["lo"], t["hi"]
|
||||
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
|
||||
|
||||
grid = [picks(j) for j in range(1, 7)]
|
||||
res = {}
|
||||
for j in range(1, 7):
|
||||
res["J%d" % j] = dict(F=0.0, Mxyz=[0.0, 0.0, 0.0], tau=0.0, Lp=0.0, I=0.0, Iarm=0.0)
|
||||
|
||||
import itertools
|
||||
for combo in itertools.product(*grid):
|
||||
ang = {k + 1: v for k, v in enumerate(combo)}
|
||||
acc = chain(ang)
|
||||
pos = [(nm, m, _xf(acc[g], cg), g) for nm, m, cg, g in parts]
|
||||
tcp = None
|
||||
for nm, m, c, g in pos:
|
||||
if nm == "PAYLOAD":
|
||||
tcp = c
|
||||
for j in range(1, 7):
|
||||
ax = HL.AXIS["J%d" % j]
|
||||
pj = _xf(acc[j - 1], ax["rear"])
|
||||
dj = _dir(acc[j - 1], ax["d"])
|
||||
F = M = [0.0, 0.0, 0.0]
|
||||
wsum = 0.0
|
||||
I = Iarm = 0.0
|
||||
for _nm, m, c, g in pos:
|
||||
if g < j:
|
||||
continue
|
||||
w = m * G981 / 1000.0
|
||||
wsum += w
|
||||
r = [c[i] - pj[i] for i in range(3)]
|
||||
proj = sum(r[i] * dj[i] for i in range(3))
|
||||
perp2 = sum(r[i] * r[i] for i in range(3)) - proj * proj
|
||||
I += m * perp2 / 1e9 # g*mm^2 -> kg*m^2
|
||||
if _nm != "PAYLOAD":
|
||||
Iarm += m * perp2 / 1e9
|
||||
rc = _cross(r, [0.0, 0.0, -w])
|
||||
for i in range(3):
|
||||
M[i] += rc[i]
|
||||
r = res["J%d" % j]
|
||||
r["F"] = max(r["F"], wsum)
|
||||
for i in range(3):
|
||||
r["Mxyz"][i] = max(r["Mxyz"][i], abs(M[i]))
|
||||
tau = abs(M[0] * dj[0] + M[1] * dj[1] + M[2] * dj[2])
|
||||
if tau > r["tau"]:
|
||||
r["tau"] = tau
|
||||
r["Lp"] = math.dist(tcp, pj) if tcp else 0.0
|
||||
r["I"] = max(r["I"], I)
|
||||
r["Iarm"] = max(r["Iarm"], Iarm)
|
||||
return res
|
||||
|
||||
|
||||
def section_moment(jid, bend_axis, ref, refgrp, parts):
|
||||
"""截面绕自身形心、沿 bend_axis 的弯矩最大值 (N*mm),以及该姿态下 TCP 到该
|
||||
截面的距离 (mm)(挠度估算用)。
|
||||
|
||||
参考点取【剖面形心】而不是关节轴:这两个参考点差得很远。P03 在 z=480 的剖面
|
||||
形心 x≈-111,而臂身所有零件质心 x≈+19.5,真实力臂 130mm;若误用关节轴点
|
||||
(J1 在 x=-51)只会得到 70mm,把弯矩低估 1.85 倍。
|
||||
"""
|
||||
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
|
||||
jj = int(jid[1])
|
||||
|
||||
def picks(j):
|
||||
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
|
||||
if t.get("continuous"):
|
||||
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
|
||||
lo, hi = t["lo"], t["hi"]
|
||||
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
|
||||
|
||||
import itertools
|
||||
best = 0.0
|
||||
best_L = 0.0
|
||||
for combo in itertools.product(*[picks(j) for j in range(1, 7)]):
|
||||
acc = chain({k + 1: v for k, v in enumerate(combo)})
|
||||
rw = _xf(acc[refgrp], ref)
|
||||
s = 0.0
|
||||
tcp = rw
|
||||
for nm, m, cg, g in parts:
|
||||
if g < jj:
|
||||
continue
|
||||
c = _xf(acc[g], cg)
|
||||
if nm == "PAYLOAD":
|
||||
tcp = c
|
||||
s += m * (c[bend_axis] - rw[bend_axis])
|
||||
if abs(s) > best:
|
||||
best = abs(s)
|
||||
best_L = math.dist(tcp, rw)
|
||||
return best * G981 / 1000.0, best_L
|
||||
|
||||
|
||||
# critical cuts: (label, part, plane normal, plane origin, which load case owns it)
|
||||
def cuts():
|
||||
"""(label, part, cut-plane normal, point, joint whose load it carries, bend axis)
|
||||
bend axis: 0=X (roll of the shoulder/forearm), 1=Y (every pitch joint), 2=Z."""
|
||||
Y = HL.Y_AXIS
|
||||
return [
|
||||
("吊臂根座 @z=480", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 55.0, 480.0), "J1", 1),
|
||||
("吊臂颊板 @z=560", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 100.0, 560.0), "J1", 1),
|
||||
("肩叉腹板 @z=560", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 560.0), "J2", 1),
|
||||
("肩叉腹板 @z=600", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 600.0), "J2", 1),
|
||||
("大臂旋转座横梁 @z=480", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 480.0), "J2", 1),
|
||||
("大臂旋转座套筒 @z=440", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
|
||||
("大臂骨架中段 @z=350", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
|
||||
("大臂骨架抱箍 @z=325", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
|
||||
# 【2026-10-02 补】P06 原减重窗在 Z358~374,原有剖面(350/325)全在它下面,
|
||||
# 根本校核不到被挖的区域。z=365 穿过该段过渡块,承载同 J3。
|
||||
# 【2026-10-03】减重窗已取消、该段恢复实心,此剖面保留为该高度的安全裕度监控点。
|
||||
("大臂骨架过渡块 @z=365", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 365.0), "J3", 1),
|
||||
("小臂骨架中段 @z=245", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
|
||||
("小臂骨架抱箍 @z=195", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
|
||||
# 【2026-10-02 补】P07 减重槽在 Z255~280,同理原有剖面(245/195)没覆盖到。
|
||||
# z=270 穿过槽体本身,承载同 J4;z=280 是槽口所在的顶板上边缘,壁最薄。
|
||||
("小臂骨架减重槽 @z=270", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 270.0), "J4", 1),
|
||||
("小臂骨架减重槽口 @z=280", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 280.0), "J4", 1),
|
||||
("腕偏转座根部 @z=150", "P08_腕偏转座_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
|
||||
# P09 包络 z=93..121,剖面取 107(法兰中截面)。原 -55 落在零件之外,
|
||||
# 剖切返回空,工具法兰其实一直没被校核过。
|
||||
("工具法兰 @z=107", "P09_工具法兰_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
|
||||
]
|
||||
|
||||
|
||||
MOMENT = { # worst reachable bending moment per joint, N*mm
|
||||
# computed in main() from the reachable pose + masses
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parts = part_table(PAYLOAD)
|
||||
grpof = {nm: g for nm, _, _, g in parts}
|
||||
jl = joint_loads(parts)
|
||||
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
|
||||
masses["all"] = sum(m for _, m, _, _ in parts)
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
def compute_cut(c_item):
|
||||
lab, part, n, o, jid, bend = c_item
|
||||
sh = load(part)
|
||||
sp = section_props(sh, n, o, bend)
|
||||
if not sp:
|
||||
return (lab, part, None, jid)
|
||||
# 剖面形心的三维重建 + 弯矩所绕的轴
|
||||
nvec = [float(x) for x in n]
|
||||
p_ax, q_ax = [k for k in (0, 1, 2) if abs(nvec[k]) < 0.5]
|
||||
bend_ax = sp["bend"]
|
||||
far_ax = p_ax if bend_ax == q_ax else q_ax # I 绕 bend_ax、c 沿 far_ax
|
||||
ref = list(o)
|
||||
ref[p_ax] += sp["centroid"][0]
|
||||
ref[q_ax] += sp["centroid"][1]
|
||||
M, L = section_moment(jid, far_ax, ref, grpof.get(part, 0), parts)
|
||||
M *= DYN
|
||||
F = jl[jid]["F"] * DYN
|
||||
I, c = sp["I"], sp["c"]
|
||||
sigma = M * c / max(I, 1e-6)
|
||||
tau = 1.5 * F / sp["A"]
|
||||
defl = M * L * L / (2.0 * EMOD[MAT] * I) if I > 1e-6 and L > 0 else 0.0
|
||||
return (lab, part, dict(sp, M=M, F=F, sigma=sigma, tau=tau, L=L, defl=defl,
|
||||
bend_axis=bend_ax, SF=ALLOW[MAT] / sigma if sigma > 0 else 99.0), jid)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=min(13, os.cpu_count() or 4)) as executor:
|
||||
rows = list(executor.map(compute_cut, cuts()))
|
||||
print("材料 %s 许用弯曲 %g MPa(跨层、含缺口与蠕变折减) 弹性模量 %g MPa 末端负载 %g kg 动载系数 %g"
|
||||
% (MAT, ALLOW[MAT], EMOD[MAT], PAYLOAD, DYN))
|
||||
print("载荷模型:每个零件按真实质心(网格体积加权)随姿态变换,在可达姿态网格上取弯矩最大值;"
|
||||
"参考点为剖面形心\n")
|
||||
print("打印件按 %d%% 实体率计质量(壁+填充),电机按 %.0f g/台" % (INFILL * 100, RHO["motor_g"]))
|
||||
print("%-24s %-9s %7s %10s %6s %9s %7s %6s %7s %7s"
|
||||
% ("截面", "零件", "面积mm2", "I mm4", "c mm", "M N*mm", "sigma", "SF", "挠度mm", "需Z mm3"))
|
||||
bad = []
|
||||
for lab, part, d, jid in rows:
|
||||
if not d:
|
||||
print("%-24s %-9s 剖切失败" % (lab, part))
|
||||
continue
|
||||
Zreq = d["M"] / ALLOW[MAT] * 1.5 # section modulus x1.5 SF
|
||||
print("%-24s %-9s %7.0f %10.0f %6.1f %9.0f %7.2f %6.2f %7.1f %7.0f%s"
|
||||
% (lab, part[:9], d["A"], d["I"], d["c"], d["M"], d["sigma"], d["SF"], d["defl"],
|
||||
Zreq, " <<< 不足" if d["SF"] < 1.5 else ""))
|
||||
if d["SF"] < 1.5:
|
||||
bad.append((lab, part, d))
|
||||
print("\n各关节以下质量(g, 含电机与末端负载): "
|
||||
+ " ".join("%s=%.0f" % (k, v) for k, v in masses.items() if k != "all"))
|
||||
print("合计 %.0f g (其中末端负载 %.0f g)" % (masses["all"], PAYLOAD * 1000))
|
||||
print("\n各关节电机需出的重力力矩(绕自身轴,姿态搜索最大值):")
|
||||
for j in range(1, 7):
|
||||
r = jl["J%d" % j]
|
||||
print(" J%d %6.2f N·m (该关节以下总重 %.1f N,力臂 %.0f mm)"
|
||||
% (j, r["tau"] / 1000.0, r["F"], r["Lp"]))
|
||||
io.open(DOCS + "/strength.json", "w", encoding="utf-8").write(
|
||||
json.dumps([dict(截面=a, 零件=b, 关节=d, **(c or {})) for a, b, c, d in rows],
|
||||
ensure_ascii=False, indent=1, default=float))
|
||||
return bad
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
b = main()
|
||||
print("\n不合格截面 %d 处" % len(b))
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
@@ -23,8 +23,12 @@
|
||||
- 预剪枝:按关节轴计算每对零件的轴向/径向区间,区间不相交的零件对在该关节的
|
||||
任何角度都不可能碰撞,直接跳过(对单关节扫描是精确安全的)。
|
||||
|
||||
运行:python src/sweep_v2.py [--workers 20] [--step 2] [--combo-step 6]
|
||||
[--no-combos] [--joints J2,J4] [--no-gjk]
|
||||
台架(P01 立柱 / P02 底盘)代表躯干与机身的连接界面,默认【参与】碰撞计算——臂撞立柱
|
||||
就是臂撞躯干,是真约束。--no-stand 仅用于诊断:排除台架后重扫,给出"不含躯干约束"的
|
||||
自由空间行程,用来分辨某个限位是臂自身折叠产生的还是躯干挡的(台架位置本身是假设,
|
||||
改它等于改测试工装,不等于改设计)。
|
||||
运行:python -m arm.analysis.sweep_v2 [--workers 20] [--step 2] [--combo-step 6]
|
||||
[--no-combos] [--joints J2,J4] [--no-gjk] [--no-stand]
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
@@ -33,19 +37,17 @@ import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import multiprocessing as mp
|
||||
|
||||
import arm_build as AB
|
||||
from arm.cad import arm_build as AB
|
||||
import cadquery as cq
|
||||
from armcore import place, bb, DOCS
|
||||
|
||||
from arm import kinematics as HL
|
||||
from arm.paths import DOCS
|
||||
from arm.cad.helpers import place, bb
|
||||
VOL_MIN = 2.0 # mm3,超过即计为干涉
|
||||
CHAIN = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||||
MOUNT_PAIRS = {
|
||||
@@ -54,13 +56,14 @@ MOUNT_PAIRS = {
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体30%_墙4底5_竖打", "M6_J6"),
|
||||
}
|
||||
|
||||
# 网格化预筛参数:外扩必须 ≥ 网格化偏差,判"分离"才保守可信
|
||||
MESH_TOL = 0.5 # mm,零件网格化容差
|
||||
MESH_MARGIN = 0.8 # mm,凸包安全外扩(> MESH_TOL 保证网格内缩不致漏判)
|
||||
USE_GJK = True # 进程内全局;工作进程由 initializer 传入
|
||||
INCLUDE_FIXTURE = True # True=把 P01 立柱/P02 底盘(代表躯干)算进碰撞;--no-stand 置 False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 网格化 + GJK
|
||||
@@ -165,11 +168,15 @@ def _do_simplex(s):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 模型与上下文
|
||||
def load_model():
|
||||
import arm_model as AM
|
||||
def load_model(include_fixture=True):
|
||||
"""include_fixture=False 时把 P01 立柱 / P02 底盘(固定测试台架,非本体)排除在
|
||||
碰撞配对之外——用来得到真机工作空间而不是台架工作空间。见 human_layout.FIXTURE_PREFIX。"""
|
||||
from arm import build_model as AM
|
||||
JS = AM.build()
|
||||
parts = []
|
||||
for p in AB.PARTS:
|
||||
if not include_fixture and HL.is_fixture(p["name"]):
|
||||
continue
|
||||
g = CHAIN.index(AB.GRP.get(p["name"], "root"))
|
||||
lo, hi = bb(p["shape"])
|
||||
corners = [(x, y, z) for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])]
|
||||
@@ -212,8 +219,8 @@ def axis_intervals(parts, rear, d):
|
||||
return out
|
||||
|
||||
|
||||
def make_ctx():
|
||||
parts, joints = load_model()
|
||||
def make_ctx(include_fixture=True):
|
||||
parts, joints = load_model(include_fixture)
|
||||
impossible = {}
|
||||
for k in range(1, len(CHAIN)):
|
||||
rear, d = joints[CHAIN[k]]
|
||||
@@ -395,10 +402,10 @@ _POOL = None
|
||||
_WORKERS = 4
|
||||
|
||||
|
||||
def _init_worker(use_gjk=True):
|
||||
def _init_worker(use_gjk=True, include_fixture=True):
|
||||
global _CTX, USE_GJK
|
||||
USE_GJK = bool(use_gjk)
|
||||
_CTX = make_ctx()
|
||||
_CTX = make_ctx(include_fixture)
|
||||
|
||||
|
||||
def _task(task):
|
||||
@@ -410,7 +417,7 @@ def get_pool(workers):
|
||||
global _POOL, _WORKERS
|
||||
_WORKERS = workers
|
||||
if _POOL is None and workers > 1:
|
||||
_POOL = mp.Pool(workers, initializer=_init_worker, initargs=(USE_GJK,))
|
||||
_POOL = mp.Pool(workers, initializer=_init_worker, initargs=(USE_GJK, INCLUDE_FIXTURE))
|
||||
return _POOL
|
||||
|
||||
|
||||
@@ -448,7 +455,7 @@ def eval_angles(angles_list, workers, chunk=None, label="姿态"):
|
||||
def _ctx_single():
|
||||
global _CTX
|
||||
if _CTX is None:
|
||||
_CTX = make_ctx()
|
||||
_CTX = make_ctx(INCLUDE_FIXTURE)
|
||||
return _CTX
|
||||
|
||||
|
||||
@@ -555,6 +562,9 @@ def main():
|
||||
if "--no-gjk" in sys.argv:
|
||||
global USE_GJK
|
||||
USE_GJK = False
|
||||
if "--no-stand" in sys.argv:
|
||||
global INCLUDE_FIXTURE
|
||||
INCLUDE_FIXTURE = False
|
||||
# 内存感知的默认进程数:每个工作进程预留 ~2.5GB 峰值(建模+布尔瞬时开销)
|
||||
avail_gb = 16.0
|
||||
try:
|
||||
@@ -691,6 +701,7 @@ def main():
|
||||
open(DOCS + "/travel.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
detail = dict(generated=time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
vol_min=VOL_MIN, step=step, combo_step=combo_step, workers=workers,
|
||||
fixture_included=INCLUDE_FIXTURE,
|
||||
singles={t: dict(lo=round(singles[t]["lo"], 2), hi=round(singles[t]["hi"], 2),
|
||||
jams={str(a): v for a, v in singles[t]["jams"].items()})
|
||||
for t in singles},
|
||||
@@ -706,3 +717,10 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""关节电机力矩与负载能力校核。
|
||||
|
||||
与旧版的区别(这些是旧版实际出错的地方):
|
||||
|
||||
1. 拓扑。旧版把关节表手抄成 "J3 肘俯仰 / J4 前臂自转 / J5 腕俯仰 / J6 夹爪",
|
||||
并据此虚构了一个夹爪机构和 135mm 大臂——与 human_layout.py 的实际拓扑
|
||||
(J3 大臂自转 / J4 肘俯仰 / J6 腕自转)完全不符,整份报告的结论都是错的。
|
||||
本版不再复制拓扑:零件表、关节轴、运动学链全部复用 strength_check,
|
||||
而 strength_check 的轴又直接来自 human_layout,单一事实源。
|
||||
2. 力臂。旧版把每个关节的质量与力臂写成硬编码常数(J2 = 1.75kg @ 464mm),
|
||||
几何一改就失效且不会报错。本版按每个零件的真实质心、在可达姿态网格上
|
||||
搜索最大重力力矩。
|
||||
3. 惯量。旧版用一组手填的 kg·m²。本版按 Σm·d²(对关节轴的平行轴距离)
|
||||
在同一姿态网格上取最大值。零件自身转动惯量未计入,对大臂量级的关节
|
||||
是小修正,已在报告中标注。
|
||||
|
||||
电机参数:仓库内没有原始数据手册,以下为工程假定,可用环境变量覆盖。
|
||||
ARMT_CONT 连续额定输出力矩 (N·m)
|
||||
ARMT_PEAK 峰值堵转输出力矩 (N·m)
|
||||
ARMT_ALLOW 封闭塑料外壳热衰减后的许用连续力矩 (N·m)
|
||||
ARMT_ALPHA 角加速度 (rad/s²)
|
||||
ARMPAY 末端负载 (kg)
|
||||
|
||||
输出:docs/torque_balance.json + docs/torque_report.md
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
from arm import kinematics as HL
|
||||
from arm.paths import DOCS
|
||||
from arm.analysis import strength_check as SC
|
||||
G = 9.81
|
||||
T_CONT = float(os.environ.get("ARMT_CONT", "2.5"))
|
||||
T_PEAK = float(os.environ.get("ARMT_PEAK", "8.0"))
|
||||
T_ALLOW = float(os.environ.get("ARMT_ALLOW", "2.0"))
|
||||
ALPHA = float(os.environ.get("ARMT_ALPHA", "5.0"))
|
||||
PAYLOAD = float(os.environ.get("ARMPAY", "0.5"))
|
||||
|
||||
# 关节名与轴向,全部取自 human_layout(不手抄)
|
||||
JOINT_CN = {t[0]: t[1] for t in HL.JOINTS}
|
||||
JOINT_D = {t[0]: t[3] for t in HL.JOINTS}
|
||||
|
||||
|
||||
def _axis_kind(tag):
|
||||
d = JOINT_D[tag]
|
||||
if abs(d[2]) == 1:
|
||||
return "roll(绕Z)"
|
||||
if abs(d[0]) == 1:
|
||||
return "roll(绕X)"
|
||||
return "pitch(绕Y)"
|
||||
|
||||
|
||||
def analyze(payload_kg=PAYLOAD):
|
||||
parts = SC.part_table(payload_kg)
|
||||
jl = SC.joint_loads(parts)
|
||||
jl0 = SC.joint_loads(SC.part_table(0.0)) # 去掉末端负载,用于反推负载能力
|
||||
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
|
||||
arm_mass = {k: v - payload_kg * 1000.0 for k, v in masses.items()}
|
||||
|
||||
res = {"assumptions": {
|
||||
"motor": "60mm 方形标准关节电机模组,2 级行星减速",
|
||||
"T_cont_Nm": T_CONT, "T_peak_Nm": T_PEAK, "T_allow_Nm": T_ALLOW,
|
||||
"alpha_rad_s2": ALPHA, "payload_kg": payload_kg,
|
||||
"note": "电机参数为工程假定,仓库内无数据手册;结论对此敏感,见报告第 4 节",
|
||||
}, "joints": {}}
|
||||
|
||||
for j in range(1, 7):
|
||||
tag = "J%d" % j
|
||||
r = jl[tag]
|
||||
tau_g = r["tau"] / 1000.0 # N*m,重力
|
||||
tau_dyn = r["I"] * ALPHA # N*m,惯性
|
||||
tau_g_arm = jl0[tag]["tau"] / 1000.0 # 去掉负载后的重力力矩
|
||||
req_static = tau_g
|
||||
req_peak = tau_g + tau_dyn
|
||||
res["joints"][tag] = {
|
||||
"cn": JOINT_CN[tag], "axis": _axis_kind(tag),
|
||||
"distal_mass_kg": round(masses[tag] / 1000.0, 3),
|
||||
"arm_mass_kg": round(arm_mass[tag] / 1000.0, 3),
|
||||
"weight_N": round(r["F"], 1),
|
||||
"reach_tip_mm": round(r["Lp"], 0),
|
||||
"tau_gravity_Nm": round(tau_g, 3),
|
||||
"tau_gravity_arm_only_Nm": round(tau_g_arm, 3),
|
||||
"I_distal_kgm2": round(r["I"], 5),
|
||||
"tau_dynamic_Nm": round(tau_dyn, 3),
|
||||
"tau_required_static_Nm": round(req_static, 3),
|
||||
"tau_required_peak_Nm": round(req_peak, 3),
|
||||
"duty_vs_Tcont_pct": round(100.0 * req_static / T_CONT, 1),
|
||||
"duty_vs_Tallow_pct": round(100.0 * req_static / T_ALLOW, 1),
|
||||
"peak_margin_x": round(T_PEAK / req_peak, 2) if req_peak > 1e-3 else None,
|
||||
"status": "OK" if (req_static <= T_ALLOW and req_peak <= T_PEAK) else "OVERLOAD",
|
||||
}
|
||||
return res, jl0, jl
|
||||
|
||||
|
||||
def max_payload(res):
|
||||
"""在许用连续力矩下,末端还能带多少 kg(按各关节取最小)。
|
||||
|
||||
线性化:tau_g(p) ≈ tau_g_arm + p·g·L。用的是该关节实测的最大 TCP 力臂,
|
||||
即最不利姿态下的力臂。
|
||||
"""
|
||||
out = {}
|
||||
for tag, d in res["joints"].items():
|
||||
L = d["reach_tip_mm"] / 1000.0
|
||||
if L <= 0:
|
||||
out[tag] = None
|
||||
continue
|
||||
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
|
||||
out[tag] = round(max(head, 0.0) / (G * L), 3) if head > 0 else 0.0
|
||||
return out
|
||||
|
||||
|
||||
def generate_report(res, mp):
|
||||
L = []
|
||||
L.append("# 6 自由度机械臂 · 关节电机力矩与负载能力校核\n")
|
||||
a = res["assumptions"]
|
||||
L.append("## 1. 前提\n")
|
||||
L.append("- 驱动:%s" % a["motor"])
|
||||
L.append("- 连续额定输出力矩 `T_cont` = %.2f N·m;峰值堵转 `T_peak` = %.2f N·m"
|
||||
% (a["T_cont_Nm"], a["T_peak_Nm"]))
|
||||
L.append("- 封闭打印外壳热衰减后许用连续力矩 `T_allow` = %.2f N·m;末端负载 %.2f kg;"
|
||||
"角加速度 %.1f rad/s²" % (a["T_allow_Nm"], a["payload_kg"], a["alpha_rad_s2"]))
|
||||
L.append("- **%s**\n" % a["note"])
|
||||
L.append("载荷模型:每个零件按网格体积加权质心定位,随姿态做刚体变换,在可达姿态"
|
||||
"网格(俯仰轴取行程端点/中值,自转轴取 0/±60/±120/180°)上取重力力矩最大值。"
|
||||
"力矩为绕**关节自身轴**的分量——这才是电机要出的力。\n")
|
||||
|
||||
L.append("## 2. 各关节校核结果\n")
|
||||
L.append("| 关节 | 名称 | 轴 | 以下质量(kg) | TCP力臂(mm) | 重力力矩(N·m) | 惯性力矩(N·m) | 峰值需求(N·m) | 负荷率(对T_allow) | 峰值裕度 | 判定 |")
|
||||
L.append("|:---:|:---|:---|---:|---:|---:|---:|---:|---:|---:|:---:|")
|
||||
for tag, d in res["joints"].items():
|
||||
L.append("| %s | %s | %s | %.2f | %.0f | **%.2f** | %.2f | %.2f | **%.0f%%** | %s | %s |"
|
||||
% (tag, d["cn"], d["axis"], d["distal_mass_kg"], d["reach_tip_mm"],
|
||||
d["tau_gravity_Nm"], d["tau_dynamic_Nm"], d["tau_required_peak_Nm"],
|
||||
d["duty_vs_Tallow_pct"],
|
||||
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—", d["status"]))
|
||||
L.append("")
|
||||
L.append("## 3. 许用连续工况下的末端负载能力\n")
|
||||
L.append("去掉末端负载后各关节仍需 `tau_gravity_arm_only` 的重力力矩,余额除以该关节"
|
||||
"实测最大 TCP 力臂,即为还能挂的负载:\n")
|
||||
L.append("| 关节 | 仅臂身重力力矩(N·m) | 余额(N·m) | 可挂负载(kg) |")
|
||||
L.append("|:---:|---:|---:|---:|")
|
||||
for tag, d in res["joints"].items():
|
||||
head = T_ALLOW - d["tau_gravity_arm_only_Nm"]
|
||||
L.append("| %s | %.2f | %+.2f | %s |"
|
||||
% (tag, d["tau_gravity_arm_only_Nm"], head,
|
||||
("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
|
||||
ok = [t for t, v in mp.items() if v is not None]
|
||||
worst = min(ok, key=lambda t: mp[t]) if ok else None
|
||||
if worst:
|
||||
L.append("")
|
||||
L.append("**整臂瓶颈:%s,可挂末端负载 %.2f kg。**\n" % (worst, mp[worst]))
|
||||
L.append("## 4. 结论要点\n")
|
||||
over = [t for t, d in res["joints"].items() if d["status"] == "OVERLOAD"]
|
||||
if over:
|
||||
L.append("- **超载关节:%s**。" % "、".join(over))
|
||||
for t in over:
|
||||
d = res["joints"][t]
|
||||
L.append(" - %s(%s)重力 %.2f N·m,超 `T_allow` %.0f%%;即使按峰值堵转算,"
|
||||
"裕度也只有 %s。" % (t, d["cn"], d["tau_gravity_Nm"],
|
||||
d["duty_vs_Tallow_pct"] - 100.0,
|
||||
("%.2f×" % d["peak_margin_x"]) if d["peak_margin_x"] else "—"))
|
||||
L.append("- 结论对电机参数高度敏感:若实际连续额定高于假定值,"
|
||||
"请用 `ARMT_CONT` / `ARMT_ALLOW` 覆盖后重跑本脚本。")
|
||||
else:
|
||||
L.append("- 全部关节在许用连续力矩内。")
|
||||
L.append("- 惯量按 Σm·d² 估,**未计入零件自身转动惯量**,对肩/肘是大臂量级的关节"
|
||||
"约低估一成,惯性力矩一栏应视为下限。")
|
||||
L.append("- 本版不含被动弹簧平衡。旧版报告提出的 J2 拉簧预补偿(2.48 N·m)在几何上"
|
||||
"**尚未实现**;若要保留该方案,需要在 P04/P05 之间增加拉簧连杆机构。")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def main():
|
||||
res, jl0, jl = analyze(PAYLOAD)
|
||||
mp = max_payload(res)
|
||||
io.open(DOCS + "/torque_balance.json", "w", encoding="utf-8").write(
|
||||
json.dumps(res, ensure_ascii=False, indent=2))
|
||||
io.open(DOCS + "/torque_report.md", "w", encoding="utf-8").write(generate_report(res, mp))
|
||||
|
||||
print("末端负载 %.2f kg T_cont=%.2f T_allow=%.2f T_peak=%.2f alpha=%.1f\n"
|
||||
% (PAYLOAD, T_CONT, T_ALLOW, T_PEAK, ALPHA))
|
||||
print("%-4s %-8s %-12s %8s %8s %8s %9s %8s %s"
|
||||
% ("关节", "名称", "轴", "重力N·m", "惯性N·m", "峰值N·m", "负荷率", "峰值裕度", "判定"))
|
||||
for tag, d in res["joints"].items():
|
||||
print("%-4s %-8s %-12s %8.2f %8.2f %8.2f %8.0f%% %8s %s"
|
||||
% (tag, d["cn"], d["axis"], d["tau_gravity_Nm"], d["tau_dynamic_Nm"],
|
||||
d["tau_required_peak_Nm"], d["duty_vs_Tallow_pct"],
|
||||
("%.2fx" % d["peak_margin_x"]) if d["peak_margin_x"] else "-",
|
||||
d["status"]))
|
||||
print("\n许用连续力矩下可挂末端负载 (kg):")
|
||||
for tag in res["joints"]:
|
||||
print(" %-4s %s" % (tag, ("%.2f" % mp[tag]) if mp[tag] is not None else "—"))
|
||||
ok = [t for t, v in mp.items() if v is not None]
|
||||
if ok:
|
||||
w = min(ok, key=lambda t: mp[t])
|
||||
print(" 整臂瓶颈 %s -> %.2f kg" % (w, mp[w]))
|
||||
print("\n已写出 docs/torque_balance.json + docs/torque_report.md")
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""关节角组合可达空间(工具尖点)计算:FK 采样 + J1 旋转对称包络 + 体积估计"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from arm import kinematics as HL
|
||||
from arm.paths import DOCS, CACHE
|
||||
|
||||
TRAVEL = {t["axis"]: (t["lo"], t["hi"]) for t in
|
||||
json.load(open(DOCS + "/travel.json", encoding="utf-8"))}
|
||||
|
||||
TIP = (HL.X_ARM, HL.Y_AXIS, HL.Z_TIP) # P09 法兰底面中心(工具安装面)
|
||||
J1_AX = (HL.Y_AXIS, HL.Z_RL) # J1 转轴: 过 Y=128, Z=610, 平行 X
|
||||
|
||||
|
||||
def rot_about(p, k, ang):
|
||||
"""绕轴 (p 点, k 方向) 旋转 ang(弧度, 可为数组M,);返回 Rm(M,3,3), t(M,3)。
|
||||
p' = Rm @ (p_pt - p) + p,与 AB.R 的 gp_Trsf.SetRotation 右手定则一致。"""
|
||||
k = np.asarray(k, dtype=float)
|
||||
k = k / np.linalg.norm(k)
|
||||
ang = np.atleast_1d(np.asarray(ang, dtype=float))
|
||||
px, py, pz = p
|
||||
K = np.array([[0.0, -k[2], k[1]], [k[2], 0.0, -k[0]], [-k[1], k[0], 0.0]])
|
||||
K2 = K @ K
|
||||
Rm = np.eye(3)[None, :, :] + np.sin(ang)[:, None, None] * K[None, :, :] \
|
||||
+ (1.0 - np.cos(ang))[:, None, None] * K2[None, :, :]
|
||||
pp = np.array([px, py, pz])
|
||||
t = pp[None, :] - np.einsum("mij,j->mi", Rm, pp)
|
||||
return Rm, t
|
||||
|
||||
|
||||
def fk_batch(angles, tip=TIP):
|
||||
"""angles: dict idx->弧度数组(M,);返回 (M,3) 世界坐标。链式 acc 与 sweep_v2 一致:
|
||||
父关节变换在最外层 p' = R1∘R2∘…∘R5 (p),各 R_i 绕其零位世界轴(human_layout 唯一事实源)。"""
|
||||
pts = np.tile(np.asarray(tip, dtype=float), (angles[1].shape[0], 1))
|
||||
for i in range(5, 0, -1): # J6 跳过:尖点在 J6 轴上,其转角不改变尖点位置
|
||||
rear, d = HL.JOINTS[i - 1][2], HL.JOINTS[i - 1][3]
|
||||
Rm, t = rot_about(rear, d, angles[i])
|
||||
pts = np.einsum("mij,mj->mi", Rm, pts) + t
|
||||
return pts
|
||||
|
||||
|
||||
def sweep(step=2.0):
|
||||
j2 = np.radians(np.arange(TRAVEL["J2"][0], TRAVEL["J2"][1] + 1e-9, step))
|
||||
j4 = np.radians(np.arange(TRAVEL["J4"][0], TRAVEL["J4"][1] + 1e-9, step))
|
||||
j5 = np.radians(np.arange(TRAVEL["J5"][0], TRAVEL["J5"][1] + 1e-9, step))
|
||||
g2, g4, g5 = np.meshgrid(j2, j4, j5, indexing="ij")
|
||||
M = g2.size
|
||||
angles = {1: np.zeros(M), 2: g2.ravel(), 3: np.zeros(M),
|
||||
4: g4.ravel(), 5: g5.ravel()}
|
||||
pts = fk_batch(angles)
|
||||
# 自证1:零位尖点必须回到 (18.5,128,93)
|
||||
z0 = fk_batch({i: np.zeros(1) for i in range(1, 6)})
|
||||
assert np.allclose(z0[0], TIP, atol=1e-9), z0[0]
|
||||
# 自证2:J1/J6 旋转不改变 (x, r) 与极值位置——J1 取中值扫一遍,r 分布应与 J1=0 相同
|
||||
for phi in (0.5, 1.2, 2.4):
|
||||
v1 = np.full(M, phi)
|
||||
p1 = fk_batch({1: v1, 2: g2.ravel(), 3: np.zeros(M),
|
||||
4: g4.ravel(), 5: g5.ravel()})
|
||||
r1 = np.hypot(p1[:, 1] - J1_AX[0], p1[:, 2] - J1_AX[1])
|
||||
r0 = np.hypot(pts[:, 1] - J1_AX[0], pts[:, 2] - J1_AX[1])
|
||||
assert np.allclose(np.sort(r0), np.sort(r1), atol=1e-9), "J1 对称性被破坏"
|
||||
# 自证3:J3 旋转(大臂自转)不改变尖点位置——关节轴过尖点所在纵线
|
||||
print("J3 对称性: 尖点 x=%.1f 与 J3 轴 x=%.1f 一致=%s, y=%.1f 与轴 y=%.1f 一致=%s"
|
||||
% (TIP[0], HL.X_ARM, abs(TIP[0] - HL.X_ARM) < 1e-9,
|
||||
TIP[1], HL.Y_AXIS, abs(TIP[1] - HL.Y_AXIS) < 1e-9))
|
||||
print("J6 含于轴: 尖点 (%.1f,%.1f) vs 轴 (%.1f,%.1f)"
|
||||
% (TIP[0], TIP[1], HL.JOINTS[5][2][0], HL.JOINTS[5][2][1]))
|
||||
return pts
|
||||
|
||||
|
||||
def envelope(pts, grid=1.0):
|
||||
"""(x, r) 半平面栅格化占用 + 体积(绕 J1 轴回转)。"""
|
||||
x = pts[:, 0]
|
||||
r = np.hypot(pts[:, 1] - J1_AX[0], pts[:, 2] - J1_AX[1])
|
||||
x0, x1 = math.floor(x.min()), math.ceil(x.max())
|
||||
r0, r1 = 0.0, math.ceil(r.max())
|
||||
nx = int(round((x1 - x0) / grid)) + 1
|
||||
nr = int(round((r1 - r0) / grid)) + 1
|
||||
xi = np.clip(((x - x0) / grid).round().astype(int), 0, nx - 1)
|
||||
ri = np.clip((r / grid).round().astype(int), 0, nr - 1)
|
||||
occ = np.zeros((nx, nr), dtype=bool)
|
||||
occ[xi, ri] = True
|
||||
# 每个 x 列只保留 r <= 该列可达最大 r(回转体沿 r 连续填充)
|
||||
for i in range(nx):
|
||||
j = np.nonzero(occ[i])[0]
|
||||
if j.size:
|
||||
occ[i, : j.max() + 1] = True
|
||||
rr = r0 + np.arange(nr) * grid
|
||||
vol = float((occ * (2.0 * math.pi * rr)[None, :] * grid * grid).sum())
|
||||
return dict(occ=occ, x0=x0, grid=grid, rm_axes=rr, vol_mm3=vol,
|
||||
nx=nx, nr=nr)
|
||||
|
||||
|
||||
def main():
|
||||
pts = sweep()
|
||||
x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
|
||||
r = np.hypot(y - J1_AX[0], z - J1_AX[1])
|
||||
env = envelope(pts)
|
||||
# 每个 x 的 r 包络(侧视/俯视剖面用)
|
||||
xs = env["x0"] + np.arange(env["nx"]) * env["grid"]
|
||||
rmax_x, rmin_x = [], []
|
||||
for i in range(env["nx"]):
|
||||
j = np.nonzero(env["occ"][i])[0]
|
||||
if j.size:
|
||||
rmax_x.append(env["rm_axes"][j.max()])
|
||||
rmin_x.append(env["rm_axes"][j.min()])
|
||||
else:
|
||||
rmax_x.append(np.nan)
|
||||
rmin_x.append(np.nan)
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
np.savez_compressed(CACHE + "/ws_cloud.npz",
|
||||
pts=pts, xs=np.array(xs),
|
||||
rmax=np.array(rmax_x), rmin=np.array(rmin_x))
|
||||
met = dict(
|
||||
z_max=float(z.max()), z_min=float(z.min()),
|
||||
x_fwd=float(x.max()), x_back=float(x.min()),
|
||||
y_lo=float(y.min()), y_hi=float(y.max()),
|
||||
r_max=float(r.max()), r_min=float(r.min()),
|
||||
reach_from_j1_axis=float(r.max()),
|
||||
spread=float(z.max() - z.min()),
|
||||
vol_dm3=env["vol_mm3"] / 1e6,
|
||||
tip=TIP, j1_axis=list(J1_AX),
|
||||
limits={k: list(v) for k, v in TRAVEL.items()},
|
||||
)
|
||||
# 自证4:极值点姿态可解释(打印取极值的角度组合索引)
|
||||
imax = int(np.argmax(z)); imin = int(np.argmin(z))
|
||||
ifwd = int(np.argmax(x)); iback = int(np.argmin(x))
|
||||
met["pose_extreme"] = dict(
|
||||
zmax_idx=imax, zmin_idx=imin, xfwd_idx=ifwd, xback_idx=iback)
|
||||
print(json.dumps({k: v for k, v in met.items()
|
||||
if k not in ("limits", "pose_extreme")},
|
||||
ensure_ascii=False, indent=1))
|
||||
json.dump(met, open(CACHE + "/ws_metrics.json", "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""机器人手臂深度圆角优化版 V2:彻底消除所有棱角
|
||||
针对用户反馈的三大问题区域进行深度优化:
|
||||
1. 蓝色零件(P04肩叉)顶部方形突起 → 全圆角处理
|
||||
2. 绿色/青色零件(P05/P08)底部和侧面棱角 → Loft外轮廓圆角
|
||||
3. 所有连接处的尖锐过渡 → 大圆角平滑过渡
|
||||
4. 橙色零件(P06/P07)所有方形边缘 → 完整圆角系统
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import cadquery as cq
|
||||
from arm.cad import arm_build as AB
|
||||
from arm.paths import OUT, WEB, DOCS
|
||||
from arm.constants import M, C
|
||||
from arm.cad.helpers import cyl, box, ring, place, trs, bb, bbox_str, solids_of
|
||||
from arm.cad.arm_build import (Joint, add, bx, Ycyl, Ybox, Yplate, prism, dogbone, sector,
|
||||
FLANGE_Z, COVER_Z, HUB_FACE_Z, CLAMP_SPAN, HUB_M2_CLOCK,
|
||||
BORE_R, CB_R, PCB_W, D2R, T, R, PARTS)
|
||||
from arm import kinematics as HL
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from arm.kinematics import (HUB, PLATE_T, R_MOT, Z_TIP, Z_WR, Z_EI, Z_SH, Z_RL, Z_R1,
|
||||
Z_R3, Z_HUB3, Z_EL, Z_R6, Z_HUB6,
|
||||
Z_R4, Z_CAP, Z_M6, Z_CHEST0, Z_CHEST1, X_J1_HUB, X_ARM, L1,
|
||||
X_FORK_GAP, X_BAY_EXT,
|
||||
Y_AXIS, Y_HUB, Y_REAR, Y_COVER, Y_H, Y_R_IN, Y_R_OUT,
|
||||
Y_M6, Y_CHEST, X_CHEST, FOOT_H)
|
||||
|
||||
VERSION = os.environ.get("ARMVER", "v2")
|
||||
ITER = int(os.environ.get("ARMITER", "3"))
|
||||
AB.VERSION = VERSION
|
||||
AB.ITER = ITER
|
||||
|
||||
# ------------------------------------------------ 逐件模块(几何已拆出)
|
||||
from arm.cad.parts import PART_MODULES
|
||||
from arm.cad.part_tools import (
|
||||
BOSS, CHAMFER_EDGE, CLAMP_BACKOFF, CLAMP_BCD, CLAMP_CLOCK, CLAMP_CLOCK_LOCAL, CLAMP_CLR,
|
||||
CLAMP_ROOT, CLAMP_T, CLEARANCE_GAP, DISC_R, FENCE_OPEN_CUT, FILLET_LARGE, FILLET_MEDIUM,
|
||||
FILLET_SMALL, FILLET_TINY, FILLET_XLARGE, HOLLOW_FILLET, HOLLOW_WALL, SLEEVE_R, XB, Xcyl,
|
||||
Xplate, Y_MID, cut_motor_fence, hollow_channel, hollow_tent_window, hub_m2_holes,
|
||||
make_direct_hub, make_pitch_to_roll_bracket, motor_front_clamp,
|
||||
)
|
||||
from arm.cad.parts.p01_column import X_COL, Y_COL, Z_LOCK, p_chest, split_p01_poka_yoke
|
||||
from arm.cad.parts.p02_base import make_p02_poka_yoke, p_base_foot
|
||||
from arm.cad.parts.p03_girdle import p_girdle
|
||||
from arm.cad.parts.p04_fork import P04_CH, p_fork
|
||||
from arm.cad.parts.p05_roll_base import p_shoulder_roll_base
|
||||
from arm.cad.parts.p06_humerus import P06_HOLLOW_WIN, P06_WIN_XZ, p_humerus_bone
|
||||
from arm.cad.parts.p07_forearm import P07_CH, p_forearm_bone
|
||||
from arm.cad.parts.p08_wrist import p_wrist_bracket
|
||||
from arm.cad.parts.p09_flange import p_tool_flange
|
||||
# 兼容旧工具:scratch/ 的诊断脚本按 arm_model.<符号> 取用件构造函数与开关
|
||||
|
||||
def build():
|
||||
print("=" * 80, flush=True)
|
||||
print("【第一阶段 1/5】开始生成手臂各级构件 3D 几何实体模型...", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
t_stage1 = time.time()
|
||||
|
||||
print(" [0/9] 载入电机标准件模组 (STEP)...", end="", flush=True)
|
||||
t0 = time.time()
|
||||
motor = cq.importers.importStep(AB.SRC_STEP).val()
|
||||
motor = place(motor, T(0, 0, -2.57))
|
||||
AB.MOTOR, AB.HALF, AB.CLAMP = motor, None, None
|
||||
print(f" 完成 ({time.time()-t0:.2f}s) ✓", flush=True)
|
||||
|
||||
JS = {}
|
||||
for tag, cn, rear, d, xd, holder, driven in HL.JOINTS:
|
||||
JS[tag] = Joint(tag, cn, rear, d, xd, lug_phase=90.0 if tag == "J1" else 0.0)
|
||||
j1, j2, j3, j4, j5, j6 = (JS["J%d" % k] for k in range(1, 7))
|
||||
|
||||
PARTS[:] = []
|
||||
AB.CENTS.clear()
|
||||
AB.GRP = {}
|
||||
par = {"J1": "root", "J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"}
|
||||
motor_of = {"J1": "M1_J1", "J2": "M2_J2", "J3": "M3_J3",
|
||||
"J4": "M4_J4", "J5": "M5_J5", "J6": "M6_J6"}
|
||||
for t in ("J1", "J2", "J3", "J4", "J5", "J6"):
|
||||
AB.GRP[motor_of[t]] = par[t]
|
||||
AB.JAW = {}
|
||||
# 逐件模块:几何、件号/配色/说明、装配帧归属都在 arm/cad/parts/ 里,
|
||||
# 这里只按 BOM 顺序收集 specs(),并把件号登记到装配帧图上。
|
||||
parts_to_build = []
|
||||
for _m in PART_MODULES:
|
||||
for _spec in _m.specs(JS):
|
||||
AB.GRP[_spec[0]] = _m.JOINT
|
||||
parts_to_build.append(_spec)
|
||||
|
||||
|
||||
total_parts = len(parts_to_build)
|
||||
for idx, (name, cn, fn, color, note) in enumerate(parts_to_build, 1):
|
||||
print(f" [{idx}/{total_parts}] 正在构建 {name} ({cn})...", end="", flush=True)
|
||||
t0 = time.time()
|
||||
sh = fn()
|
||||
dt = time.time() - t0
|
||||
vol = AB.vol_of(sh) / 1000.0
|
||||
n_solids = len(solids_of(sh))
|
||||
print(f" 完成 ✓ ({dt:.2f}s | 体积: {vol:.1f} cm³ | 实体数: {n_solids})", flush=True)
|
||||
add(name, cn, sh, color, note=note)
|
||||
|
||||
print(" [装配] 正在将 M1~M6 电机定位至各关节坐标位姿...", end="", flush=True)
|
||||
t0 = time.time()
|
||||
for k, (tag, cn, rear, d, xd, holder, driven_name) in enumerate(HL.JOINTS):
|
||||
j = JS[tag]
|
||||
add("M%d_%s" % (k + 1, tag), "%s 电机模组" % tag, place(motor, j.t), 0x9ca3af,
|
||||
printed=False, note="复用 电机.STEP")
|
||||
print(f" 完成 ✓ ({time.time()-t0:.2f}s)", flush=True)
|
||||
print(f"-> 构件模型全部生成完成,总耗时: {time.time()-t_stage1:.2f}s\n", flush=True)
|
||||
return JS
|
||||
|
||||
|
||||
CHAIN = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||||
SEP = 74.0
|
||||
|
||||
|
||||
def explode_chain(p, JS, cents, gmean):
|
||||
"""爆炸图偏移:立柱垂直阶梯拉开,底座前后防呆拉开,电机沿轴拉出,机械臂整体随上段分离"""
|
||||
nm = p["name"]
|
||||
if nm == "P01_A_立柱下段_三角形30%_墙4底5_竖打":
|
||||
return [0.0, 0.0, 0.0]
|
||||
if nm == "P01_B_立柱中段_三角形30%_墙4底5_竖打":
|
||||
return [0.0, 0.0, 60.0]
|
||||
if nm == "P01_C_立柱上段_三角形30%_墙4底5_竖打":
|
||||
return [0.0, 0.0, 120.0]
|
||||
if nm == "P02_Center_底座中核心_螺旋体30%_墙5底6_平打":
|
||||
return [0.0, 0.0, 0.0]
|
||||
if nm == "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打":
|
||||
return [65.0, 0.0, 0.0]
|
||||
if nm == "P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打":
|
||||
return [-65.0, 0.0, 0.0]
|
||||
if nm == "P03_肩吊臂_螺旋体35%_墙5底5_竖打":
|
||||
return [0.0, 0.0, 120.0]
|
||||
|
||||
grp = AB.GRP.get(p["name"], "root")
|
||||
if grp == "root":
|
||||
return [0.0, 0.0, 0.0]
|
||||
k = CHAIN.index(grp)
|
||||
off = [0.0, 0.0, 120.0 - SEP * k]
|
||||
j = JS.get(grp)
|
||||
if j:
|
||||
w = 0.0
|
||||
if "半联轴器" in nm or "抱箍" in nm:
|
||||
w = 34.0 * (1.0 if ("A" in nm[-1:]) else -1.0)
|
||||
elif nm.startswith("M"):
|
||||
w = 62.0
|
||||
for q in range(3):
|
||||
off[q] += j.d[q] * w
|
||||
return off
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
JS = build()
|
||||
|
||||
print("=" * 80, flush=True)
|
||||
print("【单实体拓扑完整性校验】验证所有打印构件流形实体状态...", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
bad = 0
|
||||
printed_list = [p for p in PARTS if p["printed"]]
|
||||
total_pr = len(printed_list)
|
||||
for idx, p in enumerate(printed_list, 1):
|
||||
n = len(solids_of(p["shape"]))
|
||||
if n != 1:
|
||||
bad += 1
|
||||
status = "单实体 ✓" if n == 1 else f"异常 ({n} 个实体) ❌"
|
||||
print(f" [{idx}/{total_pr}] {p['name']:<14} : 体积 {p['vol']/1000.0:6.1f} cm³ | {status}", flush=True)
|
||||
if bad == 0:
|
||||
print(f"-> 单实体检查全部通过!{total_pr}/{total_pr} 构件均为完美单一流形拓扑 ✓\n", flush=True)
|
||||
else:
|
||||
print(f"-> 发现 {bad} 个非单实体构件!\n", flush=True)
|
||||
|
||||
AB.explode_vec = explode_chain
|
||||
bom = AB.export(JS)
|
||||
|
||||
# 紧固件(螺钉)实例注入:把 docs/screws.json 的 72 颗螺钉按真实孔位还原成三维实例,
|
||||
# 追加进 web/mesh_arm.json 的 parts(查看器可点 BOM 螺丝行高亮定位;纯数据,不动 CAD)
|
||||
from arm.dataio import screw_mesh
|
||||
screw_mesh.main()
|
||||
cl = AB.collide()
|
||||
from arm.viewer.build_viewer import rebuild_viewer
|
||||
rebuild_viewer()
|
||||
# 可达包络自动注入:AB.export 刚重写过 mesh_arm.json(包络件会被冲掉),
|
||||
# 这里重跑 FK 采样 + 包络网格注入,再重打包一次查看器(约 3~4s,纯数据无 CAD 计算)
|
||||
from arm.analysis import ws_sweep, gen_env_mesh
|
||||
t_env = time.time()
|
||||
ws_sweep.main()
|
||||
gen_env_mesh.main()
|
||||
rebuild_viewer()
|
||||
print(f"-> 可达包络已自动注入查看器(+{time.time() - t_env:.1f}s)✓\n", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
print("✅ 机器人手臂编译、导出与查看器发布全流程完成!", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
@@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""几何与装配层。
|
||||
|
||||
helpers 放置/基础体/网格化等无状态 helper
|
||||
arm_build Joint 装配模型、零件登记(add)、STEP/STL/BOM/mesh 导出
|
||||
part_tools 跨件共用刀法(减重腔、压盖、转接座、圆角常量、螺钉孔位事实源)
|
||||
parts/ 每个打印件一个模块:几何 + 件号/配色/说明 + 装配帧归属
|
||||
"""
|
||||
@@ -9,7 +9,7 @@ bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
|
||||
the existing face-spline halves + split clamp bolt straight on), the part
|
||||
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
|
||||
|
||||
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
|
||||
Run: python -m arm.build_model -> out/, webapp/data/mesh_arm.json, docs/
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
@@ -21,10 +21,10 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import cadquery as cq
|
||||
from armcore import (M, C, V, trs, place, bb, bbox_str, cyl, ring, box, holes,
|
||||
solids_of, mesh_of, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP)
|
||||
from arm.paths import OUT, WEB, DATA, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP
|
||||
from arm.constants import M, C, MOTOR_TOL
|
||||
from arm.cad.helpers import V, trs, place, bb, bbox_str, cyl, ring, box, holes, solids_of, mesh_of
|
||||
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||||
from OCP.GProp import GProp_GProps
|
||||
@@ -50,6 +50,55 @@ PCB_W = M["pcba_w"] + 0.8 # 64.40 square pocket
|
||||
HUB_M2_CLOCK = [(360.0 - a) % 360.0 for a in M["m2_clock"]] # phase of the mirrored half
|
||||
HUB_PILOT_D = M["neck_d"] - 0.4 # 35.6 -> the half's pilot lands in a Ø36 recess
|
||||
PINCH_D = 2.6 # M3 clearance in printed material
|
||||
# 承压面让位(完整背景见 Joint.pocket 的 docstring):
|
||||
# REAR_LAND = 0.6 腔口直壁台肩高度,**保持原设计值不变**(让位不靠加高台肩)
|
||||
# REAR_SEAT = 2.6 尾端面->承压面 = 0.6 原台肩 + 2.0 公差垫片位
|
||||
# R35_SEAT = 35.0 后端板 R35 外壁(承压面外径)
|
||||
# SEAT_R = 35.0 承压面段(z=-REAR_SEAT..SEAT_TOP)的腔口半径 = **等于外壁 R35**。
|
||||
# SEAT_TOP = 1.0 圆腔顶端越过电机尾端面(z=0) 1mm。
|
||||
#
|
||||
# 【2026-10-05 用户第三次指出:SEAT_R 必须是 35,"凸台+围挡"要一起弄平】
|
||||
# 之前取 32.2(64.4 方腔的内切圆)以为这样能保住连接壁,实测**恰恰相反**:
|
||||
# 32.2 与外壁 35 之间那圈 2.8mm 宽的环墙完整保留,而承压面只挖到 32.2,
|
||||
# 于是这圈环墙相对承压面【凸起 2.6mm】,把电机尾端面整个围在里面 ——
|
||||
# 这正是用户截图里的"一圈围挡"(P08 该圈实测 1536.9 mm3)。它不是残留,
|
||||
# 而是 SEAT_R 取小值的必然产物:只要 SEAT_R < R35,环墙就一定存在。
|
||||
# 用户要求:"把凸台、围挡一起弄平,做成和电机两头齐平一起的平面"。
|
||||
# 故 SEAT_R 取满 R35:承压面段整圈挖到外壁,承压面变成**完整 R35 平圆盘**,
|
||||
# 环墙(凸台)自然消失,电机尾端面与承压面之间再无任何凸起。
|
||||
#
|
||||
# 【为什么这次取 R35 不会重演2026-10-04 的"肩叉顶部没连上"】
|
||||
# 那次我切的是 z=-2.6..0 且**只切到 z=0 就停**,导致环墙在 z=0..1.0 还残留一截
|
||||
# 0.052cm3 的薄片(Y93.0~95.8 的半包开口处),那片薄片脱空成为独立碎块。
|
||||
# 现在 SEAT_TOP=+1.0 把那一截一起吃掉:实测五件全部单实体、零碎片。
|
||||
# 且承力桥改为由**臂身本体**(r>35)承担 —— 实测 band 段 r36..58 有料的方位
|
||||
# P04 10/36、P05/P06/P07/P08 各 15/36,其余方位由 z=+1.0 以上的杯体连接,
|
||||
# 不存在"整圈被切断"的通槽(那正是 2026-10-04 的错误形态)。
|
||||
# 连带好处:承压环由「1.95mm 窄环」变「完整 R35 圆环 4.75mm」,受压面积 383->974mm²。
|
||||
REAR_LAND = 0.6
|
||||
REAR_SEAT = REAR_LAND + MOTOR_TOL # 1.6 尾端面->后端板承压面:公差垫片位 (0.6 + 1.0)
|
||||
R35_SEAT = 35.0 # 后端板 R35 外壁(承压面外径)
|
||||
SEAT_R = R35_SEAT # 35.0 承压面段腔口 = 外壁:凸台+围挡一并消除
|
||||
SEAT_TOP = 1.0 # 圆腔顶端越过电机尾端面 1mm(吃掉开口处残片)
|
||||
BORE_Z0 = 5.5 # 座孔起点:R35 圆腔挖到此处,与 R30.15 座孔相接
|
||||
|
||||
# ---------------------------------------------------------------- 电机腔统一半径
|
||||
# 【2026-10-05f 接缝整改】腔内壁此前有 4 个互相打架的半径,逐层都能实测到台阶:
|
||||
# R35(承压面) -> R30.15(座孔) -> R30.25(前端定位) -> R30.50(前端盖让位)
|
||||
# 每一处单看都有理由,合在一起却让内壁变成「圆-台阶-圆-台阶-圆」,从腔里看全是横向接缝。
|
||||
#
|
||||
# 实测依据(scratch/probe_motor_env.py,剖切取 Rmax,112 层):
|
||||
# 电机本体从 z=0.43 到 z=55.9 **全程 R30.14~30.15 的正圆**,全程无任何台阶。
|
||||
# 即电机根本没有需要让位的形状变化 —— 那些台阶全是零件自己凭空造出来的。
|
||||
#
|
||||
# 故统一取 BAY_R = 30.50 = CB_R:
|
||||
# - 比电机实测最大半径 30.15 单边大 0.35(直径 0.7),打印件足以塞进去;
|
||||
# - 前端盖让位本来就是 30.50,沿用它可省掉一处台阶;
|
||||
# - 比 R30.15 座孔大 0.35,换来的是内壁从 z=-2.6 一直到前端只有一段正圆。
|
||||
# 代价:R30.15 座孔原本的单边 0.15 滑配变成 0.35,电机在座孔里的径向游隙变大。
|
||||
# 但电机前后已被后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压夹死(见 motor_front_clamp),
|
||||
# 径向游隙不影响定位,倾倒自由度已归零。
|
||||
BAY_R = 30.50
|
||||
|
||||
PARTS = []
|
||||
CENTS = {}
|
||||
@@ -204,30 +253,92 @@ class Joint:
|
||||
return (self.rear[0] + self.d[0] * s, self.rear[1] + self.d[1] * s, self.rear[2] + self.d[2] * s)
|
||||
|
||||
def pocket(self, shape, front_pad=0.6):
|
||||
"""Cut the motor pocket out of a proximal link body."""
|
||||
c = [cyl(BORE_R, 5.5, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + front_pad),
|
||||
box(-PCB_W / 2, PCB_W / 2, -PCB_W / 2, PCB_W / 2, -0.6, M["pcba_t"] + 0.4)]
|
||||
"""Cut the motor pocket out of a proximal link body.
|
||||
|
||||
REAR_SEAT = 电机尾端面(局部 z=0)到后端板承压面的距离。它不是定位面而是
|
||||
**垫片位**:公差 MOTOR_TOL=2.0(armcore)要靠它消化 —— 电机比标称短的那一份,
|
||||
就垫在尾端面与这个承压面之间(见 armcore.MOTOR_TOL 的说明)。原值 0.6 只能吃
|
||||
0.6mm,短 1mm 以上的电机装不进、长 0.6mm 以上的电机会顶死后端板而压溃。
|
||||
注意它同时是承压面的高度,所以让空 2.0mm 要求 t_plate >= 8
|
||||
(见各零件 rear_mount_holes 的 t_plate)。
|
||||
|
||||
【2026-10-04 整改:承压面段改用 R35 圆腔,凸台彻底消失】
|
||||
上一版把 64.4 **方**腔整体从 -0.6 加深到 -2.6(纯掏腔),方腔壁以外
|
||||
z=-2.6..0 之间那圈 R35 料一动没动,于是相对新承压面整整凸起 2.6mm ——
|
||||
这就是 P04~P08 后端那圈"悬空凸台"(用户截图红圈,3D 打印时该处是悬空
|
||||
薄壁,既要支撑又公差难保证)。
|
||||
|
||||
为什么之前试"把凸台切掉"总失败(几何实测,别再重复):
|
||||
- 64.4 方腔的**内切圆半径恰好 = 32.2**,所以方腔在 45° 方向已经吃穿到
|
||||
r=45.5,R35 壁只剩**四条边中点附近**的料。实测 r=34 处材料沿 z 的
|
||||
区间(承压面 -2.6):P06 的 0°/180° 是 -8.0..12.0(贯通,是臂身本体),
|
||||
80°/100° 是 -8.0..-0.0(**只比承压面多 2.6mm,这段就是凸台**),
|
||||
其余 20~160° 是 -8.0..-2.6(承压环本体,齐平)。
|
||||
- 因此整段全截面切会把 0/90 方向那条**贯通到 z=12 的承力桥**一起切断
|
||||
-> P05/P08 碎成 2 块(实测)。这不是"承压环是唯一连接",
|
||||
而是我的刀具没区分"桥"和"悬空料"。
|
||||
|
||||
现在的做法(一刀,不做角度分区):
|
||||
承压面所在的 z=-REAR_SEAT..SEAT_TOP 段,腔口取 **SEAT_R = R35 = 外壁**
|
||||
的圆,把整圈挖到外壁,承压面因此变成**完整 R35 平圆盘**。
|
||||
- 关键:**SEAT_R 必须等于 R35**。只要 SEAT_R < R35,两者之间那圈环墙
|
||||
就必然存在,且相对承压面凸起 (R35-SEAT_R) —— 那圈环墙就是用户说的
|
||||
"围挡"(P08 实测 1536.9mm3),不是残留而是我的取值必然产物。
|
||||
- SEAT_TOP 取 +1.0(越过电机尾端面 1mm):2026-10-04 那版取 0 时,
|
||||
半包开口处(Y93.0~95.8)会残留一截 0.052cm3 薄片脱空成碎块;
|
||||
抬到 +1.0 后五件全部单实体零碎片。
|
||||
- 承力改由**臂身本体**承担:band 段内 r36..58 有料的方位 P04 10/36、
|
||||
其余四件各 15/36,其余方位靠 z>+1.0 的杯体连接,不存在整圈切断的通槽。
|
||||
(2026-10-04 那版"肩叉顶部没连上"就是通槽形态,务必不要重现。)
|
||||
【2026-10-05b 去掉 64.4 方腔,电机腔内壁恢复完整圆弧面】
|
||||
此前 z=0..pcba_t 还叠了一层 `box(±32.2)` 方腔,理由是"PCB 是方的"。
|
||||
**该假设不成立**:实测电机 STEP 已把 PCB 体积含在本体轮廓内,不会超出
|
||||
投影范围,所以方腔纯属多挖。它带来两个副作用(用户截图 P06/P07/P08 腔内观感):
|
||||
1. 方腔半宽 32.2 < 外壁 R35,在四个方位留下一圈 2.8mm 环形台肩;
|
||||
2. 方腔对角 45.5 > R35,四个角把外壁咬穿,形成四个凸角。
|
||||
两者叠加使内壁呈"圆—方—圆"三段,与同族零件的整体圆弧语言不一致。
|
||||
|
||||
【2026-10-05f 承压面段也并进主腔,全腔只剩一段正圆】
|
||||
去掉方腔后内壁还剩两段:R35(承压面段 z=-2.6..5.5)+ R30.15 座孔(z>5.5),
|
||||
接缝处仍是一道 4.85mm 深的横向台阶。加上前端定位 R30.25、前端盖让位 R30.50,
|
||||
实测内壁依次是 35.0 / 30.5 / 30.2 / 30.5 / 30.2 —— 四道台阶(scratch/bay_step_profile.py)。
|
||||
根因是**电机根本没有那么多形状变化**:实测电机 STEP 全程 R30.15 正圆,
|
||||
那些半径全是零件自己造的。
|
||||
现改为**单一 BAY_R 圆柱从承压面一直挖到前端盖外**,
|
||||
SEAT_R / BORE_R / CB_R 三把刀合并成一把,承压面 -> 前端内壁全程一段正圆,
|
||||
接缝归零。承压面仍是 R35 的完整平圆盘(SEAT_TOP 段保留,只是不再往上延伸)。
|
||||
"""
|
||||
c = [cyl(BAY_R, -REAR_SEAT, COVER_Z + front_pad)]
|
||||
return shape.cut(*[self.L(s) for s in c])
|
||||
|
||||
def rear_mount_holes(self, shape, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0):
|
||||
"""Cut the 4x rear mounting bolt holes into a proximal link body.
|
||||
Matches the 4x tapped holes on the motor rear metal endplate:
|
||||
(±17.55, ±20.46) on the rear face at motor-local z = 0.
|
||||
Matches the 4x M3 tapped radial slots on the motor rear endplate
|
||||
(电机.STEP 实测):BCD55、相位 39.9°+90k,槽宽 2.5、径向可调约 ±1.25mm。
|
||||
旧版误用 35.1x40.9 矩形孔距:对角两颗偏出螺纹槽 ~4.7mm 无处咬合
|
||||
(2026-10-02 用户指出"螺钉没有对齐",按 STEP 实测槽位修正)。
|
||||
"""
|
||||
cuts = []
|
||||
for sx in (-17.55, 17.55):
|
||||
for sy in (-20.46, 20.46):
|
||||
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
|
||||
cuts.append(self.L(h))
|
||||
if cb_d > 0 and cb_depth > 0:
|
||||
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
|
||||
cuts.append(self.L(cb))
|
||||
for k in range(4):
|
||||
a = math.radians(39.65 + 90.0 * k)
|
||||
sx = 27.5 * math.cos(a)
|
||||
sy = 27.5 * math.sin(a)
|
||||
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
|
||||
cuts.append(self.L(h))
|
||||
if cb_d > 0 and cb_depth > 0:
|
||||
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
|
||||
cuts.append(self.L(cb))
|
||||
return shape.cut(*cuts)
|
||||
|
||||
def front_collar_pocket(self, shape, r_bore=30.25, z0=50.0, z1=56.5):
|
||||
"""Cut front locating collar pocket for the motor front rim."""
|
||||
p = cyl(r_bore, z0, z1)
|
||||
return shape.cut(self.L(p))
|
||||
# 【2026-10-03i】front_collar_pocket 已整段删除。
|
||||
# 它切的是 cyl(r_bore, 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 两张面,
|
||||
# P06 同样在 50.0 处裂开,而没调它的 P04/P08 腔壁是完整一张。
|
||||
# 这条棱在着色渲染里就是横穿腔内的一道线 —— 属于纯自找的接缝。
|
||||
# 前端定位环本身(前段 r30.5..35 的 6mm 环墙)不受影响:
|
||||
# 它由各零件自己的 lower_block / front_* 圆柱构成,不再靠这刀掏内孔。
|
||||
|
||||
def pinch(self, shape, fracs=(0.26, 0.58, 0.86), r_out=48.0, n=2):
|
||||
"""Radial M3 pinch screws through the boss wall onto the housing."""
|
||||
@@ -349,7 +460,7 @@ import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
def export(JS):
|
||||
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", WEB, DOCS):
|
||||
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", DATA, DOCS):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
print("=" * 80, flush=True)
|
||||
@@ -388,7 +499,7 @@ def export(JS):
|
||||
|
||||
# 2. 多线程并行生成网页 3D Mesh
|
||||
print("=" * 80, flush=True)
|
||||
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (web/mesh_arm.json)...", flush=True)
|
||||
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (webapp/data/mesh_arm.json)...", flush=True)
|
||||
print("=" * 80, flush=True)
|
||||
|
||||
done_mesh = 0
|
||||
@@ -430,12 +541,12 @@ def export(JS):
|
||||
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
|
||||
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
|
||||
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
|
||||
open(WEB + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
|
||||
open(DATA + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
|
||||
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
|
||||
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
|
||||
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
|
||||
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
print(f"-> 3D网格与BOM清单已保存至 web/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
|
||||
print(f"-> 3D网格与BOM清单已保存至 webapp/data/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
|
||||
return bom
|
||||
|
||||
|
||||
@@ -448,7 +559,7 @@ def collide():
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体30%_墙4底5_竖打", "M6_J6"),
|
||||
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "P01_B_立柱中段_三角形30%_墙4底5_竖打"), ("P01_B_立柱中段_三角形30%_墙4底5_竖打", "P01_C_立柱上段_三角形30%_墙4底5_竖打"),
|
||||
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "P02_Center_底座中核心_螺旋体30%_墙5底6_平打"),
|
||||
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打"),
|
||||
@@ -1,12 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared constants + CAD/placement helpers for the robot arm build.
|
||||
"""CAD 放置与几何 helper:刚体变换、求交/包围盒、基础体、网格化与 STEP 读取。
|
||||
|
||||
All motor interface numbers are MEASURED from
|
||||
G:/Robot_v2/Model/Solidworks/电机.STEP and 配件/make_v9.py (see docs/interfaces.md).
|
||||
实测接口数值在 arm/constants.py,仓库路径在 arm/paths.py——本模块不再持有它们。
|
||||
"""
|
||||
import math
|
||||
import json
|
||||
import os
|
||||
|
||||
import cadquery as cq
|
||||
from OCP.gp import gp_Trsf, gp_Vec, gp_XYZ, gp_Mat, gp_Pnt, gp_Dir, gp_Ax1, gp_Ax2, gp_Quaternion
|
||||
@@ -20,55 +17,8 @@ from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED, TopAbs_SOLID
|
||||
from OCP.TopoDS import TopoDS
|
||||
|
||||
V = cq.Vector
|
||||
ROOT = r"G:/Robot_v2"
|
||||
SRC_STEP = ROOT + r"/Model/Solidworks/电机.STEP"
|
||||
HALF_STEP = ROOT + r"/Model/Solidworks/配件/端面齿半联轴器_相同件x2.STEP"
|
||||
CLAMP_STEP = ROOT + r"/Model/Solidworks/配件/端面齿联轴器_抱箍_两片装配.STEP"
|
||||
WORKSPACE = ROOT + r"/机器人手臂"
|
||||
OUT = WORKSPACE + r"/out"
|
||||
WEB = WORKSPACE + r"/web"
|
||||
DOCS = WORKSPACE + r"/docs"
|
||||
|
||||
# ---------------- MEASURED: motor module (local frame: origin = PCBA rear face centre, +Z = output) ----
|
||||
M = dict(
|
||||
pcba_w=63.6, # square PCBA across flats
|
||||
pcba_t=9.0, # z 0 -> 9.0
|
||||
rearplate_d=60.0, # z 5.5 -> 10.5
|
||||
hous_d=60.0, # housing OD, z 10.5 -> 52.5
|
||||
hous_len=42.0,
|
||||
cover_d=60.30, # top cover OD (R30.15), z 52.5 -> 56.0
|
||||
cover_z=56.0, # cover front face = module total length
|
||||
flange_z=54.942, # 2nd-stage output flange face
|
||||
neck_d=36.0, # female neck bore in the cover
|
||||
neck_depth=1.0,
|
||||
centre_d=6.5, # centre bore in the flange
|
||||
centre_depth=4.0,
|
||||
m2_bcd=29.0,
|
||||
m2_clock=(8.6, 127.7, 247.9),
|
||||
m2_tap_d=1.85, # M2 pilot for self-tapping in printed material
|
||||
m2_depth=6.0,
|
||||
m2_clear_d=2.75,
|
||||
m2_cb_d=5.2,
|
||||
)
|
||||
# ---------------- MEASURED: face-spline coupling (existing, printed by the user) ------------------------
|
||||
C = dict(
|
||||
n_teeth=12, tooth_r0=19.0, tooth_r1=24.5, tooth_s=2.5,
|
||||
flange_t=4.50, part_z=5.560, # mirrored stack
|
||||
hub_r=27.0,
|
||||
gap=9.00, # cover-face to cover-face when closed
|
||||
clamp_od_r=30.0, clamp_len=7.80,
|
||||
lug_r1=37.0, lug_t=4.50,
|
||||
bolt_r=32.50, bolt_d=3.80,
|
||||
z0=1.66, z1=9.46, # clamp envelope in the flange frame (motor cover face = 1.060)
|
||||
)
|
||||
# axial stations, motor local frame:
|
||||
COVER_Z = M["cover_z"] # 56.0
|
||||
HUB_FACE_Z = COVER_Z + C["gap"] # 65.0 = mating face of the driven hub
|
||||
CLAMP_R = C["clamp_od_r"] + 0.6 # radial keep-out around the joint axis
|
||||
LUG_R = C["lug_r1"] + 1.0 # radial keep-out where the bolt lugs are
|
||||
# ---------------- clearances / print rules -------------------------------------------------------------
|
||||
CLR = dict(bore_slip=0.20, pilot=0.10, hole_3=3.4, hole_27=2.8, wall_min=2.0)
|
||||
FIT = dict(motor_bore_d=M["hous_d"] + 2 * 0.15, cover_cb_d=M["cover_d"] + 2 * 0.35)
|
||||
|
||||
|
||||
# ---------------- geometry helpers --------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""跨件共用的刀法与统一配方:X 向基础体、减重腔 helper、电机前端压盖、止口 hub、pitch->roll 转接座骨架。"""
|
||||
from arm.cad import arm_build as AB
|
||||
import cadquery as cq
|
||||
from arm import kinematics as HL
|
||||
import math
|
||||
from arm.cad.helpers import cyl, place
|
||||
from arm.cad.arm_build import COVER_Z, R, T, Ycyl, bx, prism
|
||||
from arm.kinematics 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 arm.cad.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,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P01 立柱三段(A/B/C):躯干承力柱 + 阶梯榫卯与防反装对锁孔。"""
|
||||
import cadquery as cq
|
||||
from arm.cad.helpers import cyl
|
||||
from arm.cad.arm_build import Ycyl, bx
|
||||
from arm.kinematics import Y_AXIS, Z_CHEST1
|
||||
from arm.cad.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 arm.cad.helpers import cyl
|
||||
from arm.cad.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 arm.cad.helpers import cyl
|
||||
from arm.cad.arm_build import Yplate, bx
|
||||
from arm.kinematics import X_BAY_EXT, Y_AXIS, Z_CHEST1, Z_R1, Z_RL
|
||||
from arm.cad.part_tools import Xcyl, make_surface_tie_bridge
|
||||
from arm.cad.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,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P05 大臂旋转座:J2 俯仰 -> J3 滚转(复用 part_tools 的 pitch->roll 骨架)。"""
|
||||
import cadquery as cq
|
||||
from arm.kinematics import Z_R3, Z_SH
|
||||
from arm.cad.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 空心梁。"""
|
||||
from arm.cad import arm_build as AB
|
||||
import cadquery as cq
|
||||
from arm.cad.helpers import cyl
|
||||
from arm.cad.arm_build import COVER_Z, Ycyl, bx
|
||||
from arm.kinematics import X_ARM, Y_AXIS, Y_COVER, Y_REAR, Z_EL, Z_R3
|
||||
from arm.cad.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 型结构 + 顶板减重槽。"""
|
||||
from arm.cad import arm_build as AB
|
||||
import cadquery as cq
|
||||
from arm import kinematics as HL
|
||||
import math
|
||||
from arm.cad.helpers import cyl
|
||||
from arm.cad.arm_build import Ycyl, bx
|
||||
from arm.kinematics import PLATE_T, X_ARM, Y_COVER, Y_HUB, Y_REAR, Z_EL, Z_WR
|
||||
from arm.cad.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,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P08 腕偏转座:J5 俯仰 -> J6 滚转(复用 part_tools 的 pitch->roll 骨架)。"""
|
||||
from arm.kinematics import Z_R6, Z_WR
|
||||
from arm.cad.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,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""P09 工具法兰:末端法兰盘与 J6 直锁 hub。"""
|
||||
import math
|
||||
from arm.cad.helpers import cyl
|
||||
from arm.cad.arm_build import D2R
|
||||
from arm.kinematics import PLATE_T, X_ARM, Y_AXIS, Z_HUB6
|
||||
from arm.cad.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%)"),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""实测接口数值:电机模组、端面齿联轴器、配合公差与电机轴向公差。
|
||||
|
||||
全部 MEASURED 自 G:/Robot_v2/Model/Solidworks/电机.STEP 与配件(见 docs/interfaces.md)。
|
||||
改之前先确认量的是同一个基准面。"""
|
||||
|
||||
# 注:本段由 armcore.py 原样搬来,行内容与注释未作改动。
|
||||
|
||||
# ---------------- MEASURED: motor module (local frame: origin = PCBA rear face centre, +Z = output) ----
|
||||
M = dict(
|
||||
pcba_w=63.6, # square PCBA across flats
|
||||
pcba_t=9.0, # z 0 -> 9.0
|
||||
rearplate_d=60.0, # z 5.5 -> 10.5
|
||||
hous_d=60.0, # housing OD, z 10.5 -> 52.5
|
||||
hous_len=42.0,
|
||||
cover_d=60.30, # top cover OD (R30.15), z 52.5 -> 56.0
|
||||
cover_z=56.0, # cover front face = module total length
|
||||
flange_z=54.942, # 2nd-stage output flange face
|
||||
neck_d=36.0, # female neck bore in the cover
|
||||
neck_depth=1.0,
|
||||
centre_d=6.5, # centre bore in the flange
|
||||
centre_depth=4.0,
|
||||
m2_bcd=29.0,
|
||||
m2_clock=(8.05, 128.07, 248.06),
|
||||
m2_tap_d=1.85, # M2 pilot for self-tapping in printed material
|
||||
m2_depth=6.0,
|
||||
m2_clear_d=2.75,
|
||||
m2_cb_d=5.2,
|
||||
)
|
||||
# ---------------- MEASURED: face-spline coupling (existing, printed by the user) ------------------------
|
||||
C = dict(
|
||||
n_teeth=12, tooth_r0=19.0, tooth_r1=24.5, tooth_s=2.5,
|
||||
flange_t=4.50, part_z=5.560, # mirrored stack
|
||||
hub_r=27.0,
|
||||
gap=9.00, # cover-face to cover-face when closed
|
||||
clamp_od_r=30.0, clamp_len=7.80,
|
||||
lug_r1=37.0, lug_t=4.50,
|
||||
bolt_r=32.50, bolt_d=3.80,
|
||||
z0=1.66, z1=9.46, # clamp envelope in the flange frame (motor cover face = 1.060)
|
||||
)
|
||||
# axial stations, motor local frame:
|
||||
COVER_Z = M["cover_z"] # 56.0
|
||||
HUB_FACE_Z = COVER_Z + C["gap"] # 65.0 = mating face of the driven hub
|
||||
CLAMP_R = C["clamp_od_r"] + 0.6 # radial keep-out around the joint axis
|
||||
LUG_R = C["lug_r1"] + 1.0 # radial keep-out where the bolt lugs are
|
||||
# ---------------- clearances / print rules -------------------------------------------------------------
|
||||
CLR = dict(bore_slip=0.20, pilot=0.10, hole_3=3.4, hole_27=2.8, wall_min=2.0)
|
||||
FIT = dict(motor_bore_d=M["hous_d"] + 2 * 0.15, cover_cb_d=M["cover_d"] + 2 * 0.35)
|
||||
|
||||
# 电机【轴向长度】公差(用户 2026-10-07 要求:余量偏大稍微减去 1mm,由 2mm 改为 1mm)。
|
||||
# 这条链是"后端板螺钉 + 前端压盖螺钉"两个刚性夹持面,螺钉头一旦坐死在沉孔台阶上
|
||||
# 就再也不能进给,所以公差没法靠"拧紧"消化,只能靠两件事吃掉(见 arm_build.Joint.pocket
|
||||
# 与 motor_front_clamp):
|
||||
# 长出来的 0~+1mm -> 后端板 4xM3 螺钉头下垫 0~1mm 垫片圈(螺钉被垫高 -> 电机被拉回)
|
||||
# 短下去的 0~-1mm -> 电机尾端面与后端板承压面之间垫 0~1mm 垫片(承压面已按 MOTOR_TOL
|
||||
# 让空,见 Joint.pocket 的 REAR_SEAT)
|
||||
# 两边都不动压盖/凸台的几何,关节轴向长度不变。
|
||||
MOTOR_TOL = 1.0
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据注入与工程图:螺钉实例注入 mesh、四张工程图 SVG 生成。
|
||||
"""
|
||||
@@ -17,9 +17,16 @@ import os
|
||||
import sys
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from armcore import WEB, DOCS
|
||||
D = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8"))
|
||||
from arm.paths import DATA, DOCS
|
||||
|
||||
# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物
|
||||
# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。
|
||||
if __name__ != "__main__":
|
||||
raise RuntimeError(
|
||||
"draw_sheet 是脚本:顶层代码一执行就重写产物,不能 import。"
|
||||
"要用就跑 python -m arm.dataio.draw_sheet。")
|
||||
|
||||
D = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||||
P, LAY, MS = D["parts"], D["layout"], D["measured"]
|
||||
MT, CT = MS["M"], MS["C"]
|
||||
JOINTS = {j["id"]: j for j in D["joints"]}
|
||||
@@ -76,28 +83,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 +139,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 +150,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 +207,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 +246,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 +279,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 +311,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,18 +322,16 @@ 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):
|
||||
io.open(DOCS + "/" + n + ".svg", "w", encoding="utf-8").write(s)
|
||||
io.open(WEB + "/sheets.jsdata", "w", encoding="utf-8").write(json.dumps(sheets, ensure_ascii=False))
|
||||
print("sheets: %d (%d KB) -> docs/sheet_{front,joint,exploded,side}.svg" % (
|
||||
print("sheets: %d (%d KB) -> docs/sheet_{front,joint,exploded,side}.svg(查看器读这 4 个文件)" % (
|
||||
len(sheets), sum(len(s) for s in sheets) // 1024))
|
||||
os._exit(0)
|
||||
@@ -3,32 +3,43 @@
|
||||
|
||||
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
|
||||
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
|
||||
随动关节组),生成轻量三角网格后追加进 web/mesh_arm.json 的 parts 中。
|
||||
随动关节组),生成轻量三角网格后追加进 webapp/data/mesh_arm.json 的 parts 中。
|
||||
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
|
||||
|
||||
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
||||
头面 = 沉孔/通道的"承接台阶"(螺钉从外侧塞入,一直顶到孔径变小的那一圈台阶面),
|
||||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外。
|
||||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外(S05 除外:搭接壁太薄不能开沉孔)。
|
||||
S01 J1~J6 输出毂 3xM2:joint.L() 局部 (r=m2_bcd/2, HUB_M2_CLOCK 相位),
|
||||
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
|
||||
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
|
||||
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (±17.55, ±20.46, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
|
||||
S04 P03 前止脱 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 57.5)(沉孔 Ø6.0 台阶 x=6.5)
|
||||
S05 P01 分段对锁 4xM4:Ø4.4 贯穿柱身 x∈{-131,-93}, z∈{180,329},头在 -Y 立面 y=95(模型未设沉孔)
|
||||
S06 P09 工具法兰 4xM4:R20 @0/90/180/270°,台阶 z=97(沉孔 Ø7.0 深 4mm,法兰底面 z=93)
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (Ø6.5 圆柱沉孔台阶 x=-56.5,法兰外端面 -61,
|
||||
圆柱头沉入 1.5mm)
|
||||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 60.5)
|
||||
(Ø6.0×2 圆柱沉孔在 Ø10 凸台内,台阶 x=9.5,M3x8 圆柱头露 1mm,旋入电机前端面咬合 3.5mm)
|
||||
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
|
||||
前后各拧一颗,头在 y=95 / y=161 外露(搭接壁薄不开沉孔),直接拧入自攻成型
|
||||
S06 P09 工具法兰 4xM4:R22 @0/90/180/270°(BCD44,原 R20 会与 S01 的 Ø5.6 通道咬穿沉孔),台阶 z=97(Ø7.0×4 圆柱沉孔 + Ø4.4 贯穿通孔,法兰底面 z=93,
|
||||
M4 圆柱头与法兰底面齐平)
|
||||
S07 P02 中核心 4xM5 :x∈{-135,-99}, y∈{110,146},台阶 z=6(Ø10 沉孔深 6mm,头沉入 1mm)
|
||||
S08 P02 力臂 4xM5 :(-10,76/180) + (-185,88/168),台阶 z=17(Ø10 沉孔深 5mm,头与顶面齐平)
|
||||
S09 P03 安装台 4xM5 :x∈{-136,-94}, y∈{107,149},台阶 z=455(P03 榫头底面,Ø5.5 孔 454..495),
|
||||
头藏在 P01 立柱内腔里(孔深即 M5x35:455 -> 490,与模型 454..495 完全吻合)
|
||||
S09 P03 安装台 4xM4x16:x∈{-142.5,-87.5}, y∈{114.5,141.5},台阶 z=479(P03 鞍座顶面,
|
||||
从腔体正上方往下拧,Ø3.9 直孔穿过鞍座 9mm 再吃进 P01_C 顶环 7mm,自攻成型)
|
||||
S11 P03 横向防脱锁紧 1xM4x16:x=-148(立柱 −X 外端面),y=128,z=463,头朝 -X,
|
||||
沿 +X Ø3.9 直孔穿 11mm 后壁 + 榫头同径,直接拧入自攻(榫头内咬合 4.7mm)
|
||||
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
|
||||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||||
S12 电机前端压盖 4xM3x8:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) / J5(P07 夹 M5)
|
||||
/ J6(P08 夹 M6) 各 4 颗。
|
||||
配方唯一事实源 = 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() 内的注释。
|
||||
|
||||
长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模),
|
||||
这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。
|
||||
|
||||
运行:python src/screw_mesh.py -> 注入网格(纯数据,秒级,不动 CAD)
|
||||
再 python src/build_viewer.py -> 重新打包单文件查看器
|
||||
运行:python -m arm.dataio.screw_mesh -> 注入网格(纯数据,秒级,不动 CAD)
|
||||
再 python -m arm.viewer.build_viewer -> 重新打包单文件查看器
|
||||
arm_model.py 主流程末尾已自动调用本模块,重跑完整构建不会丢失螺钉。
|
||||
"""
|
||||
import json
|
||||
@@ -40,14 +51,10 @@ try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import human_layout as HL # noqa: E402
|
||||
|
||||
X_COL = -115.0 # 立柱中心(与 arm_model.py 一致)
|
||||
from arm.paths import DATA, DOCS
|
||||
from arm import kinematics as HL
|
||||
from arm.cad.part_tools import CLAMP_BCD, CLAMP_CLOCK
|
||||
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 大臂骨架",
|
||||
@@ -57,6 +64,12 @@ PROX = {"J2": "P04 肩叉", "J3": "P05 大臂旋转座", "J4": "P06 大臂骨架
|
||||
COLOR = 0xc3cad6 # 不锈钢冷色
|
||||
LMAX = {"S08": 17.0, "S10": 22.0} # 可见段截断:螺杆伸到底座底面(z=0)即止(台面/预埋螺母未建模)
|
||||
|
||||
# 电机尾端 4x M3 螺纹径向槽的实测孔位(电机.STEP):BCD55、相位 39.9°+90k,
|
||||
# 槽宽 2.5、径向可调约 ±1.25mm。S02(P04~P08 后端板)与 S03(P03 后法兰)共用;
|
||||
# 旧 35.1×40.9 矩形孔距对角两颗偏出螺纹槽 ~4.7mm(2026-10-02 用户指出"螺钉没有对齐")。
|
||||
REAR_SLOTS = tuple((27.5 * math.cos(math.radians(39.65 + 90.0 * k)),
|
||||
27.5 * math.sin(math.radians(39.65 + 90.0 * k))) for k in range(4))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 局部坐标系工具
|
||||
def _norm(v):
|
||||
@@ -123,8 +136,9 @@ def _geom(spec):
|
||||
nom = float(str(spec["螺纹"]).lstrip("Mm"))
|
||||
rs, L = nom / 2.0, float(spec["长度mm"])
|
||||
head = spec.get("头型", "")
|
||||
if "沉头" in head: # 沉头内六角:90° 锥,最宽处在承载面
|
||||
return dict(kind="csk", rs=rs, L=L, rh=2.0 * nom)
|
||||
if "沉头" in head: # 沉头内六角:90° 锥,最宽处(刃口)在承载面
|
||||
rh = {3.0: 3.15, 4.0: 3.5, 6.0: 5.5}.get(nom, 1.05 * nom) # 头径≈沉孔座位径(GB dk)
|
||||
return dict(kind="csk", rs=rs, L=L, rh=rh)
|
||||
if "外六角" in head: # 外六角:M6 对边 10,高 4
|
||||
af = {3.0: 5.5, 4.0: 7.0, 5.0: 8.0, 6.0: 10.0}.get(nom, 1.7 * nom)
|
||||
hh = max(3.0, 0.7 * nom)
|
||||
@@ -138,9 +152,10 @@ def _mesh(g):
|
||||
"""局部网格:头部 z ∈ [-hh,0](0 = 承载头面),杆部 z ∈ [0, L]。"""
|
||||
rs, L = g["rs"], g["L"]
|
||||
if g["kind"] == "csk":
|
||||
hh = g["rh"] - rs
|
||||
head = _merge(_side(rs, g["rh"], -hh, 0.0, 14), _cap(rs, -hh, 14, up=False),
|
||||
_cap(g["rh"], 0.0, 14, up=True))
|
||||
hh = g["rh"] - rs # 90° 锥:头高 = (dk-d2)/2
|
||||
head = _merge(_side(g["rh"], rs, 0.0, hh, 14), # 锥面自承载面收进材料(沉入沉孔座位)
|
||||
_cap(g["rh"], 0.0, 14, up=False), # 承载端外露环形面
|
||||
_cap(rs, hh, 14, up=True))
|
||||
elif g["kind"] == "hex":
|
||||
hh, rh = g["hh"], 0.98 * g["rh"]
|
||||
head = _merge(_side(rh, rh, -hh, 0.0, 6), _cap(rh, 0.0, 6, up=True),
|
||||
@@ -174,7 +189,8 @@ def instances(m2_bcd, m2_clock):
|
||||
z01 = {"J1": 63.0, "J2": 58.0, "J3": 58.0, "J4": 58.0, "J5": 58.0, "J6": 58.0}
|
||||
for tag in ("J1", "J2", "J3", "J4", "J5", "J6"):
|
||||
d = frame(tag)[1]
|
||||
for a in clock01:
|
||||
c_list = m2_clock if tag == "J1" else clock01
|
||||
for a in c_list:
|
||||
u, v = r01 * math.cos(math.radians(a)), r01 * math.sin(math.radians(a))
|
||||
out.append(("S01", to_global(tag, u, v, z01[tag]), d, tag,
|
||||
"%s 输出毂" % DRIVEN[tag], "φ%.0f @%.1f°" % (m2_bcd, a)))
|
||||
@@ -182,33 +198,64 @@ def instances(m2_bcd, m2_clock):
|
||||
for tag, tp in (("J2", 8.0), ("J3", 7.0), ("J4", 8.0), ("J5", 8.0), ("J6", 7.0)):
|
||||
d = frame(tag)[1]
|
||||
zt = -tp + 4.0 # 沉孔台阶 = -t_plate + cb_depth(4)
|
||||
for u in (-17.55, 17.55):
|
||||
for v in (-20.46, 20.46):
|
||||
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
|
||||
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
|
||||
for u, v in REAR_SLOTS:
|
||||
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
|
||||
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
|
||||
|
||||
d1 = frame("J1")[1]
|
||||
for u in (-17.55, 17.55):
|
||||
for v in (-20.46, 20.46):
|
||||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||||
"P03 后置法兰", "35.1×40.9 孔距 -> M1 尾端面"))
|
||||
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
for deg in (17.47, 107.48, 197.48, 287.47):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S04", to_global("J1", u, v, 57.5), d1, "root",
|
||||
"P03 前置止脱法兰", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
out.append(("S04", to_global("J1", u, v, 60.5), d1, "root",
|
||||
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
for u, v in REAR_SLOTS:
|
||||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||||
"P03 后置法兰", "BCD55 @45° 螺纹槽 -> M1 尾端面"))
|
||||
|
||||
# S12 电机前端压盖 -> 电机前端盖:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) /
|
||||
# J5(P07 夹 M5) / J6(P08 夹 M6) 各 4 颗。J2/J4/J5 三处压盖几何完全相同,
|
||||
# J3/J6 是同一套配方的 Z 轴版本(M6 压盖 2026-10-03 补齐,见 p_wrist_bracket)。
|
||||
# R35、沉孔台阶都在"前端盖外端面 + 2.0mm":Y 轴是 Y_COVER+2=158,Z 轴是 (z_r-56)-2=387。
|
||||
# 4xM3x8 @BCD55 17.8°+90k、沉孔 Ø6.0 深 3.0、压盖背面与前端盖 0.0 面压,
|
||||
# 相位与 S04(P03→M1 前端锁紧)同一套约定,全机 BCD55 孔位一致。
|
||||
#
|
||||
# 孔位必须按 motor_front_clamp 的写法直接算全局 (x,z)/(x,y),不能用 to_global:
|
||||
# 压盖成孔是 fu = px + BCD*cos、fv = pz/py + BCD*sin,即 sin 项直接加在**全局**该轴上;
|
||||
# 而 frame() 的 y 轴 = d × x,对 J2/J4/J5(d=+Y, x=+X)算出 y=(0,0,-1),
|
||||
# 经 to_global 会把 v 映到全局 -z,与成孔方向差一个符号
|
||||
# -> 12 颗螺钉实例整体偏 2*27.5*sin(17.8°) = 16.81mm,每颗都落在两孔之间的实体上
|
||||
# (布尔校验:M3 圆柱头沉在 Ø6.0 沉孔里却与压盖实体重叠 71.27 mm^3,装不进去)。
|
||||
# 故此处显式用全局平面复刻成孔公式,与建模脚本一一对应。
|
||||
for tag, prox in (("J2", "P04 肩叉"), ("J3", "P05 大臂旋转座"),
|
||||
("J4", "P06 大臂骨架"), ("J5", "P07 小臂骨架"),
|
||||
("J6", "P08 腕偏转座")):
|
||||
dt = frame(tag)[1]
|
||||
rear = tuple(HL.AXIS[tag]["rear"])
|
||||
roll = HL.AXIS[tag]["d"][1] == 0 # J3/J6 是 roll 电机(轴向 -Z)
|
||||
for deg in CLAMP_CLOCK:
|
||||
u = CLAMP_BCD * math.cos(math.radians(deg))
|
||||
v = CLAMP_BCD * math.sin(math.radians(deg))
|
||||
hp = (rear[0] + u, rear[1] + v, rear[2] - 58.0) if roll \
|
||||
else (rear[0] + u, rear[1] + 58.0, rear[2] + v)
|
||||
out.append(("S12", hp, dt, GRP_PAR[tag],
|
||||
"%s 前端压盖" % prox,
|
||||
"BCD55 @%.1f° -> %s 电机前端盖(前端夹紧,%s 轴)"
|
||||
% (deg, tag, "Z" if roll else "Y")))
|
||||
|
||||
for x in (X_COL - 16.0, X_COL + 22.0):
|
||||
for z in (180.0, 329.0):
|
||||
out.append(("S05", (x, 95.0, z), (0.0, -1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 榫缝对锁(另配 M4 螺母)" % z))
|
||||
"P01 立柱分段缝", "z=%.0f 前面 -Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
out.append(("S05", (x, 161.0, z), (0.0, 1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 背面 +Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
|
||||
for deg in (0.0, 90.0, 180.0, 270.0):
|
||||
x = HL.X_ARM + 20.0 * math.cos(math.radians(deg))
|
||||
y = HL.Y_AXIS + 20.0 * math.sin(math.radians(deg))
|
||||
# BCD44(R22):原 R20 与 S01 输出螺钉 Ø5.6 通道中心距仅 6.06 < 6.30,
|
||||
# 沉孔壁被咬穿成月牙形;加大到 R22 后余量 1.66mm。见 p_tool_flange 注释。
|
||||
x = HL.X_ARM + 22.0 * math.cos(math.radians(deg))
|
||||
y = HL.Y_AXIS + 22.0 * math.sin(math.radians(deg))
|
||||
out.append(("S06", (x, y, 97.0), (0.0, 0.0, -1.0), "J6",
|
||||
"P09 工具法兰", "R20 @%.0f° -> 末端工具" % deg))
|
||||
"P09 工具法兰", "R22 @%.0f° -> 末端工具" % deg))
|
||||
|
||||
for x in (-135.0, -99.0):
|
||||
for y in (110.0, 146.0):
|
||||
@@ -219,10 +266,17 @@ def instances(m2_bcd, m2_clock):
|
||||
out.append(("S08", (x, y, 17.0), (0.0, 0.0, 1.0), "root",
|
||||
"P02 防翻力臂", "榫头对锁 (%.0f,%.0f)(另配 M5 预埋螺母)" % (x, y)))
|
||||
|
||||
for x in (X_COL - 21.0, X_COL + 21.0):
|
||||
for y in (Y_COL - 21.0, Y_COL + 21.0):
|
||||
out.append(("S09", (x, y, 455.0), (0.0, 0.0, -1.0), "root",
|
||||
"P03 肩吊臂安装台", "穿透榫头 Ø5.5 孔(454..495)旋入立柱,头藏于 P01 立柱内腔"))
|
||||
for x in (X_COL - 27.5, X_COL + 27.5):
|
||||
for y in (Y_COL - 13.5, Y_COL + 13.5):
|
||||
# 方向 = 螺帽朝外法向(+Z,扳手从腔体上方进入);拧入方向 = -Z,Ø3.9 直孔自攻
|
||||
out.append(("S09", (x, y, 479.0), (0.0, 0.0, 1.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从腔体正上方拧入,穿鞍座 9mm 吃进立柱顶环 7mm(自攻成型)"))
|
||||
|
||||
# 横向防脱锁紧(M4x16):螺帽压在后壁外面 x=-148,沿 +X Ø3.9 直孔自攻(穿 11mm 后壁 + 榫头)
|
||||
out.append(("S11", (X_COL - 33.0, Y_COL, 463.0), (-1.0, 0.0, 0.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从立柱后方拧入,穿 11mm 后壁吃进榫头 4.7mm(自攻成型)"))
|
||||
|
||||
for x, y in ((-97.5, 53.0), (-97.5, 203.0), (120.0, 58.0),
|
||||
(120.0, 198.0), (-215.0, 58.0), (-215.0, 198.0)):
|
||||
@@ -233,11 +287,11 @@ def instances(m2_bcd, m2_clock):
|
||||
|
||||
# ---------------------------------------------------------------- 注入
|
||||
def main():
|
||||
path = os.path.join(_ROOT, "web", "mesh_arm.json")
|
||||
path = DATA + "/mesh_arm.json"
|
||||
print("读取 %s ..." % path, flush=True)
|
||||
d = json.load(open(path, encoding="utf-8"))
|
||||
M = d["measured"]["M"]
|
||||
specs = json.load(open(os.path.join(_ROOT, "docs", "screws.json"), encoding="utf-8"))
|
||||
specs = json.load(open(DOCS + "/screws.json", encoding="utf-8"))
|
||||
byid = {s["id"]: s for s in specs}
|
||||
|
||||
d["parts"] = [p for p in d["parts"] if not p.get("screw")] # 幂等:先清旧螺钉
|
||||
@@ -284,3 +338,10 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||||
sys.stdout.flush()
|
||||
os._exit(0)
|
||||
@@ -48,8 +48,12 @@ SWEEP_R = 92.0 # farthest point of the humerus from the shoulder pit
|
||||
POD_R = 47.0 # saddle drum outer radius about the roll axis
|
||||
|
||||
X_J1_HUB = 14.0 # J1 hub mating plane, forward of the humerus plate's ±44
|
||||
X_FORK_GAP = 0.0 # P04 前移量(用户裁定=0:前移会加大输出端悬伸弯矩,保持最短力臂)
|
||||
X_ARM = X_J1_HUB + PLATE_T / 2.0 + X_FORK_GAP # 18.5 the limb's own plane in x
|
||||
X_BAY_EXT = 3.0 # M1 侧插槽加长量(2026-10-01 用户裁定:插槽开口 5.06+3=8.06,
|
||||
# 比电机前端面多 3mm 斜插余量;P03 托架前端 8.5+3=11.5)
|
||||
X_FORK_GAP = 1.0 # P04 肩叉前移量:叉板背面(10.5+1.0=11.5)与 P03 托架端面(11.5)
|
||||
# 0.0 硬贴平(2026-10-02 用户两次要求"搞成平的",0.5mm 缝仍被判台阶)。
|
||||
# 注意:J1 相对转动时 M2 预紧会经此面产生摩擦阻力,若转动发涩可微松 M2。
|
||||
X_ARM = X_J1_HUB + PLATE_T / 2.0 + X_FORK_GAP # 19.5 the limb's own plane in x
|
||||
Z_R1 = X_J1_HUB - HUB # -51 J1 rear datum, measured in x: M1 lies behind the shoulder
|
||||
Z_CHEST1 = Z_RL - 140.0 # 470 torso mast top
|
||||
Z_CHEST0 = FOOT_H
|
||||
@@ -75,7 +79,26 @@ Y_M6 = Y_AXIS
|
||||
Y_CHEST = 62.0 # mast half width
|
||||
X_CHEST = 6.0 # chest plate half thickness
|
||||
|
||||
# ------------------------------------------------------------------ joint frames
|
||||
# ------------------------------------------------------------------ 台架(非常规构件)
|
||||
# P01 立柱 / P02 底盘 是【固定测试台架】,不是机器人本体的一部分(用户 2026-10-02
|
||||
# 确认);它在扫描中代表机械臂与**躯干/机身连接界面**的那块实体。因此:
|
||||
#
|
||||
# - 臂撞立柱 = 臂撞躯干,是真约束,不能忽略。sweep_v2 报出的组合卡死(J3 ±162°、
|
||||
# J4 ±18°、J5 ±36° 等,全部是工具法兰 P09 撞立柱 P01_A)就是这类真约束。
|
||||
# - 但台架的**位置与截面是一个假设**。躯干侧工作空间只和这个假设一样准:真机躯干
|
||||
# 挪后一点或外形不同,这些限位就变。所以台架本身不要为了放宽行程去改(那是改测试
|
||||
# 工装,不是改设计);要改的是"真机躯干占位"这个输入。
|
||||
# - `--no-stand` 仅用于诊断:给出不含躯干约束的自由空间行程,用来分辨某个限位到底是
|
||||
# 臂自身折叠产生的、还是躯干挡的。默认不排除。
|
||||
FIXTURE_PREFIX = ("P01_", "P02_")
|
||||
|
||||
|
||||
def is_fixture(name):
|
||||
"""该零件是否属于固定测试台架(代表躯干/安装界面,非机器人本体)。"""
|
||||
return name.startswith(FIXTURE_PREFIX)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 关节表
|
||||
# tag, cn, rear, d, xdir, holder of the motor, driven part
|
||||
# J1 roll +X fore-aft, in the shoulder base -> drives P04 肩叉
|
||||
# J2 pitch +Y outboard in the fork -> drives P05 大臂旋转座
|
||||
@@ -83,13 +106,17 @@ X_CHEST = 6.0 # chest plate half thickness
|
||||
# J4 pitch +Y in the humerus bone -> drives P07 小臂骨架
|
||||
# J5 pitch +Y in the forearm bone -> drives P08 腕偏转座
|
||||
# J6 roll -Z in the wrist bracket -> drives P09 工具法兰
|
||||
# J1 roll 电机沿 +X 轴旋转 5.35°,使尾端 4xM3 螺纹槽对齐 45°/135°/225°/315° 规整四角
|
||||
J1_DTHETA = math.radians(5.35)
|
||||
J1_XDIR = (0, math.cos(J1_DTHETA), math.sin(J1_DTHETA))
|
||||
|
||||
JOINTS = [
|
||||
("J1", "肩抬举", (Z_R1, Y_AXIS, Z_RL), (1, 0, 0), (0, 1, 0), "P03_肩吊臂_螺旋体35%_墙5底5_竖打", "P04_肩叉_螺旋体30%_墙5底5_竖打"),
|
||||
("J1", "肩抬举", (Z_R1, Y_AXIS, Z_RL), (1, 0, 0), J1_XDIR, "P03_肩吊臂_螺旋体35%_墙5底5_竖打", "P04_肩叉_螺旋体30%_墙5底5_竖打"),
|
||||
("J2", "肩俯仰", (X_ARM, Y_REAR, Z_SH), (0, 1, 0), (1, 0, 0), "P04_肩叉_螺旋体30%_墙5底5_竖打", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打"),
|
||||
("J3", "大臂自转", (X_ARM, Y_AXIS, Z_R3), (0, 0, -1), (1, 0, 0), "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "P06_大臂骨架_螺旋体25%_墙4底5_竖打"),
|
||||
("J4", "肘部俯仰", (X_ARM, Y_REAR, Z_EL), (0, 1, 0), (1, 0, 0), "P06_大臂骨架_螺旋体25%_墙4底5_竖打", "P07_小臂骨架_螺旋体30%_墙5底5_竖打"),
|
||||
("J5", "腕部俯仰", (X_ARM, Y_REAR, Z_WR), (0, 1, 0), (1, 0, 0), "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "P08_腕偏转座_螺旋体25%_墙4底5_竖打"),
|
||||
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "P09_工具法兰_螺旋体100%_墙4底5_竖打"),
|
||||
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "P09_工具法兰_螺旋体30%_墙4底5_竖打"),
|
||||
]
|
||||
AXIS = {t[0]: dict(rear=t[2], d=t[3], xdir=t[4]) for t in JOINTS}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""仓库内路径的唯一来源。
|
||||
|
||||
原先这些常量是硬编码的 G:/Robot_v2 绝对路径,仓库换盘符即全断;现在由本文件
|
||||
位置推出。REPO 的父目录就是原来的 ROOT(=G:/Robot_v2),所以外部 STEP 输入的解析
|
||||
结果与从前逐字相同。三个 ARM_* 环境变量是临时覆盖入口。
|
||||
"""
|
||||
import os
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/") # .../arm
|
||||
REPO = os.environ.get("ARM_WORKSPACE") or os.path.dirname(_HERE)
|
||||
ROOT = os.environ.get("ARM_ROOT") or os.path.dirname(REPO)
|
||||
MODEL = os.environ.get("ARM_MODEL_ROOT") or (ROOT + r"/Model/Solidworks")
|
||||
SRC_STEP = MODEL + r"/电机.STEP"
|
||||
HALF_STEP = MODEL + r"/配件/端面齿半联轴器_相同件x2.STEP"
|
||||
CLAMP_STEP = MODEL + r"/配件/端面齿联轴器_抱箍_两片装配.STEP"
|
||||
OUT = REPO + r"/out"
|
||||
WEB = REPO + r"/web" # 旧单文件产物目录,S8 删除;新代码不要再往里写
|
||||
# 查看器运行时数据与生成脚本(gitignore,由 build_viewer 重写)
|
||||
DATA = REPO + r"/webapp/data"
|
||||
GEN = REPO + r"/webapp/generated"
|
||||
DOCS = REPO + r"/docs"
|
||||
# 可达包络 FK 采样的缓存(ws_cloud.npz / ws_metrics.json)。原先躺在 gitignore 的
|
||||
# scratch/ 里,构建却依赖它——缺了新克隆就跑不出 ENV 件。
|
||||
CACHE = REPO + r"/data/cache"
|
||||
# 前端源码目录与分发产物
|
||||
WEBAPP = REPO + r"/webapp"
|
||||
DIST = REPO + r"/dist"
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""查看器侧:数据打包、本地静态服务、headless 截图。
|
||||
"""
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""把生成物落到 webapp/,并打一份单文件分发版。
|
||||
|
||||
单文件时代这里做 8 路字符串替换再写 3 份 16MB 副本;现在前端本身就是 webapp/ 下的
|
||||
文件,本脚本只剩两件事:
|
||||
write_data() 网格/工程图/行程/碰撞/螺钉 -> generated/00_data.js(普通脚本 const)
|
||||
build_single() 把 webapp/ 的 css/js 原样内联成 dist/机器人手臂3D查看器.html(分发用)
|
||||
|
||||
S5 数据改 fetch() 后 write_data() 的 00_data.js 退役;S8 由 pack.py 的受限 ESM 内联器
|
||||
接管 build_single()。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from arm.paths import DATA, DIST, DOCS, GEN, WEBAPP
|
||||
|
||||
# 与 SHEET_METAS 的顺序一致;工程图读 draw_sheet 写出的真 SVG,
|
||||
# 不再维护第二份 sheets.jsdata(单文件时代的内联载体,内容与 SVG 完全重复)。
|
||||
SHEET_ORDER = ["sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"]
|
||||
|
||||
|
||||
def _read(path, dflt=None):
|
||||
if os.path.exists(path):
|
||||
return io.open(path, encoding="utf-8").read()
|
||||
if dflt is None:
|
||||
raise SystemExit("缺少查看器数据文件: %s(先跑 python -m arm.build_model)" % path)
|
||||
return dflt
|
||||
|
||||
|
||||
def write_data():
|
||||
t0 = time.time()
|
||||
print("【数据】汇总查看器数据 -> webapp/generated/00_data.js", flush=True)
|
||||
os.makedirs(GEN, exist_ok=True)
|
||||
|
||||
mesh = _read(DATA + "/mesh_arm.json")
|
||||
svgs = [_read(DOCS + "/%s.svg" % n) for n in SHEET_ORDER]
|
||||
trv = _read(DOCS + "/travel.json", "[]")
|
||||
col = _read(DOCS + "/collide.json", '{"caps":[],"parts":{}}')
|
||||
scr = _read(DOCS + "/screws.json", "[]")
|
||||
print(" - 网格 %d KB / 工程图 %d 张 / travel %d B / collide %d KB / screws %d B"
|
||||
% (len(mesh) // 1024, len(svgs), len(trv), len(col) // 1024, len(scr)), flush=True)
|
||||
|
||||
out = ["/* 生成物,勿手改:由 python -m arm.viewer.build_viewer 写入。",
|
||||
" 普通脚本顶层 const —— 经典脚本共享全局词法作用域,后续脚本直接可见。 */"]
|
||||
for name, raw in (("DATA", mesh), ("TRAVEL", trv), ("COLLIDE", col), ("SCREWS", scr)):
|
||||
out.append("const %s = %s;" % (name, raw.strip()))
|
||||
out.append("const SHEETS = %s;" % json.dumps(svgs, ensure_ascii=False))
|
||||
text = "\n".join(out) + "\n"
|
||||
tmp = GEN + "/00_data.js.tmp"
|
||||
io.open(tmp, "w", encoding="utf-8").write(text)
|
||||
os.replace(tmp, GEN + "/00_data.js")
|
||||
print(" 完成:%d KB, %.2fs ✓" % (len(text) // 1024, time.time() - t0), flush=True)
|
||||
|
||||
|
||||
def build_single():
|
||||
"""分发用单文件:仍是普通脚本,所以内联是平凡的(S5 变 ESM 后交给 pack.py)。"""
|
||||
t0 = time.time()
|
||||
os.makedirs(DIST, exist_ok=True)
|
||||
html = _read(WEBAPP + "/index.html")
|
||||
total = len(html)
|
||||
|
||||
def sub(m, kind):
|
||||
nonlocal total
|
||||
body = _read(WEBAPP + "/" + m.group(1))
|
||||
total += len(body)
|
||||
return ("<style>\n" + body + "\n</style>") if kind == "css" \
|
||||
else ("<script>\n" + body + "\n</script>")
|
||||
|
||||
out = re.sub(r'<link rel="stylesheet" href="([^"]+)">', lambda m: sub(m, "css"), html)
|
||||
out = re.sub(r'<script src="([^"]+)"></script>', lambda m: sub(m, "js"), out)
|
||||
|
||||
left = re.findall(r'(?:src|href)="(?!data:)([^"]+)"', out)
|
||||
assert not left, "单文件里仍有外部引用: %s" % left[:5]
|
||||
assert len(out) >= total * 0.95, "内联后 %d 远小于各部之和 %d,疑似漏文件" % (len(out), total)
|
||||
dst = DIST + "/机器人手臂3D查看器.html"
|
||||
# Windows 上被浏览器打开的 html 是独占锁;写不进去就直接报,不要静默留旧版。
|
||||
io.open(dst, "w", encoding="utf-8").write(out)
|
||||
print(" - 单文件分发版 %.2f MB, %.2fs ✓ %s"
|
||||
% (os.path.getsize(dst) / 1e6, time.time() - t0, dst), flush=True)
|
||||
|
||||
|
||||
def rebuild_viewer():
|
||||
"""build_model 流水线的最后一步(沿用旧函数名,调用点不改)。"""
|
||||
write_data()
|
||||
build_single()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rebuild_viewer()
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""查看器的本地静态服务器(前端拆分后,多文件 + ES Module + Worker 都需要 http 才能跑)。
|
||||
|
||||
python -m arm.viewer.serve [--port 8123] [--print-only]
|
||||
|
||||
要点:
|
||||
- docroot = 仓库根,`/` 直接给 webapp/index.html;/docs/*.json 这类数据同域可读。
|
||||
- Cache-Control: no-store —— 重新生成后旧标签页仍显示旧版是这里踩过的坑,
|
||||
过去只能人肉 Ctrl+F5;不缓存之后刷新即新构建。
|
||||
- 显式 MIME:Windows 上 .js 常被注册成 text/plain,那会让 <script type=module>
|
||||
静默不执行(页面全黑且没有明显报错),所以这里写死。
|
||||
- 屏蔽 .git / backups / 定位图 / out 等目录,不把整个仓库暴露到端口上。
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from arm.paths import REPO
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
DENY = (".git", "backups", ".bak", "定位图", "out", "scratch", "node_modules")
|
||||
MIME = {
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".npz": "application/octet-stream",
|
||||
".png": "image/png",
|
||||
".wasm": "application/wasm",
|
||||
}
|
||||
|
||||
|
||||
class H(SimpleHTTPRequestHandler):
|
||||
# HTTP/1.1 + keep-alive + 大缓冲:默认 HTTP/1.0 每请求一条连接,
|
||||
# 4 个 headless Edge 同时各拉 14MB 数据脚本时会把页面加载拖到截图之后。
|
||||
protocol_version = "HTTP/1.1"
|
||||
wbufsize = 1 << 20
|
||||
# 按 (路径, mtime) 缓存字节:同一份 14MB 数据在一次批量截图里要发几十次。
|
||||
_CACHE = {}
|
||||
|
||||
def __init__(self, *a, **k):
|
||||
super().__init__(*a, directory=REPO, **k)
|
||||
|
||||
def _cached_bytes(self, path):
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
return None
|
||||
key = (path, st.st_mtime_ns, st.st_size)
|
||||
body = H._CACHE.get(key)
|
||||
if body is None:
|
||||
with open(path, "rb") as f:
|
||||
body = f.read()
|
||||
if st.st_size <= 64 << 20:
|
||||
if len(H._CACHE) > 12:
|
||||
H._CACHE.clear()
|
||||
H._CACHE[key] = body
|
||||
return body
|
||||
|
||||
def send_head(self):
|
||||
rel = self.path.split("?", 1)[0].split("#", 1)[0].lstrip("/").replace("\\", "/")
|
||||
top = rel.split("/")[0].lower()
|
||||
if top in [d.lower() for d in DENY] or ".." in rel:
|
||||
self.send_error(404, "denied")
|
||||
return None
|
||||
fs = self.translate_path(self.path)
|
||||
if os.path.isfile(fs):
|
||||
body = self._cached_bytes(fs)
|
||||
if body is not None:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", self.guess_type(fs))
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return None
|
||||
return super().send_head()
|
||||
|
||||
def guess_type(self, path):
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
return MIME.get(ext) or super().guess_type(path)
|
||||
|
||||
|
||||
def do_GET(self):
|
||||
"""`/` 必须 302 到 /webapp/index.html,不能在原地把内容发出去:
|
||||
那样文档基址是 `/`,页面里所有相对路径(styles/… js/… vendor/…)都会
|
||||
解析到 /styles/base.css 之类而 404 —— 实测整页样式与脚本全丢。"""
|
||||
if self.path.split("?", 1)[0].rstrip("/") in ("", "/"):
|
||||
q = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||
self.send_response(302)
|
||||
self.send_header("Location", "/webapp/index.html" + ("?" + q if q else ""))
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
super().do_GET()
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
super().end_headers()
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
|
||||
def serve(port=8123):
|
||||
srv = None
|
||||
for p in range(port, port + 20):
|
||||
try:
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", p), H)
|
||||
port = p
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if srv is None:
|
||||
raise SystemExit("端口 %d..%d 都被占用" % (port, port + 19))
|
||||
url = "http://127.0.0.1:%d/" % port
|
||||
print("docroot %s" % REPO, flush=True)
|
||||
print("查看器: %s (Ctrl+C 停止;已禁用缓存,改完刷新即新构建)" % url, flush=True)
|
||||
srv.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser(description="查看器本地静态服务器")
|
||||
ap.add_argument("--port", type=int, default=8123)
|
||||
serve(ap.parse_args().port)
|
||||
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Headless Edge screenshots of the viewer, for the visual self-check loop."""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
import glob
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from arm.paths import DOCS, REPO
|
||||
SHOTS = DOCS + "/shots"
|
||||
# 虚拟时间预算:多文件版每页要拉 14MB 数据脚本 + 668KB three,4 并发下 20s 会不够(实测 8/10 视图两轮不一致)
|
||||
# 时可上调。默认仍给 20000,用 ARM_VTB 覆盖做对照实验。
|
||||
VTB = os.environ.get("ARM_VTB", "40000")
|
||||
WORKERS = int(os.environ.get("ARM_SHOT_WORKERS", "2"))
|
||||
# 前端拆分后是多文件 + Worker,只能走 http(file:// 下模块与 Worker 会被 CORS 拦掉)。
|
||||
PORT = int(os.environ.get("ARM_PORT", "8123"))
|
||||
BASE = "http://127.0.0.1:%d/webapp/index.html" % PORT
|
||||
CAND = [r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"]
|
||||
EDGE = next((c for c in CAND if os.path.exists(c)), None)
|
||||
if not EDGE:
|
||||
EDGE = glob.glob(r"C:\Users\*\AppData\Local\Microsoft\Edge\*\msedge.exe")
|
||||
EDGE = EDGE[0] if EDGE else None
|
||||
if not EDGE:
|
||||
raise SystemExit("msedge not found")
|
||||
|
||||
VIEWS = {
|
||||
"01_iso": "?cam=iso",
|
||||
"02_front": "?cam=front",
|
||||
"03_side": "?cam=side",
|
||||
"04_top": "?cam=top",
|
||||
"05_exploded": "?cam=iso&expl=100",
|
||||
"06_wrist": "?cam=wrist&xray=0",
|
||||
"07_joint": "?cam=joint",
|
||||
"08_pose": "?cam=iso&j2=35&j3=60&j4=-25&j1=25",
|
||||
"09_sheet": "?sheet=1",
|
||||
"10_solid": "?cam=iso&xray=0",
|
||||
"11_tool_flange": "?cam=wrist&xray=0&j6=45",
|
||||
"12_elbow_flare": "?cam=elbow&xray=0",
|
||||
"13_forearm_side": "?cam=side&xray=0&rd=340&ty=245&tz=-135",
|
||||
|
||||
# 工程图 / 装配向导 / 运动模拟三种模式。URL 参数(?view=&sheet=&theme=&guide=)就是
|
||||
# 为这几条截图加的:不把它们纳入基线,拆 CSS 与那 700 行工程图 JS 时等于没有关卡。
|
||||
|
||||
"m1_sheet_front": "?view=sheet",
|
||||
"m2_sheet_joint": "?view=sheet&sheet=1",
|
||||
"m3_sheet_exploded": "?view=sheet&sheet=2",
|
||||
"m4_sheet_side": "?view=sheet&sheet=3",
|
||||
"m5_guide_step1": "?view=sheet&sheet=guide&guide=0",
|
||||
"m6_guide_step5": "?view=sheet&sheet=guide&guide=4",
|
||||
"m7_sheet_blueprint": "?view=sheet&theme=blueprint",
|
||||
"m8_sheet_dark": "?view=sheet&theme=dark",
|
||||
"m9_motion": "?view=motion",
|
||||
|
||||
"harness_full_iso": "?cam=iso&xray=0",
|
||||
"harness_overview": "?az=-0.7&po=1.2&rd=520&tx=0&ty=400&tz=-120&xray=0",
|
||||
"harness_p04_p05": "?az=0.8&po=1.35&rd=280&tx=25&ty=470&tz=-128&xray=0",
|
||||
"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_p07_window": "?az=-0.65&po=1.25&rd=260&tx=25&ty=275&tz=-130&xray=0",
|
||||
"harness_p07_xray": "?az=-0.65&po=1.25&rd=260&tx=25&ty=275&tz=-130&alpha=0.35&xray=1",
|
||||
"harness_xray_full": "?cam=iso&xray=1",
|
||||
"harness_xray_internal": "?az=-0.7&po=1.2&rd=520&tx=0&ty=400&tz=-120&alpha=0.35&xray=1",
|
||||
}
|
||||
os.makedirs(SHOTS, exist_ok=True)
|
||||
|
||||
want = sys.argv[1:] or list(VIEWS)
|
||||
|
||||
|
||||
def take_shot(nm):
|
||||
q = VIEWS[nm]
|
||||
out = "%s/%s.png" % (SHOTS, nm)
|
||||
import time, tempfile
|
||||
t_bust = int(time.time() * 1000)
|
||||
# serve.py 已下发 Cache-Control: no-store,不再需要 _t= 破缓存
|
||||
target = q if q.startswith("http") else BASE + q + (
|
||||
"&still=1" if "?" in q else "?still=1")
|
||||
udd = tempfile.mkdtemp(prefix="eshot")
|
||||
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
||||
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
||||
"--user-data-dir=" + udd,
|
||||
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
||||
"--hide-scrollbars", "--force-device-scale-factor=1",
|
||||
"--window-size=1500,860", "--virtual-time-budget=" + VTB,
|
||||
"--screenshot=" + out, target]
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
||||
shutil.rmtree(udd, ignore_errors=True)
|
||||
ok = os.path.exists(out) and os.path.getsize(out) > 20000
|
||||
err = (r.stderr or b"").decode("utf-8", errors="ignore").strip()[:90].replace("\n", " ")
|
||||
sz = os.path.getsize(out) if os.path.exists(out) else 0
|
||||
print("%-18s %s %8s %s" % (nm, "OK " if ok else "FAIL", sz, err), flush=True)
|
||||
return ok
|
||||
|
||||
def warmup():
|
||||
"""拉一次最大的那个数据脚本,成功才算服务可用。"""
|
||||
import urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen(BASE, timeout=5) as r:
|
||||
r.read()
|
||||
with urllib.request.urlopen("http://127.0.0.1:%d/webapp/generated/00_data.js" % PORT,
|
||||
timeout=30) as r:
|
||||
n = len(r.read())
|
||||
return n > 1000000
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def ensure_server():
|
||||
"""端口没开就自己拉一个 serve 子进程,跑完回收;不让截图依赖"你先把服务开着"。"""
|
||||
import socket
|
||||
import time
|
||||
sk = socket.socket()
|
||||
try:
|
||||
sk.connect(("127.0.0.1", PORT))
|
||||
sk.close()
|
||||
return None
|
||||
except OSError:
|
||||
pass
|
||||
errf = open(os.path.join(REPO, "scratch", "_serve_err.txt"), "wb")
|
||||
p = subprocess.Popen([sys.executable, "-m", "arm.viewer.serve", "--port", str(PORT)],
|
||||
cwd=REPO, stdout=subprocess.DEVNULL, stderr=errf)
|
||||
for _ in range(60):
|
||||
time.sleep(0.25)
|
||||
sk = socket.socket()
|
||||
try:
|
||||
sk.connect(("127.0.0.1", PORT))
|
||||
sk.close()
|
||||
if warmup():
|
||||
print("serve 已拉起 :%d(首屏数据已预热)" % PORT, flush=True)
|
||||
return p
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
sk.close()
|
||||
except Exception:
|
||||
pass
|
||||
p.kill()
|
||||
err = open(os.path.join(REPO, "scratch", "_serve_err.txt"), "rb").read().decode("utf-8", "ignore").strip()[-400:]
|
||||
raise SystemExit("serve 拉不起来:端口 %d 可能被占;子进程输出:\n%s" % (PORT, err))
|
||||
|
||||
if __name__ == "__main__":
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
srv = ensure_server()
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
|
||||
results = list(executor.map(take_shot, want))
|
||||
finally:
|
||||
if srv:
|
||||
srv.terminate()
|
||||
|
||||
print("edge:", EDGE)
|
||||
print("all shots ok:", all(results))
|
||||
@@ -0,0 +1,71 @@
|
||||
# ML 运行手册审查(ml/README.md)· 精简版
|
||||
|
||||
八轮审查(`0218d7f` → 本次)的已解决条目全部删去,原文在 git 历史里。本文件只留:实测数字 / 已定判据 / 仍待真数据回答的问题 / 正式训练步骤。
|
||||
|
||||
环境:本机 CBB-003(RTX 3090,训练可用 19–20 GB;28 核;Python 3.12 + torch 2.10;Edge + onnxruntime-web 1.20.1,已入库 `webapp/vendor/ort/`)。
|
||||
当前结论:第 1–6 步本机实测可跑;R4 通过;自检 7/8 通过(唯一失败项是 Windows 限制,已接受);**审查条目已全部处置,下一步直接正式训练。**
|
||||
|
||||
---
|
||||
|
||||
## 一、实测数字(对照用)
|
||||
|
||||
| 项 | 数字 | 口径 |
|
||||
|---|---|---|
|
||||
| 对拍 300 条 | PASS(fk 3.4e-13 mm、特征 6e-8、间隙梯度 1.4e-6) | 第 2 步 |
|
||||
| 生成 | 1.56 s/条 | 第 4 步,80 条 |
|
||||
| 统计 | keepRate 0.977、samplesPerTask 36.5、cert.minGap p50 4.4 / p90 5.9 mm | 88 条,未加 3 mm 过滤 |
|
||||
| 预训练 | 4.92 M 参数、6208 样本/s、60k 步 ≈ 2.8 h、显存峰值 2.76 GiB | batch 1024 |
|
||||
| APG 显存 | batch 48 / plans 150 → 13.08 GiB;batch 128 必 OOM | d=192,1 步 |
|
||||
| R4 部署口径 | wasm·16 线程·proxy P99 **12.67 ms**(< 15 通过);WebGPU P99 27.09,不用 | `feat_dyn` 新模型 |
|
||||
| 自检 | 7/8,3.5 分钟;`train_term=false`(Windows 下 TERM 送不到 python,超时靠 STOP 文件 + 每 10 分钟存档) | `selftest.json` |
|
||||
|
||||
## 二、已定的判据与做法
|
||||
|
||||
| 事项 | 定为 |
|
||||
|---|---|
|
||||
| R9(真查看器、训练好的模型、30 道贴柱题) | 稳态 `planWarmP99` < 45 ms、运动中饥饿 < 1%、认证超时占比 < 5%;冷启动 P99、刹停率、`tickGapMax` 只报 |
|
||||
| R9 不过 | 几何查询(特征、投影、认证)搬进 Worker,主线程只执行 |
|
||||
| APG | batch 48、steps 8000(总任务量同原 128×3000),本进程显存上限浮动(整卡 − 已用 − 1 GB 余量,见 `ml/common/vram.py`) |
|
||||
| 离线 ort | 只用入库本地副本,缺失即报错,不退回 CDN |
|
||||
| 训练余量 3 mm | `stats.json` 间隙 p10 ≥ 3 mm 保持,否则降到 p10 |
|
||||
| `R2_STRICT` | `pool_check.independent=true` 且截距 < 0.10 时打开 |
|
||||
| 共识过滤 | 门 1 < 0.90 自动开 |
|
||||
| 分歧 | 实现方为准,下一轮数据定 |
|
||||
|
||||
## 三、仍待真数据回答(正式训练会自动产出,不单独验证)
|
||||
|
||||
1. `gapDrop`:3 mm 过滤的真实剔除比例 → `stats.json`。
|
||||
2. R2 池无关性 → `r2.json` 的 `pool_check`。
|
||||
3. R8 出题耗时 → `policy_bench.json` 的 `genS`。
|
||||
4. APG 收敛与机时(batch 48 下首测)→ APG 日志。
|
||||
5. 真模型 R9 → `r9_browser.json`。
|
||||
|
||||
## 三′、首次正式预训练失败(2026-10-18,21850 步)与修正
|
||||
|
||||
现象:闭环到达 0–0.2%、穿柱 77–97%;MF 残差 2 万步停在 7–8、17k–19k 尖峰到 96;梯度 100% 被裁剪。用 last.pt 实测定位:
|
||||
1. **多峰头塌成单峰(主因)**:同一状态 8 个候选间标准差 0.030、动作幅度 0.151。FK / 平滑损失加在 `one_step(随机噪声)` 上去对齐这一条老师 Y,最优解是条件均值 E[Y|X],左绕 / 右绕取平均正好穿柱。→ 改加在 r = t、t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上。
|
||||
2. **MeanFlow 目标失控**:∂u/∂t 均方根 6–12,而 v 约 1.1,目标被自激的 JVP 项主导。时间嵌入 sin(1000·t) 太陡,JVP 又在 bf16 下算。→ 最高角频率 50;MF 分支 fp32;前 5000 步 p_neq = 0。
|
||||
3. **闭环分布偏移被低估(推断)**:增广只有 1°。→ 增广 40% / 3° / 12 步收回;新增 DAgger 阶段(策略走到的状态请老师重规划,2 轮);开训前加老师轨迹回放自检;每次评估加开环误差与候选多样性。
|
||||
|
||||
## 四、正式训练(三段,每段跑完回传)
|
||||
|
||||
| 段 | 命令 | 耗时 | 回传 |
|
||||
|---|---|---|---|
|
||||
| 数据 | `FROM=kin TO=prep bash ml/pipeline.sh all` | 数小时 | `ml/data/stats.json`、`ml/data/r2.json`、`ml/runs/r4.json` |
|
||||
| 训练(含 DAgger) | `FRESH_FROM=prep FROM=prep TO=m5 bash ml/pipeline.sh all`(增广改了,样本要重打包;gen 不重跑) | 约 9–11 小时(未实测:prep 0.5 + 预训练约 5(fp32 变慢)+ DAgger 约 3.5 + m5 0.3) | `ml/runs/replay_teacher.json`、`ml/runs/base/log.jsonl`、`ml/runs/m5_gate_*.json` |
|
||||
| 后训练与评测 | `FROM=apg TO=accept bash ml/pipeline.sh all` | 约 10–20 小时 | `ml/runs/acceptance.json`、`policy_bench.json`、`r9_browser.json` |
|
||||
|
||||
中途出错:把出错信息发我,修完用 `FROM=<出错阶段>` 接着跑,前面的不重做。
|
||||
|
||||
---
|
||||
|
||||
## 七、第一次真跑(2026-10-18,网页「开始训练」)暴露的一处阻塞 + 门 1 实测
|
||||
|
||||
**阻塞(已修,一行)**:`pipeline.sh` 的 `run() { echo "== $*"; "$@"; }` 把命令回显写进 **stdout**,而 `gate1` 是 `run node ml/gen/consistency.cjs … | tee ml/data/consistency.json.tmp` → 回显被一起存进 `consistency.json`,文件首行变成 `== node ml/gen/consistency.cjs 300 4242`。`gen` 阶段读它判 CONSENSUS 时 `json.load` 当场 `JSONDecodeError: Expecting value: line 1 column 1 (char 0)`,`set -e` 下整条 `all` 停在 gen(阶段 rc:kin 0、r4 0、gate1 0、gen 1)。
|
||||
- 与平台无关,沙箱 Linux 同样会中,只是没跑到这一步。
|
||||
- 修法:回显改走 stderr(`echo "== $*" >&2`)。全仓只有 `gate1` 这一处把 `run` 的输出重定向进文件(`check_r2` 等用 `--out` 自写,不受影响)。
|
||||
- 已写坏的 `ml/data/consistency.json` 就地剥掉首行复用,**没有重跑 25–30 分钟的门禁**。
|
||||
- 建议实现方顺手加一条自检:任何被 `tee`/`>` 落盘成 JSON 的产物,读之前先确认首字符是 `{`。
|
||||
|
||||
**门 1 实测(第一次真数据)**:`tasks 300 / pairs 1133 / consistentRate 0.838 / consistentByGateRule 0.838 / gate FAIL / deterministic 300/300`,分类看 `C 0.824`、`B 0.844`,`devP50 3.6`、`devP90 145.1`。
|
||||
→ 0.838 < 0.90,**自动走共识分支**(`--consensus 4`),与方案 R1 首测 78% 同一侧;`worstAxisOfInconsistent [43,4,42,40,16,39]` 说明跳变在各轴上分布均匀,不是某一根轴的系统性问题。gen 因此按 4–5 h 那一档估,不是 1 h。
|
||||
@@ -0,0 +1,530 @@
|
||||
# Transformer 避障运动策略:最终方案
|
||||
|
||||
> 基于 `main@3c7c814`(贴立柱修复后的规划器)。本文是方案,代码尚未编写。
|
||||
> 训练机:RTX 3090 24 GB,训练可用 19–20 GB(桌面常驻 4–5 GB,进程硬帽 0.78)。部署:浏览器(后续真机)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 定位与边界
|
||||
|
||||
| 项目 | 决定 |
|
||||
|---|---|
|
||||
| 部署形态 | **无运行时老师**:运行时只有「神经策略 + 碰撞安全层」,不加载 IK 大表、PRM 路网、A*/RRT,也不调用 `solveDyn` / `routeRT` |
|
||||
| 运行方式 | **闭环滚动执行**:执行频率 50 Hz(每步 20 ms);每 60 ms 读一次当前关节角和目标,推理一段 16 步动作块,安全层认证后提交前 3 步 |
|
||||
| 碰撞安全层 | 不做搜索,只对模型给出的候选做**距离下界认证**(§8.3),不安全就换候选或沿已认证刹停段停下 |
|
||||
| 老师的角色 | 只在**离线**出现:生成预训练数据;后训练以可微碰撞代价为主(§7) |
|
||||
| 不做的事 | 不学动力学(速度/加速度上限由时间律与插补器保证);不用相机/点云(环境固定:台架 + P01 立柱,几何已知) |
|
||||
| 收益目标 | 冷启动(新目标到第一个动作)< 10 ms;每次推理 + 安全认证 < 20 ms;消除现有拖拽重规划 P99 约 321 ms 的长尾 |
|
||||
| 底线 | 实时拖拽**碰撞帧 = 0**(由安全层距离认证保证);到达率、行程在 §9 指标上不低于现有规划器 |
|
||||
|
||||
**关于"风险":** 论文证据只能说明"别人在相近条件下做成过",没有一篇论文的条件(低维状态、单根固定立柱、约 4M 参数、浏览器推理、30/30)与本项目完全一致。因此本方案中只有安全层是**硬保证**;每个设计点都已定为唯一选择(§1.1),只有 MeanFlow 头带一个预先写定触发条件的回退(碰撞与否不依赖模型好坏);其余每一项都配有证据等级、本仓库可测的**门禁**(不过不进下一步)和**回退**(§10)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 方案总览
|
||||
|
||||
| 模块 | 最终选择 | 证据等级 | 关键依据 |
|
||||
|---|---|---|---|
|
||||
| 模型规模 | 约 4.9M 参数编码器-解码器 Transformer,不用 VLA 大模型 | A | IMPACT 4.67M、300 Hz;π0 3B 单次预填充 46 ms(4090) |
|
||||
| 数据 | 规划器离线造 5 万条任务 → 约 200 万"状态→动作块"样本 | B | MπNets、DRP、ELMP 均用规划器老师 |
|
||||
| 时间律 | 匀速巡航 + 梯形加减速,只依赖"当前速度 + 剩余路程";当前速度作输入 | B | MπNets 将老师路径重定时为匀速 |
|
||||
| 动作表示 | 16 步 × 6 轴关节增量 Δq | A | DRP;Diffusion Policy(位置式优于速度式) |
|
||||
| 动作头 | **MeanFlow 一步生成头**,每次并行采 8 个噪声 → 8 条候选 | B | 沙箱双模态实验(§2.4);MP1 约 6.8 ms |
|
||||
| 块间连续 | 训练时前缀条件化:输入已提交未执行的步 | B | 2512.05964:推理零开销,d≥2 时优于推理时 RTC |
|
||||
| 碰撞修正 | 输出后 1 步解析胶囊距离梯度投影(每轴 ≤ 0.5°,仅 d < 余量时) | B | DiffusionSeeder:少步生成下在环引导收益很小;投影在已知环境下够用 |
|
||||
| 候选排序 | 与已提交块重叠步的一致性打分 + 价值头 | B | BID:一致性打分 + 多样本相对提升 32% |
|
||||
| 预训练 | 行为克隆 + FK 胶囊端点损失 | B | ELMP 式 FK 损失 |
|
||||
| 后训练(主) | APG:可微运动学展开 + 胶囊距离代价反传 | A | ELMP:碰撞率 31.3%→8.9%;1 万示范 + APG 胜 60 万纯 BC |
|
||||
| 后训练(辅) | 无老师挖错循环 + 门禁 | B | DRP 迭代微调 +45% |
|
||||
| 安全层 | 距离下界认证 + 可刹停不变式 | A | Schwarzer 2005 连续认证;Thumm & Althoff 2022 failsafe |
|
||||
| 30/30 手段 | 子目标头、价值头、冷启动/卡死时短前瞻、卡死换向 | B / C | AWE、SHAC、BID |
|
||||
| 老师 | PRM 路网 A* + 捷径平滑(确定性);A* 失败才用 `solveDyn` | B | MπNets:老师不一致碰撞率 13.78%,一致老师 0.94% |
|
||||
| 推理后端 | onnxruntime-web wasm-SIMD 多线程,推理放代理 Worker(不占主线程),线程数 min(16, ⌊0.6×逻辑核⌋);R4 探针同测 WebGPU,wasm 不达标而 WebGPU 达标则部署用 WebGPU | C | 小模型下 WebGPU 调度开销通常占主导,以 R4 实测为准 |
|
||||
|
||||
### 1.1 取舍决定(每项只有一个答案)
|
||||
|
||||
| 选项 | 利 | 弊 | 决定 |
|
||||
|---|---|---|---|
|
||||
| 动作头:MeanFlow 一步头 vs WTA K=4 | MeanFlow:连续多模态,能同时表达左/右绕行与位姿目标的多个 IK 分支;沙箱 1 步达到 10 步流匹配质量。WTA:训练稳、实现简单 | MeanFlow:训练需 JVP(训练慢约 1.5–2 倍)、原生版本在部分任务失败(K-MF 表 1)。WTA:只有 4 个固定模式,左右 × 肘/腕分支可能超过 4 个;沙箱分不出两者高下 | **上 MeanFlow**。WTA 不实现;只有 M5 门禁不过(near_column 穿柱率高于 2% 或训练发散)才换成 WTA K=8,二者共用骨干,切换约 1 天 |
|
||||
| Shortcut + 条件 OT 配对 | 无需 JVP | 本项目无任何实测,组合无论文先例 | **不上** |
|
||||
| B 样条动作表示 | 平滑由表示保证;输出维度小 | 贴柱时拐角被抹圆、局部精度下降;只有间接证据;平滑已由梯形时间律 + 平滑损失保证 | **不上**,固定 16 步 Δq |
|
||||
| 输出后梯度投影 | 修掉 1–2 mm 的轻微侵入,成本约 0.1 ms,减少刹停 | 推离数据分布、改变块间一致性;修不了"绕错侧" | **上**,限 1 步、每轴修正量 ≤ 0.5°,仅 d < 余量时触发 |
|
||||
| 短前瞻(多推一块打分) | 避开"走两步就撞墙"的分支 | 推理次数翻倍,加剧最大不确定项(浏览器延迟) | **只在冷启动和卡死换向时上**,平时不做 |
|
||||
| 拒绝采样自蒸馏 | 进一步缩短行程 | 偏向贴柱、余量变小、多样性塌缩;APG 行程项已覆盖同一目标 | **不上** |
|
||||
| ReinFlow 强化学习微调 | 能直接优化到达/卡死/刹停等离散目标 | RL 会重塑左右模式比例;奖励调参与训练多出数天;这些离散目标已由卡死换向规则 + APG 覆盖 | **不上** |
|
||||
| 挖错循环中 E2 错题请老师补路径 | 解决可微代价绕不出的局部极小 | 量小、离线 | **上** |
|
||||
| 老师:PRM 路网 A* vs `solveDyn` | A*:确定性,同类任务左右选择一致,且是路网上的最短;`solveDyn` 覆盖更全 | A*:受路网分辨率限制,个别任务失败 | **A* 为主老师**,失败的任务用 `solveDyn`(固定种子)补 |
|
||||
| 推理后端:WebGPU vs wasm | WebGPU 大模型快 | 本模型约 4M 参数,WebGPU 每次调度 33–67 μs × 150–300 次,调度开销反而大 | **wasm-SIMD 多线程**;M4 实测若 WebGPU P99 更低再换 |
|
||||
| 训练余量 | 越大越不易被认证拒绝 | 越大行程越长、贴柱越难;高于老师轨迹自身间隙(实测最小间隙 p50 4.4 / p90 5.9 mm)会与 BC 标签对抗 | 数据过滤与 APG 碰撞项 **3 mm**(认证最小间隙 < 3 mm 的老师轨迹不进训练集);运行时投影护栏独立保持 **5 mm**,护栏不迁就数据分布 |
|
||||
| 关节噪声增广 σ | 覆盖偏离状态 | 过大产生无意义状态 | **1°** |
|
||||
| 局部牛顿精修 | 末端精度到 < 1 mm | 无 | **上**,同样过安全层 |
|
||||
|
||||
证据等级:**A** 有直接先例与数字 · **B** 有相近先例,需本仓库验证 · **C** 仅推断。
|
||||
|
||||
---
|
||||
|
||||
## 2. 依据
|
||||
|
||||
### 2.1 业界神经运动规划
|
||||
|
||||
| 工作 | 年份 / 会议 | 输入 | 输出 | 老师与数据量 | 关键做法 | 运行方式 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **MπNets**(NVIDIA) | 2022 CoRL | 点云 + 关节角 + 目标位姿 | 下一步关节位置 | 规划器 300 万+ 问题 | 端到端行为克隆;老师路径重定时为匀速 | 闭环 9 Hz,1 kHz PD 跟踪 |
|
||||
| **Neural MP**(CMU) | 2024 / IROS 2025 | 点云 + 关节角 + 目标 | GMM(LSTM) | 100 万–300 万条 | 测试时采样多条,选穿透最少的(去掉后成功率 81%→31%) | 开环为主 |
|
||||
| **DRP / IMPACT**(CMU) | 2025 CoRL | 点云 + 关节角 + 目标关节角 | 10 步关节增量 | cuRobo 1000 万条 | 大规模预训练 + 迭代师生微调 + "目标被挡停在最近安全处" | 闭环;4.67M 参数,300 Hz |
|
||||
| **ELMP**(ETH) | 2026 arXiv | 点云 + 关节角 + 目标位姿 | 关节增量 | AIT* 约 60 万条 | 行为克隆后 **APG 微调**,碰撞/到达/平滑损失直接反传 | 闭环 30 Hz |
|
||||
|
||||
- ELMP:1 万条老师轨迹 + APG(成功率 72.0%)超过 60 万条纯行为克隆(57.9%);APG 把碰撞率降到 1/3 以下。
|
||||
- 最好的方法在静态场景仍有约 9–10% 碰撞(ELMP-APG 8.9%,DRP 10.16%),实际部署都配检查或反应层。
|
||||
- 共识:闭环、短动作块滚动执行、老师只离线用、必须后训练。
|
||||
|
||||
### 2.2 本项目的条件
|
||||
|
||||
| 条件 | 主流 | 本项目 | 意味着 |
|
||||
|---|---|---|---|
|
||||
| 环境 | 未知、多变,靠点云 | 固定且精确已知(台架 + 立柱,1936 个胶囊) | 不需要点云编码器;模型可以小;碰撞检查精确 |
|
||||
| 机器人 | Franka 7 轴 | 自研 6 轴,J1/J3/J6 连续转 | 需处理连续轴展开 |
|
||||
| 碰撞模型 | SDF / 球近似 | 胶囊解析距离,可微 | 直接做 APG;安全层可认证 |
|
||||
| 平台 | 台式 GPU | 浏览器(后续真机) | 模型小,ONNX + WebGPU/wasm |
|
||||
|
||||
资源应投在**闭环稳定性、左右绕行选择和安全层**上,而不是感知和泛化。
|
||||
|
||||
### 2.3 前沿生成式技术取舍
|
||||
|
||||
| 技术 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| VLA 大模型(π0/π0.5、GR00T、OpenVLA) | 不采用 | 无图像、无语言指令;3B 参数更慢不更准 |
|
||||
| 一步生成:MeanFlow(MP1) | **采用** | 1 次前向;沙箱双模态实验通过 |
|
||||
| Consistency Policy、OneDP、朴素 1 步流匹配 | 不采用 | CP 原文承认偏向一侧;OneDP 有意取单模态;朴素 1 步 FM 等于条件均值,会穿柱 |
|
||||
| Shortcut / 条件 OT 配对 | 不采用 | 见 §1.1 |
|
||||
| OFP 自蒸馏 | 不采用 | 未找到公开代码;自引导 λ=0.5 时出现模态塌缩 |
|
||||
| 生成过程中代价引导(MPD/EDMP/GPD) | 不采用 | 无论文在 1–2 步下验证;改用输出后投影 |
|
||||
| 推理时 RTC | 不采用 | 原文只验证 5 步去噪且需反向传播;改用训练时前缀条件化 |
|
||||
| 时间集成 / 交叉淡入 | 不采用 | 执行的是未认证的混合块;左右两块平均可能穿柱;π0 弃用 |
|
||||
| FAST 动作分词 | 不采用 | 服务于语言骨干,自回归推理慢 |
|
||||
| B 样条动作表示 | 不采用 | 见 §1.1 |
|
||||
| ReinFlow / DPPO | 不采用 | 见 §1.1 |
|
||||
|
||||
### 2.4 沙箱双模态对照实验
|
||||
|
||||
2D 点从柱左侧到右侧,示教一半从上绕一半从下绕,16 步动作块、每 3 步重推理,与本方案同构。代码 `ml/toy/toy_bimodal.py`,150 个"起终点都贴近中轴"的难例:
|
||||
|
||||
| 动作头 | 单样本、无安全层:到达 / 穿柱回合 | 8 候选 + 安全层:到达 / 穿柱 / 平均刹停 | 推理(CPU,8 候选) |
|
||||
|---|---|---|---|
|
||||
| MSE 回归 | 100% / 0% | 100% / 0% / 0 | 0.13 ms |
|
||||
| WTA K=4 | 100% / 0% | 100% / 0% / 0 | 0.19 ms |
|
||||
| 流匹配 10 步 | 100% / 4.0% | 99.3% / 0% / 3.7 | 2.13 ms |
|
||||
| 朴素流匹配 1 步 | 87.3% / **23.3%** | 81.3% / 0% / 14.0 | 0.25 ms |
|
||||
| **MeanFlow 1 步** | 100% / 2.7% | 100% / 0% / 0.3 | 0.25 ms |
|
||||
|
||||
- 朴素 1 步流匹配会把模态平均进柱子;MeanFlow 1 步达到 10 步流匹配的质量,推理快约 8 倍。
|
||||
- 安全层在所有方法上把穿柱降为 0,代价是刹停。
|
||||
- 该任务过于简单(闭环中状态一偏离中轴即打破对称),**分不出一步头与 WTA 谁更好**;因此 WTA 只作为 M5 门禁不过时的预定回退(§1.1)。
|
||||
- 训练仅 300 步的 MSE 在同一难例上 100% 穿柱、加安全层后 0% 到达,说明欠拟合时平均模态的风险真实存在。
|
||||
|
||||
---
|
||||
|
||||
## 3. 训练环境
|
||||
|
||||
| 阶段 | 在哪里跑 | 说明 |
|
||||
|---|---|---|
|
||||
| 数据生成 | Hark 沙箱(2 vCPU / 7 GB,Node)或用户机器 | 纯 JS,无需 GPU |
|
||||
| 预训练 + 后训练 | 用户 RTX 3090(可用 19–20 GB) | 预训练 batch 1024(峰值 2.76 GiB);APG batch 48 × 8000 步(峰值 13.08 GiB) |
|
||||
| 部署推理 | 浏览器 onnxruntime-web wasm-SIMD 多线程(代理 Worker);ONNX 接口 feat[1,83]、z[K,16,6]:观测编码一次、K=8 条候选共享编码再解码;后续真机控制器 | 与现有查看器同页 |
|
||||
|
||||
```bash
|
||||
conda create -n armtf python=3.11 -y && conda activate armtf
|
||||
pip install torch --index-url https://download.pytorch.org/whl/cu121 # 按本机 CUDA 版本选
|
||||
pip install numpy pyarrow tqdm tensorboard onnx onnxruntime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 离线数据(老师只在这里出现)
|
||||
|
||||
### 4.1 文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `ml/gen/gen_dataset.cjs` | 采样任务 → 调老师 → 校验 → 时间参数化 → 写分片 |
|
||||
| `ml/gen/sampler.cjs` | 起点/目标采样 |
|
||||
| `ml/gen/run_parallel.sh` | 多进程,不同种子 |
|
||||
| `ml/data/shard_XXXX.jsonl` | 原始样本(不入库) |
|
||||
|
||||
复用 `tests/route_bench_setup.cjs` 装配求解器(关节表、`docs/travel.json`、`docs/collide.json`)。
|
||||
|
||||
### 4.2 原始样本
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "s0003_000127", "mode": "pos" | "pose",
|
||||
"q0": [6], "T": [x,y,z], "R": [[3x3]] | null, "tool": 0,
|
||||
"path": [[6], ...],
|
||||
"reached": true,
|
||||
"travel_deg": 123.4, "plan_ms": 812.0,
|
||||
"fp": "collide.json 指纹"
|
||||
}
|
||||
```
|
||||
|
||||
`reached=false` 表示目标不可达,路径终点为离目标最近的安全位形("停靠")。
|
||||
|
||||
### 4.3 采样分布(5 万条任务)
|
||||
|
||||
关节限位:J1/J3/J6 连续 ±180°;J2 [−92, 9],J4 [−111, 16],J5 [−93, 34]。
|
||||
|
||||
| 类别 | 占比 | 采样方式 |
|
||||
|---|---:|---|
|
||||
| A 均匀 | 45% | q0、目标位形 b 均匀采样且无干涉;目标 = `fk(b)`(pose 模式加 `orient(b)`) |
|
||||
| B 直线被挡 | 30% | q0→b 关节直线有干涉 |
|
||||
| C 贴立柱 | 15% | b 满足 `nearColumn`,q0 在立柱另一侧 |
|
||||
| D 目标不可达 | 10% | 目标在立柱内/所有分支都撞/工作空间外;标签为停靠 |
|
||||
|
||||
- pos / pose 各半;老师为 PRM 路网 A* + 捷径平滑(确定性;复用 `routeRT` 的路网图搜索,放宽时间预算);A* 失败的任务用 `solveDyn`(固定种子)补。
|
||||
- 另造三个互不重叠的集合:**门禁集** 2000 条(挖错循环每轮用)、**held-out 验收集** 2000 条、**near_column 扩展验收集** ≥1000 条;两个验收集只在最终验收时用一次。现有三个基准不进训练集。
|
||||
|
||||
### 4.4 过滤
|
||||
|
||||
1. 整条路径用距离下界认证(与 §8.3 同口径)复核,任一段不过即丢弃。仓库 `tests/route_bench.cjs` 上现有规划器有 1/40 例路径细验不过,这类样本必须丢掉。
|
||||
2. `poseRelaxed` 样本只当 pos 模式用。
|
||||
3. 行程 > 同任务 `routeRT` 1.5 倍的先统计,再定是否丢弃;按类别统计丢弃率(C 类 > 20% 说明老师在贴柱场景不可靠)。
|
||||
|
||||
### 4.5 时间参数化 → 闭环训练样本
|
||||
|
||||
1. 时间律:按轴速比 `VREL = [0.6, 0.6, 0.9, 0.9, 1.2, 1.8]` 匀速巡航,梯形加减速,速度只取决于"当前速度 + 剩余路程"(每轴加速度上限 a_max 为待定参数,真机型号确定后设定),dt = 20 ms。
|
||||
2. 得到状态序列 q_0 … q_N,每隔 3 步取一个样本:
|
||||
- 输入:§5.1 全部字段(含当前速度、已提交前缀)
|
||||
- 标签:未来 16 步 Δq_{t+k} = q_{t+k} − q_t(到终点后补零)
|
||||
3. 每条路径约 40–50 个样本 → 约 200 万样本(以 `stats.json` 为准)。
|
||||
4. 协变量偏移对策:关节噪声增广(30% 样本,σ = 1°;噪声态须无干涉且间隙 > 1 mm,标签为 8 步内线性收回老师轨迹的增量);偏离状态的进一步覆盖靠 §7。
|
||||
|
||||
### 4.6 耗时与存储
|
||||
|
||||
- 路网 A* 每条数十毫秒,耗时主要在过滤认证;按 `solveDyn` 0.7 s/条上限估算,沙箱 2 进程不超过 5 小时。
|
||||
- 200 万样本以 float32 `.npy` 存放约 1.5 GB(训练时整体载入内存)。
|
||||
|
||||
### 4.7 检查点
|
||||
|
||||
- [ ] `ml/data/stats.json`:类别、到达率、行程、样本数、补零样本占比
|
||||
- [ ] 老师一致性:起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90%
|
||||
- [ ] 抽 20 条在查看器回放(新增 `?replay=` 参数)
|
||||
- [ ] 各集合与训练集无重叠
|
||||
|
||||
---
|
||||
|
||||
## 5. 表示与预处理
|
||||
|
||||
文件 `ml/prep/build_dataset.cjs`(Node,特征直接调用部署运行时 `webapp/js/ik-policy.js` 的同一份实现)→ `ml/data/samples/*.{X,Y,V,SG,TID}.npy` + `*.tasks.jsonl`;按任务 5% 划为验证集。PyTorch 侧特征 `ml/common/features.py` 与 JS 逐项对拍(闭环仿真与 APG 使用)。
|
||||
|
||||
### 5.1 输入
|
||||
|
||||
| 量 | 维度 | 编码 |
|
||||
|---|---:|---|
|
||||
| 当前 q_t | 6 | 每轴 (sin, cos) → 12;限位轴另加归一化值 → +3 |
|
||||
| 当前关节速度 q_t − q_{t−1} | 6 | 按每轴速度上限归一化 |
|
||||
| 当前 TCP 位置与姿态 | 3 + 6 | `fk(q_t)`、`orient(q_t)` 的 6D 表示 |
|
||||
| 目标位置 T | 3 | /600 mm |
|
||||
| 目标姿态 R | 6 | 6D 表示;pos 模式填 0 |
|
||||
| 目标 − 当前 TCP | 3 | /600 mm |
|
||||
| 模式 | 2 | one-hot [pos, pose] |
|
||||
| 已提交未执行前缀 | 3×6 + 3 | 前缀 Δq + 有效长度掩码;训练时随机遮盖 0–3 步 |
|
||||
| 当前子目标 | 6 | 子目标头输出的关节途经点(归一化);无子目标时填当前 q |
|
||||
| 关键胶囊对最小距离 | 8 | 臂各段到立柱的胶囊距离 |
|
||||
|
||||
### 5.2 输出
|
||||
|
||||
- MeanFlow 一步头:每条候选 16 步 × 6 轴 Δq(按每轴最大步长归一化)。
|
||||
- 附加头:价值头(剩余时间/代价)、子目标头(2 个关节途经点 + 是否需要)。
|
||||
|
||||
### 5.3 连续轴
|
||||
|
||||
J1/J3/J6 统一用 `unwrapTo` 展开后再做增量;预处理与部署共用一份实现,对拍时一并验证。
|
||||
|
||||
---
|
||||
|
||||
## 6. 模型与预训练
|
||||
|
||||
### 6.1 结构
|
||||
|
||||
```
|
||||
条件特征(约 90 维)──MLP──▶ 8 个条件 token (d=192)
|
||||
│
|
||||
4 层 Transformer encoder
|
||||
│ (memory)
|
||||
16 个动作 token(注入噪声 z + 时间嵌入 (r, t))
|
||||
▼
|
||||
4 层 Transformer decoder(非自回归,一次前向)
|
||||
├─ 动作输出:每 token 6 维 Δq
|
||||
├─ 价值头:1
|
||||
└─ 子目标头:2 个关节途经点 + 是否需要
|
||||
```
|
||||
|
||||
| 超参 | 值 |
|
||||
|---|---|
|
||||
| d_model / 头数 / FFN | 192 / 8 / 768 |
|
||||
| encoder / decoder 层数 | 4 / 4 |
|
||||
| 参数量 | 约 4.9M |
|
||||
|
||||
子目标触发:新目标或目标跳变 > 50 mm 时输出;到达当前子目标 < 5° 后切下一个。
|
||||
|
||||
### 6.2 动作头:MeanFlow 一步生成
|
||||
|
||||
| 项 | 做法 |
|
||||
|---|---|
|
||||
| 多模态 | 连续分布;每次推理并行采 8 个噪声 → 8 条候选 |
|
||||
| 训练 | MeanFlow 平均速度场损失(MP1 配方:25% 样本 r≠t,自适应损失加权),JVP 用 `torch.func.jvp` |
|
||||
| 推理 | 1 次批量前向(r=0, t=1) |
|
||||
| 后处理 | 1 步解析胶囊距离梯度投影,每轴修正 ≤ 0.5°,仅 d < 余量时(JS 实现,不走 ONNX) |
|
||||
| APG 兼容 | 固定噪声后为确定性可微映射 |
|
||||
| 预定回退 | M5 门禁不过(near_column 穿柱率 > 2% 或训练发散)→ 同骨干换 WTA K=8 头(WTA-L1 + 模式 CE),约 1 天 |
|
||||
|
||||
### 6.3 损失
|
||||
|
||||
```
|
||||
L = MeanFlow 损失 1.0
|
||||
+ FK 损失:一步输出(固定噪声)的各连杆胶囊端点位置差 0.5
|
||||
+ 平滑:二阶差分 0.05
|
||||
+ 价值头 L1 + 子目标头 L1 0.1 / 0.1
|
||||
```
|
||||
|
||||
### 6.4 训练配置
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 优化器 | AdamW,β=(0.9, 0.95),wd 0.01 |
|
||||
| 学习率 | 1e-3,预热 1000 步,余弦退火 |
|
||||
| batch | 1024 |
|
||||
| 精度 | bf16 混合精度 |
|
||||
| 步数 | 先跑 2000 步测吞吐再定;参考 200 万样本 × 30 epoch ÷ 1024 ≈ 6 万步;验证集闭环到达率不再上升即停 |
|
||||
| 预计耗时 | RTX 3090 约 1.5–5.6 小时(按 2000 步吞吐实测重算) |
|
||||
|
||||
### 6.5 对拍与验证
|
||||
|
||||
- `ml/eval/collide_torch.py`:可微胶囊距离 PyTorch 批量版,与 JS `collideFree` 对拍 10 万位形:布尔判定 0 差异,距离误差 < 1e-4 mm,梯度与有限差分相对误差 < 1e-3。
|
||||
- 每 epoch 闭环验证:从 1000 个验证任务出发,按部署节奏(每 60 ms 推理、提交 3 步)运行到到达或超时(老师路径时长 × 2,最少 3 s),统计到达率、用时 ≤ 老师 × 1.3 的达标率、碰撞步数(关安全层)、行程比、左/右/穿柱三类比例。
|
||||
|
||||
---
|
||||
|
||||
## 7. 后训练
|
||||
|
||||
### 7.1 主力:APG 可微微调
|
||||
|
||||
```
|
||||
for 每个 batch(随机采样 q0 和目标,不需要老师):
|
||||
q = q0
|
||||
for t in 0..H-1 (H = 本批老师时长 p90 × 1.2,60–150 次推理 × 3 步 ≈ 3.6–9 s;无老师题按 6 s;每 10 次推理截断反传):
|
||||
chunk = π(obs(q, 目标, 前缀), z) # 每条展开固定一个噪声 z
|
||||
Δq = chunk[0:3] # 与部署一致
|
||||
q = clamp(q + Δq)
|
||||
累计 L_col(q), L_smooth(Δq)
|
||||
L = w_tgt·Σ_段末 L_target(q) + w_col·ΣL_col + w_smooth·ΣL_smooth (+ w_len·行程) + w_bc·L_BC
|
||||
反传,梯度裁剪 ≤ 1.0,lr 1e-4
|
||||
```
|
||||
|
||||
- 碰撞项 L_col = Σ ReLU(margin − 胶囊距离),margin = 训练余量 3 mm(间隙已扣除 `margin_mm`);每步端点与中点,两处取均值(与只查端点同量纲);含臂×立柱、臂×台架、臂×臂(非相邻段)。
|
||||
- 到达项:每个截断段末的位置误差² + λ_rot·‖R − R*‖_F²;不做到达即停,段末项在整条展开上持续给出接近梯度。
|
||||
- 平滑项:速度² + 加速度²。
|
||||
- BC 锚定项:保留预训练损失,防止多模态被压缩;每轮监控左右比例。
|
||||
- 采样分布同 §4.3,B/C/D 类提到 60%,其中贴柱类 40%。
|
||||
|
||||
### 7.2 辅助:无老师挖错循环
|
||||
|
||||
每轮:
|
||||
1. 出题:新种子 5 万任务 + 2000 段模型在环拖拽仿真。
|
||||
2. 闭环作答:完全按部署方式运行,**关闭安全层**以暴露真实错误。
|
||||
3. 验算:GPU 批量胶囊检查。
|
||||
4. 错题分类:E1 撞 / E2 没到(卡在局部极小)/ E3 绕远(> 1.2× 参考)/ E4 末端精度差 / E5 摇摆(≥ 3 次切换)/ E6 拖拽失稳。
|
||||
5. 错题及其 ±5 mm / ±3° 的 10 个邻居加入 APG 采样池(E1、E6 ×2);仅 E2 可离线请老师补少量路径。
|
||||
6. 门禁(用门禁集,不用验收集):到达率上升、A 类每轮降幅 ≤ 0.5 个百分点、上一轮修好的错题 95% 仍对;否则回滚。
|
||||
7. 停止:连续 2 轮提升 < 0.5 个百分点,或满 6 轮。
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署
|
||||
|
||||
### 8.1 执行循环
|
||||
|
||||
```
|
||||
执行:50 Hz,每步 20 ms,按已提交步走
|
||||
每 60 ms:
|
||||
1. 读当前 q、当前速度、已提交未执行前缀、最新目标
|
||||
2. 推理:8 个噪声 → 8 条 16 步动作块(冷启动或卡死换向时,再各推一块做短前瞻)
|
||||
3. 每条候选做 1 步胶囊距离梯度投影(每轴 ≤ 0.5°,仅 d < 余量时)
|
||||
4. 排序(§8.2)
|
||||
5. 安全层依次认证候选前 3 步 + 刹停段(§8.3),第一个通过的提交给执行器
|
||||
6. 全部不通过 → 沿上一次已认证的刹停段停下,下次继续推理
|
||||
7. 到达:TCP 误差 < 1 mm(pose 另需 < 0.5°)且速度≈0;
|
||||
接近目标(< 5 mm)时做 1–3 次局部牛顿精修(solveTo 单步),精修段同样过安全层认证
|
||||
```
|
||||
|
||||
执行的始终是安全层认证过的那一块,不做新旧块混合。执行时钟放在 Web Worker、按实际 dt 推进,避免 `requestAnimationFrame`(60 Hz)与 20 ms 不整除及后台节流造成的抖动。
|
||||
|
||||
### 8.2 候选排序(防左右摇摆)
|
||||
|
||||
分数 = −Σ w_k‖候选第 k 步 − 已提交块第 k 步‖²(重叠步加权)− λ·价值头剩余代价。冷启动时无已提交块,只用价值头 + 短前瞻打分。
|
||||
|
||||
### 8.3 安全层(唯一的硬保证)
|
||||
|
||||
- **距离下界认证**:对一段运动 q_a → q_b,若两端到障碍的最小距离之和 d(q_a) + d(q_b) > Σ_j r_j·|Δθ_j|,则整段安全;否则二分,直到全部子段通过或判不安全。按组对分别判定:组对 (g_A, g_B) 的间隙只受关节 g_A…g_B−1 带动组 g_B 影响,右端取 Σ_{k=g_A}^{g_B−1} T[k][g_B]·|Δθ_k|,T 为分组杠杆表(组 g_B 胶囊端点到轴 k 的最大距离,随机 4000 位形采样再放大 5%;末端组对 J1…J6 分别为 9.56 / 7.37 / 4.47 / 4.48 / 2.47 / 0.69 mm/°)。间隙取全部 |Δg| ≥ 2 胶囊对的 距离 − (r_i + r_j + margin)。外加关节限位。先用离散 `collideFree` 剔除明显干涉的候选;运行时每条候选最多 40 次距离查询;墙钟截止下沉到认证内部:第 1 条候选截止 45 ms,其余候选共享到 30 ms,到点即判不过、沿已认证刹停段停。安全层耗时在几何查询而不在模型(沙箱 Node 每次 gapMatrix 3.6–5.5 ms、collideFree 1.5 ms,推理 1.5 ms)。
|
||||
- 离散采样检查(现有 `pathFine`,1.5° + 半格)只作快速预筛,不作保证依据。
|
||||
- **可刹停不变式**:只有当"候选前 3 步 + 从第 3 步末按每轴 a_max 减速到停"整段都通过认证时才提交。任何时候系统都持有一条已认证的停车路线,下一次推理即使完全失效也只会停下。
|
||||
- 耗时:每段一次距离查询覆盖整段,不通过才二分(通常 2–6 次);按排序逐个认证,第一个通过即停;包围球预筛先行。目标每次 < 5 ms,否则放 Web Worker 与推理流水线并行。
|
||||
- `ik-glue.js` 的每帧碰撞守卫保留为第二道保险。
|
||||
|
||||
### 8.4 部署包
|
||||
|
||||
| 有 | 没有 |
|
||||
|---|---|
|
||||
| `traj_policy.onnx`(fp32,约 20 MB;wasm / CPU 后端上 fp16 不保证更快)+ `traj_policy.meta.json`(指纹 / 特征版本 / TCP) | PRM 路网、IK 候选大表 |
|
||||
| 胶囊距离与认证(`collideFree`、包围球预筛、`pair2d`) | `solveDyn` / `routeRT` / RRT / A* |
|
||||
| `fk` / `orient`、局部牛顿精修 `solveTo` 单步 | 全量 IK 枚举 |
|
||||
|
||||
`collide.json` 指纹写进模型;不一致时前端拒绝运行并提示重训。
|
||||
|
||||
### 8.5 near_column 30/30 的保障手段
|
||||
|
||||
"超时"指机械臂在规定时间内没走到目标,是决策问题而非计算速度问题。典型失效:局部极小/活锁(每次都想直穿、每次被安全层挡住)、左右摇摆、绕远。对策(运行时均不需要老师):
|
||||
|
||||
1. **子目标头**:给出 1–2 个关节途经点作为绕柱方向锚点,标签取老师路径拐点(离线)。
|
||||
2. **价值头**:预测剩余时间/代价,用于候选打分与进展判断;APG 中用 TD 自举监督。
|
||||
3. **短前瞻**(仅冷启动和卡死换向时):对每条候选再推一块,用胶囊认证 + 价值头给 32 步后终态打分。
|
||||
4. **卡死检测 + 强制换向**:0.3 s 内价值不降或连续 2 次刹停 → 切到另一绕行方向并锁定 1 s。
|
||||
5. **训练侧加强**:贴柱类占 APG 采样 40%;展开覆盖整段绕行。
|
||||
|
||||
### 8.6 控制周期
|
||||
|
||||
伺服层(参考):Franka FCI 1 ms;UR e-Series 2 ms、CB3 8 ms;ABB EGM 4 ms(滞后 10–20 ms);KUKA RSI 4/12 ms;FANUC DPM 约 8 ms;MoveIt Servo 默认 10 ms;RC 舵机 20 ms。
|
||||
|
||||
策略层(参考):MπNets 9 Hz;Diffusion Policy 10 Hz、预测 16 执行 8;ELMP 30 Hz;π0 50 Hz、约每 0.5 s 推理一次;DRP 300 Hz。业界均为"策略层 10–50 Hz + 伺服层 125 Hz–1 kHz"两层结构。
|
||||
|
||||
本方案:执行 50 Hz、每 60 ms 推理一次、提交 3 步。真机上提交步数 n ≥ ⌈(通信滞后 + 一次推理最坏耗时) / 20 ms⌉ + 1,刹停段另行认证;策略输出 50 Hz 关节目标,由插补器升采样到电机控制器频率。`dt`、n、动作块长度均可配置。
|
||||
|
||||
---
|
||||
|
||||
## 9. 评估与验收
|
||||
|
||||
### 9.1 测试集合
|
||||
|
||||
| 集合 | 来源 | 用途 |
|
||||
|---|---|---|
|
||||
| route_bench | `tests/route_bench.cjs`(40 例)+ 60 例随机基准 | 验收 |
|
||||
| near_column_bench | `tests/near_column_bench.cjs`,30 例(pos / pose / rt) | 验收 |
|
||||
| near_column 扩展 | §4.3 新造 ≥1000 例 | 一次性验收 |
|
||||
| rt_drag_bench | `tests/rt_drag_bench.cjs`,12 例(模型在环) | 验收 |
|
||||
| held-out | §4.3 2000 条 | 一次性验收 |
|
||||
| 门禁集 | §4.3 2000 条 | 训练中门禁,不参与验收 |
|
||||
|
||||
新增 `tests/policy_bench.cjs`:①现有规划器 ②策略(关安全层)③策略 + 安全层,输出对比表。
|
||||
|
||||
### 9.2 验收标准(部署形态 = 策略 + 安全层)
|
||||
|
||||
| 指标 | 要求 | 现有规划器 |
|
||||
|---|---|---|
|
||||
| 实际执行的碰撞 | **= 0**(距离认证或 0.01° 审计测量) | rt_drag_bench 0 |
|
||||
| 到达率 | route 全部可达例到达;near pos **30/30**、near rt **30/30**;near_column 扩展集 **0 失败**;held-out 可达任务 ≥ 98%,D 类停靠距离 ≤ 老师 + 5 mm | `route_bench` 40 例:到达 40/40、细验有效 39/40;60 例随机基准有效 60/60;near 30/30、30/30 |
|
||||
| 到达用时 | ≤ 老师路径时长 × 1.3 | — |
|
||||
| 关节行程 | ≤ 现有规划器 × 1.10(按类别分别报告) | 60 例平均 467° |
|
||||
| 冷启动 | < 10 ms | 首次全量 IK 最差约 2 s |
|
||||
| 每次推理 + 安全认证 P99 | < 20 ms | 拖拽重规划 P99 约 321 ms |
|
||||
| 刹停发生率 | < 2% 的推理周期 | — |
|
||||
| 模式摇摆(≥3 次切换) | < 1% 的任务 | — |
|
||||
| 策略单独(关安全层)碰撞率 | < 3% | — |
|
||||
|
||||
说明:30 例全对时,失败率的 95% 置信上界约 9.5%;扩展集 1000 例 0 失败时约 0.3%,因此以扩展集作为 30/30 的统计支撑。
|
||||
|
||||
口径:本表全部按部署形态(`tests/policy_bench.cjs`,与浏览器同一份运行时)或 held-out 闭环仿真(开仿真安全层)测量,由 `ml/eval/acceptance.py` 汇总;M5 门禁与挖错用的是关安全层的纯策略口径,二者互不替代。挖错题与门禁集没有老师参考,到达按定长 6 s 判,「到达用时」「关节行程」两项只在有老师参考的任务上统计。
|
||||
|
||||
---
|
||||
|
||||
## 10. 当前存在的问题与风险
|
||||
|
||||
### 10.1 只能靠实测消除的风险(按里程碑顺序)
|
||||
|
||||
| # | 风险 | 门禁(不过不进下一步) | 回退 | 何时测 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 老师在相邻任务上左右绕行跳变,标签自相矛盾 | 起终点扰动 ±5 mm 的邻居任务同伦类一致率 ≥ 90% | 对 A* 边代价加左右偏好项,强制同区域选同侧 | M1 |
|
||||
| R2 | 时间律仍有不可观测成分,闭环起不了步 | 近邻样本标签方差 < 10% 步长(`ml/eval/check_r2.py`);起步失败率 = 0(闭环 `start_fail`) | 进一步把剩余路程估计加入输入 | M1 / M5 |
|
||||
| R3 | 距离认证过于保守,贴柱处拒绝率高,影响 30/30 | near_column 上认证拒绝率统计;离线 10⁵ 段 0.01° 审计漏检 = 0 | 训练余量提高到 4–6 mm,接受行程变长;收紧杠杆界(按位形分区) | M2 / M7 |
|
||||
| R4 | 浏览器纯推理延迟 | 训练前用随机初始化模型导出 ONNX(延迟与权重无关),`pipeline.sh r4` 自动测部署口径 K=8 P99 < 15 ms。2026-10-18 本机:编码一次 + 16 线程 + 代理 Worker 后 P99 10.2–11.3 ms,**已通过**(改前 37.0 ms) | WebGPU 达标即用 WebGPU,否则 d=128(`configs/small.json`)。K=8 不降 | **M1 前(已结)** |
|
||||
| R5 | MeanFlow 头在 6 轴、位姿目标下穿柱或训练不稳 | near_column 穿柱率(关安全层)≤ 2%;损失不发散 | 同骨干换 WTA K=8 头 | M5 |
|
||||
| R6 | 位姿目标的多 IK 分支带来额外多峰 | 不同肘/腕分支子集到达率 ≥ 主集 − 2 个百分点 | 加目标分支标志 | M5 |
|
||||
| R7 | APG 压缩多模态或拉长路径(ELMP 中路径 +23%) | 每轮门禁:B/C 类左右绕行比例变化 ≤ 5 个百分点、行程中位数比 ≤ 1.05(`ml/mining/gate.py`) | 加大 BC 锚定权重;行程项 | M6 |
|
||||
| R8 | 卡死换向活锁 | 50 个"两侧都要绕远"对抗用例(J1 起终差 ≥ 140°、终点贴立柱,部署运行时)首次换向锁后 3 s 内到达 ≥ 95%,活锁 = 0(`tests/policy_bench.cjs` r8) | 子目标改为离散绕行锚点分类 | M6 / M7 |
|
||||
| R9 | 整周期超标(推理 + 前瞻 + 安全层 > 60 ms 周期,执行时钟饥饿丢步) | 训练好的模型在**真查看器**里跑与 bench 同一批 30 道贴柱题:稳态规划 P99 < 45 ms、运动中饥饿节拍 < 1%、认证超时占比 < 5%(`r4_probe --viewer` → `r9_browser.json`;冷启动 P99、刹停率、tickGapMax 只报);同题安全层浏览器 / Node 差 ≤ 3× | 几何查询(features、投影、认证)移到 Worker,主线程只执行;前瞻只在冷启动时做 | M7 |
|
||||
| R10 | 训练耗时估计偏差 | 60000 步 ≤ 3.5 h,即 2000 步测速 ≥ 5000 样本/s(3090 实测 6.2 k/s ≈ 2.8 h) | FK 损失胶囊数 96 → 48;仍不够则步数降到 40000(MeanFlow JVP 受算力限制,增大 batch 无效) | M5 |
|
||||
|
||||
### 10.2 已知缺陷
|
||||
|
||||
1. 现有规划器(老师)在 `tests/route_bench.cjs` 上有 1/40 例到达但路径细验不过;§4.4 的认证过滤会丢弃这类样本。
|
||||
2. `ci27` 姿态根枚举漏分支(姿态偏差约 97.5°)会影响 pose 数据;APG 姿态项可部分纠正,必要时先修老师。
|
||||
3. 胶囊模型或立柱变动即需重训:数据(用户机器 16 核约 2 h)+ 预训练 1.5–6 h + 后训练数小时。
|
||||
4. 老师一致性沙箱首测 78%(100 任务、373 对邻居,门禁 90%):不一致主要来自 J6 冗余与时间预算造成的非确定性。已改为确定性老师(预算按碰撞判定次数计、每条前清空跨调用缓存)并在 pos 任务冻结 J6,待在用户机器复测;仍不达标则生成时启用 4 邻居共识过滤。
|
||||
5. 安全层耗时:未训练模型(候选为噪声)在 Node 单线程实测每次推理安全层约 20–60 ms,高于 5 ms 目标;训练后候选平滑会下降,以 `policy_bench` 与浏览器 `__POLICY.latency()` 实测为准,超标则把安全层移入 Web Worker 与推理流水线并行。
|
||||
|
||||
### 10.3 需要用户确认
|
||||
|
||||
1. **真机电机/驱动型号与通信方式**:决定每轴加速度上限 a_max、插补频率与提交步数。
|
||||
2. **运行时多候选打分是否算"运行时老师"**:本方案的理解是不算(只对模型自己的候选做距离认证和打分,不做搜索)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 目录规划
|
||||
|
||||
运行步骤、命令与耗时见 `ml/README.md`。所有长步骤都能断点续跑,意外中断最多损失几分钟:生成按条续写(每条任务的随机数只由种子和序号决定),打包按 shard 跳过,预训练与 APG 每 10 分钟原子存一次 `last.pt`,停止时先存档再退出。
|
||||
|
||||
```
|
||||
ml/
|
||||
pipeline.sh 阶段脚本:setup / kin / gate1 / gen / prep / train / m5 / apg / mine / export / bench
|
||||
gen/ sampler / teacher(确定性 PRM A*)/ timelaw / gen_dataset / consistency / stats / run_parallel.sh
|
||||
prep/ export_kin.cjs → assets/kin.json;build_dataset.cjs → data/samples/*.npy;run_prep.sh
|
||||
data/ (不入库)shard_*.jsonl stats.json consistency.json samples/
|
||||
common/ kin_torch.py(可微运动学 + 胶囊间隙) features.py(83 维特征) ckpt.py(原子存档、定时存档、体面停止)
|
||||
model/ policy.py(MeanFlow 头;WTA K=8 回退头) meanflow.py
|
||||
data.py train.py 样本加载、预训练
|
||||
sim/closed_loop.py 闭环仿真(验证、挖错、门禁共用)
|
||||
apg/apg_finetune.py APG 可微微调
|
||||
mining/ tasks.py mine_round.py gate.py loop.sh
|
||||
eval/ dump_parity.cjs check_parity.py(JS ↔ PyTorch 对拍)
|
||||
configs/ base.json wta.json small.json apg.json
|
||||
export_onnx.py serve.py README.md
|
||||
webapp/js/ik-policy.js 运行时:特征、距离下界认证、刹停、投影、精修、Runner(浏览器与 Node 共用)
|
||||
webapp/js/ik-policy-glue.js 查看器接入(?policy=1)
|
||||
webapp/models/ traj_policy.onnx traj_policy.meta.json
|
||||
tests/policy_bench.cjs 部署形态评测
|
||||
```
|
||||
|
||||
## 12. 里程碑
|
||||
|
||||
| # | 内容 | 产出 | 在哪跑 |
|
||||
|---|---|---|---|
|
||||
| M1 | 数据生成器 + 时间律 + 1000 条试跑;老师一致性门禁 | `ml/gen/*`、stats.json | 沙箱 |
|
||||
| M2 | 可微胶囊距离(PyTorch)与 JS 对拍;距离认证实现与 10⁵ 段审计 | `collide_torch.py`、认证模块 | 沙箱 |
|
||||
| M3 | 5 万条老师数据 → 约 180 万样本 | `samples/*.npy` | 用户机器 |
|
||||
| M4 | 模型 + 训练工程 + 闭环仿真;小数据过拟合;**导出 ONNX 测浏览器延迟** | `train.py`、`closed_loop.py`、延迟报告 | 沙箱 + 浏览器 |
|
||||
| M5 | 预训练 + MeanFlow 头门禁 | checkpoint、门禁报告 | 用户 GPU |
|
||||
| M6 | APG 微调 + 挖错循环 3–6 轮 | 每轮 report.md | 用户 GPU |
|
||||
| M7 | `ik-policy.js` 闭环部署 + 安全层实测 + `policy_bench` + 一次性验收 | 对比表 | 沙箱 + 浏览器 |
|
||||
| M8 | 移除查看器对 `solveDyn`/`routeRT`/路网的运行时依赖(保留在 `ml/gen` 离线使用) | 部署包瘦身 | 沙箱 |
|
||||
|
||||
## 13. 参考文献
|
||||
|
||||
- MπNets:https://arxiv.org/abs/2210.12209
|
||||
- Neural MP:https://arxiv.org/abs/2409.05864
|
||||
- Deep Reactive Policy / IMPACT:https://arxiv.org/abs/2509.06953
|
||||
- ELMP:https://arxiv.org/abs/2607.00215
|
||||
- Diffusion Policy:https://arxiv.org/abs/2303.04137
|
||||
- π0:https://arxiv.org/abs/2410.24164 ;π0.5:https://arxiv.org/abs/2504.16054
|
||||
- FAST:https://arxiv.org/abs/2501.09747
|
||||
- MP1 / MeanFlow:https://arxiv.org/abs/2507.10543
|
||||
- One-Step Flow Policy:https://arxiv.org/abs/2603.12480
|
||||
- 条件 OT 流策略:https://proceedings.mlr.press/v305/sochopoulos25a.html
|
||||
- Kinematic MeanFlow:https://arxiv.org/abs/2610.00864
|
||||
- Consistency Policy:https://arxiv.org/abs/2405.07503 ;OneDP:https://arxiv.org/abs/2410.21257
|
||||
- MPD:https://arxiv.org/abs/2308.01557 ;EDMP:https://arxiv.org/abs/2309.11414 ;GPD:https://arxiv.org/abs/2501.18229 ;DiffusionSeeder:https://arxiv.org/abs/2410.16727
|
||||
- Real-Time Chunking:https://arxiv.org/abs/2506.07339 ;训练时动作条件化:https://arxiv.org/abs/2512.05964
|
||||
- Bidirectional Decoding(BID):https://arxiv.org/abs/2408.17355
|
||||
- DPPO:https://arxiv.org/abs/2409.00588 ;ReinFlow:https://arxiv.org/abs/2505.22094
|
||||
- Schwarzer, Saha, Latombe, *Adaptive Dynamic Collision Checking for Single and Multiple Articulated Robots in Complex Environments*, IEEE T-RO 2005
|
||||
- Thumm & Althoff, *Provably Safe Deep Reinforcement Learning for Robotic Manipulation in Human Environments*, ICRA 2022
|
||||
- Zhou et al., *On the Continuity of Rotation Representations in Neural Networks*, CVPR 2019
|
||||
- AWE(航点提取):https://arxiv.org/abs/2307.14326 ;SHAC:https://arxiv.org/abs/2204.07137
|
||||
- 控制周期:Franka FCI https://frankarobotics.github.io/docs/overview.html ;UR RTDE https://www.universal-robots.com/articles/ur/interface-communication/remote-control-via-tcpip ;ABB EGM 应用手册 3HAC073318;KUKA RSI https://docs.quanser.com/quarc/documentation/kuka_rsi_block.html
|
||||
@@ -0,0 +1,101 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 700" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="700" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
|
||||
</marker>
|
||||
<marker id="arb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#7c5cd6"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">系统总览:一个事实源、两个碰撞模型、一条数据链</text>
|
||||
|
||||
<!-- 列标题 -->
|
||||
<text x="175" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#1f6f43">① 建模侧(Python · CadQuery)</text>
|
||||
<text x="490" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#8a5a00">② 数据契约(docs/*.json)</text>
|
||||
<text x="805" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#1f4f8f">③ 网页查看器(单文件 HTML)</text>
|
||||
|
||||
<!-- ① 建模侧 -->
|
||||
<rect x="40" y="88" width="270" height="86" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.6"/>
|
||||
<text x="175" y="112" text-anchor="middle" font-size="15" font-weight="bold" fill="#14472c">human_layout.py</text>
|
||||
<text x="175" y="133" text-anchor="middle" font-size="12.5" fill="#2c5c44">唯一事实源:6 根关节轴 rear/d、</text>
|
||||
<text x="175" y="151" text-anchor="middle" font-size="12.5" fill="#2c5c44">连杆高度链、台架/机身界面</text>
|
||||
|
||||
<rect x="40" y="206" width="270" height="86" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.6"/>
|
||||
<text x="175" y="230" text-anchor="middle" font-size="15" font-weight="bold" fill="#14472c">arm_model.py · arm_build.py</text>
|
||||
<text x="175" y="251" text-anchor="middle" font-size="12.5" fill="#2c5c44">参数化 CAD:9 打印件 + 6 电机模组</text>
|
||||
<text x="175" y="269" text-anchor="middle" font-size="12.5" fill="#2c5c44">装配体 / STEP / STL / 质量属性</text>
|
||||
|
||||
<rect x="40" y="324" width="270" height="70" rx="8" fill="#f6f8fa" stroke="#8aa2b5" stroke-width="1.2"/>
|
||||
<text x="175" y="348" text-anchor="middle" font-size="13" font-weight="bold" fill="#3d5164">strength_check.py · torque_balance.py</text>
|
||||
<text x="175" y="368" text-anchor="middle" font-size="12" fill="#5c7185">同一关节表:截面刚度 / 重力力矩网格搜索</text>
|
||||
|
||||
<rect x="40" y="426" width="270" height="70" rx="8" fill="#fdf2ee" stroke="#b0562f" stroke-width="1.4"/>
|
||||
<text x="175" y="450" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7c3a1c">sweep_v2.py(离线权威碰撞)</text>
|
||||
<text x="175" y="470" text-anchor="middle" font-size="12" fill="#8a5433">多进程 + GJK 预筛 + OCCT 精确布尔</text>
|
||||
|
||||
<rect x="40" y="528" width="270" height="62" rx="8" fill="#fdf2ee" stroke="#b0562f" stroke-width="1.2" stroke-dasharray="5 3"/>
|
||||
<text x="175" y="552" text-anchor="middle" font-size="13" font-weight="bold" fill="#7c3a1c">scratch/fit_capsules.py</text>
|
||||
<text x="175" y="571" text-anchor="middle" font-size="12" fill="#8a5433">CAD 网格 → 实时胶囊模型拟合</text>
|
||||
|
||||
<line x1="175" y1="174" x2="175" y2="204" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="175" y1="292" x2="175" y2="322" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="175" y1="394" x2="175" y2="424" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
|
||||
<!-- ② 数据契约 -->
|
||||
<g>
|
||||
<rect x="365" y="426" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
|
||||
<text x="490" y="449" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">travel.json / travel_detail.json</text>
|
||||
<text x="490" y="468" text-anchor="middle" font-size="11.5" fill="#8f6d1c">单关节硬限位(滑块)+ 组合折叠明细</text>
|
||||
<rect x="365" y="528" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
|
||||
<text x="490" y="551" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">collide.json</text>
|
||||
<text x="490" y="570" text-anchor="middle" font-size="11.5" fill="#8f6d1c">1995 胶囊 + adj 机械区间 + 硬边标记</text>
|
||||
<rect x="365" y="324" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
|
||||
<text x="490" y="347" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">torque_balance.json · strength.json</text>
|
||||
<text x="490" y="366" text-anchor="middle" font-size="11.5" fill="#8f6d1c">逐关节力矩负载率 / 截面安全系数</text>
|
||||
<rect x="365" y="222" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
|
||||
<text x="490" y="245" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">mesh_arm.json(网格)</text>
|
||||
<text x="490" y="264" text-anchor="middle" font-size="11.5" fill="#8f6d1c">AB.export → build_viewer.py 打包</text>
|
||||
</g>
|
||||
|
||||
<line x1="310" y1="455" x2="363" y2="455" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="310" y1="559" x2="363" y2="559" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="310" y1="355" x2="363" y2="355" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="310" y1="255" x2="363" y2="252" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<text x="336" y="446" text-anchor="middle" font-size="11" fill="#5c7185">写出</text>
|
||||
<text x="336" y="550" text-anchor="middle" font-size="11" fill="#5c7185">写出</text>
|
||||
|
||||
<!-- ③ 查看器 -->
|
||||
<rect x="670" y="120" width="270" height="96" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
|
||||
<text x="805" y="145" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">arm-ik-solver.js(ArmIK)</text>
|
||||
<text x="805" y="166" text-anchor="middle" font-size="12.5" fill="#3a5a82">闭式支 + (θ4,θ5) 扫描 + 牛顿收口</text>
|
||||
<text x="805" y="184" text-anchor="middle" font-size="12.5" fill="#3a5a82">DLS 兜底 · 实时胶囊碰撞 · 动态限位</text>
|
||||
<text x="805" y="202" text-anchor="middle" font-size="12.5" fill="#3a5a82">纯 JS 无依赖 · 主线程 / Worker 同源</text>
|
||||
|
||||
<rect x="670" y="248" width="270" height="76" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
|
||||
<text x="805" y="273" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">ik-glue.js(交互层)</text>
|
||||
<text x="805" y="294" text-anchor="middle" font-size="12.5" fill="#3a5a82">拖拽 / 箭头姿态 / 实时追踪 / Worker</text>
|
||||
<text x="805" y="312" text-anchor="middle" font-size="12.5" fill="#3a5a82">滑块动态限位夹取(边界缓存)</text>
|
||||
|
||||
<rect x="670" y="356" width="270" height="70" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
|
||||
<text x="805" y="381" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">three.js 场景图</text>
|
||||
<text x="805" y="402" text-anchor="middle" font-size="12.5" fill="#3a5a82">6 级嵌套 pivot(与 FK 链逐位一致)</text>
|
||||
<text x="805" y="420" text-anchor="middle" font-size="12.5" fill="#3a5a82">FK 渲染 + 残差复核(双通道对账)</text>
|
||||
|
||||
<line x1="615" y1="460" x2="668" y2="168" stroke="#7c5cd6" stroke-width="1.8" marker-end="url(#arb)"/>
|
||||
<line x1="615" y1="556" x2="668" y2="300" stroke="#7c5cd6" stroke-width="1.8" marker-end="url(#arb)"/>
|
||||
<line x1="615" y1="250" x2="668" y2="392" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="805" y1="216" x2="805" y2="246" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<line x1="805" y1="324" x2="805" y2="354" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
|
||||
<text x="636" y="508" font-size="11.5" fill="#7c5cd6">限位 / 碰撞注入求解器与 UI</text>
|
||||
<text x="621" y="262" font-size="11" fill="#5c7185" stroke="#ffffff" stroke-width="3" paint-order="stroke">几何网格</text>
|
||||
|
||||
<!-- human_layout → solver 直接注入 -->
|
||||
<path d="M 310 130 C 480 96, 560 100, 668 138" fill="none" stroke="#1f6f43" stroke-width="1.8" stroke-dasharray="6 4" marker-end="url(#ar)"/>
|
||||
<text x="470" y="84" text-anchor="middle" font-size="11.5" fill="#1f6f43">关节 rear/d 常量经 build_viewer 注入两处(CAD 与网页同源)</text>
|
||||
|
||||
<!-- 底部工作流 -->
|
||||
<rect x="40" y="620" width="900" height="56" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="490" y="643" text-anchor="middle" font-size="12.5" fill="#3d5164">改几何的同步纪律:arm_model.py 重建 → AB.export 写 mesh_arm.json → build_viewer.py 重打包 → 渲染出图确认 →</text>
|
||||
<text x="490" y="663" text-anchor="middle" font-size="12.5" fill="#3d5164">几何变更必须重跑 sweep_v2.py(行程/碰撞权威);collide.json 由 fit_capsules.py 重新拟合。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,125 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 760" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="760" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="axr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#d64545"/>
|
||||
</marker>
|
||||
<marker id="axb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
|
||||
</marker>
|
||||
<marker id="dim" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#8a6d3b"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">运动链与关节轴(正视简化示意)</text>
|
||||
<text x="490" y="58" text-anchor="middle" font-size="13" fill="#5c7185">沿 −Y 看:X 向右、Z 向上、Y 指向纸内(⊗)。零位 θ=0,臂竖直下垂。</text>
|
||||
|
||||
<!-- 地面与台架 -->
|
||||
<line x1="70" y1="700" x2="910" y2="700" stroke="#8a9aa8" stroke-width="3"/>
|
||||
<rect x="150" y="676" width="290" height="24" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
|
||||
<rect x="228" y="80" width="52" height="598" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
|
||||
<rect x="276" y="78" width="288" height="26" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
|
||||
<text x="254" y="666" text-anchor="middle" font-size="11.5" fill="#5c7185">立柱 P01(台架)</text>
|
||||
<text x="295" y="694" text-anchor="middle" font-size="11.5" fill="#5c7185">底座 P02</text>
|
||||
|
||||
<!-- 臂主干 -->
|
||||
<rect x="592" y="86" width="16" height="522" rx="8" fill="#c3d2e0" stroke="#7ba3c7" stroke-width="1"/>
|
||||
<rect x="560" y="72" width="80" height="36" rx="8" fill="#dbe7f3" stroke="#7ba3c7" stroke-width="1.4"/>
|
||||
|
||||
<!-- J1: X 轴 -->
|
||||
<line x1="508" y1="90" x2="692" y2="90" stroke="#d64545" stroke-width="2.4" marker-start="url(#axr)" marker-end="url(#axr)"/>
|
||||
<circle cx="600" cy="90" r="7" fill="#7c5cd6"/>
|
||||
<text x="702" y="82" font-size="13" font-weight="bold" fill="#16324f">J1 肩抬举</text>
|
||||
<text x="702" y="100" font-size="12" fill="#5c7185">轴 +X(滚转,前后方向)</text>
|
||||
|
||||
<!-- J2: Y ⊗ -->
|
||||
<circle cx="600" cy="210" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="593" y1="203" x2="607" y2="217" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="607" y1="203" x2="593" y2="217" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<text x="620" y="202" font-size="13" font-weight="bold" fill="#16324f">J2 肩俯仰</text>
|
||||
<text x="620" y="220" font-size="12" fill="#5c7185">轴 +Y(⊗,与 J4/J5 平行同向)</text>
|
||||
|
||||
<!-- J3: Z 自转 -->
|
||||
<line x1="600" y1="225" x2="600" y2="352" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5" marker-start="url(#axb)" marker-end="url(#axb)"/>
|
||||
<ellipse cx="600" cy="255" rx="26" ry="9" fill="none" stroke="#3572c9" stroke-width="1.6"/>
|
||||
<circle cx="600" cy="255" r="5.5" fill="#7c5cd6"/>
|
||||
<text x="640" y="250" font-size="13" font-weight="bold" fill="#16324f">J3 大臂自转</text>
|
||||
<text x="640" y="268" font-size="12" fill="#5c7185">轴 −Z(臂自身轴线)</text>
|
||||
|
||||
<!-- J4 -->
|
||||
<circle cx="600" cy="368" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="593" y1="361" x2="607" y2="375" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="607" y1="361" x2="593" y2="375" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<text x="620" y="360" font-size="13" font-weight="bold" fill="#16324f">J4 肘部俯仰</text>
|
||||
<text x="620" y="378" font-size="12" fill="#5c7185">轴 +Y(⊗)</text>
|
||||
|
||||
<!-- J5 -->
|
||||
<circle cx="600" cy="478" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="593" y1="471" x2="607" y2="485" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<line x1="607" y1="471" x2="593" y2="485" stroke="#1f9d63" stroke-width="2.2"/>
|
||||
<text x="620" y="470" font-size="13" font-weight="bold" fill="#16324f">J5 腕部俯仰</text>
|
||||
<text x="620" y="488" font-size="12" fill="#5c7185">轴 +Y(⊗,与 J4 同向 ⇒ R4·R5 可合并)</text>
|
||||
|
||||
<!-- J6 -->
|
||||
<line x1="600" y1="492" x2="600" y2="596" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5" marker-start="url(#axb)" marker-end="url(#axb)"/>
|
||||
<ellipse cx="600" cy="524" rx="24" ry="8" fill="none" stroke="#3572c9" stroke-width="1.6"/>
|
||||
<circle cx="600" cy="524" r="5.5" fill="#7c5cd6"/>
|
||||
<text x="640" y="520" font-size="13" font-weight="bold" fill="#16324f">J6 腕自转</text>
|
||||
<text x="640" y="538" font-size="12" fill="#5c7185">轴 −Z(滚转,TCP 在轴线上 ⇒ 不动位置)</text>
|
||||
|
||||
<!-- TCP -->
|
||||
<circle cx="600" cy="607" r="6" fill="#e08a1e"/>
|
||||
<text x="620" y="612" font-size="13" font-weight="bold" fill="#b06a10">TCP</text>
|
||||
<text x="620" y="630" font-size="11.5" fill="#5c7185">工具法兰安装面中心(J6 组打印件最低面)</text>
|
||||
|
||||
<!-- 左侧尺寸链 -->
|
||||
<g stroke="#8a6d3b" stroke-width="1.3">
|
||||
<line x1="480" y1="90" x2="480" y2="210" marker-start="url(#dim)" marker-end="url(#dim)"/>
|
||||
<line x1="480" y1="210" x2="480" y2="255" marker-start="url(#dim)" marker-end="url(#dim)"/>
|
||||
<line x1="480" y1="255" x2="480" y2="368" marker-start="url(#dim)" marker-end="url(#dim)"/>
|
||||
<line x1="480" y1="368" x2="480" y2="478" marker-start="url(#dim)" marker-end="url(#dim)"/>
|
||||
<line x1="480" y1="478" x2="480" y2="524" marker-start="url(#dim)" marker-end="url(#dim)"/>
|
||||
<line x1="470" y1="90" x2="490" y2="90"/><line x1="470" y1="210" x2="490" y2="210"/>
|
||||
<line x1="470" y1="255" x2="490" y2="255"/><line x1="470" y1="368" x2="490" y2="368"/>
|
||||
<line x1="470" y1="478" x2="490" y2="478"/><line x1="470" y1="524" x2="490" y2="524"/>
|
||||
</g>
|
||||
<g font-size="11.5" fill="#8a6d3b" text-anchor="end">
|
||||
<text x="472" y="154">C0 = 120</text>
|
||||
<text x="472" y="236">45</text>
|
||||
<text x="472" y="315">113</text>
|
||||
<text x="472" y="427">110(前臂)</text>
|
||||
<text x="472" y="505">46</text>
|
||||
</g>
|
||||
<g font-size="11" fill="#8a9aa8">
|
||||
<text x="498" y="122">z=610</text><text x="498" y="226">z=490</text><text x="498" y="271">z=445</text>
|
||||
<text x="498" y="384">z=332</text><text x="498" y="494">z=222</text><text x="498" y="540">z=176</text>
|
||||
</g>
|
||||
|
||||
<!-- 右侧段名 -->
|
||||
<g font-size="12" fill="#3d5164">
|
||||
<text x="800" y="154" text-anchor="middle">肩关节组</text>
|
||||
<line x1="770" y1="92" x2="770" y2="212" stroke="#b9c8d6" stroke-width="1.2"/>
|
||||
<text x="800" y="295" text-anchor="middle">大臂 158</text>
|
||||
<line x1="770" y1="212" x2="770" y2="370" stroke="#b9c8d6" stroke-width="1.2"/>
|
||||
<text x="800" y="428" text-anchor="middle">前臂 110</text>
|
||||
<line x1="770" y1="370" x2="770" y2="480" stroke="#b9c8d6" stroke-width="1.2"/>
|
||||
<text x="800" y="560" text-anchor="middle">腕组 46 + 法兰</text>
|
||||
<line x1="770" y1="480" x2="770" y2="606" stroke="#b9c8d6" stroke-width="1.2"/>
|
||||
</g>
|
||||
|
||||
<!-- 图例 -->
|
||||
<rect x="70" y="560" width="300" height="120" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="86" y="586" font-size="13" font-weight="bold" fill="#16324f">图例</text>
|
||||
<line x1="86" y1="606" x2="126" y2="606" stroke="#d64545" stroke-width="2.4" marker-start="url(#axr)" marker-end="url(#axr)"/>
|
||||
<text x="136" y="610" font-size="12" fill="#3d5164">X 轴(水平,前后)</text>
|
||||
<circle cx="106" cy="630" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="101" y1="625" x2="111" y2="635" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="111" y1="625" x2="101" y2="635" stroke="#1f9d63" stroke-width="2"/>
|
||||
<text x="136" y="634" font-size="12" fill="#3d5164">Y 轴(指向纸内)</text>
|
||||
<line x1="106" y1="648" x2="106" y2="668" stroke="#3572c9" stroke-width="2" marker-start="url(#axb)" marker-end="url(#axb)"/>
|
||||
<text x="136" y="662" font-size="12" fill="#3d5164">Z 轴(竖直,J3/J6 实为 −Z)</text>
|
||||
|
||||
<rect x="70" y="700" width="0" height="0" fill="none"/>
|
||||
<text x="490" y="730" text-anchor="middle" font-size="12.5" fill="#3d5164">结构前提(求解器启动即断言):J2/J4/J5 平行同向且同一 x 平面(X_ARM≈19.5)、J1 ∥ X、J3/J6 ∥ Z、TCP 落在 J6 轴线上。</text>
|
||||
<text x="490" y="750" text-anchor="middle" font-size="12.5" fill="#3d5164">这五条性质让六轴问题可以降阶为「二维冗余扫描 + 闭式支」,任何一条不成立时求解器会打出 WARN 并回落通用路径。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 440" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="440" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="fb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">正运动学:枢轴变换与链式累积</text>
|
||||
|
||||
<!-- 左:单关节枢轴 -->
|
||||
<text x="230" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">单关节:绕“不在原点的轴”旋转</text>
|
||||
<line x1="230" y1="110" x2="230" y2="300" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5"/>
|
||||
<rect x="212" y="160" width="36" height="120" rx="10" fill="#e7edf3" stroke="#9db0c0" stroke-width="1.4" stroke-dasharray="5 4"/>
|
||||
<rect x="212" y="160" width="36" height="120" rx="10" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6" transform="rotate(32 230 160)"/>
|
||||
<ellipse cx="230" cy="160" rx="46" ry="14" fill="none" stroke="#3572c9" stroke-width="1.8" marker-end="url(#fb)"/>
|
||||
<circle cx="230" cy="160" r="7" fill="#7c5cd6"/>
|
||||
<text x="296" y="148" font-size="12.5" fill="#3d5164">枢轴点 P + 轴方向 d</text>
|
||||
<text x="296" y="166" font-size="12.5" fill="#3d5164">θ=0 位形(虚线)→ θ 位形(蓝)</text>
|
||||
<text x="296" y="184" font-size="12.5" fill="#3d5164">关节角 θi = 绕轴 d 的有符号转角</text>
|
||||
|
||||
<rect x="60" y="318" width="400" height="104" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="80" y="344" font-size="13" fill="#16324f" font-family="Consolas,monospace">A(θ) = T(P) · R(d,θ) · T(−P)</text>
|
||||
<text x="80" y="368" font-size="13" fill="#16324f" font-family="Consolas,monospace">x' = R·(x − P) + P</text>
|
||||
<text x="80" y="396" font-size="12" fill="#5c7185">先平移枢轴点到原点 → 绕轴转 θ → 平移回去。关节表只存</text>
|
||||
<text x="80" y="412" font-size="12" fill="#5c7185">P(rear)与 d(单位轴向量),六个关节共 12 个常量。</text>
|
||||
|
||||
<!-- 右:链 -->
|
||||
<text x="735" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">六链累积:World = A₁·A₂·A₃·A₄·A₅·A₆·TCP</text>
|
||||
<g font-size="14" font-weight="bold" text-anchor="middle">
|
||||
<rect x="510" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="536" y="157" fill="#153a6e">A1</text>
|
||||
<rect x="578" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="604" y="157" fill="#153a6e">A2</text>
|
||||
<rect x="646" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="672" y="157" fill="#153a6e">A3</text>
|
||||
<rect x="714" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="740" y="157" fill="#153a6e">A4</text>
|
||||
<rect x="782" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="808" y="157" fill="#153a6e">A5</text>
|
||||
<rect x="850" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
|
||||
<text x="876" y="157" fill="#153a6e">A6</text>
|
||||
</g>
|
||||
<g font-size="11" fill="#5c7185" text-anchor="middle">
|
||||
<text x="536" y="190">θ1</text><text x="604" y="190">θ2</text><text x="672" y="190">θ3</text>
|
||||
<text x="740" y="190">θ4</text><text x="808" y="190">θ5</text><text x="876" y="190">θ6</text>
|
||||
</g>
|
||||
<g stroke="#5a7d9a" stroke-width="1.5">
|
||||
<line x1="472" y1="152" x2="508" y2="152"/>
|
||||
<line x1="562" y1="152" x2="576" y2="152"/>
|
||||
<line x1="630" y1="152" x2="644" y2="152"/>
|
||||
<line x1="698" y1="152" x2="712" y2="152"/>
|
||||
<line x1="766" y1="152" x2="780" y2="152"/>
|
||||
<line x1="834" y1="152" x2="848" y2="152"/>
|
||||
<line x1="902" y1="152" x2="918" y2="152"/>
|
||||
</g>
|
||||
<text x="462" y="157" text-anchor="end" font-size="13" font-weight="bold" fill="#16324f">World</text>
|
||||
<text x="960" y="157" text-anchor="end" font-size="13" font-weight="bold" fill="#b06a10">TCP</text>
|
||||
<text x="735" y="216" text-anchor="middle" font-size="12" fill="#5c7185">每级只读父级位姿与自己 —— 与 three.js 场景图 6 级嵌套 pivot 逐位一致</text>
|
||||
|
||||
<rect x="510" y="240" width="420" height="192" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="530" y="266" font-size="13" font-weight="bold" fill="#16324f">两套实现,互为对账:</text>
|
||||
<text x="530" y="290" font-size="12.5" fill="#3d5164">· fkChain():严格 4×4 同质矩阵链 —— 对照基准,慢而直白</text>
|
||||
<text x="530" y="312" font-size="12.5" fill="#3d5164">· fk():按结构常量展开三角函数的闭式快路径 —— 求解器</text>
|
||||
<text x="530" y="332" font-size="12.5" fill="#3d5164"> 热路径专用,每姿态仅 ~20 次乘加</text>
|
||||
<text x="530" y="360" font-size="12.5" fill="#3d5164">· 自检:5000 个随机位形,两实现的 TCP 差 ≤ 5.7e-13 mm</text>
|
||||
<text x="530" y="384" font-size="12.5" fill="#3d5164">· 工具长度 tool 沿 J6 轴外伸:TCP_z 随 tool 平移(ax=J6 轴)</text>
|
||||
<text x="530" y="406" font-size="11.5" fill="#8a9aa8">指纹校验:预置自检值绑定(限位表+常量+源码)的 FNV-1a 指纹,</text>
|
||||
<text x="530" y="422" font-size="11.5" fill="#8a9aa8">模型或求解器一变即提示重算。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 920" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="920" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="pa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
|
||||
</marker>
|
||||
<marker id="pao" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#b06a10"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">逆运动学求解流水线(solve → solveDyn)</text>
|
||||
|
||||
<!-- 主流程 -->
|
||||
<g>
|
||||
<rect x="265" y="60" width="450" height="64" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6"/>
|
||||
<text x="280" y="84" font-size="12.5" font-weight="bold" fill="#153a6e">① 输入</text>
|
||||
<text x="280" y="102" font-size="12" fill="#2a4a6e">目标 TCP (x,y,z) · 工具长 tool · 可选姿态 R(az,el,roll)</text>
|
||||
<text x="280" y="118" font-size="12" fill="#2a4a6e">ref = 当前姿态:取「本步行程最小」的构型</text>
|
||||
|
||||
<rect x="265" y="148" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
|
||||
<text x="280" y="172" font-size="12.5" font-weight="bold" fill="#16324f">② 冗余维准备:(θ4,θ5) 网格 + 逐行可行段探测</text>
|
||||
<text x="280" y="190" font-size="12" fill="#3d5164">步长 h=0.25°;先按探测步 hp 找每行有解的连续段(位置 2° / 姿态 0.5°)</text>
|
||||
<text x="280" y="206" font-size="12" fill="#3d5164">段端外扩 PAD=2° 兜住段界不确定性;段内仍按 h 精扫,密度不降</text>
|
||||
|
||||
<rect x="265" y="236" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
|
||||
<text x="280" y="260" font-size="12.5" font-weight="bold" fill="#16324f">③ 全域粗扫:每格 branch() 闭式解 θ1..θ3(≤4 支)</text>
|
||||
<text x="280" y="279" font-size="12" fill="#3d5164">位置由闭式支精确命中(零残差),扫描只优化冗余维;候选有界保留 4000</text>
|
||||
|
||||
<rect x="265" y="324" width="450" height="64" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="280" y="348" font-size="12.5" font-weight="bold" fill="#7a5a08">④ [姿态任务] 孤立根枚举:残差变号括号 → 阻尼高斯-牛顿收根</text>
|
||||
<text x="280" y="367" font-size="12" fill="#6b5312">切向相切的根不变号,按每支 ‖e‖ 最小的节点补种;精确根直接进候选池</text>
|
||||
|
||||
<rect x="265" y="412" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
|
||||
<text x="280" y="436" font-size="12.5" font-weight="bold" fill="#16324f">⑤ 局部加密 + 模式搜索:32 种子 × 窗口 ±2h(步长 h/8)</text>
|
||||
<text x="280" y="455" font-size="12" fill="#3d5164">8 邻域模式搜索步长逐轮减半至 2e-4°,把每条谷底走到最低</text>
|
||||
|
||||
<rect x="265" y="500" width="450" height="64" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="280" y="524" font-size="12.5" font-weight="bold" fill="#7a5a08">⑥ [姿态任务] 二维牛顿收口 + escalating 加压</text>
|
||||
<text x="280" y="542" font-size="12" fill="#6b5312">对工具轴切向偏差做 (θ4,θ5) 牛顿迭代,轴残差 →1e-12、位置保持精确</text>
|
||||
<text x="280" y="558" font-size="12" fill="#6b5312">仍无 oerr≤0.05° 达标解则姿态权重 ×100 重扫(最多 3 轮)</text>
|
||||
|
||||
<rect x="265" y="588" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
|
||||
<text x="280" y="612" font-size="12.5" font-weight="bold" fill="#16324f">⑦ 选解:cost = Σ w·|wrap180(θ−ref)| + 1e-6·Σ|θ|</text>
|
||||
<text x="280" y="630" font-size="12" fill="#3d5164">姿态任务先取 oerr≤0.05° 达标集,集内再比代价</text>
|
||||
<text x="280" y="646" font-size="12" fill="#3d5164">精确根只在代价 ≤ 达标最优+2° 时接管(防漏检多付几十度)</text>
|
||||
|
||||
<rect x="265" y="676" width="450" height="64" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.4"/>
|
||||
<text x="280" y="700" font-size="12.5" font-weight="bold" fill="#14472c">⑧ solveDyn 碰撞后处理(装了胶囊模型时)</text>
|
||||
<text x="280" y="718" font-size="12" fill="#2c5c44">解先过全姿态碰撞校验:不安全 → 候选表找 ≤1.5×cost+20° 的安全解</text>
|
||||
<text x="280" y="734" font-size="12" fill="#2c5c44">贴边轴二分动态边界放宽(≥5° 才值得重解,重解 cost 不升才采用)</text>
|
||||
|
||||
<rect x="265" y="764" width="450" height="58" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6"/>
|
||||
<text x="280" y="788" font-size="12.5" font-weight="bold" fill="#153a6e">输出:q[6] · 残差 err / oerr · 本步行程 moveSum · 候选表 list</text>
|
||||
<text x="280" y="806" font-size="12" fill="#2a4a6e">徽章分级:已到达 / 姿态到达·位置最近 / 混合折中 / 不可达·最近位姿</text>
|
||||
</g>
|
||||
|
||||
<g stroke="#5a7d9a" stroke-width="1.8">
|
||||
<line x1="490" y1="124" x2="490" y2="146" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="212" x2="490" y2="234" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="300" x2="490" y2="322" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="388" x2="490" y2="410" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="476" x2="490" y2="498" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="564" x2="490" y2="586" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="652" x2="490" y2="674" marker-end="url(#pa)"/>
|
||||
<line x1="490" y1="740" x2="490" y2="762" marker-end="url(#pa)"/>
|
||||
</g>
|
||||
|
||||
<!-- 右侧:快路 -->
|
||||
<rect x="740" y="236" width="215" height="196" rx="10" fill="#ffffff" stroke="#b06a10" stroke-width="1.6" stroke-dasharray="7 5"/>
|
||||
<text x="755" y="262" font-size="13" font-weight="bold" fill="#b06a10">solveWarm 实时快路</text>
|
||||
<text x="755" y="284" font-size="11.5" fill="#6b5312">拖拽中:只在上帧解附近的</text>
|
||||
<text x="755" y="302" font-size="11.5" fill="#6b5312">(θ4,θ5) 递进窗口 ±4→14→40°</text>
|
||||
<text x="755" y="320" font-size="11.5" fill="#6b5312">同支跟随(Σ|Δθ|≤25°)+ 残差</text>
|
||||
<text x="755" y="338" font-size="11.5" fill="#6b5312">≤1e-6 + 姿态牛顿收口</text>
|
||||
<text x="755" y="362" font-size="11.5" font-weight="bold" fill="#b06a10">任一校验不过 → null</text>
|
||||
<text x="755" y="380" font-size="11.5" fill="#6b5312">自动回落全量扫描(③)</text>
|
||||
<text x="755" y="400" font-size="11" fill="#8a9aa8">快路只负责「能快时快」,</text>
|
||||
<text x="755" y="416" font-size="11" fill="#8a9aa8">松手后全量求解才是终值</text>
|
||||
<path d="M 738 268 C 727 240, 727 130, 719 96" fill="none" stroke="#b06a10" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
|
||||
<path d="M 847 434 C 847 480, 700 560, 721 600" fill="none" stroke="#b06a10" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
|
||||
<text x="742" y="470" font-size="11" fill="#b06a10">校验不过回落</text>
|
||||
|
||||
<!-- 左侧:兜底 -->
|
||||
<rect x="25" y="324" width="215" height="230" rx="10" fill="#ffffff" stroke="#8a5a8f" stroke-width="1.6" stroke-dasharray="7 5"/>
|
||||
<text x="40" y="350" font-size="13" font-weight="bold" fill="#7a3a80">兜底:目标不可达时</text>
|
||||
<text x="40" y="372" font-size="11.5" fill="#5c3a60">closest() 位置 DLS:160 迭代 ×</text>
|
||||
<text x="40" y="390" font-size="11.5" fill="#5c3a60">160 次 LCG 确定性重启(同输入</text>
|
||||
<text x="40" y="408" font-size="11.5" fill="#5c3a60">逐位一致)+ anchor 回拉 0.03</text>
|
||||
<text x="40" y="426" font-size="11.5" fill="#5c3a60">压退化漂移 + 零空间抛光收敛</text>
|
||||
<text x="40" y="450" font-size="11.5" fill="#5c3a60">closest6() 位置+姿态 6 维联合</text>
|
||||
<text x="40" y="468" font-size="11.5" fill="#5c3a60">DLS(姿态按 20mm/rad 折算)</text>
|
||||
<text x="40" y="492" font-size="11.5" fill="#5c3a60">closestOri() 姿态优先:闭式轴</text>
|
||||
<text x="40" y="510" font-size="11.5" fill="#5c3a60">对齐 + (θ3,s,θ4) 坐标下降,</text>
|
||||
<text x="40" y="528" font-size="11.5" fill="#5c3a60">姿态精确、位置取最近</text>
|
||||
<text x="40" y="548" font-size="11" fill="#8a9aa8">距离差 ≤0.5mm 时优先锚定解</text>
|
||||
<path d="M 263 620 C 180 620, 140 590, 133 556" fill="none" stroke="#8a5a8f" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
|
||||
<text x="148" y="648" font-size="11" fill="#8a5a8f">0 采样点 / 不可达</text>
|
||||
<path d="M 133 322 C 140 220, 200 130, 262 96" fill="none" stroke="#8a5a8f" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
|
||||
<text x="120" y="200" font-size="11" fill="#8a5a8f">返回最近位姿</text>
|
||||
|
||||
<text x="490" y="860" text-anchor="middle" font-size="12.5" fill="#3d5164">热路径性能手段:轴对齐特化 rotAxis(免 Rodrigues 通用式)· sin/cos 单槽缓存 · 关节限位数组预取 · 残差 fk 复核延后到入选候选统一补算</text>
|
||||
<text x="490" y="884" text-anchor="middle" font-size="12.5" fill="#3d5164">全量扫描 40~430 ms(后台 Worker);实时快路毫秒级 —— 这是拖拽能跟手的原因</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,88 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 620" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="620" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="ta" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#8a9aa8"/>
|
||||
</marker>
|
||||
<marker id="to" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">冗余维 (θ4,θ5) 平面:可行带、加密窗口与牛顿收口</text>
|
||||
|
||||
<!-- 绘图区 -->
|
||||
<rect x="110" y="80" width="740" height="430" fill="#f8fafc" stroke="#b9c8d6" stroke-width="1.2"/>
|
||||
<!-- 可行带(斜窄带,宽度做了夸张) -->
|
||||
<polygon points="245,452 830,168 830,132 245,416" fill="#d9f2e5" opacity="0.9"/>
|
||||
<text x="585" y="215" font-size="12" fill="#1f7a4d" transform="rotate(-26 585 215)">可行带:branch 判据筛掉 73~93% 的格点</text>
|
||||
|
||||
<!-- 不可行散点 -->
|
||||
<g fill="#c3ccd4" font-size="10">
|
||||
<text x="200" y="140">×</text><text x="330" y="120">×</text><text x="500" y="110">×</text>
|
||||
<text x="700" y="128">×</text><text x="790" y="180">×</text><text x="170" y="300">×</text>
|
||||
<text x="300" y="260">×</text><text x="430" y="210">×</text><text x="640" y="150">×</text>
|
||||
<text x="220" y="470">×</text><text x="480" y="480">×</text><text x="700" y="460">×</text>
|
||||
<text x="820" y="430">×</text><text x="360" y="420">×</text><text x="560" y="440">×</text>
|
||||
</g>
|
||||
<!-- 可行带内格点 -->
|
||||
<g fill="#2f9d68">
|
||||
<circle cx="270" cy="428" r="2.4"/><circle cx="310" cy="410" r="2.4"/><circle cx="352" cy="390" r="2.4"/>
|
||||
<circle cx="395" cy="368" r="2.4"/><circle cx="438" cy="348" r="2.4"/><circle cx="480" cy="328" r="2.4"/>
|
||||
<circle cx="522" cy="308" r="2.4"/><circle cx="565" cy="288" r="2.4"/><circle cx="608" cy="268" r="2.4"/>
|
||||
<circle cx="650" cy="248" r="2.4"/><circle cx="692" cy="228" r="2.4"/><circle cx="735" cy="206" r="2.4"/>
|
||||
<circle cx="778" cy="186" r="2.4"/><circle cx="812" cy="168" r="2.4"/>
|
||||
<circle cx="292" cy="418" r="2.4"/><circle cx="375" cy="378" r="2.4"/><circle cx="459" cy="338" r="2.4"/>
|
||||
<circle cx="543" cy="298" r="2.4"/><circle cx="628" cy="256" r="2.4"/><circle cx="712" cy="216" r="2.4"/>
|
||||
<circle cx="796" cy="178" r="2.4"/>
|
||||
</g>
|
||||
|
||||
<!-- 行可行段 -->
|
||||
<g stroke="#3572c9" stroke-width="5" opacity="0.75">
|
||||
<line x1="228" y1="440" x2="360" y2="376"/>
|
||||
<line x1="415" y1="352" x2="548" y2="288"/>
|
||||
<line x1="602" y1="264" x2="735" y2="200"/>
|
||||
</g>
|
||||
<text x="238" y="486" font-size="12" fill="#2a4a6e">逐行可行段 rowSpans(探测步 hp,段端外扩 PAD=2°):</text>
|
||||
<text x="238" y="504" font-size="12" fill="#2a4a6e">每行通常是单一连续段,段内仍按 h 精扫 —— 密度不降、成本大降</text>
|
||||
|
||||
<!-- 加密窗口 -->
|
||||
<rect x="478" y="278" width="92" height="92" fill="none" stroke="#e08a1e" stroke-width="1.8" stroke-dasharray="6 4"/>
|
||||
<text x="580" y="368" font-size="12" fill="#b06a10">加密窗口 ±2h(步长 h/8,</text>
|
||||
<text x="580" y="386" font-size="12" fill="#b06a10">小步长档 h/4)罩住种子</text>
|
||||
|
||||
<!-- 模式搜索折线 -->
|
||||
<polyline points="500,340 524,318 512,296 540,282 560,296 578,282" fill="none" stroke="#e08a1e" stroke-width="1.8" marker-end="url(#to)"/>
|
||||
<text x="700" y="330" font-size="12" fill="#b06a10">8 邻域模式搜索:</text>
|
||||
<text x="700" y="348" font-size="12" fill="#b06a10">步长减半,停在窄谷谷壁</text>
|
||||
|
||||
<!-- 牛顿收口点 -->
|
||||
<g>
|
||||
<path d="M 596 244 l 4.5 10.5 11 1 -8.5 7.5 2.5 11 -9.5 -6 -9.5 6 2.5 -11 -8.5 -7.5 11 -1 z" fill="#d64545"/>
|
||||
<line x1="602" y1="254" x2="640" y2="286" stroke="#d64545" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<text x="648" y="296" font-size="12.5" font-weight="bold" fill="#b03030">牛顿收口点</text>
|
||||
<text x="648" y="314" font-size="12" fill="#b03030">轴残差 →1e-12(位置保持精确)</text>
|
||||
</g>
|
||||
|
||||
<!-- 坐标轴 -->
|
||||
<g stroke="#8a9aa8" stroke-width="1.4">
|
||||
<line x1="110" y1="510" x2="850" y2="510" marker-start="url(#ta)"/>
|
||||
<line x1="110" y1="510" x2="110" y2="80" marker-end="url(#ta)"/>
|
||||
<line x1="130" y1="510" x2="130" y2="516"/><line x1="245" y1="510" x2="245" y2="516"/>
|
||||
<line x1="411" y1="510" x2="411" y2="516"/><line x1="576" y1="510" x2="576" y2="516"/>
|
||||
<line x1="741" y1="510" x2="741" y2="516"/><line x1="830" y1="510" x2="830" y2="516"/>
|
||||
<line x1="104" y1="500" x2="110" y2="500"/><line x1="104" y1="396" x2="110" y2="396"/>
|
||||
<line x1="104" y1="302" x2="110" y2="302"/><line x1="104" y1="207" x2="110" y2="207"/>
|
||||
<line x1="104" y1="100" x2="110" y2="100"/>
|
||||
</g>
|
||||
<g font-size="11.5" fill="#5c7185">
|
||||
<text x="126" y="530">−111°</text><text x="238" y="530">−90°</text><text x="404" y="530">−60°</text>
|
||||
<text x="569" y="530">−30°</text><text x="734" y="530">0°</text><text x="812" y="530">+16°</text>
|
||||
<text x="66" y="504">−93°</text><text x="66" y="400">−60°</text><text x="66" y="306">−30°</text>
|
||||
<text x="76" y="211">0°</text><text x="80" y="104">+34°</text>
|
||||
<text x="840" y="552" font-size="13" font-weight="bold" fill="#16324f">θ4(J4 肘部俯仰)</text>
|
||||
<text x="66" y="70" font-size="13" font-weight="bold" fill="#16324f">θ5(J5 腕俯仰)</text>
|
||||
</g>
|
||||
|
||||
<text x="490" y="582" text-anchor="middle" font-size="12.5" fill="#3d5164">每个格点的位置解由闭式支精确给出 —— 扫描不是在「逼近」目标,而是在冗余维里「挑构型」;</text>
|
||||
<text x="490" y="602" text-anchor="middle" font-size="12.5" fill="#3d5164">姿态任务的达标带实测最窄 0.98°,2° 探测步会整条漏掉(误报不可达),故姿态任务探测步降到 0.5°。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,80 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 580" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="580" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="ga" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#8a9aa8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">闭式支 branch():给定 (θ4,θ5),θ1..θ3 一步解出(简化示意)</text>
|
||||
|
||||
<!-- 左panel -->
|
||||
<text x="255" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">第 1 步:(θ4,θ5) ⇒ 腕心平面坐标 V</text>
|
||||
<line x1="150" y1="120" x2="150" y2="330" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5"/>
|
||||
<text x="150" y="352" text-anchor="middle" font-size="11.5" fill="#5c7185">J4 轴(+Y,⊗)</text>
|
||||
<circle cx="150" cy="140" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="144" y1="134" x2="156" y2="146" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="156" y1="134" x2="144" y2="146" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="150" y1="140" x2="278" y2="188" stroke="#3572c9" stroke-width="5" stroke-linecap="round"/>
|
||||
<circle cx="278" cy="188" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="272" y1="182" x2="284" y2="194" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="284" y1="182" x2="272" y2="194" stroke="#1f9d63" stroke-width="2"/>
|
||||
<text x="292" y="186" font-size="11.5" fill="#5c7185">J5 轴</text>
|
||||
<line x1="278" y1="188" x2="330" y2="252" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
|
||||
<circle cx="330" cy="252" r="6" fill="#e08a1e"/>
|
||||
<text x="344" y="258" font-size="12" font-weight="bold" fill="#b06a10">腕心方向(L = L5+tool)</text>
|
||||
<path d="M 150 170 A 30 30 0 0 0 178 150.5" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
|
||||
<text x="186" y="178" font-size="12" fill="#7c5cd6">θ4</text>
|
||||
<path d="M 262 216 A 30 30 0 0 1 296 214" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
|
||||
<text x="272" y="232" font-size="12" fill="#7c5cd6">θ5</text>
|
||||
<text x="150" y="120" font-size="11.5" fill="#5c7185">肘(J4)</text>
|
||||
|
||||
<rect x="50" y="386" width="420" height="152" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="68" y="412" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">u = −L·sinθ5 w = A54 − L·cosθ5</text>
|
||||
<text x="68" y="436" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">dx = u·cosθ4 + w·sinθ4</text>
|
||||
<text x="68" y="460" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">dz = A43 − u·sinθ4 + w·cosθ4</text>
|
||||
<text x="68" y="484" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">V = (dx, dz + A32)</text>
|
||||
<text x="68" y="514" font-size="11.5" fill="#5c7185">J4∥J5 同向 ⇒ R4·R5 = R(θ4+θ5):腕心只由这两个角决定,</text>
|
||||
<text x="68" y="530" font-size="11.5" fill="#5c7185">姿态任务里工具轴对齐也因此只差 2 个未知数。</text>
|
||||
|
||||
<!-- 右panel -->
|
||||
<text x="735" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">第 2 步:肩部圆交 + 肘部四支 ⇒ θ1/θ2/θ3</text>
|
||||
<!-- 俯视:J1 轴与 φ -->
|
||||
<circle cx="600" cy="130" r="9" fill="#ffffff" stroke="#d64545" stroke-width="2"/>
|
||||
<circle cx="600" cy="130" r="2" fill="#d64545"/>
|
||||
<text x="576" y="112" font-size="11.5" fill="#5c7185">J1 轴(+X,⊙)</text>
|
||||
<circle cx="700" cy="96" r="5" fill="#e08a1e"/>
|
||||
<text x="712" y="94" font-size="11.5" fill="#b06a10">目标(柱坐标 R, φ)</text>
|
||||
<line x1="600" y1="130" x2="700" y2="96" stroke="#e08a1e" stroke-width="1.6" stroke-dasharray="5 4"/>
|
||||
<path d="M 640 117 A 42 42 0 0 0 636 108" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
|
||||
<text x="648" y="106" font-size="12" fill="#7c5cd6">φ</text>
|
||||
<text x="566" y="158" font-size="12" fill="#16324f" font-family="Consolas,monospace">θ1 = φ − atan2(cc, b)</text>
|
||||
|
||||
<!-- 正视:肩 C0 偏置与肘三角 -->
|
||||
<circle cx="620" cy="290" r="9" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="614" y1="284" x2="626" y2="296" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="626" y1="284" x2="614" y2="296" stroke="#1f9d63" stroke-width="2"/>
|
||||
<text x="560" y="294" text-anchor="end" font-size="11.5" fill="#5c7185">J1(正视)</text>
|
||||
<circle cx="620" cy="382" r="9" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="614" y1="376" x2="626" y2="388" stroke="#1f9d63" stroke-width="2"/>
|
||||
<line x1="626" y1="376" x2="614" y2="388" stroke="#1f9d63" stroke-width="2"/>
|
||||
<text x="560" y="386" text-anchor="end" font-size="11.5" fill="#5c7185">J2</text>
|
||||
<line x1="620" y1="299" x2="620" y2="373" stroke="#8a6d3b" stroke-width="1.3" marker-start="url(#ga)" marker-end="url(#ga)"/>
|
||||
<text x="632" y="341" font-size="11.5" fill="#8a6d3b">C0=120</text>
|
||||
<line x1="620" y1="382" x2="700" y2="322" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
|
||||
<line x1="700" y1="322" x2="820" y2="290" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
|
||||
<circle cx="820" cy="290" r="6" fill="#e08a1e"/>
|
||||
<text x="834" y="294" font-size="12" font-weight="bold" fill="#b06a10">腕心(由 V 决定)</text>
|
||||
<path d="M 676 342 A 44 44 0 0 0 666 314" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
|
||||
<text x="688" y="318" font-size="12" fill="#7c5cd6">σ</text>
|
||||
<text x="690" y="368" font-size="11.5" fill="#5c7185">肘部三角:σ=acos(√m2)</text>
|
||||
<text x="690" y="386" font-size="11.5" fill="#5c7185">θ3 四支:±σ 与 ±(π−σ)</text>
|
||||
<text x="690" y="404" font-size="11.5" fill="#5c7185">(肘上/肘下 × 同号/反号镜像族)</text>
|
||||
|
||||
<rect x="510" y="428" width="440" height="110" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="528" y="454" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">n = (R²+A²−Vx²−Vz²−C0²) / (2·C0),cc = C0+n</text>
|
||||
<text x="528" y="478" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">θ2 = atan2((λA−κn)/det, (κA+λn)/det) κ=Vx·c3, λ=Vz</text>
|
||||
<text x="528" y="502" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">b = −Vx·sinθ3 (θ1 由柱角 φ 反推)</text>
|
||||
<text x="528" y="526" font-size="11.5" fill="#8a5433">退化防线:|Vx|<1e-7、det<1e-12、m2 越界 → 该支无解,直接落选</text>
|
||||
|
||||
<text x="490" y="560" text-anchor="middle" font-size="12.5" fill="#3d5164">示意省略了 X0/Y0 基准平移与 tool 常量;四支解的位置全部精确命中目标 ——「扫描代替数值搜索」的根基。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,76 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 750" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="750" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="fa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">离线碰撞检测(sweep_v2.py):逐层过滤漏斗</text>
|
||||
<text x="490" y="58" text-anchor="middle" font-size="13" fill="#5c7185">目标:把「每姿态都做 NURBS 布尔」压到只剩真正贴 face 的零件对 —— 精确性一分不减,调用量掉几个数量级</text>
|
||||
|
||||
<g font-size="12.5">
|
||||
<rect x="120" y="76" width="600" height="46" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.4"/>
|
||||
<circle cx="145" cy="99" r="12" fill="#3572c9"/><text x="145" y="104" text-anchor="middle" fill="#ffffff" font-weight="bold">1</text>
|
||||
<text x="168" y="95" font-weight="bold" fill="#153a6e">全对全候选对</text>
|
||||
<text x="168" y="113" fill="#2a4a6e">19 件 ≈ 171 对(9 打印件 + 6 电机 + 台架 P01/P02),按姿态刚体变换</text>
|
||||
<line x1="420" y1="122" x2="420" y2="136" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="140" y="138" width="560" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<circle cx="165" cy="161" r="12" fill="#5a86b5"/><text x="165" y="166" text-anchor="middle" fill="#ffffff" font-weight="bold">2</text>
|
||||
<text x="188" y="157" font-weight="bold" fill="#153a6e">跳过刚性安装对</text>
|
||||
<text x="188" y="175" fill="#2a4a6e">MOUNT_PAIRS 12 对:电机 ↔ 被驱动件由螺钉锁死,无相对运动</text>
|
||||
<line x1="420" y1="184" x2="420" y2="198" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="160" y="200" width="520" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<circle cx="185" cy="223" r="12" fill="#5a86b5"/><text x="185" y="228" text-anchor="middle" fill="#ffffff" font-weight="bold">3</text>
|
||||
<text x="208" y="219" font-weight="bold" fill="#153a6e">组跨度规则</text>
|
||||
<text x="208" y="237" fill="#2a4a6e">该姿态中没有活动关节跨越的对跳过(零位时全部必查——静止不代表不干涉)</text>
|
||||
<line x1="420" y1="246" x2="420" y2="260" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="180" y="262" width="480" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<circle cx="205" cy="285" r="12" fill="#5a86b5"/><text x="205" y="290" text-anchor="middle" fill="#ffffff" font-weight="bold">4</text>
|
||||
<text x="228" y="281" font-weight="bold" fill="#153a6e">轴区间预剪枝(单关节扫描专用,精确安全)</text>
|
||||
<text x="228" y="299" fill="#2a4a6e">沿关节轴的轴向/径向区间永不相交的对,任何角度都不可能碰撞</text>
|
||||
<line x1="420" y1="308" x2="420" y2="322" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="200" y="324" width="440" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<circle cx="225" cy="347" r="12" fill="#5a86b5"/><text x="225" y="352" text-anchor="middle" fill="#ffffff" font-weight="bold">5</text>
|
||||
<text x="248" y="343" font-weight="bold" fill="#153a6e">AABB 分离</text>
|
||||
<text x="248" y="361" fill="#2a4a6e">8 角点变换后取包围盒,0.1mm 余量;分离即跳过</text>
|
||||
<line x1="420" y1="370" x2="420" y2="384" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="220" y="386" width="400" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<circle cx="245" cy="409" r="12" fill="#5a86b5"/><text x="245" y="414" text-anchor="middle" fill="#ffffff" font-weight="bold">6</text>
|
||||
<text x="268" y="405" font-weight="bold" fill="#153a6e">包围球分离</text>
|
||||
<text x="268" y="423" fill="#2a4a6e">球心随组变换、半径旋转不变;球分离 ⇒ 零件必分离</text>
|
||||
<line x1="420" y1="432" x2="420" y2="446" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="230" y="444" width="400" height="66" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.4"/>
|
||||
<circle cx="255" cy="477" r="12" fill="#c99418"/><text x="255" y="482" text-anchor="middle" fill="#ffffff" font-weight="bold">7</text>
|
||||
<text x="278" y="468" font-weight="bold" fill="#7a5a08">GJK 凸包预筛(保守证书)</text>
|
||||
<text x="278" y="487" fill="#6b5312">0.5mm 凸包 ⊕ 0.8mm 球:确证分离才允许跳过</text>
|
||||
<text x="278" y="500" fill="#6b5312" font-size="11">可能干涉/数值退化一律回落精确布尔——结果与纯布尔一致</text>
|
||||
<line x1="420" y1="510" x2="420" y2="516" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="255" y="518" width="350" height="66" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.4"/>
|
||||
<circle cx="280" cy="551" r="12" fill="#c99418"/><text x="280" y="556" text-anchor="middle" fill="#ffffff" font-weight="bold">8</text>
|
||||
<text x="303" y="542" font-weight="bold" fill="#7a5a08">顶点侵入实锤(反方向 shortcut)</text>
|
||||
<text x="303" y="561" fill="#6b5312">一方网格顶点(≤240 采样)落入对方实体 >1mm</text>
|
||||
<text x="303" y="574" fill="#6b5312" font-size="11">⇒ 直接判干涉,免 4~12s 布尔;只证「撞」不证「自由」</text>
|
||||
<line x1="420" y1="584" x2="420" y2="590" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
|
||||
|
||||
<rect x="275" y="592" width="300" height="52" rx="8" fill="#fbe9e7" stroke="#c0533a" stroke-width="1.5"/>
|
||||
<circle cx="300" cy="618" r="12" fill="#c0533a"/><text x="300" y="623" text-anchor="middle" fill="#ffffff" font-weight="bold">9</text>
|
||||
<text x="323" y="612" font-weight="bold" fill="#7c2a18">OCCT 精确布尔 Common</text>
|
||||
<text x="323" y="630" fill="#8a3a26">实体交体积 > 2mm³ 记干涉(附干涉质心)</text>
|
||||
<text x="323" y="640" fill="#8a3a26" font-size="11">SetRunParallel(True) · 多进程分发姿态</text>
|
||||
</g>
|
||||
|
||||
<path d="M 745 480 C 800 480, 820 540, 820 590 C 820 620, 790 630, 566 618" fill="none" stroke="#c0533a" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#fa)"/>
|
||||
<text x="806" y="470" text-anchor="middle" font-size="12" fill="#7c2a18">布尔每对 0.5~12s</text>
|
||||
<text x="806" y="488" text-anchor="middle" font-size="12" fill="#7c2a18">前面每层都在为它省调用</text>
|
||||
|
||||
<rect x="60" y="660" width="860" height="66" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="490" y="684" text-anchor="middle" font-size="12.5" fill="#3d5164">扫描策略:单关节双向粗扫 2°(小批量、首个卡死角即停)+ 二分逼近 0.2° → travel.json(滑块硬限位)</text>
|
||||
<text x="490" y="704" text-anchor="middle" font-size="12.5" fill="#3d5164">组合姿态收敛 6°(一个关节保持极限、扫其余,两轮迭代)→ 只写 travel_detail.json,不压缩滑块 | workers = min(12, CPU−2, 内存/2.5GB)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -0,0 +1,70 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 640" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="640" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="ca" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">实时碰撞模型:CAD 网格 → 胶囊体 + 两级宽相位</text>
|
||||
|
||||
<!-- 左:胶囊化手臂示意 -->
|
||||
<text x="235" y="64" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">手臂按运动组胶囊化(fit_capsules.py 拟合)</text>
|
||||
<line x1="90" y1="590" x2="400" y2="590" stroke="#8a9aa8" stroke-width="2.5"/>
|
||||
<rect x="150" y="110" width="34" height="478" rx="6" fill="#e2e8ee" stroke="#9db0c0" stroke-width="1"/>
|
||||
<text x="167" y="102" text-anchor="middle" font-size="11" fill="#5c7185">台架 g0(root 固定)</text>
|
||||
|
||||
<rect x="196" y="128" width="52" height="86" rx="26" fill="#aecbeb" stroke="#3572c9" stroke-width="1.4"/>
|
||||
<text x="256" y="150" font-size="11.5" fill="#153a6e">g1 · J1 组(肩吊臂)</text>
|
||||
<rect x="196" y="226" width="52" height="70" rx="26" fill="#b7dcC8" stroke="#1f9d63" stroke-width="1.4"/>
|
||||
<text x="256" y="248" font-size="11.5" fill="#14573a">g2 · J2 组(肩叉)</text>
|
||||
<rect x="196" y="306" width="52" height="96" rx="26" fill="#a8d5d8" stroke="#1f7a8f" stroke-width="1.4"/>
|
||||
<text x="256" y="336" font-size="11.5" fill="#14525e">g3 · J3 组(大臂骨架)</text>
|
||||
<rect x="196" y="412" width="52" height="78" rx="26" fill="#c9d9a8" stroke="#6f8f2f" stroke-width="1.4"/>
|
||||
<text x="256" y="436" font-size="11.5" fill="#4a5f1c">g4 · J4 组(小臂骨架)</text>
|
||||
<rect x="196" y="498" width="52" height="42" rx="21" fill="#f2d5a8" stroke="#c98a18" stroke-width="1.4"/>
|
||||
<text x="256" y="512" font-size="11.5" fill="#7a5308">g5 · J5 组(腕偏转座)</text>
|
||||
<rect x="196" y="546" width="0" height="0" fill="none"/>
|
||||
<rect x="198" y="548" width="48" height="20" rx="10" fill="#f0bcb0" stroke="#c0533a" stroke-width="1.4"/>
|
||||
<text x="256" y="564" font-size="11.5" fill="#7c2a18">g6 · J6 组(工具法兰)</text>
|
||||
<text x="235" y="608" text-anchor="middle" font-size="11.5" fill="#5c7185">胶囊 = 线段端点 a,b + 半径 r,端点是零位 CAD 世界坐标;</text>
|
||||
<text x="235" y="626" text-anchor="middle" font-size="11.5" fill="#5c7185">组 g 的胶囊随 A1..Ag 嵌套旋转(与 FK 链同一累积变换)。</text>
|
||||
|
||||
<!-- 右上:宽相位级联 -->
|
||||
<text x="700" y="64" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">两级宽相位 → 胶囊精确距</text>
|
||||
<rect x="470" y="80" width="470" height="58" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<text x="486" y="102" font-size="12.5" font-weight="bold" fill="#153a6e">第 1 级 · 零件包围球(19 球)</text>
|
||||
<text x="486" y="122" font-size="11.5" fill="#2a4a6e">球心距 > rad_A+rad_B+rmax_A+rmax_B+margin 才筛除(阈值推导防漏判)</text>
|
||||
<line x1="705" y1="138" x2="705" y2="152" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ca)"/>
|
||||
<rect x="470" y="154" width="470" height="58" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<text x="486" y="176" font-size="12.5" font-weight="bold" fill="#153a6e">第 2 级 · 簇球(每零件胶囊按主轴分桶,每簇 ≤4 枚)</text>
|
||||
<text x="486" y="196" font-size="11.5" fill="#2a4a6e">主轴 = 胶囊中点的幂迭代方向(5 轮);再细分实测得不偿失</text>
|
||||
<line x1="705" y1="212" x2="705" y2="226" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ca)"/>
|
||||
<rect x="470" y="228" width="470" height="58" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.4"/>
|
||||
<text x="486" y="250" font-size="12.5" font-weight="bold" fill="#153a6e">第 3 级 · 胶囊对:线段-线段距离(Ericson 标准式)</text>
|
||||
<text x="486" y="270" font-size="11.5" fill="#2a4a6e">d(轴_a, 轴_b) < r_i + r_j + margin(0.3mm) ⇒ 干涉;无 visit 回调时首对即短路</text>
|
||||
|
||||
<!-- 胶囊距小图 -->
|
||||
<g transform="translate(490,306)">
|
||||
<rect x="0" y="18" width="120" height="34" rx="17" fill="#cfe3f5" stroke="#3572c9" stroke-width="1.4"/>
|
||||
<line x1="17" y1="35" x2="103" y2="35" stroke="#3572c9" stroke-width="1.2" stroke-dasharray="4 3"/>
|
||||
<rect x="210" y="0" width="110" height="30" rx="15" fill="#f2d5a8" stroke="#c98a18" stroke-width="1.4"/>
|
||||
<line x1="225" y1="15" x2="305" y2="15" stroke="#c98a18" stroke-width="1.2" stroke-dasharray="4 3"/>
|
||||
<line x1="110" y1="30" x2="212" y2="18" stroke="#c0533a" stroke-width="1.8" marker-start="url(#ca)" marker-end="url(#ca)"/>
|
||||
<text x="128" y="12" font-size="12" font-weight="bold" fill="#c0533a">d(轴间距)</text>
|
||||
<text x="120" y="86" font-size="12" fill="#16324f" font-family="Consolas,monospace">自由 ⇔ d ≥ r_i + r_j + margin</text>
|
||||
<text x="120" y="106" font-size="11" fill="#5c7185">胶囊并集 ⊇ 零件本体 ⇒ 「自由」可信、「干涉」需离线布尔确认</text>
|
||||
</g>
|
||||
|
||||
<!-- 右下:规则 -->
|
||||
<rect x="470" y="428" width="470" height="128" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="486" y="452" font-size="12.5" font-weight="bold" fill="#7a5a08">三条关键规则</text>
|
||||
<text x="486" y="474" font-size="12" fill="#6b5312">· 只检 |Δg|≥2 的零件对(同组相对静止、零位已保证不干涉);</text>
|
||||
<text x="486" y="492" font-size="12" fill="#6b5312"> 相邻对 P04×P05/P06×P07/P07×P08 同心跨轴,塞进模型零位必误报</text>
|
||||
<text x="486" y="510" font-size="12" fill="#6b5312"> ⇒ 改用 adj 一维自由区间表(CAD 实测、与姿态无关)</text>
|
||||
<text x="486" y="528" font-size="12" fill="#6b5312">· adj:J2 [−107°,+128°] · J4 [−112°,+112°] · J5 [−96°,+96°]</text>
|
||||
<text x="486" y="546" font-size="12" fill="#6b5312">· 硬边(lo 侧相邻件自碰撞)静态值即真值;可放宽侧(撞立柱)按姿态实时二分</text>
|
||||
|
||||
<rect x="470" y="566" width="470" height="58" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.3"/>
|
||||
<text x="486" y="588" font-size="12.5" font-weight="bold" fill="#14472c">性能:1995 胶囊 · collideFree ≈ 5ms/次(零位最坏)</text>
|
||||
<text x="486" y="608" font-size="11.5" fill="#2c5c44">预分配缓冲 + 代际戳缓存(免 GC)· 轴对齐特化旋转 · 热循环标量直传免建数组</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 560" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="560" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="da" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
|
||||
</marker>
|
||||
<marker id="dor" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">动态限位:静态值只是零位快照,真边界由碰撞模型按姿态二分</text>
|
||||
|
||||
<!-- 数轴(J5 例) -->
|
||||
<text x="490" y="70" text-anchor="middle" font-size="14" font-weight="bold" fill="#1f4f8f">以 J5 腕俯仰 + 向为例(数值取自实机数据)</text>
|
||||
<line x1="120" y1="200" x2="880" y2="200" stroke="#3d5164" stroke-width="2.5" marker-start="url(#da)" marker-end="url(#da)"/>
|
||||
<g font-size="11.5" fill="#5c7185" text-anchor="middle">
|
||||
<text x="120" y="222">−360°</text><text x="500" y="222">0°</text><text x="880" y="222">+360°(电机限位)</text>
|
||||
<line x1="500" y1="194" x2="500" y2="206" stroke="#3d5164" stroke-width="1.5"/>
|
||||
<line x1="120" y1="194" x2="120" y2="206" stroke="#8a9aa8" stroke-width="1.5"/>
|
||||
<line x1="880" y1="194" x2="880" y2="206" stroke="#8a9aa8" stroke-width="1.5"/>
|
||||
</g>
|
||||
<!-- adj 机械区间 -->
|
||||
<rect x="399" y="188" width="202" height="24" rx="4" fill="#dbe9fb" opacity="0.85" stroke="#3572c9" stroke-width="1.2"/>
|
||||
<text x="500" y="136" text-anchor="middle" font-size="12" fill="#2a4a6e">adj 机械区间(相邻件 CAD 实测):J5 = [−96°, +96°]</text>
|
||||
<!-- 静态扫描限位 -->
|
||||
<line x1="536" y1="180" x2="536" y2="220" stroke="#1f9d63" stroke-width="3"/>
|
||||
<text x="536" y="250" text-anchor="middle" font-size="12" fill="#1f7a4d">零位静态扫描 +34°(撞立柱 P01_A)</text>
|
||||
<!-- 动态边界 -->
|
||||
<line x1="601" y1="180" x2="601" y2="220" stroke="#e08a1e" stroke-width="3"/>
|
||||
<text x="672" y="272" text-anchor="middle" font-size="12" font-weight="bold" fill="#b06a10">当前姿态动态边界 +96°</text>
|
||||
<!-- 当前角 -->
|
||||
<path d="M 521 194 l 6 12 h -12 z" fill="#16324f"/>
|
||||
<text x="489" y="172" text-anchor="end" font-size="12" font-weight="bold" fill="#16324f">当前 θ</text>
|
||||
<!-- 二分括号 -->
|
||||
<g stroke="#e08a1e" stroke-width="1.4" fill="none" stroke-dasharray="5 4">
|
||||
<path d="M 521 216 C 521 230, 601 230, 601 224" marker-end="url(#dor)"/>
|
||||
<path d="M 561 216 C 561 227, 581 227, 581 224" marker-end="url(#dor)"/>
|
||||
</g>
|
||||
<text x="490" y="292" text-anchor="middle" font-size="12" fill="#b06a10">dynLimit:从当前角向限位方向二分「collideFree」翻转点(tol 0.25°)</text>
|
||||
|
||||
<rect x="120" y="304" width="760" height="58" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="490" y="326" text-anchor="middle" font-size="12.5" fill="#3d5164">为什么静态限位不可信:它来自「其它轴全零」的一次性扫描。实测 J2 正向静态 +9.75°(垂臂零位撞立柱),</text>
|
||||
<text x="490" y="346" text-anchor="middle" font-size="12.5" fill="#3d5164">J1=+90° 时动态边界放到 +108.75° —— 撞立柱这类「跨 ≥2 组」的约束必须按姿态重算,滑块才不锁死。</text>
|
||||
|
||||
<!-- solveDyn 流程 -->
|
||||
<text x="490" y="394" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">solveDyn:静态解 → 碰撞校验 → 贴边轴放宽重解(最多 2 轮)</text>
|
||||
<g font-size="11.5">
|
||||
<rect x="70" y="408" width="150" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<text x="145" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">静态限位求解</text>
|
||||
<text x="145" y="447" text-anchor="middle" fill="#2a4a6e">solveTo(全流水线)</text>
|
||||
<line x1="220" y1="433" x2="240" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
|
||||
<rect x="242" y="408" width="150" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<text x="317" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">全姿态碰撞校验</text>
|
||||
<text x="317" y="447" text-anchor="middle" fill="#2a4a6e">不安全→候选表选安全解</text>
|
||||
<line x1="392" y1="433" x2="412" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
|
||||
<rect x="414" y="408" width="160" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
|
||||
<text x="494" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">贴边轴检测(0.5°内)</text>
|
||||
<text x="494" y="447" text-anchor="middle" fill="#2a4a6e">非连续轴 · 非硬边</text>
|
||||
<line x1="574" y1="433" x2="594" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
|
||||
<rect x="596" y="408" width="150" height="50" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="671" y="429" text-anchor="middle" font-weight="bold" fill="#7a5a08">dynLimit 二分边界</text>
|
||||
<text x="671" y="447" text-anchor="middle" fill="#6b5312">放宽 ≥5° 才值得重解</text>
|
||||
<line x1="746" y1="433" x2="766" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
|
||||
<rect x="768" y="408" width="150" height="50" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.3"/>
|
||||
<text x="843" y="429" text-anchor="middle" font-weight="bold" fill="#14472c">放宽后重解(粗档)</text>
|
||||
<text x="843" y="447" text-anchor="middle" fill="#2c5c44">cost 不升才采用</text>
|
||||
</g>
|
||||
|
||||
<rect x="120" y="482" width="760" height="62" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="490" y="506" text-anchor="middle" font-size="12.5" fill="#6b5312">滑块夹取 clampAxis:缓存 [轴, 其余轴 1° 量化姿态] → {lo, hi};拖同一轴直接取缓存,</text>
|
||||
<text x="490" y="526" text-anchor="middle" font-size="12.5" fill="#6b5312">拖过边界才重算一次二分(~40ms)。当前姿已干涉时退回机械区间 —— 否则 dynLimit 会把界外值当边界,滑块再也回不来。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 440" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
|
||||
<rect x="0" y="0" width="980" height="440" fill="#ffffff"/>
|
||||
<defs>
|
||||
<marker id="zb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
|
||||
</marker>
|
||||
<marker id="zo" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">工具姿态参数化:方位角 az · 仰角 el · 滚转 roll</text>
|
||||
|
||||
<!-- 左:球面示意 -->
|
||||
<circle cx="240" cy="220" r="130" fill="#f4f8fb" stroke="#b9c8d6" stroke-width="1.4"/>
|
||||
<ellipse cx="240" cy="220" rx="130" ry="40" fill="none" stroke="#b9c8d6" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<line x1="240" y1="220" x2="240" y2="70" stroke="#3572c9" stroke-width="2" marker-end="url(#zb)"/>
|
||||
<text x="252" y="80" font-size="12.5" fill="#2a4a6e">CAD +Z</text>
|
||||
<line x1="240" y1="220" x2="400" y2="220" stroke="#8a9aa8" stroke-width="1.4"/>
|
||||
<text x="406" y="224" font-size="12" fill="#5c7185">方位基线</text>
|
||||
<!-- 工具轴 -->
|
||||
<line x1="240" y1="220" x2="296" y2="96" stroke="#e08a1e" stroke-width="2.6" marker-end="url(#zo)"/>
|
||||
<text x="306" y="94" font-size="12.5" font-weight="bold" fill="#b06a10">工具轴</text>
|
||||
<text x="306" y="112" font-size="11.5" fill="#b06a10">(法兰 +Z 外伸)</text>
|
||||
<!-- el 弧 -->
|
||||
<path d="M 240 128 A 92 92 0 0 1 277 136" fill="none" stroke="#7c5cd6" stroke-width="1.7"/>
|
||||
<text x="92" y="140" font-size="12.5" fill="#7c5cd6">el(仰角,自 +Z 抬起)</text>
|
||||
<text x="92" y="158" font-size="12.5" fill="#7c5cd6">0…180°</text>
|
||||
<line x1="216" y1="136" x2="258" y2="131" stroke="#7c5cd6" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<!-- roll 环绕工具轴 -->
|
||||
<ellipse cx="270" cy="153" rx="20" ry="9" fill="none" stroke="#d64545" stroke-width="1.7" transform="rotate(-24 270 153)"/>
|
||||
<text x="92" y="196" font-size="12.5" fill="#b03030">roll(绕轴滚转)</text>
|
||||
<text x="92" y="214" font-size="12.5" fill="#b03030">= θ6,绕工具轴</text>
|
||||
<line x1="212" y1="198" x2="256" y2="160" stroke="#d64545" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<!-- az 弧(水平面内) -->
|
||||
<path d="M 362 220 A 122 38 0 0 0 330 182" fill="none" stroke="#1f9d63" stroke-width="1.7"/>
|
||||
<text x="320" y="262" font-size="12.5" fill="#1f7a4d">az(绕 Z 方位角,−180…180°)</text>
|
||||
<circle cx="240" cy="220" r="4" fill="#16324f"/>
|
||||
<text x="160" y="248" font-size="12" fill="#5c7185">法兰中心</text>
|
||||
|
||||
<!-- 右:公式 -->
|
||||
<rect x="470" y="70" width="470" height="180" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
|
||||
<text x="488" y="96" font-size="13" font-weight="bold" fill="#16324f">poseMat(az, el, roll) → 工具系旋转矩阵(列 = x/y/z)</text>
|
||||
<text x="488" y="122" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">z = (sin el·cos az, sin el·sin az, cos el)</text>
|
||||
<text x="488" y="146" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">x0 = (−sin az, cos az, 0) (子午面基,恒⊥z)</text>
|
||||
<text x="488" y="170" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">x = R(z, roll)·x0 y = z × x</text>
|
||||
<text x="488" y="200" font-size="11.5" fill="#5c7185">poseAng() 为其逆分解;el ∈ [0,180],az/roll ∈ (−180,180]。</text>
|
||||
<text x="488" y="220" font-size="11.5" fill="#5c7185">界面以「工具朝外」描述;求解器内部法兰法向朝臂内,</text>
|
||||
<text x="488" y="236" font-size="11.5" fill="#5c7185">两者相差绕工具 x 轴 180°(RX180),入栈/出栈各翻一次。</text>
|
||||
|
||||
<rect x="470" y="266" width="470" height="140" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
|
||||
<text x="488" y="292" font-size="13" font-weight="bold" fill="#7a5a08">为什么姿态任务只剩 2 个未知数</text>
|
||||
<text x="488" y="316" font-size="12" fill="#6b5312">· θ6 只提供绕工具轴的滚转,不改变工具轴 ⇒ 工具轴 = (R1…R5)·ẑ</text>
|
||||
<text x="488" y="338" font-size="12" fill="#6b5312">· 滚转由 rollClose 闭式补出:θ6 = −atan2(N[1,0], N[0,0]),N = R5ᵀ·Rtar</text>
|
||||
<text x="488" y="360" font-size="12" fill="#6b5312">· 推论:全姿态偏差 oerr ≥ 工具轴夹角(20 万随机样本验证零违反)——</text>
|
||||
<text x="488" y="380" font-size="12" fill="#6b5312"> 粗扫用一次点积预筛(1−cos2°)挡掉必被拒的候选,省掉矩阵链+acos</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 650 KiB |
@@ -6,25 +6,36 @@
|
||||
|
||||
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(Modifier) |
|
||||
|---|---|---|---|---|
|
||||
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
||||
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 顶端榫接区 + Ø3.9 对锁孔周 |
|
||||
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + Ø3.9 对锁孔周 |
|
||||
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 底端榫接区 + Ø3.9 对锁孔周 + 顶面安装孔周 |
|
||||
| P02_Center_底座中核心_螺旋体30%_墙5底6_平打 | 螺旋体 30% | 5 / 6 | 平打 | Ø48 套筒 + 12mm 阶梯榫座 |
|
||||
| P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
||||
| P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
||||
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 10mm 后法兰孔 + J1 电机座接触面 |
|
||||
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 5x Ø3.9 安装孔周 + 后法兰 4xM3 沉头孔周 + 前锁紧法兰孔周 |
|
||||
| P04_肩叉_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | J1 输出凸台贯通区(整段实心) |
|
||||
| P05_大臂旋转座_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | J2 接口孔周 |
|
||||
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
|
||||
| P07_小臂骨架_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | 根部加厚顶板区 + M5 电机半包座 |
|
||||
| P08_腕偏转座_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | J5 接口孔周 |
|
||||
| P09_工具法兰_螺旋体100%_墙4底5_竖打 | 螺旋体 100% | 4 / 5 | 竖打 | — |
|
||||
| P09_工具法兰_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | — |
|
||||
|
||||
## 通用规则
|
||||
|
||||
1. **打印方向**:全部按模型姿态打,弯曲发生在层面内,弯矩交给顶/底实心层和墙,不让层间缝扛拉。
|
||||
2. **螺纹孔一律不切螺纹**:M3/M4/M5 全部用黄铜热熔螺母;孔周按 1.5~2 倍螺栓直径加载实心 Modifier。P02 力臂的 M5 螺母沉槽同此处理。
|
||||
2. **不用热熔铜螺母、不需要攻丝、也不打沉孔/过孔**:全部连接螺钉统一为 **M4×16**,每个螺钉位**只打一个 Ø3.9 直孔**——M4×16 直接用扳手拧入,螺钉自己攻出螺纹(顶部 4 颗穿鞍座 9mm 后吃进立柱顶环 7mm;横向 1 颗穿 11mm 后壁吃进榫头 4.7mm;分段缝 8 颗穿母口壁+公榫壁共 10.5mm)。竖向载荷由承压面承担(顶部 2403mm²、分段缝榫座面),螺钉只防上抬/防滑。孔周按 1.5~2 倍螺栓直径加载实心 Modifier。
|
||||
3. **墙数和顶底壳优先于密度**:抗弯主力是墙+顶底实心层,填充件是剪力腹板。密度只在小区域加(关节毂 30~35%),不要全局堆高。
|
||||
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
|
||||
P03 已改薄壁桁架(双侧 6mm 竖板 + 帐篷减重窗 + 9mm 鞍座 + 12mm 中位龙骨筋):强度由 **墙层数** 决定,
|
||||
35% 只作用在很薄的内部芯上;想再压重量可把 P03 单独降到 螺旋体 20%(≈省 15~20g,强度不受影响)。
|
||||
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
|
||||
6. 立柱 P01 三段用三角形是唯一例外:轴压方向已知,Z 向斜撑跨层传力比螺旋体更对口。
|
||||
|
||||
## 3MF 交付(out/parts_3mf/)
|
||||
|
||||
- 13 个 3MF 已按本表参数从**当前模型**(`out/print_stl/`,随 `python src/arm_model.py` 更新)导出。P01_A 也一并重导:你 09-28 手作的原件网格比现模型少 780 个三角(立柱下段后续改过),已备份到 `.bak/P01_A_手作参考原件_20260928.3mf`。网格按模型姿态、板中心放置,缩略图已剔除避免旧预览误导。
|
||||
- 打印机/耗材等基准 profile(537 键)从你的原件抽出后固化在 `docs/bambu_profile_reference.json`,生成脚本读它,不再依赖 `out/` 里的二进制。
|
||||
- 生成的 3MF 严格按本表:注意你的参考文件 `layer_height=0.28`、顶壳 4 层,本表要求 **0.20 / 顶 5**,新文件已按参数单执行。
|
||||
- **局部实心区(Modifier)未烘进 3MF**:Bambu modifier schema 已确认可用,但各区几何需逐件定义;在 Bambu 里对对象右键→添加修饰器→覆盖 `sparse_infill_density=100%` 即可(区位见上表)。需要我烘进去说一声。
|
||||
- 重新生成:`python src/arm_model.py`(刷新 out/print_stl)→ `python scratch/make_3mf.py`;闭环校验:`python scratch/verify_3mf.py`(逐项断言本表参数 + 件高)。
|
||||
|
||||
|
||||
@@ -1,102 +1,122 @@
|
||||
[
|
||||
{
|
||||
"id": "S01",
|
||||
"规格": "M2 x 10",
|
||||
"螺纹": "M2",
|
||||
"长度mm": 10,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 18,
|
||||
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
|
||||
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
|
||||
},
|
||||
{
|
||||
"id": "S02",
|
||||
"规格": "M3 x 10",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 10,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
|
||||
},
|
||||
{
|
||||
"id": "S03",
|
||||
"规格": "M3 x 16",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 16,
|
||||
"头型": "沉头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
|
||||
},
|
||||
{
|
||||
"id": "S04",
|
||||
"规格": "M3 x 8",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"头型": "沉头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 前置止脱锁紧法兰 -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°)"
|
||||
},
|
||||
{
|
||||
"id": "S05",
|
||||
"规格": "M4 x 25",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 25,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗",
|
||||
"备注": "Ø4.4 通孔,非对称偏心孔位物理防反装;需另配 M4 螺母 4 枚"
|
||||
},
|
||||
{
|
||||
"id": "S06",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "沉头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
||||
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
|
||||
},
|
||||
{
|
||||
"id": "S07",
|
||||
"规格": "M5 x 25",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 25,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
|
||||
},
|
||||
{
|
||||
"id": "S08",
|
||||
"规格": "M5 x 25",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 25,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
|
||||
},
|
||||
{
|
||||
"id": "S09",
|
||||
"规格": "M5 x 35",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 35,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 肩吊臂安装台 -> P01_C 立柱顶面",
|
||||
"备注": "Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm"
|
||||
},
|
||||
{
|
||||
"id": "S10",
|
||||
"规格": "M6 x 35",
|
||||
"螺纹": "M6",
|
||||
"长度mm": 35,
|
||||
"头型": "外六角",
|
||||
"数量": 6,
|
||||
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
|
||||
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
|
||||
}
|
||||
]
|
||||
{
|
||||
"id": "S01",
|
||||
"规格": "M2 x 10",
|
||||
"螺纹": "M2",
|
||||
"长度mm": 10,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 18,
|
||||
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
|
||||
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
|
||||
},
|
||||
{
|
||||
"id": "S02",
|
||||
"规格": "M3 x 10",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 10,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),BCD 55;装配时配合平垫吸收电机公差,与前端压盖双向夹死"
|
||||
},
|
||||
{
|
||||
"id": "S03",
|
||||
"规格": "M3 x 16",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm),BCD 55,轴向锁死防拉脱"
|
||||
},
|
||||
{
|
||||
"id": "S04",
|
||||
"规格": "M3 x 8",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm),BCD 55,咬合电机前端面 3.5mm"
|
||||
},
|
||||
{
|
||||
"id": "S05",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 8,
|
||||
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
|
||||
"备注": "统一 Ø3.9 直孔自攻成型,非对称孔位防反装"
|
||||
},
|
||||
{
|
||||
"id": "S06",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
||||
"备注": "Ø4.4 通孔 + Ø7.0 圆柱沉孔(深 4mm,头平齐),BCD 44 均布 -> 末端工具/夹爪"
|
||||
},
|
||||
{
|
||||
"id": "S07",
|
||||
"规格": "M5 x 25",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 25,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装"
|
||||
},
|
||||
{
|
||||
"id": "S08",
|
||||
"规格": "M5 x 25",
|
||||
"螺纹": "M5",
|
||||
"长度mm": 25,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
|
||||
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
|
||||
},
|
||||
{
|
||||
"id": "S09",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
|
||||
"备注": "统一 Ø3.9 直孔自攻成型(从上方拧入),防上抬"
|
||||
},
|
||||
{
|
||||
"id": "S11",
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 1,
|
||||
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
|
||||
"备注": "Ø3.9 直孔贯穿 11mm 后壁 + 榫头,从后方自攻横向锁紧防脱"
|
||||
},
|
||||
{
|
||||
"id": "S10",
|
||||
"规格": "M6 x 35",
|
||||
"螺纹": "M6",
|
||||
"长度mm": 35,
|
||||
"头型": "外六角",
|
||||
"数量": 6,
|
||||
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
|
||||
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
|
||||
},
|
||||
{
|
||||
"id": "S12",
|
||||
"规格": "M3 x 8",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 前端压盖 -> M2/M3/M4/M5/M6 电机前端盖(J2/J3/J4/J5/J6 各 4 颗)",
|
||||
"备注": "Ø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 |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 184 KiB |