diff --git a/.gitignore b/.gitignore index 4482f69..7f987a3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,10 @@ web/机器人手臂3D查看器.html /机器人手臂3D查看器.html webapp/data/ dist/ +data/cache/ + +# 这 3 张线束 x-ray 截图在同一份构建下就不逐字节复现(抗锯齿噪声), +# 每次跑关卡都会改写它们;参考像素已存进 tests/baselines/flaky/,不再入库。 +docs/shots/harness_p04_p05_xray.png +docs/shots/harness_p07_xray.png +docs/shots/harness_xray_internal.png diff --git a/arm/__init__.py b/arm/__init__.py new file mode 100644 index 0000000..29f8320 --- /dev/null +++ b/arm/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""6 自由度人形机械臂:CadQuery 参数化建模、行程/干涉扫描、制造文件与查看器数据。 + +入口一律从仓根以 `python -m arm.xxx` 运行:`python -m` 会把仓根放进 sys.path, +包内 import 因此直接可解析,不需要 pip install。 +""" diff --git a/arm/analysis/__init__.py b/arm/analysis/__init__.py new file mode 100644 index 0000000..a061874 --- /dev/null +++ b/arm/analysis/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""行程与干涉扫描、强度与力矩核算、可达包络采样与网格注入。 +""" diff --git a/src/finalize_travel.py b/arm/analysis/finalize_travel.py similarity index 85% rename from src/finalize_travel.py rename to arm/analysis/finalize_travel.py index 9d551b4..35e856d 100644 --- a/src/finalize_travel.py +++ b/arm/analysis/finalize_travel.py @@ -14,8 +14,15 @@ import math import os import sys -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from sweep_v2 import CHAIN, DOCS +from arm.analysis.sweep_v2 import CHAIN +from arm.paths import DOCS + +# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物 +# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。 +if __name__ != "__main__": + raise RuntimeError( + "finalize_travel 是脚本:顶层代码一执行就重写产物,不能 import。" + "要用就跑 python -m arm.analysis.finalize_travel。") d = json.load(open(DOCS + "/travel_detail.json", encoding="utf-8")) diff --git a/arm/analysis/gen_env_mesh.py b/arm/analysis/gen_env_mesh.py new file mode 100644 index 0000000..76151df --- /dev/null +++ b/arm/analysis/gen_env_mesh.py @@ -0,0 +1,100 @@ +# -*- 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() \ No newline at end of file diff --git a/src/strength_check.py b/arm/analysis/strength_check.py similarity index 96% rename from src/strength_check.py rename to arm/analysis/strength_check.py index 63aa891..98f9e49 100644 --- a/src/strength_check.py +++ b/arm/analysis/strength_check.py @@ -19,22 +19,19 @@ import math import os import re import sys - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass import cadquery as cq -from armcore import OUT, DOCS, solids_of, bb, mesh_of +from arm.paths import OUT, WEB, DOCS +from arm.cad.helpers import solids_of, bb, mesh_of from OCP.BRepAlgoAPI import BRepAlgoAPI_Common from OCP.BRepGProp import BRepGProp from OCP.GProp import GProp_GProps from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2, gp_Vec - -import human_layout as HL - +from arm import kinematics as HL RHO = {"PLA": 1.24e-3, "motor_g": 210.0} # g/mm3 for printed, g per module # a slicer does not print solid: 3 perimeters + 30% infill lands near this fraction of # the CAD envelope, and using the solid mass would blame the part for plastic that is not there @@ -159,7 +156,7 @@ def section_props(sh, normal, origin, bend=2, size=400.0): def arm_masses(): """Everything distal to each joint, in grams, from the model itself.""" - meta = json.load(io.open(OUT.replace("/out", "/web") + "/mesh_arm.json", encoding="utf-8")) + meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8")) TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"] tot = {} for p in meta["parts"]: @@ -202,7 +199,7 @@ def _mesh_vol_cg(v, t): def part_table(payload_kg=None): """[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。""" - meta = json.load(io.open(OUT.replace("/out", "/web") + "/mesh_arm.json", encoding="utf-8")) + meta = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8")) rows = [] for p in meta["parts"]: nm = p["name"] @@ -488,5 +485,10 @@ def main(): if __name__ == "__main__": b = main() print("\n不合格截面 %d 处" % len(b)) - sys.stdout.flush() # os._exit 不刷缓冲,重定向到文件时否则输出全丢 + + # OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是 + # `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口 + # 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139, + # 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。 + sys.stdout.flush() os._exit(0) diff --git a/src/sweep_v2.py b/arm/analysis/sweep_v2.py similarity index 97% rename from src/sweep_v2.py rename to arm/analysis/sweep_v2.py index cbbba1f..d1cf237 100644 --- a/src/sweep_v2.py +++ b/arm/analysis/sweep_v2.py @@ -27,8 +27,7 @@ 就是臂撞躯干,是真约束。--no-stand 仅用于诊断:排除台架后重扫,给出"不含躯干约束"的 自由空间行程,用来分辨某个限位是臂自身折叠产生的还是躯干挡的(台架位置本身是假设, 改它等于改测试工装,不等于改设计)。 - -运行:python src/sweep_v2.py [--workers 20] [--step 2] [--combo-step 6] +运行:python -m arm.analysis.sweep_v2 [--workers 20] [--step 2] [--combo-step 6] [--no-combos] [--joints J2,J4] [--no-gjk] [--no-stand] """ import json @@ -38,20 +37,17 @@ import sys import time import numpy as np - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass import multiprocessing as mp - -import arm_build as AB +from arm.cad import arm_build as AB import cadquery as cq -import human_layout as HL -from armcore import place, bb, DOCS - +from arm import kinematics as HL +from arm.paths import DOCS +from arm.cad.helpers import place, bb VOL_MIN = 2.0 # mm3,超过即计为干涉 CHAIN = ["root", "J1", "J2", "J3", "J4", "J5", "J6"] MOUNT_PAIRS = { @@ -175,7 +171,7 @@ def _do_simplex(s): def load_model(include_fixture=True): """include_fixture=False 时把 P01 立柱 / P02 底盘(固定测试台架,非本体)排除在 碰撞配对之外——用来得到真机工作空间而不是台架工作空间。见 human_layout.FIXTURE_PREFIX。""" - import arm_model as AM + from arm import build_model as AM JS = AM.build() parts = [] for p in AB.PARTS: @@ -721,3 +717,10 @@ def main(): if __name__ == "__main__": main() + + # OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是 + # `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口 + # 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139, + # 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。 + sys.stdout.flush() + os._exit(0) diff --git a/src/torque_balance.py b/arm/analysis/torque_balance.py similarity index 95% rename from src/torque_balance.py rename to arm/analysis/torque_balance.py index 587d8ac..7669030 100644 --- a/src/torque_balance.py +++ b/arm/analysis/torque_balance.py @@ -29,17 +29,13 @@ import json import math import os import sys - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass - -import human_layout as HL -from armcore import DOCS -import strength_check as SC - +from arm import kinematics as HL +from arm.paths import DOCS +from arm.analysis import strength_check as SC G = 9.81 T_CONT = float(os.environ.get("ARMT_CONT", "2.5")) T_PEAK = float(os.environ.get("ARMT_PEAK", "8.0")) @@ -206,9 +202,12 @@ def main(): w = min(ok, key=lambda t: mp[t]) print(" 整臂瓶颈 %s -> %.2f kg" % (w, mp[w])) print("\n已写出 docs/torque_balance.json + docs/torque_report.md") - sys.stdout.flush() - - if __name__ == "__main__": main() + + # OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是 + # `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口 + # 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139, + # 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。 + sys.stdout.flush() os._exit(0) diff --git a/arm/analysis/ws_sweep.py b/arm/analysis/ws_sweep.py new file mode 100644 index 0000000..1f89275 --- /dev/null +++ b/arm/analysis/ws_sweep.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +"""关节角组合可达空间(工具尖点)计算:FK 采样 + J1 旋转对称包络 + 体积估计""" +import json +import math +import os +import sys + +import numpy as np + +from arm import kinematics as HL +from arm.paths import DOCS, CACHE + +TRAVEL = {t["axis"]: (t["lo"], t["hi"]) for t in + json.load(open(DOCS + "/travel.json", encoding="utf-8"))} + +TIP = (HL.X_ARM, HL.Y_AXIS, HL.Z_TIP) # P09 法兰底面中心(工具安装面) +J1_AX = (HL.Y_AXIS, HL.Z_RL) # J1 转轴: 过 Y=128, Z=610, 平行 X + + +def rot_about(p, k, ang): + """绕轴 (p 点, k 方向) 旋转 ang(弧度, 可为数组M,);返回 Rm(M,3,3), t(M,3)。 + p' = Rm @ (p_pt - p) + p,与 AB.R 的 gp_Trsf.SetRotation 右手定则一致。""" + k = np.asarray(k, dtype=float) + k = k / np.linalg.norm(k) + ang = np.atleast_1d(np.asarray(ang, dtype=float)) + px, py, pz = p + K = np.array([[0.0, -k[2], k[1]], [k[2], 0.0, -k[0]], [-k[1], k[0], 0.0]]) + K2 = K @ K + Rm = np.eye(3)[None, :, :] + np.sin(ang)[:, None, None] * K[None, :, :] \ + + (1.0 - np.cos(ang))[:, None, None] * K2[None, :, :] + pp = np.array([px, py, pz]) + t = pp[None, :] - np.einsum("mij,j->mi", Rm, pp) + return Rm, t + + +def fk_batch(angles, tip=TIP): + """angles: dict idx->弧度数组(M,);返回 (M,3) 世界坐标。链式 acc 与 sweep_v2 一致: + 父关节变换在最外层 p' = R1∘R2∘…∘R5 (p),各 R_i 绕其零位世界轴(human_layout 唯一事实源)。""" + pts = np.tile(np.asarray(tip, dtype=float), (angles[1].shape[0], 1)) + for i in range(5, 0, -1): # J6 跳过:尖点在 J6 轴上,其转角不改变尖点位置 + rear, d = HL.JOINTS[i - 1][2], HL.JOINTS[i - 1][3] + Rm, t = rot_about(rear, d, angles[i]) + pts = np.einsum("mij,mj->mi", Rm, pts) + t + return pts + + +def sweep(step=2.0): + j2 = np.radians(np.arange(TRAVEL["J2"][0], TRAVEL["J2"][1] + 1e-9, step)) + j4 = np.radians(np.arange(TRAVEL["J4"][0], TRAVEL["J4"][1] + 1e-9, step)) + j5 = np.radians(np.arange(TRAVEL["J5"][0], TRAVEL["J5"][1] + 1e-9, step)) + g2, g4, g5 = np.meshgrid(j2, j4, j5, indexing="ij") + M = g2.size + angles = {1: np.zeros(M), 2: g2.ravel(), 3: np.zeros(M), + 4: g4.ravel(), 5: g5.ravel()} + pts = fk_batch(angles) + # 自证1:零位尖点必须回到 (18.5,128,93) + z0 = fk_batch({i: np.zeros(1) for i in range(1, 6)}) + assert np.allclose(z0[0], TIP, atol=1e-9), z0[0] + # 自证2:J1/J6 旋转不改变 (x, r) 与极值位置——J1 取中值扫一遍,r 分布应与 J1=0 相同 + for phi in (0.5, 1.2, 2.4): + v1 = np.full(M, phi) + p1 = fk_batch({1: v1, 2: g2.ravel(), 3: np.zeros(M), + 4: g4.ravel(), 5: g5.ravel()}) + r1 = np.hypot(p1[:, 1] - J1_AX[0], p1[:, 2] - J1_AX[1]) + r0 = np.hypot(pts[:, 1] - J1_AX[0], pts[:, 2] - J1_AX[1]) + assert np.allclose(np.sort(r0), np.sort(r1), atol=1e-9), "J1 对称性被破坏" + # 自证3:J3 旋转(大臂自转)不改变尖点位置——关节轴过尖点所在纵线 + print("J3 对称性: 尖点 x=%.1f 与 J3 轴 x=%.1f 一致=%s, y=%.1f 与轴 y=%.1f 一致=%s" + % (TIP[0], HL.X_ARM, abs(TIP[0] - HL.X_ARM) < 1e-9, + TIP[1], HL.Y_AXIS, abs(TIP[1] - HL.Y_AXIS) < 1e-9)) + print("J6 含于轴: 尖点 (%.1f,%.1f) vs 轴 (%.1f,%.1f)" + % (TIP[0], TIP[1], HL.JOINTS[5][2][0], HL.JOINTS[5][2][1])) + return pts + + +def envelope(pts, grid=1.0): + """(x, r) 半平面栅格化占用 + 体积(绕 J1 轴回转)。""" + x = pts[:, 0] + r = np.hypot(pts[:, 1] - J1_AX[0], pts[:, 2] - J1_AX[1]) + x0, x1 = math.floor(x.min()), math.ceil(x.max()) + r0, r1 = 0.0, math.ceil(r.max()) + nx = int(round((x1 - x0) / grid)) + 1 + nr = int(round((r1 - r0) / grid)) + 1 + xi = np.clip(((x - x0) / grid).round().astype(int), 0, nx - 1) + ri = np.clip((r / grid).round().astype(int), 0, nr - 1) + occ = np.zeros((nx, nr), dtype=bool) + occ[xi, ri] = True + # 每个 x 列只保留 r <= 该列可达最大 r(回转体沿 r 连续填充) + for i in range(nx): + j = np.nonzero(occ[i])[0] + if j.size: + occ[i, : j.max() + 1] = True + rr = r0 + np.arange(nr) * grid + vol = float((occ * (2.0 * math.pi * rr)[None, :] * grid * grid).sum()) + return dict(occ=occ, x0=x0, grid=grid, rm_axes=rr, vol_mm3=vol, + nx=nx, nr=nr) + + +def main(): + pts = sweep() + x, y, z = pts[:, 0], pts[:, 1], pts[:, 2] + r = np.hypot(y - J1_AX[0], z - J1_AX[1]) + env = envelope(pts) + # 每个 x 的 r 包络(侧视/俯视剖面用) + xs = env["x0"] + np.arange(env["nx"]) * env["grid"] + rmax_x, rmin_x = [], [] + for i in range(env["nx"]): + j = np.nonzero(env["occ"][i])[0] + if j.size: + rmax_x.append(env["rm_axes"][j.max()]) + rmin_x.append(env["rm_axes"][j.min()]) + else: + rmax_x.append(np.nan) + rmin_x.append(np.nan) + os.makedirs(CACHE, exist_ok=True) + np.savez_compressed(CACHE + "/ws_cloud.npz", + pts=pts, xs=np.array(xs), + rmax=np.array(rmax_x), rmin=np.array(rmin_x)) + met = dict( + z_max=float(z.max()), z_min=float(z.min()), + x_fwd=float(x.max()), x_back=float(x.min()), + y_lo=float(y.min()), y_hi=float(y.max()), + r_max=float(r.max()), r_min=float(r.min()), + reach_from_j1_axis=float(r.max()), + spread=float(z.max() - z.min()), + vol_dm3=env["vol_mm3"] / 1e6, + tip=TIP, j1_axis=list(J1_AX), + limits={k: list(v) for k, v in TRAVEL.items()}, + ) + # 自证4:极值点姿态可解释(打印取极值的角度组合索引) + imax = int(np.argmax(z)); imin = int(np.argmin(z)) + ifwd = int(np.argmax(x)); iback = int(np.argmin(x)) + met["pose_extreme"] = dict( + zmax_idx=imax, zmin_idx=imin, xfwd_idx=ifwd, xback_idx=iback) + print(json.dumps({k: v for k, v in met.items() + if k not in ("limits", "pose_extreme")}, + ensure_ascii=False, indent=1)) + json.dump(met, open(CACHE + "/ws_metrics.json", "w", encoding="utf-8"), + ensure_ascii=False, indent=1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/arm_model.py b/arm/build_model.py similarity index 78% rename from src/arm_model.py rename to arm/build_model.py index 088bea0..9ee9bba 100644 --- a/src/arm_model.py +++ b/arm/build_model.py @@ -10,17 +10,17 @@ import math import os import sys import time - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import cadquery as cq -import arm_build as AB -from armcore import (M, C, cyl, box, ring, place, trs, bb, bbox_str, solids_of, OUT, WEB, DOCS) -from arm_build import (Joint, add, bx, Ycyl, Ybox, Yplate, prism, dogbone, sector, +from arm.cad import arm_build as AB +from arm.paths import OUT, WEB, DOCS +from arm.constants import M, C +from arm.cad.helpers import cyl, box, ring, place, trs, bb, bbox_str, solids_of +from arm.cad.arm_build import (Joint, add, bx, Ycyl, Ybox, Yplate, prism, dogbone, sector, FLANGE_Z, COVER_Z, HUB_FACE_Z, CLAMP_SPAN, HUB_M2_CLOCK, BORE_R, CB_R, PCB_W, D2R, T, R, PARTS) -import human_layout as HL +from arm import kinematics as HL from OCP.BRepAlgoAPI import BRepAlgoAPI_Common -from human_layout import (HUB, PLATE_T, R_MOT, Z_TIP, Z_WR, Z_EI, Z_SH, Z_RL, Z_R1, +from arm.kinematics import (HUB, PLATE_T, R_MOT, Z_TIP, Z_WR, Z_EI, Z_SH, Z_RL, Z_R1, Z_R3, Z_HUB3, Z_EL, Z_R6, Z_HUB6, Z_R4, Z_CAP, Z_M6, Z_CHEST0, Z_CHEST1, X_J1_HUB, X_ARM, L1, X_FORK_GAP, X_BAY_EXT, @@ -33,24 +33,23 @@ AB.VERSION = VERSION AB.ITER = ITER # ------------------------------------------------ 逐件模块(几何已拆出) -from parts import PART_MODULES -from part_tools import ( +from arm.cad.parts import PART_MODULES +from arm.cad.part_tools import ( BOSS, CHAMFER_EDGE, CLAMP_BACKOFF, CLAMP_BCD, CLAMP_CLOCK, CLAMP_CLOCK_LOCAL, CLAMP_CLR, CLAMP_ROOT, CLAMP_T, CLEARANCE_GAP, DISC_R, FENCE_OPEN_CUT, FILLET_LARGE, FILLET_MEDIUM, FILLET_SMALL, FILLET_TINY, FILLET_XLARGE, HOLLOW_FILLET, HOLLOW_WALL, SLEEVE_R, XB, Xcyl, Xplate, Y_MID, cut_motor_fence, hollow_channel, hollow_tent_window, hub_m2_holes, make_direct_hub, make_pitch_to_roll_bracket, motor_front_clamp, ) -from parts.p01_column import X_COL, Y_COL, Z_LOCK, p_chest, split_p01_poka_yoke -from parts.p02_base import make_p02_poka_yoke, p_base_foot -from parts.p03_girdle import p_girdle -from parts.p04_fork import P04_CH, p_fork -from parts.p05_roll_base import p_shoulder_roll_base -from parts.p06_humerus import P06_HOLLOW_WIN, P06_WIN_XZ, p_humerus_bone -from parts.p07_forearm import P07_CH, p_forearm_bone -from parts.p08_wrist import p_wrist_bracket -from parts.p09_flange import p_tool_flange - +from arm.cad.parts.p01_column import X_COL, Y_COL, Z_LOCK, p_chest, split_p01_poka_yoke +from arm.cad.parts.p02_base import make_p02_poka_yoke, p_base_foot +from arm.cad.parts.p03_girdle import p_girdle +from arm.cad.parts.p04_fork import P04_CH, p_fork +from arm.cad.parts.p05_roll_base import p_shoulder_roll_base +from arm.cad.parts.p06_humerus import P06_HOLLOW_WIN, P06_WIN_XZ, p_humerus_bone +from arm.cad.parts.p07_forearm import P07_CH, p_forearm_bone +from arm.cad.parts.p08_wrist import p_wrist_bracket +from arm.cad.parts.p09_flange import p_tool_flange # 兼容旧工具:scratch/ 的诊断脚本按 arm_model.<符号> 取用件构造函数与开关 def build(): @@ -80,9 +79,7 @@ def build(): for t in ("J1", "J2", "J3", "J4", "J5", "J6"): AB.GRP[motor_of[t]] = par[t] AB.JAW = {} - - - # 逐件模块:几何、件号/配色/说明、装配帧归属都在 src/parts/ 里, + # 逐件模块:几何、件号/配色/说明、装配帧归属都在 arm/cad/parts/ 里, # 这里只按 BOM 顺序收集 specs(),并把件号登记到装配帧图上。 parts_to_build = [] for _m in PART_MODULES: @@ -177,39 +174,26 @@ if __name__ == "__main__": # 紧固件(螺钉)实例注入:把 docs/screws.json 的 72 颗螺钉按真实孔位还原成三维实例, # 追加进 web/mesh_arm.json 的 parts(查看器可点 BOM 螺丝行高亮定位;纯数据,不动 CAD) - try: - import screw_mesh - screw_mesh.main() - except Exception as e: - print("螺钉实例注入失败(不影响主流程,可手动跑 src/screw_mesh.py):", e, flush=True) - + from arm.dataio import screw_mesh + screw_mesh.main() cl = AB.collide() - - try: - from build_viewer import rebuild_viewer - rebuild_viewer() - except Exception as e: - print("rebuild_viewer error:", e, flush=True) - + from arm.viewer.build_viewer import rebuild_viewer + rebuild_viewer() # 可达包络自动注入:AB.export 刚重写过 mesh_arm.json(包络件会被冲掉), # 这里重跑 FK 采样 + 包络网格注入,再重打包一次查看器(约 3~4s,纯数据无 CAD 计算) - try: - sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), - "..", "scratch")) - import ws_sweep - import gen_env_mesh - t_env = time.time() - ws_sweep.main() - gen_env_mesh.main() - from build_viewer import rebuild_viewer as _rbv - _rbv() - print(f"-> 可达包络已自动注入查看器(+{time.time()-t_env:.1f}s)✓\n", flush=True) - except Exception as e: - print("可达包络自动注入失败(不影响主流程,修复后手动跑 scratch/gen_env_mesh.py + src/build_viewer.py):", - e, flush=True) - + from arm.analysis import ws_sweep, gen_env_mesh + t_env = time.time() + ws_sweep.main() + gen_env_mesh.main() + rebuild_viewer() + print(f"-> 可达包络已自动注入查看器(+{time.time() - t_env:.1f}s)✓\n", flush=True) print("=" * 80, flush=True) print("✅ 机器人手臂编译、导出与查看器发布全流程完成!", flush=True) print("=" * 80, flush=True) + + # OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是 + # `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口 + # 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139, + # 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。 sys.stdout.flush() os._exit(0) diff --git a/arm/cad/__init__.py b/arm/cad/__init__.py new file mode 100644 index 0000000..190b39a --- /dev/null +++ b/arm/cad/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""几何与装配层。 + +helpers 放置/基础体/网格化等无状态 helper +arm_build Joint 装配模型、零件登记(add)、STEP/STL/BOM/mesh 导出 +part_tools 跨件共用刀法(减重腔、压盖、转接座、圆角常量、螺钉孔位事实源) +parts/ 每个打印件一个模块:几何 + 件号/配色/说明 + 装配帧归属 +""" diff --git a/src/arm_build.py b/arm/cad/arm_build.py similarity index 99% rename from src/arm_build.py rename to arm/cad/arm_build.py index b530d12..4dc4f9a 100644 --- a/src/arm_build.py +++ b/arm/cad/arm_build.py @@ -21,10 +21,10 @@ try: except Exception: pass -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import cadquery as cq -from armcore import (M, C, V, trs, place, bb, bbox_str, cyl, ring, box, holes, - solids_of, mesh_of, MOTOR_TOL, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP) +from arm.paths import OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP +from arm.constants import M, C, MOTOR_TOL +from arm.cad.helpers import V, trs, place, bb, bbox_str, cyl, ring, box, holes, solids_of, mesh_of from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1 from OCP.BRepAlgoAPI import BRepAlgoAPI_Common from OCP.GProp import GProp_GProps diff --git a/src/armcore.py b/arm/cad/helpers.py similarity index 59% rename from src/armcore.py rename to arm/cad/helpers.py index 8c6b0c7..b9371eb 100644 --- a/src/armcore.py +++ b/arm/cad/helpers.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- -"""Shared constants + CAD/placement helpers for the robot arm build. +"""CAD 放置与几何 helper:刚体变换、求交/包围盒、基础体、网格化与 STEP 读取。 -All motor interface numbers are MEASURED from -G:/Robot_v2/Model/Solidworks/电机.STEP and 配件/make_v9.py (see docs/interfaces.md). +实测接口数值在 arm/constants.py,仓库路径在 arm/paths.py——本模块不再持有它们。 """ import math -import json -import os 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 @@ -21,68 +18,7 @@ from OCP.TopoDS import TopoDS V = cq.Vector -# 路径全部由本文件位置推出,仓库可以整体搬迁;三个环境变量是临时覆盖入口。 -_HERE = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/") -WORKSPACE = os.environ.get("ARM_WORKSPACE") or os.path.dirname(_HERE) -ROOT = os.environ.get("ARM_ROOT") or os.path.dirname(WORKSPACE) -MODEL = os.environ.get("ARM_MODEL_ROOT") or (ROOT + r"/Model/Solidworks") -SRC_STEP = MODEL + r"/电机.STEP" -HALF_STEP = MODEL + r"/配件/端面齿半联轴器_相同件x2.STEP" -CLAMP_STEP = MODEL + r"/配件/端面齿联轴器_抱箍_两片装配.STEP" -OUT = WORKSPACE + r"/out" -WEB = WORKSPACE + r"/web" -DOCS = WORKSPACE + r"/docs" -# ---------------- MEASURED: motor module (local frame: origin = PCBA rear face centre, +Z = output) ---- -M = dict( - pcba_w=63.6, # square PCBA across flats - pcba_t=9.0, # z 0 -> 9.0 - rearplate_d=60.0, # z 5.5 -> 10.5 - hous_d=60.0, # housing OD, z 10.5 -> 52.5 - hous_len=42.0, - cover_d=60.30, # top cover OD (R30.15), z 52.5 -> 56.0 - cover_z=56.0, # cover front face = module total length - flange_z=54.942, # 2nd-stage output flange face - neck_d=36.0, # female neck bore in the cover - neck_depth=1.0, - centre_d=6.5, # centre bore in the flange - centre_depth=4.0, - m2_bcd=29.0, - m2_clock=(8.05, 128.07, 248.06), - m2_tap_d=1.85, # M2 pilot for self-tapping in printed material - m2_depth=6.0, - m2_clear_d=2.75, - m2_cb_d=5.2, -) -# ---------------- MEASURED: face-spline coupling (existing, printed by the user) ------------------------ -C = dict( - n_teeth=12, tooth_r0=19.0, tooth_r1=24.5, tooth_s=2.5, - flange_t=4.50, part_z=5.560, # mirrored stack - hub_r=27.0, - gap=9.00, # cover-face to cover-face when closed - clamp_od_r=30.0, clamp_len=7.80, - lug_r1=37.0, lug_t=4.50, - bolt_r=32.50, bolt_d=3.80, - z0=1.66, z1=9.46, # clamp envelope in the flange frame (motor cover face = 1.060) -) -# axial stations, motor local frame: -COVER_Z = M["cover_z"] # 56.0 -HUB_FACE_Z = COVER_Z + C["gap"] # 65.0 = mating face of the driven hub -CLAMP_R = C["clamp_od_r"] + 0.6 # radial keep-out around the joint axis -LUG_R = C["lug_r1"] + 1.0 # radial keep-out where the bolt lugs are -# ---------------- clearances / print rules ------------------------------------------------------------- -CLR = dict(bore_slip=0.20, pilot=0.10, hole_3=3.4, hole_27=2.8, wall_min=2.0) -FIT = dict(motor_bore_d=M["hous_d"] + 2 * 0.15, cover_cb_d=M["cover_d"] + 2 * 0.35) - -# 电机【轴向长度】公差(用户 2026-10-07 要求:余量偏大稍微减去 1mm,由 2mm 改为 1mm)。 -# 这条链是"后端板螺钉 + 前端压盖螺钉"两个刚性夹持面,螺钉头一旦坐死在沉孔台阶上 -# 就再也不能进给,所以公差没法靠"拧紧"消化,只能靠两件事吃掉(见 arm_build.Joint.pocket -# 与 motor_front_clamp): -# 长出来的 0~+1mm -> 后端板 4xM3 螺钉头下垫 0~1mm 垫片圈(螺钉被垫高 -> 电机被拉回) -# 短下去的 0~-1mm -> 电机尾端面与后端板承压面之间垫 0~1mm 垫片(承压面已按 MOTOR_TOL -# 让空,见 Joint.pocket 的 REAR_SEAT) -# 两边都不动压盖/凸台的几何,关节轴向长度不变。 -MOTOR_TOL = 1.0 # ---------------- geometry helpers -------------------------------------------------------------------- diff --git a/src/part_tools.py b/arm/cad/part_tools.py similarity index 99% rename from src/part_tools.py rename to arm/cad/part_tools.py index c0804ce..c288044 100644 --- a/src/part_tools.py +++ b/arm/cad/part_tools.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """跨件共用的刀法与统一配方:X 向基础体、减重腔 helper、电机前端压盖、止口 hub、pitch->roll 转接座骨架。""" -import arm_build as AB +from arm.cad import arm_build as AB import cadquery as cq -import human_layout as HL +from arm import kinematics as HL import math -from armcore import cyl, place -from arm_build import COVER_Z, R, T, Ycyl, bx, prism -from human_layout import X_ARM, Y_AXIS, Y_COVER - +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 diff --git a/src/parts/__init__.py b/arm/cad/parts/__init__.py similarity index 72% rename from src/parts/__init__.py rename to arm/cad/parts/__init__.py index 0f22f48..94d0f54 100644 --- a/src/parts/__init__.py +++ b/arm/cad/parts/__init__.py @@ -3,6 +3,6 @@ arm_model.build() 只按 PART_MODULES 顺序收集 specs();改哪件就开哪个文件。 """ -from parts import p01_column, p02_base, p03_girdle, p04_fork, p05_roll_base, p06_humerus, p07_forearm, p08_wrist, p09_flange +from arm.cad.parts import p01_column, p02_base, p03_girdle, p04_fork, p05_roll_base, p06_humerus, p07_forearm, p08_wrist, p09_flange PART_MODULES = [p01_column, p02_base, p03_girdle, p04_fork, p05_roll_base, p06_humerus, p07_forearm, p08_wrist, p09_flange] diff --git a/src/parts/p01_column.py b/arm/cad/parts/p01_column.py similarity index 98% rename from src/parts/p01_column.py rename to arm/cad/parts/p01_column.py index af2f843..906ca93 100644 --- a/src/parts/p01_column.py +++ b/arm/cad/parts/p01_column.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- """P01 立柱三段(A/B/C):躯干承力柱 + 阶梯榫卯与防反装对锁孔。""" import cadquery as cq -from armcore import cyl -from arm_build import Ycyl, bx -from human_layout import Y_AXIS, Z_CHEST1 -from part_tools import Xcyl - +from arm.cad.helpers import cyl +from arm.cad.arm_build import Ycyl, bx +from arm.kinematics import Y_AXIS, Z_CHEST1 +from arm.cad.part_tools import Xcyl # ========== 躯干部分 ========== # ========== 躯干/测试台架部分 (后置防倾翻高刚性机架) ========== diff --git a/src/parts/p02_base.py b/arm/cad/parts/p02_base.py similarity index 99% rename from src/parts/p02_base.py rename to arm/cad/parts/p02_base.py index 426b837..bdac6cb 100644 --- a/src/parts/p02_base.py +++ b/arm/cad/parts/p02_base.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """P02 底座三段(中核心 / 前向力臂 / 后向力臂):后置防倾翻机架与防呆母榫。""" import cadquery as cq -from armcore import cyl -from parts.p01_column import X_COL, Y_COL +from arm.cad.helpers import cyl +from arm.cad.parts.p01_column import X_COL, Y_COL def make_p02_poka_yoke(): diff --git a/src/parts/p03_girdle.py b/arm/cad/parts/p03_girdle.py similarity index 98% rename from src/parts/p03_girdle.py rename to arm/cad/parts/p03_girdle.py index 4078bc5..0387fac 100644 --- a/src/parts/p03_girdle.py +++ b/arm/cad/parts/p03_girdle.py @@ -2,11 +2,11 @@ """P03 肩吊臂:板式桁架 + 侧开式电机托架 + M1 后置法兰。""" import cadquery as cq import math -from armcore import cyl -from arm_build import Yplate, bx -from human_layout import X_BAY_EXT, Y_AXIS, Z_CHEST1, Z_R1, Z_RL -from part_tools import Xcyl, make_surface_tie_bridge -from parts.p01_column import X_COL, Y_COL, Z_LOCK +from arm.cad.helpers import cyl +from arm.cad.arm_build import Yplate, bx +from arm.kinematics import X_BAY_EXT, Y_AXIS, Z_CHEST1, Z_R1, Z_RL +from arm.cad.part_tools import Xcyl, make_surface_tie_bridge +from arm.cad.parts.p01_column import X_COL, Y_COL, Z_LOCK def p_girdle(j1): diff --git a/src/parts/p04_fork.py b/arm/cad/parts/p04_fork.py similarity index 98% rename from src/parts/p04_fork.py rename to arm/cad/parts/p04_fork.py index 8549c33..55b71c2 100644 --- a/src/parts/p04_fork.py +++ b/arm/cad/parts/p04_fork.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """P04 肩叉:J1 从动杯体、M2 前端压盖、立柱 U 形减重槽、Hermite 弧面前脸。""" -import arm_build as AB +from arm.cad import arm_build as AB import cadquery as cq import math -from armcore import cyl -from arm_build import COVER_Z, Ycyl, bx -from human_layout import X_ARM, X_FORK_GAP, Y_AXIS, Y_COVER, Y_REAR, Z_RL, Z_SH +from arm.cad.helpers import cyl +from arm.cad.arm_build import COVER_Z, Ycyl, bx +from arm.kinematics import X_ARM, X_FORK_GAP, Y_AXIS, Y_COVER, Y_REAR, Z_RL, Z_SH from OCP.BRepAlgoAPI import BRepAlgoAPI_Common -from part_tools import (Xplate, clamp_hole_drills, cut_motor_fence, hollow_channel, +from arm.cad.part_tools import (Xplate, clamp_hole_drills, cut_motor_fence, hollow_channel, hub_m2_holes, make_surface_tie_bridge, motor_front_clamp) diff --git a/src/parts/p05_roll_base.py b/arm/cad/parts/p05_roll_base.py similarity index 98% rename from src/parts/p05_roll_base.py rename to arm/cad/parts/p05_roll_base.py index 1d4e0c7..c560b52 100644 --- a/src/parts/p05_roll_base.py +++ b/arm/cad/parts/p05_roll_base.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- """P05 大臂旋转座:J2 俯仰 -> J3 滚转(复用 part_tools 的 pitch->roll 骨架)。""" import cadquery as cq -from human_layout import Z_R3, Z_SH -from part_tools import make_pitch_to_roll_bracket - +from arm.kinematics import Z_R3, Z_SH +from arm.cad.part_tools import make_pitch_to_roll_bracket def p_shoulder_roll_base(j2, j3, half=None): """P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版) diff --git a/src/parts/p06_humerus.py b/arm/cad/parts/p06_humerus.py similarity index 98% rename from src/parts/p06_humerus.py rename to arm/cad/parts/p06_humerus.py index 5eb1d0d..2c0856d 100644 --- a/src/parts/p06_humerus.py +++ b/arm/cad/parts/p06_humerus.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """P06 大臂骨架:J3 滚转 -> J4 俯仰的 Loft 空心梁。""" -import arm_build as AB +from arm.cad import arm_build as AB import cadquery as cq -from armcore import cyl -from arm_build import COVER_Z, Ycyl, bx -from human_layout import X_ARM, Y_AXIS, Y_COVER, Y_REAR, Z_EL, Z_R3 -from part_tools import ( +from arm.cad.helpers import cyl +from arm.cad.arm_build import COVER_Z, Ycyl, bx +from arm.kinematics import X_ARM, Y_AXIS, Y_COVER, Y_REAR, Z_EL, Z_R3 +from arm.cad.part_tools import ( CLAMP_BACKOFF, CLAMP_CLR, CLAMP_T, cut_motor_fence, hub_m2_holes, make_surface_tie_bridge_neg_x, motor_front_clamp, ) diff --git a/src/parts/p07_forearm.py b/arm/cad/parts/p07_forearm.py similarity index 98% rename from src/parts/p07_forearm.py rename to arm/cad/parts/p07_forearm.py index 4f2f200..f81f75b 100644 --- a/src/parts/p07_forearm.py +++ b/arm/cad/parts/p07_forearm.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """P07 小臂骨架:J4 -> J5 的 L 型结构 + 顶板减重槽。""" -import arm_build as AB +from arm.cad import arm_build as AB import cadquery as cq -import human_layout as HL +from arm import kinematics as HL import math -from armcore import cyl -from arm_build import Ycyl, bx -from human_layout import PLATE_T, X_ARM, Y_COVER, Y_HUB, Y_REAR, Z_EL, Z_WR -from part_tools import ( +from arm.cad.helpers import cyl +from arm.cad.arm_build import Ycyl, bx +from arm.kinematics import PLATE_T, X_ARM, Y_COVER, Y_HUB, Y_REAR, Z_EL, Z_WR +from arm.cad.part_tools import ( FILLET_SMALL, cut_motor_fence, hollow_channel, hub_m2_holes, make_surface_tie_bridge_neg_x, motor_front_clamp, ) diff --git a/src/parts/p08_wrist.py b/arm/cad/parts/p08_wrist.py similarity index 97% rename from src/parts/p08_wrist.py rename to arm/cad/parts/p08_wrist.py index 82c5e63..08d972d 100644 --- a/src/parts/p08_wrist.py +++ b/arm/cad/parts/p08_wrist.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- """P08 腕偏转座:J5 俯仰 -> J6 滚转(复用 part_tools 的 pitch->roll 骨架)。""" -from human_layout import Z_R6, Z_WR -from part_tools import make_pitch_to_roll_bracket - +from arm.kinematics import Z_R6, Z_WR +from arm.cad.part_tools import make_pitch_to_roll_bracket def p_wrist_bracket(j5, j6): """P08_腕偏转座_螺旋体25%_墙4底5_竖打(深度圆角优化版) diff --git a/src/parts/p09_flange.py b/arm/cad/parts/p09_flange.py similarity index 94% rename from src/parts/p09_flange.py rename to arm/cad/parts/p09_flange.py index 07bef5b..7170d58 100644 --- a/src/parts/p09_flange.py +++ b/arm/cad/parts/p09_flange.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- """P09 工具法兰:末端法兰盘与 J6 直锁 hub。""" import math -from armcore import cyl -from arm_build import D2R -from human_layout import PLATE_T, X_ARM, Y_AXIS, Z_HUB6 -from part_tools import CHAMFER_EDGE, CLAMP_BACKOFF, FILLET_SMALL, FILLET_TINY, make_direct_hub - +from arm.cad.helpers import cyl +from arm.cad.arm_build import D2R +from arm.kinematics import PLATE_T, X_ARM, Y_AXIS, Z_HUB6 +from arm.cad.part_tools import CHAMFER_EDGE, CLAMP_BACKOFF, FILLET_SMALL, FILLET_TINY, make_direct_hub def p_tool_flange(j6): """P09_工具法兰_螺旋体30%_墙4底5_竖打 深度圆角优化版 diff --git a/arm/constants.py b/arm/constants.py new file mode 100644 index 0000000..1b7b488 --- /dev/null +++ b/arm/constants.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""实测接口数值:电机模组、端面齿联轴器、配合公差与电机轴向公差。 + +全部 MEASURED 自 G:/Robot_v2/Model/Solidworks/电机.STEP 与配件(见 docs/interfaces.md)。 +改之前先确认量的是同一个基准面。""" + +# 注:本段由 armcore.py 原样搬来,行内容与注释未作改动。 + +# ---------------- MEASURED: motor module (local frame: origin = PCBA rear face centre, +Z = output) ---- +M = dict( + pcba_w=63.6, # square PCBA across flats + pcba_t=9.0, # z 0 -> 9.0 + rearplate_d=60.0, # z 5.5 -> 10.5 + hous_d=60.0, # housing OD, z 10.5 -> 52.5 + hous_len=42.0, + cover_d=60.30, # top cover OD (R30.15), z 52.5 -> 56.0 + cover_z=56.0, # cover front face = module total length + flange_z=54.942, # 2nd-stage output flange face + neck_d=36.0, # female neck bore in the cover + neck_depth=1.0, + centre_d=6.5, # centre bore in the flange + centre_depth=4.0, + m2_bcd=29.0, + m2_clock=(8.05, 128.07, 248.06), + m2_tap_d=1.85, # M2 pilot for self-tapping in printed material + m2_depth=6.0, + m2_clear_d=2.75, + m2_cb_d=5.2, +) +# ---------------- MEASURED: face-spline coupling (existing, printed by the user) ------------------------ +C = dict( + n_teeth=12, tooth_r0=19.0, tooth_r1=24.5, tooth_s=2.5, + flange_t=4.50, part_z=5.560, # mirrored stack + hub_r=27.0, + gap=9.00, # cover-face to cover-face when closed + clamp_od_r=30.0, clamp_len=7.80, + lug_r1=37.0, lug_t=4.50, + bolt_r=32.50, bolt_d=3.80, + z0=1.66, z1=9.46, # clamp envelope in the flange frame (motor cover face = 1.060) +) +# axial stations, motor local frame: +COVER_Z = M["cover_z"] # 56.0 +HUB_FACE_Z = COVER_Z + C["gap"] # 65.0 = mating face of the driven hub +CLAMP_R = C["clamp_od_r"] + 0.6 # radial keep-out around the joint axis +LUG_R = C["lug_r1"] + 1.0 # radial keep-out where the bolt lugs are +# ---------------- clearances / print rules ------------------------------------------------------------- +CLR = dict(bore_slip=0.20, pilot=0.10, hole_3=3.4, hole_27=2.8, wall_min=2.0) +FIT = dict(motor_bore_d=M["hous_d"] + 2 * 0.15, cover_cb_d=M["cover_d"] + 2 * 0.35) + +# 电机【轴向长度】公差(用户 2026-10-07 要求:余量偏大稍微减去 1mm,由 2mm 改为 1mm)。 +# 这条链是"后端板螺钉 + 前端压盖螺钉"两个刚性夹持面,螺钉头一旦坐死在沉孔台阶上 +# 就再也不能进给,所以公差没法靠"拧紧"消化,只能靠两件事吃掉(见 arm_build.Joint.pocket +# 与 motor_front_clamp): +# 长出来的 0~+1mm -> 后端板 4xM3 螺钉头下垫 0~1mm 垫片圈(螺钉被垫高 -> 电机被拉回) +# 短下去的 0~-1mm -> 电机尾端面与后端板承压面之间垫 0~1mm 垫片(承压面已按 MOTOR_TOL +# 让空,见 Joint.pocket 的 REAR_SEAT) +# 两边都不动压盖/凸台的几何,关节轴向长度不变。 +MOTOR_TOL = 1.0 diff --git a/arm/dataio/__init__.py b/arm/dataio/__init__.py new file mode 100644 index 0000000..1a438de --- /dev/null +++ b/arm/dataio/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""数据注入与工程图:螺钉实例注入 mesh、四张工程图 SVG 生成。 +""" diff --git a/src/draw_sheet.py b/arm/dataio/draw_sheet.py similarity index 97% rename from src/draw_sheet.py rename to arm/dataio/draw_sheet.py index 93e2539..f95899b 100644 --- a/src/draw_sheet.py +++ b/arm/dataio/draw_sheet.py @@ -17,8 +17,15 @@ import os import sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from armcore import WEB, DOCS +from arm.paths import WEB, DOCS + +# 这两个脚本的顶层就是"算完并写文件",被 import 时会静默重写 travel/mesh 派生产物 +# (实测:一次 importlib 冒烟测试就把 docs/travel.json 重写了一遍)。这里改成直接炸。 +if __name__ != "__main__": + raise RuntimeError( + "draw_sheet 是脚本:顶层代码一执行就重写产物,不能 import。" + "要用就跑 python -m arm.dataio.draw_sheet。") + D = json.load(io.open(WEB + "/mesh_arm.json", encoding="utf-8")) P, LAY, MS = D["parts"], D["layout"], D["measured"] MT, CT = MS["M"], MS["C"] @@ -329,4 +336,3 @@ for n, s in zip(("sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"), io.open(WEB + "/sheets.jsdata", "w", encoding="utf-8").write(json.dumps(sheets, ensure_ascii=False)) print("sheets: %d (%d KB) -> docs/sheet_{front,joint,exploded,side}.svg" % ( len(sheets), sum(len(s) for s in sheets) // 1024)) -os._exit(0) diff --git a/src/screw_mesh.py b/arm/dataio/screw_mesh.py similarity index 95% rename from src/screw_mesh.py rename to arm/dataio/screw_mesh.py index 220812f..180984f 100644 --- a/src/screw_mesh.py +++ b/arm/dataio/screw_mesh.py @@ -38,9 +38,8 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模), 这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。 - -运行:python src/screw_mesh.py -> 注入网格(纯数据,秒级,不动 CAD) - 再 python src/build_viewer.py -> 重新打包单文件查看器 +运行:python -m arm.dataio.screw_mesh -> 注入网格(纯数据,秒级,不动 CAD) + 再 python -m arm.viewer.build_viewer -> 重新打包单文件查看器 arm_model.py 主流程末尾已自动调用本模块,重跑完整构建不会丢失螺钉。 """ import json @@ -52,14 +51,9 @@ try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass - -_HERE = os.path.dirname(os.path.abspath(__file__)) -_ROOT = os.path.dirname(_HERE) -sys.path.insert(0, _HERE) - -import human_layout as HL # noqa: E402 -from part_tools import CLAMP_BCD, CLAMP_CLOCK # noqa: E402 压盖孔位唯一事实源 - +from arm.paths import WEB, DOCS +from arm import kinematics as HL +from arm.cad.part_tools import CLAMP_BCD, CLAMP_CLOCK X_COL = -115.0 # 立柱中心(与 parts/p01_column.py 的 X_COL 一致) Y_COL = HL.Y_AXIS # 128.0 GRP_PAR = {"J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"} @@ -293,11 +287,11 @@ def instances(m2_bcd, m2_clock): # ---------------------------------------------------------------- 注入 def main(): - path = os.path.join(_ROOT, "web", "mesh_arm.json") + path = WEB + "/mesh_arm.json" print("读取 %s ..." % path, flush=True) d = json.load(open(path, encoding="utf-8")) M = d["measured"]["M"] - specs = json.load(open(os.path.join(_ROOT, "docs", "screws.json"), encoding="utf-8")) + specs = json.load(open(DOCS + "/screws.json", encoding="utf-8")) byid = {s["id"]: s for s in specs} d["parts"] = [p for p in d["parts"] if not p.get("screw")] # 幂等:先清旧螺钉 @@ -344,3 +338,10 @@ def main(): if __name__ == "__main__": main() + + # OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是 + # `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口 + # 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139, + # 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。 + sys.stdout.flush() + os._exit(0) diff --git a/src/human_layout.py b/arm/kinematics.py similarity index 100% rename from src/human_layout.py rename to arm/kinematics.py diff --git a/arm/paths.py b/arm/paths.py new file mode 100644 index 0000000..0055562 --- /dev/null +++ b/arm/paths.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""仓库内路径的唯一来源。 + +原先这些常量是硬编码的 G:/Robot_v2 绝对路径,仓库换盘符即全断;现在由本文件 +位置推出。REPO 的父目录就是原来的 ROOT(=G:/Robot_v2),所以外部 STEP 输入的解析 +结果与从前逐字相同。三个 ARM_* 环境变量是临时覆盖入口。 +""" +import os + +_HERE = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/") # .../arm +REPO = os.environ.get("ARM_WORKSPACE") or os.path.dirname(_HERE) +ROOT = os.environ.get("ARM_ROOT") or os.path.dirname(REPO) +MODEL = os.environ.get("ARM_MODEL_ROOT") or (ROOT + r"/Model/Solidworks") +SRC_STEP = MODEL + r"/电机.STEP" +HALF_STEP = MODEL + r"/配件/端面齿半联轴器_相同件x2.STEP" +CLAMP_STEP = MODEL + r"/配件/端面齿联轴器_抱箍_两片装配.STEP" +OUT = REPO + r"/out" +WEB = REPO + r"/web" +DOCS = REPO + r"/docs" +# 可达包络 FK 采样的缓存(ws_cloud.npz / ws_metrics.json)。原先躺在 gitignore 的 +# scratch/ 里,构建却依赖它——缺了新克隆就跑不出 ENV 件。 +CACHE = REPO + r"/data/cache" +# 查看器模板。S4 拆成 webapp/index.html + js/** 后这条即作废。 +TPL = REPO + r"/src/viewer_tpl.html" diff --git a/arm/viewer/__init__.py b/arm/viewer/__init__.py new file mode 100644 index 0000000..80aac06 --- /dev/null +++ b/arm/viewer/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""查看器侧:数据打包、本地静态服务、headless 截图。 +""" diff --git a/src/build_viewer.py b/arm/viewer/build_viewer.py similarity index 88% rename from src/build_viewer.py rename to arm/viewer/build_viewer.py index 0ec057a..07d5850 100644 --- a/src/build_viewer.py +++ b/arm/viewer/build_viewer.py @@ -9,9 +9,7 @@ try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from armcore import WEB, DOCS, WORKSPACE -SRC = os.path.dirname(os.path.abspath(__file__)) +from arm.paths import WEB, DOCS, REPO, TPL import time @@ -36,7 +34,7 @@ def rebuild_viewer(): DOCS + "/screws.json") else "[]" ik_solver = io.open(WEB + "/arm-ik-solver.js", encoding="utf-8").read() ik_glue = io.open(WEB + "/ik-glue.js", encoding="utf-8").read() - tpl = io.open(SRC + "/viewer_tpl.html", encoding="utf-8").read() + tpl = io.open(TPL, encoding="utf-8").read() print(f" 完成 ({len(data)/1024:.0f} KB) ✓", flush=True) print(" - 打包生成全内置单文件 HTML...", end="", flush=True) @@ -56,7 +54,7 @@ def rebuild_viewer(): # 于是只写成功第一份、另外两份静默停在旧版本 —— 表现为"代码改了但网页没变"。 # 实测就是这个坑:18:52 那次 dst 是新的,web/index.html 和根目录那份还停在 18:12。 ok, failed = True, [] - for path in (dst, WEB + "/index.html", WORKSPACE + "/机器人手臂3D查看器.html"): + for path in (dst, WEB + "/index.html", REPO + "/机器人手臂3D查看器.html"): try: io.open(path, "w", encoding="utf-8").write(out) except Exception as e: @@ -68,12 +66,11 @@ def rebuild_viewer(): print("!! 以下副本写入失败(多半是浏览器正打开着该文件,Windows 独占锁):", flush=True) for f in failed: print(" - %s" % f, flush=True) - print(" 关掉对应页面后重跑 python src/build_viewer.py 即可。\n", flush=True) + print(" 关掉对应页面后重跑 python -m arm.viewer.build_viewer 即可。\n", flush=True) else: print(f"-> 3D查看器已更新至: {dst}、{WEB}/index.html 与 根目录/机器人手臂3D查看器.html\n", flush=True) if __name__ == "__main__": rebuild_viewer() - os._exit(0) diff --git a/src/shot.py b/arm/viewer/shot.py similarity index 94% rename from src/shot.py rename to arm/viewer/shot.py index d75b825..0e6fdbb 100644 --- a/src/shot.py +++ b/arm/viewer/shot.py @@ -11,9 +11,7 @@ try: except Exception: pass -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from armcore import ROOT, WORKSPACE, WEB, DOCS - +from arm.paths import WEB, DOCS SHOTS = DOCS + "/shots" HTML = WEB + "/机器人手臂3D查看器.html" CAND = [r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe", @@ -36,6 +34,18 @@ VIEWS = { "user_smooth_harness": "?az=1.50&po=1.45&rd=340&tx=-15&ty=560&tz=-128&xray=0", "user_contour_harness": "?az=1.5708&po=1.50&rd=360&tx=-15&ty=320&tz=-128&xray=0", "02_front": "?cam=front", + + # 工程图 / 装配向导 / 运动模拟三种模式。URL 参数(?view=&sheet=&theme=&guide=)就是 + # 为这几条截图加的:不把它们纳入基线,拆 CSS 与那 700 行工程图 JS 时等于没有关卡。 + "m1_sheet_front": "?view=sheet", + "m2_sheet_joint": "?view=sheet&sheet=1", + "m3_sheet_exploded": "?view=sheet&sheet=2", + "m4_sheet_side": "?view=sheet&sheet=3", + "m5_guide_step1": "?view=sheet&sheet=guide&guide=0", + "m6_guide_step5": "?view=sheet&sheet=guide&guide=4", + "m7_sheet_blueprint": "?view=sheet&theme=blueprint", + "m8_sheet_dark": "?view=sheet&theme=dark", + "m9_motion": "?view=motion", "03_side": "?cam=side", "04_top": "?cam=top", "05_exploded": "?cam=iso&expl=100", @@ -168,4 +178,3 @@ if __name__ == "__main__": print("edge:", EDGE) print("all shots ok:", all(results)) - os._exit(0) diff --git a/docs/shots/harness_p04_p05_xray.png b/docs/shots/harness_p04_p05_xray.png deleted file mode 100644 index d85a02b..0000000 Binary files a/docs/shots/harness_p04_p05_xray.png and /dev/null differ diff --git a/docs/shots/harness_p07_xray.png b/docs/shots/harness_p07_xray.png deleted file mode 100644 index c973099..0000000 Binary files a/docs/shots/harness_p07_xray.png and /dev/null differ diff --git a/docs/shots/harness_xray_internal.png b/docs/shots/harness_xray_internal.png deleted file mode 100644 index 0209c78..0000000 Binary files a/docs/shots/harness_xray_internal.png and /dev/null differ diff --git a/docs/shots/m1_sheet_front.png b/docs/shots/m1_sheet_front.png new file mode 100644 index 0000000..9332b0d Binary files /dev/null and b/docs/shots/m1_sheet_front.png differ diff --git a/docs/shots/m2_sheet_joint.png b/docs/shots/m2_sheet_joint.png new file mode 100644 index 0000000..d74dda1 Binary files /dev/null and b/docs/shots/m2_sheet_joint.png differ diff --git a/docs/shots/m3_sheet_exploded.png b/docs/shots/m3_sheet_exploded.png new file mode 100644 index 0000000..ed07ab8 Binary files /dev/null and b/docs/shots/m3_sheet_exploded.png differ diff --git a/docs/shots/m4_sheet_side.png b/docs/shots/m4_sheet_side.png new file mode 100644 index 0000000..33db0b8 Binary files /dev/null and b/docs/shots/m4_sheet_side.png differ diff --git a/docs/shots/m5_guide_step1.png b/docs/shots/m5_guide_step1.png new file mode 100644 index 0000000..1214213 Binary files /dev/null and b/docs/shots/m5_guide_step1.png differ diff --git a/docs/shots/m6_guide_step5.png b/docs/shots/m6_guide_step5.png new file mode 100644 index 0000000..bdf14af Binary files /dev/null and b/docs/shots/m6_guide_step5.png differ diff --git a/docs/shots/m7_sheet_blueprint.png b/docs/shots/m7_sheet_blueprint.png new file mode 100644 index 0000000..5074fcb Binary files /dev/null and b/docs/shots/m7_sheet_blueprint.png differ diff --git a/docs/shots/m8_sheet_dark.png b/docs/shots/m8_sheet_dark.png new file mode 100644 index 0000000..2b528e4 Binary files /dev/null and b/docs/shots/m8_sheet_dark.png differ diff --git a/docs/shots/m9_motion.png b/docs/shots/m9_motion.png new file mode 100644 index 0000000..c97c0e9 Binary files /dev/null and b/docs/shots/m9_motion.png differ diff --git a/src/viewer_tpl.html b/src/viewer_tpl.html index caf7d5f..61d508b 100644 --- a/src/viewer_tpl.html +++ b/src/viewer_tpl.html @@ -2933,6 +2933,21 @@ requestAnimationFrame(loop); }); } if (q.get('harn') === '0') { const b = document.getElementById('bHarn'); if (b && b.classList.contains('on')) b.onclick(); } + /* 视图模式与工程图状态也要能用 URL 驱动:否则工程图/装配向导这两种模式(约占一半 + CSS 与 700 行 JS)没法用 headless 截图回归,改动只能靠人眼抽查。 */ + if (q.has('view')) { + const t = document.querySelector('.tab[data-view="' + q.get('view') + '"]'); + if (t) t.click(); + } + if (q.has('sheet')) { + const b = document.querySelector('#sheetTabs .sheet-tab[data-sheet="' + q.get('sheet') + '"]'); + if (b) b.click(); + } + if (q.has('theme')) { + const b = document.querySelector('.sheet-btn[data-theme="' + q.get('theme') + '"]'); + if (b) b.click(); + } + if (q.has('guide')) { const g = +q.get('guide'); if (isFinite(g)) renderGuideStep(g); } if (q.has('j6')) { const el = document.getElementById('sJ6'); if (el) { el.value = q.get('j6'); el.dispatchEvent(new Event('input')); } } if (q.has('hide')) { diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..876d195 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +"""核验脚本与基线。跑法:从仓根 `python -m tests.test_harness_collision` +(`-m` 把仓根放进 sys.path,脚本因此能 import arm.*;直接 python tests/xxx.py 会找不到包)。 +""" diff --git a/tests/baselines/flaky/harness_p04_p05_xray.png b/tests/baselines/flaky/harness_p04_p05_xray.png new file mode 100644 index 0000000..a242dd2 Binary files /dev/null and b/tests/baselines/flaky/harness_p04_p05_xray.png differ diff --git a/tests/baselines/flaky/harness_p07_xray.png b/tests/baselines/flaky/harness_p07_xray.png new file mode 100644 index 0000000..398ec36 Binary files /dev/null and b/tests/baselines/flaky/harness_p07_xray.png differ diff --git a/tests/baselines/flaky/harness_xray_internal.png b/tests/baselines/flaky/harness_xray_internal.png new file mode 100644 index 0000000..78f2bc0 Binary files /dev/null and b/tests/baselines/flaky/harness_xray_internal.png differ diff --git a/tests/baselines/flaky/thresholds.json b/tests/baselines/flaky/thresholds.json new file mode 100644 index 0000000..1010c60 --- /dev/null +++ b/tests/baselines/flaky/thresholds.json @@ -0,0 +1,19 @@ +{ + "note": "实测同一构建两轮:mean<=0.00174, 单像素最大差 12, 无像素差>8。阈值取约 10x 余量;超过即视为真实渲染回归。", + "mean_max": 0.02, + "pixel_max": 32, + "views": { + "harness_xray_internal": { + "mean": 0.00087, + "mx": 6 + }, + "harness_p07_xray": { + "mean": 0.00097, + "mx": 5 + }, + "harness_p04_p05_xray": { + "mean": 0.00174, + "mx": 12 + } + } +} \ No newline at end of file diff --git a/tests/baselines/shots.md5 b/tests/baselines/shots.md5 deleted file mode 100644 index e382bbd..0000000 --- a/tests/baselines/shots.md5 +++ /dev/null @@ -1,228 +0,0 @@ -af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/01_iso.png -c315879cfd09f35efa4ca4ecc1564b65 *docs/shots/02_front.png -b3157f4453cf96b930f065e134a2d18b *docs/shots/03_side.png -068fc4bc3eec481ff7d7b1d4da2aad88 *docs/shots/04_top.png -fbb95ea7a9229195d80c6f487021ae2b *docs/shots/05_exploded.png -cbea750a1b52c5e8d061a9e3f4c981fe *docs/shots/06_wrist.png -bb3dc393e4cd8f2c399c343aba0b3d35 *docs/shots/07_joint.png -72e86ae7743ff7416e247d19556dc657 *docs/shots/08_pose.png -af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/09_sheet.png -af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/10_solid.png -e97dcf60224e2ad968bddf02ea680e57 *docs/shots/11_grip_open.png -8daa2255af050a225609cebbc490d3b9 *docs/shots/11_tool_flange.png -002a1f4a2b65744515f5b54364fcc2a8 *docs/shots/12_elbow_flare.png -b2f1454a6009a8b44ddd41be9efdaff5 *docs/shots/13_forearm_side.png -053a9be5edad6b577bbe902c428aa135 *docs/shots/H2_前后圆角_34.png -6d6feec7d65ea08e1175013fa3691869 *docs/shots/H2_前后圆角_侧视.png -f17fd52c9f2eee907da3c76a80cd8073 *docs/shots/H2_前后圆角_正面.png -9519d95093d835f72dfc008c4febc1a3 *docs/shots/H_正式_34视角.png -cf61473cd470b5111b678f2fa01f8bcc *docs/shots/H_正式_侧视.png -2385a6f605834163eb821a56cfa5be6a *docs/shots/H_正式_整机.png -001e85eca69fc3f1503d41b0a7366706 *docs/shots/P04_H落地_复核.png -351e5b573126d75bc5baf01b55199e5b *docs/shots/P04_前后圆角_复核.png -bac1bfb89650c343b33b680c410259e9 *docs/shots/P04_前脸轮廓_改前改后对比.png -f54e549cd743495e78e45744f3283d47 *docs/shots/P04_尖角_改后.png -3b906449664648c1555e9aadeff83bca *docs/shots/P04_顶部_改前.png -ff4ab3362882cb99895c94801896afe7 *docs/shots/P04_顶部_改后.png -ae0c77ebf8a51a2541cfb903bd34f86c *docs/shots/P05_斜棱R3圆角_实施完成对比.png -aa18505c1201d1490584ced82d16f465 *docs/shots/P05_斜棱圆角_评估对比.png -60a8a174ce455f0539503ac1aa3d58f0 *docs/shots/P07_立柱底端圆角_对比.png -baf85ba3b5c37d6ffb9c0deb6b8eb7d0 *docs/shots/arm_overview_notch_exact.png -07ec81c2fcef975b2b53577f00f5176c *docs/shots/bay_inner_wall.png -e39999d2ef28536f24d7f6178fa9a15c *docs/shots/bay_p04_inside.png -959e4bf08e174d90a1fe03a75a042070 *docs/shots/bay_p06_inside.png -f9d8d58365b4ca693525af5540d39d32 *docs/shots/bay_p07_inside.png -1b07f37e5fed9d82886b9518a3839860 *docs/shots/bay_p08_inside.png -d8955c69a48a064dc395c7c18b9db49c *docs/shots/bay_section_polar.png -90b79bb61cacea2323ca19790d4158b2 *docs/shots/boss_p04.png -dcc54259caf06f796e3dec84496abb12 *docs/shots/boss_p05.png -9f2c134297f2c7a555a6eaca2e74e4de *docs/shots/boss_p06.png -b789564315ca1ad7d6d9d18c96972cba *docs/shots/boss_p07.png -8fdee57313c9ea826dcd881d9c4655c8 *docs/shots/compare_harn_off.png -b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/compare_harn_on.png -1d0cccff978809bb5e1f357d0f45a28a *docs/shots/crop_circle_detail.png -43857f773d225ed4c6425c27f93afc2c *docs/shots/crop_p04_bottom.png -9b2f02d28cc0f40c913ca13daad4c4f4 *docs/shots/crop_p04_bottom2.png -baa4492159c3e4d7eab0a45f1965e640 *docs/shots/crop_p04_bottom_v2.png -f00fbb4fad4c0127cb403d28f91ed557 *docs/shots/crop_p04_side.png -5214b01e26e27d8033c54122c215b77b *docs/shots/crop_p04_side_v2.png -10a610706648c4d3e2dac056eaef6f6d *docs/shots/crop_side_p05.png -7c6c053e0875a0dda616fde588985bd3 *docs/shots/crop_side_p08.png -8bbdcba793f44ccaf9df47a8ac316c60 *docs/shots/crop_user_circle_p05.png -3259a8c112f1d351a1fed11973a02c95 *docs/shots/crop_user_circle_p08.png -a618bec8aa67bb2cc743e923207df914 *docs/shots/crop_user_creases.png -5994293cc45e59afdb907c52e4e2dd01 *docs/shots/crop_user_creases_fixed.png -1f3b3d68bbcf563ba50f79ab74494117 *docs/shots/crop_user_feedback.png -5768c8a960267428b10362662d78fd4e *docs/shots/crop_user_p06_yz_fixed.png -c2b2873686ec704024bdbb178f8d19f4 *docs/shots/fence_all_sections.png -efcb304e1e6d17f93e8fb0802ed429ac *docs/shots/fence_j2_after.png -5a907286d59d7f0584bf0a86cee50e01 *docs/shots/fence_j4_after.png -5a0556bda22250c7f633df0db6603154 *docs/shots/fence_j5_after.png -dd21d8f29d1a0267866b898c75ae6d4f *docs/shots/fence_j6_after.png -3123606bf71f402a9714aad63aa7f179 *docs/shots/full_arm_compare.png -93893ff628702b83137e27fdcf751f60 *docs/shots/harness_3d_viewer.png -af5c9d74bddf7d795bd38f4371fa61ec *docs/shots/harness_full_iso.png -2c8ecd1eee5204b6cf6c4af3b3c5f62e *docs/shots/harness_full_view.png -b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/harness_inner_front.png -4087f9fa75d2ef805457b1e32689785d *docs/shots/harness_inner_side.png -ae26c478855955748222592591ee6a03 *docs/shots/harness_notch_fixed.png -8896124b0c45d9796e983f026134cff6 *docs/shots/harness_overview.png -7a40f9f434e51299deb7ab9a59f8fc81 *docs/shots/harness_overview_xray.png -7cbf2c699501bff3c7ebb4beb7cd34e2 *docs/shots/harness_p04_p05.png -7b7335a79480d04dc3862f51a6377396 *docs/shots/harness_p04_p05_xray.png -687e2184dd3e149939ce6d4214ffc698 *docs/shots/harness_p06_window.png -9fca41ada43f36a338cce31797152c3c *docs/shots/harness_p07_window.png -c8a688690b418e68993d06e7b60e2c05 *docs/shots/harness_p07_xray.png -b8b89700a7a6a38b80aa1ef0d8966195 *docs/shots/harness_toggled_off.png -84fb87bb5ce6cef73866401642ab0f9d *docs/shots/harness_xray_full.png -7d07f7728a5cb0bbb71c787e2b44aeb1 *docs/shots/harness_xray_internal.png -152a3bbf2ebd3531fbde77a1c201c9a0 *docs/shots/harness_xray_motor.png -54d1ba097fdf4213811c1360204fa677 *docs/shots/harness_zoom_elbow.png -09c5d9d5c507a182d4dea5e4cfa238dc *docs/shots/harness_zoom_shoulder.png -7fe0cb0d52c1f7d97bf1c8fd47b22e0c *docs/shots/hollow_p04_iso.png -c55a40d4bc4b4e73a672326fa02d573c *docs/shots/hollow_p04_top.png -8379d580ff746c859587e29e34700d7c *docs/shots/hollow_p06_iso.png -1464e622ec8090b6052ab82f7bcd6b22 *docs/shots/hollow_p07_iso.png -f3b85ea576a6aa99a61953c4ff2a5ff4 *docs/shots/hollow_p07_top.png -e169359e86a61f7ab80d4adea0231436 *docs/shots/j3_前端压盖_全件.png -83325d3ca1b9e56fd2c70661052fd063 *docs/shots/j3_前端压盖_装配.png -8766806e981e84aaafd880af49e6bb0f *docs/shots/j4_neg80_view.png -7797c4490780e1d164a7784fc062ba32 *docs/shots/j4_pos80_view.png -26f3fd913018e16e99af2edd861342b8 *docs/shots/j4_side_neg80.png -9f411a6efadfa2fd38aa929beaaed0f0 *docs/shots/j4_side_pos80.png -cfd861e3c300b6b91f84f40bb9753d51 *docs/shots/lingxi_6DUUEF.png -82e86303cb609fa371600c0fb5f9b85a *docs/shots/lingxi_6DUUEG.png -e6616300fa1a80f4316673f733b43167 *docs/shots/lingxi_6DUUEJ.png -515d312c6cf3d2b420d19eda7a380fbe *docs/shots/m2_front_notch_exact.png -f7ad1083e20c91ef1dcf2eeb37e0f908 *docs/shots/m2_front_view.png -c84927682b860cfe7d9c2ec2ffc21305 *docs/shots/m2_iso_notch_exact.png -9bb20af2e14c64203e814423e65efc75 *docs/shots/m2_notch_zoom.png -369479ad05476793ce824fa6a58bca98 *docs/shots/m2_side_view.png -38536cef76f4e29825705a5d3bf8bfbc *docs/shots/m3_front_assembled_after.png -1c221106e26cdf4d85dd6d432483704f *docs/shots/m3_front_iso_after.png -a9182da4a194b3316ba710e5e22b4299 *docs/shots/m3_front_nomotor_after.png -88eadfa42ec551a96b52a58468bbe5ac *docs/shots/m3_front_open_after.png -e8acef55ca4305e09dbda01de7d8af51 *docs/shots/m3_front_side_after.png -c22cef65ca2a74b1332ecaba6b9a484a *docs/shots/p03_light_iso.png -ebdeb9a180e95afd38f5a42d12743579 *docs/shots/p03_light_join.png -5671bcc5a150d4899d186abd3658ef99 *docs/shots/p03_light_rear.png -2a9e076fa310b0c78b1a0e8375e8aae8 *docs/shots/p03_light_side.png -0aa155c87743426668114d876478582c *docs/shots/p03_light_xray.png -d0c82d07ab6055c1b67c03c126fe267a *docs/shots/p03_mount_iso.png -21db2e9ca69192cce0be4467b4701028 *docs/shots/p03_mount_loose.png -78f62639acb79e30fa88cc5fda87cfea *docs/shots/p03_mount_top.png -ae0305c092707e0c73944e1d889d076a *docs/shots/p04_both_ends.png -f241acda8fdae460eb38f5a054e841f9 *docs/shots/p04_m2_side.png -f9a0e5fda588e5345e32d112c3dbc116 *docs/shots/p04_top_after.png -2b07a995cfadea3396cbf1b9bcdbe3fb *docs/shots/p04_top_iso.png -8191b972269eab6e51b40b5771e7d3d5 *docs/shots/p05_after_groove_removed.png -fcbf993fb18f6e7007cb66fea153cdf2 *docs/shots/p05_corner_zoom.png -3d261042388c8b2924848d8a29f71931 *docs/shots/p05_flank_view.png -c7bbe31a5e94574fbaaf495622f999ff *docs/shots/p05_focus.png -9fcdb197a69d94949660b42032db1fda *docs/shots/p05_inner_corner_1.png -665709a81624ee17b8565df8ed1947da *docs/shots/p05_inner_corner_2.png -597ffa69b9817823c1958822482ae9d1 *docs/shots/p05_inner_corner_side.png -2efeeb47868cb57fab926aebe980bd5b *docs/shots/p05_iso_clean.png -9e930e7d033b8b4dfe8a486feb9b8eff *docs/shots/p05_iso_target.png -515b204e42d75d98afe0600f8b8b419d *docs/shots/p05_m3_side.png -bfe508bc1f44430bb3a6d43811962226 *docs/shots/p05_side.png -0e6931d83143d767ba37fa76d12aeb41 *docs/shots/p05_side_target.png -38794de6a818f31efade797afff934f3 *docs/shots/p05_zoom_v3.png -3b82ff785a29a5b1a4233404f62e3106 *docs/shots/p06_iso_v3.png -bc99a68fa6ebcc21c173188a13805834 *docs/shots/p06_iso_v4.png -4c6014a754e233411f6e89e4112d6cde *docs/shots/p06_m4_side.png -cf5939f305762bd064cdfae373c130d4 *docs/shots/p07_fillet_zoom.png -aa3cb2e1e7bff6e3ca4af66bd6c70854 *docs/shots/p07_m5_side.png -c6a061335ea757064ecf51195b89f808 *docs/shots/preview_p06_closeup.png -c712e7385b2094bc349d06f3ff1973eb *docs/shots/preview_p06_front.png -966427d5bba294035b3e6f15b0984e60 *docs/shots/preview_p06_iso.png -16e9638ec903aef101594f6cd0224a40 *docs/shots/preview_p06_yz_side.png -7635f1d057b06f014c1683261a7237b8 *docs/shots/shot_p08_user.png -20b4517e84e161abce960994380a92c6 *docs/shots/shot_service_loop_bent.png -468704c6678e540f54d3165768713991 *docs/shots/shot_service_loop_zero.png -d6fba250cab75489c4ccd44b245ce02d *docs/shots/shot_service_loop_zoom.png -977bb781966f2b94894a2fb99cc4ed69 *docs/shots/side_full_arm.png -cd787443c8c8c4be47d24f3e4c0b0e33 *docs/shots/side_full_arm_v4.png -5de5466396a959ce13f494da0d50298d *docs/shots/side_v3.png -aed480014d24fcd45bc3eafd30b0376c *docs/shots/side_zoom_p05.png -aed480014d24fcd45bc3eafd30b0376c *docs/shots/side_zoom_p05_v4.png -4e3374649995691b3556f3c86fca5904 *docs/shots/side_zoom_p08.png -4e3374649995691b3556f3c86fca5904 *docs/shots/side_zoom_p08_v4.png -f1e5bd79e6921b39d0785e4208ef5cca *docs/shots/svg_exploded.png -57ac93ac3d2be849772d51ba8300a989 *docs/shots/svg_front.png -eadcf7d6ac9853950496a559dbd8de72 *docs/shots/svg_joint.png -4a9a34e534496cb2c3be499ea2029d87 *docs/shots/svg_side.png -2c8ecd1eee5204b6cf6c4af3b3c5f62e *docs/shots/test.png -9324b57f4bfb601a6cd6606f74c0c856 *docs/shots/test_p05_fillet_after.png -9e0c65c8e53adab6305cc1b03dd3c038 *docs/shots/test_p05_view.png -a9b48aa3507d451d8e9159699cfeb6be *docs/shots/test_side_0.png -96a62a6d55b26a30a5001952a8541ed8 *docs/shots/test_side_neg_y.png -7458cdfb12232a2566142ddc6bb76424 *docs/shots/test_side_pi.png -96635ca94ec00783d84c9a3f54fb91c0 *docs/shots/test_side_pos_y.png -4434ed29679c4fdba76782b40d71c925 *docs/shots/test_side_x_neg.png -02a5cd63659b58b6c4f64782ca9f61e4 *docs/shots/test_side_x_pos.png -67a351369c1bb2e1491eb2dadc5efc00 *docs/shots/test_target_corner.png -b7aeadaf3603105f3a47780490ad0342 *docs/shots/test_user_view.png -d3bf901601ff221ecd1b09dc93dd2ccc *docs/shots/tie_after_close.png -7c2be3a8488997de588e3760f966fd7d *docs/shots/tie_after_negY.png -0b3e26c6e2fd664f91764d26b8b1ed0c *docs/shots/tie_after_oblique.png -c39cee9deabedb4dc8fad8ec76b439e5 *docs/shots/tie_after_p0607.png -b51a4e5424297c9012cc0562bbeec296 *docs/shots/tie_after_p0607_obl.png -70772a99b8c5d81619127eed7bc1dab5 *docs/shots/tie_probe_negY.png -a3bf9bd7fb796283bbb9767cc790167a *docs/shots/tie_probe_posY.png -1e1d29129833b35e43b925b25eae4b8f *docs/shots/user_circle_check.png -309cc9ac3238e747104e6ceee7bd68bc *docs/shots/user_compare_view.png -d25a25539d00674d608dca7234e331e0 *docs/shots/user_contour_harness.png -3ed51a9cbad23f7c9c7eb79ac5471eb1 *docs/shots/user_elbow_front.png -85d13ea862263aa055924df2beca98fb *docs/shots/user_elbow_rear.png -9c2bc97ed4ca490e624695c5e4086f8c *docs/shots/user_exact_clean.png -6e1c039c236d28a4bec7e7b589fb2ed3 *docs/shots/user_exact_view.png -6a86499dd36b2a0a1ac50ae28ea138e4 *docs/shots/user_fork_fixed.png -527d30d68065fd9e59020f8234998c61 *docs/shots/user_harness_full.png -d35c720c416b645b0560296f1062d878 *docs/shots/user_harness_notches.png -1389d052730b4bb09eb0a17cf9c49354 *docs/shots/user_harness_view.png -eebfbfbea1375770fbc333f83f64e959 *docs/shots/user_harness_zoom.png -ffe701c93ce7984d125cff99a10ed319 *docs/shots/user_iso_after.png -d3234ff06df60213391dab75589d65ab *docs/shots/user_m1_bypass_oblique.png -99188cb1c040a2c6494322a9845ac6cb *docs/shots/user_m1_exact.png -606e209a6d3dc64ee3a59d1fc22339f3 *docs/shots/user_m1_rear.png -d34d5d15434bc5a980e34e5511cc49a4 *docs/shots/user_p04.png -51d318f3584444ea78f2078d9ef3eddd *docs/shots/user_p04_bottom.png -b31cd81ca26198896378bc38c4393b9d *docs/shots/user_p04_bottom2.png -22ace55c6f60a6322f770b2b0a0a504c *docs/shots/user_p04_iso.png -6ea6b98ad978b993868bd302ad1dd8c6 *docs/shots/user_p04_opp.png -2c7f53c7bed7d25175948f09960ed634 *docs/shots/user_p04_side.png -20e8583cae4d1705bcecdb750ac90b3b *docs/shots/user_p06_fixed.png -2b1117068f7dc20cb24054cfea6d3453 *docs/shots/user_p06_front.png -fe0b30e16a673beb200224a0e6607faa *docs/shots/user_p06_iso.png -291d238a586ded723221e2f9ed49e7e3 *docs/shots/user_p06_j4_pitch_neg.png -4798f251b423398acb0975ecdff88aa6 *docs/shots/user_p06_j4_pitch_pos.png -5326a55ce2be7e15e78272cbdf5c6574 *docs/shots/user_p06_p07_match.png -c8448c391e21b12831c09e82fab9da24 *docs/shots/user_p06_side.png -729025864df7708c900e110ec175f0da *docs/shots/user_p06_xz.png -095da56acdc9b512709786df26691481 *docs/shots/user_p06_yz.png -152eb1db302b62238f3051b1f48fcf45 *docs/shots/user_p07_check.png -abf7113f27b6443541e3a0859d77b3fe *docs/shots/user_p07_creases.png -a17303b12669be77a97dc18f2463c809 *docs/shots/user_p07_wrist.png -a60f07bb05bba7a58bdd29e5202c478f *docs/shots/user_shoulder_solid.png -86aab5a8eb32216b4f712c730cc67068 *docs/shots/user_shoulder_top.png -a60f07bb05bba7a58bdd29e5202c478f *docs/shots/user_shoulder_xray.png -227a85ecdb2fd21b1d042f84bf3aa94c *docs/shots/user_side_full_v5.png -02cfa0aa5681fec061f73b9d69fef41c *docs/shots/user_smooth_harness.png -87a8889ba4031cdf2e378fca7e1b475c *docs/shots/user_target.png -22f62024f51ee1827f662d8754019f57 *docs/shots/user_view1.png -e96c715833f9321b9fd1501bc4bdf87a *docs/shots/user_view2.png -8513509bd468d4253fff41a769fba112 *docs/shots/user_view_after.png -f800bcb2c675c540da91ea4128626558 *docs/shots/verify_front.png -04dcb70b0420dd9b626970e84c6c6009 *docs/shots/verify_front_no_p04.png -f800bcb2c675c540da91ea4128626558 *docs/shots/verify_front_xray.png -6d08599e6af8098899be6f4e04cd587f *docs/shots/verify_rear.png -467521f6aed235e4b7e018291d7a9273 *docs/shots/view_a.png -400e33d3f92bdc5d35030bc0560793f2 *docs/shots/view_b.png -2e70d37bc8bfe39ec1f55956b8c90c31 *docs/shots/view_c.png -9ac5686881ce53db4220af7599bae200 *docs/shots/view_d.png -64a199b2c65e7f2d0ad5e2a80c5caef8 *docs/shots/view_iso_forearm.png -22ae02822c50527ff3aa0b341337add5 *docs/shots/view_iso_zoom.png -1d8277f0b43938423f2ca53b34df0c56 *docs/shots/view_yz_neg_x.png -d51cde4a5e2450c529d1c8897e16cf29 *docs/shots/view_yz_pos_x.png -d1719a9f6fb775fd63cef27c5ff17f54 *docs/shots/zoom_user.png diff --git a/tests/baselines/shots_stable.md5 b/tests/baselines/shots_stable.md5 new file mode 100644 index 0000000..599e1cc --- /dev/null +++ b/tests/baselines/shots_stable.md5 @@ -0,0 +1,104 @@ +af5c9d74bddf7d795bd38f4371fa61ec docs/shots/01_iso.png +d1719a9f6fb775fd63cef27c5ff17f54 docs/shots/zoom_user.png +1389d052730b4bb09eb0a17cf9c49354 docs/shots/user_harness_view.png +eebfbfbea1375770fbc333f83f64e959 docs/shots/user_harness_zoom.png +606e209a6d3dc64ee3a59d1fc22339f3 docs/shots/user_m1_rear.png +99188cb1c040a2c6494322a9845ac6cb docs/shots/user_m1_exact.png +d3234ff06df60213391dab75589d65ab docs/shots/user_m1_bypass_oblique.png +02cfa0aa5681fec061f73b9d69fef41c docs/shots/user_smooth_harness.png +d25a25539d00674d608dca7234e331e0 docs/shots/user_contour_harness.png +c315879cfd09f35efa4ca4ecc1564b65 docs/shots/02_front.png +30ab6da268de109cd087b93b8b175cf9 docs/shots/m1_sheet_front.png +45050caba87c801ab0c29ac939e51e43 docs/shots/m2_sheet_joint.png +615621995924b8bcaf67d79a020cd8d2 docs/shots/m3_sheet_exploded.png +2b1950dda519fac21d5e71d4a9c1f990 docs/shots/m4_sheet_side.png +86098e0fada6f54d9ea3d285b3e0ed6e docs/shots/m5_guide_step1.png +8366cefeca8b935f9fef31bfd2454047 docs/shots/m6_guide_step5.png +40f4663eef6aa61f36e47e1fb1b1ab07 docs/shots/m7_sheet_blueprint.png +2702f107e6152f05a5016b916f38cb89 docs/shots/m8_sheet_dark.png +c84473f41febb608aea0ec2ec12ef317 docs/shots/m9_motion.png +b3157f4453cf96b930f065e134a2d18b docs/shots/03_side.png +068fc4bc3eec481ff7d7b1d4da2aad88 docs/shots/04_top.png +fbb95ea7a9229195d80c6f487021ae2b docs/shots/05_exploded.png +cbea750a1b52c5e8d061a9e3f4c981fe docs/shots/06_wrist.png +bb3dc393e4cd8f2c399c343aba0b3d35 docs/shots/07_joint.png +72e86ae7743ff7416e247d19556dc657 docs/shots/08_pose.png +af5c9d74bddf7d795bd38f4371fa61ec docs/shots/09_sheet.png +af5c9d74bddf7d795bd38f4371fa61ec docs/shots/10_solid.png +8daa2255af050a225609cebbc490d3b9 docs/shots/11_tool_flange.png +002a1f4a2b65744515f5b54364fcc2a8 docs/shots/12_elbow_flare.png +b2f1454a6009a8b44ddd41be9efdaff5 docs/shots/13_forearm_side.png +87a8889ba4031cdf2e378fca7e1b475c docs/shots/user_target.png +d51cde4a5e2450c529d1c8897e16cf29 docs/shots/view_yz_pos_x.png +1d8277f0b43938423f2ca53b34df0c56 docs/shots/view_yz_neg_x.png +64a199b2c65e7f2d0ad5e2a80c5caef8 docs/shots/view_iso_forearm.png +22f62024f51ee1827f662d8754019f57 docs/shots/user_view1.png +e96c715833f9321b9fd1501bc4bdf87a docs/shots/user_view2.png +d34d5d15434bc5a980e34e5511cc49a4 docs/shots/user_p04.png +6ea6b98ad978b993868bd302ad1dd8c6 docs/shots/user_p04_opp.png +85d13ea862263aa055924df2beca98fb docs/shots/user_elbow_rear.png +a17303b12669be77a97dc18f2463c809 docs/shots/user_p07_wrist.png +3ed51a9cbad23f7c9c7eb79ac5471eb1 docs/shots/user_elbow_front.png +729025864df7708c900e110ec175f0da docs/shots/user_p06_xz.png +095da56acdc9b512709786df26691481 docs/shots/user_p06_yz.png +fe0b30e16a673beb200224a0e6607faa docs/shots/user_p06_iso.png +abf7113f27b6443541e3a0859d77b3fe docs/shots/user_p07_creases.png +959e4bf08e174d90a1fe03a75a042070 docs/shots/bay_p06_inside.png +f9d8d58365b4ca693525af5540d39d32 docs/shots/bay_p07_inside.png +1b07f37e5fed9d82886b9518a3839860 docs/shots/bay_p08_inside.png +2c7f53c7bed7d25175948f09960ed634 docs/shots/user_p04_side.png +51d318f3584444ea78f2078d9ef3eddd docs/shots/user_p04_bottom.png +b31cd81ca26198896378bc38c4393b9d docs/shots/user_p04_bottom2.png +a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_solid.png +a60f07bb05bba7a58bdd29e5202c478f docs/shots/user_shoulder_xray.png +86aab5a8eb32216b4f712c730cc67068 docs/shots/user_shoulder_top.png +c22cef65ca2a74b1332ecaba6b9a484a docs/shots/p03_light_iso.png +2a9e076fa310b0c78b1a0e8375e8aae8 docs/shots/p03_light_side.png +5671bcc5a150d4899d186abd3658ef99 docs/shots/p03_light_rear.png +ebdeb9a180e95afd38f5a42d12743579 docs/shots/p03_light_join.png +0aa155c87743426668114d876478582c docs/shots/p03_light_xray.png +c55a40d4bc4b4e73a672326fa02d573c docs/shots/hollow_p04_top.png +7fe0cb0d52c1f7d97bf1c8fd47b22e0c docs/shots/hollow_p04_iso.png +f3b85ea576a6aa99a61953c4ff2a5ff4 docs/shots/hollow_p07_top.png +1464e622ec8090b6052ab82f7bcd6b22 docs/shots/hollow_p07_iso.png +8379d580ff746c859587e29e34700d7c docs/shots/hollow_p06_iso.png +78f62639acb79e30fa88cc5fda87cfea docs/shots/p03_mount_top.png +d0c82d07ab6055c1b67c03c126fe267a docs/shots/p03_mount_iso.png +21db2e9ca69192cce0be4467b4701028 docs/shots/p03_mount_loose.png +83325d3ca1b9e56fd2c70661052fd063 docs/shots/j3_前端压盖_装配.png +e169359e86a61f7ab80d4adea0231436 docs/shots/j3_前端压盖_全件.png +90b79bb61cacea2323ca19790d4158b2 docs/shots/boss_p04.png +6a86499dd36b2a0a1ac50ae28ea138e4 docs/shots/user_fork_fixed.png +e39999d2ef28536f24d7f6178fa9a15c docs/shots/bay_p04_inside.png +959e4bf08e174d90a1fe03a75a042070 docs/shots/bay_p06_inside.png +f9d8d58365b4ca693525af5540d39d32 docs/shots/bay_p07_inside.png +ae0305c092707e0c73944e1d889d076a docs/shots/p04_both_ends.png +f241acda8fdae460eb38f5a054e841f9 docs/shots/p04_m2_side.png +4c6014a754e233411f6e89e4112d6cde docs/shots/p06_m4_side.png +aa3cb2e1e7bff6e3ca4af66bd6c70854 docs/shots/p07_m5_side.png +515b204e42d75d98afe0600f8b8b419d docs/shots/p05_m3_side.png +dcc54259caf06f796e3dec84496abb12 docs/shots/boss_p05.png +309cc9ac3238e747104e6ceee7bd68bc docs/shots/user_compare_view.png +152eb1db302b62238f3051b1f48fcf45 docs/shots/user_p07_check.png +9f2c134297f2c7a555a6eaca2e74e4de docs/shots/boss_p06.png +b789564315ca1ad7d6d9d18c96972cba docs/shots/boss_p07.png +4a9a34e534496cb2c3be499ea2029d87 docs/shots/svg_side.png +eadcf7d6ac9853950496a559dbd8de72 docs/shots/svg_joint.png +f1e5bd79e6921b39d0785e4208ef5cca docs/shots/svg_exploded.png +1e1d29129833b35e43b925b25eae4b8f docs/shots/user_circle_check.png +6e1c039c236d28a4bec7e7b589fb2ed3 docs/shots/user_exact_view.png +7635f1d057b06f014c1683261a7237b8 docs/shots/shot_p08_user.png +3d261042388c8b2924848d8a29f71931 docs/shots/p05_flank_view.png +af5c9d74bddf7d795bd38f4371fa61ec docs/shots/harness_full_iso.png +8896124b0c45d9796e983f026134cff6 docs/shots/harness_overview.png +9fca41ada43f36a338cce31797152c3c docs/shots/harness_p07_window.png +7cbf2c699501bff3c7ebb4beb7cd34e2 docs/shots/harness_p04_p05.png +687e2184dd3e149939ce6d4214ffc698 docs/shots/harness_p06_window.png +84fb87bb5ce6cef73866401642ab0f9d docs/shots/harness_xray_full.png +70772a99b8c5d81619127eed7bc1dab5 docs/shots/tie_probe_negY.png +a3bf9bd7fb796283bbb9767cc790167a docs/shots/tie_probe_posY.png +7c2be3a8488997de588e3760f966fd7d docs/shots/tie_after_negY.png +0b3e26c6e2fd664f91764d26b8b1ed0c docs/shots/tie_after_oblique.png +d3bf901601ff221ecd1b09dc93dd2ccc docs/shots/tie_after_close.png +c39cee9deabedb4dc8fad8ec76b439e5 docs/shots/tie_after_p0607.png +b51a4e5424297c9012cc0562bbeec296 docs/shots/tie_after_p0607_obl.png diff --git a/src/test_harness_collision.py b/tests/test_harness_collision.py similarity index 86% rename from src/test_harness_collision.py rename to tests/test_harness_collision.py index 2b28025..aa703df 100644 --- a/src/test_harness_collision.py +++ b/tests/test_harness_collision.py @@ -44,7 +44,16 @@ def main(): print("【线束三维物理干涉检测】全通道、外侧走线与电机全固态零碰撞验证") print("=" * 80) - with open('src/viewer_tpl.html', 'r', encoding='utf-8') as f: + # 线束通道的锚点坐标写在查看器源码里(现在是模板,拆分后是 webapp/js/harness.js)。 + # 扫候选文件而不是写死一个路径,前端拆分时这条检查不用跟着改;一个都找不到就直接失败, + # 绝不能"没匹配到 = 没有通道 = 零干涉"地假通过。 + from arm.paths import REPO + cands = [REPO + "/src/viewer_tpl.html", REPO + "/webapp/js/harness.js"] + found = [c for c in cands if os.path.exists(c)] + if not found: + print("找不到线束源码,检查项无法执行:", cands, flush=True) + sys.exit(2) + with open(found[0], 'r', encoding='utf-8') as f: tpl = f.read() tube_matches = re.findall(r'makeHarnTube\(\[\s*([\s\S]*?)\s*\]\s*,\s*[\d\.]+\s*,\s*\w+\s*,\s*\'([^\']+)\'', tpl) @@ -100,7 +109,8 @@ def main(): if total_hits == 0: print("🎉 恭喜!线束系统已实现 100% 结构腔体内规范走线与零碰撞干涉!", flush=True) print("=" * 80, flush=True) - import os + sys.stdout.flush() + # 同上:OCC 析构会段错误,所以用 os._exit 带走正确退出码 os._exit(0 if total_hits == 0 else 1) if __name__ == '__main__': diff --git a/tests/verify.py b/tests/verify.py new file mode 100644 index 0000000..96aba77 --- /dev/null +++ b/tests/verify.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +"""重构期的不变量关卡。 + + python -m tests.verify # 数据哈希 + IK 数值(秒级,每步都跑) + python -m tests.verify --shots # 再加全套截图比对(约 3 分钟,前端改动后跑) + +基线在 tests/baselines/ 下,随仓库走。产物按**文件名**匹配,不写死目录: +mesh 从 web/ 搬到 webapp/data/ 这类移动不该让关卡失效,也不该让我以为它通过了。 +任何一项对不上就退出码 1。 +""" +import hashlib +import io +import json +import os +import subprocess +import sys + +sys.stdout.reconfigure(encoding="utf-8") +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(HERE) +BASE = os.path.join(HERE, "baselines") + +# 每个产物允许出现的目录,按优先级;找到第一个即取。 +CANDIDATES = { + "mesh_arm.json": ["webapp/data", "web"], + "sheets.jsdata": ["webapp/data", "web"], + "travel.json": ["docs", "webapp/data"], + "travel_detail.json": ["docs", "webapp/data"], + "collide.json": ["docs", "webapp/data"], + "screws.json": ["docs"], +} + + +def md5(path): + h = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def read_list(name): + """基线文件是 'hash filename';md5sum 的二进制模式会在文件名前加 '*',剥掉。""" + out = {} + for ln in io.open(os.path.join(BASE, name), encoding="utf-8"): + ln = ln.strip() + if ln: + h, f = ln.split() + out[f.lstrip("*")] = h + return out + + +def locate(fname, dirs): + for d in dirs: + p = os.path.join(REPO, d, fname) + if os.path.exists(p): + return p + return None + + +def check_data(): + base = read_list("data_by_name.md5") + bad, rows = 0, [] + for fname, dirs in CANDIDATES.items(): + want = base.get(fname) + if want is None: + rows.append(("SKIP", fname, "基线里没有这项")) + continue + p = locate(fname, dirs) + if p is None: + bad += 1 + rows.append(("MISS", fname, "找不到文件(试过的目录: %s)" % ", ".join(dirs))) + continue + got = md5(p) + ok = got == want + if not ok: + bad += 1 + rows.append(("OK " if ok else "DIFF", fname, + os.path.relpath(p, REPO).replace("\\", "/") + " " + got[:12] + + ("" if ok else " 基线 " + want[:12]))) + for st, f, msg in rows: + print(" [%s] %-20s %s" % (st, f, msg)) + return bad + + +def check_ik(): + r = subprocess.run(["node", os.path.join(HERE, "ik_baseline.mjs"), + "--compare", os.path.join(BASE, "ik_exact.json")], + capture_output=True, text=True, cwd=REPO, + encoding="utf-8", errors="replace") + out = r.stdout or "" + for ln in out.splitlines(): + print(" " + ln) + if r.returncode != 0: + print(" node 退出码 %d: %s" % (r.returncode, (r.stderr or "").strip()[:300])) + # 没有 "== exact" 这一行就说明探针根本没跑出比对结果(编码炸了、node 不在、路径错…), + # 绝不能因为"没报差异"就算通过。 + if "== exact" not in out: + print(" [MISS] ik_baseline 没有输出比对结果,判 FAIL") + return 1 + return 0 if r.returncode == 0 else 1 + + +def check_shots(): + """两层判定,依据是"同一构建连跑两轮"的实测(scratch/_gate/flaky_scan.py): + 104 张逐字节可复现 -> md5 必须相同; + 3 张线束 x-ray 不可复现(mean<=0.0017、无像素差>8,纯抗锯齿噪声) + -> 与 tests/baselines/flaky/ 里的参考像素比,超阈值才算回归。 + 把不可复现的也要求 md5 相同,等于给每一步埋假 FAIL,最后只会被人肉忽略。""" + import numpy as np + from PIL import Image + + base = read_list("shots_stable.md5") + th = json.load(io.open(os.path.join(BASE, "flaky", "thresholds.json"), encoding="utf-8")) + r = subprocess.run([sys.executable, "-m", "arm.viewer.shot"], + capture_output=True, text=True, cwd=REPO, + encoding="utf-8", errors="replace") + out = r.stdout or "" + if "all shots ok" not in out and "OK " not in out: + print(" [MISS] shot.py 没有正常输出(退出码 %s),判 FAIL" % r.returncode) + return 999 + bad = [l for l in out.splitlines() if "FAIL" in l] + if bad: + print(" [MISS] 截图失败 %d 个: %s" % (len(bad), bad[:3])) + + fails = len(bad) + for fname, want in sorted(base.items()): + p = os.path.join(REPO, fname.replace("/", os.sep)) + if not os.path.exists(p): + print(" [MISS] %s" % fname) + fails += 1 + continue + if md5(p) != want: + print(" [DIFF] %-40s" % fname) + fails += 1 + print(" 逐字节比对: %d 张稳定视图,%d 张不一致" % (len(base), fails - len(bad))) + + tol_fails = 0 + for name, ref in sorted(th["views"].items()): + p = os.path.join(REPO, "docs", "shots", name + ".png") + q = os.path.join(BASE, "flaky", name + ".png") + if not (os.path.exists(p) and os.path.exists(q)): + print(" [MISS] 容差比对缺文件: %s" % name) + tol_fails += 1 + continue + d = np.abs(np.asarray(Image.open(p).convert("RGB"), dtype=np.int16) + - np.asarray(Image.open(q).convert("RGB"), dtype=np.int16)) + mean, mx = float(d.mean()), int(d.max()) + over = mean > th["mean_max"] or mx > th["pixel_max"] + tol_fails += 1 if over else 0 + print(" [%s] %-34s mean=%.5f (<= %.2f) 最大单像素差=%d (<= %d)" + % ("DIFF" if over else "OK ", name, mean, th["mean_max"], mx, th["pixel_max"])) + print(" 容差比对: %d 张噪声视图,%d 张超阈值" % (len(th["views"]), tol_fails)) + return fails + tol_fails + + +def main(): + argv = sys.argv[1:] + fails = 0 + print("== 数据产物哈希 ==") + fails += check_data() + print("== IK 数值基线 ==") + fails += check_ik() + if "--shots" in argv: + print("== 截图逐字节比对 ==") + fails += check_shots() + print("RESULT:", "PASS" if fails == 0 else "FAIL(%d 项不一致)" % fails) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/定位图/关卡_工程图模式.png b/定位图/关卡_工程图模式.png new file mode 100644 index 0000000..9332b0d Binary files /dev/null and b/定位图/关卡_工程图模式.png differ diff --git a/定位图/关卡_装配向导模式.png b/定位图/关卡_装配向导模式.png new file mode 100644 index 0000000..1214213 Binary files /dev/null and b/定位图/关卡_装配向导模式.png differ