Files
Robot_Arm/arm/analysis/gen_env_mesh.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

100 lines
4.9 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 -*-
"""生成可达外包络的 ENV 零件(经纬网单件)追加进 web/mesh_arm.json:
ENV_WL_包络经纬网 —— kind=line,LineSegments 渲染(经向 10°/条、纬向 8mm/环)
包络 = rmax(x) 母线绕 J1 轴(平行 X,过 (y=128, z=610))整周回转;grp=root 不随关节动。"""
import json
import math
import os
import sys
import numpy as np
from arm.paths import WEB, CACHE
from arm import kinematics as HL
Y_AX = HL.Y_AXIS # 128
Z_AX = HL.Z_RL # 610
def main():
d = np.load(CACHE + "/ws_cloud.npz")
xs, rmax = d["xs"], d["rmax"]
ok = ~np.isnan(rmax)
xs, rmax = xs[ok], rmax[ok]
def grid(step_x, dphi_deg):
"""经纬参数网格 -> verts(NX*NP,3), segs(扁平索引对), tris。"""
xs_e = np.arange(xs.min(), xs.max() + 1e-9, step_x)
rm_e = np.interp(xs_e, xs, rmax)
nph = int(round(360.0 / dphi_deg))
phg = np.linspace(0.0, 2.0 * math.pi, nph, endpoint=False)
nx = len(xs_e)
V = np.empty((nx * nph, 3))
for i, x in enumerate(xs_e):
r = rm_e[i]
for j, ph in enumerate(phg):
V[i * nph + j] = (x, Y_AX + r * math.cos(ph), Z_AX + r * math.sin(ph))
segs = []
for i in range(nx - 1): # 经线(沿 x)
for j in range(nph):
segs += [i * nph + j, (i + 1) * nph + j]
for i in range(nx): # 纬线(沿 φ)
for j in range(nph):
j2 = (j + 1) % nph
segs += [i * nph + j, i * nph + j2]
tris = []
for i in range(nx - 1):
for j in range(nph):
j2 = (j + 1) % nph
a, b = i * nph + j, i * nph + j2
c, e = (i + 1) * nph + j, (i + 1) * nph + j2
tris += [a, c, b, b, c, e]
return V, segs, tris
# 经纬网单件:经向 10°一条、纬向 8mm一环(无需贴片面,线自带罩体感)
Vl, sl, _tl = grid(8.0, 10.0)
# 自证:包络尺寸应等于 ws_metrics 极值。
# 【2026-10-05c 修正】原来是 <1.0 的硬比较,但网格 x 是从 xs.min() 起按 step_x
# 对齐的(np.arange),最后一个采样点最多比真极值短一个步长 —— 一旦
# (x_fwd - xs.min()) 不是 step_x 的整数倍,断言就会假失败(实测差 6.46mm,
# 正好 < 8.0),导致包络注不进去、查看器里"可达包络"整个消失。改为
# "不得超出、且至多少一个步长"这个真正成立的判据。
step_x = 8.0
met = json.load(open(CACHE + "/ws_metrics.json", encoding="utf-8"))
assert Vl[:, 0].min() >= met["x_back"] - 1e-6, (Vl[:, 0].min(), met["x_back"])
assert Vl[:, 0].min() <= met["x_back"] + step_x + 1e-6, (Vl[:, 0].min(), met["x_back"])
assert Vl[:, 0].max() <= met["x_fwd"] + 1e-6, (Vl[:, 0].max(), met["x_fwd"])
assert Vl[:, 0].max() >= met["x_fwd"] - step_x - 1e-6, (Vl[:, 0].max(), met["x_fwd"])
rl = np.hypot(Vl[:, 1] - Y_AX, Vl[:, 2] - Z_AX)
# rl.max() 就是 rmax(x) 网格插值后的峰值,与 met["r_max"](原始点云极值)之间
# 只允许有插值/取整量级的差(实测 0.044mm),故给 1.0mm 容差。
assert abs(rl.max() - met["r_max"]) < 1.0, (rl.max(), met["r_max"])
assert np.all((Vl[:, 1] - Y_AX) ** 2 + (Vl[:, 2] - Z_AX) ** 2
<= (np.interp(Vl[:, 0], xs, rmax) + 1e-6) ** 2), "line"
print("包络经纬网: 顶点%d 线段%d(经10° x 纬8mm)" % (len(Vl), len(sl) // 2))
# ---- 追加进 mesh_arm.json ----
path = WEB + "/mesh_arm.json"
data = json.load(open(path, encoding="utf-8"))
data["parts"] = [p for p in data["parts"] if not p["name"].startswith("ENV_")]
mx = max(p["idx"] for p in data["parts"])
note_ws = ("可达外包络(运动学,docs/travel.json 滑块限位下的全组合 FK 包络):"
"r=rmax(x) 绕 J1 轴整周回转。高 %.0f~%.0f mm,前伸 %.0f / 后伸 %.0f mm,"
"离 J1 轴最远 %.0f mm;包络内含死区(肩下折不进),多关节同极位由控制器避碰。"
% (met["z_min"], met["z_max"], met["x_fwd"], met["x_back"], met["r_max"]))
line = dict(name="ENV_WL_包络经纬网", cn="包络经纬网", v=[list(p) for p in Vl],
s=sl, color=0x60a5fa, printed=False, vol=round(met["vol_dm3"] * 1e6, 1),
note=note_ws + " 经纬线:经向 10°/条,纬向 8mm/环。",
grp="root", jaw=0.0, ex=[0.0, 0.0, 0.0], idx=mx + 1,
kind="line")
data["parts"] += [line]
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
os.replace(tmp, path)
print("已写入", path, "大小 %.1f MB" % (os.path.getsize(path) / 1e6))
if __name__ == "__main__":
main()