feat: P01/P02阶梯榫卯面承载+P03侧开式托架/P04加长避让重构,精简零件显示名并同步重建模型数据与查看器,清理历史文档

This commit is contained in:
cbb
2026-09-27 21:08:28 +08:00
parent b11814410a
commit bdcc27d506
179 changed files with 1598 additions and 14836 deletions
+15 -275
View File
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
"""60 mm gearmotor -> 5-DOF printable robot arm (J1 yaw, J2 shoulder, J3 elbow,
J4 wrist pitch, J5 gripper).
"""Shared geometry / assembly / export library for the robot arm build.
Joint recipe (measured, see docs/interfaces.md):
proximal link holds the motor in a Ø60.3 slip bore + 64.4 square PCBA pocket +
4 radial M3 pinch screws; motor rear datum + 65.0 = the driven link's hub mating
plane, whose face replicates the motor output (Ø36x1.0 neck, Ø6.5x4.0 centre,
3x M2 on Ø29) so the existing face-spline halves + split clamp bolt straight on.
No layout of its own: the joint frames and station positions live in
human_layout.py and arm_model.py, which import this module. What lives here is
the measured motor interface (docs/interfaces.md), the Joint class (motor slip
bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
+65.0 replicating the Ø36x1.0 neck / Ø6.5x4.0 centre / 3x M2 on Ø29 output so
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_build.py -> out/, web/mesh_arm.json, docs/
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
"""
import math
import os
@@ -50,30 +51,6 @@ HUB_M2_CLOCK = [(360.0 - a) % 360.0 for a in M["m2_clock"]] # phase of the mir
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
# ---------------------------------------------------------------- layout (ground X fwd, Y left, Z up)
PLATE_T = 9.0
ARM_L1, ARM_L2 = 175.0, 165.0
EL_A, FA_A = 58.0, 8.0
SH = (0.0, 125.0)
EL = (SH[0] + ARM_L1 * math.cos(EL_A * D2R), SH[1] + ARM_L1 * math.sin(EL_A * D2R))
WR = (EL[0] + ARM_L2 * math.cos(FA_A * D2R), EL[1] + ARM_L2 * math.sin(FA_A * D2R))
DIR_U = (math.cos(EL_A * D2R), math.sin(EL_A * D2R))
DIR_F = (math.cos(FA_A * D2R), math.sin(FA_A * D2R))
PERP_F = (-DIR_F[1], DIR_F[0]) # "up" in the arm plane for the wrist
Y0 = -26.0
Y_UP_IN = Y0 + HUB_FACE_Z
Y_UP_OUT = Y_UP_IN + PLATE_T
Y3_REAR = Y_UP_IN
Y_FA_OUT = Y3_REAR - HUB_FACE_Z - PLATE_T
Y_FA_IN = Y_FA_OUT + PLATE_T
Y4_REAR = Y_FA_IN
Y_WM_IN = Y4_REAR + HUB_FACE_Z
Y_WM_OUT = Y_WM_IN + PLATE_T
Y_M5, S0_M5 = -2.0, 58.0
BRACKET_BOLT = [(-26.0, Y0 - 3.0), (0.0, Y0 - 3.0), (26.0, Y0 - 3.0)] # turret <-> shoulder wall
COLUMN_BOLT = [(35.0 * math.cos(a * D2R), 35.0 * math.sin(a * D2R)) for a in (30, 150, 270)]
PARTS = []
CENTS = {}
@@ -287,7 +264,7 @@ class Joint:
cuts.append(cyl(M["m2_tap_d"] / 2.0, H - 0.05, H + M["m2_depth"], u, v))
return shape.cut(*[self.L(s) for s in cuts])
def hub_island(self, shape, r=30.0, t=PLATE_T + 0.5):
def hub_island(self, shape, r=30.0, t=9.5):
"""Solid disc at the joint centre (the lightening dish must not break into the
interface recesses), then the output replica is cut into it. Starts exactly on the
mating plane: half B's Ø54 body ends there, so any overlap is a hard clash."""
@@ -321,185 +298,10 @@ class Joint:
# ---------------------------------------------------------------- printed part builders
def p_base_plate():
s = cyl(76, 0, 8).cut(cyl(25, -0.01, 8.01))
for a in (45, 135, 225, 315):
u, v = 58 * math.cos(a * D2R), 58 * math.sin(a * D2R)
s = s.cut(cyl(2.75, -0.01, 8.01, u, v)).cut(cyl(5.5, -0.01, 2.8, u, v))
for a in range(0, 360, 45):
u, v = 68 * math.cos(a * D2R), 68 * math.sin(a * D2R)
s = s.cut(cyl(4.0, -0.01, 3.0, u, v))
return s
def p_column(j1):
"""Ø88 column z 8..64 holding M1; open at the bottom so it slides over the motor."""
s = cyl(44, 8, 64)
for a in (0, 90, 180, 270):
blk = box(21, 45, -12, 12, 22, 50)
blk = place(blk, R(a, 0, 0, 1))
s = s.cut(blk)
s = j1.pocket(s, front_pad=0.0)
s = j1.pinch(s, n=2)
# J1 hub must be able to pull up: top face keeps a Ø61 recess ring for the cover
s = s.cut(ring(30.6, 36.0, 63.4, 64.01))
# cable duct from the base to the turret
duct = box(-9, 9, -46, -30, 20, 64)
s = s.cut(duct)
return s
def p_turret(j1):
"""J1 driven hub: mating face at z=73, plate 73..82, carries the shoulder bracket."""
face = j1.hub_face()
zf = 73.0
s = cyl(52, zf, zf + PLATE_T)
s = s.cut(ring(38.5, 52.0, zf - 0.01, zf + 2.2)) # dish the underside for the clamp
for a in range(30, 360, 60): # lightening (off the bolt lines)
u, v = 40 * math.cos(a * D2R), 40 * math.sin(a * D2R)
s = s.cut(cyl(7.0, zf + 2.4, zf + PLATE_T - 2.4, u, v))
s = j1.hub_island(s, 30.0, PLATE_T - 0.02)
for (u, v) in COLUMN_BOLT: # 3x M3 down into the column
s = s.cut(cyl(1.6, zf - 0.01, zf + PLATE_T + 0.01, u, v))
for (u, v) in BRACKET_BOLT: # 3x M3 up for the shoulder wall
s = s.cut(cyl(1.55, zf + PLATE_T - 6.0, zf + PLATE_T + 0.01, u, v))
return s
def p_shoulder_bracket(j2):
"""Wall on the turret top + motor sleeve; holds M2 (axis +Y at SH)."""
zt = 82.0
y0, y1 = Y0 - 6.0, Y0
s = Ybox(SH[0] - 40, SH[0] + 40, y0, y1, zt + 0.02, SH[1] + 39.0)
s = s.fuse(Ycyl(38.0, Y0, Y0 + COVER_Z - 3.0, SH[0], SH[1]))
s = s.fuse(Ybox(SH[0] - 38, SH[0] + 38, Y0, Y0 + 6.0, SH[1] - 20, SH[1] + 20))
for sx in (-1, 1): # feet down onto the turret
s = s.fuse(Ybox(sx * 26 - 6, sx * 26 + 6, y0, y1, zt + 0.02, zt + 30.0))
s = j2.pocket(s) # relieve AFTER every fuse
s = j2.pinch(s, n=2)
for (bx_, by_) in BRACKET_BOLT: # 3x M3 down into the turret
s = s.cut(cyl(1.7, zt + 0.01, zt + 8.0, bx_, by_))
return s
def p_upper_arm(j2, j3):
"""Plate normal Y, y 39..48. J2 hub at the shoulder end; holds M3 at the elbow (axis -Y,
so its boss reaches toward -Y from the plate)."""
pts = dogbone(EL, SH, 41.0, 41.0)
s = Yplate(pts, Y_UP_IN, Y_UP_OUT)
s = s.fuse(Ycyl(38.0, Y3_REAR - 22.0, Y_UP_OUT, EL[0], EL[1])) # elbow motor boss
inner = dogbone(EL, SH, 41.0 - 10.0, 41.0 - 10.0)
s = s.cut(Yplate(inner, Y_UP_IN + 2.6, Y_UP_OUT - 2.6)) # lightening dish
for k in range(1, 4): # web ribs in the dish
t = k / 4.0
cx = SH[0] + (EL[0] - SH[0]) * t
cz = SH[1] + (EL[1] - SH[1]) * t
s = s.fuse(Ybox(cx - 3.5, cx + 3.5, Y_UP_IN + 2.6, Y_UP_OUT - 2.6, cz - 20, cz + 20))
s = j2.hub_island(s, 30.0, PLATE_T + 0.5)
s = j3.pocket(s)
s = j3.pinch(s, n=2)
return s
def p_forearm(j3, j4):
"""Plate normal Y, y -35..-26. J3 hub at the elbow end; holds M4 at the wrist (axis +Y,
boss reaches toward +Y)."""
pts = dogbone(WR, EL, 40.0, 40.0)
s = Yplate(pts, Y_FA_OUT, Y_FA_IN)
s = s.fuse(Ycyl(37.0, Y_FA_OUT, Y4_REAR + 22.0, WR[0], WR[1])) # wrist motor boss
inner = dogbone(WR, EL, 40.0 - 10.0, 40.0 - 10.0)
s = s.cut(Yplate(inner, Y_FA_OUT + 2.6, Y_FA_IN - 2.6))
for k in range(1, 3):
t = k / 3.0
cx = EL[0] + (WR[0] - EL[0]) * t
cz = EL[1] + (WR[1] - EL[1]) * t
s = s.fuse(Ybox(cx - 3.5, cx + 3.5, Y_FA_OUT + 2.6, Y_FA_IN - 2.6, cz - 16, cz + 16))
s = j3.hub_island(s, 30.0, PLATE_T + 0.5)
s = j4.pocket(s)
s = j4.pinch(s, n=2)
return s
def p_wrist_plate(j4):
"""J4 driven hub + round palm disc with the joint shrouds; bolts to the gripper housing."""
zf = Y_WM_IN
s = Ycyl(41.0, zf, zf + PLATE_T, WR[0], WR[1])
s = j4.hub_island(s, 30.0, PLATE_T + 0.5)
for a in range(0, 360, 60): # lightening, clear of the hub island
u, v = 34 * math.cos(a * D2R), 34 * math.sin(a * D2R)
if math.hypot(u, v) > 31.0:
s = s.cut(Ycyl(6.5, zf + 2.6, zf + PLATE_T - 2.6, WR[0] + u, WR[1] + v))
for a in (0, 120, 240): # 3x M3 forward into the housing
u, v = 30 * math.cos(a * D2R), 30 * math.sin(a * D2R)
s = s.cut(Ycyl(1.7, zf + PLATE_T - 6.0, zf + PLATE_T + 0.01, WR[0] + u, WR[1] + v))
return s
def p_shell(j4, j5):
"""Tapered gripper housing: holds M5, carries the finger guides and the crank window."""
zf = Y_WM_IN + PLATE_T
rear = j5.L(bx(-41, 41, -38, 38, -14.0, 30.0))
fore = j5.L(bx(-41, 41, -38, 38, 30.0, 62.0))
body = rear.fuse(fore)
# cavity must stay inside both sections: it was ±35.5/±29.5 against a ±34/±26 shell,
# which ate the side walls and left the guide blocks as three disconnected solids
cav = j5.L(bx(-32.6, 32.6, -32.6, 32.6, -9.0, 54.0))
s = body.cut(cav)
# mounting flange overlapping the shell by 6 mm: a thin 0.1 mm land would fuse into two
# disconnected solids, which would print as two pieces
s = s.fuse(j5.L(bx(-41, 41, -35, 35, -16.0, -8.0)))
s = j5.pocket(s)
s = j5.pinch(s, n=2)
s = s.cut(j5.L(cyl(22.5, 53.9, 62.01))) # crank pocket in the front wall
for sd in (-1, 1):
for sy in (-1, 1): # y-guides, rooted in the side wall
s = s.fuse(j5.L(bx(33.0 * sd, 41.0 * sd, 11.6 * sy, 14.2 * sy, 55.0, 66.0)))
s = s.cut(j5.L(bx(10.0 * sd, 30.0 * sd, -12.5, 12.5, 53.9, 66.01))) # cam clearance
s = s.cut(j5.L(bx(29.5 * sd, 40.5 * sd, -12.6, 12.6, 44.0, 66.01))) # finger arm exit
for a in (0, 120, 240): # M3 pilots back into the palm
u, v = 30 * math.cos(a * D2R), 30 * math.sin(a * D2R)
s = s.cut(j5.L(cyl(1.55, -16.0, -8.0, u, v)))
return s
def bx(x0, x1, y0, y1, z0, z1):
return box(min(x0, x1), max(x0, x1), min(y0, y1), max(y0, y1), min(z0, z1), max(z0, z1))
def p_finger(j5, side=+1):
"""Prismatic finger: arm riding the y-guides, 45 deg cam ramp on the inner face, inward pad.
The crank's two opposed pins bear on the two ramps and push both jaws open together."""
u = side
arm = bx(30.0 * u, 40.0 * u, -10.5, 10.5, 50.0, 140.0)
arm = arm.fuse(bx(20.0 * u, 30.0 * u, -10.0, 10.0, 60.0, 78.0)) # cam heel block
ramp = bx(-9, 9, -12, 12, 58.0, 80.0)
ramp = place(ramp, R(45 if u > 0 else -45, 0, 0, 1))
ramp = place(ramp, T(17.0 * u, 0, 0))
arm = arm.cut(ramp)
foot = bx(8.0 * u, 30.0 * u, -12.0, 12.0, 96.0, 124.0)
for k in range(3): # 2 mm grooves cut INTO the contact face
foot = foot.cut(bx(6.0 * u, 8.2 * u, -10.5 + 7.0 * k, -7.5 + 7.0 * k, 98.0, 122.0))
arm = arm.fuse(foot).fuse(bx(30.0 * u, 37.0 * u, -8.0, 8.0, 140.0, 147.0))
return j5.L(arm)
def p_crank(j5):
"""Printed disc bolted onto the M5 output: spigot + pilot + disc + 2 opposed pins at R14."""
fz = FLANGE_Z
s = j5.L(cyl(3.15, fz - 2.7, fz + 0.5)) # into the Ø6.5 bore
s = s.fuse(j5.L(cyl(17.8, fz + 0.06, COVER_Z))) # into the Ø36 neck
s = s.fuse(j5.L(cyl(20.0, COVER_Z, 62.0))) # disc
for a in (0.0, 180.0):
u, v = 14.0 * math.cos(a * D2R), 14.0 * math.sin(a * D2R)
s = s.fuse(j5.L(cyl(2.9, 62.0, 78.0, u, v)))
s = s.fuse(j5.L(cyl(4.6, 62.0, 64.4, u, v)))
for a in M["m2_clock"]: # M2 into the motor
u, v = M["m2_bcd"] / 2.0 * math.cos(a * D2R), M["m2_bcd"] / 2.0 * math.sin(a * D2R)
s = s.cut(j5.L(cyl(M["m2_clear_d"] / 2.0, fz - 0.01, 59.2, u, v)))
s = s.cut(j5.L(cyl(M["m2_cb_d"] / 2.0, 59.2, 62.01, u, v)))
return s
GRP = { # part -> the joint group that carries it (kinematic tree)
"P01_底座板": "root", "P02_立柱": "root", "M1_J1": "root",
"J1_半联轴器A": "root", "J1_抱箍A": "root", "J1_抱箍B": "root",
@@ -533,48 +335,6 @@ def explode_vec(p, JS, cents, gmean):
# ---------------------------------------------------------------- assembly
def build():
global MOTOR, HALF, CLAMP
MOTOR = cq.importers.importStep(SRC_STEP).val()
MOTOR = place(MOTOR, T(0, 0, -2.57))
motor = MOTOR
half = HALF = solids_of(cq.importers.importStep(HALF_STEP).val())[0]
clamp = CLAMP = solids_of(cq.importers.importStep(CLAMP_STEP).val())[0]
j1 = Joint("J1", "底座回转", (0, 0, 8.0), (0, 0, 1), (1, 0, 0), lug_phase=45)
j2 = Joint("J2", "肩俯仰", (SH[0], Y0, SH[1]), (0, 1, 0), (1, 0, 0), lug_phase=90)
j3 = Joint("J3", "肘俯仰", (EL[0], Y3_REAR, EL[1]), (0, -1, 0), (1, 0, 0), lug_phase=90)
j4 = Joint("J4", "腕俯仰", (WR[0], Y4_REAR, WR[1]), (0, 1, 0), (1, 0, 0), lug_phase=90)
j5 = Joint("J5", "夹爪", (WR[0] + S0_M5 * DIR_F[0], Y_M5, WR[1] + S0_M5 * DIR_F[1]),
(DIR_F[0], 0, DIR_F[1]), (0, 1, 0), lug_phase=0)
JS = dict(J1=j1, J2=j2, J3=j3, J4=j4, J5=j5)
add("P01_底座板", "底座安装板 Ø152x8", p_base_plate(), 0x4b5563, note="4x M5 地脚,中心 Ø50 走线")
add("P02_立柱", "立柱,装 M1", p_column(j1), 0x4b5563, note="Ø60.3 滑配 + 64.4 方 Pocket + 2x M3 顶丝")
add("P03_转塔", "J1 从动盘 + 转塔", p_turret(j1), 0x2563eb, note="底面复刻电机输出:Ø36x1.0 / Ø6.5x4 / 3xM2@Ø29")
add("P04_肩支架", "肩支架,装 M2", p_shoulder_bracket(j2), 0x4b5563, note="立式抱紧 M2,底面 3x M3 接转塔")
add("P05_大臂", "大臂板 y39..48", p_upper_arm(j2, j3), 0x059669, note="根部和肘部各一次接口;肘部装 M3")
add("P06_小臂", "小臂板 y-35..-26", p_forearm(j3, j4), 0x059669, note="肘部从动盘 + 腕部装 M4")
add("P07_腕板", "J4 从动盘 + 掌盘", p_wrist_plate(j4), 0xd97706,
note="含关节包壳弧段;正面 3x M3 接爪壳")
add("P11_爪壳", "夹爪壳体,装 M5", p_shell(j4, j5), 0xb45309,
note="锥形壳 + 两根指导轨 + 曲柄过孔;后端 3x M3 锁在掌盘上")
add("P08_指A", "夹爪指 A", p_finger(j5, +1), 0xdc2626, note="45° 斜槽 + 导轨舌")
add("P09_指B", "夹爪指 B(与 A 镜像)", p_finger(j5, -1), 0xdc2626, note="打印 2 件时镜像即可")
add("P10_曲柄", "夹爪曲柄盘", p_crank(j5), 0x7c3aed, note="双销 R14,直接锁在 M5 输出上")
for k, (jid, cn) in enumerate([("J1", "底座回转"), ("J2", "肩俯仰"), ("J3", "肘俯仰"),
("J4", "腕俯仰"), ("J5", "夹爪电机")]):
j = JS[jid]
add("M%d_%s" % (k + 1, jid), "%s 电机模组" % jid, place(motor, j.t), 0x9ca3af,
printed=False, note="复用 装配体2.STEP:Ø60 机座 + 两级 0.5M 行星减速")
if jid != "J5":
for nm, sh in j.place_hardware(half, clamp, jid):
add(nm, "%s 端面齿联轴器/抱箍" % jid, sh, 0xeab308, printed=False,
note="沿用 配件/ 现有 v9 零件,打印件")
return JS
def write_step(items, path):
w = STEPControl_Writer()
for nm, sh in items:
@@ -664,8 +424,11 @@ def export(JS):
joints = [dict(id=k, cn=JS[k].cn, rear=JS[k].rear, d=JS[k].d, xdir=JS[k].xdir, hub=JS[k].hub_face())
for k in JS]
json.dump(dict(parts=parts, joints=joints, layout=dict(SH=SH, EL=EL, WR=WR,
L1=ARM_L1, L2=ARM_L2, ver=VERSION, it=ITER),
d3 = lambda a, b: math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
json.dump(dict(parts=parts, joints=joints,
layout=dict(ver=VERSION, it=ITER,
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)
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
@@ -777,26 +540,3 @@ def verify_interfaces(half, clamp, JS):
spigot_proud=round(b[1] - HUB_FACE_Z, 3),
clamp_clear=round(c[0] - COVER_Z, 3)))
return rows
if __name__ == "__main__":
JS = build()
vi = verify_interfaces(HALF, CLAMP, JS)
bom = export(JS)
cl = collide()
with open(DOCS + "/collision.txt", "w", encoding="utf-8") as fh:
fh.write("干涉对(体积 mm3,>2 才算数)\n")
for r in cl:
fh.write(" %-16s x %-16s %10.1f %s\n" % r)
fh.write("total pairs: %d\n" % len(cl))
print("parts %d printed %d" % (len(PARTS), sum(1 for p in PARTS if p["printed"])))
for p in PARTS:
print(" %-22s %9.1f cm3 %s" % (p["name"], p["vol"] / 1000.0, p["bb"]))
print("JOINT STACK (hub_face=%.3f cover=%.3f)" % (HUB_FACE_Z, COVER_Z))
for r in vi:
print(" %-3s halfA=%s halfB=%s clamp=%s B销钉伸出hub面=%+.3f(应+4.06) 抱箍起点离盖板=%+.3f" %
(r["jid"], r["halfA"], r["halfB"], r["clamp"], r["spigot_proud"], r["clamp_clear"]))
print("COLLISION pairs=%d" % len(cl))
for r in cl[:18]:
print(" %-16s x %-16s %9.1f mm3 %s" % r)
print("WROTE", OUT + "/assembly/ROBOT_ARM_%s.stp" % VERSION)