Files
Robot_Arm/src/draw_sheet.py
T

333 lines
18 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, 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))
os._exit(0)