Files
cbb 531a293688 refactor(viewer): 单文件查看器拆成 webapp/ 多文件,走本地 http;Worker 换成真文件
前端不再是"一个 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 穿透。
2026-10-15 04:31:31 +08:00

348 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)