10 Commits
Author SHA1 Message Date
cbb b0efb44368 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 历史里已有)
2026-10-14 22:03:18 +08:00
cbb c754521bde feat(arm): 线束走线可视化落地,并修复承压面盒切误伤底壁
- 查看器新增线束/走线指示系统(4芯主干、电机出线分支、过关节缓冲环、T接小板),
  配套《线束走线与总线拓扑设计方案》文档与 src/test_harness_collision.py 干涉校验
- 取消 P04/P06/P07/P08 的敞口侧定位环盒切(俯仰关节局部坐标使切刀上侵底壁 3mm,
  平白造出 3x6.1mm 缺角),底壁补齐;盒切仅保留原本专用的 P05(M3),开关改名 M3_FENCE_OPEN_CUT
- 3MF 基准 profile(537 键)从手作原件抽出固化到 docs/bambu_profile_reference.json,
  导出链路不再依赖 out/ 二进制;P01_A 也改由当前模型生成,原件备份进 .bak/
- strength.json、BOM、screws、截图与三份查看器 HTML 同步重生成
2026-10-07 19:08:47 +08:00
cbb d8f6c69406 feat(arm): P04顶缘改R3圆角+竖直前壁消除刀刃状凸起,并带上承压面R35圆腔等未提交改动
用户截图圈的"非常尖的凸起"实测是顶面前沿一条刀刃:2026-10-04 把前脸改成与顶面
相切的 Hermite 弧面,但只有 9.0mm 的 Y 预算却要吸收 120.5mm 落差,90° 转弯全挤在
起点——离开顶面 0.28mm 就转到 45°,起始曲率半径 0.571mm,Y=164.32 处顶面与前脸之间
料厚只剩 0.027mm。2026-10-05 的起点加密又抹掉了第一段 57° 折线那层意外钝化。
且历次只碰前缘一条棱,顶面左缘(X=11.5)与后缘(Y=91.5)仍是 89° 直棱。

现改为常规形状:顶面 -> 前侧 R3 圆角 -> 竖直前壁 Y=164.5 直落到 Z=566 -> Hermite
(两端切线沿 -Z,G1) 顺收到 (155.5,526);顶面后缘对称再倒 R3,弧心切点按背面那条
0.24° 斜线取值以免露出台阶。crest 不再是独立最外凸点,尖从根上消失。

代价与核验:P04 体积 276.2 -> 296.2 cm3(+20.0,填掉原内凹腔);Y 最大仍 164.5,
与 P03 止脱环 R36.5/顶646.5 的齐平不变;13/13 单实体、静态干涉 8/8 零、97 颗螺钉。
J1 扫掠半径由 51.6 增至 57.2mm,动态行程数据(collide.json/travel.json)待重扫。

同时带上上一轮未提交的改动:承压面让位腔改 R35 圆腔、主腔统一 BAY_R=30.50、
M2/M3 前端压盖与围挡敞口侧切除、build_viewer 三副本逐个写盘(避免浏览器独占锁
导致"改了源码网页没变")。
2026-10-07 02:27:38 +08:00
cbb e3b25452e1 feat(arm): 电机双向夹紧与2mm厚度公差让位,立柱减重槽及文档同步更新 2026-10-04 13:16:32 +08:00
cbb b593e47c76 feat: 增加组合折叠姿态干涉扫描、力矩实测与结构强度复核数据 2026-10-02 12:06:23 +08:00
cbb 6fe97fc643 refactor(model): 重构P03为轻量化板式桁架并统一M4×16自攻螺钉连接方案 2026-10-02 04:39:12 +08:00
cbb 208b37048b fix: IK 全面审查三处修复——NaN 防线、实时拖拽被入场动画覆写、滚转角miss突跳
- 求解器 inLimit/evalAt 容差比较改否定式:原写法在 NaN 下比较恒假,
  NaN 目标/姿态曾产出 ok=true 且 q 含 NaN(UI 有 number 输入净化+isFinite
  双保险触不到,但公开 API 与将来的 MCU 端口会中招)
- glue setJoints(q,false) 补 anim=null:直接写入不再被在飞的 620ms 入场动画
  覆写、结尾不再跳回旧目标(实时追踪拖拽撞上入场动画的窗口期)
- glue rollAngleAt 射线错过平面改返回 null:原来返回 0 会让滚转角按 -a0 突跳,
  现在该帧直接跳过
- 回归:node 六项全绿;NaN/Inf 目标三种注入均 ok=false 且 q=null;
  40 组随机位形与 fast 副本仍逐位一致;浏览器冒烟已到达/零报错
2026-09-29 00:24:58 +08:00
cbb 4fdc37da9f perf: IK求解器姿态路径提速1.41×——轴夹角预筛+Set去重+免hypot,并修正牛顿收口取错行的列下标bug
- 4.3牛顿收口 err2 原取 [M5[6..8]](行主序第2行)当"列2工具轴",正确应为列2
  [M[2],M[5],M[8]];该节因此从未起作用。修复后 selfTest 姿态命中 5/6→6/6、
  最差 oerr 0.602°→0.024°
- scoreBranch 轴夹角预筛:θ6 只滚转不改工具轴 ⇒ oerr≥轴夹角(20万样本零违反),
  轴夹角就超 POSETOL 的候选免 orient 全矩阵+acos 直接拒
- keepBand.indexOf → Set.has(原 O(n²) 约6000×4000恒等比较/次);evalAt 误差范数
  hypot→平方和
- A/B 40 组随机位形输出与合入前逐位一致(Δθ=0.000°、nSol 一致、oerr 零变差),
  node 六项回归全绿,浏览器冒烟已到达/残差6.4e-14mm/零报错
