单文件约束取消后,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 段逐字节一致。
547 lines
31 KiB
Python
547 lines
31 KiB
Python
# -*- 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()
|