feat(arm): 建模代码拆成每件一模块,查看器新增工程图与装配步骤页

- src/parts/p01~p09 + src/part_tools.py:几何、件号/配色/说明与装配帧归属随各自件文件走,arm_model.py 只留装配流水线并对件级符号做只读转发;P04_CH / P07_CH / P06_WIN_XZ / FENCE_OPEN_CUT 这类开关必须写所在件文件,写 arm_model.X 不生效,A/B 判据为翻转后体积变化
- docs/sheet_{exploded,front,side,joint}.svg + draw_sheet.py:四张工程图进查看器页签,支持图层开关、件号高亮与 BOM/3D 联动跳转
- travel/travel_detail/collide/bom/screws 随最新几何重生成;补线束走线、拆卸定位点与 P04 承压面剖切验证图
- .gitignore 排除 .bak/ 与 *.bak_* 源码快照(旧版本 git 历史里已有)
This commit is contained in:
cbb
2026-10-14 22:03:18 +08:00
parent c754521bde
commit b0efb44368
72 changed files with 94603 additions and 90905 deletions
+101 -65
View File
@@ -76,28 +76,46 @@ 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 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, 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 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):
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))
@@ -114,9 +132,10 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
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))
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:
@@ -124,48 +143,56 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
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" '
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 x="%.0f" y="%.0f" font-size="10" fill="#0b57a4">%s</text>'
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"/>' % (d, col))
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"/>' % d)
'stroke-dasharray="4 2" %s/>' % (d, attrs))
out.append('</g>')
# 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.
# 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))
# 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])
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 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"]))
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
@@ -173,20 +200,30 @@ def build_sheet(kind, title, W, H, ex=0.0, note="", axes=True, dims=None, legend
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))
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>')
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))
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)
@@ -202,32 +239,31 @@ def joint_detail():
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" '
"从动盘(复刻电机输出面)。蓝色为关节轴线。", "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 stroke="#111" fill="none" stroke-width="1.5">']
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 x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
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 x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
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 x="%.1f" y="%.1f" width="%.1f" height="%.1f"/>'
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 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))
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 x="%.1f" y="%.1f" width="%.1f" height="%.1f" stroke-dasharray="6 3"/>'
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('<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="%s" stroke-width="1"/>' % (xa, y, xb, y, col))
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))
# 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))
@@ -236,19 +272,16 @@ def joint_detail():
% (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"):
"""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"/>'
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>'
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")
@@ -271,7 +304,10 @@ def joint_detail():
return "".join(out)
SH, EL, WR = LAY["SH"], LAY["EL"], LAY["WR"]
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)]
@@ -279,13 +315,13 @@ DIMS_ARM = [((0, SH[0], SH[1]), (0, EL[0], EL[1]), "上臂 %.0f" % LAY["L1"], 74
sheets = [
build_sheet("front", "总装图 - 正视(沿 -X 投影;肢体在纸面内,显示左右交替堆叠)", 1200, 800, 0.0,
"实线=打印件,虚线=电机/已有联轴器;点划线为关节轴线;蓝色为节距链。"
"俯仰电机一内一外交替,全部贴合面共用 Y=157 一个平面", True, DIMS_ARM),
"俯仰电机一内一外交替,全部贴合面共用 Y=157 一个平面", True, DIMS_ARM, sheet_id="front"),
joint_detail(),
build_sheet("iso", "爆炸装配图(轴测)", 1200, 800, 1.0,
"爆炸量 100%:每个关节沿肢体方向逐级错开一节,电机与端面齿组件再沿各自轴线抽出 —— 自上而下即装配顺序", False, None),
"爆炸量 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),
"肩吊臂侧板在 |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):