单文件约束取消后,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 段逐字节一致。
294 lines
14 KiB
Python
294 lines
14 KiB
Python
# -*- 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刻字"),
|
||
]
|