2026-09-29 00:10:12 +08:00
cbb a70724e8a9 feat: 查看器新增姿态控制算法与箭头拖拽交互(TCP位置+工具姿态6自由度联合IK),IK控制条改两行,补充定位图截图
- arm-ik-solver.js:位置闭式支附加 θ6 滚转解算(N=R5^T·R_des,工具轴与 θ6 无关的结构);达标带(e≤1°)按 e 升序取种子 + escalating(惩罚×100 重扫)+ (θ4,θ5) 二维牛顿收口,分层保证"姿态达标再谈省关节角";超可达空间走 6 维阻尼最小二乘(位置 mm+姿态 deg 联合)兜底;自检补姿态重构
- ik-glue.js:姿态控制开关 + 方位/仰角/滚转输入 + 取当前/垂直向下;目标点处方向箭头(弧球法调指向)与粉色侧球/箭杆(调滚转),就近仲裁不抢黄球拖拽;面板显示目标/实际姿态与残差(闭式/场景图双口径)
- 界面语义统一为工具朝外法向,入栈绕工具 x 翻 180° 换算求解器内部约定
2026-09-28 22:09:09 +08:00
cbb c0c78251b5 feat: 新增IK实时求解与运动模拟功能,集成正逆运动学查看器,补充定位图截图 2026-09-28 12:08:41 +08:00
281 changed files with 170810 additions and 2342 deletions
Binary file not shown.
+609
View File
@@ -0,0 +1,609 @@
# -*- coding: utf-8 -*-
"""Shared geometry / assembly / export library for the robot arm build.
No layout of its own: the joint frames and station positions live in
human_layout.py and arm_model.py, which import this module. What lives here is
the measured motor interface (docs/interfaces.md), the Joint class (motor slip
bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
+65.0 replicating the Ø36x1.0 neck / Ø6.5x4.0 centre / 3x M2 on Ø29 output so
the existing face-spline halves + split clamp bolt straight on), the part
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
"""
import math
import os
import sys
import json
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cadquery as cq
from armcore import (M, C, V, trs, place, bb, bbox_str, cyl, ring, box, holes,
solids_of, mesh_of, MOTOR_TOL, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP)
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
from OCP.STEPControl import STEPControl_Writer, STEPControl_Controller, STEPControl_StepModelType
from OCP.Interface import Interface_Static
STEPControl_Controller.Init_s()
D2R = math.pi / 180.0
VERSION = os.environ.get("ARMVER", "r1")
ITER = int(os.environ.get("ARMITER", "1"))
# ---------------------------------------------------------------- measured stations
FLANGE_Z = M["flange_z"] # 54.942
COVER_Z = M["cover_z"] # 56.000
HUB_FACE_Z = 65.0 # motor rear datum -> driven hub mating plane
CLAMP_SPAN = (FLANGE_Z + 1.66, FLANGE_Z + 9.46)
CLAMP_R_BAND = 31.0
CLAMP_R_LUG = 38.0
BORE_R = M["hous_d"] / 2.0 + 0.15 # 30.15 slip fit on the housing
CB_R = M["cover_d"] / 2.0 + 0.35 # 30.50 clearance for the cover
PCB_W = M["pcba_w"] + 0.8 # 64.40 square pocket
HUB_M2_CLOCK = [(360.0 - a) % 360.0 for a in M["m2_clock"]] # phase of the mirrored half
HUB_PILOT_D = M["neck_d"] - 0.4 # 35.6 -> the half's pilot lands in a Ø36 recess
PINCH_D = 2.6 # M3 clearance in printed material
# 承压面让位(完整背景见 Joint.pocket 的 docstring):
# REAR_LAND = 0.6 腔口直壁台肩高度,**保持原设计值不变**(让位不靠加高台肩)
# REAR_SEAT = 2.6 尾端面->承压面 = 0.6 原台肩 + 2.0 公差垫片位
# SEAT_R = 32.2 承压面段(z=-REAR_SEAT..0)的腔口半径 = PCB_W/2 = 64.4 方腔的
# **内切圆半径**。承压面下沉 2.6mm 后这一段必须让空,但绝不能连着
# 外壁 R35 一起挖掉(2026-10-04 教训,见 Joint.pocket docstring):
# 用 R35 圆腔会把 r=32.2..35 的连接壁整圈切断,P04 后端只剩
# 240~320° 方位还连着(实测),打印出来就是一道 3.6mm 宽的通槽 +
# 一片 0.5mm 厚的孤立薄片(用户截图:肩叉顶部"没连上")。
# 取 32.2 只需挖到方腔内切圆,r=32.2..35 全圈保留 ->
# 壁厚均匀的 2.8mm 环形连接壁(0/90/180/270 方向与上方方腔壁
# 齐平 seamlessly,45° 方向比方腔多留料,正好补回四角)。
# SEAT_TOP = 0.0 圆腔顶端 = 方腔起点,两者在此平接;再往上就是方腔的地盘。
# R35_SEAT = 35.0 后端板 R35 外壁(承压面外径,不可挖穿)
REAR_LAND = 0.6
REAR_SEAT = REAR_LAND + MOTOR_TOL # 2.6 尾端面->后端板承压面:公差垫片位
R35_SEAT = 35.0 # 后端板 R35 外壁(承压面外径)
SEAT_R = PCB_W / 2.0 # 32.2 承压面段腔口 = 方腔内切圆,保留外壁
SEAT_TOP = 0.0 # 圆腔顶端与方腔起点平接
PARTS = []
CENTS = {}
def add(name, cn, shape, color, printed=True, note="", qty=1):
assert shape is not None, name
lo, hi = bb(shape)
PARTS.append(dict(name=name, cn=cn, shape=shape, color=color, printed=printed,
note=note, qty=qty, bb=bbox_str(shape), vol=vol_of(shape)))
CENTS[name] = [(lo[i] + hi[i]) / 2.0 for i in range(3)]
def vol_of(sh):
p = GProp_GProps()
BRepGProp.VolumeProperties_s(sh.wrapped, p)
return p.Mass()
def common_vol(a, b):
try:
c = BRepAlgoAPI_Common(a.wrapped, b.wrapped)
c.Build()
if not c.IsDone() or c.Shape().IsNull():
return 0.0
return vol_of(cq.Shape(c.Shape()))
except Exception:
return -1.0
def T(dx=0.0, dy=0.0, dz=0.0):
t = gp_Trsf()
t.SetTranslation(gp_Vec(dx, dy, dz))
return t
def R(deg, ax, ay, az, px=0.0, py=0.0, pz=0.0):
t = gp_Trsf()
t.SetRotation(gp_Ax1(gp_Pnt(px, py, pz), gp_Dir(ax, ay, az)), deg * D2R)
return t
RX90 = R(90, 1, 0, 0)
def Ycyl(r, y0, y1, px=0.0, pz=0.0):
return place(cyl(r, -y1, -y0, px, pz), RX90)
def Ybox(x0, x1, y0, y1, z0, z1):
return place(box(x0, x1, z0, z1, -y1, -y0), RX90)
# ------------------------------------------------------- pitch-joint pivot cartridge ----
# ONE recipe for every pitch joint (J2 shoulder, J3 elbow, J5 wrist) - they are the same
# degree of freedom, so an implementation that fixes only one of them leaves the identical
# defect in the other two. See human_layout PV_* for why a cartridge is needed at all:
# measured P04 n P05 = 0.00 mm3, so J2's 38 N*m had no path into the housing except peeling
# the face-spline teeth off across the print layers.
def pivot_geom(y_face, side, h, w, gap):
"""(boss_y0, boss_y1, pocket_y0) for a cartridge against the link face at y_face.
The race pocket always opens AWAY from the link, leaving an (h-w) shoulder behind the
outer race; a printed cap ring closes the open end. The boss stops `gap` short of the
link face, or a printed face would carry the thrust and the rolling elements nothing."""
near = y_face - gap if side < 0 else y_face + gap
if side < 0:
return near - h, near, near - h
return near, near + h, near + h - w
def pivot_boss(shape, pz, y0, y1, pk0, w, px, boss_od, race_od, bore, orace_id, side):
"""Housing side: boss on the joint axis with the outer race sunk into it. The passage
beyond the pocket is reamed to the outer race bore, not to the shaft: a shaft-sized bore
would end inside the inner race and bind it, and a bound bearing carries nothing - it
just moves the failure back into the print layers."""
shape = shape.fuse(Ycyl(boss_od / 2.0, y0, y1, px, pz))
shape = shape.cut(Ycyl(race_od / 2.0, pk0 - .01, pk0 + w + .01, px, pz))
if side < 0:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, pk0 + w, y1 + .01, px, pz))
else:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, y0 - .01, pk0, px, pz))
shape = shape.cut(Ycyl(bore / 2.0 + 0.2, y0 - .01, y1 + .01, px, pz))
for a in (90.0, 210.0, 330.0): # 3x M3 cap screws, outside the outer race
u, v = 26.5 * math.cos(a * math.pi / 180.0), 26.5 * math.sin(a * math.pi / 180.0)
shape = shape.cut(Ycyl(1.25, y0 - .01, y0 + 12.0, px + u, pz + v))
return shape
def pivot_journal(shape, pz, y_link, y_tip, y_collar0, y_collar1, px, bore, collar):
"""Link side: the stub the inner race rides on, poking past the boss so the cap can bite
it, plus an integral collar the inner race bears against. The collar replaces a separate
spacer sleeve and lives in the boss's outer-race passage, so it must stay under that
diameter - the 0.1 mm it leaves is what lets the race, not the collar, take the thrust."""
a, b = sorted((y_link, y_tip))
shape = shape.fuse(Ycyl(bore / 2.0, a, b, px, pz))
c0, c1 = sorted((y_collar0, y_collar1))
return shape.fuse(Ycyl(collar / 2.0, c0, c1, px, pz))
def prism(pts2, z0, z1):
w2 = [(p[0], p[1]) for p in pts2]
return (cq.Workplane("XY", origin=(0, 0, z0))
.polyline(w2 + [w2[0]]).close().extrude(z1 - z0, combine=False)).val()
def Yplate(pts_xz, y0, y1):
return place(prism(pts_xz, -y1, -y0), RX90)
def dogbone(a, b, ra, rb, n=20):
"""Tangent hull of two discs. The outer-tangent offset angle is acos((ra-rb)/L);
using asin made the ra==rb case sweep a full circle around b and self-intersect."""
dx, dy = b[0] - a[0], b[1] - a[1]
L = math.hypot(dx, dy) or 1.0
th = math.atan2(dy, dx)
beta = math.acos(max(-1.0, min(1.0, (ra - rb) / L)))
sw_a = 2 * math.pi - 2 * beta
sw_b = 2 * beta
pts = [(a[0] + ra * math.cos(th + beta + sw_a * k / n), a[1] + ra * math.sin(th + beta + sw_a * k / n))
for k in range(n + 1)]
pts += [(b[0] + rb * math.cos(th - beta + sw_b * k / n), b[1] + rb * math.sin(th - beta + sw_b * k / n))
for k in range(1, n + 1)]
return pts
def sector(r_out, r_in, a0, a1, z0, z1, n=14):
"""Annular sector between two radii and two angles (deg) - a joint shroud segment.
Must NOT include the axis point, or this becomes a solid pie wedge that fills the joint."""
pts = [(r_out * math.cos((a0 + (a1 - a0) * k / n) * D2R),
r_out * math.sin((a0 + (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
pts += [(r_in * math.cos((a1 - (a1 - a0) * k / n) * D2R),
r_in * math.sin((a1 - (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
return prism(pts, z0, z1)
# ---------------------------------------------------------------- joint frames & interfaces
class Joint:
"""One revolute joint. `rear` = motor rear datum point, `d` = axis unit dir (motor -> hub)."""
def __init__(self, jid, cn, rear, d, xdir, lug_phase=0.0):
self.jid, self.cn = jid, cn
self.rear = rear
self.d = d
self.xdir = xdir
self.t = trs(rear, xdir, d) # motor-local -> global
self.lug_phase = lug_phase # rotation of the clamp about the axis
def L(self, shape_local):
return place(shape_local, self.t)
def at(self, s):
return (self.rear[0] + self.d[0] * s, self.rear[1] + self.d[1] * s, self.rear[2] + self.d[2] * s)
def pocket(self, shape, front_pad=0.6):
"""Cut the motor pocket out of a proximal link body.
REAR_SEAT = 电机尾端面(局部 z=0)到后端板承压面的距离。它不是定位面而是
**垫片位**:公差 MOTOR_TOL=2.0(armcore)要靠它消化 —— 电机比标称短的那一份,
就垫在尾端面与这个承压面之间(见 armcore.MOTOR_TOL 的说明)。原值 0.6 只能吃
0.6mm,短 1mm 以上的电机装不进、长 0.6mm 以上的电机会顶死后端板而压溃。
注意它同时是承压面的高度,所以让空 2.0mm 要求 t_plate >= 8
(见各零件 rear_mount_holes 的 t_plate)。
【2026-10-04 整改:承压面段改用 R35 圆腔,凸台彻底消失】
上一版把 64.4 **方**腔整体从 -0.6 加深到 -2.6(纯掏腔),方腔壁以外
z=-2.6..0 之间那圈 R35 料一动没动,于是相对新承压面整整凸起 2.6mm ——
这就是 P04~P08 后端那圈"悬空凸台"(用户截图红圈,3D 打印时该处是悬空
薄壁,既要支撑又公差难保证)。
为什么之前试"把凸台切掉"总失败(几何实测,别再重复):
- 64.4 方腔的**内切圆半径恰好 = 32.2**,所以方腔在 45° 方向已经吃穿到
r=45.5,R35 壁只剩**四条边中点附近**的料。实测 r=34 处材料沿 z 的
区间(承压面 -2.6):P06 的 0°/180° 是 -8.0..12.0(贯通,是臂身本体),
80°/100° 是 -8.0..-0.0(**只比承压面多 2.6mm,这段就是凸台**),
其余 20~160° 是 -8.0..-2.6(承压环本体,齐平)。
- 因此整段全截面切会把 0/90 方向那条**贯通到 z=12 的承力桥**一起切断
-> P05/P08 碎成 2 块(实测)。这不是"承压环是唯一连接",
而是我的刀具没区分"桥"和"悬空料"。
现在的做法(一刀,不做角度分区):
承压面所在的 z=-REAR_SEAT..0 段,腔口取 **PCB_W/2 = 32.2 的圆**
(= 64.4 方腔的内切圆半径,SEAT_R),而不是方腔、也不是 R35:
- 用方腔(64.4):方腔在 45° 方向已吃穿到 r=45.5,R35 壁只剩四条边中点
附近的料;实测那几段 r=34 处材料是 -8.0..-0.0,比承压面(-2.6)多出 2.6mm,
就是用户截图里那圈"悬空凸台"(打印要支撑、公差差)。
- 用 R35 圆腔(上一版,2026-10-04,**错**):把 r=32.2..35 的连接壁整圈切断。
实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 260° 全部掏空 ->
后端法兰与杯壁之间出现一道 3.6mm 宽的**通槽**,还在 225° 留下一片
0.5mm 厚的孤立薄片(用户截图:肩叉顶部"没连上")。且承压面整个悬空。
- 用 32.2 圆腔(当前):承压面段整圈挖到 -2.6,凸台消失;同时 r=32.2..35
这圈 2.8mm 厚的壁完整保留,把后端板与上方臂身连成一体。0/90/180/270
方向该壁与方腔壁齐平,45° 方向比方腔多留料把四角补实。
PCBA 方腔本体仍从 z=0 起(z<0 那段由 32.2 圆腔负责,两者在此平接)。
"""
half = PCB_W / 2.0
c = [cyl(BORE_R, 5.5, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + front_pad),
# 承压面段:SEAT_R(=32.2 方腔内切圆) 圆腔,从承压面挖到 z=SEAT_TOP(=0)
# 与方腔平接。挖到 32.2 而不是 35 —— 35 会把外壁整圈切断(见 docstring)
cyl(SEAT_R, -REAR_SEAT, SEAT_TOP),
# PCBA 方腔本体:从 z=0 起(承压面段那 2.6mm 由上面的圆腔负责)
box(-half, half, -half, half, 0.0, M["pcba_t"] + 0.4)]
return shape.cut(*[self.L(s) for s in c])
def rear_mount_holes(self, shape, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0):
"""Cut the 4x rear mounting bolt holes into a proximal link body.
Matches the 4x M3 tapped radial slots on the motor rear endplate
(电机.STEP 实测):BCD55、相位 39.9°+90k,槽宽 2.5、径向可调约 ±1.25mm。
旧版误用 35.1x40.9 矩形孔距:对角两颗偏出螺纹槽 ~4.7mm 无处咬合
(2026-10-02 用户指出"螺钉没有对齐",按 STEP 实测槽位修正)。
"""
cuts = []
for k in range(4):
a = math.radians(39.9 + 90.0 * k)
sx = 27.5 * math.cos(a)
sy = 27.5 * math.sin(a)
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
cuts.append(self.L(h))
if cb_d > 0 and cb_depth > 0:
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
cuts.append(self.L(cb))
return shape.cut(*cuts)
def front_collar_pocket(self, shape, r_bore=30.25, z0=50.0, z1=56.5):
"""Cut front locating collar pocket for the motor front rim."""
p = cyl(r_bore, z0, z1)
return shape.cut(self.L(p))
def pinch(self, shape, fracs=(0.26, 0.58, 0.86), r_out=48.0, n=2):
"""Radial M3 pinch screws through the boss wall onto the housing."""
cuts = []
for fr in fracs[:n]:
for a in (self.lug_phase, self.lug_phase + 180.0):
c = cyl(PINCH_D / 2.0, -1.0, r_out, 0, 0)
c = place(c, R(a, 0, 0, 1))
c = place(c, T(0, 0, fr * COVER_Z))
cuts.append(self.L(c))
return shape.cut(*cuts)
def hub_face(self):
return self.at(HUB_FACE_Z)
def cut_hub(self, shape, depth=24.0, r_clear=32.0):
"""Cut the motor-output replica into the driven hub. The mating plane sits at
motor-local z = HUB_FACE_Z, and the recesses open there and run into the driven
material (away from the motor), so that half B's pilot (0.60) and spigot (4.06)
land in them."""
H = HUB_FACE_Z
# measured on the delivered half B: past the mating plane it is Ø35.6 for 1.4 mm
# then a Ø6.3 spigot to 4.06 - the nominal neck_depth of 1.0 is 0.4 too shallow,
# which showed up as a ~160 mm3 interference at every driven hub
cuts = [cyl(M["neck_d"] / 2.0, H - 0.05, H + 1.70),
cyl(M["centre_d"] / 2.0, H - 0.05, H + 5.05),
# probed on the delivered half: its Ø54 back face sits ~0.06 mm PAST the
# nominal plane, so the driven face gets a 0.35 deep, Ø54.6 relief. That is
# also the printed-part tolerance land the joint wants.
cyl(27.30, H - 0.05, H + 0.35)]
for a in HUB_M2_CLOCK:
u = M["m2_bcd"] / 2.0 * math.cos(a * D2R)
v = M["m2_bcd"] / 2.0 * math.sin(a * D2R)
cuts.append(cyl(M["m2_tap_d"] / 2.0, H - 0.05, H + M["m2_depth"], u, v))
return shape.cut(*[self.L(s) for s in cuts])
def hub_island(self, shape, r=30.0, t=9.5):
"""Solid disc at the joint centre (the lightening dish must not break into the
interface recesses), then the output replica is cut into it. Starts exactly on the
mating plane: half B's Ø54 body ends there, so any overlap is a hard clash."""
shape = shape.fuse(self.L(cyl(r, HUB_FACE_Z, HUB_FACE_Z + t)))
shape = self.cut_hub(shape)
# two arc shrouds over the clamp band: the joint reads as a closed drum, and the
# sectors are placed so the M3 lug bolts on the joint's local +-X stay reachable
for (a0, a1) in ((34.0, 146.0), (214.0, 326.0)):
shape = shape.fuse(self.L(sector(38.0, 31.6, a0, a1, FLANGE_Z + 1.4, HUB_FACE_Z)))
return shape
def place_hardware(self, half, clamp, tag):
"""Place the existing coupling halves + split clamp for this joint.
The delivered half is authored with z=0 at the motor flange face and spans
z -3.00..8.06. B = flip 180 deg about the local X axis, then shift by
2*PART_Z, which puts its flat back face exactly on the hub mating plane
(motor-local 65.00) - verified against the measured feature bounds.
"""
tf = trs(self.at(FLANGE_Z), self.xdir, self.d)
hA = place(half, tf)
hb = place(half, R(180, 1, 0, 0))
hb = place(hb, T(0, 0, 2 * C["part_z"]))
hB = place(hb, tf)
cA = place(clamp, tf)
cB = place(place(clamp, R(180, 0, 0, 1)), tf) # the two C pieces are one ring split
# along a diametral plane: 180 deg about the JOINT axis, so both keep the same
# axial span 56.602..64.402 and sit on opposite sides of the band.
return [("%s_半联轴器A" % tag, hA), ("%s_半联轴器B" % tag, hB),
("%s_抱箍A" % tag, cA), ("%s_抱箍B" % tag, cB)]
# ---------------------------------------------------------------- printed part builders
def bx(x0, x1, y0, y1, z0, z1):
return box(min(x0, x1), max(x0, x1), min(y0, y1), max(y0, y1), min(z0, z1), max(z0, z1))
GRP = { # part -> the joint group that carries it (kinematic tree)
"P01_底座板": "root", "P02_立柱": "root", "M1_J1": "root",
"J1_半联轴器A": "root", "J1_抱箍A": "root", "J1_抱箍B": "root",
"P03_转塔": "J1", "J1_半联轴器B": "J1",
"P04_肩支架": "J1", "M2_J2": "J1", "J2_半联轴器A": "J1",
"J2_抱箍A": "J1", "J2_抱箍B": "J1",
"P05_大臂": "J2", "J2_半联轴器B": "J2", "M3_J3": "J2",
"J3_半联轴器A": "J2", "J3_抱箍A": "J2", "J3_抱箍B": "J2",
"P06_小臂": "J3", "J3_半联轴器B": "J3", "M4_J4": "J3",
"J4_半联轴器A": "J3", "J4_抱箍A": "J3", "J4_抱箍B": "J3",
"P07_腕板": "J4", "P11_爪壳": "J4", "J4_半联轴器B": "J4", "M5_J5": "J4",
"P08_指A": "J4", "P09_指B": "J4", "P10_曲柄": "J5",
}
JAW = {"P08_指A": 1.0, "P09_指B": -1.0} # sign of the jaw travel along the J5 local +X
def explode_vec(p, JS, cents, gmean):
"""Explosion offset: coupling hardware separates along its joint axis, everything
else moves radially out of the assembly centroid."""
nm = p["name"]
c = cents[nm]
for jid in ("J1", "J2", "J3", "J4"):
if nm.startswith(jid + "_半联轴器B"):
return [JS[jid].d[i] * 24.0 for i in range(3)]
if nm.startswith(jid + "_半联轴器A") or nm.startswith(jid + "_抱箍"):
k = -26.0 if "抱箍" in nm else -16.0
return [JS[jid].d[i] * k for i in range(3)]
v = [c[i] - gmean[i] for i in range(3)]
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [v[i] / m * 34.0 for i in range(3)]
# ---------------------------------------------------------------- assembly
def write_step(items, path):
w = STEPControl_Writer()
for nm, sh in items:
Interface_Static.SetCVal_s('write.step.product.name', nm)
w.Transfer(sh.wrapped, STEPControl_StepModelType.STEPControl_AsIs)
w.Write(path.replace("/", "\\"))
return os.path.getsize(path)
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def export(JS):
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", WEB, DOCS):
os.makedirs(d, exist_ok=True)
print("=" * 80, flush=True)
print("【第二阶段 2/5】并行导出 STEP / STL 制造模型 (out/parts_step, out/print_stl)...", flush=True)
print("=" * 80, flush=True)
# 1. 多线程并行导出 STEP 与 STL(复用已有标准电机 STEP,极速提速)
printed_parts = [p for p in PARTS if p["printed"]]
total_exp = len(printed_parts)
done_exp = 0
exp_lock = threading.Lock()
def export_one_part(p):
nonlocal done_exp
t0 = time.time()
fn = "%s/%s.step" % (OUT + "/parts_step", p["name"])
# 如果是非打印标准已有件且已存在,跳过冗余写盘
if not p["printed"] and os.path.exists(fn) and os.path.getsize(fn) > 1000:
pass
else:
write_step([(p["name"], p["shape"])], fn)
if p["printed"]:
stl = "%s/%s.stl" % (OUT + "/print_stl", p["name"])
cq.exporters.export(cq.Workplane(obj=p["shape"]), stl, exportType="STL")
with exp_lock:
done_exp += 1
dt = time.time() - t0
print(f" [文件导出 {done_exp}/{total_exp}] {p['name']}.step / .stl 导出完成 ({dt:.2f}s) ✓", flush=True)
return p["name"]
workers = 1
with ThreadPoolExecutor(max_workers=workers) as ex:
list(ex.map(export_one_part, PARTS))
print(f"-> 制造模型导出全部完成!已保存在 {OUT}/parts_step 与 {OUT}/print_stl\n", flush=True)
# 2. 多线程并行生成网页 3D Mesh
print("=" * 80, flush=True)
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (web/mesh_arm.json)...", flush=True)
print("=" * 80, flush=True)
done_mesh = 0
mesh_lock = threading.Lock()
total_parts = len(PARTS)
def mesh_one_part(p):
nonlocal done_mesh
t0 = time.time()
key = "motor" if p["name"].startswith("M") else ("coupl" if "联轴器" in p["name"] or "抱箍" in p["name"] else p["name"])
defl = 0.85 if p["name"].startswith("M") else (0.20 if key == "coupl" else 0.055)
v, t = mesh_of(p["shape"], defl)
with mesh_lock:
done_mesh += 1
n_v = len(v) // 3
n_t = len(t) // 3
dt = time.time() - t0
print(f" [网格细分 {done_mesh:2d}/{total_parts}] {p['name']:<14} 顶点: {n_v:5d} | 面片: {n_t:5d} ({dt:.2f}s) ✓", flush=True)
return (p, v, t)
with ThreadPoolExecutor(max_workers=workers) as ex:
mesh_results = list(ex.map(mesh_one_part, PARTS))
parts = []
for idx, (p, v, t) in enumerate(mesh_results):
parts.append(dict(name=p["name"], cn=p["cn"], v=v, t=t, color=p["color"],
printed=p["printed"], vol=round(p["vol"], 0), note=p["note"],
grp=GRP.get(p["name"], "root"), jaw=JAW.get(p["name"], 0.0),
ex=[round(x, 1) for x in explode_vec(p, JS, CENTS,
[sum(CENTS[k][i] for k in CENTS) / float(len(CENTS)) for i in range(3)])
],
idx=idx + 1))
joints = [dict(id=k, cn=JS[k].cn, rear=JS[k].rear, d=JS[k].d, xdir=JS[k].xdir, hub=JS[k].hub_face())
for k in JS]
d3 = lambda a, b: math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
json.dump(dict(parts=parts, joints=joints,
layout=dict(ver=VERSION, it=ITER,
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
open(WEB + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(f"-> 3D网格与BOM清单已保存至 web/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
return bom
def collide():
ps = [p for p in PARTS if p["vol"] > 1.0]
pairs = []
MOUNT_PAIRS = {
("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_工具法兰_螺旋体30%_墙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)):
a, b = ps[i], ps[j]
if a["name"].startswith("M") and b["name"].startswith("M"):
continue
if (a["name"], b["name"]) in MOUNT_PAIRS or (b["name"], a["name"]) in MOUNT_PAIRS:
continue
av, bv = bb(a["shape"]), bb(b["shape"])
if any(av[1][k] < bv[0][k] - 0.05 or bv[1][k] < av[0][k] + 0.05 for k in range(3)):
continue
pairs.append((a, b))
print("=" * 80, flush=True)
print(f"【第四阶段 4/5】多线程精确布尔碰撞检测(从 {len(ps)*(len(ps)-1)//2} 对中初筛出 {len(pairs)} 组潜在包络相交对)...", flush=True)
print("=" * 80, flush=True)
done_pair = 0
pair_lock = threading.Lock()
def check_pair(pair):
nonlocal done_pair
a, b = pair
t0 = time.time()
v = common_vol(a["shape"], b["shape"])
with pair_lock:
done_pair += 1
status = f"干涉 {v:.1f} mm³ ❌" if v > 2.0 else "零干涉 (0.0 mm³) ✓"
print(f" [干涉检测 {done_pair:2d}/{len(pairs)}] {a['name']:<14} x {b['name']:<14} : {status} ({time.time()-t0:.3f}s)", flush=True)
if v > 2.0:
try:
c = BRepAlgoAPI_Common(a["shape"].wrapped, b["shape"].wrapped)
c.SetRunParallel(True)
c.Build()
cb = cq.Shape(c.Shape())
lo, hi = bb(cb)
loc = "@[%.0f %.0f %.0f..%.0f %.0f %.0f]" % (
lo[0], lo[1], lo[2], hi[0], hi[1], hi[2])
except Exception:
loc = "?"
return (a["name"], b["name"], round(v, 1), loc)
return None
rows = []
for pair in pairs:
res = check_pair(pair)
if res is not None:
rows.append(res)
rows.sort(key=lambda r: -r[2])
json.dump([r[:3] for r in rows], open(DOCS + "/collision.json", "w", encoding="utf-8"),
ensure_ascii=False)
if len(rows) == 0:
print(f"\n-> 静态碰撞干涉检测通过: 0 对干涉!全部 {len(pairs)} 组接触面对零碰撞!\n", flush=True)
else:
print(f"\n-> 发现 {len(rows)} 对干涉!\n", flush=True)
return rows
def axial_extent(j, shape):
lo, hi = bb(shape)
vals = []
for i in (0, 1):
for k in range(3):
p = [lo[0], lo[1], lo[2]]
p[k] = hi[k] if i else lo[k]
vals.append(sum((p[q] - j.rear[q]) * j.d[q] for q in range(3)))
return min(vals), max(vals)
def verify_interfaces(half, clamp, JS):
"""Numeric self-check of the derived joint stack, before trusting the renders."""
rows = []
for k, j in JS.items():
if k == "J5":
continue
parts = dict(j.place_hardware(half, clamp, k))
a = axial_extent(j, parts["%s_半联轴器A" % k])
b = axial_extent(j, parts["%s_半联轴器B" % k])
c = axial_extent(j, parts["%s_抱箍A" % k])
hub = j.at(HUB_FACE_Z)
rows.append(dict(jid=k, halfA=[round(x, 3) for x in a], halfB=[round(x, 3) for x in b],
clamp=[round(x, 3) for x in c], hub_face=round(HUB_FACE_Z, 3),
spigot_proud=round(b[1] - HUB_FACE_Z, 3),
clamp_clear=round(c[0] - COVER_Z, 3)))
return rows
+627
View File
@@ -0,0 +1,627 @@
# -*- coding: utf-8 -*-
"""Shared geometry / assembly / export library for the robot arm build.
No layout of its own: the joint frames and station positions live in
human_layout.py and arm_model.py, which import this module. What lives here is
the measured motor interface (docs/interfaces.md), the Joint class (motor slip
bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
+65.0 replicating the Ø36x1.0 neck / Ø6.5x4.0 centre / 3x M2 on Ø29 output so
the existing face-spline halves + split clamp bolt straight on), the part
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
"""
import math
import os
import sys
import json
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cadquery as cq
from armcore import (M, C, V, trs, place, bb, bbox_str, cyl, ring, box, holes,
solids_of, mesh_of, MOTOR_TOL, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP)
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
from OCP.STEPControl import STEPControl_Writer, STEPControl_Controller, STEPControl_StepModelType
from OCP.Interface import Interface_Static
STEPControl_Controller.Init_s()
D2R = math.pi / 180.0
VERSION = os.environ.get("ARMVER", "r1")
ITER = int(os.environ.get("ARMITER", "1"))
# ---------------------------------------------------------------- measured stations
FLANGE_Z = M["flange_z"] # 54.942
COVER_Z = M["cover_z"] # 56.000
HUB_FACE_Z = 65.0 # motor rear datum -> driven hub mating plane
CLAMP_SPAN = (FLANGE_Z + 1.66, FLANGE_Z + 9.46)
CLAMP_R_BAND = 31.0
CLAMP_R_LUG = 38.0
BORE_R = M["hous_d"] / 2.0 + 0.15 # 30.15 slip fit on the housing
CB_R = M["cover_d"] / 2.0 + 0.35 # 30.50 clearance for the cover
PCB_W = M["pcba_w"] + 0.8 # 64.40 square pocket
HUB_M2_CLOCK = [(360.0 - a) % 360.0 for a in M["m2_clock"]] # phase of the mirrored half
HUB_PILOT_D = M["neck_d"] - 0.4 # 35.6 -> the half's pilot lands in a Ø36 recess
PINCH_D = 2.6 # M3 clearance in printed material
# 承压面让位(完整背景见 Joint.pocket 的 docstring):
# REAR_LAND = 0.6 腔口直壁台肩高度,**保持原设计值不变**(让位不靠加高台肩)
# REAR_SEAT = 2.6 尾端面->承压面 = 0.6 原台肩 + 2.0 公差垫片位
# R35_SEAT = 35.0 后端板 R35 外壁(承压面外径)
# SEAT_R = 35.0 承压面段(z=-REAR_SEAT..SEAT_TOP)的腔口半径 = **等于外壁 R35**。
# SEAT_TOP = 1.0 圆腔顶端越过电机尾端面(z=0) 1mm。
#
# 【2026-10-05 用户第三次指出:SEAT_R 必须是 35,"凸台+围挡"要一起弄平】
# 之前取 32.2(64.4 方腔的内切圆)以为这样能保住连接壁,实测**恰恰相反**:
# 32.2 与外壁 35 之间那圈 2.8mm 宽的环墙完整保留,而承压面只挖到 32.2,
# 于是这圈环墙相对承压面【凸起 2.6mm】,把电机尾端面整个围在里面 ——
# 这正是用户截图里的"一圈围挡"(P08 该圈实测 1536.9 mm3)。它不是残留,
# 而是 SEAT_R 取小值的必然产物:只要 SEAT_R < R35,环墙就一定存在。
# 用户要求:"把凸台、围挡一起弄平,做成和电机两头齐平一起的平面"。
# 故 SEAT_R 取满 R35:承压面段整圈挖到外壁,承压面变成**完整 R35 平圆盘**,
# 环墙(凸台)自然消失,电机尾端面与承压面之间再无任何凸起。
#
# 【为什么这次取 R35 不会重演2026-10-04 的"肩叉顶部没连上"】
# 那次我切的是 z=-2.6..0 且**只切到 z=0 就停**,导致环墙在 z=0..1.0 还残留一截
# 0.052cm3 的薄片(Y93.0~95.8 的半包开口处),那片薄片脱空成为独立碎块。
# 现在 SEAT_TOP=+1.0 把那一截一起吃掉:实测五件全部单实体、零碎片。
# 且承力桥改为由**臂身本体**(r>35)承担 —— 实测 band 段 r36..58 有料的方位
# P04 10/36、P05/P06/P07/P08 各 15/36,其余方位由 z=+1.0 以上的杯体连接,
# 不存在"整圈被切断"的通槽(那正是 2026-10-04 的错误形态)。
# 连带好处:承压环由「1.95mm 窄环」变「完整 R35 圆环 4.75mm」,受压面积 383->974mm²。
REAR_LAND = 0.6
REAR_SEAT = REAR_LAND + MOTOR_TOL # 2.6 尾端面->后端板承压面:公差垫片位
R35_SEAT = 35.0 # 后端板 R35 外壁(承压面外径)
SEAT_R = R35_SEAT # 35.0 承压面段腔口 = 外壁:凸台+围挡一并消除
SEAT_TOP = 1.0 # 圆腔顶端越过电机尾端面 1mm(吃掉开口处残片)
BORE_Z0 = 5.5 # 座孔起点:R35 圆腔挖到此处,与 R30.15 座孔相接
PARTS = []
CENTS = {}
def add(name, cn, shape, color, printed=True, note="", qty=1):
assert shape is not None, name
lo, hi = bb(shape)
PARTS.append(dict(name=name, cn=cn, shape=shape, color=color, printed=printed,
note=note, qty=qty, bb=bbox_str(shape), vol=vol_of(shape)))
CENTS[name] = [(lo[i] + hi[i]) / 2.0 for i in range(3)]
def vol_of(sh):
p = GProp_GProps()
BRepGProp.VolumeProperties_s(sh.wrapped, p)
return p.Mass()
def common_vol(a, b):
try:
c = BRepAlgoAPI_Common(a.wrapped, b.wrapped)
c.Build()
if not c.IsDone() or c.Shape().IsNull():
return 0.0
return vol_of(cq.Shape(c.Shape()))
except Exception:
return -1.0
def T(dx=0.0, dy=0.0, dz=0.0):
t = gp_Trsf()
t.SetTranslation(gp_Vec(dx, dy, dz))
return t
def R(deg, ax, ay, az, px=0.0, py=0.0, pz=0.0):
t = gp_Trsf()
t.SetRotation(gp_Ax1(gp_Pnt(px, py, pz), gp_Dir(ax, ay, az)), deg * D2R)
return t
RX90 = R(90, 1, 0, 0)
def Ycyl(r, y0, y1, px=0.0, pz=0.0):
return place(cyl(r, -y1, -y0, px, pz), RX90)
def Ybox(x0, x1, y0, y1, z0, z1):
return place(box(x0, x1, z0, z1, -y1, -y0), RX90)
# ------------------------------------------------------- pitch-joint pivot cartridge ----
# ONE recipe for every pitch joint (J2 shoulder, J3 elbow, J5 wrist) - they are the same
# degree of freedom, so an implementation that fixes only one of them leaves the identical
# defect in the other two. See human_layout PV_* for why a cartridge is needed at all:
# measured P04 n P05 = 0.00 mm3, so J2's 38 N*m had no path into the housing except peeling
# the face-spline teeth off across the print layers.
def pivot_geom(y_face, side, h, w, gap):
"""(boss_y0, boss_y1, pocket_y0) for a cartridge against the link face at y_face.
The race pocket always opens AWAY from the link, leaving an (h-w) shoulder behind the
outer race; a printed cap ring closes the open end. The boss stops `gap` short of the
link face, or a printed face would carry the thrust and the rolling elements nothing."""
near = y_face - gap if side < 0 else y_face + gap
if side < 0:
return near - h, near, near - h
return near, near + h, near + h - w
def pivot_boss(shape, pz, y0, y1, pk0, w, px, boss_od, race_od, bore, orace_id, side):
"""Housing side: boss on the joint axis with the outer race sunk into it. The passage
beyond the pocket is reamed to the outer race bore, not to the shaft: a shaft-sized bore
would end inside the inner race and bind it, and a bound bearing carries nothing - it
just moves the failure back into the print layers."""
shape = shape.fuse(Ycyl(boss_od / 2.0, y0, y1, px, pz))
shape = shape.cut(Ycyl(race_od / 2.0, pk0 - .01, pk0 + w + .01, px, pz))
if side < 0:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, pk0 + w, y1 + .01, px, pz))
else:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, y0 - .01, pk0, px, pz))
shape = shape.cut(Ycyl(bore / 2.0 + 0.2, y0 - .01, y1 + .01, px, pz))
for a in (90.0, 210.0, 330.0): # 3x M3 cap screws, outside the outer race
u, v = 26.5 * math.cos(a * math.pi / 180.0), 26.5 * math.sin(a * math.pi / 180.0)
shape = shape.cut(Ycyl(1.25, y0 - .01, y0 + 12.0, px + u, pz + v))
return shape
def pivot_journal(shape, pz, y_link, y_tip, y_collar0, y_collar1, px, bore, collar):
"""Link side: the stub the inner race rides on, poking past the boss so the cap can bite
it, plus an integral collar the inner race bears against. The collar replaces a separate
spacer sleeve and lives in the boss's outer-race passage, so it must stay under that
diameter - the 0.1 mm it leaves is what lets the race, not the collar, take the thrust."""
a, b = sorted((y_link, y_tip))
shape = shape.fuse(Ycyl(bore / 2.0, a, b, px, pz))
c0, c1 = sorted((y_collar0, y_collar1))
return shape.fuse(Ycyl(collar / 2.0, c0, c1, px, pz))
def prism(pts2, z0, z1):
w2 = [(p[0], p[1]) for p in pts2]
return (cq.Workplane("XY", origin=(0, 0, z0))
.polyline(w2 + [w2[0]]).close().extrude(z1 - z0, combine=False)).val()
def Yplate(pts_xz, y0, y1):
return place(prism(pts_xz, -y1, -y0), RX90)
def dogbone(a, b, ra, rb, n=20):
"""Tangent hull of two discs. The outer-tangent offset angle is acos((ra-rb)/L);
using asin made the ra==rb case sweep a full circle around b and self-intersect."""
dx, dy = b[0] - a[0], b[1] - a[1]
L = math.hypot(dx, dy) or 1.0
th = math.atan2(dy, dx)
beta = math.acos(max(-1.0, min(1.0, (ra - rb) / L)))
sw_a = 2 * math.pi - 2 * beta
sw_b = 2 * beta
pts = [(a[0] + ra * math.cos(th + beta + sw_a * k / n), a[1] + ra * math.sin(th + beta + sw_a * k / n))
for k in range(n + 1)]
pts += [(b[0] + rb * math.cos(th - beta + sw_b * k / n), b[1] + rb * math.sin(th - beta + sw_b * k / n))
for k in range(1, n + 1)]
return pts
def sector(r_out, r_in, a0, a1, z0, z1, n=14):
"""Annular sector between two radii and two angles (deg) - a joint shroud segment.
Must NOT include the axis point, or this becomes a solid pie wedge that fills the joint."""
pts = [(r_out * math.cos((a0 + (a1 - a0) * k / n) * D2R),
r_out * math.sin((a0 + (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
pts += [(r_in * math.cos((a1 - (a1 - a0) * k / n) * D2R),
r_in * math.sin((a1 - (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
return prism(pts, z0, z1)
# ---------------------------------------------------------------- joint frames & interfaces
class Joint:
"""One revolute joint. `rear` = motor rear datum point, `d` = axis unit dir (motor -> hub)."""
def __init__(self, jid, cn, rear, d, xdir, lug_phase=0.0):
self.jid, self.cn = jid, cn
self.rear = rear
self.d = d
self.xdir = xdir
self.t = trs(rear, xdir, d) # motor-local -> global
self.lug_phase = lug_phase # rotation of the clamp about the axis
def L(self, shape_local):
return place(shape_local, self.t)
def at(self, s):
return (self.rear[0] + self.d[0] * s, self.rear[1] + self.d[1] * s, self.rear[2] + self.d[2] * s)
def pocket(self, shape, front_pad=0.6):
"""Cut the motor pocket out of a proximal link body.
REAR_SEAT = 电机尾端面(局部 z=0)到后端板承压面的距离。它不是定位面而是
**垫片位**:公差 MOTOR_TOL=2.0(armcore)要靠它消化 —— 电机比标称短的那一份,
就垫在尾端面与这个承压面之间(见 armcore.MOTOR_TOL 的说明)。原值 0.6 只能吃
0.6mm,短 1mm 以上的电机装不进、长 0.6mm 以上的电机会顶死后端板而压溃。
注意它同时是承压面的高度,所以让空 2.0mm 要求 t_plate >= 8
(见各零件 rear_mount_holes 的 t_plate)。
【2026-10-04 整改:承压面段改用 R35 圆腔,凸台彻底消失】
上一版把 64.4 **方**腔整体从 -0.6 加深到 -2.6(纯掏腔),方腔壁以外
z=-2.6..0 之间那圈 R35 料一动没动,于是相对新承压面整整凸起 2.6mm ——
这就是 P04~P08 后端那圈"悬空凸台"(用户截图红圈,3D 打印时该处是悬空
薄壁,既要支撑又公差难保证)。
为什么之前试"把凸台切掉"总失败(几何实测,别再重复):
- 64.4 方腔的**内切圆半径恰好 = 32.2**,所以方腔在 45° 方向已经吃穿到
r=45.5,R35 壁只剩**四条边中点附近**的料。实测 r=34 处材料沿 z 的
区间(承压面 -2.6):P06 的 0°/180° 是 -8.0..12.0(贯通,是臂身本体),
80°/100° 是 -8.0..-0.0(**只比承压面多 2.6mm,这段就是凸台**),
其余 20~160° 是 -8.0..-2.6(承压环本体,齐平)。
- 因此整段全截面切会把 0/90 方向那条**贯通到 z=12 的承力桥**一起切断
-> P05/P08 碎成 2 块(实测)。这不是"承压环是唯一连接",
而是我的刀具没区分"桥"和"悬空料"。
现在的做法(一刀,不做角度分区):
承压面所在的 z=-REAR_SEAT..SEAT_TOP 段,腔口取 **SEAT_R = R35 = 外壁**
的圆,把整圈挖到外壁,承压面因此变成**完整 R35 平圆盘**。
- 关键:**SEAT_R 必须等于 R35**。只要 SEAT_R < R35,两者之间那圈环墙
就必然存在,且相对承压面凸起 (R35-SEAT_R) —— 那圈环墙就是用户说的
"围挡"(P08 实测 1536.9mm3),不是残留而是我的取值必然产物。
- SEAT_TOP 取 +1.0(越过电机尾端面 1mm):2026-10-04 那版取 0 时,
半包开口处(Y93.0~95.8)会残留一截 0.052cm3 薄片脱空成碎块;
抬到 +1.0 后五件全部单实体零碎片。
- 承力改由**臂身本体**承担:band 段内 r36..58 有料的方位 P04 10/36、
其余四件各 15/36,其余方位靠 z>+1.0 的杯体连接,不存在整圈切断的通槽。
(2026-10-04 那版"肩叉顶部没连上"就是通槽形态,务必不要重现。)
【2026-10-05b 去掉 64.4 方腔,电机腔内壁恢复完整圆弧面】
此前 z=0..pcba_t 还叠了一层 `box(±32.2)` 方腔,理由是"PCB 是方的"。
**该假设不成立**:实测电机 STEP 已把 PCB 体积含在本体轮廓内,不会超出
投影范围,所以方腔纯属多挖。它带来两个副作用(用户截图 P06/P07/P08 腔内观感):
1. 方腔半宽 32.2 < 外壁 R35,在四个方位留下一圈 2.8mm 环形台肩;
2. 方腔对角 45.5 > R35,四个角把外壁咬穿,形成四个凸角。
两者叠加使内壁呈"圆—方—圆"三段,与同族零件的整体圆弧语言不一致。
现改为**单一 R35 圆腔从承压面一直挖到座孔起点 BORE_Z0(=5.5)**,
与其上的 R30.15 座孔正交相接,内壁成为两段同心正圆(仅一处正常台阶),
承压面到外壁之间再无任何凸起或方角。P04 走自己那套刀具,本来就无方腔。
"""
c = [cyl(BORE_R, BORE_Z0, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + front_pad),
# 承压面段 + 尾部过渡段:SEAT_R(=R35 外壁) 圆腔,从承压面一路挖到座孔起点。
# 取满 R35 才能让承压面与外壁齐平成一个平面;取小于 R35 就会留下
# 一圈凸起的"围挡"(见 docstring)。
cyl(SEAT_R, -REAR_SEAT, BORE_Z0)]
return shape.cut(*[self.L(s) for s in c])
def rear_mount_holes(self, shape, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0):
"""Cut the 4x rear mounting bolt holes into a proximal link body.
Matches the 4x M3 tapped radial slots on the motor rear endplate
(电机.STEP 实测):BCD55、相位 39.9°+90k,槽宽 2.5、径向可调约 ±1.25mm。
旧版误用 35.1x40.9 矩形孔距:对角两颗偏出螺纹槽 ~4.7mm 无处咬合
(2026-10-02 用户指出"螺钉没有对齐",按 STEP 实测槽位修正)。
"""
cuts = []
for k in range(4):
a = math.radians(39.9 + 90.0 * k)
sx = 27.5 * math.cos(a)
sy = 27.5 * math.sin(a)
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
cuts.append(self.L(h))
if cb_d > 0 and cb_depth > 0:
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
cuts.append(self.L(cb))
return shape.cut(*cuts)
def front_collar_pocket(self, shape, r_bore=30.25, z0=50.0, z1=56.5):
"""Cut front locating collar pocket for the motor front rim."""
p = cyl(r_bore, z0, z1)
return shape.cut(self.L(p))
def pinch(self, shape, fracs=(0.26, 0.58, 0.86), r_out=48.0, n=2):
"""Radial M3 pinch screws through the boss wall onto the housing."""
cuts = []
for fr in fracs[:n]:
for a in (self.lug_phase, self.lug_phase + 180.0):
c = cyl(PINCH_D / 2.0, -1.0, r_out, 0, 0)
c = place(c, R(a, 0, 0, 1))
c = place(c, T(0, 0, fr * COVER_Z))
cuts.append(self.L(c))
return shape.cut(*cuts)
def hub_face(self):
return self.at(HUB_FACE_Z)
def cut_hub(self, shape, depth=24.0, r_clear=32.0):
"""Cut the motor-output replica into the driven hub. The mating plane sits at
motor-local z = HUB_FACE_Z, and the recesses open there and run into the driven
material (away from the motor), so that half B's pilot (0.60) and spigot (4.06)
land in them."""
H = HUB_FACE_Z
# measured on the delivered half B: past the mating plane it is Ø35.6 for 1.4 mm
# then a Ø6.3 spigot to 4.06 - the nominal neck_depth of 1.0 is 0.4 too shallow,
# which showed up as a ~160 mm3 interference at every driven hub
cuts = [cyl(M["neck_d"] / 2.0, H - 0.05, H + 1.70),
cyl(M["centre_d"] / 2.0, H - 0.05, H + 5.05),
# probed on the delivered half: its Ø54 back face sits ~0.06 mm PAST the
# nominal plane, so the driven face gets a 0.35 deep, Ø54.6 relief. That is
# also the printed-part tolerance land the joint wants.
cyl(27.30, H - 0.05, H + 0.35)]
for a in HUB_M2_CLOCK:
u = M["m2_bcd"] / 2.0 * math.cos(a * D2R)
v = M["m2_bcd"] / 2.0 * math.sin(a * D2R)
cuts.append(cyl(M["m2_tap_d"] / 2.0, H - 0.05, H + M["m2_depth"], u, v))
return shape.cut(*[self.L(s) for s in cuts])
def hub_island(self, shape, r=30.0, t=9.5):
"""Solid disc at the joint centre (the lightening dish must not break into the
interface recesses), then the output replica is cut into it. Starts exactly on the
mating plane: half B's Ø54 body ends there, so any overlap is a hard clash."""
shape = shape.fuse(self.L(cyl(r, HUB_FACE_Z, HUB_FACE_Z + t)))
shape = self.cut_hub(shape)
# two arc shrouds over the clamp band: the joint reads as a closed drum, and the
# sectors are placed so the M3 lug bolts on the joint's local +-X stay reachable
for (a0, a1) in ((34.0, 146.0), (214.0, 326.0)):
shape = shape.fuse(self.L(sector(38.0, 31.6, a0, a1, FLANGE_Z + 1.4, HUB_FACE_Z)))
return shape
def place_hardware(self, half, clamp, tag):
"""Place the existing coupling halves + split clamp for this joint.
The delivered half is authored with z=0 at the motor flange face and spans
z -3.00..8.06. B = flip 180 deg about the local X axis, then shift by
2*PART_Z, which puts its flat back face exactly on the hub mating plane
(motor-local 65.00) - verified against the measured feature bounds.
"""
tf = trs(self.at(FLANGE_Z), self.xdir, self.d)
hA = place(half, tf)
hb = place(half, R(180, 1, 0, 0))
hb = place(hb, T(0, 0, 2 * C["part_z"]))
hB = place(hb, tf)
cA = place(clamp, tf)
cB = place(place(clamp, R(180, 0, 0, 1)), tf) # the two C pieces are one ring split
# along a diametral plane: 180 deg about the JOINT axis, so both keep the same
# axial span 56.602..64.402 and sit on opposite sides of the band.
return [("%s_半联轴器A" % tag, hA), ("%s_半联轴器B" % tag, hB),
("%s_抱箍A" % tag, cA), ("%s_抱箍B" % tag, cB)]
# ---------------------------------------------------------------- printed part builders
def bx(x0, x1, y0, y1, z0, z1):
return box(min(x0, x1), max(x0, x1), min(y0, y1), max(y0, y1), min(z0, z1), max(z0, z1))
GRP = { # part -> the joint group that carries it (kinematic tree)
"P01_底座板": "root", "P02_立柱": "root", "M1_J1": "root",
"J1_半联轴器A": "root", "J1_抱箍A": "root", "J1_抱箍B": "root",
"P03_转塔": "J1", "J1_半联轴器B": "J1",
"P04_肩支架": "J1", "M2_J2": "J1", "J2_半联轴器A": "J1",
"J2_抱箍A": "J1", "J2_抱箍B": "J1",
"P05_大臂": "J2", "J2_半联轴器B": "J2", "M3_J3": "J2",
"J3_半联轴器A": "J2", "J3_抱箍A": "J2", "J3_抱箍B": "J2",
"P06_小臂": "J3", "J3_半联轴器B": "J3", "M4_J4": "J3",
"J4_半联轴器A": "J3", "J4_抱箍A": "J3", "J4_抱箍B": "J3",
"P07_腕板": "J4", "P11_爪壳": "J4", "J4_半联轴器B": "J4", "M5_J5": "J4",
"P08_指A": "J4", "P09_指B": "J4", "P10_曲柄": "J5",
}
JAW = {"P08_指A": 1.0, "P09_指B": -1.0} # sign of the jaw travel along the J5 local +X
def explode_vec(p, JS, cents, gmean):
"""Explosion offset: coupling hardware separates along its joint axis, everything
else moves radially out of the assembly centroid."""
nm = p["name"]
c = cents[nm]
for jid in ("J1", "J2", "J3", "J4"):
if nm.startswith(jid + "_半联轴器B"):
return [JS[jid].d[i] * 24.0 for i in range(3)]
if nm.startswith(jid + "_半联轴器A") or nm.startswith(jid + "_抱箍"):
k = -26.0 if "抱箍" in nm else -16.0
return [JS[jid].d[i] * k for i in range(3)]
v = [c[i] - gmean[i] for i in range(3)]
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [v[i] / m * 34.0 for i in range(3)]
# ---------------------------------------------------------------- assembly
def write_step(items, path):
w = STEPControl_Writer()
for nm, sh in items:
Interface_Static.SetCVal_s('write.step.product.name', nm)
w.Transfer(sh.wrapped, STEPControl_StepModelType.STEPControl_AsIs)
w.Write(path.replace("/", "\\"))
return os.path.getsize(path)
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def export(JS):
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", WEB, DOCS):
os.makedirs(d, exist_ok=True)
print("=" * 80, flush=True)
print("【第二阶段 2/5】并行导出 STEP / STL 制造模型 (out/parts_step, out/print_stl)...", flush=True)
print("=" * 80, flush=True)
# 1. 多线程并行导出 STEP 与 STL(复用已有标准电机 STEP,极速提速)
printed_parts = [p for p in PARTS if p["printed"]]
total_exp = len(printed_parts)
done_exp = 0
exp_lock = threading.Lock()
def export_one_part(p):
nonlocal done_exp
t0 = time.time()
fn = "%s/%s.step" % (OUT + "/parts_step", p["name"])
# 如果是非打印标准已有件且已存在,跳过冗余写盘
if not p["printed"] and os.path.exists(fn) and os.path.getsize(fn) > 1000:
pass
else:
write_step([(p["name"], p["shape"])], fn)
if p["printed"]:
stl = "%s/%s.stl" % (OUT + "/print_stl", p["name"])
cq.exporters.export(cq.Workplane(obj=p["shape"]), stl, exportType="STL")
with exp_lock:
done_exp += 1
dt = time.time() - t0
print(f" [文件导出 {done_exp}/{total_exp}] {p['name']}.step / .stl 导出完成 ({dt:.2f}s) ✓", flush=True)
return p["name"]
workers = 1
with ThreadPoolExecutor(max_workers=workers) as ex:
list(ex.map(export_one_part, PARTS))
print(f"-> 制造模型导出全部完成!已保存在 {OUT}/parts_step 与 {OUT}/print_stl\n", flush=True)
# 2. 多线程并行生成网页 3D Mesh
print("=" * 80, flush=True)
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (web/mesh_arm.json)...", flush=True)
print("=" * 80, flush=True)
done_mesh = 0
mesh_lock = threading.Lock()
total_parts = len(PARTS)
def mesh_one_part(p):
nonlocal done_mesh
t0 = time.time()
key = "motor" if p["name"].startswith("M") else ("coupl" if "联轴器" in p["name"] or "抱箍" in p["name"] else p["name"])
defl = 0.85 if p["name"].startswith("M") else (0.20 if key == "coupl" else 0.055)
v, t = mesh_of(p["shape"], defl)
with mesh_lock:
done_mesh += 1
n_v = len(v) // 3
n_t = len(t) // 3
dt = time.time() - t0
print(f" [网格细分 {done_mesh:2d}/{total_parts}] {p['name']:<14} 顶点: {n_v:5d} | 面片: {n_t:5d} ({dt:.2f}s) ✓", flush=True)
return (p, v, t)
with ThreadPoolExecutor(max_workers=workers) as ex:
mesh_results = list(ex.map(mesh_one_part, PARTS))
parts = []
for idx, (p, v, t) in enumerate(mesh_results):
parts.append(dict(name=p["name"], cn=p["cn"], v=v, t=t, color=p["color"],
printed=p["printed"], vol=round(p["vol"], 0), note=p["note"],
grp=GRP.get(p["name"], "root"), jaw=JAW.get(p["name"], 0.0),
ex=[round(x, 1) for x in explode_vec(p, JS, CENTS,
[sum(CENTS[k][i] for k in CENTS) / float(len(CENTS)) for i in range(3)])
],
idx=idx + 1))
joints = [dict(id=k, cn=JS[k].cn, rear=JS[k].rear, d=JS[k].d, xdir=JS[k].xdir, hub=JS[k].hub_face())
for k in JS]
d3 = lambda a, b: math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
json.dump(dict(parts=parts, joints=joints,
layout=dict(ver=VERSION, it=ITER,
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
open(WEB + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(f"-> 3D网格与BOM清单已保存至 web/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
return bom
def collide():
ps = [p for p in PARTS if p["vol"] > 1.0]
pairs = []
MOUNT_PAIRS = {
("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_工具法兰_螺旋体30%_墙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)):
a, b = ps[i], ps[j]
if a["name"].startswith("M") and b["name"].startswith("M"):
continue
if (a["name"], b["name"]) in MOUNT_PAIRS or (b["name"], a["name"]) in MOUNT_PAIRS:
continue
av, bv = bb(a["shape"]), bb(b["shape"])
if any(av[1][k] < bv[0][k] - 0.05 or bv[1][k] < av[0][k] + 0.05 for k in range(3)):
continue
pairs.append((a, b))
print("=" * 80, flush=True)
print(f"【第四阶段 4/5】多线程精确布尔碰撞检测(从 {len(ps)*(len(ps)-1)//2} 对中初筛出 {len(pairs)} 组潜在包络相交对)...", flush=True)
print("=" * 80, flush=True)
done_pair = 0
pair_lock = threading.Lock()
def check_pair(pair):
nonlocal done_pair
a, b = pair
t0 = time.time()
v = common_vol(a["shape"], b["shape"])
with pair_lock:
done_pair += 1
status = f"干涉 {v:.1f} mm³ ❌" if v > 2.0 else "零干涉 (0.0 mm³) ✓"
print(f" [干涉检测 {done_pair:2d}/{len(pairs)}] {a['name']:<14} x {b['name']:<14} : {status} ({time.time()-t0:.3f}s)", flush=True)
if v > 2.0:
try:
c = BRepAlgoAPI_Common(a["shape"].wrapped, b["shape"].wrapped)
c.SetRunParallel(True)
c.Build()
cb = cq.Shape(c.Shape())
lo, hi = bb(cb)
loc = "@[%.0f %.0f %.0f..%.0f %.0f %.0f]" % (
lo[0], lo[1], lo[2], hi[0], hi[1], hi[2])
except Exception:
loc = "?"
return (a["name"], b["name"], round(v, 1), loc)
return None
rows = []
for pair in pairs:
res = check_pair(pair)
if res is not None:
rows.append(res)
rows.sort(key=lambda r: -r[2])
json.dump([r[:3] for r in rows], open(DOCS + "/collision.json", "w", encoding="utf-8"),
ensure_ascii=False)
if len(rows) == 0:
print(f"\n-> 静态碰撞干涉检测通过: 0 对干涉!全部 {len(pairs)} 组接触面对零碰撞!\n", flush=True)
else:
print(f"\n-> 发现 {len(rows)} 对干涉!\n", flush=True)
return rows
def axial_extent(j, shape):
lo, hi = bb(shape)
vals = []
for i in (0, 1):
for k in range(3):
p = [lo[0], lo[1], lo[2]]
p[k] = hi[k] if i else lo[k]
vals.append(sum((p[q] - j.rear[q]) * j.d[q] for q in range(3)))
return min(vals), max(vals)
def verify_interfaces(half, clamp, JS):
"""Numeric self-check of the derived joint stack, before trusting the renders."""
rows = []
for k, j in JS.items():
if k == "J5":
continue
parts = dict(j.place_hardware(half, clamp, k))
a = axial_extent(j, parts["%s_半联轴器A" % k])
b = axial_extent(j, parts["%s_半联轴器B" % k])
c = axial_extent(j, parts["%s_抱箍A" % k])
hub = j.at(HUB_FACE_Z)
rows.append(dict(jid=k, halfA=[round(x, 3) for x in a], halfB=[round(x, 3) for x in b],
clamp=[round(x, 3) for x in c], hub_face=round(HUB_FACE_Z, 3),
spigot_proud=round(b[1] - HUB_FACE_Z, 3),
clamp_clear=round(c[0] - COVER_Z, 3)))
return rows
+627
View File
@@ -0,0 +1,627 @@
# -*- coding: utf-8 -*-
"""Shared geometry / assembly / export library for the robot arm build.
No layout of its own: the joint frames and station positions live in
human_layout.py and arm_model.py, which import this module. What lives here is
the measured motor interface (docs/interfaces.md), the Joint class (motor slip
bore + PCBA pocket + pinch screws; driven hub mating plane at motor rear datum
+65.0 replicating the Ø36x1.0 neck / Ø6.5x4.0 centre / 3x M2 on Ø29 output so
the existing face-spline halves + split clamp bolt straight on), the part
registry, and the STEP/STL/mesh/BOM/collision export pipeline.
Run: python arm_model.py -> out/, web/mesh_arm.json, docs/
"""
import math
import os
import sys
import json
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cadquery as cq
from armcore import (M, C, V, trs, place, bb, bbox_str, cyl, ring, box, holes,
solids_of, mesh_of, MOTOR_TOL, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP)
from OCP.gp import gp_Trsf, gp_Vec, gp_Pnt, gp_Dir, gp_Ax1
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
from OCP.GProp import GProp_GProps
from OCP.BRepGProp import BRepGProp
from OCP.STEPControl import STEPControl_Writer, STEPControl_Controller, STEPControl_StepModelType
from OCP.Interface import Interface_Static
STEPControl_Controller.Init_s()
D2R = math.pi / 180.0
VERSION = os.environ.get("ARMVER", "r1")
ITER = int(os.environ.get("ARMITER", "1"))
# ---------------------------------------------------------------- measured stations
FLANGE_Z = M["flange_z"] # 54.942
COVER_Z = M["cover_z"] # 56.000
HUB_FACE_Z = 65.0 # motor rear datum -> driven hub mating plane
CLAMP_SPAN = (FLANGE_Z + 1.66, FLANGE_Z + 9.46)
CLAMP_R_BAND = 31.0
CLAMP_R_LUG = 38.0
BORE_R = M["hous_d"] / 2.0 + 0.15 # 30.15 slip fit on the housing
CB_R = M["cover_d"] / 2.0 + 0.35 # 30.50 clearance for the cover
PCB_W = M["pcba_w"] + 0.8 # 64.40 square pocket
HUB_M2_CLOCK = [(360.0 - a) % 360.0 for a in M["m2_clock"]] # phase of the mirrored half
HUB_PILOT_D = M["neck_d"] - 0.4 # 35.6 -> the half's pilot lands in a Ø36 recess
PINCH_D = 2.6 # M3 clearance in printed material
# 承压面让位(完整背景见 Joint.pocket 的 docstring):
# REAR_LAND = 0.6 腔口直壁台肩高度,**保持原设计值不变**(让位不靠加高台肩)
# REAR_SEAT = 2.6 尾端面->承压面 = 0.6 原台肩 + 2.0 公差垫片位
# R35_SEAT = 35.0 后端板 R35 外壁(承压面外径)
# SEAT_R = 35.0 承压面段(z=-REAR_SEAT..SEAT_TOP)的腔口半径 = **等于外壁 R35**。
# SEAT_TOP = 1.0 圆腔顶端越过电机尾端面(z=0) 1mm。
#
# 【2026-10-05 用户第三次指出:SEAT_R 必须是 35,"凸台+围挡"要一起弄平】
# 之前取 32.2(64.4 方腔的内切圆)以为这样能保住连接壁,实测**恰恰相反**:
# 32.2 与外壁 35 之间那圈 2.8mm 宽的环墙完整保留,而承压面只挖到 32.2,
# 于是这圈环墙相对承压面【凸起 2.6mm】,把电机尾端面整个围在里面 ——
# 这正是用户截图里的"一圈围挡"(P08 该圈实测 1536.9 mm3)。它不是残留,
# 而是 SEAT_R 取小值的必然产物:只要 SEAT_R < R35,环墙就一定存在。
# 用户要求:"把凸台、围挡一起弄平,做成和电机两头齐平一起的平面"。
# 故 SEAT_R 取满 R35:承压面段整圈挖到外壁,承压面变成**完整 R35 平圆盘**,
# 环墙(凸台)自然消失,电机尾端面与承压面之间再无任何凸起。
#
# 【为什么这次取 R35 不会重演2026-10-04 的"肩叉顶部没连上"】
# 那次我切的是 z=-2.6..0 且**只切到 z=0 就停**,导致环墙在 z=0..1.0 还残留一截
# 0.052cm3 的薄片(Y93.0~95.8 的半包开口处),那片薄片脱空成为独立碎块。
# 现在 SEAT_TOP=+1.0 把那一截一起吃掉:实测五件全部单实体、零碎片。
# 且承力桥改为由**臂身本体**(r>35)承担 —— 实测 band 段 r36..58 有料的方位
# P04 10/36、P05/P06/P07/P08 各 15/36,其余方位由 z=+1.0 以上的杯体连接,
# 不存在"整圈被切断"的通槽(那正是 2026-10-04 的错误形态)。
# 连带好处:承压环由「1.95mm 窄环」变「完整 R35 圆环 4.75mm」,受压面积 383->974mm²。
REAR_LAND = 0.6
REAR_SEAT = REAR_LAND + MOTOR_TOL # 2.6 尾端面->后端板承压面:公差垫片位
R35_SEAT = 35.0 # 后端板 R35 外壁(承压面外径)
SEAT_R = R35_SEAT # 35.0 承压面段腔口 = 外壁:凸台+围挡一并消除
SEAT_TOP = 1.0 # 圆腔顶端越过电机尾端面 1mm(吃掉开口处残片)
BORE_Z0 = 5.5 # 座孔起点:R35 圆腔挖到此处,与 R30.15 座孔相接
PARTS = []
CENTS = {}
def add(name, cn, shape, color, printed=True, note="", qty=1):
assert shape is not None, name
lo, hi = bb(shape)
PARTS.append(dict(name=name, cn=cn, shape=shape, color=color, printed=printed,
note=note, qty=qty, bb=bbox_str(shape), vol=vol_of(shape)))
CENTS[name] = [(lo[i] + hi[i]) / 2.0 for i in range(3)]
def vol_of(sh):
p = GProp_GProps()
BRepGProp.VolumeProperties_s(sh.wrapped, p)
return p.Mass()
def common_vol(a, b):
try:
c = BRepAlgoAPI_Common(a.wrapped, b.wrapped)
c.Build()
if not c.IsDone() or c.Shape().IsNull():
return 0.0
return vol_of(cq.Shape(c.Shape()))
except Exception:
return -1.0
def T(dx=0.0, dy=0.0, dz=0.0):
t = gp_Trsf()
t.SetTranslation(gp_Vec(dx, dy, dz))
return t
def R(deg, ax, ay, az, px=0.0, py=0.0, pz=0.0):
t = gp_Trsf()
t.SetRotation(gp_Ax1(gp_Pnt(px, py, pz), gp_Dir(ax, ay, az)), deg * D2R)
return t
RX90 = R(90, 1, 0, 0)
def Ycyl(r, y0, y1, px=0.0, pz=0.0):
return place(cyl(r, -y1, -y0, px, pz), RX90)
def Ybox(x0, x1, y0, y1, z0, z1):
return place(box(x0, x1, z0, z1, -y1, -y0), RX90)
# ------------------------------------------------------- pitch-joint pivot cartridge ----
# ONE recipe for every pitch joint (J2 shoulder, J3 elbow, J5 wrist) - they are the same
# degree of freedom, so an implementation that fixes only one of them leaves the identical
# defect in the other two. See human_layout PV_* for why a cartridge is needed at all:
# measured P04 n P05 = 0.00 mm3, so J2's 38 N*m had no path into the housing except peeling
# the face-spline teeth off across the print layers.
def pivot_geom(y_face, side, h, w, gap):
"""(boss_y0, boss_y1, pocket_y0) for a cartridge against the link face at y_face.
The race pocket always opens AWAY from the link, leaving an (h-w) shoulder behind the
outer race; a printed cap ring closes the open end. The boss stops `gap` short of the
link face, or a printed face would carry the thrust and the rolling elements nothing."""
near = y_face - gap if side < 0 else y_face + gap
if side < 0:
return near - h, near, near - h
return near, near + h, near + h - w
def pivot_boss(shape, pz, y0, y1, pk0, w, px, boss_od, race_od, bore, orace_id, side):
"""Housing side: boss on the joint axis with the outer race sunk into it. The passage
beyond the pocket is reamed to the outer race bore, not to the shaft: a shaft-sized bore
would end inside the inner race and bind it, and a bound bearing carries nothing - it
just moves the failure back into the print layers."""
shape = shape.fuse(Ycyl(boss_od / 2.0, y0, y1, px, pz))
shape = shape.cut(Ycyl(race_od / 2.0, pk0 - .01, pk0 + w + .01, px, pz))
if side < 0:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, pk0 + w, y1 + .01, px, pz))
else:
shape = shape.cut(Ycyl(orace_id / 2.0 + 0.1, y0 - .01, pk0, px, pz))
shape = shape.cut(Ycyl(bore / 2.0 + 0.2, y0 - .01, y1 + .01, px, pz))
for a in (90.0, 210.0, 330.0): # 3x M3 cap screws, outside the outer race
u, v = 26.5 * math.cos(a * math.pi / 180.0), 26.5 * math.sin(a * math.pi / 180.0)
shape = shape.cut(Ycyl(1.25, y0 - .01, y0 + 12.0, px + u, pz + v))
return shape
def pivot_journal(shape, pz, y_link, y_tip, y_collar0, y_collar1, px, bore, collar):
"""Link side: the stub the inner race rides on, poking past the boss so the cap can bite
it, plus an integral collar the inner race bears against. The collar replaces a separate
spacer sleeve and lives in the boss's outer-race passage, so it must stay under that
diameter - the 0.1 mm it leaves is what lets the race, not the collar, take the thrust."""
a, b = sorted((y_link, y_tip))
shape = shape.fuse(Ycyl(bore / 2.0, a, b, px, pz))
c0, c1 = sorted((y_collar0, y_collar1))
return shape.fuse(Ycyl(collar / 2.0, c0, c1, px, pz))
def prism(pts2, z0, z1):
w2 = [(p[0], p[1]) for p in pts2]
return (cq.Workplane("XY", origin=(0, 0, z0))
.polyline(w2 + [w2[0]]).close().extrude(z1 - z0, combine=False)).val()
def Yplate(pts_xz, y0, y1):
return place(prism(pts_xz, -y1, -y0), RX90)
def dogbone(a, b, ra, rb, n=20):
"""Tangent hull of two discs. The outer-tangent offset angle is acos((ra-rb)/L);
using asin made the ra==rb case sweep a full circle around b and self-intersect."""
dx, dy = b[0] - a[0], b[1] - a[1]
L = math.hypot(dx, dy) or 1.0
th = math.atan2(dy, dx)
beta = math.acos(max(-1.0, min(1.0, (ra - rb) / L)))
sw_a = 2 * math.pi - 2 * beta
sw_b = 2 * beta
pts = [(a[0] + ra * math.cos(th + beta + sw_a * k / n), a[1] + ra * math.sin(th + beta + sw_a * k / n))
for k in range(n + 1)]
pts += [(b[0] + rb * math.cos(th - beta + sw_b * k / n), b[1] + rb * math.sin(th - beta + sw_b * k / n))
for k in range(1, n + 1)]
return pts
def sector(r_out, r_in, a0, a1, z0, z1, n=14):
"""Annular sector between two radii and two angles (deg) - a joint shroud segment.
Must NOT include the axis point, or this becomes a solid pie wedge that fills the joint."""
pts = [(r_out * math.cos((a0 + (a1 - a0) * k / n) * D2R),
r_out * math.sin((a0 + (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
pts += [(r_in * math.cos((a1 - (a1 - a0) * k / n) * D2R),
r_in * math.sin((a1 - (a1 - a0) * k / n) * D2R)) for k in range(n + 1)]
return prism(pts, z0, z1)
# ---------------------------------------------------------------- joint frames & interfaces
class Joint:
"""One revolute joint. `rear` = motor rear datum point, `d` = axis unit dir (motor -> hub)."""
def __init__(self, jid, cn, rear, d, xdir, lug_phase=0.0):
self.jid, self.cn = jid, cn
self.rear = rear
self.d = d
self.xdir = xdir
self.t = trs(rear, xdir, d) # motor-local -> global
self.lug_phase = lug_phase # rotation of the clamp about the axis
def L(self, shape_local):
return place(shape_local, self.t)
def at(self, s):
return (self.rear[0] + self.d[0] * s, self.rear[1] + self.d[1] * s, self.rear[2] + self.d[2] * s)
def pocket(self, shape, front_pad=0.6):
"""Cut the motor pocket out of a proximal link body.
REAR_SEAT = 电机尾端面(局部 z=0)到后端板承压面的距离。它不是定位面而是
**垫片位**:公差 MOTOR_TOL=2.0(armcore)要靠它消化 —— 电机比标称短的那一份,
就垫在尾端面与这个承压面之间(见 armcore.MOTOR_TOL 的说明)。原值 0.6 只能吃
0.6mm,短 1mm 以上的电机装不进、长 0.6mm 以上的电机会顶死后端板而压溃。
注意它同时是承压面的高度,所以让空 2.0mm 要求 t_plate >= 8
(见各零件 rear_mount_holes 的 t_plate)。
【2026-10-04 整改:承压面段改用 R35 圆腔,凸台彻底消失】
上一版把 64.4 **方**腔整体从 -0.6 加深到 -2.6(纯掏腔),方腔壁以外
z=-2.6..0 之间那圈 R35 料一动没动,于是相对新承压面整整凸起 2.6mm ——
这就是 P04~P08 后端那圈"悬空凸台"(用户截图红圈,3D 打印时该处是悬空
薄壁,既要支撑又公差难保证)。
为什么之前试"把凸台切掉"总失败(几何实测,别再重复):
- 64.4 方腔的**内切圆半径恰好 = 32.2**,所以方腔在 45° 方向已经吃穿到
r=45.5,R35 壁只剩**四条边中点附近**的料。实测 r=34 处材料沿 z 的
区间(承压面 -2.6):P06 的 0°/180° 是 -8.0..12.0(贯通,是臂身本体),
80°/100° 是 -8.0..-0.0(**只比承压面多 2.6mm,这段就是凸台**),
其余 20~160° 是 -8.0..-2.6(承压环本体,齐平)。
- 因此整段全截面切会把 0/90 方向那条**贯通到 z=12 的承力桥**一起切断
-> P05/P08 碎成 2 块(实测)。这不是"承压环是唯一连接",
而是我的刀具没区分"桥"和"悬空料"。
现在的做法(一刀,不做角度分区):
承压面所在的 z=-REAR_SEAT..SEAT_TOP 段,腔口取 **SEAT_R = R35 = 外壁**
的圆,把整圈挖到外壁,承压面因此变成**完整 R35 平圆盘**。
- 关键:**SEAT_R 必须等于 R35**。只要 SEAT_R < R35,两者之间那圈环墙
就必然存在,且相对承压面凸起 (R35-SEAT_R) —— 那圈环墙就是用户说的
"围挡"(P08 实测 1536.9mm3),不是残留而是我的取值必然产物。
- SEAT_TOP 取 +1.0(越过电机尾端面 1mm):2026-10-04 那版取 0 时,
半包开口处(Y93.0~95.8)会残留一截 0.052cm3 薄片脱空成碎块;
抬到 +1.0 后五件全部单实体零碎片。
- 承力改由**臂身本体**承担:band 段内 r36..58 有料的方位 P04 10/36、
其余四件各 15/36,其余方位靠 z>+1.0 的杯体连接,不存在整圈切断的通槽。
(2026-10-04 那版"肩叉顶部没连上"就是通槽形态,务必不要重现。)
【2026-10-05b 去掉 64.4 方腔,电机腔内壁恢复完整圆弧面】
此前 z=0..pcba_t 还叠了一层 `box(±32.2)` 方腔,理由是"PCB 是方的"。
**该假设不成立**:实测电机 STEP 已把 PCB 体积含在本体轮廓内,不会超出
投影范围,所以方腔纯属多挖。它带来两个副作用(用户截图 P06/P07/P08 腔内观感):
1. 方腔半宽 32.2 < 外壁 R35,在四个方位留下一圈 2.8mm 环形台肩;
2. 方腔对角 45.5 > R35,四个角把外壁咬穿,形成四个凸角。
两者叠加使内壁呈"圆—方—圆"三段,与同族零件的整体圆弧语言不一致。
现改为**单一 R35 圆腔从承压面一直挖到座孔起点 BORE_Z0(=5.5)**,
与其上的 R30.15 座孔正交相接,内壁成为两段同心正圆(仅一处正常台阶),
承压面到外壁之间再无任何凸起或方角。P04 走自己那套刀具,本来就无方腔。
"""
c = [cyl(BORE_R, BORE_Z0, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + front_pad),
# 承压面段 + 尾部过渡段:SEAT_R(=R35 外壁) 圆腔,从承压面一路挖到座孔起点。
# 取满 R35 才能让承压面与外壁齐平成一个平面;取小于 R35 就会留下
# 一圈凸起的"围挡"(见 docstring)。
cyl(SEAT_R, -REAR_SEAT, BORE_Z0)]
return shape.cut(*[self.L(s) for s in c])
def rear_mount_holes(self, shape, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0):
"""Cut the 4x rear mounting bolt holes into a proximal link body.
Matches the 4x M3 tapped radial slots on the motor rear endplate
(电机.STEP 实测):BCD55、相位 39.9°+90k,槽宽 2.5、径向可调约 ±1.25mm。
旧版误用 35.1x40.9 矩形孔距:对角两颗偏出螺纹槽 ~4.7mm 无处咬合
(2026-10-02 用户指出"螺钉没有对齐",按 STEP 实测槽位修正)。
"""
cuts = []
for k in range(4):
a = math.radians(39.9 + 90.0 * k)
sx = 27.5 * math.cos(a)
sy = 27.5 * math.sin(a)
h = cyl(hole_d / 2.0, -t_plate - 1.0, 3.0, sx, sy)
cuts.append(self.L(h))
if cb_d > 0 and cb_depth > 0:
cb = cyl(cb_d / 2.0, -t_plate - 1.0, -t_plate + cb_depth, sx, sy)
cuts.append(self.L(cb))
return shape.cut(*cuts)
def front_collar_pocket(self, shape, r_bore=30.25, z0=50.0, z1=56.5):
"""Cut front locating collar pocket for the motor front rim."""
p = cyl(r_bore, z0, z1)
return shape.cut(self.L(p))
def pinch(self, shape, fracs=(0.26, 0.58, 0.86), r_out=48.0, n=2):
"""Radial M3 pinch screws through the boss wall onto the housing."""
cuts = []
for fr in fracs[:n]:
for a in (self.lug_phase, self.lug_phase + 180.0):
c = cyl(PINCH_D / 2.0, -1.0, r_out, 0, 0)
c = place(c, R(a, 0, 0, 1))
c = place(c, T(0, 0, fr * COVER_Z))
cuts.append(self.L(c))
return shape.cut(*cuts)
def hub_face(self):
return self.at(HUB_FACE_Z)
def cut_hub(self, shape, depth=24.0, r_clear=32.0):
"""Cut the motor-output replica into the driven hub. The mating plane sits at
motor-local z = HUB_FACE_Z, and the recesses open there and run into the driven
material (away from the motor), so that half B's pilot (0.60) and spigot (4.06)
land in them."""
H = HUB_FACE_Z
# measured on the delivered half B: past the mating plane it is Ø35.6 for 1.4 mm
# then a Ø6.3 spigot to 4.06 - the nominal neck_depth of 1.0 is 0.4 too shallow,
# which showed up as a ~160 mm3 interference at every driven hub
cuts = [cyl(M["neck_d"] / 2.0, H - 0.05, H + 1.70),
cyl(M["centre_d"] / 2.0, H - 0.05, H + 5.05),
# probed on the delivered half: its Ø54 back face sits ~0.06 mm PAST the
# nominal plane, so the driven face gets a 0.35 deep, Ø54.6 relief. That is
# also the printed-part tolerance land the joint wants.
cyl(27.30, H - 0.05, H + 0.35)]
for a in HUB_M2_CLOCK:
u = M["m2_bcd"] / 2.0 * math.cos(a * D2R)
v = M["m2_bcd"] / 2.0 * math.sin(a * D2R)
cuts.append(cyl(M["m2_tap_d"] / 2.0, H - 0.05, H + M["m2_depth"], u, v))
return shape.cut(*[self.L(s) for s in cuts])
def hub_island(self, shape, r=30.0, t=9.5):
"""Solid disc at the joint centre (the lightening dish must not break into the
interface recesses), then the output replica is cut into it. Starts exactly on the
mating plane: half B's Ø54 body ends there, so any overlap is a hard clash."""
shape = shape.fuse(self.L(cyl(r, HUB_FACE_Z, HUB_FACE_Z + t)))
shape = self.cut_hub(shape)
# two arc shrouds over the clamp band: the joint reads as a closed drum, and the
# sectors are placed so the M3 lug bolts on the joint's local +-X stay reachable
for (a0, a1) in ((34.0, 146.0), (214.0, 326.0)):
shape = shape.fuse(self.L(sector(38.0, 31.6, a0, a1, FLANGE_Z + 1.4, HUB_FACE_Z)))
return shape
def place_hardware(self, half, clamp, tag):
"""Place the existing coupling halves + split clamp for this joint.
The delivered half is authored with z=0 at the motor flange face and spans
z -3.00..8.06. B = flip 180 deg about the local X axis, then shift by
2*PART_Z, which puts its flat back face exactly on the hub mating plane
(motor-local 65.00) - verified against the measured feature bounds.
"""
tf = trs(self.at(FLANGE_Z), self.xdir, self.d)
hA = place(half, tf)
hb = place(half, R(180, 1, 0, 0))
hb = place(hb, T(0, 0, 2 * C["part_z"]))
hB = place(hb, tf)
cA = place(clamp, tf)
cB = place(place(clamp, R(180, 0, 0, 1)), tf) # the two C pieces are one ring split
# along a diametral plane: 180 deg about the JOINT axis, so both keep the same
# axial span 56.602..64.402 and sit on opposite sides of the band.
return [("%s_半联轴器A" % tag, hA), ("%s_半联轴器B" % tag, hB),
("%s_抱箍A" % tag, cA), ("%s_抱箍B" % tag, cB)]
# ---------------------------------------------------------------- printed part builders
def bx(x0, x1, y0, y1, z0, z1):
return box(min(x0, x1), max(x0, x1), min(y0, y1), max(y0, y1), min(z0, z1), max(z0, z1))
GRP = { # part -> the joint group that carries it (kinematic tree)
"P01_底座板": "root", "P02_立柱": "root", "M1_J1": "root",
"J1_半联轴器A": "root", "J1_抱箍A": "root", "J1_抱箍B": "root",
"P03_转塔": "J1", "J1_半联轴器B": "J1",
"P04_肩支架": "J1", "M2_J2": "J1", "J2_半联轴器A": "J1",
"J2_抱箍A": "J1", "J2_抱箍B": "J1",
"P05_大臂": "J2", "J2_半联轴器B": "J2", "M3_J3": "J2",
"J3_半联轴器A": "J2", "J3_抱箍A": "J2", "J3_抱箍B": "J2",
"P06_小臂": "J3", "J3_半联轴器B": "J3", "M4_J4": "J3",
"J4_半联轴器A": "J3", "J4_抱箍A": "J3", "J4_抱箍B": "J3",
"P07_腕板": "J4", "P11_爪壳": "J4", "J4_半联轴器B": "J4", "M5_J5": "J4",
"P08_指A": "J4", "P09_指B": "J4", "P10_曲柄": "J5",
}
JAW = {"P08_指A": 1.0, "P09_指B": -1.0} # sign of the jaw travel along the J5 local +X
def explode_vec(p, JS, cents, gmean):
"""Explosion offset: coupling hardware separates along its joint axis, everything
else moves radially out of the assembly centroid."""
nm = p["name"]
c = cents[nm]
for jid in ("J1", "J2", "J3", "J4"):
if nm.startswith(jid + "_半联轴器B"):
return [JS[jid].d[i] * 24.0 for i in range(3)]
if nm.startswith(jid + "_半联轴器A") or nm.startswith(jid + "_抱箍"):
k = -26.0 if "抱箍" in nm else -16.0
return [JS[jid].d[i] * k for i in range(3)]
v = [c[i] - gmean[i] for i in range(3)]
m = math.sqrt(sum(x * x for x in v)) or 1.0
return [v[i] / m * 34.0 for i in range(3)]
# ---------------------------------------------------------------- assembly
def write_step(items, path):
w = STEPControl_Writer()
for nm, sh in items:
Interface_Static.SetCVal_s('write.step.product.name', nm)
w.Transfer(sh.wrapped, STEPControl_StepModelType.STEPControl_AsIs)
w.Write(path.replace("/", "\\"))
return os.path.getsize(path)
import threading
import time
from concurrent.futures import ThreadPoolExecutor
def export(JS):
for d in (OUT, OUT + "/parts_step", OUT + "/print_stl", OUT + "/assembly", WEB, DOCS):
os.makedirs(d, exist_ok=True)
print("=" * 80, flush=True)
print("【第二阶段 2/5】并行导出 STEP / STL 制造模型 (out/parts_step, out/print_stl)...", flush=True)
print("=" * 80, flush=True)
# 1. 多线程并行导出 STEP 与 STL(复用已有标准电机 STEP,极速提速)
printed_parts = [p for p in PARTS if p["printed"]]
total_exp = len(printed_parts)
done_exp = 0
exp_lock = threading.Lock()
def export_one_part(p):
nonlocal done_exp
t0 = time.time()
fn = "%s/%s.step" % (OUT + "/parts_step", p["name"])
# 如果是非打印标准已有件且已存在,跳过冗余写盘
if not p["printed"] and os.path.exists(fn) and os.path.getsize(fn) > 1000:
pass
else:
write_step([(p["name"], p["shape"])], fn)
if p["printed"]:
stl = "%s/%s.stl" % (OUT + "/print_stl", p["name"])
cq.exporters.export(cq.Workplane(obj=p["shape"]), stl, exportType="STL")
with exp_lock:
done_exp += 1
dt = time.time() - t0
print(f" [文件导出 {done_exp}/{total_exp}] {p['name']}.step / .stl 导出完成 ({dt:.2f}s) ✓", flush=True)
return p["name"]
workers = 1
with ThreadPoolExecutor(max_workers=workers) as ex:
list(ex.map(export_one_part, PARTS))
print(f"-> 制造模型导出全部完成!已保存在 {OUT}/parts_step 与 {OUT}/print_stl\n", flush=True)
# 2. 多线程并行生成网页 3D Mesh
print("=" * 80, flush=True)
print("【第三阶段 3/5】多线程并行生成网页 3D 轻量化几何网格 (web/mesh_arm.json)...", flush=True)
print("=" * 80, flush=True)
done_mesh = 0
mesh_lock = threading.Lock()
total_parts = len(PARTS)
def mesh_one_part(p):
nonlocal done_mesh
t0 = time.time()
key = "motor" if p["name"].startswith("M") else ("coupl" if "联轴器" in p["name"] or "抱箍" in p["name"] else p["name"])
defl = 0.85 if p["name"].startswith("M") else (0.20 if key == "coupl" else 0.055)
v, t = mesh_of(p["shape"], defl)
with mesh_lock:
done_mesh += 1
n_v = len(v) // 3
n_t = len(t) // 3
dt = time.time() - t0
print(f" [网格细分 {done_mesh:2d}/{total_parts}] {p['name']:<14} 顶点: {n_v:5d} | 面片: {n_t:5d} ({dt:.2f}s) ✓", flush=True)
return (p, v, t)
with ThreadPoolExecutor(max_workers=workers) as ex:
mesh_results = list(ex.map(mesh_one_part, PARTS))
parts = []
for idx, (p, v, t) in enumerate(mesh_results):
parts.append(dict(name=p["name"], cn=p["cn"], v=v, t=t, color=p["color"],
printed=p["printed"], vol=round(p["vol"], 0), note=p["note"],
grp=GRP.get(p["name"], "root"), jaw=JAW.get(p["name"], 0.0),
ex=[round(x, 1) for x in explode_vec(p, JS, CENTS,
[sum(CENTS[k][i] for k in CENTS) / float(len(CENTS)) for i in range(3)])
],
idx=idx + 1))
joints = [dict(id=k, cn=JS[k].cn, rear=JS[k].rear, d=JS[k].d, xdir=JS[k].xdir, hub=JS[k].hub_face())
for k in JS]
d3 = lambda a, b: math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
json.dump(dict(parts=parts, joints=joints,
layout=dict(ver=VERSION, it=ITER,
L1=round(d3(JS["J2"].rear, JS["J4"].rear), 1),
L2=round(d3(JS["J4"].rear, JS["J5"].rear), 1)),
measured=dict(M=M, C=C, HUB_FACE_Z=HUB_FACE_Z, CLAMP_SPAN=list(CLAMP_SPAN))),
open(WEB + "/mesh_arm.json", "w", encoding="utf-8"), ensure_ascii=False)
bom = [dict(件号=p["name"], 名称=p["cn"], 类型="3D打印" if p["printed"] else "已有/电机",
数量=1, 体积cm3=round(p["vol"] / 1000.0, 1),
质量g_PLA=round(p["vol"] * 1.24e-3, 1), 包络=p["bb"], 说明=p["note"]) for p in PARTS]
json.dump(bom, open(DOCS + "/bom.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
print(f"-> 3D网格与BOM清单已保存至 web/mesh_arm.json 与 docs/bom.json ✓\n", flush=True)
return bom
def collide():
ps = [p for p in PARTS if p["vol"] > 1.0]
pairs = []
MOUNT_PAIRS = {
("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_工具法兰_螺旋体30%_墙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)):
a, b = ps[i], ps[j]
if a["name"].startswith("M") and b["name"].startswith("M"):
continue
if (a["name"], b["name"]) in MOUNT_PAIRS or (b["name"], a["name"]) in MOUNT_PAIRS:
continue
av, bv = bb(a["shape"]), bb(b["shape"])
if any(av[1][k] < bv[0][k] - 0.05 or bv[1][k] < av[0][k] + 0.05 for k in range(3)):
continue
pairs.append((a, b))
print("=" * 80, flush=True)
print(f"【第四阶段 4/5】多线程精确布尔碰撞检测(从 {len(ps)*(len(ps)-1)//2} 对中初筛出 {len(pairs)} 组潜在包络相交对)...", flush=True)
print("=" * 80, flush=True)
done_pair = 0
pair_lock = threading.Lock()
def check_pair(pair):
nonlocal done_pair
a, b = pair
t0 = time.time()
v = common_vol(a["shape"], b["shape"])
with pair_lock:
done_pair += 1
status = f"干涉 {v:.1f} mm³ ❌" if v > 2.0 else "零干涉 (0.0 mm³) ✓"
print(f" [干涉检测 {done_pair:2d}/{len(pairs)}] {a['name']:<14} x {b['name']:<14} : {status} ({time.time()-t0:.3f}s)", flush=True)
if v > 2.0:
try:
c = BRepAlgoAPI_Common(a["shape"].wrapped, b["shape"].wrapped)
c.SetRunParallel(True)
c.Build()
cb = cq.Shape(c.Shape())
lo, hi = bb(cb)
loc = "@[%.0f %.0f %.0f..%.0f %.0f %.0f]" % (
lo[0], lo[1], lo[2], hi[0], hi[1], hi[2])
except Exception:
loc = "?"
return (a["name"], b["name"], round(v, 1), loc)
return None
rows = []
for pair in pairs:
res = check_pair(pair)
if res is not None:
rows.append(res)
rows.sort(key=lambda r: -r[2])
json.dump([r[:3] for r in rows], open(DOCS + "/collision.json", "w", encoding="utf-8"),
ensure_ascii=False)
if len(rows) == 0:
print(f"\n-> 静态碰撞干涉检测通过: 0 对干涉!全部 {len(pairs)} 组接触面对零碰撞!\n", flush=True)
else:
print(f"\n-> 发现 {len(rows)} 对干涉!\n", flush=True)
return rows
def axial_extent(j, shape):
lo, hi = bb(shape)
vals = []
for i in (0, 1):
for k in range(3):
p = [lo[0], lo[1], lo[2]]
p[k] = hi[k] if i else lo[k]
vals.append(sum((p[q] - j.rear[q]) * j.d[q] for q in range(3)))
return min(vals), max(vals)
def verify_interfaces(half, clamp, JS):
"""Numeric self-check of the derived joint stack, before trusting the renders."""
rows = []
for k, j in JS.items():
if k == "J5":
continue
parts = dict(j.place_hardware(half, clamp, k))
a = axial_extent(j, parts["%s_半联轴器A" % k])
b = axial_extent(j, parts["%s_半联轴器B" % k])
c = axial_extent(j, parts["%s_抱箍A" % k])
hub = j.at(HUB_FACE_Z)
rows.append(dict(jid=k, halfA=[round(x, 3) for x in a], halfB=[round(x, 3) for x in b],
clamp=[round(x, 3) for x in c], hub_face=round(HUB_FACE_Z, 3),
spigot_proud=round(b[1] - HUB_FACE_Z, 3),
clamp_clear=round(c[0] - COVER_Z, 3)))
return rows
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
[
{
"axis": "J1",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
},
{
"axis": "J2",
"lo": -105.0,
"hi": 9.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J3",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
},
{
"axis": "J4",
"lo": -111.0,
"hi": 16.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +16° / 反向 -111°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J5",
"lo": -93.0,
"hi": 34.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J6",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
}
]
+50
View File
@@ -0,0 +1,50 @@
[
{
"axis": "J1",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
},
{
"axis": "J2",
"lo": -105.0,
"hi": 9.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J3",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
},
{
"axis": "J4",
"lo": -111.0,
"hi": 16.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +16° / 反向 -111°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J5",
"lo": -93.0,
"hi": 34.0,
"continuous": false,
"zero": 0.0,
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
},
{
"axis": "J6",
"lo": -180.0,
"hi": 180.0,
"continuous": true,
"zero": 0.0,
"note": "整周无干涉,可 360° 连续旋转(全对全动态扫描验证)"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -8,6 +8,13 @@ out/
# 临时调试脚本与截图
scratch/
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
backups/
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
.bak/
*.bak_*
# 编辑器 / 系统文件
Thumbs.db
Desktop.ini
@@ -0,0 +1,204 @@
# 姿态估计算法评估与优化 —— 实施计划
> 交付物(用户已确认):**仅一份中文评估报告**,不改动求解器代码。
> 报告落盘位置:`docs/姿态估计算法评估报告.md`
---
## 1. 摘要(Summary)
本任务对机械臂的「姿态估计算法」——即 [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) 的 6 轴逆运动学(IK)姿态求解器及其查看器集成 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js)(用户已确认该文件即「姿态估计算法」)——做一次全面评估。
需要解决的核心问题:**在多个关键位姿(近工作空间边界、折叠/奇异构型、贴近关节限位处)无法达到预定姿态目标;且当目标姿态确实不可达时,返回的「最近位姿」明显劣于肉眼可见的更优姿态。**
评估已在本阶段(Plan 模式,只读)实测复现并定位到 4 类缺陷(见 §2.3),并额外回答了两个关键设计问题:**现状是「先点位、后角度」的位置优先架构**(§2.4),以及**位置与姿态在远目标上真正不可兼得、必须提供可切换优先级**(§2.4 实测取舍表)。本计划定义报告的**结构、数据来源、可复现实验命令、优化策略与验证口径**。执行阶段只产出一份报告 md,不新增任何脚本文件(实验以 `node -e` 内联执行,命令内嵌报告附录)。
---
## 2. 现状分析(Current State Analysis)
### 2.1 代码与数据资产
| 角色 | 文件 | 说明 |
| :--- | :--- | :--- |
| 姿态求解器(主) | [arm-ik-solver.js](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js) | UMD,`make(cfg)` 返回 `solve/solveTo/solveWarm/solveDyn/closest/closest6/branch/fk/orient/costOf` 等([导出清单 L1355-1360](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1355-L1360)) |
| 查看器集成 | [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) | 姿态 UI、目标拖动、`solveTo` 调用([L379](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L379)、[L399](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L399))、结果渲染与徽标判定([renderResult L318-342](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js#L318-L342)) |
| 关节表(几何真值) | [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) | `joints[] = {id, rear, d}`,FK/IK 运动学常量来源 |
| 关节限位 | [travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json) | 本报告测量口径:J1/J3/J6 连续(±180),J2∈[-106,9],J4∈[-111,16],J5∈[-95,34](2026-09-29 起该表退为零位扫描参考:滑块改电机行程 ±360°,可行域改相邻件实测区间) |
| 既有自检 | [pose_ik_check.js](file:///g:/Robot_v2/机器人手臂/scratch/pose_ik_check.js) | 7 组 node 自检(往返、位置回归、姿态重构、交互口径、兜底、耗时) |
| 既有性能基准 | [bench_ik_split.js](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.js) / [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) | 单次求解耗时与「位置/姿态」占比拆分 |
| 历史优化实验 | `scratch/ikopt/`(`solver_scanbase.js` 等) | 根枚举/窗口/同族门等过往迭代产物,报告需说明其结论 |
### 2.2 现有算法骨架(报告需准确描述)
1. **位置闭式 + 二维冗余扫描**:以 (θ4,θ5) 为冗余参数全域扫描,θ1..θ3 由 [branch() L429-463](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L429-L463) 闭式给出(最多 4 支:θ3=±σ、±(π−σ)),位置天然精确到 1e-13 mm。
2. **按行裁剪**:[rowSpans L644-654](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L644-L654) 用探测步 `hp=max(h,2)°` 找每 θ4 行的可行 θ5 段,段内再按 h 精扫。
3. **姿态任务**:工具轴指向 = 目标轴,是 (θ4,θ5) 上的 2 方程 2 未知量,解为**孤立点**;[poseTools L486-545](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L486-L545) 提供残差/牛顿原语;[§4.1b 根枚举 L697-761](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L697-L761) 逐行做变号括号 + 牛顿收口;滚转由 [rollClose L549-555](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L549-L555) 闭式补 θ6。
4. **接受门**:`POSETOL=2.0°`([L581](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L581))——`scoreBranch` 对 `oerr>POSETOL` 的候选**直接返回 null 丢弃**([L596-598](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L596-L598))。
5. **不可达兜底**:[closest6 L1054-1169](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 为 6 维阻尼最小二乘(DLS),160 次确定性拟随机重启 × 160 迭代;[solveTo L1172-1192](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1172-L1192) 在 `solve()` 失败时调用它。
6. **实时快路**:[solveWarm L1200-1265](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1200-L1265),沿上一帧解同支续解,失败回落全量。
### 2.3 已实测缺陷(本阶段只读复现,数据可直接写入报告)
**缺陷 A — 覆盖漏洞:目标姿态精确可达,求解器却静默返回 ~1–2° 残差**
- 全空间随机可达位形,`solve(step=0.25)` 返回 `oerr>0.05°` 的比例:**2.5%(3/120)**,量级 `oerr∈[1.24, 1.95]°`(真值应为 0)。
- 其中可明确归因于「覆盖漏洞」(细扫 step=0.05 能到 ≤0.05°、即确有大步长漏解)的部分,小样本估计 **0.3%–1.3%**(300 组样本:1/300;80 组样本:1/80);其余为真·近奇异构型(细扫同样到不了 ≤0.05°,属物理不可达的刀尖构型)。**执行阶段用更大样本(D1/D4)收敛该比例。**
- 典型案例 `T=[329.3,253.4,750.0]`:真解 (θ4,θ5)=(-16.45,-51.97),`branch()` 在该点给出 **位置误差 5.7e-14 mm、轴误差 0.000°** 的精确解;但 `solve(step=0.25)` 返回 oerr=1.909°。
- **步长敏感(非分辨率问题)**:同一目标 `oerr` 随 `step` 非单调跳变——`0.25°→1.60`、`0.15°→0.045`、`0.10°→1.14`、`0.05°→0.013`;用 `solveTo({warm:真解})` 则得到 **oerr=0.0000**。证明确切解存在,是**根枚举/牛顿收口的完备性缺口**。
- 工作区实测:前伸工作区 (20,210,300)±90mm 失败率 **1.7%(1/60)**;查看器默认区 (150,128,260)±60mm 为 **0%(0/60)**——缺口集中在工作空间外围/极端构型。
**缺陷 B — 不可达兜底 closest6 严重次优(「肉眼可见更优姿态」的直接来源)**
以「位置精确解中姿态误差的全局最小值」为真最优(对 (θ4,θ5) 网格 0.05° 枚举 `branch()` 全部在限位内的支),目标姿态在可达姿态基础上外推 20°:
| 案例 | 目标 TCP | 兜底返回 | 真最优姿态差 | 劣化 |
| :--- | :--- | ---: | ---: | ---: |
| 1 | (40,455,216) | 位置 0.4mm / 姿态 **74.0°** | **11.04°** | **63.0°** |
| 2 | (196,122,136) | 位置 0.5mm / 姿态 **130.0°** | **17.26°** | **112.8°** |
| 3 | (-141,-18,1030) | 位置 0.0mm / 姿态 **167.2°** | **89.09°** | **78.1°** |
**缺陷 C — 姿态不可达时把「位置精确解候选池」整体丢弃,导致兜底被便宜解严格支配**
- `T=[20,400,300]`、`R=poseMat(25,95,10)`:位置完全可达(无姿态 `solve` 返回 err=2.5e-14,0.5° 格上有 **11920** 个位置精确解),但姿态目标不可达时 `scoreBranch` 因 `oerr>POSETOL` 把全部候选置 null,`solve()` 返回 `ok=false`、候选池为空,被迫走 [closest6](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L1054-L1169) 的 6 维随机 DLS。
- 同例对照(实测):位置精确解集内最小姿态差 = **121.05°**;`closest6` 返回 **136.7° / 位置 0.0mm**。即 `closest6` 在**位置与姿态两个指标上同时劣于**一个用现有闭式扫描就能廉价得到的解 —— **被严格支配(Pareto-dominated)**。
- 后果:`位置精确 + 姿态尽力` 这条最自然的路径没有实现;兜底既不最优、又更慢(159–224ms vs 扫描 ~100ms)。
**缺陷 D — 性能超出既定预算**
- [bench_ik_split.json](file:///g:/Robot_v2/机器人手臂/scratch/bench_ik_split.json) 摘要:开姿态单次 **中位 107ms / p95 242ms / 最大 299ms**(位置基座中位 73.6ms,姿态增量中位 37.2ms 占 33%,escalating 触发率 2.5%)。
- 本阶段在工作区实测均耗时 112–157ms;不可达兜底 159–224ms。
- 均高于用户设定的 **≤100ms** 目标,且兜底路径(random-restart DLS)是主要浪费点。
### 2.4 求解范式评估:位置优先 vs 末端姿态反推(用户提出,报告中作为备选简要说明)
**用户提问**:能否用已指定的末端角度,从末端逐级反推回各关节?
**几何事实(以 [mesh_arm.json](file:///g:/Robot_v2/机器人手臂/web/mesh_arm.json) 为准)**:
| 关节 | rear | d | 类型 |
| :--- | :--- | :--- | :--- |
| J1 | (-51,128,610) | (1,0,0) | X 抬举 |
| J2 | (18.5,100,490) | (0,1,0) | Y 俯仰 |
| J3 | (18.5,128,445) | (0,0,-1) | Z 自转 |
| J4 | (18.5,100,332) | (0,**1**,0) | Y 俯仰(肘) |
| J5 | (18.5,100,222) | (0,**1**,0) | Y 俯仰(腕) |
| J6 | (18.5,128,176) | (0,0,**-1**) | Z 工具自转 |
- **J4 ∥ J5**(均 +Y,相距 110mm)⇒ 平行轴永不相交 ⇒ **非球形腕**,经典"腕心解耦 + θ6→θ5→θ4 逐级闭式反推"不成立。(J5⊥J6 在 (18.5,128,222) 相交,仅构成 2 轴腕。)
- 但存在可用的**降阶结构**:因 J4∥J5,`R4·R5 = R_y(θ4+θ5)`(代码 [L406-411](file:///g:/Robot_v2/机器人手臂/web/arm-ik-solver.js#L406-L411) 已用 `SAMEAX` 利用该性质),故
```
R = Rx(θ1)·Ry(θ2)·Rz(−θ3)·Ry(θ4+θ5)·Rz(−θ6)
```
⇒ **工具轴只由 (θ1,θ2,θ3,θ4+θ5) 决定,θ6 是纯工具滚转**;"用末端姿态驱动求解"可行,但需按此降阶链求解(3 方程 / 5 参数 → 2 参数族),而非逐级反推。
**位置与姿态在远目标上"真的不可兼得"(实测,报告须收录)**:
| 目标 | 优先级 | 位置误差 | 姿态差 |
| :--- | :--- | ---: | ---: |
| T=(20,400,300), R=poseMat(25,95,10) | 位置优先(保点) | **0.0 mm** | 121.05° |
| 同上 | 姿态优先(保角度) | 127.6 mm | **0.22°** |
| T=(196,122,136), R=poseMat(25,95,10) | 位置优先 | **0.0 mm** | 116.39° |
| 同上 | 姿态优先 | 233.6 mm | **0.45°** |
⇒ 结论:二者是**真实的取舍**,不存在"都能兼顾"的解;算法必须提供**可切换的优先级**。
### 2.5 误差来源定位(报告核心结论)
- **R1 根枚举不完备**:姿态精确解在 (θ4,θ5) 平面是一条一维曲线,只在少数孤立点穿过 `POSETOL` 窄带;当该曲线经过 θ3≈±90° 的**折叠点**(`branch()` 的 4 支两两重合、位置雅可比奇异)、或贴着关节限位/可行段边缘时,变号括号检测失效、`match()` 的「θ1..θ3 最近」同支判据在折叠处换支,牛顿收口失败。
- **R2 接受门把「不可达」与「漏解」混为一谈**:`oerr>POSETOL` 一律丢弃 ⇒ 无法区分「确实不可达」与「可达但没找到」,导致 A 类静默近失 + C 类候选池清空。
- **R3 兜底目标函数与真实目标不一致**:`closest6` 用固定的 20mm/rad 把姿态折进位置残差做联合 DLS,既不保证「位置优先、姿态最小」,也依赖随机重启初值,落点常是劣质局部极值。
- **R4 行裁剪探测步过粗**:`hp=max(h,2)°` 且按单一连续段假设(注释称仅在 5 个目标上验证过),窄于 2° 的可行段会被整段漏掉 ⇒ 整行/整区无候选。
---
## 3. 交付物与报告大纲(Proposed Changes)
**唯一变更**:新建 `docs/姿态估计算法评估报告.md`(中文)。
报告章节(每章明确数据来源,均为本阶段或执行阶段 `node -e` 实测可得):
1. **概述与结论摘要**(TL;DR 表格:缺陷 / 影响 / 根因 / 建议 / 预期收益)
2. **算法现状描述**——§2.2 的骨架,附关键函数行号链接与数据流图
3. **求解范式评估(位置优先 vs 末端姿态反推)**——§2.4:
- 确认现状为「先点位、后角度」(位置被强制精确、姿态是带 2° 门的软约束)
- 关节轴几何表 + `J4∥J5 ⇒ 非球形腕` 的判定;说明「逐级闭式反推」不成立的**原因**(备选简要说明,约半页,含降阶链 `Rx·Ry·Rz·Ry(θ4+θ5)·Rz` 的结论式)
- 位置/姿态**不可兼得**的实测取舍表(§2.4),给出「必须提供可切换优先级」的结论
4. **评估方法**——指标定义:
- 姿态残差 `oerr`(`rotErrDeg`,度)
- 位置残差 `err`(mm)
- 覆盖漏洞率 = 「真解 oerr≤0.05° 而解算 oerr>0.05°」占比
- 步长敏感度 = 同目标在 step∈{0.05,0.1,0.15,0.25,0.5} 的 oerr 波动
- 兜底劣化 = `closest6` 姿态差 −「位置精确解集内最优姿态差」
- 单次耗时中位/p95(开/关姿态、可达/兜底)
5. **性能分析**——引用 bench_ik_split.json 的耗时构成 + 工作区实测;给出「位置/姿态/兜底」三段占比
6. **误差来源定位**——§2.5 的 R1–R4,逐条给出**可复现证据**(目标坐标、真解、实测值)
7. **优化策略(设计方案,不在本轮落地)**——按收益/成本排序给出可实施建议:
- **S0(架构)双优先级求解范式**:仿 [ik-glue.js](file:///g:/Robot_v2/机器人手臂/web/ik-glue.js) 现有姿态模式的 UI 开关,新增「优先级」选择——**位置优先 / 姿态优先 / 折中**;三种模式下共用同一闭式支枚举结果,只切换目标函数与选解规则。给出各模式的数学表述:
- 位置优先:`min oerr s.t. ‖fk(q)−T‖=0`(在位置精确解集上最小化姿态误差)
- 姿态优先:`min ‖fk(q)−T‖ s.t. oerr=0`(在姿态精确解集上最小化位置误差;利用降阶链参数化)
- 折中:`min ‖fk(q)−T‖² + k·oerr²`,k 可调
- 预期:位置优先模式把 B 类劣化从 63–113° 压到接近真最优;姿态优先模式提供 oerr≤0.05° 的「保角度」解(代价是位置误差,如实上报)
- **S1(根因修复)覆盖率完备化**:把 (θ4,θ5) 上的姿态方程看作一维解曲线,用**预测-校正 continuation 跟踪**(以解析/数值切向推进、跨越折叠点不换支),或等价地做**分支索引对齐的多起点牛顿 + 折叠点/限位角种子**;预期把 A 类覆盖漏洞降到 <0.2%,且不加粗扫密度 ⇒ 耗时基本不变(≤100ms)。
- **S2(关键收益)姿态不可达时不再清空候选池**:去掉 `POSETOL` 硬门对候选池的**清空**语义,改为保留并按 `oerr` 排序入池(门只用于「达标」判定,不用于「丢弃」);预期消除 C 类 Pareto 支配,且兜底不再需要。
- **S3 兜底加速**:以 S0/S2 的闭式候选为种子替代 160 次随机重启,仅在位置也越界时才退回受限 DLS;预期兜底耗时从 ~160–220ms 降到 ≤100ms。
- **S4 可观测性**:UI 明确区分「已到达 / 位置到达·姿态最近 X° / 姿态到达·位置最近 Y mm / 均不可达」四种语义,避免把漏解或取舍结果当成功。
- 每条策略给出:改动点(函数+行号)、原理、预期指标、代价与风险、与 ≤100ms 预算的关系。
8. **验证方案与验收口径**——见 §5。
9. **附录**:可复现实验命令(`node -e` 全文内联,不落脚本文件)+ 基准数据表 + 术语表。
> 说明:报告只做「评估 + 策略设计」,不含代码修改。若后续要落地 S0–S3,需另起一次实施任务。
---
## 4. 假设与决策(Assumptions & Decisions)
| 项 | 决策 | 依据 |
| :--- | :--- | :--- |
| 交付物 | 仅评估报告 md,**不改代码** | 用户选择「仅评估报告」 |
| 「姿态估计算法」定义 | = `web/arm-ik-solver.js` 的 IK 姿态求解 + `ik-glue.js` 集成 | 全仓检索无 IMU/卡尔曼/姿态估计类算法;仓库内「姿态」相关实现仅此一处 |
| 性能预算 | 以 **≤100ms** 为优化目标约束(报告据此评估各策略) | 用户选择「保持 ≤100ms」 |
| 失败场景 | A(可达却漏解)与 B/C(不可达最近位姿)**都覆盖** | 用户选择「两者都做」 |
| 实验脚本 | 不新增文件,全部 `node -e` 内联并在报告附录给出原文 | 遵守「仅评估报告」与「不新建文件」 |
| **求解优先级** | 报告须设计**双优先级机制(位置优先 / 姿态优先 / 折中),界面可切换**;不可兼得时两种解都给出 | 用户选择「两者都做、界面可切换」 |
| **末端姿态反推** | 报告仅作**备选简要说明**(说明非球形腕故不能逐级闭式反推 + 降阶链结论),不展开公式级设计 | 用户选择「只作备选简要说明」 |
| 关节表/限位 | 以 `web/mesh_arm.json` + `docs/travel.json` 为准,tcp=[18.5,128,93] | 与 `pose_ik_check.js`、`bench_ik_split.js` 一致 |
| 真最优基准 | 「位置精确解集内姿态误差最小」的网格枚举(0.05° 分辨率,θ6 另行闭式最优) | 独立于被测求解器,避免自证 |
| 随机性 | 固定 LCG 种子的确定性采样,报告数据可复现 | 与求解器内部自检策略一致 |
---
## 5. 验证方案(报告第 8 章内容)
**基准数据集**(执行阶段固定种子生成,规模与命令写入附录):
- D1 全空间随机可达位形 ≥200 组(覆盖极端/边界构型)
- D2 前伸工作区 (20,210,300)±90mm ≥60 组、查看器默认区 (150,128,260)±60mm ≥60 组
- D3 姿态外推集(外推 3/8/20°)≥30 组 → 测兜底劣化
- D4 退化构型集(θ3≈±90°、临近限位)≥30 组 → 测覆盖漏洞
- D5 越界目标集 → 测兜底耗时与质量
- D6 **双优先级取舍集**:位置与姿态不可兼得的远目标 ≥20 组 → 测「位置优先」与「姿态优先」两解各自的误差与真实性
**指标与验收口径(现状基线 → 目标)**:
| 指标 | 现状实测 | 目标 |
| :--- | :--- | :--- |
| D1/D4 覆盖漏洞率(真解≤0.05° 却返回>0.05°) | 2.5% | ≤0.2% |
| D1 最大静默姿态残差 | 1.95° | ≤0.05° |
| 步长敏感度(同目标 oerr 波动) | 0→1.86° | ≤0.05° |
| D3/D5 兜底相对真最优劣化(姿态优先口径) | 63–113° | ≤2° |
| D6 位置优先解:位置误差 / 姿态差 | 0.0mm / 121.05° | 位置=0;姿态=真最优(劣化 ≤0.5°) |
| D6 姿态优先解:姿态差 / 位置误差 | 0.22° / 127.6mm | 姿态 ≤0.05°;位置如实上报 |
| 开姿态单次耗时 p95 | 242ms | ≤100ms |
| 兜底耗时 | 159–224ms | ≤100ms |
| 姿态残差 P95(可达集) | ≤2.05°(接受限) | ≤0.05° |
**沿用既有自检**:`node scratch/pose_ik_check.js` 的 7 项(含往返一致性、位置回归、姿态重构、roll→θ6 口径、6D 兜底、耗时对比)作为回归门,报告中给出其现状通过情况与目标。
**报告自检**:报告内每张数据表须与附录命令重跑结果一致;所有结论必须能由附录命令复现。
---
## 6. 执行步骤(待批准后按序执行)
1. 复跑并固化基线数据(D1–D6 + bench + 自检),生成报告用数据表。
2. 撰写 `docs/姿态估计算法评估报告.md` 正文(§3 大纲 1–9 章)。
3. 内嵌可复现 `node -e` 命令与原始输出,形成附录。
4. 复核报告数据与结论一致性(重跑附录命令抽查),交付。
@@ -0,0 +1,258 @@
# 机器人手臂3D查看器 · 性能优化方案
目标文件:`G:\Robot_v2\机器人手臂\机器人手臂3D查看器.html`(15.31 MB / 35986 行)
用户要求:先做完整备份(带版本标识)→ 优化 → 验证功能完好 → 记录优化项与性能数据。
---
## 一、现状分析(实测,非猜测)
### 1.1 文件结构
| 位置 | 内容 |
| --- | --- |
| L1~L420 | HTML + CSS + 界面骨架(左栏/IK 控制条/舞台/页脚/BOM 抽屉) |
| L421~L428 | `<script>` three.js r159 内联(652 KB,UMD build,无 CDN) |
| L429~L33251 | `<script>`:`DATA`(13.99 MB 网格 JSON)+ `SHEETS`(52 KB SVG)+ `TRAVEL` + `COLLIDE` + 查看器主逻辑(`const TL = {}` 起 738 行) |
| L33252~L35022 | `<script>`:`arm-ik-solver.js` 逆运动学求解器(1770 行,自带 `window.ArmIK`) |
| L35023~L35984 | `<script>`:`ik-glue.js` 界面胶水层(961 行,Worker 化求解、预置自检、姿态拖拽) |
四个内联脚本的**索引被 `chkSig()` 指纹逻辑依赖**(`document.scripts[2]` = 求解器源码),因此本次优化**不得增删 `<script>` 标签、不得改动求解器脚本块**,否则预置自检指纹失效(会误报"预置结果已失效")。
### 1.2 数据规模(node 实测)
- 零件 92 件(打印件 9 + 电机 6 + 螺钉 72 + 包络 1 + 经纬线 1)
- 顶点 354,697 / 三角面 340,178 / 线段 10,296
- **单件最大顶点 47,483(< 65536 → 索引可全部用 Uint16)**
- `JSON.parse` 69 ms;按查看器原样建几何(含 `computeVertexNormals` / 包围盒球)37 ms
- 全场景射线拾取(92 件、34 万面):**1.3 ms/次**(60 次均值),200 次连测 2.1 ms/次
- `scene.updateMatrixWorld(true)` 全量强制刷新:**0.004 ms/次**(≈110 个对象)
### 1.3 已定位的性能问题
| # | 问题 | 证据 | 影响 |
| --- | --- | --- | --- |
| **P1** | **rAF 循环无条件每帧 `resize() + updCam() + rend.render()`**;`needRender` 被 `requestRender()` 置位后**全文件没有任何读取点**(`Grep needRender` 仅 3 处:声明、置位、注释) | L33207~L33215 | 空闲时仍 60 fps 满帧渲染 34 万面 ×92 draw call;`preserveDrawingBuffer:true` 使每帧多一次缓冲拷贝;持续占用 GPU/功耗 |
| **P2** | `resize()` 每帧读 `stage.clientWidth/clientHeight` | 同上 | 每帧强制布局(forced reflow),与 P1 同源 |
| **P3** | `pick()` 每次 pointermove 先 `meshes.filter(m => m.visible)` 再全量射线求交 | L32668~L32673、L32642~L32652 | 每帧≥1 次 1.3~2.1 ms 求交 + 每帧一个 92 元素临时数组,鼠标扫过模型时持续吃 CPU |
| **P4** | `applyJoint()` 每次新建 2 个 `Vector3` + 3 个 `Matrix4`,并线性查 `JO.find` | L32506~L32518 | 每写一个关节角 = 6 次调用 = 30 个临时对象;动画/实时追踪每帧一批 |
| **P5** | 几何索引一律 `Uint32Array` | L32531、L32546 | 索引显存 340,178×3×4 B = 4.08 MB(Uint16 只需 2.04 MB) |
| **P6** | 点选/ESC 时 `for (const tr of tb.children) tr.style.background = ''` 全表刷 style | L32727、L32736、L32745 | 每次点击写 ~92 行样式 |
| **P7** | Worker 的 Blob URL 不回收(`URL.createObjectURL` 后无 `revokeObjectURL`) | L35510 附近 | 常驻一份 ~86 KB Blob;构造失败路径同样泄漏 |
### 1.4 经实测**否决**的优化(不做,写进报告避免后人重复劳动)
- **全量矩阵刷新**:0.004 ms/次 → 改"6 级关节链更新"只有 13× 相对提速但绝对量 4 μs,风险不值。
- **`updXform()`(92 件)/ `updStat()`(92 件 reduce+filter)**:量级为微秒,爆炸滑块拖动时也远低于 1 ms。
- **材质合并 / 几何合并**:只有 92 个 draw call,合并会破坏"零件树勾选显隐 + 点选 + 单独高亮"这些核心功能。
- **分片/延迟建场景**:初始化总计约 106 ms(69 解析 + 37 建几何),分片只会增加复杂度,不降低总时间。
- **实时追踪时的结果面板 DOM 节流**:`renderResult()` 含"场景图残差复算"与"备选构型"两块**验证语义**内容(用户此前专门建立了指纹/预置自检体系),节流会削弱验证可观测性,保持原样。
### 1.5 关键发现:源文件与成品已漂移(影响改动范围)
`src/build_viewer.py` 由 `src/viewer_tpl.html` + `web/ik-glue.js` + `web/arm-ik-solver.js` + `web/mesh_arm.json` 拼装成品,但:
- 成品 L35023~L35984 的 glue 区域(961 行)**比 `web/ik-glue.js`(776 行)新**:Worker 化求解、`PRECOMPUTED_CHK` 预置自检、`refreshJointLabelsLazy` 节流、`±360°` 滑块等**只存在于成品内**。
- 成品主逻辑区与 `src/viewer_tpl.html` 的差异只有:① `±360°` 滑块范围(成品新)② `/*__SCREWS__*/` 占位符展开(预期)。
**已与用户确认的处理方式**:本次**直接改成品 HTML**;同时把"能对得上"的性能补丁**同步回 `src/viewer_tpl.html`**(源文件落后部分保持原状)。在源文件完成反向同步之前**不要重跑 `build_viewer.py`**(会把成品回退到旧功能)——此警告写入报告。
---
## 二、改动清单
### 改动 0:备份(先做,且必须先验证备份完整)
1. 新建目录 `backups/`,复制原件为
`backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html`
(v1 = 优化前版本号;2151 取自原件最后写入时间 2026-09-29 21:51)
2. 记录 `SHA-256` 与字节数(`Get-FileHash`),校验备份与原件一致后再动原件。
3. `.gitignore` 追加 `backups/`,避免 15 MB 备份污染 git 状态。
### 改动 1(P1+P2,**核心**):按需渲染
文件:成品 `loop()`(L33207~L33215)与 `src/viewer_tpl.html` 对应段。
```js
/* 改动前 */
function loop() {
resize();
if (cv.style.display !== 'none') { updCam(); rend.render(scene, cam); }
requestAnimationFrame(loop);
}
```
```js
/* 改动后 */
/* 按需渲染:needRender 由 requestRender() 置位(所有交互/状态变化都已调用它);
旧版置位后无人读取,故恒以 60 fps 空转。装配图页(画布隐藏)不清标志,
切回三维页由 tab 处理器 resize()+requestRender() 补渲染。 */
function loop() {
if (needRender && cv.style.display !== 'none') {
needRender = false;
resize();
updCam();
rend.render(scene, cam);
}
requestAnimationFrame(loop);
}
```
要点:
- **保留 `preserveDrawingBuffer:true`**:按需渲染后画布不再重绘,必须靠它保住最后一帧图像。
- **保留 rAF 循环**:`animTo()` 动画、实时追踪、拖拽都依赖后续 rAF 里的渲染。
- 已核对全部渲染状态变更点均有 `requestRender()`(主逻辑 36 处;glue 的 `refreshTarget()`/`setJoints()`/`highlightSpec()`/`setView()` 内部均调用),无遗漏。
- `resize()` 仍在渲染前调用一次(进入渲染路径时同步画布尺寸),窗口尺寸变化由 `ResizeObserver → resize() → requestRender()` 覆盖。
### 改动 2(P3):拾取零分配 + 悬浮限流
文件:成品 L32642~L32682 与 `src/viewer_tpl.html` 对应段。
- `pick()`:`meshes.filter(...)` → 复用模块级数组
```js
const pickList = [], ndcPick = new THREE.Vector2();
function pick(e) {
const r = cv.getBoundingClientRect();
ndcPick.set(((e.clientX-r.left)/r.width)*2-1, -((e.clientY-r.top)/r.height)*2+1);
ray.setFromCamera(ndcPick, cam);
pickList.length = 0;
for (const m of meshes) if (m.visible) pickList.push(m); // 语义与 filter 完全一致
const hit = ray.intersectObjects(pickList, false)[0];
return hit ? hit.object : null;
}
```
(three r159 的 Raycaster **不检查 `object.visible`**,已在源码中确认,故可见性过滤必须保留。)
- `schedHover()`:rAF 节流 → **60 ms 时间配额 + 位置去重**(实测 1.3 ms/次求交,60 次/s → ~16 次/s):
```js
let hoverPending = false, lastHoverEv = null, hoverT = -1e9, hoverX = -1, hoverY = -1;
const HOVER_MS = 60;
function schedHover(e) {
if (e.clientX === hoverX && e.clientY === hoverY) return; // 未移动不重算
hoverX = e.clientX; hoverY = e.clientY; lastHoverEv = e;
if (hoverPending) return;
hoverPending = true;
setTimeout(() => {
hoverPending = false;
if (!lastHoverEv || drag) return;
hoverT = performance.now(); hover(lastHoverEv);
}, Math.max(0, HOVER_MS - (performance.now() - hoverT)));
}
```
尾部仍会补一次(指针停住后取最新坐标),tooltip 行为不变;点击/右键拾取仍**立即**执行,不受限流影响。
### 改动 3(P4):`applyJoint()` 去分配
```js
const JP = {}, JD = {};
for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); }
const _mT = new THREE.Matrix4(), _mR = new THREE.Matrix4(), _mTi = new THREE.Matrix4();
function applyJoint(id) {
const P = JP[id], D = JD[id]; if (!P) return;
const a = (ang[id] || 0) * Math.PI / 180;
JG[id].matrix.copy(_mT.makeTranslation(P.x, P.y, P.z)
.multiply(_mR.makeRotationAxis(D, a))
.multiply(_mTi.makeTranslation(-P.x, -P.y, -P.z)));
JG[id].matrixWorldNeedsUpdate = true;
requestRender();
}
```
数学与原式逐位相同(`T(P)·R(D,θ)·T(−P)`),仅把每次新建的 5 个对象换成预计算 + 3 个共享矩阵。
### 改动 4(P5):索引降位(Uint16)
```js
const sIdx = p.v.length > 65535 ? new Uint32Array(p.s) : new Uint16Array(p.s); // line 分支
const idx = p.v.length > 65535 ? new Uint32Array(p.t.length*3) : new Uint16Array(p.t.length*3);
```
全部 92 件顶点数 ≤ 47,483(实测),Uint16 合法;索引内存 4.08 MB → 2.04 MB,上传带宽减半。保留 >65535 分支作安全兜底。
### 改动 5(P6):BOM 高亮行只记一行
新增 `let bomHlRow = null; function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } }`,替换 L32727、L32736、L32745 三处全表 `tr.style.background = ''` 循环;设置高亮处同步记录 `bomHlRow = tr`。可见结果完全一致。
### 改动 6(P7):Worker Blob URL 回收
```js
const u = URL.createObjectURL(new Blob([src + boot], { type: 'application/javascript' }));
let w; try { w = new Worker(u); } finally { URL.revokeObjectURL(u); }
```
(URL 只在 Worker 构造时被读取,构造后即可回收;失败路径也不再泄漏。此改动**仅成品有**——`web/ik-glue.js` 仍是旧同步求解版,无 Worker。)
### 改动 7:源文件同步(用户已确认)
- `src/viewer_tpl.html`:逐条应用改动 1~5(该文件对应代码段存在,锚点为唯一代码串;`±360°` 差异行不与改动区重叠)。
- `web/ik-glue.js`:核对后逐条应用(预计仅"渲染相关"部分不适用——拾取/循环/几何都在模板里;Worker 相关不存在)。
- 同步后**不重跑 `build_viewer.py`**(会回退旧功能),源文件只作为"补丁已留存"。
### 改动 8:文档
新建 `docs/查看器性能优化报告.md`(与 `docs/姿态估计算法评估报告.md` 同目录同风格,中文):优化项逐条说明(问题/证据/改法/收益)、前后实测数据表、功能验证清单与结论、已否决项、备份路径 + SHA-256、**"源文件漂移未修,勿重跑构建"**警告。
---
## 三、假定与决策
1. 只改代码区,**不动** `DATA` / `SHEETS` / `TRAVEL` / `COLLIDE` 数据行与求解器脚本(指纹依赖)。
2. 不新增/不拆分 `<script>` 标签(`document.scripts[2]` 索引依赖)。
3. 不改 `antialias` / `setPixelRatio(min(dpr,1.5))` / 灯光 / 材质参数——属画质取舍,且实测 draw call 与面数不构成瓶颈。
4. 测量用的"插桩副本"只放在 `scratch/perf/`(已被 `.gitignore` 忽略),成品文件保持干净。
5. 备份放项目内 `backups/`(用户已选),并写入 `.gitignore`。
---
## 四、验证方案
### 4.1 静态校验
1. `backups/` 内备份与原件 `SHA-256` 一致(改前)。
2. 抽取改后文件的 4 个 `<script>` 块到 `scratch/perf/`,逐个 `node --check` 通过。
3. 求解器脚本块 + `TRAVEL` / `COLLIDE` 数据行改前/改后**字节一致**(保证指纹)。
4. 全文 diff 只落在预期的 8 处锚点内。
### 4.2 功能验证(浏览器,改前/改后各跑一遍对照)
用 `scratch/perf/serve.py`(`SimpleHTTPRequestHandler`,把 `/v.html` 映射到项目根的真实成品)起 `http://localhost:8123/v.html`,用浏览器自动化逐项核对并截图:
| 项 | 判据 |
| --- | --- |
| 首屏加载 | 无 JS 报错;三维模型正常显示 |
| 关节滑块 | 拖动后手臂实时跟随、角度标签同步 |
| 爆炸滑块 / 装配视图按钮 | 模型散开与复位正确 |
| 线框 / 电机半透 / 螺钉 / 可达包络 / 复位 | 逐项开关生效 |
| 装配图页 ↔ 三维页切换 | SVG 显示、切回后三维画布重新渲染(验证"隐藏分支不清标志") |
| 运动模拟页 | 进页即求解并动画到位;IK 表格/徽标/备选构型正常 |
| 姿态控制 | 开关、取当前、垂直向下、箭头拖拽可用 |
| 自检面板 | 显示"指纹 … 匹配";FK/IK/场景图残差仍为 1e-13 mm 量级(验证改动的数学等价性) |
| BOM 螺丝行点击 | 高亮 + 相机对焦 + HUD 文案(昨天的功能) |
| 悬浮 tooltip / 右键隐藏 / H 键 / ESC | 行为一致 |
| 相机预设 / 拖拽旋转 / 滚轮缩放 | 行为一致 |
| URL 参数(`?view=motion&tgt=…&j1=…&hide=…`) | 脚本化视图仍生效 |
### 4.3 性能前后对比(插桩副本,同一浏览器同一机器)
生成 `scratch/perf/measure_before.html`(备份 + 尾部追加插桩脚本)与 `measure_after.html`(改后 + 同一插桩脚本),插桩内容:包装 `rend.render` 计次与计 CPU 时长、`rend.info`、`performance.memory`、导航时序、交互探针(滑块 input、`IK.setView('motion')`+`IK.run()` 动画、20 次 pointermove 拾取计时),结果输出到页面固定 div + `document.title` 以便截图/读取。
预期指标:
| 指标 | 改前(预期) | 改后(预期) |
| --- | --- | --- |
| 空闲 5 s 渲染次数 | ≈300 | ≈0~1 |
| 单次 render CPU 时长 / draw call / 三角面 | 基线 | 相同(画质不变) |
| 20 次 pointermove 拾取总耗时 | ≈26~42 ms | ≈7~11 ms(限流 + 零分配) |
| 索引显存 | 4.08 MB | 2.04 MB |
| 加载(navigation→首次渲染、DOMContentLoaded→load) | 基线 | 持平或略优 |
| 交互后渲染次数(滑块/动画) | — | 每次变化 ≥1 次,无丢帧 |
### 4.4 回滚
`Copy-Item backups/机器人手臂3D查看器.v1_2026-09-29_2151_优化前.html 机器人手臂3D查看器.html`(并同步回源文件的 `git checkout`)。
---
## 五、执行顺序
1. 备份 + 校验 + `.gitignore`(改动 0)
2. 起静态服务器 + 跑改前功能对照与改前性能插桩(拿到基线数据)
3. 改成品 HTML(改动 1→6,逐处 Edit,锚点唯一串)
4. 静态校验(4.1)
5. 改后功能对照 + 改后性能插桩(4.2/4.3)
6. 同步源文件补丁(改动 7)
7. 写报告(改动 8)
+374
View File
@@ -0,0 +1,374 @@
# 2026-10-03 工作日志
## P06 大臂骨架减重窗取消(视觉整改)
**问题**:用户实机视点反馈,J4 关节正上方 P06 大臂骨架(青绿色)顶部的"帐篷形减重窗"
正面看是一个贯通的矩形黑洞,位于整机最显眼的落脚点,非常不雅观。
该窗是 2026-10-02 减重改造时开的(XZ 轮廓 X3~37 沿 Y 切穿,Z362~374)。
**处理**:在 `src/arm_model.py` 加开关 `P06_HOLLOW_WIN = False`,`p_upper_bone()` 5c 处
改为条件 cut,该段恢复实心。保留 `P06_WIN_XZ` 轮廓与全部设计注释,改开关即可复原。
**结果**:
- P06 体积 161.1 -> **183.3 cm3**(与减重前的 183 一致),+22cm3 / 约 +27g PLA
- 碰撞检测 8 组接触面全部零干涉
- 强度校核 0 处不合格;z=365 剖面 SF=101.23(该段本就不缺强度,减重收益纯锦上添花)
- 已同步更新 BOM 说明、`src/strength_check.py` 剖面名("大臂骨架减重窗 @z=365"
-> "大臂骨架过渡块 @z=365",保留为该高度的安全裕度监控点)
- 重新导出 STEP/STL,重跑 `scratch/make_3mf.py` 打包 3MF,查看器已刷新
**备份**:`backups/20261003_p06_fill/arm_model.py.bak`
**结论/偏好**:减重窗不是越多越好。P04 立柱槽与 P07 顶板槽是朝上敞开的(不破坏外观)
故保留;P06 这个沿 Y 贯通、直接开在外观面上的窗属于"减重减出负面观感",已回退。
以后开减重腔要先问一句:会不会在外露面上形成可见的洞。
## J3(M3) 电机前端压盖 —— 补齐 roll 关节的"电机两端固定"
**定位**:用户截图里红圈标的电机裸露端 = J3 大臂自转电机 M3 的前端盖。
红圈周围两个零件:P05 大臂旋转座(绿 0x059669,抱紧 M3 的**holder**)与
P06 大臂骨架(青 0x0d9488,被 M3 驱动的 driven)。M3 前端盖外端面在 Z=389,
原来只有 P05 后端板 4xM3x10(S02)单面锁死 —— 与 2026-10-02 已做完的
J2/J4/J5(M2/M4/M5)三处相比是唯一还缺前端夹紧的 pitch/roll 关节。
**统一配方(复用,不重复实现)**:`arm_model.motor_front_clamp` 改成轴向无关 —
`axis="Y"`(J2/J4/J5 旧路径,几何逐位不变)/ `axis="Z"`(J3 roll,轴向 -Z)。
新增 `CLAMP_CLOCK_LOCAL`(= 72.2°+90k 的**关节局部相位**):trs() 里 y = z×x,
两个轴向换算到全局都是"全局相位 = −局部相位",所以 Y 轴仍是原来的 17.8°+90k,
Z 轴同一组数落在全局 (x,y) 平面 —— 全机 BCD55 孔位约定不变,改一处必须同步另一处。
**代价(关键)**:J3 的从动件 P06 毂面原本在 Z=388.5,与 M3 前端盖只差 0.5mm,
**放不下 5mm 压盖**。按 Y 轴三处的老办法让位:`z_top` 388.5→383.0、直锁凸台
59.5→65.0(+CLAMP_BACKOFF=5.5 = 压盖 5.0 + 动间隙 1.0 − 原基准 0.5)。
凸台同步加长后 3xS01 的头面台阶(s=58)/杆长/咬合量与 J3 直锁关系一律不变。
P05 侧 `roll_clamp=True` 开关在 `make_pitch_to_roll_bracket`(P05/P08 共用),
P08 传 False —— J6/M6 的前端压盖**还没做**,要从动件 P09 工具法兰让位(改直锁凸台)。
**结果**:13/13 单实体 ✓;P05 155.0 cm³(Z 384..525)、P06 166.5 cm³;
碰撞 8 组全零干涉(P05×P06 = 0.00 mm³);4xM3x8 与压盖零干涉(各自 0.00 mm³);
螺钉 S12 12→16 颗(docs/screws.json + screw_mesh.py 同步,实例数自洽断言通过);
3MF 重打包(P05 切片高 136→141mm)。改前改后对比图 `docs/shots/cmp_j3_before_after.png`。
**教训**:J3/J6 这类 roll 关节的"电机前端盖外侧"就是从动件的毂面,
加压盖前必须先量从动件到前端盖的轴向净空(Y 轴是 1.0mm,Z 轴只有 0.5mm)。
## 电机轴向厚度公差 2mm —— 全机 5 个夹紧关节的承压面让空
**用户提问**:"类似这种安装电机的地方是否考虑了 2mm 的电机厚度公差"。
**审计结论:完全没考虑,而且比"没留量"更糟 —— +2mm 的电机根本装不进去。**
轴向链是「后端板 4×M3x10 顶住电机尾端面 + 前端 R35 压盖 4×M3x8 压住前端盖」两个刚性面。
螺钉头一旦坐在沉孔台阶上就不能再进给(螺钉只能进不能退),所以两端都是硬约束,长度必须
对上。实测窗口:承压面在 s=-0.6 时,电机只能 L ≤ 56.6(长边 +0.6mm);L=58 时前端盖越位
1.4mm 直接撞压盖本体。
**关键设计判断:公差不能靠"拧紧"消化,只能靠垫片位。** 曾评估过三条路:
- 加长压盖/让关节长 2mm → 从动毂面让位要再动 5 个零件的轮毂间距,且关节轴向全链 +2mm;
- 一端改径向夹紧 → 要重做后端接口;
- **后端承压面让空成垫片位(选中)** —— 几何只改一处数,关节长度不变,全机统一。
**实现(单一事实源)**:
- `armcore.MOTOR_TOL = 2.0`(现场口径),`arm_build.REAR_SEAT = 0.6 + MOTOR_TOL = 2.6`;
- `Joint.pocket()` 的 PCB 腔底(电机尾端面承压面)从 -0.6 让到 -2.6 → 覆盖 J3/J4/J5/J6;
- P04 走自带刀具 `c_m2`(没调 `Joint.pocket`),同步改成 `-AB.REAR_SEAT` → 覆盖 J2;
- roll 关节 `rear_mount_holes(t_plate)` 7.0→8.0:让空 2mm 后承压面到螺钉沉孔台阶只剩
`t_plate - cb_depth - REAR_SEAT`,给到 8 才保住 1.4mm 膜厚(给 7 会退化成 0.4mm);
连带 `z_plate = z_r + 7` → `z_r + 8`(P05 杯顶 452→453,J2 让位圆柱底在 454.5,仍有余量)。
- 装配配方写进 README 与 `docs/screws.json` S02:T = 58.6 − L,量程 0~2.6mm,M3 平垫组合。
**验证**:`scratch/motor_tol_window.py`(窗口换算,改前 +0.6 → 改后 +2.6);
`scratch/motor_tol_check.py`(真实电机包络 R30/R30.15 布尔)5 个夹紧关节 × ΔL∈{-2,0,+2}
**全部 0.0mm³ 干涉且前端盖面压间隙 0.0**;13/13 单实体;碰撞 8 组全零;3MF 重打包。
**教训(比结论更重要)**:两个刚性夹持面 = 固定长度,零公差。想吃公差必须有一个面变成
"可垫可让"的软面,否则再大的公差也换不来行程 —— 这条同样适用于以后任何"双面夹紧"改法。
另:J1/M1 天然免疫(P03 是 Ø60.7 直通长孔,比电机长 3mm),所以公差只需要改 J2~J6。
## 承压面让位 2mm 的"残留凸台"—— 排查结论:消不掉,是承压面下沉的必然结果
**用户反馈**:P05/P06/P07/P08 后端都有凸台,是之前给电机预留 2mm 时"削掉了一些"
造成的;认为应该"通过减薄面"而不是"掏掉",没掏干净所以残留了凸台。
**实测确认现象**(`scratch` 里用双 Common 相减法量,band z=-2.6..0 内、64.4 方柱之外):
P05 759 / P06 1089 / P07 3139 / P08 721 mm3。成因确认无误:REAR_SEAT 从 0.6 抬到
2.6 时只把 64.4 方形腔加深(纯掏腔),腔壁以外 z=-2.6..0 的 R35 料没动,
相对新承压面(-2.6)凸起 2.6mm。
**两条消除路线都实测失败**(重要,别再重复试):
- A. 整段全截面切 z=-2.6..0 -> **P05/P08 碎成 2~3 块**。这圈 R35 料连同承压环
是"电机尾端安装板"与"臂身"之间**唯一**的轴向连接,切掉就断。
- B. 沿腔口做让位斜面(方形放样 taper_rect 与圆锥 taper_round 都试了)
-> 余料只降约 20%,且 P05/P08 仍碎 2 块。因为这圈料在 z>0 是杯体本体
(继续往上长),斜面一刀切下去等于削臂身。
**结论**:这不是"没掏干净"。承压面下沉 2mm 与"腔壁保持原 0.6mm 直壁"在几何上
互斥 —— 台阶是下沉的必然产物。它不顶到电机(电机尾端面在腔内、四周已让空),
只是外观上的一道台阶。要彻底消掉必须改承压面方案本身(承压环整体下沉 / R35 座
改短),那属于关节轴向长度调整,不在"预留 2mm 公差"这件事的范围内。
**本次改动**:`Joint.pocket` 与 P04 `c_m2` 的**几何未动**(体积与改前逐件一致,
13/13 单实体),只把上述实测结论写进 `Joint.pocket` 的 docstring 与
`arm_build.py` 常量区注释;`armcore.py` 新增 `REAR_LAND` / `R35_SEAT` 两个具名
常量把"0.6 原台肩"和"R35 外壁"显式化,避免以后再靠注释里的裸数字。
**教训(方法论,比结论更重要)**:
1. 这次我在**错误的坐标系**上浪费了大量时间 —— `map_pt(jp.t, ...)` 必须用
**被抱电机所属关节**的变换(P06 用 J4,不是驱动它的 J3),用错关节算出来的
footprint 全是错的 U 形,还误导我把余料误判成"Loft 方棱"。
2. 布尔测量不要用 `BRepAlgoAPI_Common(shape, slab.cut(sq))` 这种嵌套 cut ——
结果不可靠(我因此得到过 bbox 跨 86mm 的荒谬值)。用**两个独立 Common 相减**:
`vol(∩slab) - vol(∩sq)`。
3. 结论前先确认工具本身对不对:中途 `HALF, SEAT = AB.PCB_W/2.0, AB.REAR_SEAT`
之后又被覆盖,导致打印出 `SEAT=32.2` 这种自相矛盾的值,我却继续解读了好几轮。
## 承压面悬空凸台 —— 已彻底解决(用户第二次指出,我第一次判断错了)
**用户纠正**(重要):上一轮我结论是"凸台消不掉,是承压面下沉的必然产物",**错了**。
用户明确指出:① 不是去掉整个地方,只消掉一部分凸台;② 凸台本身不起任何作用;
③ 正确做法是把上面的厚度改成下面的厚度,或用更大半径的电机做布尔把面弄平;
④ 现状不好 3D 打印(需更多支撑、公差打不好)。
**真正的根因(这次用正确坐标系统一量就清楚了)**:
`Joint.pocket` 原来用 64.4 **方**腔从 -0.6 加深到 -2.6 掏出垫片位。64.4 方腔的
**内切圆半径恰好 = 32.2**,所以方腔在 45° 方向已吃穿到 r=45.5,R35 壁只剩
**四条边中点附近**的料 —— 定量实测 r=34 处材料沿 z 的区间(承压面 -2.6):
- 0°/180°/260°/280°:-8.0..12.0(贯通,属臂身本体 = 承力桥)
- 80°/100°:-8.0..-0.0(**只比承压面多 2.6mm,这段就是凸台**)
- 其余 20~160°:-8.0..-2.6(承压环本体,齐平)
**我上一轮失败的原因**:一直以为"整圈 R35 料是承压板与臂身之间唯一连接",
所以任何整段切都试过。实际上 0/90 方向那条桥在 z>0 是**臂身本体**(r=34 贯通到 z=12),
不是承压环的一部分。是我没量清楚才下了错误结论。
**修法(一刀,不做角度分区)**:承压面所在的 z=-REAR_SEAT..+SEAT_TOP 段,
腔口从 64.4 **方**改成 R35 **圆**(新增常量 `arm_build.SEAT_R=35.0` / `SEAT_TOP=1.0`);
z=0 以上的 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。整圈一次挖到承压面高度,
凸台零残留。P04 走自己的 `c_m2` 圆柱刀具,同步加 `cyl(AB.SEAT_R, -AB.REAR_SEAT, AB.SEAT_TOP)`。
`SEAT_TOP=1.0` 顺带清掉 roll 关节半包薄壁(R32.2..35 的 0.05cm3 残片)。
**连带好处**:承压环从"方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环"
变成"完整 R35 圆环 4.75mm",**受压面积 383 → 974 mm²(2.5 倍)**。
**验证**:13/13 单实体 ✓;静态干涉 8 组全零 ✓;`motor_tol_check.py` 5 关节 ×
±2mm 共 15 组全部 0.0mm³ 且面压间隙 0.0 ✓(±2mm 公差未受影响);
4xS02 沉孔残余 0.0(未被 R35 圆腔侵占)✓。
**方法论教训(比结论重要)**:
1. 上一轮我因为在**错误坐标系**里反复兜圈(`map_pt` 用了驱动关节而非电机所属关节),
得出了"消不掉"的错误结论。**遇到"改不动"的几何,先确认坐标系对不对**,
再考虑方案 —— 坐标系错了会把可解的问题判成无解。
2. **区分"桥"和"悬空料"要靠轴向区间实测**(固定半径 r=34 扫 z),
而不是靠体积或目测。体积法(P05 759 / P07 3139 mm3)会让人误以为整圈都是残留。
3. `solids_of` 会把 <0.01cm3 的布尔退化薄片也算成独立实体,判单实体时要设阈值,
否则会误报"碎块"。
## 修复:R35 圆腔切断连接壁 —— 肩叉顶部"没连上"(我 10-04 的改法有错)
**用户反馈**:最上面的蓝色肩叉(P04)顶部有一道缝,没连接上,要求连上。
**根因(几何实测)**:上一轮为了消"悬空凸台",我把承压面段(z=-2.6..0)的让位腔
从 64.4 方腔改成 **R35 圆腔**(`arm_build.SEAT_R=35.0`, `SEAT_TOP=1.0`)。这是错的 ——
R35 恰好等于后端板外壁半径,等于把 r=32.2..35 那圈 **2.8mm 厚的连接壁整圈挖穿**。
实测(`scratch/_rad.py` 逐方位径向扫描,P04 电机局部系):
| z | 0°/45°/90°/135°/180° | 225° | 270° |
|---|---|---|---|
| -3.0(承压面下) | 28.0-35.0 有料 | 有料 | 有料 |
| -2.4 .. +0.8 | **全空** | 35.0-35.5(0.5mm 薄片) | 35.0-56.0 |
即:后端法兰(R35 圆盘,z=-8..-2.6)与杯壁(z>=+1.0)之间被切出一条 **3.6mm 宽、
360° 全通的环槽**,只剩 240~320° 方位靠臂身本体相连,225° 还留了一片 0.5mm 厚的
孤立薄片 —— 打印出来就是你截图里那道缝。**单实体校验查不出来**(13/13 仍通过,
因为臂身本体兜住了),只有逐方位测轴向材料区间才看得见。
**修法(一处数值)**:`SEAT_R: 35.0 -> PCB_W/2.0 = 32.2`、`SEAT_TOP: 1.0 -> 0.0`。
32.2 = 64.4 方腔的内切圆半径,正好只挖掉悬空凸台、保留 r=32.2..35 整圈连接壁,
且在0/90/180/270 方向与上方方腔壁齐平、45° 方向比方腔多留料把四角补实。
同步改 P04 的 `c_m2`(走自己的圆柱刀具)+ 5 处 BOM 说明 + `screws.json` S02 备注。
**验证**:
- 改前/改后对照(`scratch` 布尔复现两版):改前连接壁 **36/36 方位全断**,
改后 **360° 整圈连续**
- 5/5 件(P04~P08)连接壁整圈连续、单实体、4xS02 沉孔残余 0.00mm³
- 13/13 单实体 ✓;静态干涉 8 组全零 ✓
- `motor_tol_check.py` 5 关节 × ±2mm 共 15 组全部 0.0mm³、面压间隙 0.0 ✓(公差未受影响)
- 体积回归:P04 280.1 -> 282.1 cm³(补回了被误挖的料)
**教训(补上一轮的方法论空白)**:
1. **单实体 + 零干涉不代表结构对**。这次13/13 全过、8 组零干涉,但零件内部被切出一条
360° 通槽 —— 因为臂身本体在另一侧兜住了连接。"是否连通"必须单独验证:
固定半径(如 r=33.6)逐方位扫 z,找材料轴向区间是否连续。
2. **"把面弄平"不等于"把料全挖到外壁"**。用户说"用更大半径的电机做布尔把面弄平",
正确解读是"挖到能让表面齐平的那个半径",即方腔内切圆 32.2,而不是外壁 R35。
选刀半径前必须先确认**哪一圈料是承力连接**,不能只看凸台本身。
3. 改完要**回头验上一轮修过的所有件**,不能只验当前这件。P05~P08 是共享 `pocket()`
自动受益,但 P04 走自己的 `c_m2`,是最容易漏的一处 —— 本次两处都改了。
## 压盖"拼接痕"视觉整改:四处前端压盖外缘倒 R4(新增 blend_clamp_seam)
**用户反馈**:给了两张 P04/M2 压盖的截图 —— 第一张红圈里能看到一圈**硬棱**("圆盘贴在
臂身上"的接缝),第二张是圆润连续的过渡。要求"两面的都变成第二张一样圆润的"。
**定位(几何实测,不是目测)**:
1. 先怀疑是压盖 R35 圆柱面本身的棱 -> 枚举同心 R35 圆边,发现 Y157 以上压盖外表面
**完全暴露**(四周 r>35.2 无料),R35 圆柱面上根本没有任何棱边。
2. 又怀疑压盖圆盘被叉板切出缺口 -> 扫 Y=161.0 截面每个方位的 Rmax,**全周都是
35.00,完整圆盘无缺口**。
3. 最后按"轴向坐标 ≈ 压盖前端面 且 到轴距离 34~36.5"筛棱边,命中**唯一一条
弧长 219.9mm 的样条边**(Y=161.0 平面、r 恒 35.0)—— 这就是拼接缝:
压盖是 `s.fuse(fp)` 直接熔接的圆盘,前缘落在臂身曲面上,交角处留硬棱。
4. 沿 Y 的 Rmax 剖面佐证:Y<=156.5 时 R@90=60(叉板圆柱),Y>=157.5 时 R@90=35
—— 台阶高 25mm,圆角正是要把它化开。
**修法**:新增 `blend_clamp_seam(shape, px, py, pz, s_face, axis, r, tag)`,
按"轴向坐标 ≈ 前端面 且 半径 ≈ R35"定位缝边并倒圆;倒圆失败只打印提示、**退回原形状**
(一个装饰性圆角绝不能毁掉整件)。`CLAMP_SEAM_FILLET = 4.0`(实测 R4 可行、R5 报
BRep_API 失败)。四处统一调用:P04/M2、P05/M3(roll-Z)、P06/M4、P07/M5。
**必须放在各自零件的最后一步布尔之后** —— 缝边是压盖与臂身相交才产生的,中途任何
一刀都可能改变它。
**踩的坑**:`near_face()` 里我把 Y 轴关节的面坐标写成了 `p.z`(应为 `p.y`),
导致函数一条边都找不到、静默"保持原样"(幸好有打印,否则会以为改好了)。
**教训:几何选择器写错时的默认行为是"什么都不做",必须让它打印命中数量**,
否则"没报错"会被误读成"已生效"。调试手段:先用独立脚本按同样条件验证选择器
能命中 1 条,再进源码。
**验证**:4/4 命中并倒圆;13/13 单实体 ✓;静态干涉 8 组全零 ✓;
`motor_tol_check.py` 5 关节 × ±2mm 共 15 组仍 0.0mm³ ✓(沉孔在 BCD27.5,
离 R35 外缘还有 7.5mm,倒圆完全不涉及螺钉孔)。
**方法论**:定位"视觉棱线"要**按几何条件筛边**(轴向坐标 + 半径区间),
不要靠渲染图目测猜是哪条棱。我这次先猜了两次圆柱面、一次缺口,三次都错,
第四次用条件筛选才一次命中。
## 撤回:压盖"拼接痕"R4 圆角(我误判,用户纠正)
**用户反馈**(两次纠正):
1. 我把圆角加到了 J2/J3/J4/J5 **四处**压盖 -> 用户:"我说的是**同个零件的两边**,
只改我发你图片的部分"。已把 P05/P06/P07 的调用撤回。
2. 我又改回只做 P04 一处 -> 用户:"不是圆角,而是你要把**拼接的痕迹去掉**,
现在另外一边是中间有个面,两边会凸出出来的"。
**已全部撤回**:删掉 `blend_clamp_seam()` 函数、`CLAMP_SEAM_FILLET` 常量、
P04 的调用、P04 BOM 里的说明、`screws.json` S12 的圆角备注。
`grep blend_clamp_seam|CLAMP_SEAM_FILLET src/arm_model.py` = 0。
重跑构建:13/13 单实体、8 组零干涉,代码回到加圆角之前的状态。
**为什么撤回(我的诊断是错的)**:
用户描述的目标是"**去掉拼接的痕迹**"(几何问题),我却理解成"**在缝上加圆角**"
(装饰问题)。这两件事完全不同:加圆角不改变"这里是两个零件熔接出来的"这个事实,
只是把锐边磨圆;用户要的是让它们**本来就长成一个连续曲面**。
**我实测到的(可能有用,但结论未定)**:
- 压盖本体在 Y=158 沿径向的最大材料半径**全周恒为 35.00**,是完整 R35 圆筒、
没有任何切削或缺口 -> 第一张图里那条"竖直直棱 + 右侧平面"**不是压盖本体**。
- 那个"中间有个面"实测是 P04 上Z=526..646.5 / X=2.8..46.1 / Y=155.5..164.5 的
**4048mm² 平面**(法向 (0,1,-0.07),带 4° 斜度),位于**电机上方 36mm 处**,
由 `p_fork` 第 5 步 `Xplate(yz_pts, -20.0, 60.0)` 的轮廓线段与 XZ loft 求交切出。
- 两侧"凸出的弧"最可能是 X=45.0 平面(5697mm²,法向 +X)与 R=164 圆柱
@(209,128,545) 的交接,以及立柱减重槽的两片腹板(X=23.0 / X=35.0 两面)。
- 减重槽参数 `P04_CH = x0=23, x1=35, y0=102, y1=154, z_floor=525`。
**教训(比这次的错更重要)**:
1. **"视觉缺陷"先分辨是几何问题还是表面质量问题**。用户的措辞
("去掉痕迹""两边凸出来")指向**形状本身不对**;我却直接跳到"加个圆角_smooth 一下"
—— 这是用装饰手段掩盖几何问题,既没解决需求,也白改了四个零件。
2. **"两边"是拓扑位置,不是"多处"**。用户说"同个零件的两边"指同一个零件上
对称的两个侧面,我却推广成了"全机四处"。**改动范围要严格贴着用户指的地方**,
有歧义就问(我第一次就该问,而不是先改了四处再撤回)。
3. 定位不准时**该早点问**。我换了 6~7 种测量方式(面枚举、边枚举、截面扫、
径向扫、剪影投影)都没能确定那个面,就该停下来问,而不是继续猜下去。
## 视觉整改(成功):P04 电机上方 4048mm2 大平面 -> Hermite 弧面
**用户反馈**(第三次纠正后终于对题):给了标注红线的截图 —— "在电机上方的地方,
是一整个非常大的面,需要做成弧形面",并确认"平顶面需要匹配两边突出的弧度"。
**定位(面枚举法,一次命中)**:枚举 P04 所有平面中「法向 +Y、Z>=520」且面积最大的面:
```
面积 4048mm² 法向 (0.00, 1.00, -0.07) X 2.8..46.1 Y 155.5..164.5 Z 526.0..646.5
```
就是它。成因是 `p_fork` 第 5 步 `yz_pts` 里这一段**直线**:
```
(Y_AXIS+36.5, Z_RL+36.5)=(164.5,646.5)→ (Y_COVER-0.5, Z_SH+36.0)=(155.5,526)
```
沿 X 拉成平板,法向 (0,1,-0.07) 的 4° 斜度就是这么来的。两侧"鼓"的是
`xz_wire` 的 R18 圆弧(`(28,646.5)→(44,630)` 那段 radiusArc(18.0))。
**为什么不能用单段圆弧**:起点 A=(155.5,526) 处切线须沿 Y(接 Y=155.5 竖直段),
终点 B=(164.5,646.5) 处切线须沿 Z(接 Z=646.5 顶段)—— 解圆心得 R=9 与 R=120.5
**互相矛盾**,单个圆弧无法同时满足。故用**三次 Hermite**(与 P05/P06 的 Loft 过渡同款):
m0 = -0.5·d·0.35(沿 -Y),m1 = 0.5·d·0.5(沿 +Z),12 段采样。
中部向 +Y 外凸约 3mm,弧度观感与两侧 R18 协调。
**踩的坑(重要)**:第一次实现把曲线采样成 `y_a → y_b`,而原列表是 `y_b → y_a`
—— **顶点绕序反了**,多边形自交,布尔后零件 **碎成 3 块、体积从 282 掉到 48.7cm³**。
改回原绕序立刻恢复单实体、体积 274.27cm³。
**教训:改动多边形顶点序列时必须保持原有绕序**,否则自交会让布尔结果彻底失真,
而症状(体积暴跌 + 碎块)看起来像"刀切掉了大量料",极易误判方向。
这次是体积对不上基准(差 233cm³)才立刻回头查绕序。
**验证**:13/13 单实体 ✓;静态干涉 8 组全零 ✓;
`motor_tol_check.py` 5 关节 × ±2mm 共 15 组仍 0.0mm³ ✓;
面枚举确认 4048mm2 平面已消失,原位置变成 **R=18 圆柱面 1447mm²**(Z 630..646.5);
体积 282.07 → 274.27 cm³(弧面替代平面削掉 7.8cm³,符合预期)。
**方法论(这次做对了的地方)**:不再靠截图目测猜,而是**按几何条件筛面**
(法向 +Y + Z 下界 + 面积最大),第一轮就锁定了目标面及其成因代码行。
前两轮失败(改圆角、改范围)都是因为没先定位清楚就开始动手。
## 承压面"围挡"消除:SEAT_R 必须是 R35(用户第三次指出,我前两次都错在"取值方向"上)
**用户反馈**(第三次纠正):"之前削平面的时候还是不对,凸台还是在的,你加了一圈围挡,
这不是正确做法,需要把凸台、围挡一起弄平,做成和电机两头齐平一起的平面。"
截图是 P08(#d97706 橙色)M6 电机后端,能看到 R35 端面上有一圈凸起的环。
**根因(一行公式,之前两次都没想透)**:
```
只要 SEAT_R < R35,外壁与刀半径之间那圈环墙就必然存在,
且相对承压面凸起 (R35 − SEAT_R),把电机尾端面围在中间。
```
- 我第一次取 64.4 **方**腔 -> 四角被吃穿,残留 4 段弧 = "凸台"
- 我第二次取 32.2 = 方腔**内切圆**(想"保住连接壁")-> 32.2 与 35 之间那圈
**2.8mm 宽环墙完整保留**,承压面只挖到 32.2,于是环墙凸起 2.6mm = "围挡"
(P08 实测该圈**1536.9 mm³**)
两次都不是"残留",而是**取值的必然产物**。用户第三次要求"一起弄平"就是指这个。
**正确改法(一处数值)**:`SEAT_R: 32.2 -> 35.0`(= 外壁 R35)、`SEAT_TOP: 0.0 -> 1.0`。
承压面段整圈挖到外壁 -> 承压面变成**完整 R35 平圆盘**,凸台与围挡同时消失。
`SEAT_TOP=+1.0` 是为了吃掉半包开口(Y93.0~95.8)那截 0.052cm³ 残片
(取 0 时它脱空成独立碎块,P05/P08 各多一块)。
**为什么不重演 2026-10-04 的"肩叉顶部没连上"**:
那版切 R35 但 `SEAT_TOP=0`,在 z=0..1.0 残留一截薄片 + 承力桥判定失误。
这次实测:band 段(z=-2.4..+0.8)r33.5 处**围挡残留 0/36**,
r36..58(臂身)有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,
**无整圈通槽**。关键差别是 SEAT_TOP 抬到 +1.0,不是改回 32.2。
**连带好处**:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」
变为「完整 R35 圆环 4.75mm」,**受压面积 383 -> 974 mm²(2.5 倍)**。
**验证**:
- 13/13 单实体 ✓(P04 272.3 / P05 151.4 / P06 158.8 / P07 222.6 / P08 138.1 cm³)
- 静态干涉 8/8 零 ✓
- `motor_tol_check.py` 5 关节 × ΔL∈{−2,0,+2} 共 15 组全部 0.0mm³、面压间隙 0.0 ✓
- 4×S02 沉孔残余 **0.00mm³**(沉孔 z=-8..-4 与圆腔 z=-2.6..+1.0 不重叠)✓
**踩的坑(重要)**:
1. **自己写的光栅化投影渲染器(从 mesh_arm.json 三角网格直接光栅)完全不能用** ——
深度缓冲/背面剔除/缩放全错,出一堆三角形碎面。排查这个问题花掉十几轮。
**教训:项目已有 `src/shot.py`(headless Edge)就是唯一可靠的渲染链路,别自己造。**
2. `src/shot.py` 并发渲染会随机 FAIL —— 根因是**没加 `--user-data-dir`**,
多个 Edge 实例抢同一个默认 profile 的锁。已修:每次 `tempfile.mkdtemp()`
建独立 profile,收尾 `shutil.rmtree`。加上之后 4 视图并发 100% 成功。
3. 写 BOM 说明时在 `"` 引号字符串里放了未转义的中文引号 `"一圈围挡"` ——
直接 SyntaxError 把 109KB 的 arm_model.py 写坏。已从 `.bak/` 备份恢复。
**教训:批量替换长中文注释时,别把双引号文本塞进 Python 双引号字符串。**
已建立 `.bak/arm_model.py.bak20261005` / `arm_build.py.bak20261005`。
**方法论教训(第三次了)**:
"改不动 / 消不掉"的几何问题,先想清楚**是不是某个取值的必然结果**,
而不是急着找"残留料"去切。SEAT_R 这个旋钮从 64.4 方腔 -> 32.2 -> 35.0,
每次换取值都改变了"环墙是否存在",而我一直只在 32.2 这一档里找原因。
**参数化几何里,"某段料消失"几乎总是某个半径/长度取值的函数,不是布尔残留。**
+78
View File
@@ -0,0 +1,78 @@
# 2026-10-04 工作日志
## J3(M3)电机前端"围挡":只切敞口那一圈(最终方案)
**用户反馈**(配图,第四次提"围挡"):
"我又有重新更改,改动文件记得重新读取,然后这里电机的另一头也有围挡,需要删除"
→ 上一轮修的是**尾端**承压面(SEAT_R=35),这次是电机**另一头**(前端)。
中途两次纠正:
1. "切太猛了,下面整个切了我怎么固定电机,只切我圈起来的一点"
2. "只切一圈围挡,不切整个面"
### 定位(先证后动,这次做对的地方)
1. **颜色比值锁零件**:截图里 B/G≈0.73 = P05 大臂旋转座(0x059669 翠绿),
环内青色盘 B/G≈1.00 = P06 大臂骨架(0x0d9488)。在渲染图上实测这两个比值确实
分别落在 0.7 / 1.0 两簇,映射成立。
2. 颜色 + 结构(P05 围着 P06 的 J3 凸台、Ø37 孔 ≈ 0.53×外径)⇒ 唯一可能是 **J3 关节**
(P05 与 P06 全机唯一交界面,Z384~390)。
3. 沿 J3 轴逐 z / 逐方位扫"首个材料半径"(`BRepClass3d_SolidClassifier`),实测:
- P05:`Z389.0~395.5` 是 **r30.25~35.0 的前端定位环**(6mm 高环墙)
- P05:`Z384.0~389.5` 是 **R35/Ø37 前端压盖**(4×M3x8 BCD55)
- P06:`Z=383.0` 毂面 + R17.8 凸台顶到 Z=390.05(穿过压盖 Ø37 孔)
⇒ 环墙 + Ø70 压盖把 M3 前端盖(Ø60.3)连同凸台围在中间 = 用户说的"围挡"。
### 我走的两条弯路(重点教训)
**弯路 1:整个前端都切掉**(用户当场驳回:"下面整个切了我怎么固定电机")
把 `z_bot` 389→395 + 删定位环 + 删压盖 + P06 毂面 383→388.5。后果:M3 退回
"仅后端板 4×M3x10 单面锁死",电机前端失去支撑面。
**教训:先问清楚要删的是哪个特征、哪一圈,不要把"围挡"理解成"整个前端端部"。**
**弯路 2:整圈削到与电机齐平(R35→R31.8)**(自测就没过)
- 这圈壁与第 3 步 Loft 在 `X=±35` 处与 Ø70 柱面**相切**(退化的切向楔形),
整圈切完两片楔形脱空 → 实测 **1.32mm³ 孤立碎块,P05 变成 2 个实体** ✗
(把切环外半径放到 44 消掉碎块,则会在背脊下端开一个 6.5mm 深的缺口,也不能接受)
- 压盖根部只有 0.5mm 熔接带(CLAMP_ROOT),环削到 1.55mm 宽后连接面
从 2.37mm² 掉到 0.78mm² → 4×M3x8 的夹紧力会把压盖从本体上拉下来 ✗
### 最终方案:只切敞口(-Y)那一圈
新增开关 `arm_model.M3_FENCE_OPEN_CUT = True`,在第 12 步熔接压盖之后加一刀
**盒切**,Y 界限与第 6 步 `cut_open` 完全一致(`Y_AXIS-40 .. Y_AXIS+3`),Z `388.9~395.6`:
- 敞口(-Y)半圈从 Z=395.5 一路开到压盖背面 Z=389 → 用户圈的那一圈围挡消失
- 背脊(+Y)侧整圈定位环保留:继续定位 M3 前端盖(内孔 r30.25 vs 前端盖 Ø60.3,
单边 0.10mm),并作为 R35 压盖**唯一且足够强**的支撑/熔接面(4.75mm 宽 × 0.5mm 熔接带)
- 压盖本体 / Ø37 让位孔 / 4×M3x8 沉孔 / 腔口下沿 z_bot=389 全部不动
- 附带好处:盒切把 Loft 与柱面**相切的那两片楔形**一起带走(楔形在 Y_AXIS 附近
< Y_AXIS+3)→ 不会出现孤立碎块,单实体天然保持
- 同步回退了弯路 1 造成的所有连带改动:S12 恢复 16 颗(`screw_mesh.py` /
`docs/screws.json`)、P06 毂面回到 383.0、凸台回到 65.0、README 全部改写
### 验证(全部通过)
- P05 **solids=1** ✓ 体积 154.8 → **151.6 cm³(只少 3.2)** Z 384.0..525.0(压盖与腔口都在)
- 逐方位剖面(`scratch/check_ring_flush.py`):Z384~388.5 八个方位全是 r18.5~35.0
(压盖整圈完好);Z≥389 只在 225°/270°/315° 有料(背脊侧环墙保留),
0°~180°(敞口侧)已清空 ✓✓
- 全量重建:**13/13 单实体** ✓、**8/8 静态干涉零** ✓(含新增的 P05×P06 一对)
- `motor_tol_check.py` 5 关节 × ±2mm 共 15 组 0.0mm³、面压间隙 0.0 ✓
- `verify_3mf.py` 12/12 通过(EXPECT 表里 P05 件高原本写 136.0,是 2026-10-03 加压盖
之前的老值,改回 141.0)
- STEP/STL/3MF 重导 ✓、螺钉实例 93 颗(S12:16)✓、查看器 + 可达包络重打包 ✓
- `sweep_v2.py` 全行程重扫:travel.json 与改前**逐字节一致**(J2 −105…+9、J4 −111…+16、
J5 −93…+34、J1/J3/J6 整周)✓
- 渲染确认:`docs/shots/m3_front_open_after.png`(隐藏电机、从敞口侧看进去,
围挡已无、压盖面与 4 颗 S12 螺钉都在)、`m3_front_assembled_after.png`
### 顺带修掉的两个工具 bug(非本次需求,但一直让构建不干净)
- `scratch/gen_env_mesh.py` 的包络自证断言是**假失败**:网格 x 从 `xs.min()` 起按
`np.arange(step_x=8)` 对齐,最后一格最多比真极值短一个步长(实测差 6.46mm),
而断言写的是 `abs(max-x_fwd) < 1.0` → 包络注不进 `web/mesh_arm.json`,
查看器里"可达包络"整个消失。已改为"不得超出、且至多少一个步长"。
- `src/shot.py` 的 `take_shot` 先 `os.remove` 旧图,在带 safe-delete 垫片的环境里
会把"删到回收站"的失败抛出来、打断整轮渲染;Edge 的 `--screenshot` 本来就覆盖,
已删掉这次删除。
### 方法论(第四次"围挡"事件的总结)
"围挡"这类"看着多余的一圈料"几乎总是**某个半径/高度取值的必然几何**,不是残留:
- 尾端:承压面段腔口半径 `SEAT_R` < 外壁 R35 → 环墙必然存在(1536.9mm³)
- 前端:定位环 6mm 高 + R35 压盖比电机 Ø63.6 凸 3.2mm → 环墙+凸缘必然围住前端盖
两次的正确解法都不是"多切一点",而是**改那个取值**;而且改动前要先问清楚
删的是哪一圈 —— 删整圈会同时删掉压盖唯一的支撑面。
+73
View File
@@ -0,0 +1,73 @@
## 【2026-10-05e】前端围挡整改推广到全机 5 处(J2/J3/J4/J5/J6)
**用户要求**:"其他几个也有一样的情况,也修一下"
→ J3/M3 那一处的前端围挡,其余四处电机舱(J2/M2、J4/M4、J5/M5、J6/M6)完全同构,一并处理。
### 根因(几何实测,不是残留料)
`Joint.front_collar_pocket(r_bore=30.25)` 把座孔从 R30.15 扩到 R30.25,于是留下一圈
**r30.25..35、6mm 高的环墙**,比电机前端盖 Ø60.3 凸出 4.75mm,把前端盖整圈围住;
J2/J3/J4/J5 上还骑着 R35 前端压盖(Ø70 再凸 3.2mm)。
实测五处环段都在**关节局部 z 49.5..56**:Y 轴电机 Y150..156、roll 电机 Z395.5..389。
### 做法:把 P05 专用盒切泛化成 `cut_motor_fence()`
- 开关 `M3_FENCE_OPEN_CUT` → **`FENCE_OPEN_CUT`**(语义从 P05 专用变全机共用)。
- 新函数 `cut_motor_fence(shape, j, tag, name3, z0=49.4, z1=56.1)`:
在**关节局部坐标系**里切 `x ±40 / y -3..+40 / z 49.4..56.1`,再 `j.L()` 摆到全局。
- 敞口方向恒为**局部 +y**,**无需逐件配置**:实测 5 处电机舱的本体
(环外 r=37/40 有料处 = 背脊侧)方位角全部落在局部角 180~360
(J2 240..315 / J3 225..315 / J4 210..330 / J5 210..330 / J6 195..345)。
- 一对 z 参数通吃全机(因为五处环段都在局部 z 49.5..56)。
- 切口边界取 **y=-3.0 而非 0**:环壁在该处仍与背脊侧实体连通,切出来是真正的缺口
而不是零厚度缝,单实体拓扑天然保持。
- 四个调用点(都在压盖熔接之后、收尾刀具之前):
P04 7b2(j2)、P06 5b2(j4)、P07 7c2(j5)、make_pitch_to_roll_bracket 第13步(j_roll,
条件放宽为 `roll_clamp or FENCE_OPEN_CUT`,这样**尚无前端压盖的 P08/M6 也只切定位环**)。
### 关键教训(值得记住)
1. **P05 的全局盒切其实是局部盒切**:当时那记
`(X_ARM±40, Y_AXIS-40..+3, Z388.9..395.6)` 换算到 J3 局部系正好是
`x±40 / y -3..+40 / z 49.4..56.1`。一开始我按全局坐标去复用,语义就错了 ——
泛化时必须先确认原刀在局部系里是什么。
2. **P05 回归验证**:改用通用函数后 P05 体积 151.60 cm³ 与 2026-10-05d 版**逐位一致**,
剖面 0..165° 敞口侧全空、180..345° 背脊侧有料,证明泛化没有改变既有行为。
3. **写文件务必"先在内存改好、断言全过、最后一次性写出"**:本轮用
`io.open(P,"w").write(s)` 时中途抛异常(`newline="\\n"` 非法值),
**把 119KB 的 arm_model.py 截成了 0 字节**,靠 `.bak/arm_model.py.bak20261005e` 救回。
后续改成 `写 .tmp 再 os.replace()` + 所有 `assert` 前置,原子性才可靠。
4. **heredoc 里 `sed -i` 转义`**:`sed -i 's/newline="\\\\n"/newline="\\n"/'` 这类在
Bash heredoc 里层数容易错,直接改脚本文件更稳。
### 验证结果(全过)
| 项目 | 结果 |
|:--|:--|
| 单实体 | **13/13** ✓ |
| 静态碰撞 | **8/8 零干涉** ✓ |
| 电机 ±2mm 公差 | **15/15** ✓(现覆盖 J2~J6 全部 5 个关节) |
| 3MF | **12/12** ✓ |
| 全行程重扫 | 696 s,`docs/travel.json` 与改前**完全一致**(J2 −105…+9、J4 −111…+16、J5 −93…+34、J1/J3/J6 整周) |
| 逐方位剖面 | 5/5 敞口侧 0/12 有料、背脊侧 11/12 有料 |
| 误伤检查 | 环外本体(r=37/40)改前改后**逐方位完全一致**;压盖沉孔段(z=58)一致 |
减重:P04 −3.4 / P05 −3.2 / P06 −3.2 / P07 −2.1 / P08 −3.0 cm³
(P07 只减 2.1 是因为它 `front_collar_pocket` 用了 `r_bore=30.25+CLEARANCE_GAP=32.25`,
环壁只有 r32.25..35 更薄)。
### 文档同步
- `README.md`:零件表 P04/P06/P07/P08 行加"前端围挡只切敞口那半圈"标记;把
2026-10-05d 的 P05 专用段落升级合并成 2026-10-05e 全机版(去掉了重复叙述)。
- `docs/bom.json`:P04/P06/P07/P08 四件说明各追加一段(bom.json 是 **CRLF**,
写出时必须保持原行尾风格,否则整文件 diff 会被刷满)。
- `docs/screws.json`:S12 备注把 `M3_FENCE_OPEN_CUT` 旧名改掉 + 追加推广说明,
强调 16 颗螺钉的数量/型号/孔位/相位/沉孔深度**一概不变**。
### 产出
- `docs/shots/fence_all_sections.svg` / `.png` —— 五处「改前 vs 改后」横剖面对比图
(脚本 `scratch/gen_fence_sections.py`,用 BRepClass3d_SolidClassifier 沿 720 个方位
逐 0.2mm 步进探测材料外缘半径,直接画材料剖面多边形;比调相机截图可靠得多)。
- `docs/shots/fence_j2_after.png` / `j4` / `j5` / `j6` —— 四处查看器渲染图。
### 查看器渲染的坑(省得再踩)
`look.py` 的相机参数是**世界坐标偏移**:`tx/ty` 要填 **95 / 部件高度**(例
`az=1.5708&po=1.95&rd=150&tx=95&ty=520&tz=-128`),tz 是**负值**。
我先前按局部坐标猜(`tx=19.5`、`tz=+440`),四张图 md5 完全相同 —— 那是渲染没生效,
不是"参数无效但画面相同"。判断参数是否生效要看 **md5 是否互不相同**。
+6
View File
@@ -0,0 +1,6 @@
{
"network": {
"httpProxy": "http://127.0.0.1:7897",
"noProxy": "localhost,127.0.0.1,192.168.31.10,z.ai,bigmodel.cn,modelscope.cn,sharellm.net,motomoto.lol"
}
}
+118 -14
View File
@@ -7,9 +7,10 @@
## 🌟 核心设计特性
1. **一体化直锁电机**:全面取消外挂独立端面齿联轴器,所有关节由 3×M2 沉头螺钉直锁电机输出轴,轴向尺寸大幅压缩,刚度显著提升。
2. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
3. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
4. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
2. **电机前端双向夹紧 + 2mm 厚度公差**:每个关节的电机由「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」前后两面夹死;后端板承压面按 `MOTOR_TOL=2.0` 让空成**尾端垫片位**(`armcore.MOTOR_TOL` / `arm_build.REAR_SEAT`),装配时在电机尾端面与承压面之间叠 0~2.6mm 的 M3 平垫,**逐台电机按实测长度配垫**(T = 58.6 − 电机实际长度 L),让前端盖正好落到压盖面上。
3. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
4. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
5. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
---
@@ -41,7 +42,18 @@ python src/sweep_v2.py
├── 机器人手臂3D查看器.html # 单文件内置 3D 查看器(双击即用)
├── README.md # 本说明文档
├── src/ # 核心源码目录
│ ├── arm_model.py # ⭐ 唯一权威主力建模脚本(含五级圆角与最新几何)
│ ├── arm_model.py # ⭐ 装配流水线:载电机模组→按件收集 specs()→导出 STEP/STL/BOM/网格→打包查看器
│ ├── part_tools.py # 跨件共用刀法:X 向基础体、减重腔 helper、电机前端压盖、pitch->roll 转接座骨架
│ ├── parts/ # ⭐ 每个零部件一个文件:几何 + 件号/配色/说明 + 装配帧归属 JOINT
│ │ ├── p01_column.py # P01_A/B/C 立柱三段(含立柱定位 X_COL)
│ │ ├── p02_base.py # P02 底座三段(中核心/前向力臂/后向力臂)
│ │ ├── p03_girdle.py # P03 肩吊臂
│ │ ├── p04_fork.py # P04 肩叉(减重槽开度 P04_CH 在此)
│ │ ├── p05_roll_base.py # P05 大臂旋转座
│ │ ├── p06_humerus.py # P06 大臂骨架(窗开度 P06_WIN_XZ / P06_HOLLOW_WIN 在此)
│ │ ├── p07_forearm.py # P07 小臂骨架(减重槽开度 P07_CH 在此)
│ │ ├── p08_wrist.py # P08 腕偏转座
│ │ └── p09_flange.py # P09 工具法兰
│ ├── sweep_v2.py # ⭐ 权威动态碰撞检测与行程扫描工具
│ ├── arm_build.py # 底层几何基础库与通用装配/导出函数
│ ├── build_viewer.py # 3D 查看器打包生成脚本
@@ -51,6 +63,12 @@ python src/sweep_v2.py
│ ├── finalize_travel.py # 行程数据维护(从 sweep 明细重建 travel.json)
│ ├── strength_check.py # 刚度与应力分析
│ └── torque_balance.py # 关节力矩平衡校核
> **改哪件就开哪个文件**;`arm_model.py` 里不再有件号字符串和几何代码。
> 件级开关(`FENCE_OPEN_CUT`、`P04_CH`、`P07_CH`、`P06_WIN_XZ`)随各自的模块搬走:
> 做 A/B 必须写它所在的件文件(如 `parts.p04_fork.P04_CH = cfg`),
> 写 `arm_model.P04_CH = cfg` **不生效**(`arm_model` 对件级符号只作只读转发,供旧脚本取值)。
> 判据:翻转开关后体积必须变化(`scratch/verify_switches.py` 实测 2.70 / 22.23 cm³ 灵敏度)。
├── docs/ # 权威数据与渲染截图(历史报告已清理)
│ ├── travel.json # 各关节实测行程与硬限位(sweep_v2.py 扫描生成)
│ ├── travel_detail.json # 逐对扫描明细与组合折叠约束
@@ -69,15 +87,89 @@ python src/sweep_v2.py
| 构件编号 | 构件名称 | 关联驱动关节 | 设计说明 |
| :--- | :--- | :---: | :--- |
| **P01** | 胸板 / 安装板 | 根基固定 | 12mm 基础基座,预留 4x M4 紧固位与躯干走线窗 |
| **P02** | 底脚 / 支撑座 | 根基固定 | 200×320mm 重型防倾覆底盘,提供稳固调试基座 |
| **P03** | 肩吊臂 / 肩机舱 | J1 驱动抱紧 | 仿生规整风道机舱,带 7 组轴向散热格栅,抱紧 M1 电机 |
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2,后侧刚性抱紧 M3 |
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机 |
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5 |
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5,立式抱紧 M6 腕旋转电机 |
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 40mm) |
| **P01** | 立柱(三段 A/B/C) | 根基固定 | 66×66mm 三角截面立柱,分段榫接 + M4 自攻对锁孔防反装;顶端为 P03 安装台 |
| **P02** | 底盘(核心座 + 前/后防翻力臂) | 根基固定 | 195×190mm 核心承力座 + 48mm 套筒 + 前后三角加强筋;前后防翻力臂带 M5 预埋螺母 |
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2;**M2 前端压盖**(R35 / Y155.5~161):电机前端盖再加 4×M3x8(BCD55)夹一次,与后端板 4×M3x10 前后双向面压,消掉大扭矩下电机在座孔里的倾斜;**前端围挡只切敞口那半圈**(见下);**立柱减重槽**(X23~35 / Y102~154 / Z525↑ 敞开):立柱由实心变 U 形槽钢,361→289cm³,抗弯靠两侧 X 向腹板(z=560 截面 I 207370→199777,降 3.6%,而面积降 28%) |
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2(J2 法兰盘背面后移到 Y=162,给 P04 前端压盖让位,J2 凸台同步加长以保住 S01 咬合),后侧刚性抱紧 M3;**M3 前端压盖**(R35 / Z 384.0~389.5,4×M3x8 BCD55):M3 是 roll 电机(轴向 -Z),压盖与后端板 4×M3x10 前后双向面压;代价是对侧 P06 的 J3 毂面后移 5.5mm 让位 |
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机;**M4 前端压盖** 4×M3x8(BCD55),同 P04 配方;**前端围挡只切敞口那半圈**(见下);**J3 毂面后移 5.5mm**(顶端 388.5→383.0,直锁凸台 59.5→65.0):给 P05 的 M3 前端压盖让位,留 1.0mm 动间隙——与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源,换算式同为「压盖 5.0 + 动间隙 1.0 − 原基准 0.5」;凸台同步加长,3×S01 输出螺钉咬合量与 J3 直锁关系一律不变 |
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5;**M5 前端压盖** 4×M3x8(BCD55);**前端围挡只切敞口那半圈**(本件环壁 r32.25~35,因 2.0mm 关节间隙而更薄,见下);J4 从动侧(法兰盘/立柱/圆弧加强筋)整体后移到 Y=162 给 P06 压盖让位,J4 凸台同步加长;**顶板减重槽**(X−3~43 / Y103~153 / Z255↑ 敞开):288→230cm³,Y 上限取 153 以避开 y_hub_in=158.5 起的竖直立柱本体(那是 J4 从动法兰的承力柱) |
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5(J5 法兰盘背面后移到 Y=162,给 P07 前端压盖让位,J5 凸台同步加长),立式抱紧 M6 腕旋转电机;**M6 前端定位环只切敞口那半圈**(尚无 M6 前端压盖,环是光板,见下)。壁厚峰值 8.3mm,本就是薄壳 |
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 44mm);**填充 100%→30%**:该截面安全系数 288,是全机裕度最大的件,满填充纯属浪费 |
| **M1~M6** | 关节电机模组 ×6 | 各关节驱动 | Ø60 方形标准减速电机模组(63.6mm 方形尾盖),210 g/台,整机 1260 g |
> ⚠️ **电机装配必须配垫片(厚度公差 2mm)**:各台 Ø60 电机的轴向长度实测差可达 2mm。
> 这条轴向链是「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」两个刚性夹持面,螺钉头一旦坐死
> 在沉孔台阶上就不能再进给,所以公差**不能靠拧紧消化**,只能由后端板的承压面让空出来的
> 垫片位吸收(`armcore.MOTOR_TOL = 2.0`,`arm_build.REAR_SEAT = 0.6 + 2.0 = 2.6`)。
> 装配顺序:① 后端板承压面上叠 **T = 58.6 − 电机实测长度 L** 的 M3 平垫(0.5/1.0/1.5 组合,
> 标称电机约 0.6mm,量程 0~2.6mm);② 电机推入到尾端面贴住垫片;③ 前端压盖 4×M3x8 拧紧,
> 此时前端盖正好落在压盖面上(0.0 面压);④ 后端板 4×M3x10 拧紧夹死垫片。
> **不做第①步的后果**:电机长 1mm 以上就装不进去(前端盖越位 1mm 撞压盖本体),
> 电机短 1mm 以上压盖就压不上、只剩后端板单面锁死,大扭矩下电机照样点头。
> 校核脚本:`scratch/motor_tol_window.py`(窗口换算)+ `scratch/motor_tol_check.py`
> (真实电机包络布尔验证 5 个夹紧关节 × ±2mm,全部零干涉)。
> 另:J1/M1(P03 侧开式电机舱)本身就是 Ø60.7 直通长孔、比电机长 3mm,天然覆盖该公差,无需配垫。
>
> **承压面为什么是 R35 圆面而不是 64.4 方腔**(2026-10-04 整改):承压面下沉 2.6mm 后,
> 如果承压面段仍用 64.4 **方**腔,方腔壁以外 `z=-2.6..0` 那圈 R35 料就相对新承压面
> 凸起 2.6mm —— P04~P08 后端整圈「悬空凸台」,3D 打印时是悬空薄壁(要支撑、公差难保)。
> 现在承压面段用 **R35 圆腔**(`arm_build.SEAT_R`),整圈一次挖到承压面高度,凸台零残留。
> `SEAT_R` 必须**等于**外壁 R35:只要小于 R35,刀半径与外壁之间那圈环墙就必然保留并相对
> 承压面凸起(P08 实测 1536.9mm³)—— 这就是用户 2026-10-05 在**尾端**指出的"围挡",
> 是取值的必然产物而不是残留。连带好处:承压环从「1.95mm 窄环」变成「完整 R35 圆环 4.75mm」,
> **受压面积 383 → 974 mm²(2.5 倍)**,夹紧更实。`SEAT_TOP=+1.0` 越过电机尾端面 1mm,
> 吃掉半包开口处的 0.052cm³ 残片(取 0 时该残片会脱空成独立碎块)。
>
> **前端那一圈"围挡"(2026-10-05e,全机 5 处)**:用户 2026-10-05 先在 J3/M3 指出**电机
> 另一头(前端)也有一圈围挡**,随后确认其余四处(M2/M4/M5/M6)完全同构。围挡本体不是残留料,
> 而是 `front_collar_pocket` 的必然产物:它把座孔从 R30.15 扩到 R30.25,于是留下那圈
> `r30.25..35`、6mm 高的环墙,比电机前端盖 Ø60.3 凸出 4.75mm;J2/J3/J4/J5 上还骑着 R35 前端
> 压盖(Ø70 再凸 3.2mm)。我第一版把整个前端(定位环 + 压盖 + 腔口下沿)都切了 → **过度**:
> 电机前端没了支撑面、M3 退回单面锁死,用户当场驳回。
>
> **【2026-10-07 修复:消除 P04/P06/P07/P08 误切缺角,完整补齐底壁】**:
> 2026-10-05e 曾将 P05(M3) 的定位环敞口盒切推广到全机。但俯仰关节(J2/J4/J5)的局部 y=-3..40 坐标导致盒切向上侵入底壁实体 3mm(Z_axis+3),造成 P04、P06、P07、P08 的底壁和侧耳平白无故缺了一个 3mm x 6.1mm 的台阶缺角。现已彻底取消 P04/P06/P07/P08 处的切除,完整补齐底壁;该盒切仅保留于原本专用的 P05(M3) 敞口侧定位环(开关 `arm_model.M3_FENCE_OPEN_CUT`)。
> **原 P05(M3) 前端定位环敞口切**(开关 `arm_model.M3_FENCE_OPEN_CUT`,切刀 `cut_motor_fence()`):
> 按各关节自己的局部坐标切 —— 局部 y 从 -3 到 +40、z 49.4~56.1。5 处的定位环都恰好落在
> 局部 z 49.5~56(Y 轴电机 Y150~156、roll 电机 Z395.5~389),故一对参数通吃;敞口方向恒为
> 局部 +y(实测 5 处背脊方位角全部落在局部角 180~360,无需逐件配置)。切口边界取 y=-3.0 而非 0:
> 环壁在该处仍与背脊侧实体连通,切出来是真正的缺口而不是零厚度缝,单实体拓扑天然保持(5/5 实测
> solids=1)。**背脊侧整圈定位环保留** —— 继续定位前端盖,并作为压盖唯一且足够强的支撑面
> (4.75mm 宽 × 0.5mm 熔接带,本次完全不碰);压盖本体 / Ø37 让位孔 / 4×M3x8 沉孔 / 腔口下沿
> 全部不动,电机仍是「后端板 4×M3x10 + 前端压盖 4×M3x8」前后双向面压。
> 各件减重:P04 −3.4 / P05 −3.2 / P06 −3.2 / P07 −2.1 / P08 −3.0 cm³(P07 环壁因 2.0mm 关节
> 间隙而较薄 r32.25~35,故减得最少)。
>
> **为什么不整圈削到与电机齐平(R35→R31.8)**:① 那圈壁与本体在两个方位与外壁**相切**,整圈
> 切完那两片切向楔形(P05 实测 1.32mm³)会脱空成孤立碎块,破坏单实体拓扑;② 压盖根部只有
> 0.5mm 熔接带,环削窄后连接面从 2.37mm² 掉到 0.78mm²,4×M3x8 的夹紧力会把压盖从本体上拉下来。
> ⚠️ **P01 / P02 是固定测试台架,不是机器人本体**。它在扫描中代表机械臂与躯干的连接
> 界面——**臂撞立柱 = 臂撞躯干,是真约束,碰撞计算必须包含它**。但台架的位置与截面
> 是一个假设,所以躯干侧的工作空间只和这个假设一样准。要改的是"真机躯干占位"这个
> 输入,而不是改台架(改台架是改测试工装)。`sweep_v2.py --no-stand` 是诊断开关:
> 排除台架重扫,给出不含躯干约束的自由空间行程,用来分辨某个限位是臂自身折叠产生的
> 还是躯干挡的。**默认包含台架。**
> ⚠️ **关节电机力矩现状**:按 `torque_balance.py` 的实测(零件真实质心 + 可达姿态搜索),
> **J1 肩抬举仅悬停自身臂身即需 3.84 N·m、J2 需 2.48 N·m**,已超出假定许用连续力矩
> 2.0 N·m;带 0.5 kg 末端负载时 J1 达 6.14 N·m(307%)。详见 `docs/torque_report.md`。
> 结论对电机额定参数敏感,该参数目前为工程假定(无数据手册),可用 `ARMT_CONT` 覆盖重算。
>
> 相反地,**结构本身不是瓶颈**:12 个剖面全部合格,最差的 P03 吊臂根座安全系数 9.25,
> 最大挠度 1.6mm。减重救不了电机——J1 的 distal 载荷里 5 台电机占 65%(1.05kg),
> 打印件只占 0.58kg,把 P07/P08/P09 全部减掉 J1 也只降到 2.54 N·m。
>
> **2026-10-02 手臂件减重**:按厚度场扫描(`scratch/thickness.py`)定位厚料后,
> P04 立柱 / P06 过渡块 / P07 顶板改为薄壁槽与减重窗,P09 填充 100%→30%,
> 手臂 6 件实心 1214→1061cm³、BOM 1506→1316g。按 gyroid 实际打印口径(墙 5×0.42mm
> 以内 100%、深处 25~30%)估,手臂打印件 906→817g,**净省约 90g**。
> 减重开度由 `scratch/tune_hollow.py` 按「减重区最差安全系数 ≥ 无槽基线 95%」选定
> (第一版开太大,P06 曾掉到基线的 66%,收窄后全部回到 95~100%)。
> 代价只有惯性矩小幅下降(P04 截面 I 降 3.6%、P06/P07 减重区强度回到满值),
> 行程范围与碰撞检测完全不变,13/13 零件仍是单实体。
---
@@ -85,8 +177,20 @@ python src/sweep_v2.py
- 📘 **权威数据(工具自动生成)**:
- [各关节行程与硬限位 travel.json](file:///g:/Robot_v2/机器人手臂/docs/travel.json)(sweep_v2.py 扫描生成,修改几何后必须重扫)
> 滑块范围 = **单关节**硬限位。组合折叠姿态下另有干涉约束,见 `travel_detail.json` 的
> `combos` 字段(目前全部是工具法兰 P09 撞躯干占位 P01_A,属真约束);查看器运行期
> 另有胶囊碰撞模型按姿态夹取。
- [零件明细与质量属性 bom.json](file:///g:/Robot_v2/机器人手臂/docs/bom.json)(构建自动生成)
- [静态干涉检测结果 collision.json](file:///g:/Robot_v2/机器人手臂/docs/collision.json)(构建自动生成)
- [截面刚度与应力 strength.json](file:///g:/Robot_v2/机器人手臂/docs/strength.json)(strength_check.py 生成)
- [关节力矩与负载能力 torque_report.md](file:///g:/Robot_v2/机器人手臂/docs/torque_report.md)(torque_balance.py 生成)
- 🔬 **测试与校验工具**(src/ 目录):
- sweep_v2.py — 权威动态碰撞检测与行程扫描
- strength_check.py / torque_balance.py — 刚度、应力与力矩平衡校核(按需重新生成报告)
- strength_check.py — 剖面刚度/应力校核(零件真实质心 + 可达姿态搜索,输出含挠度估算)
- torque_balance.py — 关节电机力矩、惯量与负载能力校核(拓扑取自 human_layout,不手抄)
- finalize_travel.py — 从 travel_detail 重建 travel.json(含组合卡死阈值提示)
> 三个校验脚本共用同一套零件表与运动学链(`strength_check.part_table / chain`,
> 关节轴来自 `human_layout.py`),改几何后不存在"报告与模型不同步"的问题。
> 环境变量:`ARMPAY`(末端负载 kg)、`ARMDYN`(动载系数)、`ARMINFILL`(实体率)、
> `ARMMAT`(材料)、`ARMT_CONT`/`ARMT_PEAK`/`ARMT_ALLOW`(电机力矩)、`ARMT_ALPHA`(角加速度)。
+101
View File
@@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 700" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="700" fill="#ffffff"/>
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
</marker>
<marker id="arb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#7c5cd6"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">系统总览:一个事实源、两个碰撞模型、一条数据链</text>
<!-- 列标题 -->
<text x="175" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#1f6f43">① 建模侧(Python · CadQuery)</text>
<text x="490" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#8a5a00">② 数据契约(docs/*.json)</text>
<text x="805" y="70" text-anchor="middle" font-size="15" font-weight="bold" fill="#1f4f8f">③ 网页查看器(单文件 HTML)</text>
<!-- ① 建模侧 -->
<rect x="40" y="88" width="270" height="86" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.6"/>
<text x="175" y="112" text-anchor="middle" font-size="15" font-weight="bold" fill="#14472c">human_layout.py</text>
<text x="175" y="133" text-anchor="middle" font-size="12.5" fill="#2c5c44">唯一事实源:6 根关节轴 rear/d、</text>
<text x="175" y="151" text-anchor="middle" font-size="12.5" fill="#2c5c44">连杆高度链、台架/机身界面</text>
<rect x="40" y="206" width="270" height="86" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.6"/>
<text x="175" y="230" text-anchor="middle" font-size="15" font-weight="bold" fill="#14472c">arm_model.py · arm_build.py</text>
<text x="175" y="251" text-anchor="middle" font-size="12.5" fill="#2c5c44">参数化 CAD:9 打印件 + 6 电机模组</text>
<text x="175" y="269" text-anchor="middle" font-size="12.5" fill="#2c5c44">装配体 / STEP / STL / 质量属性</text>
<rect x="40" y="324" width="270" height="70" rx="8" fill="#f6f8fa" stroke="#8aa2b5" stroke-width="1.2"/>
<text x="175" y="348" text-anchor="middle" font-size="13" font-weight="bold" fill="#3d5164">strength_check.py · torque_balance.py</text>
<text x="175" y="368" text-anchor="middle" font-size="12" fill="#5c7185">同一关节表:截面刚度 / 重力力矩网格搜索</text>
<rect x="40" y="426" width="270" height="70" rx="8" fill="#fdf2ee" stroke="#b0562f" stroke-width="1.4"/>
<text x="175" y="450" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7c3a1c">sweep_v2.py(离线权威碰撞)</text>
<text x="175" y="470" text-anchor="middle" font-size="12" fill="#8a5433">多进程 + GJK 预筛 + OCCT 精确布尔</text>
<rect x="40" y="528" width="270" height="62" rx="8" fill="#fdf2ee" stroke="#b0562f" stroke-width="1.2" stroke-dasharray="5 3"/>
<text x="175" y="552" text-anchor="middle" font-size="13" font-weight="bold" fill="#7c3a1c">scratch/fit_capsules.py</text>
<text x="175" y="571" text-anchor="middle" font-size="12" fill="#8a5433">CAD 网格 → 实时胶囊模型拟合</text>
<line x1="175" y1="174" x2="175" y2="204" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="175" y1="292" x2="175" y2="322" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="175" y1="394" x2="175" y2="424" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<!-- ② 数据契约 -->
<g>
<rect x="365" y="426" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
<text x="490" y="449" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">travel.json / travel_detail.json</text>
<text x="490" y="468" text-anchor="middle" font-size="11.5" fill="#8f6d1c">单关节硬限位(滑块)+ 组合折叠明细</text>
<rect x="365" y="528" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
<text x="490" y="551" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">collide.json</text>
<text x="490" y="570" text-anchor="middle" font-size="11.5" fill="#8f6d1c">1995 胶囊 + adj 机械区间 + 硬边标记</text>
<rect x="365" y="324" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
<text x="490" y="347" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">torque_balance.json · strength.json</text>
<text x="490" y="366" text-anchor="middle" font-size="11.5" fill="#8f6d1c">逐关节力矩负载率 / 截面安全系数</text>
<rect x="365" y="222" width="250" height="56" rx="8" fill="#fff8ea" stroke="#c99418" stroke-width="1.4"/>
<text x="490" y="245" text-anchor="middle" font-size="13.5" font-weight="bold" fill="#7a5a08">mesh_arm.json(网格)</text>
<text x="490" y="264" text-anchor="middle" font-size="11.5" fill="#8f6d1c">AB.export → build_viewer.py 打包</text>
</g>
<line x1="310" y1="455" x2="363" y2="455" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="310" y1="559" x2="363" y2="559" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="310" y1="355" x2="363" y2="355" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="310" y1="255" x2="363" y2="252" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<text x="336" y="446" text-anchor="middle" font-size="11" fill="#5c7185">写出</text>
<text x="336" y="550" text-anchor="middle" font-size="11" fill="#5c7185">写出</text>
<!-- ③ 查看器 -->
<rect x="670" y="120" width="270" height="96" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
<text x="805" y="145" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">arm-ik-solver.js(ArmIK)</text>
<text x="805" y="166" text-anchor="middle" font-size="12.5" fill="#3a5a82">闭式支 + (θ4,θ5) 扫描 + 牛顿收口</text>
<text x="805" y="184" text-anchor="middle" font-size="12.5" fill="#3a5a82">DLS 兜底 · 实时胶囊碰撞 · 动态限位</text>
<text x="805" y="202" text-anchor="middle" font-size="12.5" fill="#3a5a82">纯 JS 无依赖 · 主线程 / Worker 同源</text>
<rect x="670" y="248" width="270" height="76" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
<text x="805" y="273" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">ik-glue.js(交互层)</text>
<text x="805" y="294" text-anchor="middle" font-size="12.5" fill="#3a5a82">拖拽 / 箭头姿态 / 实时追踪 / Worker</text>
<text x="805" y="312" text-anchor="middle" font-size="12.5" fill="#3a5a82">滑块动态限位夹取(边界缓存)</text>
<rect x="670" y="356" width="270" height="70" rx="8" fill="#edf3fc" stroke="#1f4f8f" stroke-width="1.6"/>
<text x="805" y="381" text-anchor="middle" font-size="15" font-weight="bold" fill="#153a6e">three.js 场景图</text>
<text x="805" y="402" text-anchor="middle" font-size="12.5" fill="#3a5a82">6 级嵌套 pivot(与 FK 链逐位一致)</text>
<text x="805" y="420" text-anchor="middle" font-size="12.5" fill="#3a5a82">FK 渲染 + 残差复核(双通道对账)</text>
<line x1="615" y1="460" x2="668" y2="168" stroke="#7c5cd6" stroke-width="1.8" marker-end="url(#arb)"/>
<line x1="615" y1="556" x2="668" y2="300" stroke="#7c5cd6" stroke-width="1.8" marker-end="url(#arb)"/>
<line x1="615" y1="250" x2="668" y2="392" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="805" y1="216" x2="805" y2="246" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<line x1="805" y1="324" x2="805" y2="354" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ar)"/>
<text x="636" y="508" font-size="11.5" fill="#7c5cd6">限位 / 碰撞注入求解器与 UI</text>
<text x="621" y="262" font-size="11" fill="#5c7185" stroke="#ffffff" stroke-width="3" paint-order="stroke">几何网格</text>
<!-- human_layout → solver 直接注入 -->
<path d="M 310 130 C 480 96, 560 100, 668 138" fill="none" stroke="#1f6f43" stroke-width="1.8" stroke-dasharray="6 4" marker-end="url(#ar)"/>
<text x="470" y="84" text-anchor="middle" font-size="11.5" fill="#1f6f43">关节 rear/d 常量经 build_viewer 注入两处(CAD 与网页同源)</text>
<!-- 底部工作流 -->
<rect x="40" y="620" width="900" height="56" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="490" y="643" text-anchor="middle" font-size="12.5" fill="#3d5164">改几何的同步纪律:arm_model.py 重建 → AB.export 写 mesh_arm.json → build_viewer.py 重打包 → 渲染出图确认 →</text>
<text x="490" y="663" text-anchor="middle" font-size="12.5" fill="#3d5164">几何变更必须重跑 sweep_v2.py(行程/碰撞权威);collide.json 由 fit_capsules.py 重新拟合。</text>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

+125
View File
@@ -0,0 +1,125 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 760" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="760" fill="#ffffff"/>
<defs>
<marker id="axr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#d64545"/>
</marker>
<marker id="axb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
</marker>
<marker id="dim" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#8a6d3b"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">运动链与关节轴(正视简化示意)</text>
<text x="490" y="58" text-anchor="middle" font-size="13" fill="#5c7185">沿 −Y 看:X 向右、Z 向上、Y 指向纸内(⊗)。零位 θ=0,臂竖直下垂。</text>
<!-- 地面与台架 -->
<line x1="70" y1="700" x2="910" y2="700" stroke="#8a9aa8" stroke-width="3"/>
<rect x="150" y="676" width="290" height="24" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
<rect x="228" y="80" width="52" height="598" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
<rect x="276" y="78" width="288" height="26" fill="#dfe8f0" stroke="#7ba3c7" stroke-width="1.2"/>
<text x="254" y="666" text-anchor="middle" font-size="11.5" fill="#5c7185">立柱 P01(台架)</text>
<text x="295" y="694" text-anchor="middle" font-size="11.5" fill="#5c7185">底座 P02</text>
<!-- 臂主干 -->
<rect x="592" y="86" width="16" height="522" rx="8" fill="#c3d2e0" stroke="#7ba3c7" stroke-width="1"/>
<rect x="560" y="72" width="80" height="36" rx="8" fill="#dbe7f3" stroke="#7ba3c7" stroke-width="1.4"/>
<!-- J1: X 轴 -->
<line x1="508" y1="90" x2="692" y2="90" stroke="#d64545" stroke-width="2.4" marker-start="url(#axr)" marker-end="url(#axr)"/>
<circle cx="600" cy="90" r="7" fill="#7c5cd6"/>
<text x="702" y="82" font-size="13" font-weight="bold" fill="#16324f">J1 肩抬举</text>
<text x="702" y="100" font-size="12" fill="#5c7185">轴 +X(滚转,前后方向)</text>
<!-- J2: Y ⊗ -->
<circle cx="600" cy="210" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="593" y1="203" x2="607" y2="217" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="607" y1="203" x2="593" y2="217" stroke="#1f9d63" stroke-width="2.2"/>
<text x="620" y="202" font-size="13" font-weight="bold" fill="#16324f">J2 肩俯仰</text>
<text x="620" y="220" font-size="12" fill="#5c7185">轴 +Y(⊗,与 J4/J5 平行同向)</text>
<!-- J3: Z 自转 -->
<line x1="600" y1="225" x2="600" y2="352" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5" marker-start="url(#axb)" marker-end="url(#axb)"/>
<ellipse cx="600" cy="255" rx="26" ry="9" fill="none" stroke="#3572c9" stroke-width="1.6"/>
<circle cx="600" cy="255" r="5.5" fill="#7c5cd6"/>
<text x="640" y="250" font-size="13" font-weight="bold" fill="#16324f">J3 大臂自转</text>
<text x="640" y="268" font-size="12" fill="#5c7185">轴 −Z(臂自身轴线)</text>
<!-- J4 -->
<circle cx="600" cy="368" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="593" y1="361" x2="607" y2="375" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="607" y1="361" x2="593" y2="375" stroke="#1f9d63" stroke-width="2.2"/>
<text x="620" y="360" font-size="13" font-weight="bold" fill="#16324f">J4 肘部俯仰</text>
<text x="620" y="378" font-size="12" fill="#5c7185">轴 +Y(⊗)</text>
<!-- J5 -->
<circle cx="600" cy="478" r="10" fill="#ffffff" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="593" y1="471" x2="607" y2="485" stroke="#1f9d63" stroke-width="2.2"/>
<line x1="607" y1="471" x2="593" y2="485" stroke="#1f9d63" stroke-width="2.2"/>
<text x="620" y="470" font-size="13" font-weight="bold" fill="#16324f">J5 腕部俯仰</text>
<text x="620" y="488" font-size="12" fill="#5c7185">轴 +Y(⊗,与 J4 同向 ⇒ R4·R5 可合并)</text>
<!-- J6 -->
<line x1="600" y1="492" x2="600" y2="596" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5" marker-start="url(#axb)" marker-end="url(#axb)"/>
<ellipse cx="600" cy="524" rx="24" ry="8" fill="none" stroke="#3572c9" stroke-width="1.6"/>
<circle cx="600" cy="524" r="5.5" fill="#7c5cd6"/>
<text x="640" y="520" font-size="13" font-weight="bold" fill="#16324f">J6 腕自转</text>
<text x="640" y="538" font-size="12" fill="#5c7185">轴 −Z(滚转,TCP 在轴线上 ⇒ 不动位置)</text>
<!-- TCP -->
<circle cx="600" cy="607" r="6" fill="#e08a1e"/>
<text x="620" y="612" font-size="13" font-weight="bold" fill="#b06a10">TCP</text>
<text x="620" y="630" font-size="11.5" fill="#5c7185">工具法兰安装面中心(J6 组打印件最低面)</text>
<!-- 左侧尺寸链 -->
<g stroke="#8a6d3b" stroke-width="1.3">
<line x1="480" y1="90" x2="480" y2="210" marker-start="url(#dim)" marker-end="url(#dim)"/>
<line x1="480" y1="210" x2="480" y2="255" marker-start="url(#dim)" marker-end="url(#dim)"/>
<line x1="480" y1="255" x2="480" y2="368" marker-start="url(#dim)" marker-end="url(#dim)"/>
<line x1="480" y1="368" x2="480" y2="478" marker-start="url(#dim)" marker-end="url(#dim)"/>
<line x1="480" y1="478" x2="480" y2="524" marker-start="url(#dim)" marker-end="url(#dim)"/>
<line x1="470" y1="90" x2="490" y2="90"/><line x1="470" y1="210" x2="490" y2="210"/>
<line x1="470" y1="255" x2="490" y2="255"/><line x1="470" y1="368" x2="490" y2="368"/>
<line x1="470" y1="478" x2="490" y2="478"/><line x1="470" y1="524" x2="490" y2="524"/>
</g>
<g font-size="11.5" fill="#8a6d3b" text-anchor="end">
<text x="472" y="154">C0 = 120</text>
<text x="472" y="236">45</text>
<text x="472" y="315">113</text>
<text x="472" y="427">110(前臂)</text>
<text x="472" y="505">46</text>
</g>
<g font-size="11" fill="#8a9aa8">
<text x="498" y="122">z=610</text><text x="498" y="226">z=490</text><text x="498" y="271">z=445</text>
<text x="498" y="384">z=332</text><text x="498" y="494">z=222</text><text x="498" y="540">z=176</text>
</g>
<!-- 右侧段名 -->
<g font-size="12" fill="#3d5164">
<text x="800" y="154" text-anchor="middle">肩关节组</text>
<line x1="770" y1="92" x2="770" y2="212" stroke="#b9c8d6" stroke-width="1.2"/>
<text x="800" y="295" text-anchor="middle">大臂 158</text>
<line x1="770" y1="212" x2="770" y2="370" stroke="#b9c8d6" stroke-width="1.2"/>
<text x="800" y="428" text-anchor="middle">前臂 110</text>
<line x1="770" y1="370" x2="770" y2="480" stroke="#b9c8d6" stroke-width="1.2"/>
<text x="800" y="560" text-anchor="middle">腕组 46 + 法兰</text>
<line x1="770" y1="480" x2="770" y2="606" stroke="#b9c8d6" stroke-width="1.2"/>
</g>
<!-- 图例 -->
<rect x="70" y="560" width="300" height="120" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="86" y="586" font-size="13" font-weight="bold" fill="#16324f">图例</text>
<line x1="86" y1="606" x2="126" y2="606" stroke="#d64545" stroke-width="2.4" marker-start="url(#axr)" marker-end="url(#axr)"/>
<text x="136" y="610" font-size="12" fill="#3d5164">X 轴(水平,前后)</text>
<circle cx="106" cy="630" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
<line x1="101" y1="625" x2="111" y2="635" stroke="#1f9d63" stroke-width="2"/>
<line x1="111" y1="625" x2="101" y2="635" stroke="#1f9d63" stroke-width="2"/>
<text x="136" y="634" font-size="12" fill="#3d5164">Y 轴(指向纸内)</text>
<line x1="106" y1="648" x2="106" y2="668" stroke="#3572c9" stroke-width="2" marker-start="url(#axb)" marker-end="url(#axb)"/>
<text x="136" y="662" font-size="12" fill="#3d5164">Z 轴(竖直,J3/J6 实为 −Z)</text>
<rect x="70" y="700" width="0" height="0" fill="none"/>
<text x="490" y="730" text-anchor="middle" font-size="12.5" fill="#3d5164">结构前提(求解器启动即断言):J2/J4/J5 平行同向且同一 x 平面(X_ARM≈19.5)、J1 ∥ X、J3/J6 ∥ Z、TCP 落在 J6 轴线上。</text>
<text x="490" y="750" text-anchor="middle" font-size="12.5" fill="#3d5164">这五条性质让六轴问题可以降阶为「二维冗余扫描 + 闭式支」,任何一条不成立时求解器会打出 WARN 并回落通用路径。</text>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

+69
View File
@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 440" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="440" fill="#ffffff"/>
<defs>
<marker id="fb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">正运动学:枢轴变换与链式累积</text>
<!-- 左:单关节枢轴 -->
<text x="230" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">单关节:绕“不在原点的轴”旋转</text>
<line x1="230" y1="110" x2="230" y2="300" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5"/>
<rect x="212" y="160" width="36" height="120" rx="10" fill="#e7edf3" stroke="#9db0c0" stroke-width="1.4" stroke-dasharray="5 4"/>
<rect x="212" y="160" width="36" height="120" rx="10" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6" transform="rotate(32 230 160)"/>
<ellipse cx="230" cy="160" rx="46" ry="14" fill="none" stroke="#3572c9" stroke-width="1.8" marker-end="url(#fb)"/>
<circle cx="230" cy="160" r="7" fill="#7c5cd6"/>
<text x="296" y="148" font-size="12.5" fill="#3d5164">枢轴点 P + 轴方向 d</text>
<text x="296" y="166" font-size="12.5" fill="#3d5164">θ=0 位形(虚线)→ θ 位形(蓝)</text>
<text x="296" y="184" font-size="12.5" fill="#3d5164">关节角 θi = 绕轴 d 的有符号转角</text>
<rect x="60" y="318" width="400" height="104" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="80" y="344" font-size="13" fill="#16324f" font-family="Consolas,monospace">A(θ) = T(P) · R(d,θ) · T(−P)</text>
<text x="80" y="368" font-size="13" fill="#16324f" font-family="Consolas,monospace">x' = R·(x − P) + P</text>
<text x="80" y="396" font-size="12" fill="#5c7185">先平移枢轴点到原点 → 绕轴转 θ → 平移回去。关节表只存</text>
<text x="80" y="412" font-size="12" fill="#5c7185">P(rear)与 d(单位轴向量),六个关节共 12 个常量。</text>
<!-- 右:链 -->
<text x="735" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">六链累积:World = A₁·A₂·A₃·A₄·A₅·A₆·TCP</text>
<g font-size="14" font-weight="bold" text-anchor="middle">
<rect x="510" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="536" y="157" fill="#153a6e">A1</text>
<rect x="578" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="604" y="157" fill="#153a6e">A2</text>
<rect x="646" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="672" y="157" fill="#153a6e">A3</text>
<rect x="714" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="740" y="157" fill="#153a6e">A4</text>
<rect x="782" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="808" y="157" fill="#153a6e">A5</text>
<rect x="850" y="130" width="52" height="44" rx="6" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.5"/>
<text x="876" y="157" fill="#153a6e">A6</text>
</g>
<g font-size="11" fill="#5c7185" text-anchor="middle">
<text x="536" y="190">θ1</text><text x="604" y="190">θ2</text><text x="672" y="190">θ3</text>
<text x="740" y="190">θ4</text><text x="808" y="190">θ5</text><text x="876" y="190">θ6</text>
</g>
<g stroke="#5a7d9a" stroke-width="1.5">
<line x1="472" y1="152" x2="508" y2="152"/>
<line x1="562" y1="152" x2="576" y2="152"/>
<line x1="630" y1="152" x2="644" y2="152"/>
<line x1="698" y1="152" x2="712" y2="152"/>
<line x1="766" y1="152" x2="780" y2="152"/>
<line x1="834" y1="152" x2="848" y2="152"/>
<line x1="902" y1="152" x2="918" y2="152"/>
</g>
<text x="462" y="157" text-anchor="end" font-size="13" font-weight="bold" fill="#16324f">World</text>
<text x="960" y="157" text-anchor="end" font-size="13" font-weight="bold" fill="#b06a10">TCP</text>
<text x="735" y="216" text-anchor="middle" font-size="12" fill="#5c7185">每级只读父级位姿与自己 —— 与 three.js 场景图 6 级嵌套 pivot 逐位一致</text>
<rect x="510" y="240" width="420" height="192" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="530" y="266" font-size="13" font-weight="bold" fill="#16324f">两套实现,互为对账:</text>
<text x="530" y="290" font-size="12.5" fill="#3d5164">· fkChain():严格 4×4 同质矩阵链 —— 对照基准,慢而直白</text>
<text x="530" y="312" font-size="12.5" fill="#3d5164">· fk():按结构常量展开三角函数的闭式快路径 —— 求解器</text>
<text x="530" y="332" font-size="12.5" fill="#3d5164"> 热路径专用,每姿态仅 ~20 次乘加</text>
<text x="530" y="360" font-size="12.5" fill="#3d5164">· 自检:5000 个随机位形,两实现的 TCP 差 ≤ 5.7e-13 mm</text>
<text x="530" y="384" font-size="12.5" fill="#3d5164">· 工具长度 tool 沿 J6 轴外伸:TCP_z 随 tool 平移(ax=J6 轴)</text>
<text x="530" y="406" font-size="11.5" fill="#8a9aa8">指纹校验:预置自检值绑定(限位表+常量+源码)的 FNV-1a 指纹,</text>
<text x="530" y="422" font-size="11.5" fill="#8a9aa8">模型或求解器一变即提示重算。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

+103
View File
@@ -0,0 +1,103 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 920" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="920" fill="#ffffff"/>
<defs>
<marker id="pa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
</marker>
<marker id="pao" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#b06a10"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">逆运动学求解流水线(solve → solveDyn)</text>
<!-- 主流程 -->
<g>
<rect x="265" y="60" width="450" height="64" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6"/>
<text x="280" y="84" font-size="12.5" font-weight="bold" fill="#153a6e">① 输入</text>
<text x="280" y="102" font-size="12" fill="#2a4a6e">目标 TCP (x,y,z) · 工具长 tool · 可选姿态 R(az,el,roll)</text>
<text x="280" y="118" font-size="12" fill="#2a4a6e">ref = 当前姿态:取「本步行程最小」的构型</text>
<rect x="265" y="148" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
<text x="280" y="172" font-size="12.5" font-weight="bold" fill="#16324f">② 冗余维准备:(θ4,θ5) 网格 + 逐行可行段探测</text>
<text x="280" y="190" font-size="12" fill="#3d5164">步长 h=0.25°;先按探测步 hp 找每行有解的连续段(位置 2° / 姿态 0.5°)</text>
<text x="280" y="206" font-size="12" fill="#3d5164">段端外扩 PAD=2° 兜住段界不确定性;段内仍按 h 精扫,密度不降</text>
<rect x="265" y="236" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
<text x="280" y="260" font-size="12.5" font-weight="bold" fill="#16324f">③ 全域粗扫:每格 branch() 闭式解 θ1..θ3(≤4 支)</text>
<text x="280" y="279" font-size="12" fill="#3d5164">位置由闭式支精确命中(零残差),扫描只优化冗余维;候选有界保留 4000</text>
<rect x="265" y="324" width="450" height="64" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
<text x="280" y="348" font-size="12.5" font-weight="bold" fill="#7a5a08">④ [姿态任务] 孤立根枚举:残差变号括号 → 阻尼高斯-牛顿收根</text>
<text x="280" y="367" font-size="12" fill="#6b5312">切向相切的根不变号,按每支 ‖e‖ 最小的节点补种;精确根直接进候选池</text>
<rect x="265" y="412" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
<text x="280" y="436" font-size="12.5" font-weight="bold" fill="#16324f">⑤ 局部加密 + 模式搜索:32 种子 × 窗口 ±2h(步长 h/8)</text>
<text x="280" y="455" font-size="12" fill="#3d5164">8 邻域模式搜索步长逐轮减半至 2e-4°,把每条谷底走到最低</text>
<rect x="265" y="500" width="450" height="64" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
<text x="280" y="524" font-size="12.5" font-weight="bold" fill="#7a5a08">⑥ [姿态任务] 二维牛顿收口 + escalating 加压</text>
<text x="280" y="542" font-size="12" fill="#6b5312">对工具轴切向偏差做 (θ4,θ5) 牛顿迭代,轴残差 →1e-12、位置保持精确</text>
<text x="280" y="558" font-size="12" fill="#6b5312">仍无 oerr≤0.05° 达标解则姿态权重 ×100 重扫(最多 3 轮)</text>
<rect x="265" y="588" width="450" height="64" rx="8" fill="#f4f8fb" stroke="#7ba3c7" stroke-width="1.4"/>
<text x="280" y="612" font-size="12.5" font-weight="bold" fill="#16324f">⑦ 选解:cost = Σ w·|wrap180(θ−ref)| + 1e-6·Σ|θ|</text>
<text x="280" y="630" font-size="12" fill="#3d5164">姿态任务先取 oerr≤0.05° 达标集,集内再比代价</text>
<text x="280" y="646" font-size="12" fill="#3d5164">精确根只在代价 ≤ 达标最优+2° 时接管(防漏检多付几十度)</text>
<rect x="265" y="676" width="450" height="64" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.4"/>
<text x="280" y="700" font-size="12.5" font-weight="bold" fill="#14472c">⑧ solveDyn 碰撞后处理(装了胶囊模型时)</text>
<text x="280" y="718" font-size="12" fill="#2c5c44">解先过全姿态碰撞校验:不安全 → 候选表找 ≤1.5×cost+20° 的安全解</text>
<text x="280" y="734" font-size="12" fill="#2c5c44">贴边轴二分动态边界放宽(≥5° 才值得重解,重解 cost 不升才采用)</text>
<rect x="265" y="764" width="450" height="58" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.6"/>
<text x="280" y="788" font-size="12.5" font-weight="bold" fill="#153a6e">输出:q[6] · 残差 err / oerr · 本步行程 moveSum · 候选表 list</text>
<text x="280" y="806" font-size="12" fill="#2a4a6e">徽章分级:已到达 / 姿态到达·位置最近 / 混合折中 / 不可达·最近位姿</text>
</g>
<g stroke="#5a7d9a" stroke-width="1.8">
<line x1="490" y1="124" x2="490" y2="146" marker-end="url(#pa)"/>
<line x1="490" y1="212" x2="490" y2="234" marker-end="url(#pa)"/>
<line x1="490" y1="300" x2="490" y2="322" marker-end="url(#pa)"/>
<line x1="490" y1="388" x2="490" y2="410" marker-end="url(#pa)"/>
<line x1="490" y1="476" x2="490" y2="498" marker-end="url(#pa)"/>
<line x1="490" y1="564" x2="490" y2="586" marker-end="url(#pa)"/>
<line x1="490" y1="652" x2="490" y2="674" marker-end="url(#pa)"/>
<line x1="490" y1="740" x2="490" y2="762" marker-end="url(#pa)"/>
</g>
<!-- 右侧:快路 -->
<rect x="740" y="236" width="215" height="196" rx="10" fill="#ffffff" stroke="#b06a10" stroke-width="1.6" stroke-dasharray="7 5"/>
<text x="755" y="262" font-size="13" font-weight="bold" fill="#b06a10">solveWarm 实时快路</text>
<text x="755" y="284" font-size="11.5" fill="#6b5312">拖拽中:只在上帧解附近的</text>
<text x="755" y="302" font-size="11.5" fill="#6b5312">(θ4,θ5) 递进窗口 ±4→14→40°</text>
<text x="755" y="320" font-size="11.5" fill="#6b5312">同支跟随(Σ|Δθ|≤25°)+ 残差</text>
<text x="755" y="338" font-size="11.5" fill="#6b5312">≤1e-6 + 姿态牛顿收口</text>
<text x="755" y="362" font-size="11.5" font-weight="bold" fill="#b06a10">任一校验不过 → null</text>
<text x="755" y="380" font-size="11.5" fill="#6b5312">自动回落全量扫描(③)</text>
<text x="755" y="400" font-size="11" fill="#8a9aa8">快路只负责「能快时快」,</text>
<text x="755" y="416" font-size="11" fill="#8a9aa8">松手后全量求解才是终值</text>
<path d="M 738 268 C 727 240, 727 130, 719 96" fill="none" stroke="#b06a10" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
<path d="M 847 434 C 847 480, 700 560, 721 600" fill="none" stroke="#b06a10" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
<text x="742" y="470" font-size="11" fill="#b06a10">校验不过回落</text>
<!-- 左侧:兜底 -->
<rect x="25" y="324" width="215" height="230" rx="10" fill="#ffffff" stroke="#8a5a8f" stroke-width="1.6" stroke-dasharray="7 5"/>
<text x="40" y="350" font-size="13" font-weight="bold" fill="#7a3a80">兜底:目标不可达时</text>
<text x="40" y="372" font-size="11.5" fill="#5c3a60">closest() 位置 DLS:160 迭代 ×</text>
<text x="40" y="390" font-size="11.5" fill="#5c3a60">160 次 LCG 确定性重启(同输入</text>
<text x="40" y="408" font-size="11.5" fill="#5c3a60">逐位一致)+ anchor 回拉 0.03</text>
<text x="40" y="426" font-size="11.5" fill="#5c3a60">压退化漂移 + 零空间抛光收敛</text>
<text x="40" y="450" font-size="11.5" fill="#5c3a60">closest6() 位置+姿态 6 维联合</text>
<text x="40" y="468" font-size="11.5" fill="#5c3a60">DLS(姿态按 20mm/rad 折算)</text>
<text x="40" y="492" font-size="11.5" fill="#5c3a60">closestOri() 姿态优先:闭式轴</text>
<text x="40" y="510" font-size="11.5" fill="#5c3a60">对齐 + (θ3,s,θ4) 坐标下降,</text>
<text x="40" y="528" font-size="11.5" fill="#5c3a60">姿态精确、位置取最近</text>
<text x="40" y="548" font-size="11" fill="#8a9aa8">距离差 ≤0.5mm 时优先锚定解</text>
<path d="M 263 620 C 180 620, 140 590, 133 556" fill="none" stroke="#8a5a8f" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
<text x="148" y="648" font-size="11" fill="#8a5a8f">0 采样点 / 不可达</text>
<path d="M 133 322 C 140 220, 200 130, 262 96" fill="none" stroke="#8a5a8f" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#pao)"/>
<text x="120" y="200" font-size="11" fill="#8a5a8f">返回最近位姿</text>
<text x="490" y="860" text-anchor="middle" font-size="12.5" fill="#3d5164">热路径性能手段:轴对齐特化 rotAxis(免 Rodrigues 通用式)· sin/cos 单槽缓存 · 关节限位数组预取 · 残差 fk 复核延后到入选候选统一补算</text>
<text x="490" y="884" text-anchor="middle" font-size="12.5" fill="#3d5164">全量扫描 40~430 ms(后台 Worker);实时快路毫秒级 —— 这是拖拽能跟手的原因</text>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

+88
View File
@@ -0,0 +1,88 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 620" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="620" fill="#ffffff"/>
<defs>
<marker id="ta" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#8a9aa8"/>
</marker>
<marker id="to" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">冗余维 (θ4,θ5) 平面:可行带、加密窗口与牛顿收口</text>
<!-- 绘图区 -->
<rect x="110" y="80" width="740" height="430" fill="#f8fafc" stroke="#b9c8d6" stroke-width="1.2"/>
<!-- 可行带(斜窄带,宽度做了夸张) -->
<polygon points="245,452 830,168 830,132 245,416" fill="#d9f2e5" opacity="0.9"/>
<text x="585" y="215" font-size="12" fill="#1f7a4d" transform="rotate(-26 585 215)">可行带:branch 判据筛掉 73~93% 的格点</text>
<!-- 不可行散点 -->
<g fill="#c3ccd4" font-size="10">
<text x="200" y="140">×</text><text x="330" y="120">×</text><text x="500" y="110">×</text>
<text x="700" y="128">×</text><text x="790" y="180">×</text><text x="170" y="300">×</text>
<text x="300" y="260">×</text><text x="430" y="210">×</text><text x="640" y="150">×</text>
<text x="220" y="470">×</text><text x="480" y="480">×</text><text x="700" y="460">×</text>
<text x="820" y="430">×</text><text x="360" y="420">×</text><text x="560" y="440">×</text>
</g>
<!-- 可行带内格点 -->
<g fill="#2f9d68">
<circle cx="270" cy="428" r="2.4"/><circle cx="310" cy="410" r="2.4"/><circle cx="352" cy="390" r="2.4"/>
<circle cx="395" cy="368" r="2.4"/><circle cx="438" cy="348" r="2.4"/><circle cx="480" cy="328" r="2.4"/>
<circle cx="522" cy="308" r="2.4"/><circle cx="565" cy="288" r="2.4"/><circle cx="608" cy="268" r="2.4"/>
<circle cx="650" cy="248" r="2.4"/><circle cx="692" cy="228" r="2.4"/><circle cx="735" cy="206" r="2.4"/>
<circle cx="778" cy="186" r="2.4"/><circle cx="812" cy="168" r="2.4"/>
<circle cx="292" cy="418" r="2.4"/><circle cx="375" cy="378" r="2.4"/><circle cx="459" cy="338" r="2.4"/>
<circle cx="543" cy="298" r="2.4"/><circle cx="628" cy="256" r="2.4"/><circle cx="712" cy="216" r="2.4"/>
<circle cx="796" cy="178" r="2.4"/>
</g>
<!-- 行可行段 -->
<g stroke="#3572c9" stroke-width="5" opacity="0.75">
<line x1="228" y1="440" x2="360" y2="376"/>
<line x1="415" y1="352" x2="548" y2="288"/>
<line x1="602" y1="264" x2="735" y2="200"/>
</g>
<text x="238" y="486" font-size="12" fill="#2a4a6e">逐行可行段 rowSpans(探测步 hp,段端外扩 PAD=2°):</text>
<text x="238" y="504" font-size="12" fill="#2a4a6e">每行通常是单一连续段,段内仍按 h 精扫 —— 密度不降、成本大降</text>
<!-- 加密窗口 -->
<rect x="478" y="278" width="92" height="92" fill="none" stroke="#e08a1e" stroke-width="1.8" stroke-dasharray="6 4"/>
<text x="580" y="368" font-size="12" fill="#b06a10">加密窗口 ±2h(步长 h/8,</text>
<text x="580" y="386" font-size="12" fill="#b06a10">小步长档 h/4)罩住种子</text>
<!-- 模式搜索折线 -->
<polyline points="500,340 524,318 512,296 540,282 560,296 578,282" fill="none" stroke="#e08a1e" stroke-width="1.8" marker-end="url(#to)"/>
<text x="700" y="330" font-size="12" fill="#b06a10">8 邻域模式搜索:</text>
<text x="700" y="348" font-size="12" fill="#b06a10">步长减半,停在窄谷谷壁</text>
<!-- 牛顿收口点 -->
<g>
<path d="M 596 244 l 4.5 10.5 11 1 -8.5 7.5 2.5 11 -9.5 -6 -9.5 6 2.5 -11 -8.5 -7.5 11 -1 z" fill="#d64545"/>
<line x1="602" y1="254" x2="640" y2="286" stroke="#d64545" stroke-width="1" stroke-dasharray="3 3"/>
<text x="648" y="296" font-size="12.5" font-weight="bold" fill="#b03030">牛顿收口点</text>
<text x="648" y="314" font-size="12" fill="#b03030">轴残差 →1e-12(位置保持精确)</text>
</g>
<!-- 坐标轴 -->
<g stroke="#8a9aa8" stroke-width="1.4">
<line x1="110" y1="510" x2="850" y2="510" marker-start="url(#ta)"/>
<line x1="110" y1="510" x2="110" y2="80" marker-end="url(#ta)"/>
<line x1="130" y1="510" x2="130" y2="516"/><line x1="245" y1="510" x2="245" y2="516"/>
<line x1="411" y1="510" x2="411" y2="516"/><line x1="576" y1="510" x2="576" y2="516"/>
<line x1="741" y1="510" x2="741" y2="516"/><line x1="830" y1="510" x2="830" y2="516"/>
<line x1="104" y1="500" x2="110" y2="500"/><line x1="104" y1="396" x2="110" y2="396"/>
<line x1="104" y1="302" x2="110" y2="302"/><line x1="104" y1="207" x2="110" y2="207"/>
<line x1="104" y1="100" x2="110" y2="100"/>
</g>
<g font-size="11.5" fill="#5c7185">
<text x="126" y="530">−111°</text><text x="238" y="530">−90°</text><text x="404" y="530">−60°</text>
<text x="569" y="530">−30°</text><text x="734" y="530">0°</text><text x="812" y="530">+16°</text>
<text x="66" y="504">−93°</text><text x="66" y="400">−60°</text><text x="66" y="306">−30°</text>
<text x="76" y="211">0°</text><text x="80" y="104">+34°</text>
<text x="840" y="552" font-size="13" font-weight="bold" fill="#16324f">θ4(J4 肘部俯仰)</text>
<text x="66" y="70" font-size="13" font-weight="bold" fill="#16324f">θ5(J5 腕俯仰)</text>
</g>
<text x="490" y="582" text-anchor="middle" font-size="12.5" fill="#3d5164">每个格点的位置解由闭式支精确给出 —— 扫描不是在「逼近」目标,而是在冗余维里「挑构型」;</text>
<text x="490" y="602" text-anchor="middle" font-size="12.5" fill="#3d5164">姿态任务的达标带实测最窄 0.98°,2° 探测步会整条漏掉(误报不可达),故姿态任务探测步降到 0.5°。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+80
View File
@@ -0,0 +1,80 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 580" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="580" fill="#ffffff"/>
<defs>
<marker id="ga" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#8a9aa8"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">闭式支 branch():给定 (θ4,θ5),θ1..θ3 一步解出(简化示意)</text>
<!-- 左panel -->
<text x="255" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">第 1 步:(θ4,θ5) ⇒ 腕心平面坐标 V</text>
<line x1="150" y1="120" x2="150" y2="330" stroke="#3572c9" stroke-width="1.6" stroke-dasharray="7 5"/>
<text x="150" y="352" text-anchor="middle" font-size="11.5" fill="#5c7185">J4 轴(+Y,⊗)</text>
<circle cx="150" cy="140" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
<line x1="144" y1="134" x2="156" y2="146" stroke="#1f9d63" stroke-width="2"/>
<line x1="156" y1="134" x2="144" y2="146" stroke="#1f9d63" stroke-width="2"/>
<line x1="150" y1="140" x2="278" y2="188" stroke="#3572c9" stroke-width="5" stroke-linecap="round"/>
<circle cx="278" cy="188" r="8" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
<line x1="272" y1="182" x2="284" y2="194" stroke="#1f9d63" stroke-width="2"/>
<line x1="284" y1="182" x2="272" y2="194" stroke="#1f9d63" stroke-width="2"/>
<text x="292" y="186" font-size="11.5" fill="#5c7185">J5 轴</text>
<line x1="278" y1="188" x2="330" y2="252" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
<circle cx="330" cy="252" r="6" fill="#e08a1e"/>
<text x="344" y="258" font-size="12" font-weight="bold" fill="#b06a10">腕心方向(L = L5+tool)</text>
<path d="M 150 170 A 30 30 0 0 0 178 150.5" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
<text x="186" y="178" font-size="12" fill="#7c5cd6">θ4</text>
<path d="M 262 216 A 30 30 0 0 1 296 214" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
<text x="272" y="232" font-size="12" fill="#7c5cd6">θ5</text>
<text x="150" y="120" font-size="11.5" fill="#5c7185">肘(J4)</text>
<rect x="50" y="386" width="420" height="152" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="68" y="412" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">u = −L·sinθ5 w = A54 − L·cosθ5</text>
<text x="68" y="436" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">dx = u·cosθ4 + w·sinθ4</text>
<text x="68" y="460" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">dz = A43 − u·sinθ4 + w·cosθ4</text>
<text x="68" y="484" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">V = (dx, dz + A32)</text>
<text x="68" y="514" font-size="11.5" fill="#5c7185">J4∥J5 同向 ⇒ R4·R5 = R(θ4+θ5):腕心只由这两个角决定,</text>
<text x="68" y="530" font-size="11.5" fill="#5c7185">姿态任务里工具轴对齐也因此只差 2 个未知数。</text>
<!-- 右panel -->
<text x="735" y="66" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">第 2 步:肩部圆交 + 肘部四支 ⇒ θ1/θ2/θ3</text>
<!-- 俯视:J1 轴与 φ -->
<circle cx="600" cy="130" r="9" fill="#ffffff" stroke="#d64545" stroke-width="2"/>
<circle cx="600" cy="130" r="2" fill="#d64545"/>
<text x="576" y="112" font-size="11.5" fill="#5c7185">J1 轴(+X,⊙)</text>
<circle cx="700" cy="96" r="5" fill="#e08a1e"/>
<text x="712" y="94" font-size="11.5" fill="#b06a10">目标(柱坐标 R, φ)</text>
<line x1="600" y1="130" x2="700" y2="96" stroke="#e08a1e" stroke-width="1.6" stroke-dasharray="5 4"/>
<path d="M 640 117 A 42 42 0 0 0 636 108" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
<text x="648" y="106" font-size="12" fill="#7c5cd6">φ</text>
<text x="566" y="158" font-size="12" fill="#16324f" font-family="Consolas,monospace">θ1 = φ − atan2(cc, b)</text>
<!-- 正视:肩 C0 偏置与肘三角 -->
<circle cx="620" cy="290" r="9" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
<line x1="614" y1="284" x2="626" y2="296" stroke="#1f9d63" stroke-width="2"/>
<line x1="626" y1="284" x2="614" y2="296" stroke="#1f9d63" stroke-width="2"/>
<text x="560" y="294" text-anchor="end" font-size="11.5" fill="#5c7185">J1(正视)</text>
<circle cx="620" cy="382" r="9" fill="#ffffff" stroke="#1f9d63" stroke-width="2"/>
<line x1="614" y1="376" x2="626" y2="388" stroke="#1f9d63" stroke-width="2"/>
<line x1="626" y1="376" x2="614" y2="388" stroke="#1f9d63" stroke-width="2"/>
<text x="560" y="386" text-anchor="end" font-size="11.5" fill="#5c7185">J2</text>
<line x1="620" y1="299" x2="620" y2="373" stroke="#8a6d3b" stroke-width="1.3" marker-start="url(#ga)" marker-end="url(#ga)"/>
<text x="632" y="341" font-size="11.5" fill="#8a6d3b">C0=120</text>
<line x1="620" y1="382" x2="700" y2="322" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
<line x1="700" y1="322" x2="820" y2="290" stroke="#3572c9" stroke-width="4" stroke-linecap="round"/>
<circle cx="820" cy="290" r="6" fill="#e08a1e"/>
<text x="834" y="294" font-size="12" font-weight="bold" fill="#b06a10">腕心(由 V 决定)</text>
<path d="M 676 342 A 44 44 0 0 0 666 314" fill="none" stroke="#7c5cd6" stroke-width="1.6"/>
<text x="688" y="318" font-size="12" fill="#7c5cd6">σ</text>
<text x="690" y="368" font-size="11.5" fill="#5c7185">肘部三角:σ=acos(√m2)</text>
<text x="690" y="386" font-size="11.5" fill="#5c7185">θ3 四支:±σ 与 ±(π−σ)</text>
<text x="690" y="404" font-size="11.5" fill="#5c7185">(肘上/肘下 × 同号/反号镜像族)</text>
<rect x="510" y="428" width="440" height="110" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="528" y="454" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">n = (R²+A²−Vx²−Vz²−C0²) / (2·C0),cc = C0+n</text>
<text x="528" y="478" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">θ2 = atan2((λA−κn)/det, (κA+λn)/det) κ=Vx·c3, λ=Vz</text>
<text x="528" y="502" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">b = −Vx·sinθ3 (θ1 由柱角 φ 反推)</text>
<text x="528" y="526" font-size="11.5" fill="#8a5433">退化防线:|Vx|&lt;1e-7、det&lt;1e-12、m2 越界 → 该支无解,直接落选</text>
<text x="490" y="560" text-anchor="middle" font-size="12.5" fill="#3d5164">示意省略了 X0/Y0 基准平移与 tool 常量;四支解的位置全部精确命中目标 ——「扫描代替数值搜索」的根基。</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

+76
View File
@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 750" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="750" fill="#ffffff"/>
<defs>
<marker id="fa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">离线碰撞检测(sweep_v2.py):逐层过滤漏斗</text>
<text x="490" y="58" text-anchor="middle" font-size="13" fill="#5c7185">目标:把「每姿态都做 NURBS 布尔」压到只剩真正贴 face 的零件对 —— 精确性一分不减,调用量掉几个数量级</text>
<g font-size="12.5">
<rect x="120" y="76" width="600" height="46" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.4"/>
<circle cx="145" cy="99" r="12" fill="#3572c9"/><text x="145" y="104" text-anchor="middle" fill="#ffffff" font-weight="bold">1</text>
<text x="168" y="95" font-weight="bold" fill="#153a6e">全对全候选对</text>
<text x="168" y="113" fill="#2a4a6e">19 件 ≈ 171 对(9 打印件 + 6 电机 + 台架 P01/P02),按姿态刚体变换</text>
<line x1="420" y1="122" x2="420" y2="136" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="140" y="138" width="560" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<circle cx="165" cy="161" r="12" fill="#5a86b5"/><text x="165" y="166" text-anchor="middle" fill="#ffffff" font-weight="bold">2</text>
<text x="188" y="157" font-weight="bold" fill="#153a6e">跳过刚性安装对</text>
<text x="188" y="175" fill="#2a4a6e">MOUNT_PAIRS 12 对:电机 ↔ 被驱动件由螺钉锁死,无相对运动</text>
<line x1="420" y1="184" x2="420" y2="198" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="160" y="200" width="520" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<circle cx="185" cy="223" r="12" fill="#5a86b5"/><text x="185" y="228" text-anchor="middle" fill="#ffffff" font-weight="bold">3</text>
<text x="208" y="219" font-weight="bold" fill="#153a6e">组跨度规则</text>
<text x="208" y="237" fill="#2a4a6e">该姿态中没有活动关节跨越的对跳过(零位时全部必查——静止不代表不干涉)</text>
<line x1="420" y1="246" x2="420" y2="260" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="180" y="262" width="480" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<circle cx="205" cy="285" r="12" fill="#5a86b5"/><text x="205" y="290" text-anchor="middle" fill="#ffffff" font-weight="bold">4</text>
<text x="228" y="281" font-weight="bold" fill="#153a6e">轴区间预剪枝(单关节扫描专用,精确安全)</text>
<text x="228" y="299" fill="#2a4a6e">沿关节轴的轴向/径向区间永不相交的对,任何角度都不可能碰撞</text>
<line x1="420" y1="308" x2="420" y2="322" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="200" y="324" width="440" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<circle cx="225" cy="347" r="12" fill="#5a86b5"/><text x="225" y="352" text-anchor="middle" fill="#ffffff" font-weight="bold">5</text>
<text x="248" y="343" font-weight="bold" fill="#153a6e">AABB 分离</text>
<text x="248" y="361" fill="#2a4a6e">8 角点变换后取包围盒,0.1mm 余量;分离即跳过</text>
<line x1="420" y1="370" x2="420" y2="384" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="220" y="386" width="400" height="46" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<circle cx="245" cy="409" r="12" fill="#5a86b5"/><text x="245" y="414" text-anchor="middle" fill="#ffffff" font-weight="bold">6</text>
<text x="268" y="405" font-weight="bold" fill="#153a6e">包围球分离</text>
<text x="268" y="423" fill="#2a4a6e">球心随组变换、半径旋转不变;球分离 ⇒ 零件必分离</text>
<line x1="420" y1="432" x2="420" y2="446" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="230" y="444" width="400" height="66" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.4"/>
<circle cx="255" cy="477" r="12" fill="#c99418"/><text x="255" y="482" text-anchor="middle" fill="#ffffff" font-weight="bold">7</text>
<text x="278" y="468" font-weight="bold" fill="#7a5a08">GJK 凸包预筛(保守证书)</text>
<text x="278" y="487" fill="#6b5312">0.5mm 凸包 ⊕ 0.8mm 球:确证分离才允许跳过</text>
<text x="278" y="500" fill="#6b5312" font-size="11">可能干涉/数值退化一律回落精确布尔——结果与纯布尔一致</text>
<line x1="420" y1="510" x2="420" y2="516" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="255" y="518" width="350" height="66" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.4"/>
<circle cx="280" cy="551" r="12" fill="#c99418"/><text x="280" y="556" text-anchor="middle" fill="#ffffff" font-weight="bold">8</text>
<text x="303" y="542" font-weight="bold" fill="#7a5a08">顶点侵入实锤(反方向 shortcut)</text>
<text x="303" y="561" fill="#6b5312">一方网格顶点(≤240 采样)落入对方实体 &gt;1mm</text>
<text x="303" y="574" fill="#6b5312" font-size="11">⇒ 直接判干涉,免 4~12s 布尔;只证「撞」不证「自由」</text>
<line x1="420" y1="584" x2="420" y2="590" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#fa)"/>
<rect x="275" y="592" width="300" height="52" rx="8" fill="#fbe9e7" stroke="#c0533a" stroke-width="1.5"/>
<circle cx="300" cy="618" r="12" fill="#c0533a"/><text x="300" y="623" text-anchor="middle" fill="#ffffff" font-weight="bold">9</text>
<text x="323" y="612" font-weight="bold" fill="#7c2a18">OCCT 精确布尔 Common</text>
<text x="323" y="630" fill="#8a3a26">实体交体积 &gt; 2mm³ 记干涉(附干涉质心)</text>
<text x="323" y="640" fill="#8a3a26" font-size="11">SetRunParallel(True) · 多进程分发姿态</text>
</g>
<path d="M 745 480 C 800 480, 820 540, 820 590 C 820 620, 790 630, 566 618" fill="none" stroke="#c0533a" stroke-width="1.5" stroke-dasharray="6 4" marker-end="url(#fa)"/>
<text x="806" y="470" text-anchor="middle" font-size="12" fill="#7c2a18">布尔每对 0.5~12s</text>
<text x="806" y="488" text-anchor="middle" font-size="12" fill="#7c2a18">前面每层都在为它省调用</text>
<rect x="60" y="660" width="860" height="66" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="490" y="684" text-anchor="middle" font-size="12.5" fill="#3d5164">扫描策略:单关节双向粗扫 2°(小批量、首个卡死角即停)+ 二分逼近 0.2° → travel.json(滑块硬限位)</text>
<text x="490" y="704" text-anchor="middle" font-size="12.5" fill="#3d5164">组合姿态收敛 6°(一个关节保持极限、扫其余,两轮迭代)→ 只写 travel_detail.json,不压缩滑块 | workers = min(12, CPU−2, 内存/2.5GB)</text>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 640" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="640" fill="#ffffff"/>
<defs>
<marker id="ca" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">实时碰撞模型:CAD 网格 → 胶囊体 + 两级宽相位</text>
<!-- 左:胶囊化手臂示意 -->
<text x="235" y="64" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">手臂按运动组胶囊化(fit_capsules.py 拟合)</text>
<line x1="90" y1="590" x2="400" y2="590" stroke="#8a9aa8" stroke-width="2.5"/>
<rect x="150" y="110" width="34" height="478" rx="6" fill="#e2e8ee" stroke="#9db0c0" stroke-width="1"/>
<text x="167" y="102" text-anchor="middle" font-size="11" fill="#5c7185">台架 g0(root 固定)</text>
<rect x="196" y="128" width="52" height="86" rx="26" fill="#aecbeb" stroke="#3572c9" stroke-width="1.4"/>
<text x="256" y="150" font-size="11.5" fill="#153a6e">g1 · J1 组(肩吊臂)</text>
<rect x="196" y="226" width="52" height="70" rx="26" fill="#b7dcC8" stroke="#1f9d63" stroke-width="1.4"/>
<text x="256" y="248" font-size="11.5" fill="#14573a">g2 · J2 组(肩叉)</text>
<rect x="196" y="306" width="52" height="96" rx="26" fill="#a8d5d8" stroke="#1f7a8f" stroke-width="1.4"/>
<text x="256" y="336" font-size="11.5" fill="#14525e">g3 · J3 组(大臂骨架)</text>
<rect x="196" y="412" width="52" height="78" rx="26" fill="#c9d9a8" stroke="#6f8f2f" stroke-width="1.4"/>
<text x="256" y="436" font-size="11.5" fill="#4a5f1c">g4 · J4 组(小臂骨架)</text>
<rect x="196" y="498" width="52" height="42" rx="21" fill="#f2d5a8" stroke="#c98a18" stroke-width="1.4"/>
<text x="256" y="512" font-size="11.5" fill="#7a5308">g5 · J5 组(腕偏转座)</text>
<rect x="196" y="546" width="0" height="0" fill="none"/>
<rect x="198" y="548" width="48" height="20" rx="10" fill="#f0bcb0" stroke="#c0533a" stroke-width="1.4"/>
<text x="256" y="564" font-size="11.5" fill="#7c2a18">g6 · J6 组(工具法兰)</text>
<text x="235" y="608" text-anchor="middle" font-size="11.5" fill="#5c7185">胶囊 = 线段端点 a,b + 半径 r,端点是零位 CAD 世界坐标;</text>
<text x="235" y="626" text-anchor="middle" font-size="11.5" fill="#5c7185">组 g 的胶囊随 A1..Ag 嵌套旋转(与 FK 链同一累积变换)。</text>
<!-- 右上:宽相位级联 -->
<text x="700" y="64" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">两级宽相位 → 胶囊精确距</text>
<rect x="470" y="80" width="470" height="58" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<text x="486" y="102" font-size="12.5" font-weight="bold" fill="#153a6e">第 1 级 · 零件包围球(19 球)</text>
<text x="486" y="122" font-size="11.5" fill="#2a4a6e">球心距 &gt; rad_A+rad_B+rmax_A+rmax_B+margin 才筛除(阈值推导防漏判)</text>
<line x1="705" y1="138" x2="705" y2="152" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ca)"/>
<rect x="470" y="154" width="470" height="58" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<text x="486" y="176" font-size="12.5" font-weight="bold" fill="#153a6e">第 2 级 · 簇球(每零件胶囊按主轴分桶,每簇 ≤4 枚)</text>
<text x="486" y="196" font-size="11.5" fill="#2a4a6e">主轴 = 胶囊中点的幂迭代方向(5 轮);再细分实测得不偿失</text>
<line x1="705" y1="212" x2="705" y2="226" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#ca)"/>
<rect x="470" y="228" width="470" height="58" rx="8" fill="#dbe9fb" stroke="#3572c9" stroke-width="1.4"/>
<text x="486" y="250" font-size="12.5" font-weight="bold" fill="#153a6e">第 3 级 · 胶囊对:线段-线段距离(Ericson 标准式)</text>
<text x="486" y="270" font-size="11.5" fill="#2a4a6e">d(轴_a, 轴_b) &lt; r_i + r_j + margin(0.3mm) ⇒ 干涉;无 visit 回调时首对即短路</text>
<!-- 胶囊距小图 -->
<g transform="translate(490,306)">
<rect x="0" y="18" width="120" height="34" rx="17" fill="#cfe3f5" stroke="#3572c9" stroke-width="1.4"/>
<line x1="17" y1="35" x2="103" y2="35" stroke="#3572c9" stroke-width="1.2" stroke-dasharray="4 3"/>
<rect x="210" y="0" width="110" height="30" rx="15" fill="#f2d5a8" stroke="#c98a18" stroke-width="1.4"/>
<line x1="225" y1="15" x2="305" y2="15" stroke="#c98a18" stroke-width="1.2" stroke-dasharray="4 3"/>
<line x1="110" y1="30" x2="212" y2="18" stroke="#c0533a" stroke-width="1.8" marker-start="url(#ca)" marker-end="url(#ca)"/>
<text x="128" y="12" font-size="12" font-weight="bold" fill="#c0533a">d(轴间距)</text>
<text x="120" y="86" font-size="12" fill="#16324f" font-family="Consolas,monospace">自由 ⇔ d ≥ r_i + r_j + margin</text>
<text x="120" y="106" font-size="11" fill="#5c7185">胶囊并集 ⊇ 零件本体 ⇒ 「自由」可信、「干涉」需离线布尔确认</text>
</g>
<!-- 右下:规则 -->
<rect x="470" y="428" width="470" height="128" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
<text x="486" y="452" font-size="12.5" font-weight="bold" fill="#7a5a08">三条关键规则</text>
<text x="486" y="474" font-size="12" fill="#6b5312">· 只检 |Δg|≥2 的零件对(同组相对静止、零位已保证不干涉);</text>
<text x="486" y="492" font-size="12" fill="#6b5312"> 相邻对 P04×P05/P06×P07/P07×P08 同心跨轴,塞进模型零位必误报</text>
<text x="486" y="510" font-size="12" fill="#6b5312"> ⇒ 改用 adj 一维自由区间表(CAD 实测、与姿态无关)</text>
<text x="486" y="528" font-size="12" fill="#6b5312">· adj:J2 [−107°,+128°] · J4 [−112°,+112°] · J5 [−96°,+96°]</text>
<text x="486" y="546" font-size="12" fill="#6b5312">· 硬边(lo 侧相邻件自碰撞)静态值即真值;可放宽侧(撞立柱)按姿态实时二分</text>
<rect x="470" y="566" width="470" height="58" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.3"/>
<text x="486" y="588" font-size="12.5" font-weight="bold" fill="#14472c">性能:1995 胶囊 · collideFree ≈ 5ms/次(零位最坏)</text>
<text x="486" y="608" font-size="11.5" fill="#2c5c44">预分配缓冲 + 代际戳缓存(免 GC)· 轴对齐特化旋转 · 热循环标量直传免建数组</text>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 560" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="560" fill="#ffffff"/>
<defs>
<marker id="da" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#5a7d9a"/>
</marker>
<marker id="dor" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">动态限位:静态值只是零位快照,真边界由碰撞模型按姿态二分</text>
<!-- 数轴(J5 例) -->
<text x="490" y="70" text-anchor="middle" font-size="14" font-weight="bold" fill="#1f4f8f">以 J5 腕俯仰 + 向为例(数值取自实机数据)</text>
<line x1="120" y1="200" x2="880" y2="200" stroke="#3d5164" stroke-width="2.5" marker-start="url(#da)" marker-end="url(#da)"/>
<g font-size="11.5" fill="#5c7185" text-anchor="middle">
<text x="120" y="222">−360°</text><text x="500" y="222">0°</text><text x="880" y="222">+360°(电机限位)</text>
<line x1="500" y1="194" x2="500" y2="206" stroke="#3d5164" stroke-width="1.5"/>
<line x1="120" y1="194" x2="120" y2="206" stroke="#8a9aa8" stroke-width="1.5"/>
<line x1="880" y1="194" x2="880" y2="206" stroke="#8a9aa8" stroke-width="1.5"/>
</g>
<!-- adj 机械区间 -->
<rect x="399" y="188" width="202" height="24" rx="4" fill="#dbe9fb" opacity="0.85" stroke="#3572c9" stroke-width="1.2"/>
<text x="500" y="136" text-anchor="middle" font-size="12" fill="#2a4a6e">adj 机械区间(相邻件 CAD 实测):J5 = [−96°, +96°]</text>
<!-- 静态扫描限位 -->
<line x1="536" y1="180" x2="536" y2="220" stroke="#1f9d63" stroke-width="3"/>
<text x="536" y="250" text-anchor="middle" font-size="12" fill="#1f7a4d">零位静态扫描 +34°(撞立柱 P01_A)</text>
<!-- 动态边界 -->
<line x1="601" y1="180" x2="601" y2="220" stroke="#e08a1e" stroke-width="3"/>
<text x="672" y="272" text-anchor="middle" font-size="12" font-weight="bold" fill="#b06a10">当前姿态动态边界 +96°</text>
<!-- 当前角 -->
<path d="M 521 194 l 6 12 h -12 z" fill="#16324f"/>
<text x="489" y="172" text-anchor="end" font-size="12" font-weight="bold" fill="#16324f">当前 θ</text>
<!-- 二分括号 -->
<g stroke="#e08a1e" stroke-width="1.4" fill="none" stroke-dasharray="5 4">
<path d="M 521 216 C 521 230, 601 230, 601 224" marker-end="url(#dor)"/>
<path d="M 561 216 C 561 227, 581 227, 581 224" marker-end="url(#dor)"/>
</g>
<text x="490" y="292" text-anchor="middle" font-size="12" fill="#b06a10">dynLimit:从当前角向限位方向二分「collideFree」翻转点(tol 0.25°)</text>
<rect x="120" y="304" width="760" height="58" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="490" y="326" text-anchor="middle" font-size="12.5" fill="#3d5164">为什么静态限位不可信:它来自「其它轴全零」的一次性扫描。实测 J2 正向静态 +9.75°(垂臂零位撞立柱),</text>
<text x="490" y="346" text-anchor="middle" font-size="12.5" fill="#3d5164">J1=+90° 时动态边界放到 +108.75° —— 撞立柱这类「跨 ≥2 组」的约束必须按姿态重算,滑块才不锁死。</text>
<!-- solveDyn 流程 -->
<text x="490" y="394" text-anchor="middle" font-size="14.5" font-weight="bold" fill="#1f4f8f">solveDyn:静态解 → 碰撞校验 → 贴边轴放宽重解(最多 2 轮)</text>
<g font-size="11.5">
<rect x="70" y="408" width="150" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<text x="145" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">静态限位求解</text>
<text x="145" y="447" text-anchor="middle" fill="#2a4a6e">solveTo(全流水线)</text>
<line x1="220" y1="433" x2="240" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
<rect x="242" y="408" width="150" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<text x="317" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">全姿态碰撞校验</text>
<text x="317" y="447" text-anchor="middle" fill="#2a4a6e">不安全→候选表选安全解</text>
<line x1="392" y1="433" x2="412" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
<rect x="414" y="408" width="160" height="50" rx="8" fill="#e6eef7" stroke="#5a86b5" stroke-width="1.3"/>
<text x="494" y="429" text-anchor="middle" font-weight="bold" fill="#153a6e">贴边轴检测(0.5°内)</text>
<text x="494" y="447" text-anchor="middle" fill="#2a4a6e">非连续轴 · 非硬边</text>
<line x1="574" y1="433" x2="594" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
<rect x="596" y="408" width="150" height="50" rx="8" fill="#fdf2e2" stroke="#c99418" stroke-width="1.3"/>
<text x="671" y="429" text-anchor="middle" font-weight="bold" fill="#7a5a08">dynLimit 二分边界</text>
<text x="671" y="447" text-anchor="middle" fill="#6b5312">放宽 ≥5° 才值得重解</text>
<line x1="746" y1="433" x2="766" y2="433" stroke="#5a7d9a" stroke-width="1.6" marker-end="url(#da)"/>
<rect x="768" y="408" width="150" height="50" rx="8" fill="#eef8f1" stroke="#1f6f43" stroke-width="1.3"/>
<text x="843" y="429" text-anchor="middle" font-weight="bold" fill="#14472c">放宽后重解(粗档)</text>
<text x="843" y="447" text-anchor="middle" fill="#2c5c44">cost 不升才采用</text>
</g>
<rect x="120" y="482" width="760" height="62" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
<text x="490" y="506" text-anchor="middle" font-size="12.5" fill="#6b5312">滑块夹取 clampAxis:缓存 [轴, 其余轴 1° 量化姿态] → {lo, hi};拖同一轴直接取缓存,</text>
<text x="490" y="526" text-anchor="middle" font-size="12.5" fill="#6b5312">拖过边界才重算一次二分(~40ms)。当前姿已干涉时退回机械区间 —— 否则 dynLimit 会把界外值当边界,滑块再也回不来。</text>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 440" font-family="'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif">
<rect x="0" y="0" width="980" height="440" fill="#ffffff"/>
<defs>
<marker id="zb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#3572c9"/>
</marker>
<marker id="zo" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#e08a1e"/>
</marker>
</defs>
<text x="490" y="34" text-anchor="middle" font-size="21" font-weight="bold" fill="#16324f">工具姿态参数化:方位角 az · 仰角 el · 滚转 roll</text>
<!-- 左:球面示意 -->
<circle cx="240" cy="220" r="130" fill="#f4f8fb" stroke="#b9c8d6" stroke-width="1.4"/>
<ellipse cx="240" cy="220" rx="130" ry="40" fill="none" stroke="#b9c8d6" stroke-width="1" stroke-dasharray="4 3"/>
<line x1="240" y1="220" x2="240" y2="70" stroke="#3572c9" stroke-width="2" marker-end="url(#zb)"/>
<text x="252" y="80" font-size="12.5" fill="#2a4a6e">CAD +Z</text>
<line x1="240" y1="220" x2="400" y2="220" stroke="#8a9aa8" stroke-width="1.4"/>
<text x="406" y="224" font-size="12" fill="#5c7185">方位基线</text>
<!-- 工具轴 -->
<line x1="240" y1="220" x2="296" y2="96" stroke="#e08a1e" stroke-width="2.6" marker-end="url(#zo)"/>
<text x="306" y="94" font-size="12.5" font-weight="bold" fill="#b06a10">工具轴</text>
<text x="306" y="112" font-size="11.5" fill="#b06a10">(法兰 +Z 外伸)</text>
<!-- el 弧 -->
<path d="M 240 128 A 92 92 0 0 1 277 136" fill="none" stroke="#7c5cd6" stroke-width="1.7"/>
<text x="92" y="140" font-size="12.5" fill="#7c5cd6">el(仰角,自 +Z 抬起)</text>
<text x="92" y="158" font-size="12.5" fill="#7c5cd6">0…180°</text>
<line x1="216" y1="136" x2="258" y2="131" stroke="#7c5cd6" stroke-width="1" stroke-dasharray="3 3"/>
<!-- roll 环绕工具轴 -->
<ellipse cx="270" cy="153" rx="20" ry="9" fill="none" stroke="#d64545" stroke-width="1.7" transform="rotate(-24 270 153)"/>
<text x="92" y="196" font-size="12.5" fill="#b03030">roll(绕轴滚转)</text>
<text x="92" y="214" font-size="12.5" fill="#b03030">= θ6,绕工具轴</text>
<line x1="212" y1="198" x2="256" y2="160" stroke="#d64545" stroke-width="1" stroke-dasharray="3 3"/>
<!-- az 弧(水平面内) -->
<path d="M 362 220 A 122 38 0 0 0 330 182" fill="none" stroke="#1f9d63" stroke-width="1.7"/>
<text x="320" y="262" font-size="12.5" fill="#1f7a4d">az(绕 Z 方位角,−180…180°)</text>
<circle cx="240" cy="220" r="4" fill="#16324f"/>
<text x="160" y="248" font-size="12" fill="#5c7185">法兰中心</text>
<!-- 右:公式 -->
<rect x="470" y="70" width="470" height="180" rx="8" fill="#f4f6f8" stroke="#9db0c0" stroke-width="1.2"/>
<text x="488" y="96" font-size="13" font-weight="bold" fill="#16324f">poseMat(az, el, roll) → 工具系旋转矩阵(列 = x/y/z)</text>
<text x="488" y="122" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">z = (sin el·cos az, sin el·sin az, cos el)</text>
<text x="488" y="146" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">x0 = (−sin az, cos az, 0) (子午面基,恒⊥z)</text>
<text x="488" y="170" font-size="12.5" fill="#16324f" font-family="Consolas,monospace">x = R(z, roll)·x0  y = z × x</text>
<text x="488" y="200" font-size="11.5" fill="#5c7185">poseAng() 为其逆分解;el ∈ [0,180],az/roll ∈ (−180,180]。</text>
<text x="488" y="220" font-size="11.5" fill="#5c7185">界面以「工具朝外」描述;求解器内部法兰法向朝臂内,</text>
<text x="488" y="236" font-size="11.5" fill="#5c7185">两者相差绕工具 x 轴 180°(RX180),入栈/出栈各翻一次。</text>
<rect x="470" y="266" width="470" height="140" rx="8" fill="#fdf6ec" stroke="#c99418" stroke-width="1.3"/>
<text x="488" y="292" font-size="13" font-weight="bold" fill="#7a5a08">为什么姿态任务只剩 2 个未知数</text>
<text x="488" y="316" font-size="12" fill="#6b5312">· θ6 只提供绕工具轴的滚转,不改变工具轴 ⇒ 工具轴 = (R1…R5)·ẑ</text>
<text x="488" y="338" font-size="12" fill="#6b5312">· 滚转由 rollClose 闭式补出:θ6 = −atan2(N[1,0], N[0,0]),N = R5ᵀ·Rtar</text>
<text x="488" y="360" font-size="12" fill="#6b5312">· 推论:全姿态偏差 oerr ≥ 工具轴夹角(20 万随机样本验证零违反)——</text>
<text x="488" y="380" font-size="12" fill="#6b5312"> 粗扫用一次点积预筛(1−cos2°)挡掉必被拒的候选,省掉矩阵链+acos</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 650 KiB

File diff suppressed because one or more lines are too long
+44 -44
View File
File diff suppressed because one or more lines are too long
+31053
View File
File diff suppressed because it is too large Load Diff
+17 -6
View File
@@ -6,25 +6,36 @@
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(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 通孔周 |
| P01_A_立柱下段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 顶端榫接区 + Ø3.9 对锁孔周 |
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + Ø3.9 对锁孔周 |
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 底端榫接区 + Ø3.9 对锁孔周 + 顶面安装孔周 |
| 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 电机座接触面 |
| P03_肩吊臂_螺旋体35%_墙5底5_竖打 | 螺旋体 35% | 5 / 5 | 竖打 | 5x Ø3.9 安装孔周 + 后法兰 4xM3 沉头孔周 + 前锁紧法兰孔周 |
| 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 | 竖打 | — |
| P09_工具法兰_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | — |
## 通用规则
1. **打印方向**:全部按模型姿态打,弯曲发生在层面内,弯矩交给顶/底实心层和墙,不让层间缝扛拉。
2. **螺纹孔一律不切螺纹**:M3/M4/M5 全部用黄铜热熔螺母;孔周按 1.5~2 倍螺栓直径加载实心 Modifier。P02 力臂的 M5 螺母沉槽同此处理。
2. **不用热熔铜螺母、不需要攻丝、也不打沉孔/过孔**:全部连接螺钉统一为 **M4×16**,每个螺钉位**只打一个 Ø3.9 直孔**——M4×16 直接用扳手拧入,螺钉自己攻出螺纹(顶部 4 颗穿鞍座 9mm 后吃进立柱顶环 7mm;横向 1 颗穿 11mm 后壁吃进榫头 4.7mm;分段缝 8 颗穿母口壁+公榫壁共 10.5mm)。竖向载荷由承压面承担(顶部 2403mm²、分段缝榫座面),螺钉只防上抬/防滑。孔周按 1.5~2 倍螺栓直径加载实心 Modifier。
3. **墙数和顶底壳优先于密度**:抗弯主力是墙+顶底实心层,填充件是剪力腹板。密度只在小区域加(关节毂 30~35%),不要全局堆高。
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
P03 已改薄壁桁架(双侧 6mm 竖板 + 帐篷减重窗 + 9mm 鞍座 + 12mm 中位龙骨筋):强度由 **墙层数** 决定,
35% 只作用在很薄的内部芯上;想再压重量可把 P03 单独降到 螺旋体 20%(≈省 15~20g,强度不受影响)。
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
6. 立柱 P01 三段用三角形是唯一例外:轴压方向已知,Z 向斜撑跨层传力比螺旋体更对口。
## 3MF 交付(out/parts_3mf/)
- 13 个 3MF 已按本表参数从**当前模型**(`out/print_stl/`,随 `python src/arm_model.py` 更新)导出。P01_A 也一并重导:你 09-28 手作的原件网格比现模型少 780 个三角(立柱下段后续改过),已备份到 `.bak/P01_A_手作参考原件_20260928.3mf`。网格按模型姿态、板中心放置,缩略图已剔除避免旧预览误导。
- 打印机/耗材等基准 profile(537 键)从你的原件抽出后固化在 `docs/bambu_profile_reference.json`,生成脚本读它,不再依赖 `out/` 里的二进制。
- 生成的 3MF 严格按本表:注意你的参考文件 `layer_height=0.28`、顶壳 4 层,本表要求 **0.20 / 顶 5**,新文件已按参数单执行。
- **局部实心区(Modifier)未烘进 3MF**:Bambu modifier schema 已确认可用,但各区几何需逐件定义;在 Bambu 里对对象右键→添加修饰器→覆盖 `sparse_infill_density=100%` 即可(区位见上表)。需要我烘进去说一声。
- 重新生成:`python src/arm_model.py`(刷新 out/print_stl)→ `python scratch/make_3mf.py`;闭环校验:`python scratch/verify_3mf.py`(逐项断言本表参数 + 件高)。
+120 -100
View File
@@ -1,102 +1,122 @@
[
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
},
{
"id": "S04",
"规格": "M3 x 8",
"螺纹": "M3",
"长度mm": 8,
"头型": "沉头内六角",
"数量": 4,
"位置": "P03 前置止脱锁紧法兰 -> M1 电机前端面",
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°)"
},
{
"id": "S05",
"规格": "M4 x 25",
"螺纹": "M4",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗",
"备注": "Ø4.4 通孔,非对称偏心孔位物理防反装;需另配 M4 螺母 4 枚"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "沉头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M5 x 35",
"螺纹": "M5",
"长度mm": 35,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 肩吊臂安装台 -> P01_C 立柱顶面",
"备注": "Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm"
},
{
"id": "S10",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
}
{
"id": "S01",
"规格": "M2 x 10",
"螺纹": "M2",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 18,
"位置": "J1~J6 输出毂:P04/P05/P06/P07/P08/P09 各 3 颗",
"备注": "Ø2.8 通孔 + Ø5.6 沉孔,BCD 29(3 孔均布),旋入电机输出毂螺纹孔(深 6mm)"
},
{
"id": "S02",
"规格": "M3 x 10",
"螺纹": "M3",
"长度mm": 10,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),BCD 55;装配时配合平垫吸收电机公差,与前端压盖双向夹死"
},
{
"id": "S03",
"规格": "M3 x 16",
"螺纹": "M3",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm),BCD 55,轴向锁死防拉脱"
},
{
"id": "S04",
"规格": "M3 x 8",
"螺纹": "M3",
"长度mm": 8,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm),BCD 55,咬合电机前端面 3.5mm"
},
{
"id": "S05",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 8,
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
"备注": "统一 Ø3.9 直孔自攻成型,非对称孔位防反装"
},
{
"id": "S06",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
"备注": "Ø4.4 通孔 + Ø7.0 圆柱沉孔(深 4mm,头平齐),BCD 44 均布 -> 末端工具/夹爪"
},
{
"id": "S07",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装"
},
{
"id": "S08",
"规格": "M5 x 25",
"螺纹": "M5",
"长度mm": 25,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P02 前/后力臂 -> 中核心:前臂 2 颗 + 后臂 2 颗",
"备注": "Ø5.5 通孔 + Ø10 沉孔;需另配 M5 六角螺母 4 枚(嵌入螺母沉槽)"
},
{
"id": "S09",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 4,
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
"备注": "统一 Ø3.9 直孔自攻成型(从上方拧入),防上抬"
},
{
"id": "S11",
"规格": "M4 x 16",
"螺纹": "M4",
"长度mm": 16,
"头型": "圆柱头内六角",
"数量": 1,
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
"备注": "Ø3.9 直孔贯穿 11mm 后壁 + 榫头,从后方自攻横向锁紧防脱"
},
{
"id": "S10",
"规格": "M6 x 35",
"螺纹": "M6",
"长度mm": 35,
"头型": "外六角",
"数量": 6,
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
},
{
"id": "S12",
"规格": "M3 x 8",
"螺纹": "M3",
"长度mm": 8,
"头型": "圆柱头内六角",
"数量": 20,
"位置": "P04/P05/P06/P07/P08 前端压盖 -> M2/M3/M4/M5/M6 电机前端盖(J2/J3/J4/J5/J6 各 4 颗)",
"备注": "Ø3.4 通孔 + Ø6.0 圆柱沉孔(深 3.0mm,头全沉入),BCD 55;与后端安装板 S02 配合双向夹紧电机"
}
]
+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1150" height="760" viewBox="0 0 1150 760">
<rect width="1150" height="760" fill="#15181f"/>
<rect x="400.0" y="154.0" width="396.0" height="396.0" fill="#1d4ed8" stroke="#9dc4ff" stroke-width="2.0"/>
<rect x="466.0" y="220.0" width="264.0" height="264.0" fill="#12161d" stroke="#6f86b8" stroke-width="1.4"/>
<rect x="488.8" y="484.0" width="26.4" height="66.0" fill="#243043" stroke="#ffd166" stroke-width="1.4"/>
<rect x="488.8" y="154.0" width="26.4" height="66.0" fill="#243043" stroke="#96dcff" stroke-width="1.4"/>
<rect x="484.0" y="460.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
<rect x="484.0" y="220.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
<rect x="488.8" y="400.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
<rect x="488.8" y="154.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
<rect x="481.0" y="550.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
<rect x="481.0" y="130.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
<rect x="716.8" y="484.0" width="26.4" height="66.0" fill="#243043" stroke="#ffd166" stroke-width="1.4"/>
<rect x="716.8" y="154.0" width="26.4" height="66.0" fill="#243043" stroke="#96dcff" stroke-width="1.4"/>
<rect x="712.0" y="460.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
<rect x="712.0" y="220.0" width="36.0" height="24.0" fill="#7d8794" stroke="#c3cad6" stroke-width="1.2"/>
<rect x="716.8" y="400.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
<rect x="716.8" y="154.0" width="26.4" height="150.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.0"/>
<rect x="709.0" y="550.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
<rect x="709.0" y="130.0" width="42.0" height="24.0" fill="#c3cad6" stroke="#ffffff" stroke-width="1.2"/>
<text x="598.0" y="360.0" font-size="22" fill="#5f7396" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">内腔 44x44</text>
<text x="598.0" y="558.0" font-size="17" fill="#ffd166" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">前 -Y y=95</text>
<text x="598.0" y="158.0" font-size="17" fill="#96dcff" text-anchor="middle" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">后 +Y y=161</text>
<path d="M481.0,562.0 L150.0,562.0" stroke="#8fa4c8" stroke-width="1.2"/>
<text x="10.0" y="554.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">M4x25 前面一颗</text>
<text x="10.0" y="574.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">头 y=91..95 外露 4mm · 杆到 y=120</text>
<path d="M484.0,472.0 L150.0,472.0" stroke="#8fa4c8" stroke-width="1.2"/>
<text x="10.0" y="464.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">前螺母贴前壁内侧</text>
<text x="10.0" y="484.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">y=106..110 · 压紧前侧 10.5mm 搭接壁</text>
<path d="M515.2,352.0 L150.0,352.0" stroke="#8fa4c8" stroke-width="1.2"/>
<text x="10.0" y="344.0" font-size="16" fill="#96dcff" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">两端杆头净空 16.2</text>
<text x="10.0" y="364.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">前 120 / 后 136 · 互不碰头</text>
<path d="M484.0,232.0 L150.0,232.0" stroke="#8fa4c8" stroke-width="1.2"/>
<text x="10.0" y="224.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">后螺母贴后壁内侧</text>
<text x="10.0" y="244.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">y=146..150 · 压紧后侧 10.5mm 搭接壁</text>
<path d="M481.0,142.0 L150.0,142.0" stroke="#8fa4c8" stroke-width="1.2"/>
<text x="10.0" y="134.0" font-size="16" fill="#d9e4f7" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">M4x25 背面一颗</text>
<text x="10.0" y="154.0" font-size="13" fill="#8fa4c8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">头 y=161..165 · 杆到 y=136</text>
<text x="12.0" y="30.0" font-size="21" fill="#e8eefc" text-anchor="start" font-weight="700" font-family="Consolas,Microsoft YaHei,monospace">P01 立柱分段缝 · 两侧对锁(M4x25 前后各一颗,共 8 颗)· 俯视剖面</text>
<text x="12.0" y="52.0" font-size="14" fill="#9fb4d8" text-anchor="start" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">孔位 x=-131/-93 与 Ø4.4 通孔均未改动,与已打印的 A/B 段完全一致;偏心差 6mm 仍作防反装防呆</text>
<text x="1138.0" y="746.0" font-size="13" fill="#6f86b8" text-anchor="end" font-weight="400" font-family="Consolas,Microsoft YaHei,monospace">单位 mm · 6px/mm · 数据取自 web/mesh_arm.json 的 S05 实例</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Some files were not shown because too many files have changed in this diff Show More