feat: 新增螺丝网格生成与强度校核功能,重建模型数据与查看器,补充定位图与BOM文档

新增 screw_mesh.py 螺丝网格划分模块与 strength_check.py 强度校核逻辑,arm_model.py 同步集成网格生成与校核流程;viewer_tpl.html、web/index.html 与 web/机器人手臂3D查看器.html 更新查看器模板以支持新数据展示;docs/bom.json 与 docs/travel_detail.json 调整 BOM 清单与行程参数;docs/print_settings.md 新增打印设置说明,新增螺丝采购清单 HTML;补充大量定位图与验证截图。
This commit is contained in:
cbb
2026-09-28 03:33:27 +08:00
parent 5518d12abb
commit 79c1265202
41 changed files with 1265 additions and 182 deletions
+14 -14
View File
@@ -1,6 +1,6 @@
[
{
"件号": "P01_A_立柱下段",
"件号": "P01_A_立柱下段_三角形30%_墙4底5_竖打",
"名称": "立柱下段",
"类型": "3D打印",
"数量": 1,
@@ -10,7 +10,7 @@
"说明": "200mm适配分段A:高166mm+16mm阶梯公榫+非对称M4防反装通孔+底部50x50x12榫头+P01-A 竖排TOP刻字"
},
{
"件号": "P01_B_立柱中段",
"件号": "P01_B_立柱中段_三角形30%_墙4底5_竖打",
"名称": "立柱中段",
"类型": "3D打印",
"数量": 1,
@@ -20,7 +20,7 @@
"说明": "200mm适配分段B:高165mm+双端公母榫+非对称M4防反装通孔+P01-B 竖排TOP刻字"
},
{
"件号": "P01_C_立柱上段",
"件号": "P01_C_立柱上段_三角形30%_墙4底5_竖打",
"名称": "立柱上段",
"类型": "3D打印",
"数量": 1,
@@ -30,7 +30,7 @@
"说明": "200mm适配分段C:高149mm+底端母榫+非对称M4防反装通孔+顶面P03安装台+P01-C 竖排TOP刻字"
},
{
"件号": "P02_Center_底座中核心",
"件号": "P02_Center_底座中核心_螺旋体30%_墙5底6_平打",
"名称": "底座核心承力座",
"类型": "3D打印",
"数量": 1,
@@ -40,7 +40,7 @@
"说明": "200mm适配中座:195x190mm核心承力座+48mm套筒+12mm阶梯榫座(轴向面承载)+前后三角加强筋+前后非对称防呆母榫槽+刻字"
},
{
"件号": "P02_Front_前向防翻力臂",
"件号": "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打",
"名称": "前向防翻力臂",
"类型": "3D打印",
"数量": 1,
@@ -50,7 +50,7 @@
"说明": "200mm适配前臂:145x190mm扩展力臂+104mm宽跨距双公榫+底部M5预埋螺母沉槽+P02-FRONT刻字"
},
{
"件号": "P02_Rear_后向防翻力臂",
"件号": "P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打",
"名称": "后向防翻力臂",
"类型": "3D打印",
"数量": 1,
@@ -60,7 +60,7 @@
"说明": "200mm适配后臂:45x190mm扩展力臂+80mm窄跨距双公榫+底部M5预埋螺母沉槽+P02-REAR刻字"
},
{
"件号": "P03_肩吊臂",
"件号": "P03_肩吊臂_螺旋体35%_墙5底5_竖打",
"名称": "肩关节安装悬臂",
"类型": "3D打印",
"数量": 1,
@@ -70,7 +70,7 @@
"说明": "高强多肋机架:10mm后置法兰+双侧8mm大三角加强筋+下腹12mm抗弯龙骨筋+侧开式电机托架(M1从+Y侧滑入装配,前后法兰8xM3双向锁死)+前置止脱锁紧法兰(4xM3止脱螺孔)+大散热窗与对流槽"
},
{
"件号": "P04_肩叉",
"件号": "P04_肩叉_螺旋体30%_墙5底5_竖打",
"名称": "肩叉",
"类型": "3D打印",
"数量": 1,
@@ -80,17 +80,17 @@
"说明": "加长避让版:头部与身子前移加长至X=10.5,与P03锁紧法兰保持2.0mm轴向安全间隙(不做前移,保持输出端最短悬伸力臂),J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"
},
{
"件号": "P05_大臂旋转座",
"件号": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
"名称": "大臂旋转座",
"类型": "3D打印",
"数量": 1,
"体积cm3": 159.5,
"质量g_PLA": 197.8,
"包络": "[-16.5 93.0 389.0 .. 53.5 174.0 525.0] (70.0x81.0x136.0)",
"说明": "V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J2全角度0干涉"
"说明": "V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"
},
{
"件号": "P06_大臂骨架",
"件号": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
"名称": "大臂骨架",
"类型": "3D打印",
"数量": 1,
@@ -100,7 +100,7 @@
"说明": "Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体,单实体0干涉"
},
{
"件号": "P07_小臂骨架",
"件号": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
"名称": "小臂骨架",
"类型": "3D打印",
"数量": 1,
@@ -110,7 +110,7 @@
"说明": "V2 L型结构:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉"
},
{
"件号": "P08_腕偏转座",
"件号": "P08_腕偏转座_螺旋体25%_墙4底5_竖打",
"名称": "腕部转接座",
"类型": "3D打印",
"数量": 1,
@@ -120,7 +120,7 @@
"说明": "V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉"
},
{
"件号": "P09_工具法兰",
"件号": "P09_工具法兰_螺旋体100%_墙4底5_竖打",
"名称": "末端工具法兰",
"类型": "3D打印",
"数量": 1,
+30
View File
@@ -0,0 +1,30 @@
# 3D 打印参数单(零件 × 填充/设置)
> 打印配方已写进零件名(STEP/STL 文件名、BOM 件号、查看器标签同源):`件号_图案<密度>_墙<W>底<T>_<姿态>`。图案名一律用 slicer 原名(螺旋体/三角形)。层间是最弱面 → 主力件用跨层图案;已知竖直受压件用三角形。
## 逐件参数(件号 = 文件名,直接对照切片)
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(Modifier) |
|---|---|---|---|---|
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
| P02_Center_底座中核心_螺旋体30%_墙5底6_平打 | 螺旋体 30% | 5 / 6 | 平打 | Ø48 套筒 + 12mm 阶梯榫座 |
| P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
| P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 10mm 后法兰孔 + J1 电机座接触面 |
| P04_肩叉_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | J1 输出凸台贯通区(整段实心) |
| P05_大臂旋转座_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | J2 接口孔周 |
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
| P07_小臂骨架_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | 根部加厚顶板区 + M5 电机半包座 |
| P08_腕偏转座_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | J5 接口孔周 |
| P09_工具法兰_螺旋体100%_墙4底5_竖打 | 螺旋体 100% | 4 / 5 | 竖打 | — |
## 通用规则
1. **打印方向**:全部按模型姿态打,弯曲发生在层面内,弯矩交给顶/底实心层和墙,不让层间缝扛拉。
2. **螺纹孔一律不切螺纹**:M3/M4/M5 全部用黄铜热熔螺母;孔周按 1.5~2 倍螺栓直径加载实心 Modifier。P02 力臂的 M5 螺母沉槽同此处理。
3. **墙数和顶底壳优先于密度**:抗弯主力是墙+顶底实心层,填充件是剪力腹板。密度只在小区域加(关节毂 30~35%),不要全局堆高。
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
6. 立柱 P01 三段用三角形是唯一例外:轴压方向已知,Z 向斜撑跨层传力比螺旋体更对口。
+12 -12
View File
@@ -16,8 +16,8 @@
"jams": {
"12.0": [
[
"P01_A_立柱下段",
"P08_腕偏转座",
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
3161.1,
[
-53.9,
@@ -28,8 +28,8 @@
],
"-108.0": [
[
"P04_肩叉",
"P05_大臂旋转座",
"P04_肩叉_螺旋体30%_墙5底5_竖打",
"P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
16.9,
[
56.3,
@@ -51,8 +51,8 @@
"jams": {
"18.0": [
[
"P01_A_立柱下段",
"P09_工具法兰",
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
29.2,
[
-82.0,
@@ -63,8 +63,8 @@
],
"-114.0": [
[
"P06_大臂骨架",
"P07_小臂骨架",
"P06_大臂骨架_螺旋体25%_墙4底5_竖打",
"P07_小臂骨架_螺旋体30%_墙5底5_竖打",
56.3,
[
18.5,
@@ -81,8 +81,8 @@
"jams": {
"36.0": [
[
"P01_A_立柱下段",
"P09_工具法兰",
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
45.9,
[
-82.4,
@@ -93,8 +93,8 @@
],
"-98.0": [
[
"P07_小臂骨架",
"P08_腕偏转座",
"P07_小臂骨架_螺旋体30%_墙5底5_竖打",
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
328.6,
[
58.4,
+11 -11
View File
@@ -443,17 +443,17 @@ def collide():
ps = [p for p in PARTS if p["vol"] > 1.0]
pairs = []
MOUNT_PAIRS = {
("P03_肩吊臂", "M1_J1"), ("P04_肩叉", "M1_J1"),
("P04_肩叉", "M2_J2"), ("P05_大臂旋转座", "M2_J2"),
("P05_大臂旋转座", "M3_J3"), ("P06_大臂骨架", "M3_J3"),
("P06_大臂骨架", "M4_J4"), ("P07_小臂骨架", "M4_J4"),
("P07_小臂骨架", "M5_J5"), ("P08_腕偏转座", "M5_J5"),
("P08_腕偏转座", "M6_J6"), ("P09_工具法兰", "M6_J6"),
("P01_A_立柱下段", "P01_B_立柱中段"), ("P01_B_立柱中段", "P01_C_立柱上段"),
("P01_A_立柱下段", "P02_Center_底座中核心"),
("P02_Center_底座中核心", "P02_Front_前向防翻力臂"),
("P02_Center_底座中核心", "P02_Rear_后向防翻力臂"),
("P01_C_立柱上段", "P03_肩吊臂"),
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "M1_J1"), ("P04_肩叉_螺旋体30%_墙5底5_竖打", "M1_J1"),
("P04_肩叉_螺旋体30%_墙5底5_竖打", "M2_J2"), ("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M2_J2"),
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "P01_B_立柱中段_三角形30%_墙4底5_竖打"), ("P01_B_立柱中段_三角形30%_墙4底5_竖打", "P01_C_立柱上段_三角形30%_墙4底5_竖打"),
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "P02_Center_底座中核心_螺旋体30%_墙5底6_平打"),
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打"),
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打"),
("P01_C_立柱上段_三角形30%_墙4底5_竖打", "P03_肩吊臂_螺旋体35%_墙5底5_竖打"),
}
for i in range(len(ps)):
for j in range(i + 1, len(ps)):
+69 -45
View File
@@ -653,7 +653,7 @@ def p_girdle(j1):
def p_fork(j1, j2):
"""P04_肩叉 (Shoulder Fork / L-Bracket) - 加长避让顺滑流线版:
"""P04_肩叉_螺旋体30%_墙5底5_竖打 (Shoulder Fork / L-Bracket) - 加长避让顺滑流线版:
1. 头部与身子起点自 X=5.5 前移加长至 X=10.5,与 P03 前置锁紧法兰 (X=8.5) 保持 2.0mm 均匀轴向安全间隙
(X_FORK_GAP=0:不做整体前移,保持 J1 输出端最短悬伸力臂,避免额外弯矩负荷);
2. J1 从动法兰圆盘已删除(盘面在 column 前缘形成鼓楞):J1 由 boss(Ø35.6)贯通 P03 前盖中心孔直连电机输出盘;column 顶边保持 Z=646.5 与 P03 环齐高,R=18mm 大圆弧顺滑过渡;
@@ -772,32 +772,30 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket"):
s = s.fuse(disc)
# 3. 前侧从底端 Ø70mm 圆柱到顶端法兰的有机相切平滑渐变过渡体 (Loft Morph)
# 后缘不取恒定 v=-5(那会在 y=123 处相对 R35 柱面 (x=53.14) 外凸 0.36mm,
# 形成一条 z 向竖鳍,并在顶端被 cut_top_solid 切出 z=452 硬台面):
# 改为从柱面 (v=0, y=128) 平滑长到顶壁 (v=+29, y=157):
# - 低端后角落在 R35 柱面上,与立柱共面零外凸,底端裙边不再有拼接竖筋;
# - 高端后角与 top_fill 矩形 (y 157..174) 齐平,z=452 台面与 R2 小圆角
# 不再需要,立柱到竖直壁完全由放样曲面相切过渡。
Z_top = z_pitch - 35.0
z_start = z_bot
N = 24
N = 16
wp = cq.Workplane("XY")
for i in range(N + 1):
t = i / float(N)
z_curr = z_start + t * (Z_top - z_start)
st = 3.0 * (t**2) - 2.0 * (t**3)
sb = 6.0 * (t**5) - 15.0 * (t**4) + 10.0 * (t**3) # 后缘 smoothstep(两端切线水平)
y_center = y_cyl_max + st * (y_out - y_cyl_max)
y_side = HL.Y_AXIS + st * (y_out - HL.Y_AXIS)
v_c = y_center - HL.Y_AXIS
v_s = y_side - HL.Y_AXIS
v_b = sb * (y_hub_in - HL.Y_AXIS) # 0 (柱面) -> +29 (顶壁)
v_mid = max(v_c, v_s + 0.02)
# 后缘两角 R2 圆角:把 y=123 凸包与 R35 柱面交汇的两条 90° 竖棱圆润化
# (磨掉"凸起来的"尖角,不动后缘高度/位置,打印无悬挂)
FR = 2.0
w = (cq.Workplane("XY", origin=(HL.X_ARM, HL.Y_AXIS, z_curr))
.moveTo(-35.0, v_s)
.threePointArc((0.0, v_mid), (35.0, v_s))
.lineTo(35.0, v_b)
.lineTo(-35.0, v_b)
.lineTo(35.0, -5.0 + FR)
.radiusArc((35.0 - FR, -5.0), FR)
.lineTo(-35.0 + FR, -5.0)
.radiusArc((-35.0, -5.0 + FR), FR)
.lineTo(-35.0, v_s)
.close())
wp = wp.add(w)
@@ -865,12 +863,12 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket"):
def p_shoulder_roll_base(j2, j3, half=None):
"""P05_大臂旋转座(深度圆角优化版)"""
return make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3, name="P05_大臂旋转座")
"""P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版)"""
return make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3, name="P05_大臂旋转座_螺旋体30%_墙4底5_竖打")
def p_humerus_bone(j3, j4):
"""P06_大臂骨架 (Humerus Bone - Loft 圆弧转方有机过渡版):
"""P06_大臂骨架_螺旋体25%_墙4底5_竖打 (Humerus Bone - Loft 圆弧转方有机过渡版):
受 J3 (大臂自转, -Z轴, hub@Z_HUB3=380) 驱动,
向下延伸构成大臂主体骨架,底端抱紧 M4 (肘部俯仰电机, +Y轴, 轴线位于 Z_EL=332.0)。
1. 借鉴 P05 大臂旋转座几何放样算法:
@@ -944,7 +942,7 @@ def p_humerus_bone(j3, j4):
def p_forearm_bone(j4, j5, half=None):
"""P07_小臂骨架 L型高刚度无干涉版(深度圆角优化版)
"""P07_小臂骨架_螺旋体30%_墙5底5_竖打 L型高刚度无干涉版(深度圆角优化版)
设计要点:
1. 改为与 P05/P08 统一的 L 型结构(L-bracket):
- +Y侧垂直背脊立柱(Y: y_hub_in..y_out),避开上级大臂骨架 P06(Y <= 155.5)
@@ -1060,12 +1058,12 @@ def p_forearm_bone(j4, j5, half=None):
def p_wrist_bracket(j5, j6):
"""P08_腕偏转座(深度圆角优化版)"""
return make_pitch_to_roll_bracket(j5, j6, Z_WR, Z_R6, name="P08_腕偏转座")
"""P08_腕偏转座_螺旋体25%_墙4底5_竖打(深度圆角优化版)"""
return make_pitch_to_roll_bracket(j5, j6, Z_WR, Z_R6, name="P08_腕偏转座_螺旋体25%_墙4底5_竖打")
def p_tool_flange(j6):
"""P09_工具法兰 深度圆角优化版
"""P09_工具法兰_螺旋体100%_墙4底5_竖打 深度圆角优化版
关键改进:
1. 外轮廓全圆角(R=4mm)
2. 法兰面倒角(2mm)
@@ -1136,17 +1134,17 @@ def build():
AB.CENTS.clear()
AB.GRP = {}
par = {"J1": "root", "J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"}
driven = {"J1": "P04_肩叉", "J2": "P05_大臂旋转座", "J3": "P06_大臂骨架",
"J4": "P07_小臂骨架", "J5": "P08_腕偏转座", "J6": "P09_工具法兰"}
driven = {"J1": "P04_肩叉_螺旋体30%_墙5底5_竖打", "J2": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "J3": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
"J4": "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "J5": "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "J6": "P09_工具法兰_螺旋体100%_墙4底5_竖打"}
motor_of = {"J1": "M1_J1", "J2": "M2_J2", "J3": "M3_J3",
"J4": "M4_J4", "J5": "M5_J5", "J6": "M6_J6"}
for t in ("J1", "J2", "J3", "J4", "J5", "J6"):
AB.GRP[motor_of[t]] = par[t]
AB.GRP[driven[t]] = t
AB.GRP.update({
"P01_A_立柱下段": "root", "P01_B_立柱中段": "root", "P01_C_立柱上段": "root",
"P02_Center_底座中核心": "root", "P02_Front_前向防翻力臂": "root", "P02_Rear_后向防翻力臂": "root",
"P03_肩吊臂": "root"
"P01_A_立柱下段_三角形30%_墙4底5_竖打": "root", "P01_B_立柱中段_三角形30%_墙4底5_竖打": "root", "P01_C_立柱上段_三角形30%_墙4底5_竖打": "root",
"P02_Center_底座中核心_螺旋体30%_墙5底6_平打": "root", "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打": "root", "P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打": "root",
"P03_肩吊臂_螺旋体35%_墙5底5_竖打": "root"
})
AB.JAW = {}
@@ -1155,31 +1153,31 @@ def build():
p02_c, p02_f, p02_r = make_p02_poka_yoke()
parts_to_build = [
("P01_A_立柱下段", "立柱下段", lambda: p01_a, 0x0284c7,
("P01_A_立柱下段_三角形30%_墙4底5_竖打", "立柱下段", lambda: p01_a, 0x0284c7,
"200mm适配分段A:高166mm+16mm阶梯公榫+非对称M4防反装通孔+底部50x50x12榫头+P01-A 竖排TOP刻字"),
("P01_B_立柱中段", "立柱中段", lambda: p01_b, 0x2563eb,
("P01_B_立柱中段_三角形30%_墙4底5_竖打", "立柱中段", lambda: p01_b, 0x2563eb,
"200mm适配分段B:高165mm+双端公母榫+非对称M4防反装通孔+P01-B 竖排TOP刻字"),
("P01_C_立柱上段", "立柱上段", lambda: p01_c, 0x1d4ed8,
("P01_C_立柱上段_三角形30%_墙4底5_竖打", "立柱上段", lambda: p01_c, 0x1d4ed8,
"200mm适配分段C:高149mm+底端母榫+非对称M4防反装通孔+顶面P03安装台+P01-C 竖排TOP刻字"),
("P02_Center_底座中核心", "底座核心承力座", lambda: p02_c, 0x475569,
("P02_Center_底座中核心_螺旋体30%_墙5底6_平打", "底座核心承力座", lambda: p02_c, 0x475569,
"200mm适配中座:195x190mm核心承力座+48mm套筒+12mm阶梯榫座(轴向面承载)+前后三角加强筋+前后非对称防呆母榫槽+刻字"),
("P02_Front_前向防翻力臂", "前向防翻力臂", lambda: p02_f, 0xe11d48,
("P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打", "前向防翻力臂", lambda: p02_f, 0xe11d48,
"200mm适配前臂:145x190mm扩展力臂+104mm宽跨距双公榫+底部M5预埋螺母沉槽+P02-FRONT刻字"),
("P02_Rear_后向防翻力臂", "后向防翻力臂", lambda: p02_r, 0xd97706,
("P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打", "后向防翻力臂", lambda: p02_r, 0xd97706,
"200mm适配后臂:45x190mm扩展力臂+80mm窄跨距双公榫+底部M5预埋螺母沉槽+P02-REAR刻字"),
("P03_肩吊臂", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
"高强多肋机架:10mm后置法兰+双侧8mm大三角加强筋+下腹12mm抗弯龙骨筋+侧开式电机托架(M1从+Y侧滑入装配,前后法兰8xM3双向锁死)+前置止脱锁紧法兰(4xM3止脱螺孔)+大散热窗与对流槽"),
("P04_肩叉", "肩叉", lambda: p_fork(j1, j2), 0x2563eb,
("P04_肩叉_螺旋体30%_墙5底5_竖打", "肩叉", lambda: p_fork(j1, j2), 0x2563eb,
"加长避让版:头部与身子前移加长至X=10.5,与P03锁紧法兰保持2.0mm轴向安全间隙(不做前移,保持输出端最短悬伸力臂),J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉"),
("P05_大臂旋转座", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
"V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J2全角度0干涉"),
("P06_大臂骨架", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
"V3圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂),保证J2全角度0干涉"),
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
"Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体,单实体0干涉"),
("P07_小臂骨架", "小臂骨架", lambda: p_forearm_bone(j4, j5, None), 0x7c3aed,
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "小臂骨架", lambda: p_forearm_bone(j4, j5, None), 0x7c3aed,
"V2 L型结构:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉"),
("P08_腕偏转座", "腕部转接座", lambda: p_wrist_bracket(j5, j6), 0xd97706,
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "腕部转接座", lambda: p_wrist_bracket(j5, j6), 0xd97706,
"V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉"),
("P09_工具法兰", "末端工具法兰", lambda: p_tool_flange(j6), 0xdc2626,
("P09_工具法兰_螺旋体100%_墙4底5_竖打", "末端工具法兰", lambda: p_tool_flange(j6), 0xdc2626,
"V2圆角:外轮廓R=3mm,法兰面倒角2mm"),
]
@@ -1212,19 +1210,19 @@ SEP = 74.0
def explode_chain(p, JS, cents, gmean):
"""爆炸图偏移:立柱垂直阶梯拉开,底座前后防呆拉开,电机沿轴拉出,机械臂整体随上段分离"""
nm = p["name"]
if nm == "P01_A_立柱下段":
if nm == "P01_A_立柱下段_三角形30%_墙4底5_竖打":
return [0.0, 0.0, 0.0]
if nm == "P01_B_立柱中段":
if nm == "P01_B_立柱中段_三角形30%_墙4底5_竖打":
return [0.0, 0.0, 60.0]
if nm == "P01_C_立柱上段":
if nm == "P01_C_立柱上段_三角形30%_墙4底5_竖打":
return [0.0, 0.0, 120.0]
if nm == "P02_Center_底座中核心":
if nm == "P02_Center_底座中核心_螺旋体30%_墙5底6_平打":
return [0.0, 0.0, 0.0]
if nm == "P02_Front_前向防翻力臂":
if nm == "P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打":
return [65.0, 0.0, 0.0]
if nm == "P02_Rear_后向防翻力臂":
if nm == "P02_Rear_后向防翻力臂_螺旋体30%_墙4底5_平打":
return [-65.0, 0.0, 0.0]
if nm == "P03_肩吊臂":
if nm == "P03_肩吊臂_螺旋体35%_墙5底5_竖打":
return [0.0, 0.0, 120.0]
grp = AB.GRP.get(p["name"], "root")
@@ -1266,6 +1264,15 @@ if __name__ == "__main__":
AB.explode_vec = explode_chain
bom = AB.export(JS)
# 紧固件(螺钉)实例注入:把 docs/screws.json 的 72 颗螺钉按真实孔位还原成三维实例,
# 追加进 web/mesh_arm.json 的 parts(查看器可点 BOM 螺丝行高亮定位;纯数据,不动 CAD)
try:
import screw_mesh
screw_mesh.main()
except Exception as e:
print("螺钉实例注入失败(不影响主流程,可手动跑 src/screw_mesh.py):", e, flush=True)
cl = AB.collide()
try:
@@ -1274,6 +1281,23 @@ if __name__ == "__main__":
except Exception as e:
print("rebuild_viewer error:", e, flush=True)
# 可达包络自动注入:AB.export 刚重写过 mesh_arm.json(包络件会被冲掉),
# 这里重跑 FK 采样 + 包络网格注入,再重打包一次查看器(约 3~4s,纯数据无 CAD 计算)
try:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "scratch"))
import ws_sweep
import gen_env_mesh
t_env = time.time()
ws_sweep.main()
gen_env_mesh.main()
from build_viewer import rebuild_viewer as _rbv
_rbv()
print(f"-> 可达包络已自动注入查看器(+{time.time()-t_env:.1f}s)✓\n", flush=True)
except Exception as e:
print("可达包络自动注入失败(不影响主流程,修复后手动跑 scratch/gen_env_mesh.py + src/build_viewer.py):",
e, flush=True)
print("=" * 80, flush=True)
print("✅ 机器人手臂编译、导出与查看器发布全流程完成!", flush=True)
print("=" * 80, flush=True)
+6 -6
View File
@@ -84,12 +84,12 @@ X_CHEST = 6.0 # chest plate half thickness
# J5 pitch +Y in the forearm bone -> drives P08 腕偏转座
# J6 roll -Z in the wrist bracket -> drives P09 工具法兰
JOINTS = [
("J1", "肩抬举", (Z_R1, Y_AXIS, Z_RL), (1, 0, 0), (0, 1, 0), "P03_肩部基座", "P04_肩叉"),
("J2", "肩俯仰", (X_ARM, Y_REAR, Z_SH), (0, 1, 0), (1, 0, 0), "P04_肩叉", "P05_大臂旋转座"),
("J3", "大臂自转", (X_ARM, Y_AXIS, Z_R3), (0, 0, -1), (1, 0, 0), "P05_大臂旋转座", "P06_大臂骨架"),
("J4", "肘部俯仰", (X_ARM, Y_REAR, Z_EL), (0, 1, 0), (1, 0, 0), "P06_大臂骨架", "P07_小臂骨架"),
("J5", "腕部俯仰", (X_ARM, Y_REAR, Z_WR), (0, 1, 0), (1, 0, 0), "P07_小臂骨架", "P08_腕偏转座"),
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座", "P09_工具法兰"),
("J1", "肩抬举", (Z_R1, Y_AXIS, Z_RL), (1, 0, 0), (0, 1, 0), "P03_肩吊臂_螺旋体35%_墙5底5_竖打", "P04_肩叉_螺旋体30%_墙5底5_竖打"),
("J2", "肩俯仰", (X_ARM, Y_REAR, Z_SH), (0, 1, 0), (1, 0, 0), "P04_肩叉_螺旋体30%_墙5底5_竖打", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打"),
("J3", "大臂自转", (X_ARM, Y_AXIS, Z_R3), (0, 0, -1), (1, 0, 0), "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "P06_大臂骨架_螺旋体25%_墙4底5_竖打"),
("J4", "肘部俯仰", (X_ARM, Y_REAR, Z_EL), (0, 1, 0), (1, 0, 0), "P06_大臂骨架_螺旋体25%_墙4底5_竖打", "P07_小臂骨架_螺旋体30%_墙5底5_竖打"),
("J5", "腕部俯仰", (X_ARM, Y_REAR, Z_WR), (0, 1, 0), (1, 0, 0), "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "P08_腕偏转座_螺旋体25%_墙4底5_竖打"),
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "P09_工具法兰_螺旋体100%_墙4底5_竖打"),
]
AXIS = {t[0]: dict(rear=t[2], d=t[3], xdir=t[4]) for t in JOINTS}
+286
View File
@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
"""紧固件(螺钉)实例化网格生成器。
docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量 / 位置 / 备注)。
本模块把清单里的每一颗螺钉按其真实装配孔位还原成三维实例(承载头面坐标 + 拧入方向 +
随动关节组),生成轻量三角网格后追加进 web/mesh_arm.json 的 parts 中。
查看器因此可以:显示全部螺钉、点击 BOM 螺丝行高亮定位、点击螺钉反查清单。
实例坐标来源(与 arm_model.py 成孔代码一一对应,改模型后按同样规则复核):
头面 = 沉孔/通道的"承接台阶"(螺钉从外侧塞入,一直顶到孔径变小的那一圈台阶面),
因此每种规格的头都落在模型预留的沉孔里,不会露在零件表面之外。
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 局部 (±17.55, ±20.46, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
S04 P03 前止脱 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 57.5)(沉孔 Ø6.0 台阶 x=6.5)
S05 P01 分段对锁 4xM4:Ø4.4 贯穿柱身 x∈{-131,-93}, z∈{180,329},头在 -Y 立面 y=95(模型未设沉孔)
S06 P09 工具法兰 4xM4:R20 @0/90/180/270°,台阶 z=97(沉孔 Ø7.0 深 4mm,法兰底面 z=93)
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 安装台 4xM5 :x∈{-136,-94}, y∈{107,149},台阶 z=455(P03 榫头底面,Ø5.5 孔 454..495),
头藏在 P01 立柱内腔里(孔深即 M5x35:455 -> 490,与模型 454..495 完全吻合)
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模),
这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。
运行:python src/screw_mesh.py -> 注入网格(纯数据,秒级,不动 CAD)
再 python src/build_viewer.py -> 重新打包单文件查看器
arm_model.py 主流程末尾已自动调用本模块,重跑完整构建不会丢失螺钉。
"""
import json
import math
import os
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_HERE)
sys.path.insert(0, _HERE)
import human_layout as HL # noqa: E402
X_COL = -115.0 # 立柱中心(与 arm_model.py 一致)
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)即止(台面/预埋螺母未建模)
# ---------------------------------------------------------------- 局部坐标系工具
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° 锥,最宽处在承载面
return dict(kind="csk", rs=rs, L=L, rh=2.0 * nom)
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
head = _merge(_side(rs, g["rh"], -hh, 0.0, 14), _cap(rs, -hh, 14, up=False),
_cap(g["rh"], 0.0, 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]
for a in clock01:
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 in (-17.55, 17.55):
for v in (-20.46, 20.46):
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 u in (-17.55, 17.55):
for v in (-20.46, 20.46):
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
"P03 后置法兰", "35.1×40.9 孔距 -> M1 尾端面"))
for deg in (17.8, 107.8, 197.8, 287.8):
u = 27.5 * math.cos(math.radians(deg))
v = 27.5 * math.sin(math.radians(deg))
out.append(("S04", to_global("J1", u, v, 57.5), d1, "root",
"P03 前置止脱法兰", "BCD55 @%.1f° -> M1 前端面" % deg))
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 榫缝对锁(另配 M4 螺母)" % z))
for deg in (0.0, 90.0, 180.0, 270.0):
x = HL.X_ARM + 20.0 * math.cos(math.radians(deg))
y = HL.Y_AXIS + 20.0 * math.sin(math.radians(deg))
out.append(("S06", (x, y, 97.0), (0.0, 0.0, -1.0), "J6",
"P09 工具法兰", "R20 @%.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 - 21.0, X_COL + 21.0):
for y in (Y_COL - 21.0, Y_COL + 21.0):
out.append(("S09", (x, y, 455.0), (0.0, 0.0, -1.0), "root",
"P03 肩吊臂安装台", "穿透榫头 Ø5.5 孔(454..495)旋入立柱,头藏于 P01 立柱内腔"))
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 = os.path.join(_ROOT, "web", "mesh_arm.json")
print("读取 %s ..." % path, flush=True)
d = json.load(open(path, encoding="utf-8"))
M = d["measured"]["M"]
specs = json.load(open(os.path.join(_ROOT, "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()
+12 -12
View File
@@ -177,18 +177,18 @@ def cuts():
bend axis: 0=X (roll of the shoulder/forearm), 1=Y (every pitch joint), 2=Z."""
Y = HL.Y_AXIS
return [
("吊臂根座 @z=480", "P03_肩吊臂", (0, 0, 1), (0.0, 55.0, 480.0), "J1", 1),
("吊臂颊板 @z=560", "P03_肩吊臂", (0, 0, 1), (0.0, 100.0, 560.0), "J1", 1),
("肩叉腹板 @z=560", "P04_肩叉", (0, 0, 1), (HL.X_ARM, Y, 560.0), "J2", 1),
("肩叉腹板 @z=600", "P04_肩叉", (0, 0, 1), (HL.X_ARM, Y, 600.0), "J2", 1),
("大臂旋转座横梁 @z=480", "P05_大臂旋转座", (0, 0, 1), (HL.X_ARM, Y, 480.0), "J2", 1),
("大臂旋转座套筒 @z=440", "P05_大臂旋转座", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
("大臂骨架中段 @z=350", "P06_大臂骨架", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
("大臂骨架抱箍 @z=325", "P06_大臂骨架", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
("小臂骨架中段 @z=245", "P07_小臂骨架", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
("小臂骨架抱箍 @z=195", "P07_小臂骨架", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
("腕偏转座根部 @z=150", "P08_腕偏转座", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
("工具法兰 @z=55", "P09_工具法兰", (0, 0, 1), (HL.X_ARM, Y, 55.0), "J6", 0),
("吊臂根座 @z=480", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 55.0, 480.0), "J1", 1),
("吊臂颊板 @z=560", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 100.0, 560.0), "J1", 1),
("肩叉腹板 @z=560", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 560.0), "J2", 1),
("肩叉腹板 @z=600", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 600.0), "J2", 1),
("大臂旋转座横梁 @z=480", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 480.0), "J2", 1),
("大臂旋转座套筒 @z=440", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
("大臂骨架中段 @z=350", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
("大臂骨架抱箍 @z=325", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
("小臂骨架中段 @z=245", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
("小臂骨架抱箍 @z=195", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
("腕偏转座根部 @z=150", "P08_腕偏转座_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
("工具法兰 @z=55", "P09_工具法兰_螺旋体100%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 55.0), "J6", 0),
]
+6 -6
View File
@@ -49,12 +49,12 @@ from armcore import place, bb, DOCS
VOL_MIN = 2.0 # mm3,超过即计为干涉
CHAIN = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
MOUNT_PAIRS = {
("P03_肩吊臂", "M1_J1"), ("P04_肩叉", "M1_J1"),
("P04_肩叉", "M2_J2"), ("P05_大臂旋转座", "M2_J2"),
("P05_大臂旋转座", "M3_J3"), ("P06_大臂骨架", "M3_J3"),
("P06_大臂骨架", "M4_J4"), ("P07_小臂骨架", "M4_J4"),
("P07_小臂骨架", "M5_J5"), ("P08_腕偏转座", "M5_J5"),
("P08_腕偏转座", "M6_J6"), ("P09_工具法兰", "M6_J6"),
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "M1_J1"), ("P04_肩叉_螺旋体30%_墙5底5_竖打", "M1_J1"),
("P04_肩叉_螺旋体30%_墙5底5_竖打", "M2_J2"), ("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M2_J2"),
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
}
# 网格化预筛参数:外扩必须 ≥ 网格化偏差,判"分离"才保守可信
+165 -18
View File
@@ -91,6 +91,10 @@ small{color:var(--dim)}
.dk-note{color:var(--dim);font-size:11px;margin:1px 0 6px}
#screws td{white-space:normal}
#screws td:first-child{white-space:nowrap}
#screws tbody tr{cursor:pointer;transition:background .12s}
#screws tbody tr:hover{background:rgba(76,154,255,.14)}
#screws tbody tr.scrHl{background:#1d3f74;box-shadow:inset 3px 0 0 var(--warn)}
.hudlink{pointer-events:auto;cursor:pointer;color:var(--ac);text-decoration:underline;margin-left:8px}
@media (max-width:760px){#bomTab{top:auto;bottom:36px}#bomDock{top:46px;bottom:26px}}
</style>
</head>
@@ -111,7 +115,9 @@ small{color:var(--dim)}
<option value="wrist">腕部特写</option><option value="joint">关节特写</option><option value="elbow">肘部特写</option><option value="hand">手部特写</option>
</select>
<button id="bWire">线框</button>
<button id="bXRay" class="on">电机半透</button>
<button id="bXRay">电机半透</button>
<button id="bScr" class="on" title="显示/隐藏全部螺钉(S01~S10);点右侧 BOM 螺丝行可在模型中高亮定位">螺钉</button>
<button id="bEnv" title="显示/隐藏可达外包络(运动学边界)">可达包络</button>
<button id="bFit">复位</button>
<button id="bL" title="显示/隐藏左栏">左栏</button>
</header>
@@ -138,7 +144,7 @@ small{color:var(--dim)}
</div>
<footer>
<span>左键旋转 · 滚轮缩放 · 右键平移(拖动) · 单击选中高亮 · <b>右键单击隐藏部件(或按H键)</b> · 左侧全选/勾选恢复</span>
<span>左键旋转 · 滚轮缩放 · 右键平移(拖动) · 单击选中高亮 · <b>BOM 点螺丝行 = 高亮定位该规格螺钉(穿透显示)</b> · <b>右键单击隐藏部件(或按H键)</b> · 左侧全选/勾选恢复</span>
<span class="sp"></span>
<span id="stat"></span>
</footer>
@@ -212,6 +218,25 @@ for (const p of DATA.parts) {
const geo = new THREE.BufferGeometry();
const pos = new Float32Array(p.v.length * 3);
for (let i = 0; i < p.v.length; i++) { pos[i*3] = p.v[i][0]; pos[i*3+1] = p.v[i][2]; pos[i*3+2] = -p.v[i][1]; }
p.triCount = p.t ? p.t.length : 0;
p.vertCount = p.v.length;
const isEnv = p.name.slice(0, 4) === 'ENV_'; // 可达包络:运动学边界,不随关节动
if (p.kind === 'line') { // 经纬线:LineSegments,无三角面
const sIdx = new Uint32Array(p.s);
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
geo.setIndex(new THREE.BufferAttribute(sIdx, 1));
geo.computeBoundingSphere(); geo.computeBoundingBox();
const lm = new THREE.LineBasicMaterial({color: p.color, transparent: true, opacity: 0.8});
const ls = new THREE.LineSegments(geo, lm);
ls.userData = p;
ls.position.set(p.ex[0], p.ex[2], -p.ex[1]);
ls.base = ls.position.clone();
ls.visible = !isEnv; // 包络默认隐藏,走开关显示
JG[p.grp || 'root'].add(ls);
meshes.push(ls); byName[p.name] = ls;
delete p.v; delete p.s;
continue;
}
const idx = new Uint32Array(p.t.length * 3);
// the vertex map (x,y,z)->(x,z,-y) has determinant +1, so triangle winding must be kept
// as-is; swapping two indices here inverted every face and made the normals point inward
@@ -221,19 +246,22 @@ for (const p of DATA.parts) {
geo.computeVertexNormals();
geo.computeBoundingSphere();
geo.computeBoundingBox();
p.triCount = p.t.length;
p.vertCount = p.v.length;
delete p.v;
delete p.t;
const isMo = !p.printed && p.name[0] === 'M';
const isScr = !!p.screw; // 紧固件(螺钉):金属亮色,可被 BOM 行高亮
const mat = new THREE.MeshStandardMaterial({
color: p.color, metalness: isMo ? .55 : .05, roughness: isMo ? .42 : .78,
transparent: isMo, opacity: isMo ? .34 : 1, flatShading: false, side: THREE.FrontSide});
color: p.color, metalness: isScr ? .78 : (isMo ? .55 : .05),
roughness: isScr ? .30 : (isMo ? .42 : .78),
transparent: isEnv, opacity: isEnv ? .06 : 1,
depthWrite: !isEnv,
flatShading: false, side: isEnv ? THREE.DoubleSide : THREE.FrontSide});
const ms = new THREE.Mesh(geo, mat);
ms.userData = p;
ms.position.set(p.ex[0], p.ex[2], -p.ex[1]);
ms.base = ms.position.clone();
ms.zero = [0,0,0];
if (isEnv) ms.visible = false; // 包络默认隐藏,走开关显示
JG[p.grp || 'root'].add(ms);
meshes.push(ms); byName[p.name] = ms;
}
@@ -286,7 +314,8 @@ let BB = [[1e9,1e9,1e9],[-1e9,-1e9,-1e9]];
/* robust global bounds in the viewer frame (x, z, -y) */
(function(){
let lo=[1e9,1e9,1e9], hi=[-1e9,-1e9,-1e9];
for (const m of meshes) { const b=m.geometry.boundingBox;
for (const m of meshes) { if (m.userData.name.slice(0,4) === 'ENV_') continue; // 取景只按手臂本体,包络不入包围盒
const b=m.geometry.boundingBox;
const a=[b.min.x+m.base.x,b.min.y+m.base.y,b.min.z+m.base.z];
const c=[b.max.x+m.base.x,b.max.y+m.base.y,b.max.z+m.base.z];
for(let i=0;i<3;i++){lo[i]=Math.min(lo[i],a[i]);hi[i]=Math.max(hi[i],c[i]);} }
@@ -381,6 +410,7 @@ window.addEventListener('keydown', e => {
if (e.key === 'h' || e.key === 'H' || e.key === 'Delete') {
if (sel) hidePart(sel);
} else if (e.key === 'Escape') {
clearScrewHl();
if (sel) {
sel.material.emissive.setHex(0);
sel.material.emissiveIntensity = 0;
@@ -398,18 +428,21 @@ cv.addEventListener('click', e => {
if (o === sel) {
if (sel) { sel.material.emissive.setHex(0); sel.material.emissiveIntensity = 0; sel = null; }
const tb = document.querySelector('#bom tbody'); if (tb) for (const tr of tb.children) tr.style.background = '';
const hud = document.getElementById('hud'); if (hud) hud.textContent = '';
clearScrewHl();
const hud = document.getElementById('hud'); if (hud) { hud.textContent = ''; delete hud.dataset.scrHl; }
requestRender();
return;
}
if (sel) { sel.material.emissive.setHex(0); sel.material.emissiveIntensity = 0; }
if (sel && !sel.userData.screw) { sel.material.emissive.setHex(0); sel.material.emissiveIntensity = 0; }
sel = o;
if (sel) { sel.material.emissive.setHex(0x2f6bff); sel.material.emissiveIntensity = .55; }
if (sel && !sel.userData.screw) { sel.material.emissive.setHex(0x2f6bff); sel.material.emissiveIntensity = .55; }
const tb = document.querySelector('#bom tbody'); for (const tr of tb.children) tr.style.background = '';
const hud = document.getElementById('hud');
hud.textContent = '';
delete hud.dataset.scrHl;
if (sel) {
const u = sel.userData;
if (u.screw) { highlightSpec(u.spec, false); requestRender(); return; } // 点中螺钉 -> 反查 BOM 行并定位
hud.innerHTML = '<b>#' + u.idx + ' ' + u.cn + '</b> · ' + (u.vol / 1000).toFixed(1) +
' cm3 · ' + (u.printed ? '打印件' : '电机/已有件') +
' <span style="margin-left:8px;color:var(--warn);font-size:11px">[右键单击或按H键隐藏此部件]</span>';
@@ -450,9 +483,10 @@ if (bRstJ) bRstJ.onclick = () => {
};
/* ---- UI: part tree ---- */
const th = document.getElementById('tree');
const groups = [['打印件(新建)', m => m.userData.printed],
const groups = [['可达包络(运动学)', m => m.userData.name.slice(0,4) === 'ENV_'],
['打印件(新建)', m => m.userData.printed],
['电机模组(复用)', m => !m.userData.printed && m.userData.name[0]==='M'],
['端面齿联轴器/抱箍(已有)', m => !m.userData.printed && m.userData.name[0]!=='M']];
['端面齿联轴器/抱箍(已有)', m => !m.userData.printed && !m.userData.screw && m.userData.name[0]!=='M' && m.userData.name.slice(0,4) !== 'ENV_']];
for (const [gt, test] of groups) {
const ms = meshes.filter(test); if (!ms.length) continue;
const h = document.createElement('div'); h.className = 'gr'; h.textContent = gt + ' · ' + ms.length;
@@ -480,11 +514,42 @@ for (const [gt, test] of groups) {
th.appendChild(r);
}
}
/* ---- 紧固件(螺钉)分组:按规格聚合,一行 = 一种规格,勾选/点击作用于该规格全部实例 ---- */
const scrAll = meshes.filter(m => m.userData.screw);
const bySpec = {}, specChk = {};
for (const m of scrAll) (bySpec[m.userData.spec] = bySpec[m.userData.spec] || []).push(m);
if (scrAll.length) {
const h = document.createElement('div'); h.className = 'gr';
h.textContent = '紧固件 / 螺钉(按规格聚合) · ' + Object.keys(bySpec).length + ' 种 ' +
scrAll.length + ' 颗(点击名称定位)';
th.appendChild(h);
for (const sid of Object.keys(bySpec).sort()) {
const ms = bySpec[sid], p0 = ms[0].userData;
const r = document.createElement('div'); r.className = 'row';
r.innerHTML = '<input type="checkbox" checked><span class="chip" style="background:#' +
p0.color.toString(16).padStart(6,'0') + '"></span><label title="' + p0.note + '">' +
'<span class="ex">' + sid + ' ' + p0.cn.split(' · ')[0] + '</span></label>' +
'<span class="q">' + ms.length + ' 颗</span>';
const chk = r.querySelector('input');
specChk[sid] = chk;
chk.onchange = e => {
for (const m of ms) {
m.visible = e.target.checked;
if (!m.visible && m === sel) { sel.material.emissive.setHex(0); sel = null; }
}
requestRender();
};
r.querySelector('label').onclick = () => highlightSpec(sid);
for (const m of ms) m.chk = chk;
th.appendChild(r);
}
}
document.getElementById('allOn').onclick = () => {
meshes.forEach(m => {
m.visible = true;
if (m.chk) m.chk.checked = true;
});
bEnv.classList.toggle('on', meshes.some(m => m.userData.name.slice(0,4) === 'ENV_' && m.visible));
requestRender();
};
document.getElementById('allOff').onclick = () => {
@@ -493,6 +558,9 @@ document.getElementById('allOff').onclick = () => {
if (m.chk) m.chk.checked = false;
if (sel) { sel.material.emissive.setHex(0); sel = null; }
});
bEnv.classList.remove('on');
bScr.classList.remove('on');
clearScrewHl();
requestRender();
};
document.getElementById('onlyPrint').onclick = () => {
@@ -501,6 +569,9 @@ document.getElementById('onlyPrint').onclick = () => {
if (m.chk) m.chk.checked = m.visible;
if (!m.visible && m === sel) { sel.material.emissive.setHex(0); sel = null; }
});
bEnv.classList.remove('on');
bScr.classList.remove('on');
clearScrewHl();
requestRender();
};
/* ---- BOM ---- */
@@ -532,16 +603,79 @@ for (const s of SCREWS) {
nTot += s.数量;
bySize[s.螺纹] = (bySize[s.螺纹] || 0) + s.数量;
const tr = document.createElement('tr');
tr.innerHTML = '<td><b>' + s.规格 + '</b><br><small>' + s.头型 + '</small></td>' +
tr.innerHTML = '<td><b>' + s.id + '</b> ' + s.规格 + '<br><small>' + s.头型 + '</small></td>' +
'<td>' + s.长度mm + '</td><td>' + s.数量 + '</td>' +
'<td>' + s.位置 + '<br><small>' + s.备注 + '</small></td>';
tr.dataset.spec = s.id;
tr.title = '点击:在三维模型中高亮该规格全部 ' + s.数量 + ' 颗并定位(再次点击切换到下一处)';
tr.onclick = () => highlightSpec(s.id);
skb.appendChild(tr);
}
const tally = Object.keys(bySize).sort().map(k => k + 'x' + bySize[k]).join(' / ');
document.getElementById('scrSum').textContent =
'共 ' + SCREWS.length + ' 种 ' + nTot + ' 颗(' + tally + ');另需 M4 螺母 4、M5 螺母 4、M6 螺母+平垫圈各 6,见各行备注';
'共 ' + SCREWS.length + ' 种 ' + nTot + ' 颗(' + tally + ');另需 M4 螺母 4、M5 螺母 4、M6 螺母+平垫圈各 6,见各行备注' +
' —— 点击任一行可在模型中高亮定位该行螺钉(再次点击切换下一处,Esc 清除)';
document.getElementById('bomStat').textContent =
'紧固件 ' + nTot + ' 颗 / 打印件 ' + DATA.parts.filter(p=>p.printed).length + ' 种';
/* ---- 紧固件高亮定位(BOM 螺丝行 / 零件树 / 三维点选 三处共用)----
螺钉多数埋在沉孔或螺丝刀通道里,因此高亮时同时关闭深度测试做"穿透"显示,
让位置直接透过零件亮出来;相机再对该孔组自动对焦。 */
let hlSpec = null, hlIdx = 0;
function specMeshes(sid) { return meshes.filter(m => m.userData.screw && m.userData.spec === sid); }
function clearScrewHl() {
if (!hlSpec) return;
for (const m of specMeshes(hlSpec)) {
const mt = m.material;
mt.emissive.setHex(0); mt.emissiveIntensity = 0;
mt.transparent = false; mt.opacity = 1;
mt.depthTest = true; mt.depthWrite = true;
mt.needsUpdate = true; m.renderOrder = 0;
}
hlSpec = null;
for (const tr of document.querySelectorAll('#screws tbody tr')) tr.classList.remove('scrHl');
const hud = document.getElementById('hud');
if (hud && hud.dataset.scrHl) { hud.textContent = ''; delete hud.dataset.scrHl; }
requestRender();
}
function highlightSpec(sid, step) {
if (hlSpec !== sid) { clearScrewHl(); hlSpec = sid; hlIdx = 0; }
else if (step !== false) hlIdx++; // 再次点击同一行 -> 切换到下一处
const ms = specMeshes(sid);
if (!ms.length) return;
const inf = SCREWS.find(s => s.id === sid) || {规格: sid, 头型: '', 数量: ms.length, 位置: '', 备注: ''};
for (const m of ms) {
const mt = m.material;
if (!m.visible) { m.visible = true; if (m.chk) m.chk.checked = true; }
mt.emissive.setHex(0xff8a1e); mt.emissiveIntensity = 1.0;
mt.transparent = true; mt.opacity = .96;
mt.depthTest = false; mt.depthWrite = false; mt.needsUpdate = true;
m.renderOrder = 999;
}
scene.updateMatrixWorld(true);
const pts = ms.map(m => new V3(...m.userData.hl).applyMatrix4(m.parent.matrixWorld));
hlIdx = ((hlIdx % pts.length) + pts.length) % pts.length;
const c0 = pts[hlIdx];
const cl = pts.filter(p => p.distanceTo(c0) < 40.0); // 同一孔组(如某关节 3 颗)一同入框
const lo = [1e9,1e9,1e9], hi = [-1e9,-1e9,-1e9];
for (const p of cl) for (let i = 0; i < 3; i++) {
lo[i] = Math.min(lo[i], p.getComponent(i)); hi[i] = Math.max(hi[i], p.getComponent(i));
}
tx = (lo[0]+hi[0])/2; ty = (lo[1]+hi[1])/2; tz = (lo[2]+hi[2])/2;
const sp = Math.max(hi[0]-lo[0], hi[1]-lo[1], hi[2]-lo[2]);
rd = Math.max(150, sp * 2.6 + 130);
for (const tr of document.querySelectorAll('#screws tbody tr'))
tr.classList.toggle('scrHl', tr.dataset.spec === sid);
const tr0 = document.querySelector('#screws tbody tr[data-spec="' + sid + '"]');
if (tr0) tr0.scrollIntoView({block: 'nearest'});
const hud = document.getElementById('hud');
hud.dataset.scrHl = '1';
hud.innerHTML = '<b>' + sid + ' ' + inf.规格 + ' ' + inf.头型 + '</b> · ' + inf.数量 +
' 颗 —— 第 ' + (hlIdx + 1) + '/' + pts.length + ' 处:' + ms[hlIdx].userData.cn.split(' · ')[1] +
'<span class="hudlink" id="scrClr">清除高亮</span><br><small>' + inf.位置 + ';' + inf.备注 + '</small>';
const scl = document.getElementById('scrClr');
if (scl) scl.onclick = () => clearScrewHl();
requestRender();
}
const dock = document.getElementById('bomDock'), bTab = document.getElementById('bomTab');
function dockSet(open) {
dock.classList.toggle('open', open);
@@ -550,12 +684,14 @@ function dockSet(open) {
}
bTab.onclick = () => dockSet(!dock.classList.contains('open'));
document.getElementById('bomHide').onclick = () => dockSet(false);
document.addEventListener('keydown', e => { if (e.key === 'Escape') dockSet(false); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !hlSpec) dockSet(false); });
/* ---- toolbar ---- */
const bAsm = document.getElementById('bAsm');
const bExp = document.getElementById('bExp');
const bWire = document.getElementById('bWire');
const bXRay = document.getElementById('bXRay');
const bScr = document.getElementById('bScr');
const bEnv = document.getElementById('bEnv');
const bL = document.getElementById('bL');
const sheet = document.getElementById('sheet');
const stage = document.getElementById('stage');
@@ -566,9 +702,16 @@ function tog(a, b) { bAsm.classList.toggle('on', a); bExp.classList.toggle('on',
const exp = document.getElementById('exp');
exp.oninput = () => setExpl(exp.value/100);
on('bWire', () => { bWire.classList.toggle('on'); const w = bWire.classList.contains('on');
meshes.forEach(m => { m.material.wireframe = w; }); requestRender(); });
meshes.forEach(m => { if (m.userData.name.slice(0,4) !== 'ENV_') m.material.wireframe = w; }); requestRender(); });
on('bXRay', () => { bXRay.classList.toggle('on'); const o = bXRay.classList.contains('on');
meshes.forEach(m => { if (!m.userData.printed && m.userData.name[0]==='M') { m.material.opacity = o ? .34 : 1; m.material.transparent = o; } }); requestRender(); });
on('bEnv', () => { bEnv.classList.toggle('on'); const o = bEnv.classList.contains('on');
meshes.forEach(m => { if (m.userData.name.slice(0,4) === 'ENV_') { m.visible = o; if (m.chk) m.chk.checked = o; } });
requestRender(); });
on('bScr', () => { bScr.classList.toggle('on'); const o = bScr.classList.contains('on');
meshes.forEach(m => { if (m.userData.screw) { m.visible = o; if (m.chk) m.chk.checked = o; } });
if (!o) clearScrewHl();
requestRender(); });
on('bFit', () => { fitTo(BB); az = -0.9; po = 1.15; camUp.copy(UP_Y); requestRender(); });
const app = document.getElementById('app');
on('bL', () => { app.classList.toggle('l'); bL.classList.toggle('on'); });
@@ -628,8 +771,10 @@ document.getElementById('ver').textContent = DATA.layout.ver + ' · 迭代 ' + D
function updStat() {
const el = document.getElementById('stat');
const tri = (typeof meshes !== 'undefined' && meshes) ? meshes.reduce((a, m) => a + (m.userData.triCount || 0), 0) : 0;
if (el) el.textContent = '零件 ' + (typeof meshes !== 'undefined' ? meshes.length : 0) + ' · 三角面 ' + (tri/1000).toFixed(0) + 'k · ' +
'爆炸 ' + Math.round(expl*100) + '%';
const nScr = (typeof meshes !== 'undefined' && meshes) ? meshes.filter(m => m.userData.screw).length : 0;
const nTot = (typeof meshes !== 'undefined' && meshes) ? meshes.length : 0;
if (el) el.textContent = '零件 ' + (nTot - nScr) + (nScr ? ' + 螺钉 ' + nScr : '') +
' · 三角面 ' + (tri/1000).toFixed(0) + 'k · ' + '爆炸 ' + Math.round(expl*100) + '%';
}
updStat();
@@ -659,6 +804,8 @@ requestAnimationFrame(loop);
if (q.has('tx')) tx = +q.get('tx');
if (q.has('ty')) ty = +q.get('ty');
if (q.has('tz')) tz = +q.get('tz');
if (q.get('env') === '1') { const b = document.getElementById('bEnv');
if (b && !b.classList.contains('on')) b.onclick(); }
if (q.get('xray') === '0') { const b = document.getElementById('bXRay'); if (b.classList.contains('on')) b.onclick(); }
if (q.has('j6')) { const el = document.getElementById('sJ6');
if (el) { el.value = q.get('j6'); el.dispatchEvent(new Event('input')); } }
+166 -19
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

File diff suppressed because one or more lines are too long
+155
View File
@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>机器人手臂 · 螺丝采购清单 BOM</title>
<style>
:root{
--bg:#0d0f13; --panel:#151a22; --panel2:#1b222c; --line:#2b323e;
--fg:#e8eef7; --dim:#93a1b5; --accent:#4aa3ff; --ok:#3ddc97; --warn:#ffb454;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);
font:14px/1.6 "Microsoft YaHei","PingFang SC",system-ui,-apple-system,Segoe UI,sans-serif;
padding:28px 20px 60px}
.wrap{max-width:1080px;margin:0 auto}
h1{margin:0 0 4px;font-size:22px;letter-spacing:.5px}
h1 small{color:var(--dim);font-weight:400;font-size:13px;letter-spacing:0}
.sub{color:var(--dim);font-size:13px;margin:0 0 18px}
.cards{display:flex;gap:12px;flex-wrap:wrap;margin:0 0 20px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;
padding:10px 16px;min-width:132px}
.card b{display:block;font-size:20px;color:var(--accent)}
.card span{font-size:12px;color:var(--dim)}
.card.amber b{color:var(--warn)}
h2{font-size:15px;margin:26px 0 10px;padding-left:10px;border-left:3px solid var(--accent)}
h2.muted{border-left-color:var(--line);color:var(--dim)}
table{width:100%;border-collapse:collapse;background:var(--panel);
border:1px solid var(--line);border-radius:10px;overflow:hidden}
th,td{padding:9px 12px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
th{background:var(--panel2);color:var(--dim);font-weight:600;font-size:12px;
letter-spacing:.6px;white-space:nowrap}
tbody tr:hover{background:#1a212c}
tbody tr:last-child td{border-bottom:none}
td.id b{color:var(--accent);font-size:15px}
td.num{text-align:center;font-variant-numeric:tabular-nums;white-space:nowrap}
small{color:var(--dim)}
.chip{display:inline-block;padding:1px 7px;border-radius:20px;font-size:11px;
border:1px solid var(--line);color:var(--dim);margin-left:6px}
a.buy{display:inline-block;padding:4px 12px;border-radius:6px;text-decoration:none;
background:#2b3a52;border:1px solid #3d5a80;color:#cfe4ff;
font-size:12px;white-space:nowrap;transition:.15s}
a.buy:hover{background:var(--accent);color:#04121f;border-color:var(--accent)}
tr.base{opacity:.5}
tr.base td.id b{color:var(--dim)}
.tag-skip{color:var(--warn);font-size:12px}
.foot{margin-top:22px;color:var(--dim);font-size:12px;line-height:1.9}
.foot code{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 5px;color:#cfe4ff}
</style>
</head>
<body>
<div class="wrap">
<h1>机器人手臂 · 螺丝采购清单 <small>(BOM · 淘宝搜索直链)</small></h1>
<p class="sub">数据源:<b>docs/screws.json</b> | 底座 P02 相关紧固件按要求本次暂不采购</p>
<div class="cards" id="cards"></div>
<h2>需采购紧固件(S01 ~ S06、S09)</h2>
<table>
<thead><tr>
<th>编号</th><th>规格</th><th>头型</th><th>数量</th><th>位置 / 用途</th>
<th>备注</th><th>淘宝链接</th>
</tr></thead>
<tbody id="tbBuy"></tbody>
</table>
<h2 class="muted">底座紧固件(本次暂不采购)</h2>
<table>
<thead><tr>
<th>编号</th><th>规格</th><th>头型</th><th>数量</th><th>位置 / 用途</th>
<th>备注</th><th>淘宝链接</th>
</tr></thead>
<tbody id="tbSkip"></tbody>
</table>
<p class="foot">
链接为淘宝<b>搜索</b>直链(关键词已按规格/头型/材质预置),点击后可在搜索结果中自行比价选店。<br>
建议关键词补充:结构件优先选 <code>304 不锈钢</code> 或 <code>12.9 级发黑</code>;M2/M3 小规格建议含 <code>十字/内六角</code> 配套扳手。<br>
备注中的螺母/垫圈需与螺钉一并下单(S05 需 M4 螺母 4 枚)。
</p>
</div>
<script>
/* 唯一事实源:docs/screws.json 的规格 / 头型 / 数量 / 位置 / 备注 */
const ROWS = [
{id:'S01', size:'M2x10', spec:'M2 × 10', head:'圆柱头内六角', qty:18,
pos:'J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗',
note:'Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布)', base:false},
{id:'S02', size:'M3x10', spec:'M3 × 10', head:'圆柱头内六角', qty:20,
pos:'P04/P05/P06/P07/P08 各 4 颗 → 下级电机尾端面',
note:'Ø2.8 通孔 + Ø5.5 沉孔(深 4mm)', base:false},
{id:'S03', size:'M3x16', spec:'M3 × 16', head:'沉头内六角', qty:4,
pos:'P03 后置重载法兰 → M1 电机尾端面',
note:'Ø3.4 通孔 + Ø6.5 沉孔,轴向锁死防拉脱', base:false},
{id:'S04', size:'M3x8', spec:'M3 × 8', head:'沉头内六角', qty:4,
pos:'P03 前置止脱锁紧法兰 → M1 电机前端面',
note:'Ø3.2 通孔 + Ø6.0 沉孔,BCD 55', base:false},
{id:'S05', size:'M4x25', spec:'M4 × 25', head:'圆柱头内六角', qty:4,
pos:'P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗',
note:'Ø4.4 通孔,非对称偏心孔位防反装', base:false,
extra:{label:'M4 螺母 ×4', q:'M4 六角螺母 304不锈钢'}},
{id:'S06', size:'M4x16', spec:'M4 × 16', head:'沉头内六角', qty:4,
pos:'P09 末端工具法兰 → 末端工具/夹爪',
note:'Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20', base:false},
{id:'S09', size:'M5x35', spec:'M5 × 35', head:'圆柱头内六角', qty:4,
pos:'P03 肩吊臂安装台 → P01_C 立柱顶面',
note:'Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm', base:false},
/* —— 底座 P02 相关,本次暂不采购 —— */
{id:'S07', size:'M5x25', spec:'M5 × 25', head:'圆柱头内六角', qty:4,
pos:'P02 底座中核心 → P01 立柱底部榫头',
note:'Ø5.5 通孔 + Ø10 沉孔(深 6mm)', base:true},
{id:'S08', size:'M5x25', spec:'M5 × 25', head:'圆柱头内六角', qty:4,
pos:'P02 前/后力臂 → 中核心:前臂 2 颗 + 后臂 2 颗',
note:'Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚', base:true},
{id:'S10', size:'M6x35', spec:'M6 × 35', head:'外六角', qty:6,
pos:'底座 6 组 28×7 紧固腰槽 → 工作台/桌面',
note:'需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)', base:true},
];
const tb = (q) => 'https://s.taobao.com/search?q=' + encodeURIComponent(q);
const kw = (r) => r.size + ' 内六角' + (r.head.indexOf('沉头') === 0 ? '沉头' : '圆柱头') + '螺丝 304不锈钢';
const buy = q => '<a class="buy" target="_blank" rel="noopener" href="' + tb(q) + '">搜淘宝 ↗</a>';
function row(r) {
const tr = document.createElement('tr');
if (r.base) tr.className = 'base';
tr.innerHTML =
'<td class="id"><b>' + r.id + '</b></td>' +
'<td>' + r.spec + '</td>' +
'<td>' + r.head + (r.head.indexOf('外六角') === 0 ? '<span class="chip">需扳手</span>' : '') + '</td>' +
'<td class="num">' + r.qty + '</td>' +
'<td>' + r.pos + '</td>' +
'<td><small>' + r.note + '</small></td>' +
'<td>' + (r.base
? '<span class="tag-skip">暂不采购</span>'
: buy(kw(r)) + (r.extra ? '<br>' + buy(r.extra.q) + ' <small>' + r.extra.label + '</small>' : '')) +
'</td>';
return tr;
}
const tbBuy = document.getElementById('tbBuy');
const tbSkip = document.getElementById('tbSkip');
let nBuy = 0, nSkip = 0;
for (const r of ROWS) (r.base ? (nSkip += r.qty, tbSkip) : (nBuy += r.qty, tbBuy)).appendChild(row(r));
document.getElementById('cards').innerHTML =
'<div class="card"><b>' + ROWS.filter(r => !r.base).length + '</b><span>需采购规格数</span></div>' +
'<div class="card"><b>' + nBuy + '</b><span>需采购螺钉总数</span></div>' +
'<div class="card amber"><b>' + ROWS.filter(r => r.base).length + '</b><span>暂不采购规格数</span></div>' +
'<div class="card amber"><b>' + nSkip + '</b><span>暂不采购螺钉数</span></div>';
</script>
</body>
</html>