# -*- 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 ['' % (W, H, W, H, sheet_id, esc(title), esc(note), scale), '' '' '' '' '' '' '' '', '' % (W, H), '', '' % (W - 16, H - 16), '' % (W - 28, H - 28), '' % (H - 44, W - 15, H - 44), '%s' % (H - 27, esc(title)), '%s' % (H - 16, esc(note)), '' % (W - 268, H - 44, W - 268, H - 15), '比例 1:%s(按图幅适配) 单位 mm' % (W - 260, H - 29, scale), '六电机人形机器人手臂 %s / 迭代 %s' % (W - 260, H - 17, LAY["ver"], LAY["it"]), ''] def balloon(x, y, p): idx = p.get("idx", 0) name = esc(p.get("name", "")) cn = esc(p.get("cn", "")) return ('' '' '%d' '' % (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('') 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('' % (pa[0], pa[1], pb[0], pb[1])) out.append('%s' % (pb[0] + 3, pb[1] - 3, jid)) out.append('') out.append('') 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('' % (d, col, attrs)) else: out.append('' % (d, attrs)) out.append('') # 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('') 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('' % (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('') if legend: out.append('') rows = sorted(side, key=lambda s: s[0]["idx"]) half = (len(rows) + 1) // 2 tx, ty0 = W - MR + 74, 62 out.append('零 件 表' % (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('' '' '%d' '%s' '' % (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('') if dims: out.append('') 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('' % (pa[0], y, pb[0], y)) for q in (pa, pb): out.append('' % (q[0], q[1], q[0], y)) out.append('%s' % ((pa[0] + pb[0]) / 2, y - 3, lab)) out.append('') out.append('') 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('' % (x0 - 60, cy, x_hub + 190, cy)) g = [''] # motor: housing Ø60 x 42, rear plate, cover Ø60.3 x 3.5 g.append('' % (x0 + 5.5 * s, cy - 30 * s, 47 * s, 60 * s)) g.append('' % (x0, cy - MT["pcba_w"] / 2.0 * s, MT["pcba_t"] * s, MT["pcba_w"] * s)) g.append('' % (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('' % (x_cover, cy - 27 * s, 4.5 * s, 54 * s)) g.append('' % (x_cover + 4.5 * s, cy - 27 * s, 4.5 * s, 54 * s)) g.append('' % (x_hub, cy - 41 * s, 24 * s, 82 * s)) # clamp band over the closed pair g.append('' % (x_cover + 0.6 * s, cy - 30 * s, 8.4 * s, 60 * s)) g.append('') out.extend(g) def dim(y, xa, xb, txt, col="#c00", lx=None): out.append('' % (xa, y, xb, y, col)) for x in (xa, xb): out.append('' % (x, y - 6, x, y + 6, col)) if lx is None: out.append('%s' % ((xa + xb) / 2, y - 8, col, txt)) else: out.append('%s' % (lx, y - 8, col, txt)) out.append('' % (lx + 4, y - 11, xa, y, col)) out.append('') 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('' % (x, y, x, ty, tx, ty)) out.append('%s' % (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('M2 相位(时钟):电机侧 %s ;从动盘侧取镜像 %s' % (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('抱箍:带宽 Ø%.0f,凸耳 R%.1f(位于关节局部 ±X,包壳在此处开口);M3 x2 @ R%.1f' % (H - 48, CT["clamp_od_r"] * 2, CT["lug_r1"], CT["bolt_r"])) out.append('') 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))