Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0efb44368 | ||
|
|
c754521bde | ||
|
|
d8f6c69406 | ||
|
|
e3b25452e1 | ||
|
|
b593e47c76 | ||
|
|
6fe97fc643 | ||
|
|
208b37048b | ||
|
|
4fdc37da9f | ||
|
|
a70724e8a9 | ||
|
|
c0c78251b5 |
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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° 连续旋转(全对全动态扫描验证)"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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° 连续旋转(全对全动态扫描验证)"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -8,6 +8,13 @@ out/
|
|||||||
# 临时调试脚本与截图
|
# 临时调试脚本与截图
|
||||||
scratch/
|
scratch/
|
||||||
|
|
||||||
|
# 单文件查看器优化前的完整备份(带版本标识,勿随手删)
|
||||||
|
backups/
|
||||||
|
|
||||||
|
# 重构前的源码快照与脚本自动备份(git 历史里已有旧版本,不重复入库)
|
||||||
|
.bak/
|
||||||
|
*.bak_*
|
||||||
|
|
||||||
# 编辑器 / 系统文件
|
# 编辑器 / 系统文件
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
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)
|
||||||
@@ -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 这一档里找原因。
|
||||||
|
**参数化几何里,"某段料消失"几乎总是某个半径/长度取值的函数,不是布尔残留。**
|
||||||
@@ -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 → 环墙+凸缘必然围住前端盖
|
||||||
|
两次的正确解法都不是"多切一点",而是**改那个取值**;而且改动前要先问清楚
|
||||||
|
删的是哪一圈 —— 删整圈会同时删掉压盖唯一的支撑面。
|
||||||
@@ -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 是否互不相同**。
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
## 🌟 核心设计特性
|
## 🌟 核心设计特性
|
||||||
|
|
||||||
1. **一体化直锁电机**:全面取消外挂独立端面齿联轴器,所有关节由 3×M2 沉头螺钉直锁电机输出轴,轴向尺寸大幅压缩,刚度显著提升。
|
1. **一体化直锁电机**:全面取消外挂独立端面齿联轴器,所有关节由 3×M2 沉头螺钉直锁电机输出轴,轴向尺寸大幅压缩,刚度显著提升。
|
||||||
2. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
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. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
3. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
||||||
4. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
4. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
||||||
|
5. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,7 +42,18 @@ python src/sweep_v2.py
|
|||||||
├── 机器人手臂3D查看器.html # 单文件内置 3D 查看器(双击即用)
|
├── 机器人手臂3D查看器.html # 单文件内置 3D 查看器(双击即用)
|
||||||
├── README.md # 本说明文档
|
├── README.md # 本说明文档
|
||||||
├── src/ # 核心源码目录
|
├── 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 # ⭐ 权威动态碰撞检测与行程扫描工具
|
│ ├── sweep_v2.py # ⭐ 权威动态碰撞检测与行程扫描工具
|
||||||
│ ├── arm_build.py # 底层几何基础库与通用装配/导出函数
|
│ ├── arm_build.py # 底层几何基础库与通用装配/导出函数
|
||||||
│ ├── build_viewer.py # 3D 查看器打包生成脚本
|
│ ├── build_viewer.py # 3D 查看器打包生成脚本
|
||||||
@@ -51,6 +63,12 @@ python src/sweep_v2.py
|
|||||||
│ ├── finalize_travel.py # 行程数据维护(从 sweep 明细重建 travel.json)
|
│ ├── finalize_travel.py # 行程数据维护(从 sweep 明细重建 travel.json)
|
||||||
│ ├── strength_check.py # 刚度与应力分析
|
│ ├── strength_check.py # 刚度与应力分析
|
||||||
│ └── torque_balance.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/ # 权威数据与渲染截图(历史报告已清理)
|
├── docs/ # 权威数据与渲染截图(历史报告已清理)
|
||||||
│ ├── travel.json # 各关节实测行程与硬限位(sweep_v2.py 扫描生成)
|
│ ├── travel.json # 各关节实测行程与硬限位(sweep_v2.py 扫描生成)
|
||||||
│ ├── travel_detail.json # 逐对扫描明细与组合折叠约束
|
│ ├── travel_detail.json # 逐对扫描明细与组合折叠约束
|
||||||
@@ -69,15 +87,89 @@ python src/sweep_v2.py
|
|||||||
|
|
||||||
| 构件编号 | 构件名称 | 关联驱动关节 | 设计说明 |
|
| 构件编号 | 构件名称 | 关联驱动关节 | 设计说明 |
|
||||||
| :--- | :--- | :---: | :--- |
|
| :--- | :--- | :---: | :--- |
|
||||||
| **P01** | 胸板 / 安装板 | 根基固定 | 12mm 基础基座,预留 4x M4 紧固位与躯干走线窗 |
|
| **P01** | 立柱(三段 A/B/C) | 根基固定 | 66×66mm 三角截面立柱,分段榫接 + M4 自攻对锁孔防反装;顶端为 P03 安装台 |
|
||||||
| **P02** | 底脚 / 支撑座 | 根基固定 | 200×320mm 重型防倾覆底盘,提供稳固调试基座 |
|
| **P02** | 底盘(核心座 + 前/后防翻力臂) | 根基固定 | 195×190mm 核心承力座 + 48mm 套筒 + 前后三角加强筋;前后防翻力臂带 M5 预埋螺母 |
|
||||||
| **P03** | 肩吊臂 / 肩机舱 | J1 驱动抱紧 | 仿生规整风道机舱,带 7 组轴向散热格栅,抱紧 M1 电机 |
|
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
|
||||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
|
| **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,后侧刚性抱紧 M3 |
|
| **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 肘电机 |
|
| **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 |
|
| **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,立式抱紧 M6 腕旋转电机 |
|
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5(J5 法兰盘背面后移到 Y=162,给 P07 前端压盖让位,J5 凸台同步加长),立式抱紧 M6 腕旋转电机;**M6 前端定位环只切敞口那半圈**(尚无 M6 前端压盖,环是光板,见下)。壁厚峰值 8.3mm,本就是薄壳 |
|
||||||
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 40mm) |
|
| **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.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)(构建自动生成)
|
- [零件明细与质量属性 bom.json](file:///g:/Robot_v2/机器人手臂/docs/bom.json)(构建自动生成)
|
||||||
- [静态干涉检测结果 collision.json](file:///g:/Robot_v2/机器人手臂/docs/collision.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/ 目录):
|
- 🔬 **测试与校验工具**(src/ 目录):
|
||||||
- sweep_v2.py — 权威动态碰撞检测与行程扫描
|
- 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`(角加速度)。
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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|<1e-7、det<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 |
@@ -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 采样)落入对方实体 >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">实体交体积 > 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 |
@@ -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">球心距 > 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) < 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 |
@@ -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 |
|
After Width: | Height: | Size: 650 KiB |
@@ -6,25 +6,36 @@
|
|||||||
|
|
||||||
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(Modifier) |
|
| 件号 | 图案 / 密度 | 墙 / 顶底 | 打印姿态 | 局部实心区(Modifier) |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| P01_A_立柱下段_三角形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 | 竖打 | 两端榫接区 + M4 通孔周 |
|
| P01_B_立柱中段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + Ø3.9 对锁孔周 |
|
||||||
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 两端榫接区 + M4 通孔周 |
|
| P01_C_立柱上段_三角形30%_墙4底5_竖打 | 三角形 30% | 4 / 5 | 竖打 | 底端榫接区 + Ø3.9 对锁孔周 + 顶面安装孔周 |
|
||||||
| P02_Center_底座中核心_螺旋体30%_墙5底6_平打 | 螺旋体 30% | 5 / 6 | 平打 | Ø48 套筒 + 12mm 阶梯榫座 |
|
| P02_Center_底座中核心_螺旋体30%_墙5底6_平打 | 螺旋体 30% | 5 / 6 | 平打 | Ø48 套筒 + 12mm 阶梯榫座 |
|
||||||
| P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
| P02_Front_前向防翻力臂_螺旋体30%_墙4底5_平打 | 螺旋体 30% | 4 / 5 | 平打 | M5 螺母沉槽区 |
|
||||||
| P02_Rear_后向防翻力臂_螺旋体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 输出凸台贯通区(整段实心) |
|
| P04_肩叉_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | J1 输出凸台贯通区(整段实心) |
|
||||||
| P05_大臂旋转座_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | J2 接口孔周 |
|
| P05_大臂旋转座_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | J2 接口孔周 |
|
||||||
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
|
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
|
||||||
| P07_小臂骨架_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | 根部加厚顶板区 + M5 电机半包座 |
|
| P07_小臂骨架_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | 根部加厚顶板区 + M5 电机半包座 |
|
||||||
| P08_腕偏转座_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | J5 接口孔周 |
|
| P08_腕偏转座_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | J5 接口孔周 |
|
||||||
| P09_工具法兰_螺旋体100%_墙4底5_竖打 | 螺旋体 100% | 4 / 5 | 竖打 | — |
|
| P09_工具法兰_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | — |
|
||||||
|
|
||||||
## 通用规则
|
## 通用规则
|
||||||
|
|
||||||
1. **打印方向**:全部按模型姿态打,弯曲发生在层面内,弯矩交给顶/底实心层和墙,不让层间缝扛拉。
|
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%),不要全局堆高。
|
3. **墙数和顶底壳优先于密度**:抗弯主力是墙+顶底实心层,填充件是剪力腹板。密度只在小区域加(关节毂 30~35%),不要全局堆高。
|
||||||
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
|
4. **层高 0.2mm 不变**;P03/P04/P07 受力最狠,墙给 5 层,其余 4 层。
|
||||||
|
P03 已改薄壁桁架(双侧 6mm 竖板 + 帐篷减重窗 + 9mm 鞍座 + 12mm 中位龙骨筋):强度由 **墙层数** 决定,
|
||||||
|
35% 只作用在很薄的内部芯上;想再压重量可把 P03 单独降到 螺旋体 20%(≈省 15~20g,强度不受影响)。
|
||||||
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
|
5. BOM 质量按 PLA 折算;换 PETG/ASA 参数不变,层间强度只会更好。
|
||||||
6. 立柱 P01 三段用三角形是唯一例外:轴压方向已知,Z 向斜撑跨层传力比螺旋体更对口。
|
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`(逐项断言本表参数 + 件高)。
|
||||||
|
|
||||||
|
|||||||
@@ -17,47 +17,47 @@
|
|||||||
"头型": "圆柱头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 20,
|
"数量": 20,
|
||||||
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
||||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4 螺纹孔 (±17.55, ±20.46)"
|
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),BCD 55;装配时配合平垫吸收电机公差,与前端压盖双向夹死"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S03",
|
"id": "S03",
|
||||||
"规格": "M3 x 16",
|
"规格": "M3 x 16",
|
||||||
"螺纹": "M3",
|
"螺纹": "M3",
|
||||||
"长度mm": 16,
|
"长度mm": 16,
|
||||||
"头型": "沉头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 4,
|
||||||
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
||||||
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔距 35.1 x 40.9,轴向锁死防拉脱"
|
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm),BCD 55,轴向锁死防拉脱"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S04",
|
"id": "S04",
|
||||||
"规格": "M3 x 8",
|
"规格": "M3 x 8",
|
||||||
"螺纹": "M3",
|
"螺纹": "M3",
|
||||||
"长度mm": 8,
|
"长度mm": 8,
|
||||||
"头型": "沉头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 4,
|
||||||
"位置": "P03 前置止脱锁紧法兰 -> M1 电机前端面",
|
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||||
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°)"
|
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm),BCD 55,咬合电机前端面 3.5mm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S05",
|
"id": "S05",
|
||||||
"规格": "M4 x 25",
|
"规格": "M4 x 16",
|
||||||
"螺纹": "M4",
|
"螺纹": "M4",
|
||||||
"长度mm": 25,
|
"长度mm": 16,
|
||||||
"头型": "圆柱头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 8,
|
||||||
"位置": "P01 立柱分段对锁:A-B 缝 2 颗 + B-C 缝 2 颗",
|
"位置": "P01 立柱分段对锁:A-B 缝 4 颗 + B-C 缝 4 颗(每缝前后各 2 颗)",
|
||||||
"备注": "Ø4.4 通孔,非对称偏心孔位物理防反装;需另配 M4 螺母 4 枚"
|
"备注": "统一 Ø3.9 直孔自攻成型,非对称孔位防反装"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S06",
|
"id": "S06",
|
||||||
"规格": "M4 x 16",
|
"规格": "M4 x 16",
|
||||||
"螺纹": "M4",
|
"螺纹": "M4",
|
||||||
"长度mm": 16,
|
"长度mm": 16,
|
||||||
"头型": "沉头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 4,
|
||||||
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
||||||
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
|
"备注": "Ø4.4 通孔 + Ø7.0 圆柱沉孔(深 4mm,头平齐),BCD 44 均布 -> 末端工具/夹爪"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S07",
|
"id": "S07",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"头型": "圆柱头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 4,
|
||||||
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
|
"位置": "P02 底座中核心 -> P01 立柱底部榫头",
|
||||||
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装,轴向力由榫座面承载"
|
"备注": "Ø5.5 通孔 + Ø10 沉孔(深 6mm),非对称孔位防反装"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S08",
|
"id": "S08",
|
||||||
@@ -81,13 +81,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S09",
|
"id": "S09",
|
||||||
"规格": "M5 x 35",
|
"规格": "M4 x 16",
|
||||||
"螺纹": "M5",
|
"螺纹": "M4",
|
||||||
"长度mm": 35,
|
"长度mm": 16,
|
||||||
"头型": "圆柱头内六角",
|
"头型": "圆柱头内六角",
|
||||||
"数量": 4,
|
"数量": 4,
|
||||||
"位置": "P03 肩吊臂安装台 -> P01_C 立柱顶面",
|
"位置": "P03 肩吊臂 -> P01_C 立柱顶面(从腔体上方往下拧)",
|
||||||
"备注": "Ø5.5 通孔,穿过 15mm 凸台旋入立柱 20mm"
|
"备注": "统一 Ø3.9 直孔自攻成型(从上方拧入),防上抬"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "S11",
|
||||||
|
"规格": "M4 x 16",
|
||||||
|
"螺纹": "M4",
|
||||||
|
"长度mm": 16,
|
||||||
|
"头型": "圆柱头内六角",
|
||||||
|
"数量": 1,
|
||||||
|
"位置": "P01_C 立柱后壁 -> P03 榫头(横向防脱锁紧)",
|
||||||
|
"备注": "Ø3.9 直孔贯穿 11mm 后壁 + 榫头,从后方自攻横向锁紧防脱"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "S10",
|
"id": "S10",
|
||||||
@@ -98,5 +108,15 @@
|
|||||||
"数量": 6,
|
"数量": 6,
|
||||||
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
|
"位置": "底座 6 组 28x7 紧固腰槽 -> 工作台/桌面",
|
||||||
"备注": "需另配 M6 螺母 + 平垫圈各 6(建议加弹垫防松)"
|
"备注": "需另配 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 配合双向夹紧电机"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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 |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 193 KiB |