Files
Robot_Arm/arm/cad/part_tools.py
T
cbb 649bd69810 refactor(layout): src/ 收成顶层包 arm/,入口统一 python -m;提升承担构建的 scratch 脚本
单文件约束取消后,Python 侧没必要再靠 16 处 sys.path.insert 维持扁平模块互认。
现在按职责分包:
  arm/paths.py 路径唯一来源(含新增 CACHE、TPL)
  arm/constants.py 实测接口数值(原 armcore 的 M/C/CLR/FIT/MOTOR_TOL 段)
  arm/cad/{helpers,arm_build,part_tools,parts/} 几何与装配
  arm/kinematics.py(原 human_layout.py,唯一运动学事实源)
  arm/analysis/{sweep_v2,strength_check,torque_balance,finalize_travel,ws_sweep,gen_env_mesh}
  arm/dataio/{screw_mesh,draw_sheet}   arm/viewer/{build_viewer,shot}
  arm/build_model.py(原 arm_model.py)

顺带修掉的既有缺陷(都是实测撞出来的,不是预防性改动):
- arm_model 第二次 sys.path 跳到 gitignore 的 scratch/ 去 import 构建必需的包络脚本:
  ws_sweep/gen_env_mesh 提升进 arm/analysis/,缓存挪到 data/cache/(gitignore,可重生成)。
- finalize_travel / draw_sheet 顶层就是"算完写文件",一次 importlib 冒烟测试就把
  docs/travel.json 重写了一遍;现在被 import 直接抛错。
- strength_check 用 OUT.replace("/out","/web") 靠字符串形状推路径,改成直接用 WEB。
- build_viewer 里 WORKSPACE 使用点漏改导致 NameError —— 已由构建关卡抓到并修掉。

os._exit 的处置与计划里的假设相反:实测光是 `python -c "import arm.cad.helpers"`
就会在解释器退出时段错误(exit 139,OCC 静态析构),全流程跑完也一样。所以入口
脚本尾部保留 flush + os._exit,并把原因写进注释;被 import 时不生效。

关卡(tests/):
- verify.py 一条命令跑三层:产物 md5、IK 数值 exact 段、截图。
- 截图基线重做并分级:同一构建连跑两轮实测 102 张逐字节可复现(要求 md5 相同),
  3 张线束 x-ray 不可复现(mean<=0.0017、无像素差>8)改为与参考像素比容差;
  这 3 张不再入库,免得每次跑关卡都脏 git。
- 新增 ?view=sheet / ?sheet=N / ?theme / ?guide=N 四个 URL 参数:工程图与装配向导
  占了约一半 CSS 和 700 行 JS,原先没有任何截图覆盖,拆它们等于没有关卡。
- 关卡灵敏度已验:改坏 IK 基线一个数 -> FAIL;换旧版求解器 -> 5 处 exact 差异。

实测:python -m arm.build_model 退出码 0,全部产物 md5 与基线一致,
105 视图截图全过,IK exact 段逐字节一致。
2026-10-15 03:31:20 +08:00

547 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()