单文件约束取消后,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 段逐字节一致。
339 lines
19 KiB
Python
339 lines
19 KiB
Python
# -*- 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))
|