前端不再是"一个 16MB HTML + 一个 2955 行模板"。现在: webapp/index.html 343 行 DOM 骨架,零内联数据 webapp/styles/*.css 811 行 CSS 按段切成 8 个文件 webapp/js/app.js 原内联主脚本逐字搬出(仍是普通脚本,S5 才改 ESM) webapp/js/ik-solver.js / ik-glue.js / vendor/three.min.js(原 web/ 三个文件) webapp/workers/ik.worker.js 真 Worker 文件 webapp/generated/00_data.js 生成物(网格/工程图/行程/碰撞/螺钉) dist/机器人手臂3D查看器.html 分发用单文件,由 build_viewer 内联上面这些文件产出 入口:python -m arm.viewer.serve 后开 http://127.0.0.1:8123/ (serve 带 no-store,改完刷新即新构建,不再需要 Ctrl+F5;显式 MIME, .js 被 Windows 注册表判成 text/plain 会让模块静默不执行) 删掉 src/viewer_tpl.html 与 web/ 目录(旧产物移入 backups/pre_split_20261015/)。 工程图不再维护第二份 sheets.jsdata —— 直接读 draw_sheet 写出的 4 个真 SVG。 实测中修掉的问题: - serve 的 / 原先"内部映射"到 webapp/index.html,导致文档基址是 /、相对路径的 8 个 CSS 与 4 个 JS 全部 404(整页无样式无脚本)。改成 302 跳转。 - file:// 下 new Worker('workers/…') 构造阶段不抛错、错误以异步 onerror 到达, 所以"构造失败才试 Blob 兜底"永远不会触发;改为按协议选路。两条路径都已实测 backend=worker(http 走文件、file:// 走 Blob)。 - Worker 握手 3s 在 headless 并发下会误判"没起来"(截图里红徽标即此),放宽到 10s, 并把后端状态暴露成 window.__IK_BACKEND / __IK_BACKEND_LOG 供测试直读。 - 截图竞态:4 并发 × 每页 14MB 数据时,虚拟时间预算会在页面加载完成前到期, 拍到半初始化画面(且能"稳定地坏",旧基线里 m9_motion 就是这么混进来的)。 并发降到 2、预算 40s、serve 加 HTTP/1.1+大缓冲+字节缓存,并用"同一构建两轮" 重新界定稳定性:99 张逐字节一致、6 张属噪声改走像素容差(mean<=0.02、单像素<=32)。 - 关卡自身两处假通过已修:node 输出被 GBK 解码炸掉时不再算 PASS; 线束干涉测试读不到通道源码时会 exit 2,而不是"零通道=零穿透=通过"。 关卡结果:python -m arm.build_model 退出码 0;10 项数据哈希与基线一致; IK exact 段逐字节一致;99 张截图逐字节一致 + 6 张在噪声阈内;线束干涉测试 1800 点 0 穿透。
348 lines
19 KiB
Python
348 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""紧固件(螺钉)实例化网格生成器。
|
||
|
||
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
|
||
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
|
||
随动关节组),生成轻量三角网格后追加进 webapp/data/mesh_arm.json 的 parts 中。
|
||
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
|
||
|
||
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
|
||
头面 = 沉孔/通道的"承接台阶"(螺钉从外侧塞入,一直顶到孔径变小的那一圈台阶面),
|
||
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外(S05 除外:搭接壁太薄不能开沉孔)。
|
||
S01 J1~J6 输出毂 3xM2:joint.L() 局部 (r=m2_bcd/2, HUB_M2_CLOCK 相位),
|
||
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
|
||
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
|
||
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
|
||
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (Ø6.5 圆柱沉孔台阶 x=-56.5,法兰外端面 -61,
|
||
圆柱头沉入 1.5mm)
|
||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 60.5)
|
||
(Ø6.0×2 圆柱沉孔在 Ø10 凸台内,台阶 x=9.5,M3x8 圆柱头露 1mm,旋入电机前端面咬合 3.5mm)
|
||
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
|
||
前后各拧一颗,头在 y=95 / y=161 外露(搭接壁薄不开沉孔),直接拧入自攻成型
|
||
S06 P09 工具法兰 4xM4:R22 @0/90/180/270°(BCD44,原 R20 会与 S01 的 Ø5.6 通道咬穿沉孔),台阶 z=97(Ø7.0×4 圆柱沉孔 + Ø4.4 贯穿通孔,法兰底面 z=93,
|
||
M4 圆柱头与法兰底面齐平)
|
||
S07 P02 中核心 4xM5 :x∈{-135,-99}, y∈{110,146},台阶 z=6(Ø10 沉孔深 6mm,头沉入 1mm)
|
||
S08 P02 力臂 4xM5 :(-10,76/180) + (-185,88/168),台阶 z=17(Ø10 沉孔深 5mm,头与顶面齐平)
|
||
S09 P03 安装台 4xM4x16:x∈{-142.5,-87.5}, y∈{114.5,141.5},台阶 z=479(P03 鞍座顶面,
|
||
从腔体正上方往下拧,Ø3.9 直孔穿过鞍座 9mm 再吃进 P01_C 顶环 7mm,自攻成型)
|
||
S11 P03 横向防脱锁紧 1xM4x16:x=-148(立柱 −X 外端面),y=128,z=463,头朝 -X,
|
||
沿 +X Ø3.9 直孔穿 11mm 后壁 + 榫头同径,直接拧入自攻(榫头内咬合 4.7mm)
|
||
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
|
||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||
S12 电机前端压盖 4xM3x8:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) / J5(P07 夹 M5)
|
||
/ J6(P08 夹 M6) 各 4 颗。
|
||
配方唯一事实源 = part_tools.motor_front_clamp(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔
|
||
深 3.0、沉孔台阶 = 前端盖外端面 +2.0mm、压盖背面 0.0 面压);J3 是同一套配方的 Z 轴版本
|
||
(台阶落在 Z=(z_r-56)-2=387),半径/相位直接取 arm_model 的 CLAMP_BCD/CLAMP_CLOCK。
|
||
孔位必须显式算全局 (x,z)/(x,y) 而不能用 to_global,原因见 instances() 内的注释。
|
||
|
||
长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模),
|
||
这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。
|
||
运行:python -m arm.dataio.screw_mesh -> 注入网格(纯数据,秒级,不动 CAD)
|
||
再 python -m arm.viewer.build_viewer -> 重新打包单文件查看器
|
||
arm_model.py 主流程末尾已自动调用本模块,重跑完整构建不会丢失螺钉。
|
||
"""
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
from arm.paths import DATA, DOCS
|
||
from arm import kinematics as HL
|
||
from arm.cad.part_tools import CLAMP_BCD, CLAMP_CLOCK
|
||
X_COL = -115.0 # 立柱中心(与 parts/p01_column.py 的 X_COL 一致)
|
||
Y_COL = HL.Y_AXIS # 128.0
|
||
GRP_PAR = {"J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"}
|
||
DRIVEN = {"J1": "P04 肩叉", "J2": "P05 大臂旋转座", "J3": "P06 大臂骨架",
|
||
"J4": "P07 小臂骨架", "J5": "P08 腕偏转座", "J6": "P09 工具法兰"}
|
||
PROX = {"J2": "P04 肩叉", "J3": "P05 大臂旋转座", "J4": "P06 大臂骨架",
|
||
"J5": "P07 小臂骨架", "J6": "P08 腕偏转座"}
|
||
COLOR = 0xc3cad6 # 不锈钢冷色
|
||
LMAX = {"S08": 17.0, "S10": 22.0} # 可见段截断:螺杆伸到底座底面(z=0)即止(台面/预埋螺母未建模)
|
||
|
||
# 电机尾端 4x M3 螺纹径向槽的实测孔位(电机.STEP):BCD55、相位 39.9°+90k,
|
||
# 槽宽 2.5、径向可调约 ±1.25mm。S02(P04~P08 后端板)与 S03(P03 后法兰)共用;
|
||
# 旧 35.1×40.9 矩形孔距对角两颗偏出螺纹槽 ~4.7mm(2026-10-02 用户指出"螺钉没有对齐")。
|
||
REAR_SLOTS = tuple((27.5 * math.cos(math.radians(39.65 + 90.0 * k)),
|
||
27.5 * math.sin(math.radians(39.65 + 90.0 * k))) for k in range(4))
|
||
|
||
|
||
# ---------------------------------------------------------------- 局部坐标系工具
|
||
def _norm(v):
|
||
m = math.sqrt(sum(c * c for c in v)) or 1.0
|
||
return tuple(c / m for c in v)
|
||
|
||
|
||
def _cross(a, b):
|
||
return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
||
|
||
|
||
def _dot(a, b):
|
||
return sum(x * y for x, y in zip(a, b))
|
||
|
||
|
||
def frame(tag):
|
||
"""电机局部系 (u,v,w) -> 全局:与 armcore.trs 一致(局部 +Z -> d,+X -> xdir,y = z × x)。"""
|
||
a = HL.AXIS[tag]
|
||
rear, d, xdir = tuple(a["rear"]), _norm(a["d"]), a["xdir"]
|
||
x = _norm(tuple(xdir[c] - _dot(xdir, d) * d[c] for c in range(3)))
|
||
y = _cross(d, x)
|
||
return rear, d, x, y
|
||
|
||
|
||
def to_global(tag, u, v, w):
|
||
rear, d, x, y = frame(tag)
|
||
return tuple(rear[c] + u * x[c] + v * y[c] + w * d[c] for c in range(3))
|
||
|
||
|
||
# ---------------------------------------------------------------- 网格基元(局部 +Z = 拧入方向)
|
||
def _side(r0, r1, z0, z1, n):
|
||
"""圆台侧面(外向缠绕)。"""
|
||
V, T = [], []
|
||
for i in range(n):
|
||
a0 = 2.0 * math.pi * i / n
|
||
a1 = 2.0 * math.pi * (i + 1) / n
|
||
V += [(r0 * math.cos(a0), r0 * math.sin(a0), z0), (r0 * math.cos(a1), r0 * math.sin(a1), z0),
|
||
(r1 * math.cos(a1), r1 * math.sin(a1), z1), (r1 * math.cos(a0), r1 * math.sin(a0), z1)]
|
||
b = len(V) - 4
|
||
T += [[b, b + 1, b + 2], [b, b + 2, b + 3]]
|
||
return V, T
|
||
|
||
|
||
def _cap(r, z, n, up):
|
||
V = [(0.0, 0.0, z)]
|
||
for i in range(n):
|
||
a = 2.0 * math.pi * i / n
|
||
V.append((r * math.cos(a), r * math.sin(a), z))
|
||
T = [[0, 1 + i, 1 + (i + 1) % n] if up else [0, 1 + (i + 1) % n, 1 + i] for i in range(n)]
|
||
return V, T
|
||
|
||
|
||
def _merge(*chunks):
|
||
V, T = [], []
|
||
for v, t in chunks:
|
||
b = len(V)
|
||
V += list(v)
|
||
T += [[i + b for i in tri] for tri in t]
|
||
return V, T
|
||
|
||
|
||
def _geom(spec):
|
||
"""清单条目 -> 杆径/长/头型/头径(mm)。"""
|
||
nom = float(str(spec["螺纹"]).lstrip("Mm"))
|
||
rs, L = nom / 2.0, float(spec["长度mm"])
|
||
head = spec.get("头型", "")
|
||
if "沉头" in head: # 沉头内六角:90° 锥,最宽处(刃口)在承载面
|
||
rh = {3.0: 3.15, 4.0: 3.5, 6.0: 5.5}.get(nom, 1.05 * nom) # 头径≈沉孔座位径(GB dk)
|
||
return dict(kind="csk", rs=rs, L=L, rh=rh)
|
||
if "外六角" in head: # 外六角:M6 对边 10,高 4
|
||
af = {3.0: 5.5, 4.0: 7.0, 5.0: 8.0, 6.0: 10.0}.get(nom, 1.7 * nom)
|
||
hh = max(3.0, 0.7 * nom)
|
||
return dict(kind="hex", rs=rs, L=L, rh=(af / 2.0) / math.cos(math.pi / 6.0), hh=hh)
|
||
rh, hh = {2.0: (3.8, 2.0), 3.0: (5.5, 3.0), 4.0: (7.0, 4.0),
|
||
5.0: (8.5, 5.0), 6.0: (10.0, 6.0)}.get(nom, (1.75 * nom, 0.9 * nom))
|
||
return dict(kind="sh", rs=rs, L=L, rh=rh / 2.0, hh=hh)
|
||
|
||
|
||
def _mesh(g):
|
||
"""局部网格:头部 z ∈ [-hh,0](0 = 承载头面),杆部 z ∈ [0, L]。"""
|
||
rs, L = g["rs"], g["L"]
|
||
if g["kind"] == "csk":
|
||
hh = g["rh"] - rs # 90° 锥:头高 = (dk-d2)/2
|
||
head = _merge(_side(g["rh"], rs, 0.0, hh, 14), # 锥面自承载面收进材料(沉入沉孔座位)
|
||
_cap(g["rh"], 0.0, 14, up=False), # 承载端外露环形面
|
||
_cap(rs, hh, 14, up=True))
|
||
elif g["kind"] == "hex":
|
||
hh, rh = g["hh"], 0.98 * g["rh"]
|
||
head = _merge(_side(rh, rh, -hh, 0.0, 6), _cap(rh, 0.0, 6, up=True),
|
||
_cap(rh, -hh, 6, up=False))
|
||
else:
|
||
hh, rh = g["hh"], g["rh"]
|
||
head = _merge(_side(rh, rh, -hh, 0.0, 14), _cap(rh, 0.0, 14, up=True),
|
||
_cap(rh, -hh, 14, up=False))
|
||
shaft = _merge(_side(rs * 0.985, rs * 0.86, 0.0, L * 0.95, 12),
|
||
_side(rs * 0.86, 0.18, L * 0.95, L * 0.995, 12),
|
||
_cap(0.18, L * 0.995, 12, up=True), _cap(rs * 0.985, 0.0, 12, up=False))
|
||
return _merge(head, shaft)
|
||
|
||
|
||
def place(V, hp, ax):
|
||
"""局部 -> 全局 CAD 坐标:局部 +Z = 拧入方向 = -ax(ax 为头朝外法向),右手系保证缠绕不翻。"""
|
||
zl = _norm(tuple(-c for c in ax))
|
||
t = (1.0, 0.0, 0.0) if abs(zl[0]) < 0.9 else (0.0, 1.0, 0.0)
|
||
xl = _norm(_cross(t, zl))
|
||
yl = _cross(zl, xl)
|
||
return [tuple(round(hp[c] + lx * xl[c] + ly * yl[c] + lz * zl[c], 2) for c in range(3))
|
||
for (lx, ly, lz) in V]
|
||
|
||
|
||
# ---------------------------------------------------------------- 72 个实例
|
||
def instances(m2_bcd, m2_clock):
|
||
out = []
|
||
r01 = m2_bcd / 2.0
|
||
clock01 = [(360.0 - a) % 360.0 for a in m2_clock] # 从动件沉孔相位(arm_build.HUB_M2_CLOCK)
|
||
|
||
z01 = {"J1": 63.0, "J2": 58.0, "J3": 58.0, "J4": 58.0, "J5": 58.0, "J6": 58.0}
|
||
for tag in ("J1", "J2", "J3", "J4", "J5", "J6"):
|
||
d = frame(tag)[1]
|
||
c_list = m2_clock if tag == "J1" else clock01
|
||
for a in c_list:
|
||
u, v = r01 * math.cos(math.radians(a)), r01 * math.sin(math.radians(a))
|
||
out.append(("S01", to_global(tag, u, v, z01[tag]), d, tag,
|
||
"%s 输出毂" % DRIVEN[tag], "φ%.0f @%.1f°" % (m2_bcd, a)))
|
||
|
||
for tag, tp in (("J2", 8.0), ("J3", 7.0), ("J4", 8.0), ("J5", 8.0), ("J6", 7.0)):
|
||
d = frame(tag)[1]
|
||
zt = -tp + 4.0 # 沉孔台阶 = -t_plate + cb_depth(4)
|
||
for u, v in REAR_SLOTS:
|
||
out.append(("S02", to_global(tag, u, v, zt), tuple(-c for c in d), GRP_PAR[tag],
|
||
"%s 后端板" % PROX[tag], "孔位(%.2f,%.2f) -> %s 电机尾端面" % (u, v, tag)))
|
||
|
||
d1 = frame("J1")[1]
|
||
for deg in (17.47, 107.48, 197.48, 287.47):
|
||
u = 27.5 * math.cos(math.radians(deg))
|
||
v = 27.5 * math.sin(math.radians(deg))
|
||
out.append(("S04", to_global("J1", u, v, 60.5), d1, "root",
|
||
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||
for u, v in REAR_SLOTS:
|
||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||
"P03 后置法兰", "BCD55 @45° 螺纹槽 -> M1 尾端面"))
|
||
|
||
# S12 电机前端压盖 -> 电机前端盖:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) /
|
||
# J5(P07 夹 M5) / J6(P08 夹 M6) 各 4 颗。J2/J4/J5 三处压盖几何完全相同,
|
||
# J3/J6 是同一套配方的 Z 轴版本(M6 压盖 2026-10-03 补齐,见 p_wrist_bracket)。
|
||
# R35、沉孔台阶都在"前端盖外端面 + 2.0mm":Y 轴是 Y_COVER+2=158,Z 轴是 (z_r-56)-2=387。
|
||
# 4xM3x8 @BCD55 17.8°+90k、沉孔 Ø6.0 深 3.0、压盖背面与前端盖 0.0 面压,
|
||
# 相位与 S04(P03→M1 前端锁紧)同一套约定,全机 BCD55 孔位一致。
|
||
#
|
||
# 孔位必须按 motor_front_clamp 的写法直接算全局 (x,z)/(x,y),不能用 to_global:
|
||
# 压盖成孔是 fu = px + BCD*cos、fv = pz/py + BCD*sin,即 sin 项直接加在**全局**该轴上;
|
||
# 而 frame() 的 y 轴 = d × x,对 J2/J4/J5(d=+Y, x=+X)算出 y=(0,0,-1),
|
||
# 经 to_global 会把 v 映到全局 -z,与成孔方向差一个符号
|
||
# -> 12 颗螺钉实例整体偏 2*27.5*sin(17.8°) = 16.81mm,每颗都落在两孔之间的实体上
|
||
# (布尔校验:M3 圆柱头沉在 Ø6.0 沉孔里却与压盖实体重叠 71.27 mm^3,装不进去)。
|
||
# 故此处显式用全局平面复刻成孔公式,与建模脚本一一对应。
|
||
for tag, prox in (("J2", "P04 肩叉"), ("J3", "P05 大臂旋转座"),
|
||
("J4", "P06 大臂骨架"), ("J5", "P07 小臂骨架"),
|
||
("J6", "P08 腕偏转座")):
|
||
dt = frame(tag)[1]
|
||
rear = tuple(HL.AXIS[tag]["rear"])
|
||
roll = HL.AXIS[tag]["d"][1] == 0 # J3/J6 是 roll 电机(轴向 -Z)
|
||
for deg in CLAMP_CLOCK:
|
||
u = CLAMP_BCD * math.cos(math.radians(deg))
|
||
v = CLAMP_BCD * math.sin(math.radians(deg))
|
||
hp = (rear[0] + u, rear[1] + v, rear[2] - 58.0) if roll \
|
||
else (rear[0] + u, rear[1] + 58.0, rear[2] + v)
|
||
out.append(("S12", hp, dt, GRP_PAR[tag],
|
||
"%s 前端压盖" % prox,
|
||
"BCD55 @%.1f° -> %s 电机前端盖(前端夹紧,%s 轴)"
|
||
% (deg, tag, "Z" if roll else "Y")))
|
||
|
||
for x in (X_COL - 16.0, X_COL + 22.0):
|
||
for z in (180.0, 329.0):
|
||
out.append(("S05", (x, 95.0, z), (0.0, -1.0, 0.0), "root",
|
||
"P01 立柱分段缝", "z=%.0f 前面 -Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||
out.append(("S05", (x, 161.0, z), (0.0, 1.0, 0.0), "root",
|
||
"P01 立柱分段缝", "z=%.0f 背面 +Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||
|
||
for deg in (0.0, 90.0, 180.0, 270.0):
|
||
# BCD44(R22):原 R20 与 S01 输出螺钉 Ø5.6 通道中心距仅 6.06 < 6.30,
|
||
# 沉孔壁被咬穿成月牙形;加大到 R22 后余量 1.66mm。见 p_tool_flange 注释。
|
||
x = HL.X_ARM + 22.0 * math.cos(math.radians(deg))
|
||
y = HL.Y_AXIS + 22.0 * math.sin(math.radians(deg))
|
||
out.append(("S06", (x, y, 97.0), (0.0, 0.0, -1.0), "J6",
|
||
"P09 工具法兰", "R22 @%.0f° -> 末端工具" % deg))
|
||
|
||
for x in (-135.0, -99.0):
|
||
for y in (110.0, 146.0):
|
||
out.append(("S07", (x, y, 6.0), (0.0, 0.0, -1.0), "root",
|
||
"P02 底座中核心", "非对称防反装 (%.0f,%.0f)" % (x, y)))
|
||
|
||
for x, y in ((-10.0, 76.0), (-10.0, 180.0), (-185.0, 88.0), (-185.0, 168.0)):
|
||
out.append(("S08", (x, y, 17.0), (0.0, 0.0, 1.0), "root",
|
||
"P02 防翻力臂", "榫头对锁 (%.0f,%.0f)(另配 M5 预埋螺母)" % (x, y)))
|
||
|
||
for x in (X_COL - 27.5, X_COL + 27.5):
|
||
for y in (Y_COL - 13.5, Y_COL + 13.5):
|
||
# 方向 = 螺帽朝外法向(+Z,扳手从腔体上方进入);拧入方向 = -Z,Ø3.9 直孔自攻
|
||
out.append(("S09", (x, y, 479.0), (0.0, 0.0, 1.0), "root",
|
||
"P03 肩吊臂安装台",
|
||
"Ø3.9 直孔:从腔体正上方拧入,穿鞍座 9mm 吃进立柱顶环 7mm(自攻成型)"))
|
||
|
||
# 横向防脱锁紧(M4x16):螺帽压在后壁外面 x=-148,沿 +X Ø3.9 直孔自攻(穿 11mm 后壁 + 榫头)
|
||
out.append(("S11", (X_COL - 33.0, Y_COL, 463.0), (-1.0, 0.0, 0.0), "root",
|
||
"P03 肩吊臂安装台",
|
||
"Ø3.9 直孔:从立柱后方拧入,穿 11mm 后壁吃进榫头 4.7mm(自攻成型)"))
|
||
|
||
for x, y in ((-97.5, 53.0), (-97.5, 203.0), (120.0, 58.0),
|
||
(120.0, 198.0), (-215.0, 58.0), (-215.0, 198.0)):
|
||
out.append(("S10", (x, y, 22.0), (0.0, 0.0, 1.0), "root",
|
||
"P02 底座腰槽", "28×7 通腰槽 (%.1f,%.0f) -> 工作台" % (x, y)))
|
||
return out
|
||
|
||
|
||
# ---------------------------------------------------------------- 注入
|
||
def main():
|
||
path = DATA + "/mesh_arm.json"
|
||
print("读取 %s ..." % path, flush=True)
|
||
d = json.load(open(path, encoding="utf-8"))
|
||
M = d["measured"]["M"]
|
||
specs = json.load(open(DOCS + "/screws.json", encoding="utf-8"))
|
||
byid = {s["id"]: s for s in specs}
|
||
|
||
d["parts"] = [p for p in d["parts"] if not p.get("screw")] # 幂等:先清旧螺钉
|
||
mx = max(p["idx"] for p in d["parts"])
|
||
|
||
inst = instances(M["m2_bcd"], M["m2_clock"])
|
||
cnt = {}
|
||
for sid, *_ in inst:
|
||
cnt[sid] = cnt.get(sid, 0) + 1
|
||
for sid, s in byid.items(): # 与清单数量自洽校验
|
||
assert cnt.get(sid) == s["数量"], ("实例数与清单不符", sid, cnt.get(sid), s["数量"])
|
||
|
||
geo = {sid: _geom(s) for sid, s in byid.items()}
|
||
seen, new = {}, []
|
||
for sid, hp, ax, grp, short, detail in inst:
|
||
g = dict(geo[sid])
|
||
clip = ""
|
||
if sid in LMAX: # 螺杆穿出底座底面后伸向未建模的工作台,按可见段截断
|
||
g["L"] = min(g["L"], LMAX[sid])
|
||
clip = ";螺杆伸出底座底面(伸入工作台)的部分按可见段截断"
|
||
V, T = _mesh(g)
|
||
n = seen.get(sid, 0) + 1
|
||
seen[sid] = n
|
||
mx += 1
|
||
s = byid[sid]
|
||
new.append(dict(
|
||
name="%s_%d" % (sid, n),
|
||
cn="%s · %s %d/%d" % (s["规格"], short, n, s["数量"]),
|
||
v=place(V, hp, ax), t=T, color=COLOR, printed=False, vol=0.0,
|
||
note="%s %s — %s:%s%s;%s" % (s["规格"], s["头型"], s["位置"], detail, clip, s["备注"]),
|
||
grp=grp, jaw=0.0, ex=[0.0, 0.0, 0.0], idx=mx,
|
||
screw=True, spec=sid,
|
||
hl=[round(hp[0], 2), round(hp[2], 2), round(-hp[1], 2)])) # 查看器系头面点 (x,z,-y)
|
||
|
||
d["parts"] += new
|
||
tmp = path + ".tmp"
|
||
with open(tmp, "w", encoding="utf-8") as f:
|
||
json.dump(d, f, ensure_ascii=False, separators=(",", ":"))
|
||
os.replace(tmp, path)
|
||
print("螺钉实例 %d 颗已注入:%s" % (len(new), " / ".join("%s:%d" % (k, cnt[k]) for k in sorted(cnt))))
|
||
print("零件数 %d -> %d,文件 %.1f MB"
|
||
% (len(d["parts"]) - len(new), len(d["parts"]), os.path.getsize(path) / 1e6))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||
sys.stdout.flush()
|
||
os._exit(0)
|