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 段逐字节一致。
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据注入与工程图:螺钉实例注入 mesh、四张工程图 SVG 生成。
|
||||
"""
|
||||
@@ -0,0 +1,338 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Assembly drawings (SVG) for the arm.
|
||||
|
||||
Round-2 approach, after the first version was rejected: per-part convex hulls FILLED made
|
||||
the whole arm read as one blob and the balloons' leaders crossed the sheet. Now:
|
||||
- parts are stroked only (no fill), so overlaps still read as separate outlines
|
||||
- balloons live in two side columns sorted by projected height, each with a short leader
|
||||
to the nearest point of its own outline (no long crossing leaders, no collisions)
|
||||
- joint axes are drawn as dash-dot centrelines and the pitch chain is dimensioned
|
||||
- the scale is computed from the actual pixels-per-mm, not hard-coded
|
||||
Sheets: 01 总装侧视 02 关节局部放大(A-A) 03 爆炸轴测 04 前视
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
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"]
|
||||
JOINTS = {j["id"]: j for j in D["joints"]}
|
||||
|
||||
|
||||
def hull(pts):
|
||||
pts = sorted(set(pts))
|
||||
if len(pts) < 3:
|
||||
return pts
|
||||
def cr(o, a, b):
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||||
lo = []
|
||||
for p in pts:
|
||||
while len(lo) >= 2 and cr(lo[-2], lo[-1], p) <= 0:
|
||||
lo.pop()
|
||||
lo.append(p)
|
||||
up = []
|
||||
for p in reversed(pts):
|
||||
while len(up) >= 2 and cr(up[-2], up[-1], p) <= 0:
|
||||
up.pop()
|
||||
up.append(p)
|
||||
return lo[:-1] + up[:-1]
|
||||
|
||||
|
||||
def proj(p, kind):
|
||||
x, y, z = p
|
||||
if kind == "side":
|
||||
return (x, z)
|
||||
if kind == "front":
|
||||
return (y, z)
|
||||
if kind == "iso":
|
||||
a, b = math.radians(30), math.radians(35)
|
||||
u = x * math.cos(a) - y * math.sin(a)
|
||||
v = -x * math.sin(a) * math.sin(b) - y * math.cos(a) * math.sin(b) + z * math.cos(b)
|
||||
return (u, v)
|
||||
raise ValueError(kind)
|
||||
|
||||
|
||||
def part_pts(p, kind, ex=0.0):
|
||||
vv = p["v"]
|
||||
step = max(1, len(vv) // 1400)
|
||||
out = []
|
||||
for i in range(0, len(vv), step):
|
||||
q = [vv[i][k] + p["ex"][k] * ex for k in range(3)] if ex else list(vv[i])
|
||||
out.append(proj(q, kind))
|
||||
return out
|
||||
|
||||
|
||||
def nearest(h, pt):
|
||||
return min(h, key=lambda q: (q[0] - pt[0]) ** 2 + (q[1] - pt[1]) ** 2)
|
||||
|
||||
|
||||
def esc(t):
|
||||
return str(t).replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
def frame(W, H, title, note, scale, sheet_id=""):
|
||||
return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d" '
|
||||
'class="assembly-sheet-svg" data-sheet-id="%s" data-sheet-title="%s" data-sheet-note="%s" data-sheet-scale="%s" '
|
||||
'font-family="Microsoft YaHei,sans-serif">' % (W, H, W, H, sheet_id, esc(title), esc(note), scale),
|
||||
'<defs>'
|
||||
'<filter id="partGlow" x="-20%" y="-20%" width="140%" height="140%">'
|
||||
'<feDropShadow dx="0" dy="0" stdDeviation="3.5" flood-color="#2563eb" flood-opacity="0.85"/>'
|
||||
'</filter>'
|
||||
'<filter id="balloonShadow" x="-20%" y="-20%" width="140%" height="140%">'
|
||||
'<feDropShadow dx="0" dy="1.5" stdDeviation="1.5" flood-color="#000" flood-opacity="0.25"/>'
|
||||
'</filter>'
|
||||
'</defs>',
|
||||
'<rect class="sheet-bg" width="%d" height="%d" fill="#fff"/>' % (W, H),
|
||||
'<g class="sheet-border">',
|
||||
'<rect class="sheet-border-outer" x="8" y="8" width="%d" height="%d" fill="none" stroke="#111" stroke-width="1.6"/>' % (W - 16, H - 16),
|
||||
'<rect class="sheet-border-inner" x="14" y="14" width="%d" height="%d" fill="none" stroke="#111" stroke-width=".5"/>' % (W - 28, H - 28),
|
||||
'<line class="sheet-title-line" x1="14" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (H - 44, W - 15, H - 44),
|
||||
'<text class="sheet-title-text" x="24" y="%d" font-size="13" font-weight="bold">%s</text>' % (H - 27, esc(title)),
|
||||
'<text class="sheet-note-text" x="24" y="%d" font-size="9" fill="#444">%s</text>' % (H - 16, esc(note)),
|
||||
'<line class="sheet-meta-line" x1="%d" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (W - 268, H - 44, W - 268, H - 15),
|
||||
'<text class="sheet-scale-text" x="%d" y="%d" font-size="9">比例 1:%s(按图幅适配) 单位 mm</text>' % (W - 260, H - 29, scale),
|
||||
'<text class="sheet-ver-text" x="%d" y="%d" font-size="9">六电机人形机器人手臂 %s / 迭代 %s</text>' % (W - 260, H - 17, LAY["ver"], LAY["it"]),
|
||||
'</g>']
|
||||
|
||||
|
||||
def balloon(x, y, p):
|
||||
idx = p.get("idx", 0)
|
||||
name = esc(p.get("name", ""))
|
||||
cn = esc(p.get("cn", ""))
|
||||
return ('<g class="sheet-balloon" data-idx="%d" data-name="%s" data-cn="%s">'
|
||||
'<circle class="balloon-circle" cx="%.0f" cy="%.0f" r="10" fill="#fff" stroke="#111" stroke-width="1"/>'
|
||||
'<text class="balloon-text" x="%.0f" y="%.0f" font-size="9.5" text-anchor="middle">%d</text>'
|
||||
'</g>' % (idx, name, cn, x, y, x, y + 3.4, idx))
|
||||
|
||||
|
||||
def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend=True, sheet_id=""):
|
||||
shp = []
|
||||
for p in P:
|
||||
if p["name"].startswith("ENV_") or p["name"].startswith("S"):
|
||||
continue
|
||||
h = hull(part_pts(p, kind, ex))
|
||||
if len(h) >= 3:
|
||||
shp.append((p, h))
|
||||
if not shp:
|
||||
return ""
|
||||
allp = [q for _, hs in shp for q in hs]
|
||||
xs, ys = [q[0] for q in allp], [q[1] for q in allp]
|
||||
lo_x, hi_x, lo_y, hi_y = min(xs), max(xs), min(ys), max(ys)
|
||||
ML, MR = 78, (300 if legend else 18)
|
||||
aw, ah = W - ML - MR - 20, H - 2 * 40 - 60
|
||||
if (hi_y - lo_y) > 1.25 * (hi_x - lo_x):
|
||||
aw -= 34.0 # room for the single balloon column
|
||||
sc = min(aw / max(1e-6, hi_x - lo_x), ah / max(1e-6, hi_y - lo_y))
|
||||
ox = ML + (aw - (hi_x - lo_x) * sc) / 2 - lo_x * sc
|
||||
oy = 34 + (ah + (hi_y - lo_y) * sc) / 2 + lo_y * sc
|
||||
T = lambda q: (q[0] * sc + ox, oy - q[1] * sc)
|
||||
out = frame(W, H, title, note, round(1 / sc, 2), sheet_id)
|
||||
|
||||
if axes:
|
||||
out.append('<g class="sheet-axes-group">')
|
||||
for jid in ("J1", "J2", "J3", "J4", "J5"):
|
||||
j = JOINTS.get(jid)
|
||||
if not j:
|
||||
continue
|
||||
a = proj(j["rear"], kind)
|
||||
b = proj([j["rear"][k] + j["d"][k] * 90 for k in range(3)], kind)
|
||||
pa, pb = T(a), T(b)
|
||||
out.append('<line class="sheet-axis" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
'stroke-width=".8" stroke-dasharray="14 3 2 3"/>' % (pa[0], pa[1], pb[0], pb[1]))
|
||||
out.append('<text class="sheet-axis-label" x="%.0f" y="%.0f" font-size="10" fill="#0b57a4">%s</text>'
|
||||
% (pb[0] + 3, pb[1] - 3, jid))
|
||||
out.append('</g>')
|
||||
|
||||
out.append('<g class="sheet-parts-group">')
|
||||
for p, h in shp:
|
||||
d = "M" + " L".join("%.1f %.1f" % T(q) for q in h) + "Z"
|
||||
col = "#%06x" % p["color"]
|
||||
idx = p.get("idx", 0)
|
||||
cn = esc(p.get("cn", ""))
|
||||
name = esc(p.get("name", ""))
|
||||
printed = 1 if p.get("printed") else 0
|
||||
attrs = 'class="sheet-part-path" data-idx="%d" data-name="%s" data-cn="%s" data-color="%s" data-printed="%d"' % (
|
||||
idx, name, cn, col, printed)
|
||||
if p["printed"]:
|
||||
out.append('<path d="%s" fill="none" stroke="%s" stroke-width="1.35" %s/>' % (d, col, attrs))
|
||||
else:
|
||||
out.append('<path d="%s" fill="none" stroke="#9aa1ad" stroke-width=".7" '
|
||||
'stroke-dasharray="4 2" %s/>' % (d, attrs))
|
||||
out.append('</g>')
|
||||
|
||||
# balloons in two side columns.
|
||||
side = []
|
||||
for p, h in shp:
|
||||
cx = sum(T(q)[0] for q in h) / len(h)
|
||||
cy = sum(T(q)[1] for q in h) / len(h)
|
||||
anchor = T(nearest(h, (cx, cy)))
|
||||
side.append((p, anchor, cx))
|
||||
tall = (hi_y - lo_y) > 1.25 * (hi_x - lo_x)
|
||||
right = sorted(side, key=lambda s: s[1][1]) if tall else sorted([s for s in side if s[2] >= ox + (hi_x - lo_x) * sc / 2.0], key=lambda s: s[1][1])
|
||||
left = [] if tall else sorted([s for s in side if s[2] < ox + (hi_x - lo_x) * sc / 2.0],
|
||||
key=lambda s: s[1][1])
|
||||
|
||||
out.append('<g class="sheet-balloons-group">')
|
||||
def col(lst, x):
|
||||
n = max(1, len(lst))
|
||||
span = H - 130
|
||||
for i, (p, t, _cx) in enumerate(lst):
|
||||
by = 52 + span * (i + 0.5) / n
|
||||
out.append('<polyline class="sheet-leader" data-idx="%d" points="%.0f,%.0f %.0f,%.0f %.0f,%.0f" fill="none" '
|
||||
'stroke="#555" stroke-width=".6"/>' % (p["idx"], t[0], t[1], (t[0] + x) / 2, by, x, by))
|
||||
out.append(balloon(x, by, p))
|
||||
col(left, ML - 30)
|
||||
col(right, (ox + (hi_x - lo_x) * sc + 52.0) if tall else (W - MR + 44))
|
||||
out.append('</g>')
|
||||
|
||||
if legend:
|
||||
out.append('<g class="sheet-legend-box">')
|
||||
rows = sorted(side, key=lambda s: s[0]["idx"])
|
||||
half = (len(rows) + 1) // 2
|
||||
tx, ty0 = W - MR + 74, 62
|
||||
out.append('<text x="%d" y="%d" font-size="10" font-weight="bold">零 件 表</text>' % (tx, ty0 - 8))
|
||||
for c in (0, 1):
|
||||
for i, (p, _t, _cx) in enumerate(rows[c * half:(c + 1) * half]):
|
||||
y = ty0 + 12 + i * 12.6
|
||||
nm = esc(p["cn"].replace(" 半联轴器/抱箍", " 联轴器/抱箍").replace("端面齿", "")[:11])
|
||||
out.append('<g class="sheet-legend-row" data-idx="%d" data-name="%s" data-cn="%s">'
|
||||
'<rect class="legend-row-bg" x="%d" y="%.0f" width="102" height="12.5" fill="transparent" rx="2"/>'
|
||||
'<text class="legend-row-idx" x="%d" y="%.0f" font-size="8.5">%d</text>'
|
||||
'<text class="legend-row-name" x="%d" y="%.0f" font-size="8.5">%s</text>'
|
||||
'</g>' % (p["idx"], esc(p.get("name","")), esc(p.get("cn","")),
|
||||
tx + c * 106 - 3, y - 9.5,
|
||||
tx + c * 106, y, p["idx"],
|
||||
tx + c * 106 + 13, y, nm))
|
||||
out.append('</g>')
|
||||
|
||||
if dims:
|
||||
out.append('<g class="sheet-dims-group">')
|
||||
for (a, b, lab, off) in dims:
|
||||
pa, pb = T(proj(a, kind)), T(proj(b, kind))
|
||||
y = (pa[1] + pb[1]) / 2 + off
|
||||
out.append('<line class="sheet-dim-line" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" stroke-width=".9"/>'
|
||||
% (pa[0], y, pb[0], y))
|
||||
for q in (pa, pb):
|
||||
out.append('<line class="sheet-dim-ext" x1="%.0f" y1="%.0f" x2="%.0f" y2="%.0f" stroke="#0b57a4" '
|
||||
'stroke-width=".5" stroke-dasharray="2 2"/>' % (q[0], q[1], q[0], y))
|
||||
out.append('<text class="sheet-dim-text" x="%.0f" y="%.0f" font-size="10" fill="#0b57a4" text-anchor="middle">%s</text>'
|
||||
% ((pa[0] + pb[0]) / 2, y - 3, lab))
|
||||
out.append('</g>')
|
||||
out.append('</svg>')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def joint_detail():
|
||||
"""Sheet A-A: one joint drawn FROM THE MEASURED NUMBERS (not a mesh projection) with every
|
||||
governing dimension called out - this is the sheet you actually build against."""
|
||||
W, H = 1400, 780 # wide enough that the right-hand callouts stay on the sheet
|
||||
s = 6.0
|
||||
cy = 330
|
||||
x0 = 250.0 # motor rear datum
|
||||
x_cover = x0 + MT["cover_z"] * s # cover face = the joint's datum plane
|
||||
x_hub = x_cover + CT["gap"] * s
|
||||
out = frame(W, H, "A-A 回转副局部放大 — 全部尺寸为实测值(按数值绘制,非网格投影)",
|
||||
"电机 → 端面齿半联轴器 A/B(12 齿,盘脸贴死为唯一轴向止动)→ 对开抱箍防脱 → "
|
||||
"从动盘(复刻电机输出面)。蓝色为关节轴线。", "1/6(本图放大 6 倍)", "joint")
|
||||
out.append('<line class="sheet-axis" x1="%d" y1="%d" x2="%d" y2="%d" stroke="#0b57a4" stroke-width="1.1" '
|
||||
'stroke-dasharray="22 5 4 5"/>' % (x0 - 60, cy, x_hub + 190, cy))
|
||||
g = ['<g class="joint-geom-group" stroke="#111" fill="none" stroke-width="1.5">']
|
||||
# motor: housing Ø60 x 42, rear plate, cover Ø60.3 x 3.5
|
||||
g.append('<rect class="joint-geom-part" data-part="电机机座" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x0 + 5.5 * s, cy - 30 * s, 47 * s, 60 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="尾部PCBA" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x0, cy - MT["pcba_w"] / 2.0 * s, MT["pcba_t"] * s, MT["pcba_w"] * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="电机输出端盖" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
|
||||
% (x_cover - 3.5 * s, cy - 30.15 * s, 3.5 * s, 60.3 * s))
|
||||
# coupling halves (4.5 each) and the driven hub
|
||||
g.append('<rect class="joint-geom-part" data-part="半联轴器A" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="半联轴器B" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover + 4.5 * s, cy - 27 * s, 4.5 * s, 54 * s))
|
||||
g.append('<rect class="joint-geom-part" data-part="从动盘" x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_hub, cy - 41 * s, 24 * s, 82 * s))
|
||||
# clamp band over the closed pair
|
||||
g.append('<rect class="joint-geom-part" data-part="对开抱箍" x="%.1f" y="%.1f" width="%.1f" height="%.1f" stroke-dasharray="6 3"/>'
|
||||
% (x_cover + 0.6 * s, cy - 30 * s, 8.4 * s, 60 * s))
|
||||
g.append('</g>')
|
||||
out.extend(g)
|
||||
|
||||
def dim(y, xa, xb, txt, col="#c00", lx=None):
|
||||
out.append('<g class="sheet-dim"><line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>' % (xa, y, xb, y, col))
|
||||
for x in (xa, xb):
|
||||
out.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width=".6"/>' % (x, y - 6, x, y + 6, col))
|
||||
if lx is None:
|
||||
out.append('<text x="%.1f" y="%d" font-size="11" fill="%s" text-anchor="middle">%s</text>'
|
||||
% ((xa + xb) / 2, y - 8, col, txt))
|
||||
else:
|
||||
out.append('<text x="%.1f" y="%d" font-size="11" fill="%s" text-anchor="end">%s</text>'
|
||||
% (lx, y - 8, col, txt))
|
||||
out.append('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width=".6"/>'
|
||||
% (lx + 4, y - 11, xa, y, col))
|
||||
out.append('</g>')
|
||||
dim(cy - 240, x0, x_cover, "整机长 %.1f" % MT["cover_z"])
|
||||
dim(cy - 215, x_cover, x_hub, "两盖板面跨距 %.2f" % CT["gap"], lx=620)
|
||||
dim(cy + 250, x0, x_hub, "电机尾基准 → 从动盘贴合面 = %.3f ← 全臂节距的唯一来源" % MS["HUB_FACE_Z"])
|
||||
dim(cy + 215, x_cover + 0.6 * s, x_cover + 9.0 * s, "抱箍轴向 %.3f~%.3f" % tuple(MS["CLAMP_SPAN"]), lx=470)
|
||||
|
||||
def note(x, y, tx, ty, txt, anchor="start"):
|
||||
out.append('<g class="sheet-note"><polyline points="%.1f,%.1f %.1f,%.1f %.1f,%.1f" fill="none" stroke="#555" stroke-width=".7"/>'
|
||||
% (x, y, x, ty, tx, ty))
|
||||
out.append('<text x="%.1f" y="%.1f" font-size="10.5" text-anchor="%s">%s</text></g>'
|
||||
% (tx + (3 if anchor == "start" else -3), ty + 3.5, anchor, txt))
|
||||
note(x0 + 28 * s, cy - 30 * s, 235, cy - 190, "机座 Ø%.0f x %.0f,滑配孔按 Ø%.1f"
|
||||
% (MT["hous_d"], MT["hous_len"], 60.3), "end")
|
||||
note(x0 + 4 * s, cy - 31.8 * s, 235, cy - 160, "尾部 PCBA 方 %.1f → 口袋 %.1f"
|
||||
% (MT["pcba_w"], MT["pcba_w"] + 0.8), "end")
|
||||
note(x_cover + 2.2 * s, cy - 27 * s, 900, cy - 200, "半联轴器 A(12 齿端面花键 R%.1f~%.1f,齿高 %.1f)"
|
||||
% (CT["tooth_r0"], CT["tooth_r1"], CT["tooth_s"]))
|
||||
note(x_cover + 6.7 * s, cy + 27 * s, 900, cy - 170, "半联轴器 B = A 绕局部 X 转 180° 后沿轴移 2xPART_Z")
|
||||
note(x_hub + 12 * s, cy + 41 * s, 900, cy + 150, "从动盘:Ø%.0fx%.1f 颈孔 + Ø%.1fx5.0 中心孔 + 3xM2 @Ø%.0f"
|
||||
% (MT["neck_d"], MT["neck_depth"], MT["centre_d"], MT["m2_bcd"]))
|
||||
note(x_hub, cy + 30.5 * s, 900, cy + 178, "盘脸贴死 = 唯一轴向止动;齿只传扭,不定位")
|
||||
note(x_hub + 0.5 * s, cy - 20 * s, 900, cy + 206,
|
||||
"从动面卸荷:Ø54.6 x 0.35(B 半件背面实测越过贴合面 0.06)+ Ø36 x 1.70 导柱孔")
|
||||
out.append('<text x="60" y="%d" font-size="10.5">M2 相位(时钟):电机侧 %s ;从动盘侧取镜像 %s</text>'
|
||||
% (H - 62, "/".join("%.1f" % a for a in MT["m2_clock"]),
|
||||
"/".join("%.1f" % a for a in [(360 - a) % 360 for a in MT["m2_clock"]])))
|
||||
out.append('<text x="60" y="%d" font-size="10.5">抱箍:带宽 Ø%.0f,凸耳 R%.1f(位于关节局部 ±X,包壳在此处开口);M3 x2 @ R%.1f</text>'
|
||||
% (H - 48, CT["clamp_od_r"] * 2, CT["lug_r1"], CT["bolt_r"]))
|
||||
out.append('</svg>')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
j2 = JOINTS["J2"]["rear"]
|
||||
j4 = JOINTS["J4"]["rear"]
|
||||
j5 = JOINTS["J5"]["rear"]
|
||||
SH, EL, WR = (j2[1], j2[2]), (j4[1], j4[2]), (j5[1], j5[2])
|
||||
DIMS_ARM = [((0, SH[0], SH[1]), (0, EL[0], EL[1]), "上臂 %.0f" % LAY["L1"], 74),
|
||||
((0, EL[0], EL[1]), (0, WR[0], WR[1]), "前臂 %.0f" % LAY["L2"], 112),
|
||||
((0, 0, 0), (0, WR[0], WR[1]), "腕轴离地 %.0f" % WR[1], -128)]
|
||||
|
||||
sheets = [
|
||||
build_sheet("front", "总装图 - 正视(沿 -X 投影;肢体在纸面内,显示左右交替堆叠)", 1200, 800, 0.0,
|
||||
"实线=打印件,虚线=电机/已有联轴器;点划线为关节轴线;蓝色为节距链。"
|
||||
"俯仰电机一内一外交替,全部贴合面共用 Y=157 一个平面", True, DIMS_ARM, sheet_id="front"),
|
||||
joint_detail(),
|
||||
build_sheet("iso", "爆炸装配图(轴测)", 1200, 800, 1.0,
|
||||
"爆炸量 100%:每个关节沿肢体方向逐级错开一节,电机与端面齿组件再沿各自轴线抽出 —— 自上而下即装配顺序", False, None, sheet_id="exploded"),
|
||||
build_sheet("side", "总装图 - 侧视(沿 +Y 投影;俯仰轴垂直纸面)", 900, 800, 0.0,
|
||||
"肩抬举(J1)轴沿 X 前后布置,与俯仰轴平行偏置 92 mm;"
|
||||
"肩吊臂侧板在 |x| ≥ 64,肢体包络 |x| ≤ 60.5,故整周不碰", True, None, sheet_id="side"),
|
||||
]
|
||||
os.makedirs(DOCS, exist_ok=True)
|
||||
for n, s in zip(("sheet_front", "sheet_joint", "sheet_exploded", "sheet_side"), sheets):
|
||||
io.open(DOCS + "/" + n + ".svg", "w", encoding="utf-8").write(s)
|
||||
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))
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""紧固件(螺钉)实例化网格生成器。
|
||||
|
||||
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
|
||||
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
|
||||
随动关节组),生成轻量三角网格后追加进 web/mesh_arm.json 的 parts 中。
|
||||
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
|
||||
|
||||
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
||||
头面 = 沉孔/通道的"承接台阶"(螺钉从外侧塞入,一直顶到孔径变小的那一圈台阶面),
|
||||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外(S05 除外:搭接壁太薄不能开沉孔)。
|
||||
S01 J1~J6 输出毂 3xM2:joint.L() 局部 (r=m2_bcd/2, HUB_M2_CLOCK 相位),
|
||||
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
|
||||
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
|
||||
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (Ø6.5 圆柱沉孔台阶 x=-56.5,法兰外端面 -61,
|
||||
圆柱头沉入 1.5mm)
|
||||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 60.5)
|
||||
(Ø6.0×2 圆柱沉孔在 Ø10 凸台内,台阶 x=9.5,M3x8 圆柱头露 1mm,旋入电机前端面咬合 3.5mm)
|
||||
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
|
||||
前后各拧一颗,头在 y=95 / y=161 外露(搭接壁薄不开沉孔),直接拧入自攻成型
|
||||
S06 P09 工具法兰 4xM4:R22 @0/90/180/270°(BCD44,原 R20 会与 S01 的 Ø5.6 通道咬穿沉孔),台阶 z=97(Ø7.0×4 圆柱沉孔 + Ø4.4 贯穿通孔,法兰底面 z=93,
|
||||
M4 圆柱头与法兰底面齐平)
|
||||
S07 P02 中核心 4xM5 :x∈{-135,-99}, y∈{110,146},台阶 z=6(Ø10 沉孔深 6mm,头沉入 1mm)
|
||||
S08 P02 力臂 4xM5 :(-10,76/180) + (-185,88/168),台阶 z=17(Ø10 沉孔深 5mm,头与顶面齐平)
|
||||
S09 P03 安装台 4xM4x16:x∈{-142.5,-87.5}, y∈{114.5,141.5},台阶 z=479(P03 鞍座顶面,
|
||||
从腔体正上方往下拧,Ø3.9 直孔穿过鞍座 9mm 再吃进 P01_C 顶环 7mm,自攻成型)
|
||||
S11 P03 横向防脱锁紧 1xM4x16:x=-148(立柱 −X 外端面),y=128,z=463,头朝 -X,
|
||||
沿 +X Ø3.9 直孔穿 11mm 后壁 + 榫头同径,直接拧入自攻(榫头内咬合 4.7mm)
|
||||
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
|
||||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||||
S12 电机前端压盖 4xM3x8:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) / J5(P07 夹 M5)
|
||||
/ J6(P08 夹 M6) 各 4 颗。
|
||||
配方唯一事实源 = part_tools.motor_front_clamp(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔
|
||||
深 3.0、沉孔台阶 = 前端盖外端面 +2.0mm、压盖背面 0.0 面压);J3 是同一套配方的 Z 轴版本
|
||||
(台阶落在 Z=(z_r-56)-2=387),半径/相位直接取 arm_model 的 CLAMP_BCD/CLAMP_CLOCK。
|
||||
孔位必须显式算全局 (x,z)/(x,y) 而不能用 to_global,原因见 instances() 内的注释。
|
||||
|
||||
长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模),
|
||||
这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。
|
||||
运行:python -m arm.dataio.screw_mesh -> 注入网格(纯数据,秒级,不动 CAD)
|
||||
再 python -m arm.viewer.build_viewer -> 重新打包单文件查看器
|
||||
arm_model.py 主流程末尾已自动调用本模块,重跑完整构建不会丢失螺钉。
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
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"}
|
||||
DRIVEN = {"J1": "P04 肩叉", "J2": "P05 大臂旋转座", "J3": "P06 大臂骨架",
|
||||
"J4": "P07 小臂骨架", "J5": "P08 腕偏转座", "J6": "P09 工具法兰"}
|
||||
PROX = {"J2": "P04 肩叉", "J3": "P05 大臂旋转座", "J4": "P06 大臂骨架",
|
||||
"J5": "P07 小臂骨架", "J6": "P08 腕偏转座"}
|
||||
COLOR = 0xc3cad6 # 不锈钢冷色
|
||||
LMAX = {"S08": 17.0, "S10": 22.0} # 可见段截断:螺杆伸到底座底面(z=0)即止(台面/预埋螺母未建模)
|
||||
|
||||
# 电机尾端 4x M3 螺纹径向槽的实测孔位(电机.STEP):BCD55、相位 39.9°+90k,
|
||||
# 槽宽 2.5、径向可调约 ±1.25mm。S02(P04~P08 后端板)与 S03(P03 后法兰)共用;
|
||||
# 旧 35.1×40.9 矩形孔距对角两颗偏出螺纹槽 ~4.7mm(2026-10-02 用户指出"螺钉没有对齐")。
|
||||
REAR_SLOTS = tuple((27.5 * math.cos(math.radians(39.65 + 90.0 * k)),
|
||||
27.5 * math.sin(math.radians(39.65 + 90.0 * k))) for k in range(4))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 局部坐标系工具
|
||||
def _norm(v):
|
||||
m = math.sqrt(sum(c * c for c in v)) or 1.0
|
||||
return tuple(c / m for c in v)
|
||||
|
||||
|
||||
def _cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def _dot(a, b):
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def frame(tag):
|
||||
"""电机局部系 (u,v,w) -> 全局:与 armcore.trs 一致(局部 +Z -> d,+X -> xdir,y = z × x)。"""
|
||||
a = HL.AXIS[tag]
|
||||
rear, d, xdir = tuple(a["rear"]), _norm(a["d"]), a["xdir"]
|
||||
x = _norm(tuple(xdir[c] - _dot(xdir, d) * d[c] for c in range(3)))
|
||||
y = _cross(d, x)
|
||||
return rear, d, x, y
|
||||
|
||||
|
||||
def to_global(tag, u, v, w):
|
||||
rear, d, x, y = frame(tag)
|
||||
return tuple(rear[c] + u * x[c] + v * y[c] + w * d[c] for c in range(3))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 网格基元(局部 +Z = 拧入方向)
|
||||
def _side(r0, r1, z0, z1, n):
|
||||
"""圆台侧面(外向缠绕)。"""
|
||||
V, T = [], []
|
||||
for i in range(n):
|
||||
a0 = 2.0 * math.pi * i / n
|
||||
a1 = 2.0 * math.pi * (i + 1) / n
|
||||
V += [(r0 * math.cos(a0), r0 * math.sin(a0), z0), (r0 * math.cos(a1), r0 * math.sin(a1), z0),
|
||||
(r1 * math.cos(a1), r1 * math.sin(a1), z1), (r1 * math.cos(a0), r1 * math.sin(a0), z1)]
|
||||
b = len(V) - 4
|
||||
T += [[b, b + 1, b + 2], [b, b + 2, b + 3]]
|
||||
return V, T
|
||||
|
||||
|
||||
def _cap(r, z, n, up):
|
||||
V = [(0.0, 0.0, z)]
|
||||
for i in range(n):
|
||||
a = 2.0 * math.pi * i / n
|
||||
V.append((r * math.cos(a), r * math.sin(a), z))
|
||||
T = [[0, 1 + i, 1 + (i + 1) % n] if up else [0, 1 + (i + 1) % n, 1 + i] for i in range(n)]
|
||||
return V, T
|
||||
|
||||
|
||||
def _merge(*chunks):
|
||||
V, T = [], []
|
||||
for v, t in chunks:
|
||||
b = len(V)
|
||||
V += list(v)
|
||||
T += [[i + b for i in tri] for tri in t]
|
||||
return V, T
|
||||
|
||||
|
||||
def _geom(spec):
|
||||
"""清单条目 -> 杆径/长/头型/头径(mm)。"""
|
||||
nom = float(str(spec["螺纹"]).lstrip("Mm"))
|
||||
rs, L = nom / 2.0, float(spec["长度mm"])
|
||||
head = spec.get("头型", "")
|
||||
if "沉头" in head: # 沉头内六角:90° 锥,最宽处(刃口)在承载面
|
||||
rh = {3.0: 3.15, 4.0: 3.5, 6.0: 5.5}.get(nom, 1.05 * nom) # 头径≈沉孔座位径(GB dk)
|
||||
return dict(kind="csk", rs=rs, L=L, rh=rh)
|
||||
if "外六角" in head: # 外六角:M6 对边 10,高 4
|
||||
af = {3.0: 5.5, 4.0: 7.0, 5.0: 8.0, 6.0: 10.0}.get(nom, 1.7 * nom)
|
||||
hh = max(3.0, 0.7 * nom)
|
||||
return dict(kind="hex", rs=rs, L=L, rh=(af / 2.0) / math.cos(math.pi / 6.0), hh=hh)
|
||||
rh, hh = {2.0: (3.8, 2.0), 3.0: (5.5, 3.0), 4.0: (7.0, 4.0),
|
||||
5.0: (8.5, 5.0), 6.0: (10.0, 6.0)}.get(nom, (1.75 * nom, 0.9 * nom))
|
||||
return dict(kind="sh", rs=rs, L=L, rh=rh / 2.0, hh=hh)
|
||||
|
||||
|
||||
def _mesh(g):
|
||||
"""局部网格:头部 z ∈ [-hh,0](0 = 承载头面),杆部 z ∈ [0, L]。"""
|
||||
rs, L = g["rs"], g["L"]
|
||||
if g["kind"] == "csk":
|
||||
hh = g["rh"] - rs # 90° 锥:头高 = (dk-d2)/2
|
||||
head = _merge(_side(g["rh"], rs, 0.0, hh, 14), # 锥面自承载面收进材料(沉入沉孔座位)
|
||||
_cap(g["rh"], 0.0, 14, up=False), # 承载端外露环形面
|
||||
_cap(rs, hh, 14, up=True))
|
||||
elif g["kind"] == "hex":
|
||||
hh, rh = g["hh"], 0.98 * g["rh"]
|
||||
head = _merge(_side(rh, rh, -hh, 0.0, 6), _cap(rh, 0.0, 6, up=True),
|
||||
_cap(rh, -hh, 6, up=False))
|
||||
else:
|
||||
hh, rh = g["hh"], g["rh"]
|
||||
head = _merge(_side(rh, rh, -hh, 0.0, 14), _cap(rh, 0.0, 14, up=True),
|
||||
_cap(rh, -hh, 14, up=False))
|
||||
shaft = _merge(_side(rs * 0.985, rs * 0.86, 0.0, L * 0.95, 12),
|
||||
_side(rs * 0.86, 0.18, L * 0.95, L * 0.995, 12),
|
||||
_cap(0.18, L * 0.995, 12, up=True), _cap(rs * 0.985, 0.0, 12, up=False))
|
||||
return _merge(head, shaft)
|
||||
|
||||
|
||||
def place(V, hp, ax):
|
||||
"""局部 -> 全局 CAD 坐标:局部 +Z = 拧入方向 = -ax(ax 为头朝外法向),右手系保证缠绕不翻。"""
|
||||
zl = _norm(tuple(-c for c in ax))
|
||||
t = (1.0, 0.0, 0.0) if abs(zl[0]) < 0.9 else (0.0, 1.0, 0.0)
|
||||
xl = _norm(_cross(t, zl))
|
||||
yl = _cross(zl, xl)
|
||||
return [tuple(round(hp[c] + lx * xl[c] + ly * yl[c] + lz * zl[c], 2) for c in range(3))
|
||||
for (lx, ly, lz) in V]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 72 个实例
|
||||
def instances(m2_bcd, m2_clock):
|
||||
out = []
|
||||
r01 = m2_bcd / 2.0
|
||||
clock01 = [(360.0 - a) % 360.0 for a in m2_clock] # 从动件沉孔相位(arm_build.HUB_M2_CLOCK)
|
||||
|
||||
z01 = {"J1": 63.0, "J2": 58.0, "J3": 58.0, "J4": 58.0, "J5": 58.0, "J6": 58.0}
|
||||
for tag in ("J1", "J2", "J3", "J4", "J5", "J6"):
|
||||
d = frame(tag)[1]
|
||||
c_list = m2_clock if tag == "J1" else clock01
|
||||
for a in c_list:
|
||||
u, v = r01 * math.cos(math.radians(a)), r01 * math.sin(math.radians(a))
|
||||
out.append(("S01", to_global(tag, u, v, z01[tag]), d, tag,
|
||||
"%s 输出毂" % DRIVEN[tag], "φ%.0f @%.1f°" % (m2_bcd, a)))
|
||||
|
||||
for tag, tp in (("J2", 8.0), ("J3", 7.0), ("J4", 8.0), ("J5", 8.0), ("J6", 7.0)):
|
||||
d = frame(tag)[1]
|
||||
zt = -tp + 4.0 # 沉孔台阶 = -t_plate + cb_depth(4)
|
||||
for u, v in REAR_SLOTS:
|
||||
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
|
||||
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
|
||||
|
||||
d1 = frame("J1")[1]
|
||||
for deg in (17.47, 107.48, 197.48, 287.47):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S04", to_global("J1", u, v, 60.5), d1, "root",
|
||||
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
for u, v in REAR_SLOTS:
|
||||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||||
"P03 后置法兰", "BCD55 @45° 螺纹槽 -> M1 尾端面"))
|
||||
|
||||
# S12 电机前端压盖 -> 电机前端盖:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) /
|
||||
# J5(P07 夹 M5) / J6(P08 夹 M6) 各 4 颗。J2/J4/J5 三处压盖几何完全相同,
|
||||
# J3/J6 是同一套配方的 Z 轴版本(M6 压盖 2026-10-03 补齐,见 p_wrist_bracket)。
|
||||
# R35、沉孔台阶都在"前端盖外端面 + 2.0mm":Y 轴是 Y_COVER+2=158,Z 轴是 (z_r-56)-2=387。
|
||||
# 4xM3x8 @BCD55 17.8°+90k、沉孔 Ø6.0 深 3.0、压盖背面与前端盖 0.0 面压,
|
||||
# 相位与 S04(P03→M1 前端锁紧)同一套约定,全机 BCD55 孔位一致。
|
||||
#
|
||||
# 孔位必须按 motor_front_clamp 的写法直接算全局 (x,z)/(x,y),不能用 to_global:
|
||||
# 压盖成孔是 fu = px + BCD*cos、fv = pz/py + BCD*sin,即 sin 项直接加在**全局**该轴上;
|
||||
# 而 frame() 的 y 轴 = d × x,对 J2/J4/J5(d=+Y, x=+X)算出 y=(0,0,-1),
|
||||
# 经 to_global 会把 v 映到全局 -z,与成孔方向差一个符号
|
||||
# -> 12 颗螺钉实例整体偏 2*27.5*sin(17.8°) = 16.81mm,每颗都落在两孔之间的实体上
|
||||
# (布尔校验:M3 圆柱头沉在 Ø6.0 沉孔里却与压盖实体重叠 71.27 mm^3,装不进去)。
|
||||
# 故此处显式用全局平面复刻成孔公式,与建模脚本一一对应。
|
||||
for tag, prox in (("J2", "P04 肩叉"), ("J3", "P05 大臂旋转座"),
|
||||
("J4", "P06 大臂骨架"), ("J5", "P07 小臂骨架"),
|
||||
("J6", "P08 腕偏转座")):
|
||||
dt = frame(tag)[1]
|
||||
rear = tuple(HL.AXIS[tag]["rear"])
|
||||
roll = HL.AXIS[tag]["d"][1] == 0 # J3/J6 是 roll 电机(轴向 -Z)
|
||||
for deg in CLAMP_CLOCK:
|
||||
u = CLAMP_BCD * math.cos(math.radians(deg))
|
||||
v = CLAMP_BCD * math.sin(math.radians(deg))
|
||||
hp = (rear[0] + u, rear[1] + v, rear[2] - 58.0) if roll \
|
||||
else (rear[0] + u, rear[1] + 58.0, rear[2] + v)
|
||||
out.append(("S12", hp, dt, GRP_PAR[tag],
|
||||
"%s 前端压盖" % prox,
|
||||
"BCD55 @%.1f° -> %s 电机前端盖(前端夹紧,%s 轴)"
|
||||
% (deg, tag, "Z" if roll else "Y")))
|
||||
|
||||
for x in (X_COL - 16.0, X_COL + 22.0):
|
||||
for z in (180.0, 329.0):
|
||||
out.append(("S05", (x, 95.0, z), (0.0, -1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 前面 -Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
out.append(("S05", (x, 161.0, z), (0.0, 1.0, 0.0), "root",
|
||||
"P01 立柱分段缝", "z=%.0f 背面 +Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
|
||||
for deg in (0.0, 90.0, 180.0, 270.0):
|
||||
# BCD44(R22):原 R20 与 S01 输出螺钉 Ø5.6 通道中心距仅 6.06 < 6.30,
|
||||
# 沉孔壁被咬穿成月牙形;加大到 R22 后余量 1.66mm。见 p_tool_flange 注释。
|
||||
x = HL.X_ARM + 22.0 * math.cos(math.radians(deg))
|
||||
y = HL.Y_AXIS + 22.0 * math.sin(math.radians(deg))
|
||||
out.append(("S06", (x, y, 97.0), (0.0, 0.0, -1.0), "J6",
|
||||
"P09 工具法兰", "R22 @%.0f° -> 末端工具" % deg))
|
||||
|
||||
for x in (-135.0, -99.0):
|
||||
for y in (110.0, 146.0):
|
||||
out.append(("S07", (x, y, 6.0), (0.0, 0.0, -1.0), "root",
|
||||
"P02 底座中核心", "非对称防反装 (%.0f,%.0f)" % (x, y)))
|
||||
|
||||
for x, y in ((-10.0, 76.0), (-10.0, 180.0), (-185.0, 88.0), (-185.0, 168.0)):
|
||||
out.append(("S08", (x, y, 17.0), (0.0, 0.0, 1.0), "root",
|
||||
"P02 防翻力臂", "榫头对锁 (%.0f,%.0f)(另配 M5 预埋螺母)" % (x, y)))
|
||||
|
||||
for x in (X_COL - 27.5, X_COL + 27.5):
|
||||
for y in (Y_COL - 13.5, Y_COL + 13.5):
|
||||
# 方向 = 螺帽朝外法向(+Z,扳手从腔体上方进入);拧入方向 = -Z,Ø3.9 直孔自攻
|
||||
out.append(("S09", (x, y, 479.0), (0.0, 0.0, 1.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从腔体正上方拧入,穿鞍座 9mm 吃进立柱顶环 7mm(自攻成型)"))
|
||||
|
||||
# 横向防脱锁紧(M4x16):螺帽压在后壁外面 x=-148,沿 +X Ø3.9 直孔自攻(穿 11mm 后壁 + 榫头)
|
||||
out.append(("S11", (X_COL - 33.0, Y_COL, 463.0), (-1.0, 0.0, 0.0), "root",
|
||||
"P03 肩吊臂安装台",
|
||||
"Ø3.9 直孔:从立柱后方拧入,穿 11mm 后壁吃进榫头 4.7mm(自攻成型)"))
|
||||
|
||||
for x, y in ((-97.5, 53.0), (-97.5, 203.0), (120.0, 58.0),
|
||||
(120.0, 198.0), (-215.0, 58.0), (-215.0, 198.0)):
|
||||
out.append(("S10", (x, y, 22.0), (0.0, 0.0, 1.0), "root",
|
||||
"P02 底座腰槽", "28×7 通腰槽 (%.1f,%.0f) -> 工作台" % (x, y)))
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 注入
|
||||
def main():
|
||||
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(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")] # 幂等:先清旧螺钉
|
||||
mx = max(p["idx"] for p in d["parts"])
|
||||
|
||||
inst = instances(M["m2_bcd"], M["m2_clock"])
|
||||
cnt = {}
|
||||
for sid, *_ in inst:
|
||||
cnt[sid] = cnt.get(sid, 0) + 1
|
||||
for sid, s in byid.items(): # 与清单数量自洽校验
|
||||
assert cnt.get(sid) == s["数量"], ("实例数与清单不符", sid, cnt.get(sid), s["数量"])
|
||||
|
||||
geo = {sid: _geom(s) for sid, s in byid.items()}
|
||||
seen, new = {}, []
|
||||
for sid, hp, ax, grp, short, detail in inst:
|
||||
g = dict(geo[sid])
|
||||
clip = ""
|
||||
if sid in LMAX: # 螺杆穿出底座底面后伸向未建模的工作台,按可见段截断
|
||||
g["L"] = min(g["L"], LMAX[sid])
|
||||
clip = ";螺杆伸出底座底面(伸入工作台)的部分按可见段截断"
|
||||
V, T = _mesh(g)
|
||||
n = seen.get(sid, 0) + 1
|
||||
seen[sid] = n
|
||||
mx += 1
|
||||
s = byid[sid]
|
||||
new.append(dict(
|
||||
name="%s_%d" % (sid, n),
|
||||
cn="%s · %s %d/%d" % (s["规格"], short, n, s["数量"]),
|
||||
v=place(V, hp, ax), t=T, color=COLOR, printed=False, vol=0.0,
|
||||
note="%s %s — %s:%s%s;%s" % (s["规格"], s["头型"], s["位置"], detail, clip, s["备注"]),
|
||||
grp=grp, jaw=0.0, ex=[0.0, 0.0, 0.0], idx=mx,
|
||||
screw=True, spec=sid,
|
||||
hl=[round(hp[0], 2), round(hp[2], 2), round(-hp[1], 2)])) # 查看器系头面点 (x,z,-y)
|
||||
|
||||
d["parts"] += new
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(d, f, ensure_ascii=False, separators=(",", ":"))
|
||||
os.replace(tmp, path)
|
||||
print("螺钉实例 %d 颗已注入:%s" % (len(new), " / ".join("%s:%d" % (k, cnt[k]) for k in sorted(cnt))))
|
||||
print("零件数 %d -> %d,文件 %.1f MB"
|
||||
% (len(d["parts"]) - len(new), len(d["parts"]), os.path.getsize(path) / 1e6))
|
||||
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user