Files
Robot_Arm/arm/cad/helpers.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

174 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
"""CAD 放置与几何 helper:刚体变换、求交/包围盒、基础体、网格化与 STEP 读取。
实测接口数值在 arm/constants.py,仓库路径在 arm/paths.py——本模块不再持有它们。
"""
import math
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
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRep import BRep_Tool
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_FACE, TopAbs_REVERSED, TopAbs_SOLID
from OCP.TopoDS import TopoDS
V = cq.Vector
# ---------------- geometry helpers --------------------------------------------------------------------
def _nrm(v):
m = v.Magnitude()
return gp_Vec(v.X() / m, v.Y() / m, v.Z() / m)
def _dot(a, b):
return a.X() * b.X() + a.Y() * b.Y() + a.Z() * b.Z()
def _perp(e, z):
d = _dot(e, z)
return gp_Vec(e.X() - d * z.X(), e.Y() - d * z.Y(), e.Z() - d * z.Z())
def trs(pos=(0, 0, 0), xdir=(1, 0, 0), zdir=(0, 0, 1)):
"""Local->global rigid transform: local +Z -> zdir, local +X -> xdir, y = z x x.
Uses SetValues (row-major 3x4) because gp_Quaternion / SetCols overloads in this
OCP build do not map axes the way the docs suggest - verified against known points."""
z = _nrm(gp_Vec(*zdir))
x = _perp(gp_Vec(*xdir), z)
if x.Magnitude() < 1e-9:
x = _perp(gp_Vec(0, 0, 1), z)
x = _nrm(x)
y = z.Crossed(x)
t = gp_Trsf()
t.SetValues(x.X(), y.X(), z.X(), pos[0],
x.Y(), y.Y(), z.Y(), pos[1],
x.Z(), y.Z(), z.Z(), pos[2])
return t
def map_pt(t, p):
v = cq.Vertex.makeVertex(*p)
w = cq.Shape(BRepBuilderAPI_Transform(v.wrapped, t, True).Shape())
q = w.Vertices()[0].Center()
return (q.x, q.y, q.z)
def place(shape, t):
if shape is None:
return None
return cq.Shape(BRepBuilderAPI_Transform(shape.wrapped, t, True).Shape())
def bb(shape):
b = Bnd_Box()
BRepBndLib.Add_s(shape.wrapped, b, True)
lo, hi = b.CornerMin(), b.CornerMax()
return (lo.X(), lo.Y(), lo.Z()), (hi.X(), hi.Y(), hi.Z())
def bbox_str(shape):
a, b = bb(shape)
return "[%.1f %.1f %.1f .. %.1f %.1f %.1f] (%.1fx%.1fx%.1f)" % (
a[0], a[1], a[2], b[0], b[1], b[2], b[0] - a[0], b[1] - a[1], b[2] - a[2])
def cyl(r, z0, z1, x=0.0, y=0.0):
return cq.Solid.makeCylinder(r, z1 - z0, V(x, y, z0), V(0, 0, 1))
def ring(r0, r1, z0, z1):
return cyl(r1, z0, z1).cut(cyl(r0, z0 - 0.001, z1 + 0.001))
def box(x0, x1, y0, y1, z0, z1):
return cq.Solid.makeBox(x1 - x0, y1 - y0, z1 - z0, V(x0, y0, z0))
def rect_pocket(cx, cy, w, h, z0, z1, rot=0.0):
s = box(cx - w / 2.0, cx + w / 2.0, cy - h / 2.0, cy + h / 2.0, z0 - 0.001, z1 + 0.001)
if rot:
s = s.rotated(V(0, 0, 0), V(0, 0, 1), rot)
return s
def holes(shape, pts, d, z0, z1):
"""Through/ blind cylindrical cuts at the given (x, y) points."""
cut = [cyl(d / 2.0, z0, z1, p[0], p[1]) for p in pts]
return shape.cut(*cut) if cut else shape
def slot_holes(shape, pts, d, cb_d=None, cb_z0=None, cb_z1=None, z0=-1, z1=1):
cuts = [cyl(d / 2.0, z0, z1, p[0], p[1]) for p in pts]
if cb_d and cb_z0 is not None:
cuts += [cyl(cb_d / 2.0, cb_z0, cb_z1, p[0], p[1]) for p in pts]
return shape.cut(*cuts)
def polygon_pts(r, n, phase=0.0):
return [(r * math.cos(math.radians(phase + 360.0 * k / n)),
r * math.sin(math.radians(phase + 360.0 * k / n))) for k in range(n)]
def convex_prism(pts2d, z0, z1):
"""Solid from an ordered 2D outline (assumed convex or simple)."""
wp = cq.Workplane("XY").workplane(centerSection=(0, 0, z0))
w2 = [(p[0], p[1]) for p in pts2d]
r = (cq.Workplane("XY", origin=(0, 0, z0))
.polyline(w2 + [w2[0]]).close().extrude(z1 - z0, combine=False))
return r.val()
def fillet_all_edges(shape, r):
try:
return shape.edges().fillet(r).val() if isinstance(shape, cq.Workplane) else \
cq.Workplane(obj=shape).edges().fillet(r).val()
except Exception:
return shape
def mesh_of(shape, defl=0.08):
BRepMesh_IncrementalMesh(shape.wrapped, defl, False, 0.55, True)
verts, tris, base = [], [], 0
e = TopExp_Explorer(shape.wrapped, TopAbs_FACE)
while e.More():
cur = e.Current()
f = TopoDS.Face_s(cur)
tri = BRep_Tool.Triangulation_s(f, cur.Location())
if tri is not None:
trsf = cur.Location().Transformation()
for i in range(1, tri.NbNodes() + 1):
p = tri.Node(i).Transformed(trsf)
verts.append((round(p.X(), 2), round(p.Y(), 2), round(p.Z(), 2)))
rev = f.Orientation() == TopAbs_REVERSED
for i in range(1, tri.NbTriangles() + 1):
t = tri.Triangle(i)
a, b, c = t.Value(1) - 1, t.Value(2) - 1, t.Value(3) - 1
tris.append((base + a, base + c, base + b) if rev else (base + a, base + b, base + c))
base += tri.NbNodes()
e.Next()
return verts, tris
def solids_of(shape):
out = []
e = TopExp_Explorer(shape.wrapped, TopAbs_SOLID)
while e.More():
out.append(TopoDS.Solid_s(e.Current()))
e.Next()
return [cq.Shape(s) for s in out]
def read_step(p):
return cq.importers.importStep(p)
def norm_angle(a):
return (a + 180.0) % 360.0 - 180.0