Files
Robot_Arm/src/draw_sheet.py
T

297 lines
16 KiB
Python
Raw Normal View History

# -*- 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")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from armcore import WEB, DOCS
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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def frame(W, H, title, note, scale):
return ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" '
'font-family="Microsoft YaHei,sans-serif">' % (W, H),
'<rect width="%d" height="%d" fill="#fff"/>' % (W, H),
'<rect x="8" y="8" width="%d" height="%d" fill="none" stroke="#111" stroke-width="1.6"/>' % (W - 16, H - 16),
'<rect x="14" y="14" width="%d" height="%d" fill="none" stroke="#111" stroke-width=".5"/>' % (W - 28, H - 28),
'<line x1="14" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (H - 44, W - 15, H - 44),
'<text x="24" y="%d" font-size="13" font-weight="bold">%s</text>' % (H - 27, esc(title)),
'<text x="24" y="%d" font-size="9" fill="#444">%s</text>' % (H - 16, esc(note)),
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#111" stroke-width=".8"/>' % (W - 268, H - 44, W - 268, H - 15),
'<text x="%d" y="%d" font-size="9">比例 1:%s(按图幅适配) 单位 mm</text>' % (W - 260, H - 29, scale),
'<text x="%d" y="%d" font-size="9">六电机人形机器人手臂 %s / 迭代 %s</text>' % (W - 260, H - 17, LAY["ver"], LAY["it"])]
def balloon(x, y, i):
return ('<circle cx="%.0f" cy="%.0f" r="10" fill="#fff" stroke="#111" stroke-width="1"/>'
'<text x="%.0f" y="%.0f" font-size="9.5" text-anchor="middle">%d</text>' % (x, y, x, y + 3.4, i))
def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend=True):
shp = []
for p in P:
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))
if axes:
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 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 x="%.0f" y="%.0f" font-size="10" fill="#0b57a4">%s</text>'
% (pb[0] + 3, pb[1] - 3, jid))
for p, h in shp:
d = "M" + " L".join("%.1f %.1f" % T(q) for q in h) + "Z"
col = "#%06x" % p["color"]
if p["printed"]:
out.append('<path d="%s" fill="none" stroke="%s" stroke-width="1.35"/>' % (d, col))
else:
out.append('<path d="%s" fill="none" stroke="#9aa1ad" stroke-width=".7" '
'stroke-dasharray="4 2"/>' % d)
# balloons in two side columns. Sort each column by the ANCHOR height (the nearest point
# on the part's own outline), not by centroid - sorting by centroid made the leaders cross
# in the middle of the sheet because the anchor can sit far from the centroid.
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))
# A hanging arm projects tall and narrow, so splitting the balloons left/right pushed the
# whole left column's leaders across the assembly. When the view is taller than wide every
# balloon goes to one column just right of the drawing, sorted by anchor height.
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])
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 points="%.0f,%.0f %.0f,%.0f %.0f,%.0f" fill="none" '
'stroke="#555" stroke-width=".6"/>' % (t[0], t[1], (t[0] + x) / 2, by, x, by))
out.append(balloon(x, by, p["idx"]))
col(left, ML - 30)
col(right, (ox + (hi_x - lo_x) * sc + 52.0) if tall else (W - MR + 44))
if legend:
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
out.append('<text x="%d" y="%.0f" font-size="8.5">%d</text>' % (tx + c * 106, y, p["idx"]))
nm = esc(p["cn"].replace(" 半联轴器/抱箍", " 联轴器/抱箍").replace("端面齿", "")[:11])
out.append('<text x="%d" y="%.0f" font-size="8.5">%s</text>' % (tx + c * 106 + 13, y, nm))
for (a, b, lab, off) in (dims or []):
pa, pb = T(proj(a, kind)), T(proj(b, kind))
y = (pa[1] + pb[1]) / 2 + off
out.append('<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 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 x="%.0f" y="%.0f" font-size="10" fill="#0b57a4" text-anchor="middle">%s</text>'
% ((pa[0] + pb[0]) / 2, y - 3, lab))
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 倍)")
out.append('<line 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 stroke="#111" fill="none" stroke-width="1.5">']
# motor: housing Ø60 x 42, rear plate, cover Ø60.3 x 3.5
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
% (x0 + 5.5 * s, cy - 30 * s, 47 * s, 60 * s))
g.append('<rect 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 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 x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover, cy - 27 * s, 4.5 * s, 54 * s))
g.append('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>' % (x_cover + 4.5 * s, cy - 27 * s, 4.5 * s, 54 * s))
g.append('<rect 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 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('<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))
# a short span cannot hold a long label centred on it - hang the text off to the left
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))
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"):
"""Leader + label. Labels go in one of the two gutters (x=235 right-aligned, x=900
left-aligned) - the first version dropped them on top of the geometry they describe.
The leader runs vertical off the anchor first, then horizontal: a straight diagonal
from anchor to gutter cut across the section it is describing."""
out.append('<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>'
% (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)
SH, EL, WR = LAY["SH"], LAY["EL"], LAY["WR"]
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),
joint_detail(),
build_sheet("iso", "爆炸装配图(轴测)", 1200, 800, 1.0,
"爆炸量 100%:每个关节沿肢体方向逐级错开一节,电机与端面齿组件再沿各自轴线抽出 —— 自上而下即装配顺序", False, None),
build_sheet("side", "总装图 - 侧视(沿 +Y 投影;俯仰轴垂直纸面)", 900, 800, 0.0,
"肩抬举(J1)轴沿 X 前后布置,与俯仰轴平行偏置 92 mm;"
"肩吊臂侧板在 |x| ≥ 64,肢体包络 |x| ≤ 60.5,故整周不碰", True, None),
]
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))
os._exit(0)