feat(arm): 电机双向夹紧与2mm厚度公差让位,立柱减重槽及文档同步更新
@@ -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° 连续旋转(全对全动态扫描验证)"
|
||||
}
|
||||
]
|
||||
@@ -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 是否互不相同**。
|
||||
@@ -7,9 +7,10 @@
|
||||
## 🌟 核心设计特性
|
||||
|
||||
1. **一体化直锁电机**:全面取消外挂独立端面齿联轴器,所有关节由 3×M2 沉头螺钉直锁电机输出轴,轴向尺寸大幅压缩,刚度显著提升。
|
||||
2. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
||||
3. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
||||
4. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
||||
2. **电机前端双向夹紧 + 2mm 厚度公差**:每个关节的电机由「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」前后两面夹死;后端板承压面按 `MOTOR_TOL=2.0` 让空成**尾端垫片位**(`armcore.MOTOR_TOL` / `arm_build.REAR_SEAT`),装配时在电机尾端面与承压面之间叠 0~2.6mm 的 M3 平垫,**逐台电机按实测长度配垫**(T = 58.6 − 电机实际长度 L),让前端盖正好落到压盖面上。
|
||||
3. **五级流线圆角系统**:建立统一的有机曲面过渡规范(R=1.5mm 微圆角 ~ R=15mm 视觉主导大圆角),彻底消除锐边与机械生硬台阶。
|
||||
4. **全行程动态零干涉**:经 `sweep_v2.py` 多进程全域扫描与二分逼近验证,各自由度极限角度下实现 0 碰撞。
|
||||
5. **100% 流形单实体**:9/9 构件均为无缝单一实体拓扑,完全杜绝浮动壳体与布尔破碎,满足 FDM/SLA 打印及 CNC 加工要求。
|
||||
|
||||
---
|
||||
|
||||
@@ -72,14 +73,59 @@ python src/sweep_v2.py
|
||||
| **P01** | 立柱(三段 A/B/C) | 根基固定 | 66×66mm 三角截面立柱,分段榫接 + M4 自攻对锁孔防反装;顶端为 P03 安装台 |
|
||||
| **P02** | 底盘(核心座 + 前/后防翻力臂) | 根基固定 | 195×190mm 核心承力座 + 48mm 套筒 + 前后三角加强筋;前后防翻力臂带 M5 预埋螺母 |
|
||||
| **P03** | 肩吊臂 / 肩关节安装悬臂 | J1 驱动抱紧 | 轻量化板式桁架:双侧 6mm 竖板 + 帐篷减重窗(≥52°自支撑)+ 9mm 承力鞍座 + Ø15 螺母柱,抱紧 M1 电机(实心 728→286cm³,-61%,外轮廓不变) |
|
||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2 |
|
||||
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2,后侧刚性抱紧 M3 |
|
||||
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机 |
|
||||
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5 |
|
||||
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5,立式抱紧 M6 腕旋转电机 |
|
||||
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 40mm) |
|
||||
| **P04** | 肩叉 | J1 ➔ J2 | 90° 一体化高刚度正交转向肘,直锁 M1,抱紧 M2;**M2 前端压盖**(R35 / Y155.5~161):电机前端盖再加 4×M3x8(BCD55)夹一次,与后端板 4×M3x10 前后双向面压,消掉大扭矩下电机在座孔里的倾斜;**前端围挡只切敞口那半圈**(见下);**立柱减重槽**(X23~35 / Y102~154 / Z525↑ 敞开):立柱由实心变 U 形槽钢,361→289cm³,抗弯靠两侧 X 向腹板(z=560 截面 I 207370→199777,降 3.6%,而面积降 28%) |
|
||||
| **P05** | 大臂旋转座 | J2 ➔ J3 | 16 层 Loft 曲面平滑过渡,直锁 M2(J2 法兰盘背面后移到 Y=162,给 P04 前端压盖让位,J2 凸台同步加长以保住 S01 咬合),后侧刚性抱紧 M3;**M3 前端压盖**(R35 / Z 384.0~389.5,4×M3x8 BCD55):M3 是 roll 电机(轴向 -Z),压盖与后端板 4×M3x10 前后双向面压;代价是对侧 P06 的 J3 毂面后移 5.5mm 让位 |
|
||||
| **P06** | 大臂骨架 | J3 ➔ J4 | 流线型工字背脊骨架,直锁 M3,下端抱紧 M4 肘电机;**M4 前端压盖** 4×M3x8(BCD55),同 P04 配方;**前端围挡只切敞口那半圈**(见下);**J3 毂面后移 5.5mm**(顶端 388.5→383.0,直锁凸台 59.5→65.0):给 P05 的 M3 前端压盖让位,留 1.0mm 动间隙——与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源,换算式同为「压盖 5.0 + 动间隙 1.0 − 原基准 0.5」;凸台同步加长,3×S01 输出螺钉咬合量与 J3 直锁关系一律不变 |
|
||||
| **P07** | 小臂骨架 | J4 ➔ J5 | L 型圆弧强化立柱,大范围自由运动避让,抱紧 M5;**M5 前端压盖** 4×M3x8(BCD55);**前端围挡只切敞口那半圈**(本件环壁 r32.25~35,因 2.0mm 关节间隙而更薄,见下);J4 从动侧(法兰盘/立柱/圆弧加强筋)整体后移到 Y=162 给 P06 压盖让位,J4 凸台同步加长;**顶板减重槽**(X−3~43 / Y103~153 / Z255↑ 敞开):288→230cm³,Y 上限取 153 以避开 y_hub_in=158.5 起的竖直立柱本体(那是 J4 从动法兰的承力柱) |
|
||||
| **P08** | 腕偏转座 | J5 ➔ J6 | 16 层 Loft 双曲面过渡,直锁 M5(J5 法兰盘背面后移到 Y=162,给 P07 前端压盖让位,J5 凸台同步加长),立式抱紧 M6 腕旋转电机;**M6 前端定位环只切敞口那半圈**(尚无 M6 前端压盖,环是光板,见下)。壁厚峰值 8.3mm,本就是薄壳 |
|
||||
| **P09** | 工具法兰 | J6 输出 | 直锁 M6 输出轴,标准工业 ISO 法兰(4x M4 螺孔 BCD 44mm);**填充 100%→30%**:该截面安全系数 288,是全机裕度最大的件,满填充纯属浪费 |
|
||||
| **M1~M6** | 关节电机模组 ×6 | 各关节驱动 | Ø60 方形标准减速电机模组(63.6mm 方形尾盖),210 g/台,整机 1260 g |
|
||||
|
||||
> ⚠️ **电机装配必须配垫片(厚度公差 2mm)**:各台 Ø60 电机的轴向长度实测差可达 2mm。
|
||||
> 这条轴向链是「后端板 4×M3x10 + 前端 R35 压盖 4×M3x8」两个刚性夹持面,螺钉头一旦坐死
|
||||
> 在沉孔台阶上就不能再进给,所以公差**不能靠拧紧消化**,只能由后端板的承压面让空出来的
|
||||
> 垫片位吸收(`armcore.MOTOR_TOL = 2.0`,`arm_build.REAR_SEAT = 0.6 + 2.0 = 2.6`)。
|
||||
> 装配顺序:① 后端板承压面上叠 **T = 58.6 − 电机实测长度 L** 的 M3 平垫(0.5/1.0/1.5 组合,
|
||||
> 标称电机约 0.6mm,量程 0~2.6mm);② 电机推入到尾端面贴住垫片;③ 前端压盖 4×M3x8 拧紧,
|
||||
> 此时前端盖正好落在压盖面上(0.0 面压);④ 后端板 4×M3x10 拧紧夹死垫片。
|
||||
> **不做第①步的后果**:电机长 1mm 以上就装不进去(前端盖越位 1mm 撞压盖本体),
|
||||
> 电机短 1mm 以上压盖就压不上、只剩后端板单面锁死,大扭矩下电机照样点头。
|
||||
> 校核脚本:`scratch/motor_tol_window.py`(窗口换算)+ `scratch/motor_tol_check.py`
|
||||
> (真实电机包络布尔验证 5 个夹紧关节 × ±2mm,全部零干涉)。
|
||||
> 另:J1/M1(P03 侧开式电机舱)本身就是 Ø60.7 直通长孔、比电机长 3mm,天然覆盖该公差,无需配垫。
|
||||
>
|
||||
> **承压面为什么是 R35 圆面而不是 64.4 方腔**(2026-10-04 整改):承压面下沉 2.6mm 后,
|
||||
> 如果承压面段仍用 64.4 **方**腔,方腔壁以外 `z=-2.6..0` 那圈 R35 料就相对新承压面
|
||||
> 凸起 2.6mm —— P04~P08 后端整圈「悬空凸台」,3D 打印时是悬空薄壁(要支撑、公差难保)。
|
||||
> 现在承压面段用 **R35 圆腔**(`arm_build.SEAT_R`),整圈一次挖到承压面高度,凸台零残留。
|
||||
> `SEAT_R` 必须**等于**外壁 R35:只要小于 R35,刀半径与外壁之间那圈环墙就必然保留并相对
|
||||
> 承压面凸起(P08 实测 1536.9mm³)—— 这就是用户 2026-10-05 在**尾端**指出的"围挡",
|
||||
> 是取值的必然产物而不是残留。连带好处:承压环从「1.95mm 窄环」变成「完整 R35 圆环 4.75mm」,
|
||||
> **受压面积 383 → 974 mm²(2.5 倍)**,夹紧更实。`SEAT_TOP=+1.0` 越过电机尾端面 1mm,
|
||||
> 吃掉半包开口处的 0.052cm³ 残片(取 0 时该残片会脱空成独立碎块)。
|
||||
>
|
||||
> **前端那一圈"围挡"(2026-10-05e,全机 5 处)**:用户 2026-10-05 先在 J3/M3 指出**电机
|
||||
> 另一头(前端)也有一圈围挡**,随后确认其余四处(M2/M4/M5/M6)完全同构。围挡本体不是残留料,
|
||||
> 而是 `front_collar_pocket` 的必然产物:它把座孔从 R30.15 扩到 R30.25,于是留下那圈
|
||||
> `r30.25..35`、6mm 高的环墙,比电机前端盖 Ø60.3 凸出 4.75mm;J2/J3/J4/J5 上还骑着 R35 前端
|
||||
> 压盖(Ø70 再凸 3.2mm)。我第一版把整个前端(定位环 + 压盖 + 腔口下沿)都切了 → **过度**:
|
||||
> 电机前端没了支撑面、M3 退回单面锁死,用户当场驳回。
|
||||
>
|
||||
> **现全机 5 处统一只切敞口那一半**(开关 `arm_model.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 是固定测试台架,不是机器人本体**。它在扫描中代表机械臂与躯干的连接
|
||||
> 界面——**臂撞立柱 = 臂撞躯干,是真约束,碰撞计算必须包含它**。但台架的位置与截面
|
||||
> 是一个假设,所以躯干侧的工作空间只和这个假设一样准。要改的是"真机躯干占位"这个
|
||||
@@ -92,9 +138,18 @@ python src/sweep_v2.py
|
||||
> 2.0 N·m;带 0.5 kg 末端负载时 J1 达 6.14 N·m(307%)。详见 `docs/torque_report.md`。
|
||||
> 结论对电机额定参数敏感,该参数目前为工程假定(无数据手册),可用 `ARMT_CONT` 覆盖重算。
|
||||
>
|
||||
> 相反地,**结构本身不是瓶颈**:12 个剖面全部合格,最差的 P03 吊臂根座安全系数 8.93,
|
||||
> 相反地,**结构本身不是瓶颈**: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 零件仍是单实体。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 |
@@ -18,7 +18,7 @@
|
||||
| P06_大臂骨架_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | M4 抱紧套筒 |
|
||||
| P07_小臂骨架_螺旋体30%_墙5底5_竖打 | 螺旋体 30% | 5 / 5 | 竖打 | 根部加厚顶板区 + M5 电机半包座 |
|
||||
| P08_腕偏转座_螺旋体25%_墙4底5_竖打 | 螺旋体 25% | 4 / 5 | 竖打 | J5 接口孔周 |
|
||||
| P09_工具法兰_螺旋体100%_墙4底5_竖打 | 螺旋体 100% | 4 / 5 | 竖打 | — |
|
||||
| P09_工具法兰_螺旋体30%_墙4底5_竖打 | 螺旋体 30% | 4 / 5 | 竖打 | — |
|
||||
|
||||
## 通用规则
|
||||
|
||||
|
||||
@@ -17,27 +17,27 @@
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 20,
|
||||
"位置": "P04/P05/P06/P07/P08 各 4 颗 -> 下级电机尾端面",
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4x M3 螺纹径向槽(电机.STEP 实测:BCD55、相位 39.9°+90k,槽宽 2.5)"
|
||||
"备注": "Ø2.8 通孔 + Ø5.5 沉孔(深 4mm),旋入电机尾端面 4x M3 螺纹径向槽(电机.STEP 实测:BCD55、相位 39.9°+90k,槽宽 2.5,径向可调约 ±1.25mm)。【2026-10-03 电机厚度公差】各台电机轴向长度实测差可达 2mm,本孔的承压面已按 MOTOR_TOL=2.0 从 s=-0.6 让空到 s=-2.6(armcore.MOTOR_TOL / arm_build.REAR_SEAT),所以这 4 颗螺钉**不负责把电机压死在某个长度上**,只负责夹紧垫片:装配时在电机尾端面与承压面之间叠 0~2.6mm 的 M3 平垫(标称电机约 0.6mm),厚度 T = 58.6 - 电机实际长度 L,让前端盖正好落到前端压盖面上(压盖背面与前端盖 0.0 面压)。螺钉杆长按最坏情况 T=2.6 选:M3x10 在 T=0.6 时咬合最深,T 每加 1mm 咬合减 1mm;咬合不足 3mm 时改用 M3x12。【2026-10-05 承压面让位腔】承压面段(z=-2.6..+1.0)用 SEAT_R=35.0(= 外壁 R35)圆腔掏出垫片位。**刀半径必须等于 R35**:只要小于 R35,刀半径与外壁之间那圈环墙就必然保留并相对承压面凸起,把电机尾端面围成\"一圈围挡\"(P08 实测 1536.9mm3)—— 这是取值的必然产物,不是残留。取满 R35 后承压面成为完整 R35 平圆盘,凸台与围挡一并消失。SEAT_TOP=+1.0 用于吃掉半包开口处的 0.052cm3 残片(取 0 时该残片脱空成独立碎块)。沉孔位置与深度完全不变(Ø5.5 深 4,z=-8..-4,与圆腔 z 区间不重叠,实测残余 0.00mm³),螺钉头仍坐在沉孔底、杆穿承压面进入电机螺纹槽,咬合量不变。承压环同时由 1.95mm 窄环变 4.75mm 完整圆环,受压面积 383->974mm²。"
|
||||
},
|
||||
{
|
||||
"id": "S03",
|
||||
"规格": "M3 x 16",
|
||||
"螺纹": "M3",
|
||||
"长度mm": 16,
|
||||
"头型": "沉头内六角",
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P03 后置重载法兰 -> M1 电机尾端面",
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 沉孔,孔位 BCD55、相位 39.9°+90k(电机.STEP 实测螺纹径向槽),轴向锁死防拉脱;旧 35.1×40.9 矩形孔距对角两颗偏出槽位 ~4.7mm,2026-10-02 修正"
|
||||
"备注": "Ø3.4 通孔 + Ø6.5 圆柱沉孔(深 5mm,头沉入 1.5mm),孔位 BCD55、相位 39.9°+90k(电机.STEP 实测螺纹径向槽),轴向锁死防拉脱;旧 35.1×40.9 矩形孔距对角两颗偏出槽位 ~4.7mm,2026-10-02 修正;头型按用户已采购件用圆柱头内六角(沉孔本为圆柱形)"
|
||||
},
|
||||
{
|
||||
"id": "S04",
|
||||
"位置": "P03 前置锁紧凸台(Ø10 x4) -> M1 电机前端面",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 沉孔,BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°);座位为舱孔前段 4 个 Ø10 局部凸台(2026-10-02 平直舱孔方案),M3x10 旋入电机前端面咬合 3.5mm",
|
||||
"头型": "沉头内六角",
|
||||
"备注": "Ø3.2 通孔 + Ø6.0 圆柱沉孔(深 2mm,头露出 1mm),孔位 BCD55 @17.8°+90k(电机.STEP 实测前端螺纹径向槽);头坐在沉孔底 x=9.5,M3x8 咬合电机前端面 3.5mm;2026-10-02 按用户已采购规格由 M3x10 沉头改回 M3x8 圆柱头",
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"螺纹": "M3",
|
||||
"规格": "M3 x 10",
|
||||
"长度mm": 10
|
||||
"规格": "M3 x 8",
|
||||
"长度mm": 8
|
||||
},
|
||||
{
|
||||
"id": "S05",
|
||||
@@ -54,10 +54,10 @@
|
||||
"规格": "M4 x 16",
|
||||
"螺纹": "M4",
|
||||
"长度mm": 16,
|
||||
"头型": "沉头内六角",
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P09 末端工具法兰 -> 末端工具/夹爪",
|
||||
"备注": "Ø3.7 通孔 + Ø7.0 沉孔(深 4mm),4 孔分布圆 R20"
|
||||
"备注": "Ø4.4 通孔(贯穿法兰)+ Ø7.0 圆柱沉孔(深 4mm,头与法兰底面齐平),R22 @0/90/180/270°(BCD44)-> 末端工具;头型按用户已采购件用圆柱头内六角。2026-10-02 由 R20 改 R22:原 R20 与 3xS01 输出螺钉 Ø5.6 螺丝刀通道中心距 6.06mm < 所需 6.30mm,0° 孔的 Ø7.0 沉孔壁被啃穿成月牙形(圆柱面 88mm² 裂成 45.7+35.4);相位差仅 8.6°,转相位躲不开,R22 后余量 1.66mm、边壁仍余 9.5mm;同日修正通孔:原 Ø3.7 比 M4 杆 Ø4.0 还细且只到 z=105,而 M4x16 杆需走到 z=113,杆尾 8mm 埋在实心料里(干涉 147.3mm³),改为 Ø4.4 贯穿"
|
||||
},
|
||||
{
|
||||
"id": "S07",
|
||||
@@ -115,8 +115,8 @@
|
||||
"螺纹": "M3",
|
||||
"长度mm": 8,
|
||||
"头型": "圆柱头内六角",
|
||||
"数量": 4,
|
||||
"位置": "P04 前端压盖 -> M2 电机前端盖(J2)",
|
||||
"备注": "Ø3.4 通孔 + Ø6.0 沉孔(深 3.0,自压盖前端面 Y=161 沉到 Y=158),BCD 55(R27.5,4 孔 17.8/107.8/197.8/287.8°,与 P03→M1 的 S04 同约定);螺钉头全沉入压盖,头面 Y=158 压孔口台阶,压盖背面 Y=156 与电机前端盖外端面0.0 间隙面压贴合 —— 与后端安装板 S02 的 4xM3x10 一起把 M2 前后双向夹死,消掉大扭矩下电机绕 J2 轴点头/翘起的倾向;M3x8 穿过 3.5mm 端盖后再自攻入壳体约 4.5mm"
|
||||
"数量": 16,
|
||||
"位置": "P04/P05/P06/P07 前端压盖 -> M2/M3/M4/M5 电机前端盖(J2/J3/J4/J5 各 4 颗)",
|
||||
"备注": "Ø3.4 通孔 + Ø6.0 圆柱沉孔(深 3.0,头 Ø5.5×3.0 全沉入),孔位 BCD55 @17.8°+90k—— 正是电机前端面 4 个 M3 螺纹径向槽的相位(与 S04 同一套,勿改);螺钉头面压在孔口台阶上(Y 轴关节 Y=158;J3 的 Z 轴版本 Z=387),压盖背面与电机前端盖外端面 0.0 间隙面压贴合—— 与后端安装板 S02 的 4xM3x10 一起把电机前后双向夹死,消掉大扭矩下电机绕 J 轴点头/翘起的倾向。J2(P04 夹 M2)/J4(P06 夹 M4)/J5(P07 夹 M5) 三处压盖几何完全相同;【2026-10-03 新增 J3】P05 夹 M3 是同一套配方的 Z 轴版本(roll 电机轴向 -Z,压盖占 Z384.0..389.0,背面 Z=389 与 M3 前端盖面压),沉孔台阶 Z=387,杆穿 2.0mm 压盖 + 3.5mm 端盖后旋入前端螺纹槽约 2.5mm。【2026-10-05d 只切敞口那一圈围挡】用户指出 M3 电机前端那圈围挡(P05 在 Z389.0..395.5 的 r30.25..35 前端定位环 + 骑在它上面的 R35 压盖外圈,Ø70 比电机 Ø63.6 凸出 3.2mm)需要删除。之前一版把整个前端(定位环 + 压盖 + 腔口下沿 389->395)都切了,电机前端没了支撑面 —— 过度;现已回退。现只切敞口(-Y)那一圈(盒切,Y 界限与 cut_open 一致),-Y 半圈从 Z=395.5 一路开到压盖背面 Z=389;背脊(+Y)侧整圈定位环保留,继续定位 M3 前端盖并作为R35 压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带)。压盖本体、Ø37 让位孔、4xM3x8 沉孔、腔口下沿 z_bot=389、P06 毂面 383.0 与直锁凸台 65.0 全部不动,本组 16 颗螺钉的孔位/相位/沉孔台阶/咬合量一概不变(M3 仍是后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压)。不整圈削到 R31.8 的原因:那圈壁与 Loft 在 X=±35 处与 Ø70 柱面相切,整圈切完会脱空出 1.32mm3 碎块破坏单实体;且压盖根部熔接带会从 2.37mm2 缩到 0.78mm2,夹紧力会把压盖拉下来。开关 arm_model.FENCE_OPEN_CUT(2026-10-05e 起为全机 5 处共用)。。【2026-10-05e 推广到其余三处】上述\"只切敞口那半圈围挡\"的做法与 P05/M3 完全同源,已推广到 J2/M2(P04)、J4/M4(P06)、J5/M5(P07) 三处同样有 M2/M4/M5 前端压盖的电机,以及尚无前端压盖的 J6/M6(P08)(只切定位环)。切刀 cut_motor_fence() 按各关节自己的局部坐标切(y -3..+40 / z 49.4~56.1,5 处定位环都落在局部 z 49.5~56,一对参数通吃;敞口方向恒为局部 +y)。**本组 16 颗螺钉的数量、型号、孔位 BCD55、相位、沉孔台阶深度、拧紧力矩一概不变** —— 被切掉的只是压盖外侧那圈围挡环墙,压盖本体与沉孔区一根没动,夹紧力与夹紧面积不变。"
|
||||
}
|
||||
]
|
||||
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 100 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: 190 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 135 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: 174 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 193 KiB |
@@ -11,14 +11,14 @@
|
||||
-110.07716623688886,
|
||||
128.0
|
||||
],
|
||||
"M": 14218.240146201913,
|
||||
"F": 41.72937644104729,
|
||||
"sigma": 1.3441760077937133,
|
||||
"tau": 0.06539022933626484,
|
||||
"M": 13909.380595143337,
|
||||
"F": 40.43051800646868,
|
||||
"sigma": 1.3149767824295406,
|
||||
"tau": 0.06335490894195162,
|
||||
"L": 526.087122895359,
|
||||
"defl": 1.6350063694519446,
|
||||
"defl": 1.5994895032255896,
|
||||
"bend_axis": 1,
|
||||
"SF": 8.927402312213866
|
||||
"SF": 9.125636406924915
|
||||
},
|
||||
{
|
||||
"截面": "吊臂颊板 @z=560",
|
||||
@@ -32,77 +32,77 @@
|
||||
-98.0257258064516,
|
||||
127.99999999999997
|
||||
],
|
||||
"M": 13715.34105182334,
|
||||
"F": 41.72937644104729,
|
||||
"sigma": 0.922016873786273,
|
||||
"tau": 0.05534987325054023,
|
||||
"M": 13422.134615816658,
|
||||
"F": 40.43051800646868,
|
||||
"sigma": 0.9023060054615691,
|
||||
"tau": 0.053627066540838125,
|
||||
"L": 518.8210523704677,
|
||||
"defl": 0.9212023341880543,
|
||||
"defl": 0.9015088790834574,
|
||||
"bend_axis": 1,
|
||||
"SF": 13.014946191518016
|
||||
"SF": 13.299257599268081
|
||||
},
|
||||
{
|
||||
"截面": "肩叉腹板 @z=560",
|
||||
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 2217.3650000000002,
|
||||
"I": 207369.82260416728,
|
||||
"c": 16.750000100002925,
|
||||
"A": 1607.2294,
|
||||
"I": 199777.10677271383,
|
||||
"c": 17.03471471510569,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
28.249999999999996,
|
||||
124.95500000000001
|
||||
27.965285384899023,
|
||||
123.79905866269004
|
||||
],
|
||||
"M": 7818.411904486269,
|
||||
"F": 34.39607710754928,
|
||||
"sigma": 0.6315210117722179,
|
||||
"tau": 0.023268210538780903,
|
||||
"L": 384.4650327126411,
|
||||
"defl": 0.9288282882988673,
|
||||
"M": 7715.182607919432,
|
||||
"F": 33.63905427237436,
|
||||
"sigma": 0.6578628393611474,
|
||||
"tau": 0.03139476008126876,
|
||||
"L": 384.35971314950393,
|
||||
"defl": 0.9508783541854641,
|
||||
"bend_axis": 1,
|
||||
"SF": 19.001743055745322
|
||||
"SF": 18.240884394159178
|
||||
},
|
||||
{
|
||||
"截面": "肩叉腹板 @z=600",
|
||||
"零件": "P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 2393.7248,
|
||||
"I": 289729.609479537,
|
||||
"c": 22.374104171783745,
|
||||
"A": 1839.1492000000003,
|
||||
"I": 278077.74717628444,
|
||||
"c": 21.564804309254765,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
26.31610407177968,
|
||||
126.97437823679647
|
||||
25.50680420925067,
|
||||
126.39730477005344
|
||||
],
|
||||
"M": 7884.930337951309,
|
||||
"F": 34.39607710754928,
|
||||
"sigma": 0.6089065355988086,
|
||||
"tau": 0.02155390446776669,
|
||||
"L": 391.71974764776274,
|
||||
"defl": 0.6959926630802069,
|
||||
"M": 7797.883589614678,
|
||||
"F": 33.63905427237436,
|
||||
"sigma": 0.6047223675535119,
|
||||
"tau": 0.02743582815823835,
|
||||
"L": 392.2867363986807,
|
||||
"defl": 0.7192278729466778,
|
||||
"bend_axis": 1,
|
||||
"SF": 19.707458039022367
|
||||
"SF": 19.84381700407025
|
||||
},
|
||||
{
|
||||
"截面": "大臂旋转座横梁 @z=480",
|
||||
"零件": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
"关节": "J2",
|
||||
"A": 1250.9348,
|
||||
"I": 490359.70932822645,
|
||||
"c": 35.00000010000009,
|
||||
"A": 1048.8348,
|
||||
"I": 358227.1267915601,
|
||||
"c": 35.00000010000008,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.500000000000004,
|
||||
165.03578024690017
|
||||
19.5,
|
||||
166.10246986083982
|
||||
],
|
||||
"M": 8644.341221650102,
|
||||
"F": 34.39607710754928,
|
||||
"sigma": 0.6170000060499929,
|
||||
"tau": 0.041244448280856785,
|
||||
"L": 420.96884917144854,
|
||||
"defl": 0.5206738812710487,
|
||||
"M": 8513.356526578775,
|
||||
"F": 33.63905427237436,
|
||||
"sigma": 0.8317836841400634,
|
||||
"tau": 0.048109179261177776,
|
||||
"L": 421.3881746144544,
|
||||
"defl": 0.7033245693677515,
|
||||
"bend_axis": 1,
|
||||
"SF": 19.448946324690457
|
||||
"SF": 14.426827826523379
|
||||
},
|
||||
{
|
||||
"截面": "大臂旋转座套筒 @z=440",
|
||||
@@ -116,98 +116,161 @@
|
||||
19.49928077488516,
|
||||
162.1529414131134
|
||||
],
|
||||
"M": 7224.027167183286,
|
||||
"F": 28.801398145740663,
|
||||
"sigma": 0.5490192396606857,
|
||||
"tau": 0.04677253732065479,
|
||||
"M": 7116.046120241427,
|
||||
"F": 28.210271278866706,
|
||||
"sigma": 0.5408127821103745,
|
||||
"tau": 0.045812566443470384,
|
||||
"L": 384.25524483485555,
|
||||
"defl": 0.38601035205736267,
|
||||
"defl": 0.38024046761743446,
|
||||
"bend_axis": 1,
|
||||
"SF": 21.85715751494692
|
||||
"SF": 22.18882466714871
|
||||
},
|
||||
{
|
||||
"截面": "大臂骨架中段 @z=350",
|
||||
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J3",
|
||||
"A": 1516.920000000001,
|
||||
"I": 1131945.0546960002,
|
||||
"c": 35.00000010000099,
|
||||
"A": 1775.9400000000005,
|
||||
"I": 1218698.119611809,
|
||||
"c": 35.36460360574915,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.499999999999993,
|
||||
119.46110407931855
|
||||
19.864603505749066,
|
||||
125.42235013570274
|
||||
],
|
||||
"M": 5824.4734611263175,
|
||||
"F": 28.801398145740663,
|
||||
"sigma": 0.1800940521592921,
|
||||
"tau": 0.028480142142374657,
|
||||
"L": 287.98631358672816,
|
||||
"defl": 0.07112524430753915,
|
||||
"M": 5838.021996079116,
|
||||
"F": 28.210271278866706,
|
||||
"sigma": 0.16940974176504492,
|
||||
"tau": 0.023827047602002344,
|
||||
"L": 290.61713304781716,
|
||||
"defl": 0.06743118064861804,
|
||||
"bend_axis": 1,
|
||||
"SF": 66.63185072534252
|
||||
"SF": 70.83417916215734
|
||||
},
|
||||
{
|
||||
"截面": "大臂骨架抱箍 @z=325",
|
||||
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 562.2200000000005,
|
||||
"I": 255126.52178866687,
|
||||
"c": 34.31311712541806,
|
||||
"A": 719.8700000000005,
|
||||
"I": 364721.10908443725,
|
||||
"c": 34.9936863254125,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.499999999999996,
|
||||
100.98098075486462
|
||||
20.18056919999444,
|
||||
113.54348979676884
|
||||
],
|
||||
"M": 4674.1717086407425,
|
||||
"F": 23.115225549656877,
|
||||
"sigma": 0.6286504444087546,
|
||||
"tau": 0.06167130006845235,
|
||||
"L": 259.0371452768405,
|
||||
"defl": 0.20489053402473298,
|
||||
"M": 4766.261568803768,
|
||||
"F": 22.395162749075034,
|
||||
"sigma": 0.4573057553544944,
|
||||
"tau": 0.046665014688224996,
|
||||
"L": 264.40855086080256,
|
||||
"defl": 0.15227082967198685,
|
||||
"bend_axis": 1,
|
||||
"SF": 19.088509531375568
|
||||
"SF": 26.240649411240923
|
||||
},
|
||||
{
|
||||
"截面": "大臂骨架过渡块 @z=365",
|
||||
"零件": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"关节": "J3",
|
||||
"A": 4458.1709500000015,
|
||||
"I": 1812470.99216772,
|
||||
"c": 35.000057416518544,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.49994268348248,
|
||||
124.61008050936232
|
||||
],
|
||||
"M": 6138.388109490228,
|
||||
"F": 28.210271278866706,
|
||||
"sigma": 0.11853648262810473,
|
||||
"tau": 0.009491651933692682,
|
||||
"L": 303.0677649681537,
|
||||
"defl": 0.0518455542166412,
|
||||
"bend_axis": 1,
|
||||
"SF": 101.23465564309588
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架中段 @z=245",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 1688.4280000000003,
|
||||
"I": 1225116.8186629335,
|
||||
"c": 35.00000010000015,
|
||||
"A": 1955.5180000000007,
|
||||
"I": 1288379.7804570175,
|
||||
"c": 35.135670341849114,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.5,
|
||||
120.60880570566229
|
||||
19.364329758151037,
|
||||
125.86408670234685
|
||||
],
|
||||
"M": 3519.6618136307306,
|
||||
"F": 23.115225549656877,
|
||||
"sigma": 0.10055217751682426,
|
||||
"tau": 0.020535574110643336,
|
||||
"L": 199.5734012002831,
|
||||
"defl": 0.019071177205131774,
|
||||
"M": 3553.2419643052863,
|
||||
"F": 22.395162749075034,
|
||||
"sigma": 0.09690119341858144,
|
||||
"tau": 0.017178437694571225,
|
||||
"L": 202.73130161742685,
|
||||
"defl": 0.01889170908201219,
|
||||
"bend_axis": 1,
|
||||
"SF": 119.34102568780449
|
||||
"SF": 123.83748410779553
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架抱箍 @z=195",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J5",
|
||||
"A": 381.97200000000026,
|
||||
"I": 75885.39226040008,
|
||||
"c": 22.39173964832456,
|
||||
"A": 590.9720000000002,
|
||||
"I": 114405.68067274656,
|
||||
"c": 22.728382247297116,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.5,
|
||||
103.25743876514508
|
||||
19.163357401027465,
|
||||
122.73950864000324
|
||||
],
|
||||
"M": 2244.921562213841,
|
||||
"F": 16.321781071344514,
|
||||
"sigma": 0.6624160151865485,
|
||||
"tau": 0.06409546146580575,
|
||||
"L": 159.53235542447365,
|
||||
"defl": 0.1254842711067732,
|
||||
"M": 2499.415698384399,
|
||||
"F": 16.14463396798306,
|
||||
"sigma": 0.4965459324548058,
|
||||
"tau": 0.04097816978126643,
|
||||
"L": 172.60323575097078,
|
||||
"defl": 0.10847696343914033,
|
||||
"bend_axis": 1,
|
||||
"SF": 18.11550404109807
|
||||
"SF": 24.16694854527321
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架减重槽 @z=270",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 3453.8644,
|
||||
"I": 1944097.2364364942,
|
||||
"c": 35.33095339393962,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.16904670606061,
|
||||
136.30953293939396
|
||||
],
|
||||
"M": 3978.6632379954935,
|
||||
"F": 22.395162749075034,
|
||||
"sigma": 0.07230603634284398,
|
||||
"tau": 0.009726132885706963,
|
||||
"L": 225.66589175250482,
|
||||
"defl": 0.01736999898793822,
|
||||
"bend_axis": 1,
|
||||
"SF": 165.96124759350914
|
||||
},
|
||||
{
|
||||
"截面": "小臂骨架减重槽口 @z=280",
|
||||
"零件": "P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"关节": "J4",
|
||||
"A": 1556.8381000000002,
|
||||
"I": 671763.1534315376,
|
||||
"c": 35.04697412971099,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.453025976175688,
|
||||
162.82490726385316
|
||||
],
|
||||
"M": 4587.022676232362,
|
||||
"F": 22.395162749075034,
|
||||
"sigma": 0.23931241873732387,
|
||||
"tau": 0.021577544976328973,
|
||||
"L": 248.71544110712577,
|
||||
"defl": 0.07039939955948334,
|
||||
"bend_axis": 1,
|
||||
"SF": 50.14365766438365
|
||||
},
|
||||
{
|
||||
"截面": "腕偏转座根部 @z=150",
|
||||
@@ -215,40 +278,40 @@
|
||||
"关节": "J5",
|
||||
"A": 877.5228499999998,
|
||||
"I": 452897.5474791437,
|
||||
"c": 35.00001954545841,
|
||||
"c": 35.0000195454584,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.499980554542446,
|
||||
153.23437984777257
|
||||
],
|
||||
"M": 2445.425561814797,
|
||||
"F": 16.321781071344514,
|
||||
"sigma": 0.18898301158149458,
|
||||
"tau": 0.02789975395742319,
|
||||
"M": 2430.9020288202305,
|
||||
"F": 16.14463396798306,
|
||||
"sigma": 0.1878606298386295,
|
||||
"tau": 0.027596946281198943,
|
||||
"L": 164.6512204903413,
|
||||
"defl": 0.02439681521795845,
|
||||
"defl": 0.024251921030086537,
|
||||
"bend_axis": 1,
|
||||
"SF": 63.497771040786255
|
||||
"SF": 63.87714131645298
|
||||
},
|
||||
{
|
||||
"截面": "工具法兰 @z=107",
|
||||
"零件": "P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"零件": "P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
"关节": "J6",
|
||||
"A": 3734.962150000003,
|
||||
"I": 1169654.3875857776,
|
||||
"c": 35.0000685148389,
|
||||
"A": 3674.473750000001,
|
||||
"I": 1154943.4472954571,
|
||||
"c": 35.000056371141866,
|
||||
"bend": 1,
|
||||
"centroid": [
|
||||
19.499931585161203,
|
||||
127.9986725177389
|
||||
19.49994372885823,
|
||||
127.99865066501017
|
||||
],
|
||||
"M": 1390.8472530369402,
|
||||
"F": 10.722602788827801,
|
||||
"sigma": 0.04161891723455653,
|
||||
"tau": 0.004306309819830889,
|
||||
"L": 130.23020962176682,
|
||||
"defl": 0.0033611981177080657,
|
||||
"M": 1389.8230238810052,
|
||||
"F": 10.71458030870356,
|
||||
"sigma": 0.04211797927912047,
|
||||
"tau": 0.004373924419260129,
|
||||
"L": 130.23018996182086,
|
||||
"defl": 0.0034015031734828767,
|
||||
"bend_axis": 1,
|
||||
"SF": 288.3304227346958
|
||||
"SF": 284.91395374110147
|
||||
}
|
||||
]
|
||||
@@ -12,91 +12,91 @@
|
||||
"J1": {
|
||||
"cn": "肩抬举",
|
||||
"axis": "roll(绕X)",
|
||||
"distal_mass_kg": 2.127,
|
||||
"arm_mass_kg": 1.627,
|
||||
"weight_N": 20.9,
|
||||
"distal_mass_kg": 2.019,
|
||||
"arm_mass_kg": 1.519,
|
||||
"weight_N": 19.8,
|
||||
"reach_tip_mm": 512.0,
|
||||
"tau_gravity_Nm": 6.137,
|
||||
"tau_gravity_arm_only_Nm": 3.84,
|
||||
"I_distal_kgm2": 0.28468,
|
||||
"tau_dynamic_Nm": 1.423,
|
||||
"tau_required_static_Nm": 6.137,
|
||||
"tau_required_peak_Nm": 7.561,
|
||||
"duty_vs_Tcont_pct": 245.5,
|
||||
"duty_vs_Tallow_pct": 306.9,
|
||||
"peak_margin_x": 1.06,
|
||||
"tau_gravity_Nm": 5.97,
|
||||
"tau_gravity_arm_only_Nm": 3.673,
|
||||
"I_distal_kgm2": 0.27885,
|
||||
"tau_dynamic_Nm": 1.394,
|
||||
"tau_required_static_Nm": 5.97,
|
||||
"tau_required_peak_Nm": 7.364,
|
||||
"duty_vs_Tcont_pct": 238.8,
|
||||
"duty_vs_Tallow_pct": 298.5,
|
||||
"peak_margin_x": 1.09,
|
||||
"status": "OVERLOAD"
|
||||
},
|
||||
"J2": {
|
||||
"cn": "肩俯仰",
|
||||
"axis": "pitch(绕Y)",
|
||||
"distal_mass_kg": 1.753,
|
||||
"arm_mass_kg": 1.253,
|
||||
"weight_N": 17.2,
|
||||
"distal_mass_kg": 1.688,
|
||||
"arm_mass_kg": 1.188,
|
||||
"weight_N": 16.6,
|
||||
"reach_tip_mm": 394.0,
|
||||
"tau_gravity_Nm": 4.395,
|
||||
"tau_gravity_arm_only_Nm": 2.478,
|
||||
"I_distal_kgm2": 0.14623,
|
||||
"tau_dynamic_Nm": 0.731,
|
||||
"tau_required_static_Nm": 4.395,
|
||||
"tau_required_peak_Nm": 5.126,
|
||||
"duty_vs_Tcont_pct": 175.8,
|
||||
"duty_vs_Tallow_pct": 219.8,
|
||||
"peak_margin_x": 1.56,
|
||||
"tau_gravity_Nm": 4.29,
|
||||
"tau_gravity_arm_only_Nm": 2.372,
|
||||
"I_distal_kgm2": 0.14396,
|
||||
"tau_dynamic_Nm": 0.72,
|
||||
"tau_required_static_Nm": 4.29,
|
||||
"tau_required_peak_Nm": 5.009,
|
||||
"duty_vs_Tcont_pct": 171.6,
|
||||
"duty_vs_Tallow_pct": 214.5,
|
||||
"peak_margin_x": 1.6,
|
||||
"status": "OVERLOAD"
|
||||
},
|
||||
"J3": {
|
||||
"cn": "大臂自转",
|
||||
"axis": "roll(绕Z)",
|
||||
"distal_mass_kg": 1.468,
|
||||
"arm_mass_kg": 0.968,
|
||||
"weight_N": 14.4,
|
||||
"distal_mass_kg": 1.411,
|
||||
"arm_mass_kg": 0.911,
|
||||
"weight_N": 13.8,
|
||||
"reach_tip_mm": 250.0,
|
||||
"tau_gravity_Nm": 1.712,
|
||||
"tau_gravity_arm_only_Nm": 0.742,
|
||||
"I_distal_kgm2": 0.03871,
|
||||
"tau_dynamic_Nm": 0.194,
|
||||
"tau_required_static_Nm": 1.712,
|
||||
"tau_required_peak_Nm": 1.906,
|
||||
"duty_vs_Tcont_pct": 68.5,
|
||||
"duty_vs_Tallow_pct": 85.6,
|
||||
"peak_margin_x": 4.2,
|
||||
"tau_gravity_Nm": 1.691,
|
||||
"tau_gravity_arm_only_Nm": 0.721,
|
||||
"I_distal_kgm2": 0.03855,
|
||||
"tau_dynamic_Nm": 0.193,
|
||||
"tau_required_static_Nm": 1.691,
|
||||
"tau_required_peak_Nm": 1.884,
|
||||
"duty_vs_Tcont_pct": 67.6,
|
||||
"duty_vs_Tallow_pct": 84.5,
|
||||
"peak_margin_x": 4.25,
|
||||
"status": "OK"
|
||||
},
|
||||
"J4": {
|
||||
"cn": "肘部俯仰",
|
||||
"axis": "pitch(绕Y)",
|
||||
"distal_mass_kg": 1.178,
|
||||
"arm_mass_kg": 0.678,
|
||||
"weight_N": 11.6,
|
||||
"distal_mass_kg": 1.133,
|
||||
"arm_mass_kg": 0.633,
|
||||
"weight_N": 11.1,
|
||||
"reach_tip_mm": 241.0,
|
||||
"tau_gravity_Nm": 2.062,
|
||||
"tau_gravity_arm_only_Nm": 0.89,
|
||||
"I_distal_kgm2": 0.04258,
|
||||
"tau_dynamic_Nm": 0.213,
|
||||
"tau_required_static_Nm": 2.062,
|
||||
"tau_required_peak_Nm": 2.275,
|
||||
"duty_vs_Tcont_pct": 82.5,
|
||||
"duty_vs_Tallow_pct": 103.1,
|
||||
"peak_margin_x": 3.52,
|
||||
"tau_gravity_Nm": 2.036,
|
||||
"tau_gravity_arm_only_Nm": 0.864,
|
||||
"I_distal_kgm2": 0.04239,
|
||||
"tau_dynamic_Nm": 0.212,
|
||||
"tau_required_static_Nm": 2.036,
|
||||
"tau_required_peak_Nm": 2.248,
|
||||
"duty_vs_Tcont_pct": 81.4,
|
||||
"duty_vs_Tallow_pct": 101.8,
|
||||
"peak_margin_x": 3.56,
|
||||
"status": "OVERLOAD"
|
||||
},
|
||||
"J5": {
|
||||
"cn": "腕部俯仰",
|
||||
"axis": "pitch(绕Y)",
|
||||
"distal_mass_kg": 0.832,
|
||||
"arm_mass_kg": 0.332,
|
||||
"weight_N": 8.2,
|
||||
"distal_mass_kg": 0.823,
|
||||
"arm_mass_kg": 0.323,
|
||||
"weight_N": 8.1,
|
||||
"reach_tip_mm": 132.0,
|
||||
"tau_gravity_Nm": 0.863,
|
||||
"tau_gravity_Nm": 0.862,
|
||||
"tau_gravity_arm_only_Nm": 0.23,
|
||||
"I_distal_kgm2": 0.01018,
|
||||
"I_distal_kgm2": 0.01019,
|
||||
"tau_dynamic_Nm": 0.051,
|
||||
"tau_required_static_Nm": 0.863,
|
||||
"tau_required_peak_Nm": 0.914,
|
||||
"tau_required_static_Nm": 0.862,
|
||||
"tau_required_peak_Nm": 0.913,
|
||||
"duty_vs_Tcont_pct": 34.5,
|
||||
"duty_vs_Tallow_pct": 43.1,
|
||||
"peak_margin_x": 8.75,
|
||||
"peak_margin_x": 8.76,
|
||||
"status": "OK"
|
||||
},
|
||||
"J6": {
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
| 关节 | 名称 | 轴 | 以下质量(kg) | TCP力臂(mm) | 重力力矩(N·m) | 惯性力矩(N·m) | 峰值需求(N·m) | 负荷率(对T_allow) | 峰值裕度 | 判定 |
|
||||
|:---:|:---|:---|---:|---:|---:|---:|---:|---:|---:|:---:|
|
||||
| J1 | 肩抬举 | roll(绕X) | 2.13 | 512 | **6.14** | 1.42 | 7.56 | **307%** | 1.06× | OVERLOAD |
|
||||
| J2 | 肩俯仰 | pitch(绕Y) | 1.75 | 394 | **4.39** | 0.73 | 5.13 | **220%** | 1.56× | OVERLOAD |
|
||||
| J3 | 大臂自转 | roll(绕Z) | 1.47 | 250 | **1.71** | 0.19 | 1.91 | **86%** | 4.20× | OK |
|
||||
| J4 | 肘部俯仰 | pitch(绕Y) | 1.18 | 241 | **2.06** | 0.21 | 2.27 | **103%** | 3.52× | OVERLOAD |
|
||||
| J5 | 腕部俯仰 | pitch(绕Y) | 0.83 | 132 | **0.86** | 0.05 | 0.91 | **43%** | 8.75× | OK |
|
||||
| J1 | 肩抬举 | roll(绕X) | 2.02 | 512 | **5.97** | 1.39 | 7.36 | **298%** | 1.09× | OVERLOAD |
|
||||
| J2 | 肩俯仰 | pitch(绕Y) | 1.69 | 394 | **4.29** | 0.72 | 5.01 | **214%** | 1.60× | OVERLOAD |
|
||||
| J3 | 大臂自转 | roll(绕Z) | 1.41 | 250 | **1.69** | 0.19 | 1.88 | **84%** | 4.25× | OK |
|
||||
| J4 | 肘部俯仰 | pitch(绕Y) | 1.13 | 241 | **2.04** | 0.21 | 2.25 | **102%** | 3.56× | OVERLOAD |
|
||||
| J5 | 腕部俯仰 | pitch(绕Y) | 0.82 | 132 | **0.86** | 0.05 | 0.91 | **43%** | 8.76× | OK |
|
||||
| J6 | 腕自转 | roll(绕Z) | 0.55 | 83 | **0.00** | 0.00 | 0.00 | **0%** | — | OK |
|
||||
|
||||
## 3. 许用连续工况下的末端负载能力
|
||||
@@ -26,10 +26,10 @@
|
||||
|
||||
| 关节 | 仅臂身重力力矩(N·m) | 余额(N·m) | 可挂负载(kg) |
|
||||
|:---:|---:|---:|---:|
|
||||
| J1 | 3.84 | -1.84 | 0.00 |
|
||||
| J2 | 2.48 | -0.48 | 0.00 |
|
||||
| J3 | 0.74 | +1.26 | 0.51 |
|
||||
| J4 | 0.89 | +1.11 | 0.47 |
|
||||
| J1 | 3.67 | -1.67 | 0.00 |
|
||||
| J2 | 2.37 | -0.37 | 0.00 |
|
||||
| J3 | 0.72 | +1.28 | 0.52 |
|
||||
| J4 | 0.86 | +1.14 | 0.48 |
|
||||
| J5 | 0.23 | +1.77 | 1.37 |
|
||||
| J6 | 0.00 | +2.00 | 2.46 |
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
## 4. 结论要点
|
||||
|
||||
- **超载关节:J1、J2、J4**。
|
||||
- J1(肩抬举)重力 6.14 N·m,超 `T_allow` 207%;即使按峰值堵转算,裕度也只有 1.06×。
|
||||
- J2(肩俯仰)重力 4.39 N·m,超 `T_allow` 120%;即使按峰值堵转算,裕度也只有 1.56×。
|
||||
- J4(肘部俯仰)重力 2.06 N·m,超 `T_allow` 3%;即使按峰值堵转算,裕度也只有 3.52×。
|
||||
- J1(肩抬举)重力 5.97 N·m,超 `T_allow` 198%;即使按峰值堵转算,裕度也只有 1.09×。
|
||||
- J2(肩俯仰)重力 4.29 N·m,超 `T_allow` 114%;即使按峰值堵转算,裕度也只有 1.60×。
|
||||
- J4(肘部俯仰)重力 2.04 N·m,超 `T_allow` 2%;即使按峰值堵转算,裕度也只有 3.56×。
|
||||
- 结论对电机参数高度敏感:若实际连续额定高于假定值,请用 `ARMT_CONT` / `ARMT_ALLOW` 覆盖后重跑本脚本。
|
||||
- 惯量按 Σm·d² 估,**未计入零件自身转动惯量**,对肩/肘是大臂量级的关节约低估一成,惯性力矩一栏应视为下限。
|
||||
- 本版不含被动弹簧平衡。旧版报告提出的 J2 拉簧预补偿(2.48 N·m)在几何上**尚未实现**;若要保留该方案,需要在 P04/P05 之间增加拉簧连杆机构。
|
||||
@@ -9,11 +9,11 @@
|
||||
},
|
||||
{
|
||||
"axis": "J2",
|
||||
"lo": -106.0,
|
||||
"lo": -105.0,
|
||||
"hi": 9.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +9° / 反向 -106°,总行程 115°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J3",
|
||||
@@ -33,11 +33,11 @@
|
||||
},
|
||||
{
|
||||
"axis": "J5",
|
||||
"lo": -95.0,
|
||||
"lo": -93.0,
|
||||
"hi": 34.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +34° / 反向 -95°,总行程 129°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J6",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-10-02 12:06:22",
|
||||
"generated": "2026-10-04 02:00:11",
|
||||
"vol_min": 2.0,
|
||||
"step": 2.0,
|
||||
"combo_step": 6.0,
|
||||
@@ -12,17 +12,17 @@
|
||||
"jams": {}
|
||||
},
|
||||
"J2": {
|
||||
"lo": -107.5,
|
||||
"lo": -106.25,
|
||||
"hi": 10.38,
|
||||
"jams": {
|
||||
"12.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
2557.1,
|
||||
2401.7,
|
||||
[
|
||||
-52.9,
|
||||
137.7,
|
||||
149.9,
|
||||
149.2
|
||||
]
|
||||
]
|
||||
@@ -31,11 +31,11 @@
|
||||
[
|
||||
"P04_肩叉_螺旋体30%_墙5底5_竖打",
|
||||
"P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
16.5,
|
||||
175.1,
|
||||
[
|
||||
46.8,
|
||||
136.3,
|
||||
534.6
|
||||
141.0,
|
||||
532.9
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -47,13 +47,13 @@
|
||||
"jams": {}
|
||||
},
|
||||
"J4": {
|
||||
"lo": -112.5,
|
||||
"lo": -112.62,
|
||||
"hi": 17.88,
|
||||
"jams": {
|
||||
"18.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
@@ -64,26 +64,26 @@
|
||||
],
|
||||
"-114.0": [
|
||||
[
|
||||
"P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
"P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
56.3,
|
||||
30.3,
|
||||
[
|
||||
19.5,
|
||||
53.6,
|
||||
128.0,
|
||||
383.1
|
||||
384.9
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"J5": {
|
||||
"lo": -96.25,
|
||||
"lo": -94.75,
|
||||
"hi": 35.75,
|
||||
"jams": {
|
||||
"36.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
@@ -92,15 +92,15 @@
|
||||
]
|
||||
]
|
||||
],
|
||||
"-98.0": [
|
||||
"-96.0": [
|
||||
[
|
||||
"P07_小臂骨架_螺旋体30%_墙5底5_竖打",
|
||||
"P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
328.2,
|
||||
144.8,
|
||||
[
|
||||
59.4,
|
||||
139.3,
|
||||
227.9
|
||||
55.5,
|
||||
139.7,
|
||||
257.0
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -129,7 +129,7 @@
|
||||
"hold_at": -180.0,
|
||||
"swept": "J2",
|
||||
"direction": "lo",
|
||||
"old": -107.5,
|
||||
"old": -106.25,
|
||||
"new": -103.0,
|
||||
"jams": {}
|
||||
},
|
||||
@@ -159,7 +159,7 @@
|
||||
"hold_at": -180.0,
|
||||
"swept": "J4",
|
||||
"direction": "lo",
|
||||
"old": -112.5,
|
||||
"old": -112.625,
|
||||
"new": -109.0,
|
||||
"jams": {}
|
||||
},
|
||||
@@ -173,6 +173,16 @@
|
||||
"new": 29.0,
|
||||
"jams": {}
|
||||
},
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J1",
|
||||
"hold_at": -180.0,
|
||||
"swept": "J5",
|
||||
"direction": "lo",
|
||||
"old": -94.75,
|
||||
"new": -91.0,
|
||||
"jams": {}
|
||||
},
|
||||
{
|
||||
"pass_": 0,
|
||||
"hold": "J1",
|
||||
@@ -315,7 +325,7 @@
|
||||
"-18.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
@@ -338,7 +348,7 @@
|
||||
"-18.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
6.2,
|
||||
[
|
||||
-82.0,
|
||||
@@ -361,7 +371,7 @@
|
||||
"-36.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
@@ -378,13 +388,13 @@
|
||||
"hold_at": -180.0,
|
||||
"swept": "J5",
|
||||
"direction": "lo",
|
||||
"old": -96.25,
|
||||
"old": -91.0,
|
||||
"new": -36.625,
|
||||
"jams": {
|
||||
"-36.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
11.4,
|
||||
[
|
||||
-82.2,
|
||||
@@ -437,7 +447,7 @@
|
||||
"-18.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
186.7,
|
||||
[
|
||||
-83.0,
|
||||
@@ -460,7 +470,7 @@
|
||||
"-18.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
186.7,
|
||||
[
|
||||
-83.0,
|
||||
@@ -483,7 +493,7 @@
|
||||
"-36.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
2.5,
|
||||
[
|
||||
-82.0,
|
||||
@@ -536,7 +546,7 @@
|
||||
"162.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
@@ -548,7 +558,7 @@
|
||||
"-162.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
@@ -571,7 +581,7 @@
|
||||
"162.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
@@ -583,7 +593,7 @@
|
||||
"-162.0": [
|
||||
[
|
||||
"P01_A_立柱下段_三角形30%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体100%_墙4底5_竖打",
|
||||
"P09_工具法兰_螺旋体30%_墙4底5_竖打",
|
||||
52.0,
|
||||
[
|
||||
-82.4,
|
||||
@@ -1246,11 +1256,11 @@
|
||||
},
|
||||
{
|
||||
"axis": "J2",
|
||||
"lo": -106.0,
|
||||
"lo": -105.0,
|
||||
"hi": 9.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +9° / 反向 -106°,总行程 115°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"note": "正向 +9° / 反向 -105°,总行程 114°(全对全动态扫描验证,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J3",
|
||||
@@ -1270,11 +1280,11 @@
|
||||
},
|
||||
{
|
||||
"axis": "J5",
|
||||
"lo": -95.0,
|
||||
"lo": -93.0,
|
||||
"hi": 34.0,
|
||||
"continuous": false,
|
||||
"zero": 0.0,
|
||||
"note": "正向 +34° / 反向 -95°,总行程 129°(全对全动态扫描验证,含相邻件与电机)"
|
||||
"note": "正向 +34° / 反向 -93°,总行程 127°(全对全动态扫描验证,含相邻件与电机)"
|
||||
},
|
||||
{
|
||||
"axis": "J6",
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# 机器人手臂:运动学模拟与碰撞算法详解
|
||||
|
||||
> 本文基于 2026-10-03 的代码现状逐文件核对写成,所有数字(关节轴坐标、行程、胶囊数、自检误差)均取自当前仓库里的真实代码与数据文件,不是设计目标值。
|
||||
> 配图:`docs/algo_svg/*.svg` 为算法示意图,`docs/shots/*.png` 为查看器实机渲染截图。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [第一篇 系统总览:一个事实源、两个碰撞模型](#第一篇-系统总览一个事实源两个碰撞模型)
|
||||
- [第二篇 运动学建模与正运动学(FK)](#第二篇-运动学建模与正运动学fk)
|
||||
- [第三篇 逆运动学:闭式支 + 二维冗余扫描 + 牛顿收口](#第三篇-逆运动学闭式支--二维冗余扫描--牛顿收口)
|
||||
- [第四篇 交互层:把求解器接进 30fps](#第四篇-交互层把求解器接进-30fps)
|
||||
- [第五篇 碰撞检测 I:离线权威扫描(sweep_v2.py)](#第五篇-碰撞检测-i离线权威扫描sweep_v2py)
|
||||
- [第六篇 碰撞检测 II:实时胶囊模型与动态限位](#第六篇-碰撞检测-ii实时胶囊模型与动态限位)
|
||||
- [第七篇 力矩与强度校核(简述)](#第七篇-力矩与强度校核简述)
|
||||
- [第八篇 自检体系与复现命令](#第八篇-自检体系与复现命令)
|
||||
|
||||
---
|
||||
|
||||
## 第一篇 系统总览:一个事实源、两个碰撞模型
|
||||
|
||||
整台手臂的"运动学模拟"分布在两个运行环境里,它们共享同一份关节几何,靠数据文件(而不是人工抄写)保持一致:
|
||||
|
||||
| 环境 | 载体 | 算法 |
|
||||
|---|---|---|
|
||||
| 离线(Python/CadQuery) | `src/human_layout.py` → `arm_model.py` → `sweep_v2.py` | 参数化 CAD 建模、OCCT 精确布尔碰撞、行程扫描 |
|
||||
| 在线(浏览器) | `web/arm-ik-solver.js` + `ik-glue.js` + three.js 场景图 | 闭式 IK、实时胶囊碰撞、动态限位 |
|
||||
|
||||

|
||||
|
||||
关键纪律是**单一事实源**:6 根关节轴的位置 `rear` 与方向 `d` 只在 `human_layout.py` 里定义一次(`human_layout.py:110-115`),CAD 装配、离线碰撞扫描、网页求解器三处都从这里取值——网页端由 `build_viewer.py` 打包时把同一组常量注入 HTML,因此"CAD 里看到的"和"网页里算的"逐位一致,不存在第二套会漂移的关节表。
|
||||
|
||||
两个碰撞模型各司其职:
|
||||
|
||||
- **离线精确模型**(`sweep_v2.py`):对真实 CAD 实体做布尔求交,慢(每姿态秒级)但精确,负责产出权威行程数据;
|
||||
- **在线近似模型**(`collide.json` 胶囊体):把每件零件拟合成一组胶囊,快(每次 ~5ms)但近似,负责求解过程中的实时避碰。
|
||||
|
||||
几何变更时的同步链(缺一步网页就是旧的、行程就是错的):
|
||||
|
||||
```
|
||||
arm_model.py 重建 → AB.export 写 mesh_arm.json → build_viewer.py 重打包
|
||||
→ 渲染出图确认 → sweep_v2.py 重扫(travel.json)→ fit_capsules.py 重拟合(collide.json)
|
||||
```
|
||||
|
||||
实机全貌(查看器渲染,零位姿态):
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 第二篇 运动学建模与正运动学(FK)
|
||||
|
||||
### 2.1 关节拓扑
|
||||
|
||||
对标智元灵犀 X1 的人形构型:肩部两轴(抬举+俯仰)→ 大臂自转 → 肘俯仰 → 腕俯仰 → 腕自转。所有关节轴都是坐标轴方向的(±X/±Y/±Z),这是后面一切降阶解法的前提:
|
||||
|
||||
| 关节 | 名称 | 轴方向 | 轴位置(CAD 系) | 驱动 |
|
||||
|---|---|---|---|---|
|
||||
| J1 | 肩抬举 | +X(滚转) | x=−51, z=610 | M1 → P04 肩叉 |
|
||||
| J2 | 肩俯仰 | +Y(俯仰) | x=19.5, z=490 | M2 → P05 大臂旋转座 |
|
||||
| J3 | 大臂自转 | −Z(滚转) | x=19.5, z=445 | M3 → P06 大臂骨架 |
|
||||
| J4 | 肘部俯仰 | +Y(俯仰) | x=19.5, z=332 | M4 → P07 小臂骨架 |
|
||||
| J5 | 腕部俯仰 | +Y(俯仰) | x=19.5, z=222 | M5 → P08 腕偏转座 |
|
||||
| J6 | 腕自转 | −Z(滚转) | x=19.5, z=176 | M6 → P09 工具法兰 |
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
求解器启动时会对五条**结构前提**做断言(`arm-ik-solver.js` 的 `WARN` 列表):J1 轴是 X;J2/J4/J5 是平行同向的 Y 轴且落在同一 x 平面;J3/J6 是 Z 轴;TCP 落在 J6 轴线上。任何一条被将来的改型破坏,页面会显式报警并回落到通用(慢)路径,而不是悄悄算错。
|
||||
|
||||
### 2.2 正运动学:枢轴链
|
||||
|
||||
每个关节是"绕一根不经过原点的轴"的旋转,写成枢轴变换:
|
||||
|
||||
```
|
||||
Ai(θi) = T(Pi) · R(di, θi) · T(−Pi) 对任意点:x' = R·(x−Pi) + Pi
|
||||
```
|
||||
|
||||
整条链就是六次左乘:
|
||||
|
||||
```
|
||||
World = A1(θ1)·A2(θ2)·A3(θ3)·A4(θ4)·A5(θ5)·A6(θ6)·TCP
|
||||
```
|
||||
|
||||

|
||||
|
||||
代码里有两套实现互为对账:
|
||||
|
||||
- `fkChain()`:严格 4×4 同质矩阵链,直白但慢,作为对照基准;
|
||||
- `fk()`:把结构常量(`K.C0=Z2−Z1`、`K.A32=Z3−Z2`、`K.A43=Z4−Z3`、`K.A54=Z5−Z4`、`K.L5=Z5−TCP_z` 等,即各关节轴的竖直间距链)代入后展开三角函数的闭式快路径,是求解器热循环的专用实现。
|
||||
|
||||
预置自检:5000 个随机位形下两实现的 TCP 差 ≤ **5.7e-13 mm**(数值见 `ik-glue.js` 的 `PRECOMPUTED_CHK`,绑定代码指纹,模型一变即失效提示重算)。
|
||||
|
||||
三个对算法影响深远的结构性质:
|
||||
|
||||
1. **J4 ∥ J5 同向** ⇒ `R4·R5 = R_y(θ4+θ5)`,腕部两个俯仰在"工具轴指向"这件事上可以合并成一个未知数 `s = θ4+θ5`;
|
||||
2. **TCP 在 J6 轴线上** ⇒ θ6 只提供绕工具轴的滚转,**完全不改变 TCP 位置**——位置任务与姿态任务由此解耦;
|
||||
3. **工具轴(法兰 +Z)= (R1…R5)·ẑ**,与 θ6 无关——姿态任务只剩 2 个方程。
|
||||
|
||||
---
|
||||
|
||||
## 第三篇 逆运动学:闭式支 + 二维冗余扫描 + 牛顿收口
|
||||
|
||||
### 3.1 问题形式与降阶
|
||||
|
||||
6 自由度机械臂,任务是给定 TCP 目标点(位置任务,3 个方程)或"目标点 + 工具姿态 {方位角 az, 仰角 el, 滚转 roll}"(位姿任务,再加 3 个方程)。臂本身有 6 个未知数,位置任务下存在 **2 维冗余**。
|
||||
|
||||
核心思路(`branch()`,arm-ik-solver.js:528):**把 (θ4, θ5) 当作冗余参数**。一旦给定这两个角,腕心相对量 `V` 完全确定(u=−L·sinθ5, w=A54−L·cosθ5, dx=u·c4+w·s4, dz=A43−u·s4+w·c4),剩下的 θ1..θ3 可以用平面三角学**一步闭式解出**(每格至多 4 支:θ3 的 ±σ 与 ±(π−σ)),位置精确到浮点误差,不需要任何迭代。
|
||||
|
||||

|
||||
|
||||
于是"求 IK"变成了"在 (θ4,θ5) 二维平面上挑最好的格点"——**扫描代替数值搜索**。每个格点的解天然精确,扫描的全部意义在于挑出代价(关节行程)最小的那支构型。
|
||||
|
||||
### 3.2 姿态参数化:方位角 / 仰角 / 滚转
|
||||
|
||||
姿态任务用三个角描述工具姿态(`poseMat()` 构造工具系旋转矩阵,`poseAng()` 逆分解):**az** 绕 CAD Z 的方位角、**el** 自 +Z 的仰角(0=朝上,180=朝下)、**roll** 绕工具轴自身的滚转。三个关键性质撑起了姿态算法:
|
||||
|
||||
- θ6 只提供绕工具轴的滚转,**工具轴 = (R1…R5)·ẑ 与 θ6 无关**——姿态任务因此只剩 2 个方程;
|
||||
- 滚转由 `rollClose()` 闭式补出:θ6 = −atan2(N[1,0], N[0,0]),其中 N = R5ᵀ·R_target;
|
||||
- 推论:**全姿态偏差 oerr ≥ 工具轴夹角**(20 万随机样本验证零违反)——粗扫用一次点积预筛(1−cos2°)就挡掉所有必被拒的候选,免掉矩阵链 + acos。
|
||||
|
||||

|
||||
|
||||
### 3.3 求解流水线
|
||||
|
||||

|
||||
|
||||
1. **逐行可行段探测**:位置闭式支只在平面三角有解时存在,限位矩形里只有 7~27% 的格点能过判据。先用探测步 `hp` 找每行的连续可行段(位置任务 2°、姿态任务 0.5°——姿态任务的可行带实测最窄 **0.98°**,2° 探测会整条漏掉造成"误报不可达"),段端外扩 PAD=2°,段内仍按原步长 h(默认 0.25°)精扫:密度不降、成本大降。
|
||||
2. **全域粗扫**:每格 `branch()` 出 ≤4 支 → 限位筛(NaN 候选用否定式写法保证落选)→ 打分,候选有界保留 4000。
|
||||
3. **孤立根枚举**(姿态任务):不再靠网格密度去撞窄带——逐行对姿态残差两个分量找**变号括号**,从括号中点沿该支跑阻尼高斯-牛顿收根;切向相切的根不变号,按每支 ‖e‖ 最小的节点补种。
|
||||
4. **局部加密 + 模式搜索**:32 个种子各开 ±2h 窗口(步长 h/8,小步长档 h/4),再对最优的 24 个候选做 8 邻域模式搜索(步长逐轮减半至 2e-4°)。
|
||||
5. **姿态收口**:姿态达标带是 (θ4,θ5) 平面上的斜窄谷(局部灵敏度可达 ~10°/°),模式搜索只能停在谷壁(实测最好 0.106°)。直接对"工具轴在目标轴法平面上的切向偏差"做 **(θ4,θ5) 二维牛顿迭代**(每步重解闭式位置支、按 θ1..θ3 最近匹配保持同支),轴残差压到 **1e-12** 量级、位置全程保持精确;尚无 oerr≤0.05° 的解时姿态权重 ×100 重扫(最多 3 轮)。
|
||||
6. **选解**:`cost = Σ w·|wrap180(θ−ref)| + 1e-6·Σ|θ|`。ref 是当前姿态——判据是用户定的「**所有电机需要转过的角度之和最小**」(这一步行程最短),连续轴取等价角防绕远。姿态任务先取 oerr≤0.05° 的达标集、集内再比代价,"省几度关节角、丢 0.2° 姿态"的交易被显式禁止。
|
||||
7. **solveDyn 碰撞后处理**(第六篇)。
|
||||
|
||||
### 3.4 兜底:最近可达位姿
|
||||
|
||||
目标不可达时不报错了事,而是返回最近可达位姿,并且按任务语义分三种:
|
||||
|
||||
- `closest()`:位置 DLS(阻尼最小二乘)——160 迭代 × 160 次 **LCG 确定性重启**(固定种子,同一目标逐位可复现,消灭"随机倒摆")+ anchor 回拉 0.03(压准退化构型下 J3 的无用漂移,实测 82°)+ **零空间抛光**(到目标距离不增的前提下把 Σ|θ| 滑到最小);
|
||||
- `closest6()`:位置 + 姿态 6 维联合 DLS(姿态旋转矢量按 20 mm/rad 折算进误差向量);
|
||||
- `closestOri()`:姿态优先——利用 J4∥J5 的降阶结构(工具轴 = Rx(θ1)·Ry(θ2)·Rz(−θ3)·Ry(s)·ẑ),(θ3, s, θ4) 三维粗扫 + 坐标下降,**姿态精确、位置取最近**。
|
||||
|
||||
### 3.5 混合优先(位置/姿态滑杆)
|
||||
|
||||
位置与姿态在远目标上不可兼得(实测同一目标:位置 0mm 时姿态差 44°,姿态 0° 时位置差 116mm)。`mix` 模式用两端锚点(位置优先解 / 姿态优先解)的误差各自归一,按
|
||||
|
||||
```
|
||||
J(q) = w·(e/e_ref)² + (1−w)·(o/o_ref)²
|
||||
```
|
||||
|
||||
加权折中,种子取两锚点 + 关节空间插值 + ref,6 维阻尼高斯-牛顿 + 单轴抛光;J 同量级带内取行程更小者防横跳。滑杆 0%/100% 直接走两条专用算法,结果逐位一致。
|
||||
|
||||
### 3.6 实时续解(warm 路径)
|
||||
|
||||
拖拽时目标每帧只挪几毫米,全域粗扫(0.25° 下 6~30 万点,40~430ms)是 100ms 级延迟的唯一来源。`solveWarm` 只在上一帧解附近的 (θ4,θ5) 递进窗口(±4°→14°→40°)里走**同一闭式支**:位置残差 ≤1e-6、关节跳 ≤25°(防换支/倒摆)、姿态牛顿收口——任一校验不过就返回 null 交回全量。快路只负责"能快的时候快",松手后的全量求解才是最终结果。
|
||||
|
||||
### 3.7 工作空间剖面
|
||||
|
||||
工作空间由纯 FK 采样刻画(`scratch/gen_workspace_svg.cjs`,与查看器「可达空间」同口径:travel.json + adj 机械区间内随机位形、J6=0、未计自碰撞)。J1 整周连续,可达集对 J1 轴旋转对称,所以 (x, r) 二维剖面(r = 离 J1 轴的距离)就是完整工作空间:rmax = 517mm 恰好是**零位臂竖直下垂的 TCP**,前伸边界 x≈+416mm。
|
||||
|
||||

|
||||
|
||||
自检成绩(预置值,指纹校验):
|
||||
|
||||
| 项目 | 样本 | 结果 |
|
||||
|---|---|---|
|
||||
| FK 双实现对账 | 5000 | 最大差 5.7e-13 mm |
|
||||
| 位置 IK 重构 | 200/200 命中 | 最大残差 9.9e-13 mm |
|
||||
| 姿态 IK 重构 | 400 组 | 398 命中,最大残差 1.79°,平均 77.8 ms/次 |
|
||||
| 运动学可达空间 | 400 000 位形 | x −378…415 / y −388…642 / z 94…1125 mm |
|
||||
|
||||
(姿态 IK 的 2 组未达 0.05° 达标线,以近似解如实上报 `poseTier=approx` 并给出真实残差,不虚报精确。)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 第四篇 交互层:把求解器接进 30fps
|
||||
|
||||
`ik-glue.js` 负责三件事:
|
||||
|
||||
**调度**。全量求解(40~430ms)放进 Blob Worker 后台跑,主线程只收结果渲染。每个请求按 id 记账,"是否最新一帧"用第二参交给回调——被插队作废的请求也会被放行,保证实时追踪的门闩(liveBusy/livePending)不因一次竞态永久停摆。Worker 不可用时自动回落主线程同步求解,行为逐位一致。
|
||||
|
||||
**拖拽语义**。黄球 = 移动目标(Shift 只上下);姿态控制开启后出现工具箭头:拖箭头头部 = 弧球式瞄准(指针射线与"以目标为球心、箭长为半径"的球求交,方向即工具轴指向),拖箭杆/捏环/粉色侧球 = 绕轴滚转(指针在垂直于工具轴的平面上的转角)。"实时追踪"模式下拖动即算即动,松手全量精修。
|
||||
|
||||
**动画与对账**。解算结果经 `animTo` 动画写入:连续轴(J1/J3/J6)按 `wrap180` 走**短弧**(+151.9°→−167° 若按数值插值会反向扫 318.9°,走短弧只有 41.1°),末帧落回求解器给的代表角——与插值终点恰差 360° 整数倍,同一物理角、视觉零跳变。每帧渲染后还用**场景图真实矩阵链**复算一次 TCP 位置与姿态偏差(`sceneOrientErr`),和求解器的闭式残差双通道对账——任何"求解器以为对了、场景画错了"的不同步都会立刻暴露在界面上。
|
||||
|
||||
---
|
||||
|
||||
## 第五篇 碰撞检测 I:离线权威扫描(sweep_v2.py)
|
||||
|
||||
离线扫描回答的问题是:"**每个关节到底能转到多少度?**"答案直接写进 `docs/travel.json` 成为查看器滑块的硬限位,因此判定必须精确——它直接对真实 CAD 实体做 OCCT 布尔求交,一切预筛都设计成**保守证书**:只能"确证分离"才允许跳过精确布尔。
|
||||
|
||||

|
||||
|
||||
逐层拆解(每姿态的判定路径 `test_posture()`):
|
||||
|
||||
1. **全对全**:19 件(9 打印件 + 6 电机 + 台架 P01/P02)≈ 171 对。台架默认**参与**碰撞——臂撞立柱就是臂撞躯干,是真约束(`--no-stand` 仅用于诊断某个限位是臂自身折叠还是躯干挡的)。
|
||||
2. **跳过刚性安装对**:12 对电机↔被驱动件由螺钉锁死(`MOUNT_PAIRS`),无相对运动。
|
||||
3. **组跨度规则**:某姿态下没有活动关节跨越的零件对相对静止,跳过;但**零位(无活动关节)时必须全查**——相对位姿不变不代表不干涉,零位单实体检查就是这么兜住的。
|
||||
4. **轴区间预剪枝**(单关节扫描专用):按关节轴算每对零件的轴向/径向区间,永不相交的对在该关节任何角度都不可能碰撞——对单关节扫描是**精确安全**的。
|
||||
5. **AABB**:8 角点变换后取包围盒,0.1mm 余量。
|
||||
6. **包围球**:球心随组刚体变换、半径旋转不变,仍精确保守。
|
||||
7. **GJK 凸包预筛**:零件按 0.5mm 容差网格化取顶点云 → scipy 凸包精简(电机 15 万顶点 → 千余点),GJK 带 0.8mm 安全外扩(> 网格内缩 0.5mm,保证"确证分离"判据保守)。单纯形用 Muratori 形式手写,"可能干涉/数值退化/迭代超限"一律回落精确布尔——**结果与纯布尔完全一致**,`--no-gjk` 可关闭做对比校验。
|
||||
8. **顶点侵入实锤**(反方向捷径):一方的网格顶点(≤240 采样点)落入对方实体内部 >1mm(`BRepClass3d_SolidClassifier`,容差与 `VOL_MIN=2mm³` 语义对齐)即实锤干涉,免掉 4~12s 的 NURBS 布尔。注意方向性:它能证"撞",不能证"自由"——无命中则照旧走布尔。
|
||||
9. **OCCT 精确布尔** `BRepAlgoAPI_Common`:交体积 > 2mm³ 记干涉并附干涉质心;布尔失败记入 errors 不装作没看见。
|
||||
|
||||
**扫描策略**:
|
||||
|
||||
- **单关节硬限位**:2° 粗扫(小批量并行、首个卡死角即停,少算深度卡死姿态)+ 二分逼近到 0.2°,正反两向。这是 travel.json 的来源。
|
||||
- **组合姿态收敛**:一个关节保持在极限、扫其余关节(6° 步长),两轮迭代收敛出"组合折叠约束"。这些约束**只写 travel_detail.json、不压缩滑块**——否则手臂会被静态限位锁死(真实机器人由控制器避碰);查看器改用动态模型按当前姿态实时处理(第六篇)。
|
||||
- **并行**:mp.Pool 每个工作进程独立建模(杜绝 STEP/网格过期),`imap_unordered` 保序回传,workers = min(12, CPU−2, 可用内存/2.5GB)。
|
||||
|
||||
当前权威数据(`travel.json` / `travel_detail.json`,2026-10-03 扫描;`collide.json` 胶囊拟合 2026-09-29):
|
||||
|
||||
| 轴 | 单关节硬限位 | 滑块范围 | 备注 |
|
||||
|---|---|---|---|
|
||||
| J1 | −180…+180 | ±180 连续 | 整周无干涉 |
|
||||
| J2 | −106.25…+10.38 | **−105…+9** | 含 1° 安全余量 |
|
||||
| J3 | −180…+180 | ±180 连续 | 整周无干涉 |
|
||||
| J4 | −112.62…+17.88 | **−111…+16** | |
|
||||
| J5 | −94.75…+35.75 | **−93…+34** | |
|
||||
| J6 | −180…+180 | ±180 连续 | 电机行程 ±360° |
|
||||
|
||||
组合折叠明细共 99 条(典型:工具法兰 P09 撞立柱 P01_A 限制了 J3/J4/J5 的组合极限)。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 第六篇 碰撞检测 II:实时胶囊模型与动态限位
|
||||
|
||||
### 6.1 模型:CAD → 胶囊
|
||||
|
||||
离线布尔每姿态秒级,放进 30fps 的交互里不可能。`scratch/fit_capsules.py` 把每件零件的 CAD 网格拟合为一组**胶囊体**(线段端点 a,b + 半径 r),写进 `docs/collide.json`:共 **1995 枚胶囊**,归属 7 个运动组(g0=台架固定 … g6=J6 组),端点是零位 CAD 世界坐标,组 g 的胶囊随 A1…Ag 嵌套旋转——与 FK 链同一累积变换,与场景图逐位一致。
|
||||
|
||||

|
||||
|
||||
胶囊是外凸近似,所以它判"干涉"略偏保守(含 margin=0.3mm 外扩),判"自由"需离线布尔背书;换来的是每次 `collideFree()` 只需 **~5ms**(零位最坏情形)。
|
||||
|
||||
### 6.2 宽相位与热路径
|
||||
|
||||

|
||||
|
||||
- **只检 |Δg|≥2 的零件对**:同组相对静止(零位单实体检查已保证不干涉);相邻组的对(P04×P05 / P06×P07 / P07×P08)同心跨在同一关节轴上,胶囊轴距≈0,塞进模型会在零位误报(实测 P04×P03 超报 15mm)——它们改用 **adj 一维自由区间表**(CAD 实测、与姿态无关,由 `scratch/probe_adj.py` 扫描生成;collide.json 缺 adj 时查看器会显式告警并退回静态限位):J2 [−107°,+128°]、J4 [−112°,+112°]、J5 [−96°,+96°]。
|
||||
- **两级宽相位**:零件包围球(19 球)→ 簇球(每零件的胶囊按主轴投影分桶、每簇 ≤4 枚,主轴由幂迭代 5 轮定向)→ 只有邻近簇才逐对变换胶囊端点做**线段-线段距离**(Ericson《Real-Time Collision Detection》标准式,手写标量直传版,免建数组——碰撞热循环每次调用上万次,分配开销实测占一半)。宽相位阈值带 `rmax+margin` 是有推导的:早期版本漏了 rmax,把 P01_C×P06 在 J1=20°/J2=49° 的 1618mm³ 深撞整对筛掉。
|
||||
- **性能工程**:预分配缓冲 + 代际戳缓存(一次判定 ~2500 个小数组的 GC 开销占 13% → 归零)、轴对齐特化 `rotAxis`(六个关节轴都是 ±X/±Y/±Z,Rodrigues 通用式退化为每轴 4 个非零元素)、簇球再细分实测得不偿失(62 万次球对测试 27ms/check)。
|
||||
|
||||
### 6.3 动态限位:静态值只是零位快照
|
||||
|
||||
travel.json 的单关节限位是"其它轴全零"时的一次性扫描,对"撞固定立柱"这类跨 ≥2 组的约束**系统性过紧**:实测 J2 正向静态 +9.75°(垂臂零位),J1=+90° 时真边界放到 +108.75°。查看器的滑块边界因此分三层:
|
||||
|
||||

|
||||
|
||||
1. **机械区间** = adj 表(相邻件实测,必然无自碰撞);
|
||||
2. **动态边界** = `dynLimit()` 从当前角向限位方向**二分**"collideFree"翻转点(tol 0.25°),上限受电机行程 ±360° 与 adj 封顶;"硬边"(lo 侧相邻件自碰撞,配置无关)直接取静态值不放宽;
|
||||
3. **滑块夹取** `clampAxis()` 带边界缓存:`[轴, 其余轴 1° 量化姿态] → {lo,hi}`,拖同一轴直接取缓存,拖过边界才重算一次二分(~40ms);当前姿已干涉时退回机械区间——否则 dynLimit 会把界外值当边界,滑块再也回不来。
|
||||
|
||||
求解侧对应的是 **solveDyn**:静态解先过全姿态碰撞校验(不安全→按代价升序在候选表里找 ≤1.5×cost+20° 的安全解);解贴在限位边界(0.5° 内)时对非连续、非硬边的轴二分动态边界,放宽 ≥5° 才值得重解(粗档步长 ×2,省 3~4 倍),新解 cost 不升才采用——最坏情况就是静态行为,无回退风险。这就是"动态限位放宽重解"徽章的来源。
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 第七篇 力矩与强度校核(简述)
|
||||
|
||||
运动学之外,同一张关节表还驱动两份物理校核(`torque_balance.py` / `strength_check.py`),保证"能转到位"之外还"转得动、扛得住":
|
||||
|
||||
- **力矩**(`docs/torque_balance.json` + `torque_report.md`):零件质量与质心全部取自 CAD(`bom.json`),在可达姿态网格上搜索每个关节的**最大重力力矩**与 Σm·d² 惯量——不做任何硬编码常数。电机参数为工程假定(连续 2.5 / 峰值 8.0 / 热衰减许用 2.0 N·m,可用环境变量覆盖)。当前结论:J1 重力力矩 5.97 N·m、峰值需求 7.36 N·m(峰值裕度 1.09×),连续负载率超许用(J1 238.8%/J2 171.6%),即**持臂静态工况超出热衰减后的连续额定**——这正是后续减速比/选型迭代的输入。
|
||||
- **强度**(`docs/strength.json`):对关键截面(吊臂根座、吊臂颊板、肩叉腹板等)按梁理论算弯曲应力与挠度,输出安全系数 SF(当前最小截面 SF≈9.1)。
|
||||
|
||||
---
|
||||
|
||||
## 第八篇 自检体系与复现命令
|
||||
|
||||
这套系统最重要的工程习惯是**每个算法都带数值自检**,且自检值绑定代码指纹(FNV-1a over 限位表+运动学常量+求解器源码),改模型或改求解器后指纹失配、页面自动标红要求重算:
|
||||
|
||||
| 自检 | 工具 | 通过线 |
|
||||
|---|---|---|
|
||||
| FK 双实现对账 | `ArmIK.selfTest()` | ≤1e-9 mm(实测 5.7e-13) |
|
||||
| 位置 IK 重构 | 同上 | 逐支精确命中(实测 9.9e-13) |
|
||||
| 姿态 IK 重构 | `poseSelfTest()` | oerr<0.05° 且位置<1e-3 |
|
||||
| 线段距离解析式 | 暴力密集采样对比(scratch 自检脚本) | 逐位一致 |
|
||||
| 螺钉孔对位 | `scratch/fit_check.py` 布尔干涉 | 0 干涉(轴线取样法会误报,弃用) |
|
||||
| 全行程零干涉 | `sweep_v2.py` 全对全扫描 | 单关节推满 0 碰撞 |
|
||||
|
||||
复现命令:
|
||||
|
||||
```bash
|
||||
python src/arm_model.py # 重建模型 + mesh_arm.json + STEP/STL + bom
|
||||
python src/build_viewer.py # 重打包单文件网页查看器
|
||||
python src/sweep_v2.py # 离线行程/碰撞权威扫描(几何变更后必跑)
|
||||
node scratch/compute_precomputed.cjs # 重算预置自检值与指纹
|
||||
python src/torque_balance.py # 力矩校核报告
|
||||
python src/strength_check.py # 强度校核
|
||||
```
|
||||
|
||||
### 文件索引
|
||||
|
||||
| 文件 | 角色 |
|
||||
|---|---|
|
||||
| `src/human_layout.py` | 关节轴/拓扑单一事实源 |
|
||||
| `src/arm_model.py` · `arm_build.py` | 参数化 CAD 与导出 |
|
||||
| `src/sweep_v2.py` | 离线碰撞与行程权威工具 |
|
||||
| `src/shot.py` | 渲染出图(docs/shots/) |
|
||||
| `web/arm-ik-solver.js` | 六轴 IK 求解器(纯 JS,浏览器/Node 双端) |
|
||||
| `web/ik-glue.js` | 交互层:拖拽/Worker/动态限位/动画 |
|
||||
| `src/finalize_travel.py` | 从 sweep 明细重建 travel.json 的维护工具 |
|
||||
| `scratch/fit_capsules.py` | CAD 网格 → 胶囊模型拟合(collide.json) |
|
||||
| `scratch/probe_adj.py` | 相邻打印件 1D 自由区间实测(adj 表) |
|
||||
| `scratch/compute_precomputed.cjs` | 重算预置自检值与代码指纹 |
|
||||
| `scratch/gen_workspace_svg.cjs` | 工作空间剖面图生成(本文图 11) |
|
||||
| `docs/travel.json` · `travel_detail.json` | 滑块硬限位 / 组合折叠明细 |
|
||||
| `docs/collide.json` | 1995 胶囊 + adj 机械区间 + 硬边表 |
|
||||
| `docs/algo_svg/` | 本文全部算法示意图源文件 |
|
||||
@@ -24,7 +24,7 @@ except Exception:
|
||||
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, OUT, WEB, DOCS, SRC_STEP, HALF_STEP, CLAMP_STEP)
|
||||
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
|
||||
@@ -50,6 +50,37 @@ 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 = {}
|
||||
@@ -204,9 +235,59 @@ class Joint:
|
||||
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."""
|
||||
c = [cyl(BORE_R, 5.5, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + front_pad),
|
||||
box(-PCB_W / 2, PCB_W / 2, -PCB_W / 2, PCB_W / 2, -0.6, M["pcba_t"] + 0.4)]
|
||||
"""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):
|
||||
@@ -452,7 +533,7 @@ def collide():
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
|
||||
("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_平打"),
|
||||
|
||||
@@ -60,6 +60,200 @@ def XB(a0, a1, y0, y1, z0, z1):
|
||||
return bx(X_ARM + a0, X_ARM + a1, y0, y1, z0, z1)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ 减重(镂空)--------
|
||||
# 三个手法件(P04/P06/P07)在关节根部都留着一大块"实心过渡料":壁厚最深到 15~17.5mm,
|
||||
# 而 gyroid 填充只有 25~35%,等于用整块实心去抗一个安全系数 19~122 的截面。
|
||||
# 下面两个 helper 把这块料掏掉,规则与 P03 的"帐篷形减重窗"一致:
|
||||
# 1) 不留封闭空腔 —— 减重腔要么朝上敞开,要么是有进出口的窗,打印不憋气、不积料;
|
||||
# 2) 斜面自支撑 —— 斜面与水平面夹角 >= 52°(与竖直面夹角 <= 38°),免支撑;
|
||||
# 3) 内角 R4 圆角 —— 挖出来的洞会把圆截面变成尖角,Kt 从 ~1.2 抬到 ~2.5,
|
||||
# 圆角把应力集中压回去;
|
||||
# 4) 只吃"距表面 > 8mm"的厚料,四周至少留 8mm 壁 —— 受力纤维一根不碰。
|
||||
|
||||
HOLLOW_WALL = 8.0 # 减重腔与原外表面的最小间距(= 留下的最小壁厚)
|
||||
HOLLOW_FILLET = 4.0 # 减重腔内角圆角,与 P03 竖板减重窗一致
|
||||
|
||||
# 三处减重腔的开度。这三个件都是绕 Y 轴弯,弯矩在 X 方向拉压,距中性面越远的料
|
||||
# 贡献越大,所以"少挖、挖窄"是恢复强度最省料的做法。
|
||||
# 数值由 scratch/tune_hollow.py 按"槽区最差安全系数 >= 无槽基线的 95%"扫出来:
|
||||
# P04 档1 X23..35 Y102..154 Z>=525 -> 95% 基线,保留 69% 的减重量
|
||||
# P07 档1 X−3..43 Y103..153 Z>=255 -> 96% 基线,保留 76%
|
||||
# P06 档1 抬高窗底到 Z362(不是收窄) -> 100% 基线,保留 56%
|
||||
# 窗底原来压在 Z358,而该处截面安全系数最低;抬高 4mm 比横向收窄 8mm 划算得多。
|
||||
P04_CH = dict(x0=23.0, x1=35.0, y0=102.0, y1=154.0, z_floor=525.0) # 肩叉立柱
|
||||
P07_CH = dict(x0=-3.0, x1=43.0, y0=103.0, y1=153.0, z_floor=255.0) # 小臂顶板
|
||||
P06_WIN_XZ = [(3.0, 362.0), (37.0, 362.0), (37.0, 371.0),
|
||||
(28.0, 374.0), (12.0, 374.0), (3.0, 371.0)] # 大臂过渡块
|
||||
P06_HOLLOW_WIN = False # 2026-10-03 起关闭:拱形窗正面看是贯通黑洞,不雅观(详见 p_upper_bone 5c)
|
||||
|
||||
|
||||
def hollow_channel(x0, x1, y0, y1, z_floor, z_open, tag="", fil=HOLLOW_FILLET):
|
||||
"""朝上敞开的减重槽:从 z_open 往下切到 z_floor,槽底水平(朝上受支撑)、侧壁竖直。
|
||||
|
||||
竖打(模型 Z = 打印 Z)下这个形状是零支撑的:槽底是水平面、槽壁全是竖直面,
|
||||
顶部完全敞开因而不构成封闭空腔。切完该件变成 U 形槽钢而不是实心料:
|
||||
抗弯靠两侧 X 向腹板(正好落在弯矩拉压区),中间中性层掏空几乎不损失 I。
|
||||
|
||||
x0/x1/y0/y1 用世界坐标;z_open 取一个保证贯通顶面的值。
|
||||
"""
|
||||
tool = bx(x0, x1, y0, y1, z_floor, z_open)
|
||||
if fil > 0:
|
||||
try:
|
||||
tool = cq.Workplane(obj=tool).edges("|Z").fillet(fil).val()
|
||||
except Exception as e:
|
||||
print(" [减重槽] %s 内角圆角跳过: %s" % (tag, e), flush=True)
|
||||
print(" [减重槽] %s: X%.1f..%.1f Y%.1f..%.1f 自 Z%.1f 向上敞开 (四周留壁 >=%.0fmm)"
|
||||
% (tag, x0, x1, y0, y1, z_floor, HOLLOW_WALL), flush=True)
|
||||
return tool
|
||||
|
||||
|
||||
def hollow_tent_window(pts_xz, y0, y1, tag="", fil=HOLLOW_FILLET):
|
||||
"""帐篷形减重窗:XZ 平面画轮廓,沿 Y 双向切穿。
|
||||
|
||||
轮廓下宽上窄(拱形)——窗顶上方的料形成一个尖顶,斜面与水平夹角 >= 52° 自支撑,
|
||||
只有顶部一小段平顶需要桥接(<=20mm,与 P03 同)。轮廓在 X 方向不能吃满整个截面,
|
||||
两侧必须留墙,否则该高度上零件会被切断、失去单实体。
|
||||
"""
|
||||
# XZ 工作平面的法线是 -Y,extrude 会朝 -Y 走,所以起点放在 y1 才能覆盖 y0..y1
|
||||
tool = (cq.Workplane("XZ", origin=(0, y1, 0)).polyline(pts_xz).close()
|
||||
.extrude(y1 - y0).val())
|
||||
if fil > 0:
|
||||
try:
|
||||
tool = cq.Workplane(obj=tool).edges("|Y").fillet(fil).val()
|
||||
except Exception as e:
|
||||
print(" [减重窗] %s 内角圆角跳过: %s" % (tag, e), flush=True)
|
||||
print(" [减重窗] %s: XZ 轮廓 %d 点, 沿 Y %.1f..%.1f 切穿 (帐篷形, 斜面自支撑)"
|
||||
% (tag, len(pts_xz), y0, y1), flush=True)
|
||||
return cq.Shape(tool.wrapped)
|
||||
|
||||
|
||||
# 各关节"电机前端夹紧"统一配方(J2/J3/J4/J5 通用,J1 由 P03 自己那套 S04 凸台实现)。
|
||||
# 电机前端盖外端面:Y 轴关节(J2/J4/J5)在 Y_COVER=156,压盖朝 +Y 生长;
|
||||
# Z 轴关节(J3,roll 电机,轴向 -Z)在 Z_R-56,压盖朝 -Z 生长(-Z 侧是从动件)。
|
||||
# 压盖背面与之 0.0 间隙面压,螺钉只提供压紧力。
|
||||
# 沉孔相位按【关节局部相位】定义(BCD55,72.2°+90k):换算到全局后 Y 轴关节是
|
||||
# 17.8°+90k(与 P03→M1 的 S04 一致),Z 轴关节是同一组数落在全局 (x,y) 平面,
|
||||
# 两种轴向的物理指向完全一致 —— 全机同一套孔位约定,改一处必须同步另一处。
|
||||
CLAMP_BCD = 27.5
|
||||
CLAMP_CLOCK = (17.8, 107.8, 197.8, 287.8) # Y 轴关节的全局 (x,z) 相位
|
||||
CLAMP_CLOCK_LOCAL = tuple((360.0 - a) % 360.0 for a in CLAMP_CLOCK) # = 72.2+90k
|
||||
CLAMP_ROOT = 0.5 # 根部薄环厚度:与前端定位环前端面共面,保证熔接成一体
|
||||
CLAMP_T = 5.0 # 压盖本体厚度
|
||||
CLAMP_CLR = 1.0 # 从动件让位间隙:与 Y 轴关节 hub_gap=6.0 同一个道理
|
||||
CLAMP_BACKOFF = CLAMP_T + CLAMP_CLR - CLAMP_ROOT # =5.5 从动件毂面整体后移量
|
||||
# 【2026-10-05e】"电机前端围挡"统一整改开关,作用于全机 5 处电机前端(J2/J3/J4/J5/J6)。
|
||||
# 围挡本体 = 各零件前端的 **前端定位环**(`front_collar_pocket` 把座孔从 R30.15 扩到
|
||||
# R30.25 时留下的那圈 r30.25..35、6mm 高的环墙)。它比电机前端盖 Ø60.3 凸出 4.75mm,
|
||||
# 把电机前端盖整圈围住;J2/J3/J4/J5 上还骑着一枚 R35 前端压盖(Ø70 再凸 3.2mm)。
|
||||
# 用户 2026-10-05 先在 J3/M3 尾部指出,2026-10-05d 只切敞口那一圈解决;本次把同一刀
|
||||
# 推广到其余四处(J2/M2、J4/M4、J5/M5、J6/M6),几何完全同构。
|
||||
# 为什么不整圈削到与电机齐平(R35 -> R31.8):
|
||||
# 1. 那圈壁与本体在两个方位与外壁**相切**,整圈切完那两片切向楔形会脱空成孤立碎块
|
||||
# (实测 1.32mm3),破坏单实体拓扑;
|
||||
# 2. 压盖根部只有 0.5mm 熔接带,环削窄后连接面从 2.37mm2 掉到 0.78mm2,
|
||||
# 4xM3x8 的夹紧力会把压盖从本体上拉下来。
|
||||
# 所以只切**敞口那一半**:背脊侧定位环继续定位电机前端盖、并作为压盖唯一且足够强的
|
||||
# 支撑/熔接面;压盖本体、Ø37 让位孔、4xM3x8 沉孔、腔口下沿全部不动。
|
||||
# 切口位置见 cut_motor_fence()。设 False 取消(恢复原来的整圈围挡)。
|
||||
FENCE_OPEN_CUT = True
|
||||
|
||||
|
||||
def cut_motor_fence(shape, j, tag, name3, z0=49.4, z1=56.1):
|
||||
"""【2026-10-05e】只切掉电机前端围挡(前端定位环)在**敞口那一半**的环墙。
|
||||
|
||||
与 2026-10-05d 给 P05/M3 那一刀完全同源 —— 当时那记全局盒切
|
||||
(X_ARM±40, Y_AXIS-40..Y_AXIS+3, Z388.9..395.6) 换算到 J3 局部系正好是
|
||||
x±40 / y -3..+40 / z 49.4..56.1。本函数把它从"P05 专用全局盒"泛化成
|
||||
"按关节局部坐标切半个环",J2/J4/J5/J6 于是同一刀通用。
|
||||
|
||||
局部坐标约定(见 trs/place):z 沿电机轴、指向电机前端盖(外端面在 z=COVER_Z=56),
|
||||
x/y 在垂直于轴的平面内。**局部 +y 一律是半包敞口侧** —— 实测 5 处电机舱的
|
||||
本体(环外 r=37/40 有料处 = 背脊侧)方位角全部落在局部角 180..360 = 局部 -y 侧:
|
||||
J2/M2 背脊 240..315 J3/M3 背脊 225..315 J4/M4 背脊 210..330
|
||||
J5/M5 背脊 210..330 J6/M6 背脊 195..345
|
||||
故敞口方向无需逐件配置,统一取局部 +y。
|
||||
|
||||
为什么一对 z 参数能覆盖全机:5 处电机的前端定位环都恰好落在**局部 z 49.5..56**
|
||||
(Y 轴电机前端环 Y150..156 -> z50..56;roll 电机 Z395.5..389 -> z49.5..56)。
|
||||
|
||||
被切掉的只有 r30.25..35 那一圈环墙在敞口侧的部分;压盖本体(4xM3x8 沉孔段)、
|
||||
Ø37 让位孔、腔口下沿、背脊侧整圈定位环全部不动。
|
||||
切口边界取在局部 y=-3.0 而不是 0:环壁在局部 y=-3 处仍与背脊侧实体连通,
|
||||
切出来的是真正的缺口而不是零厚度缝,单实体拓扑天然保持
|
||||
(P05 实测切完 solids=1;2026-10-05c 整圈削 R31.8 那版会掉出 1.32mm3 碎块)。
|
||||
"""
|
||||
if not FENCE_OPEN_CUT:
|
||||
return shape
|
||||
shape = shape.cut(j.L(bx(-40.0, 40.0, -3.0, 40.0, z0, z1)))
|
||||
print(" [围挡] %s: 只切敞口侧那一圈前端定位环(局部 y -3..+40, z %.1f..%.1f);"
|
||||
"背脊侧整圈保留定位前端盖并支撑压盖,压盖本体/沉孔/腔口下沿不变"
|
||||
% (name3, z0, z1), flush=True)
|
||||
return shape
|
||||
|
||||
|
||||
def motor_front_clamp(px, pz, plate_t=CLAMP_T, tag="", axis="Y", py=0.0, z_face=None):
|
||||
"""电机前端压盖:把"只被后端安装板单面锁死"改成前后双向面压夹紧。
|
||||
|
||||
原设计里每个关节的电机都只有后端板 4xM3x10(S02)锁在尾端面上,前端盖完全裸露,
|
||||
大扭矩下电机绕自身轴在 Ø60.3 座孔里点头/翘起。这里在电机前端盖外端面加一枚
|
||||
与后端板同径的 R35 压盖,用 4xM3x8(BCD55)在前端盖上再夹一次 —— 电机被前后两块
|
||||
同心 Ø60.3 端面双向夹死,倾斜自由度归零;夹紧力全部走面压,螺钉不参与定位。
|
||||
|
||||
两个轴向共用同一套尺寸,只换刀具体系(不重复实现任何半径/相位):
|
||||
axis="Y"(J2/J4/J5):关节轴过 (px, pz),前端盖外端面在 Y_COVER,压盖朝 +Y 长;
|
||||
axis="Z"(J3 roll,轴向 -Z):关节轴过 (px, py),前端盖外端面在 z_face,压盖朝 -Z 长。
|
||||
|
||||
局部坐标(s 从前端盖外端面起算,s>0 一律"背离电机",两个轴向同号):
|
||||
- 压盖 R35,s 从 -CLAMP_ROOT(与前端定位环前端面共面,保证熔接成一体)到 plate_t;
|
||||
- 背面精确落在 s=0,不让根部那 0.5mm 啃到端盖 R30.15 的外圆;
|
||||
- Ø37 中央让位孔(R18.5)让开从动件的直锁凸台 R17.8 与 3xS01 输出螺钉头;
|
||||
- 4xM3x8:Ø3.4 通孔 + Ø6.0 沉孔深 3.0(自前端面沉到 s=2),圆柱头全沉入,
|
||||
头面压孔口台阶,中间 2.0mm 压盖本体受压。
|
||||
"""
|
||||
if axis == "Y":
|
||||
def C(r, s0, s1, du=0.0, dv=0.0):
|
||||
return Ycyl(r, Y_COVER + s0, Y_COVER + s1, px + du, pz + dv)
|
||||
elif axis == "Z":
|
||||
def C(r, s0, s1, du=0.0, dv=0.0):
|
||||
return cyl(r, z_face - s1, z_face - s0, px + du, py + dv)
|
||||
else:
|
||||
raise ValueError("motor_front_clamp: axis 只能是 Y 或 Z,收到 %r" % (axis,))
|
||||
|
||||
s0, s1 = -CLAMP_ROOT, plate_t
|
||||
fp = C(35.0, s0, s1)
|
||||
fp = fp.cut(C(18.5, s0 - 0.5, s1 + 0.5)) # Ø37 中央让位孔
|
||||
fp = fp.cut(C(30.15, s0 - 0.5, 0.0)) # Ø60.3 让位槽
|
||||
for deg in CLAMP_CLOCK_LOCAL:
|
||||
rad = math.radians(-deg) # 全局相位 = -局部相位
|
||||
fu, fv = CLAMP_BCD * math.cos(rad), CLAMP_BCD * math.sin(rad)
|
||||
fp = fp.cut(C(3.0, 2.0, s1 + 0.5, fu, fv)) # Ø6.0 沉孔
|
||||
fp = fp.cut(C(1.7, s0 - 0.5, 2.0, fu, fv)) # Ø3.4 通孔
|
||||
where = ("Y%.1f..%.1f" % (Y_COVER - CLAMP_ROOT, Y_COVER + plate_t)) if axis == "Y" \
|
||||
else ("Z%.1f..%.1f" % (z_face - plate_t, z_face + CLAMP_ROOT))
|
||||
print(" [前端压盖] %s: R35 %s, 4xM3x8 BCD55@%s (%s 轴)"
|
||||
% (tag, where, "/".join("%.1f" % d for d in CLAMP_CLOCK), axis), flush=True)
|
||||
return fp
|
||||
|
||||
|
||||
def hub_m2_holes(shape, j, cb_z1, th_z1=58.0, th_z0=54.0, cb_z0=58.0,
|
||||
th_r=1.4, cb_r=2.8):
|
||||
"""在从动毂上开 4x M2 直锁螺钉孔:Ø2.8 通孔 + Ø5.6 沉孔,BCD 见 AB.M["m2_bcd"]。
|
||||
|
||||
全机 5 处(J1 叉板 / J2+J3 直连毂 / J4+J5 直连毂)原本各抄一份同样的循环,
|
||||
只有沉孔顶 z 与销孔顶 z 两处随件而异 —— 抄一次错一处就是一处真 bug。
|
||||
现在统一走这里,各调用点只传自己那两个 z。
|
||||
|
||||
cb_z0 默认 58.0 = th_z1,台阶因此落在通孔顶端;P03 叉板那一处例外
|
||||
(沉孔台阶固定在 63.0 不随凸台前移,见 p_fork 的注释),故两个 z 都可传。
|
||||
"""
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
shape = shape.cut(j.L(cyl(th_r, th_z0, th_z1, u, v)))
|
||||
shape = shape.cut(j.L(cyl(cb_r, cb_z0, cb_z1, u, v)))
|
||||
return shape
|
||||
|
||||
|
||||
def make_direct_hub(shape, j, r_disc=35.0, z_out=74.0):
|
||||
"""优化直连法兰:增加导向倒角"""
|
||||
disc = cyl(r_disc, 56.5, z_out)
|
||||
@@ -67,13 +261,7 @@ def make_direct_hub(shape, j, r_disc=35.0, z_out=74.0):
|
||||
chamfer_cone = cyl(18.5, 54.5, 54.942)
|
||||
shape = shape.fuse(j.L(disc)).fuse(j.L(boss)).cut(j.L(chamfer_cone))
|
||||
shape = shape.cut(j.L(cyl(3.5, 54.0, z_out + 30.0)))
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 58.0, u, v)
|
||||
cb = cyl(2.8, 58.0, z_out + 40.0, u, v)
|
||||
shape = shape.cut(j.L(th)).cut(j.L(cb))
|
||||
return shape
|
||||
return hub_m2_holes(shape, j, cb_z1=z_out + 40.0)
|
||||
|
||||
|
||||
# ========== 躯干部分 ==========
|
||||
@@ -825,14 +1013,14 @@ def p_girdle(j1):
|
||||
# Ø60.7 圆柱孔壁从前端面平直贯通到舱底(无窄颈、无台阶、无喇叭锥);
|
||||
# 舱底 = 一个完整的平坦环面(X=5.06+X_BAY_EXT=8.06,与插槽末端墙共面,中开 Ø39 凸台过孔)
|
||||
# ——即电机前限位面。电机轴向由后端法兰 4xM3x16 约束;
|
||||
# 前端锁紧由 4 个 Ø10 局部凸台上的 M3 沉头螺钉承担(见下方凸台段)。
|
||||
# 前端锁紧由 4 个 Ø10 局部凸台上的 M3 圆柱头螺钉承担(见下方凸台段)。
|
||||
motor_pocket = Xcyl(R_BORE, X_MOUNT - 0.2, 5.06 + X_BAY_EXT, Y_ARM, Z_ARM)
|
||||
s = s.cut(motor_pocket)
|
||||
|
||||
# 4x 前置 M3 锁紧螺钉凸台(BCD=55mm,17.8°/107.8°/197.8°/287.8°):
|
||||
# 平直舱孔的沉头座位由 4 个 Ø10 局部凸台承载——凸台位于舱孔前段 8.06..11.5,
|
||||
# 电机前端面(5.0)之前 3mm,电机斜插路径不经过;M3x10 沉头从凸台端面旋入
|
||||
# 电机前端面,咬合 3.5mm,兼作电机前端防转定位。
|
||||
# 电机前端面(5.0)之前 3mm,电机斜插路径不经过;M3x8 圆柱头坐在凸台沉孔
|
||||
# 底(x=9.5)旋入电机前端面,咬合 3.5mm,兼作电机前端防转定位。
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
rad = math.radians(deg)
|
||||
fy = Y_ARM + 27.5 * math.cos(rad)
|
||||
@@ -899,27 +1087,10 @@ def p_fork(j1, j2):
|
||||
front_collar = Ycyl(35.0, Y_COVER - 6.5, Y_COVER - 0.5, X_ARM, Z_SH)
|
||||
s = rear_plate.fuse(front_collar)
|
||||
|
||||
# 2b. 【2026-10-02 新增】M2 前端压盖 (Y: 155.5..161.0, R=35,与后端板同径)
|
||||
# 根部与 front_collar 前端面 (Y_COVER-0.5=155.5) 共面熔接成一体,无缝隙、无悬浮面。
|
||||
# 压盖背面 (Y_COVER=156.0) 与电机前端盖外端面 0.0 间隙面压贴合;Ø37 中央让位孔
|
||||
# 让出 P05 的 J2 直锁凸台 (R17.8) 与 3xS01 输出螺钉头;Ø60.3 让位槽把压盖背面
|
||||
# 精确落在 Y=156.0,不让根部那 0.5mm 啃到电机前端盖 R30.15 的外圆。
|
||||
FP_Y0 = Y_COVER - 0.5 # 155.5 压盖根部(= front_collar 前端面,保证熔接)
|
||||
FP_Y1 = Y_COVER + 5.0 # 161.0 压盖前端面(与 P05 法兰盘 162.0 留 1.0mm 动间隙)
|
||||
FP_BORE = 27.5 # 4xM3 分布圆半径 (BCD55,与 P03→M1 的 S04 同约定)
|
||||
fp = Ycyl(35.0, FP_Y0, FP_Y1, X_ARM, Z_SH)
|
||||
fp = fp.cut(Ycyl(18.5, FP_Y0 - 0.5, FP_Y1 + 0.5, X_ARM, Z_SH)) # Ø37 中央让位孔
|
||||
fp = fp.cut(Ycyl(30.15, FP_Y0 - 0.5, Y_COVER, X_ARM, Z_SH)) # Ø60.3 让位槽
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
rad = math.radians(deg)
|
||||
fu, fz = X_ARM + FP_BORE * math.cos(rad), Z_SH + FP_BORE * math.sin(rad)
|
||||
# Ø6.0 沉孔深 3.0 (自前端面 FP_Y1 往 -Y 沉到 Y=158.0):M3 圆柱头(Ø5.5x3.0)
|
||||
# 全沉入压盖,头面 Y=158.0 压在 Ø3.4 孔口台阶上;压盖背面(156.0)压电机前端盖,
|
||||
# 中间 2.0mm 压盖本体受压 —— 夹紧力全部走面压,螺钉只提供压紧力不提供定位。
|
||||
fp = fp.cut(Ycyl(3.0, Y_COVER + 2.0, FP_Y1 + 0.5, fu, fz))
|
||||
fp = fp.cut(Ycyl(1.7, Y_COVER - 0.5, Y_COVER + 2.0, fu, fz))
|
||||
# 压盖在第 7 步 M2 接口成型(座孔/让位槽)之后才熔接,避免被那些 R30.15~30.5 的
|
||||
# 圆柱刀具蹭掉根部一圈 0.1mm 薄边。
|
||||
# 2b. 【2026-10-02 新增】M2 前端压盖(几何与理由见 motor_front_clamp)。
|
||||
# 必须在第 6/7 步所有 R30.15~30.5 圆柱刀具走完之后才熔接,否则压盖根部会被
|
||||
# front_collar_pocket / c_m2 啃掉一圈 0.1mm 薄边(见 7b)。
|
||||
fp = motor_front_clamp(X_ARM, Z_SH, tag="P04/M2")
|
||||
|
||||
# 3. J1 从动法兰圆盘已删除:原 R=36.5 圆盘盘面在 column 前缘形成圆盘鼓楞,
|
||||
# J1 与 P04 的连接由 boss(Ø35.6 贯通 P03 前盖中心孔 R=19.5)直连电机输出盘
|
||||
@@ -964,14 +1135,34 @@ def p_fork(j1, j2):
|
||||
xz_shape = cq.Shape(xz_wire.extrude(100.0, both=True).val().wrapped)
|
||||
|
||||
# 5. YZ 平面包络
|
||||
yz_pts = [
|
||||
(Y_AXIS - 36.5, Z_RL + 36.5),
|
||||
(Y_AXIS + 36.5, Z_RL + 36.5),
|
||||
(Y_COVER - 0.5, Z_SH + 36.0),
|
||||
(Y_COVER - 0.5, Z_SH),
|
||||
# 【2026-10-04 视觉整改】电机上方那块 4048mm2 的大平面改成 Hermite 弧面。
|
||||
# 原来第 2->3 点之间是一条直线 (164.5,646.5)->(155.5,526),沿 X 方向拉成
|
||||
# 4048mm2 的平板(实测法向 (0,1,-0.07),带 4 度斜度),正对电机上方 ——
|
||||
# 两侧被 xz_wire 的 R18 圆弧挤出去,形成"中间一块平、两边鼓"的观感。
|
||||
# 改法:把这段直线换成三次 Hermite 曲线,两端切线分别沿 Y(接上面 Z=646.5
|
||||
# 的顶段)与沿 Z(接下面 Y=155.5 的竖直段),保证 G1 连续无缝;
|
||||
# 中部向 +Y 外凸约 3mm,弧度观感与两侧 R18 一致。
|
||||
# 为什么不用单段圆弧:A 处切线要沿 Y、B 处要沿 Z,单个圆心无法同时满足
|
||||
# (解出来 R=9 与 R=120.5 矛盾),必须用 Hermite/Bezier —— 与 P05/P06
|
||||
# 的 Loft 过渡同一套做法。
|
||||
# 注意:顶点绕序必须与原列表一致(先 y_b 后 y_a),否则多边形自交、
|
||||
# 布尔后零件会碎掉(实测会掉到 48cm3 / 3 块实体)。
|
||||
y_a = (Y_COVER - 0.5, Z_SH + 36.0) # (155.5, 526.0) 曲线终点
|
||||
y_b = (Y_AXIS + 36.5, Z_RL + 36.5) # (164.5, 646.5) 曲线起点
|
||||
_d = math.hypot(y_b[0] - y_a[0], y_b[1] - y_a[1])
|
||||
_m0y, _m1z = -0.5 * _d * 0.35, 0.5 * _d * 0.5 # 切线:起点沿 -Y,终点沿 +Z
|
||||
yz_pts = [(Y_AXIS - 36.5, Z_RL + 36.5)]
|
||||
for k in range(13):
|
||||
t = k / 12.0
|
||||
h00 = 2 * t ** 3 - 3 * t ** 2 + 1
|
||||
h10 = t ** 3 - 2 * t ** 2 + t
|
||||
h01 = -2 * t ** 3 + 3 * t ** 2
|
||||
h11 = t ** 3 - t ** 2
|
||||
yz_pts.append((h00 * y_b[0] + h10 * _m0y + h01 * y_a[0],
|
||||
h00 * y_b[1] + h11 * _m1z + h01 * y_a[1]))
|
||||
yz_pts += [(Y_COVER - 0.5, Z_SH),
|
||||
(Y_REAR - 8.0, Z_SH),
|
||||
(Y_REAR - 8.0, Z_SH + 36.0),
|
||||
]
|
||||
(Y_REAR - 8.0, Z_SH + 36.0)]
|
||||
yz_solid = Xplate(yz_pts, -20.0, 60.0)
|
||||
|
||||
c_int = BRepAlgoAPI_Common(yz_solid.wrapped, xz_shape.wrapped)
|
||||
@@ -984,33 +1175,60 @@ def p_fork(j1, j2):
|
||||
s = s.cut(Ycyl(30.5, Y_REAR + 0.5, Y_COVER - 6.5, X_ARM, Z_SH))
|
||||
|
||||
# 7. M2 接口成型
|
||||
# 承压面起点用 AB.REAR_SEAT(=0.6 台肩 + 2.0 公差),与 Joint.pocket 同源:
|
||||
# P04 走的是自己那套 BORE_R 圆柱刀具(没调 Joint.pocket),这里必须同步改,
|
||||
# 否则 J2 关节的电机公差位仍是旧的 0.6,与 J3~J6 不一致。
|
||||
# 【2026-10-05】承压面段加一刀 SEAT_R(=R35) 圆腔,SEAT_TOP=+1.0。
|
||||
# 取满 R35 才能让承压面与外壁齐平成**一个平面**(用户要求:把凸台、围挡
|
||||
# 一起弄平,做成和电机两头齐平的平面)。取小于 R35 必然留下一圈凸起
|
||||
# 2.6mm 的围挡环墙 —— 详见 Joint.pocket 的 docstring。
|
||||
s = j2.front_collar_pocket(s, r_bore=30.25, z0=50.0, z1=56.5)
|
||||
s = j2.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
c_m2 = [cyl(BORE_R, -0.6, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + 0.6)]
|
||||
c_m2 = [cyl(BORE_R, -AB.REAR_SEAT, COVER_Z - 3.5), cyl(CB_R, COVER_Z - 3.5, COVER_Z + 0.6),
|
||||
cyl(AB.SEAT_R, -AB.REAR_SEAT, AB.SEAT_TOP)]
|
||||
s = s.cut(*[j2.L(item) for item in c_m2])
|
||||
|
||||
|
||||
# 7b. M2 前端压盖熔接(见 2b):必须在 6/7 步所有 R30.15~30.5 圆柱刀具走完之后,
|
||||
# 否则压盖根部会被 front_collar_pocket / c_m2 啃掉一圈 0.1mm 薄边。
|
||||
s = s.fuse(fp)
|
||||
|
||||
# 7b2.【2026-10-05e】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
# 围挡 = front_collar_pocket 把座孔扩到 R30.25 后留下的那圈 r30.25..35、6mm 高
|
||||
# 环墙(Y149.5..155.5),比电机前端盖 Ø60.3 凸 4.75mm,把前端盖整圈围住;
|
||||
# 上面还骑着 R35 压盖(Ø70 再凸 3.2mm)。切敞口(局部 +y = 全局 -Z)那半圈后,
|
||||
# 背脊(局部 -y = 全局 +Z)侧整圈保留:继续定位前端盖、并作为压盖唯一且足够强
|
||||
# 的支撑面(4.75mm 宽 x 0.5mm 熔接带,本次完全不碰)→ M2 仍是
|
||||
# 后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压;压盖本体/Ø37 孔/4xM3x8 沉孔/
|
||||
# 腔口下沿 Y155.5 全部不动。
|
||||
s = cut_motor_fence(s, j2, "P04/M2", "P04")
|
||||
|
||||
# 7c. 【2026-10-02 新增】立柱减重槽(见 hollow_channel)。
|
||||
# 立柱 Z525..635 整段是壁厚最深处达 17.5mm 的实心料,而 J2 在 z=560/600 的
|
||||
# 安全系数有 19。挖成朝上敞开的 U 形槽后立柱变成槽钢:抗弯的两片 X 向腹板
|
||||
# 正好落在弯矩拉压区(z=560 截面 X13..45,中性面在 X29,腹板占 X13~23/X35~45),
|
||||
# 被掏掉的正是中性层附近几乎不参与抗弯的料。
|
||||
# 槽底 Z525 以下保留整块实心过渡到电机座;顶部敞开故非封闭腔、免支撑。
|
||||
# 必须在 8 步 J1 凸台/螺钉孔之前切:槽的 X23..35 范围离 J1 凸台(X<=11.5)与
|
||||
# 3xS01 沉孔(X12..14)都有富余,但先切可以保证后面那些刀具不会把槽壁打薄。
|
||||
s = s.cut(hollow_channel(P04_CH["x0"], P04_CH["x1"], P04_CH["y0"], P04_CH["y1"],
|
||||
P04_CH["z_floor"], 660.0, tag="P04立柱"))
|
||||
|
||||
# 8. 头部转接凸台加长(贯通 P03 前盖中心孔,跨越装配缝直连 J1 输出盘)
|
||||
boss = cyl(17.8, 54.942, 61.5 + X_FORK_GAP)
|
||||
s = s.fuse(j1.L(boss))
|
||||
s = s.cut(j1.L(cyl(3.5, 54.0, 74.0 + 30.0)))
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M['m2_bcd'] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M['m2_bcd'] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 63.0 + X_FORK_GAP, u, v)
|
||||
# 沉孔台阶固定在 63.0(不随前移):M2x10 旋入电机输出毂的咬合量与改前一致;
|
||||
# 螺钉头(63..65)相对叉板背面(64.5)外露 0.5mm,位于 2mm 叉缝空隙内,无干涉。
|
||||
cb = cyl(2.8, 63.0, 74.0 + 40.0 + X_FORK_GAP, u, v)
|
||||
s = s.cut(j1.L(th)).cut(j1.L(cb))
|
||||
s = hub_m2_holes(s, j1, cb_z1=74.0 + 40.0 + X_FORK_GAP,
|
||||
th_z1=63.0 + X_FORK_GAP, cb_z0=63.0)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
|
||||
def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hub_gap=1.0):
|
||||
def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hub_gap=1.0,
|
||||
roll_clamp=False):
|
||||
"""Half-wrapped (半包) 双曲面有机流线型 Pitch-to-Roll 结构座 (P05 & P08):
|
||||
1. 顶部从动法兰与前侧 Loft 自然相切顺接;
|
||||
2. 严格清除 z_plate 顶面以上与上级旋转件贴面的多余材料 (cut_top_solid),保证关节大范围摆动动态 0 干涉;
|
||||
@@ -1020,9 +1238,15 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hu
|
||||
hub_gap = 从动法兰盘背面与上级电机前端盖之间的轴向让位间隙。
|
||||
默认 1.0(贴合防磨)。P05 传 6.0:J2 处 P04 新增了 M2 前端压盖
|
||||
(Y 155.5..161.0),必须让出这 6.0mm 才能装下;见 p_shoulder_roll_base。
|
||||
|
||||
roll_clamp = 本件作为 roll 电机【前端夹紧件】时为 True(J3/P05 夹 M3)。
|
||||
roll 电机(J3/J6)轴向是 -Z,前端盖外端面在 z_r-56,-Z 侧是从动件:
|
||||
压盖朝 -Z 长(Z_COVER-5..Z_COVER),从动件毂面必须整体后移 CLAMP_BACKOFF=5.5
|
||||
让位(与 Y 轴关节 hub_gap=6.0 同一个道理,只是基准间隙 0.5 而非 1.0)。
|
||||
P08 传 False:J6 的 M6 前端压盖尚未做(从动件 P09 让位要改工具法兰直锁凸台)。
|
||||
"""
|
||||
z_bot = z_r - 56.0 # roll 电机底端 (front collar)
|
||||
z_plate = z_r + 7.0 # roll 电机顶端安装板 (rear plate top)
|
||||
z_plate = z_r + 8.0 # roll 电机顶端安装板 (rear plate top);见第 8 步 t_plate 说明
|
||||
y_out = HL.Y_HUB + HL.PLATE_T # 174.0 (俯仰直连法兰外侧面)
|
||||
y_hub_in = HL.Y_COVER + hub_gap # 157.0 / (P05) 162.0 从动法兰内侧面
|
||||
z_hub_in = y_hub_in - HL.Y_REAR # 法兰盘背面的电机局部 z(凸台长度跟着它走)
|
||||
@@ -1096,7 +1320,10 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hu
|
||||
s = s.cut(clearance)
|
||||
|
||||
# 8. 下方电机安装孔位与定位止口
|
||||
s = j_roll.rear_mount_holes(s, t_plate=7.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
# t_plate=8.0:Joint.pocket 的尾端面承压面让空了 MOTOR_TOL=2.0,承压面到螺钉沉孔
|
||||
# 台阶只剩 t_plate-cb_depth-REAR_SEAT = 8-4-2.6 = 1.4mm 膜厚;给到 8 而不是原来的
|
||||
# 7,是为了不让这层膜退化成 0.4mm。7->8 让 P05/P08 的杯体外壳顶 z_plate 抬 1mm。
|
||||
s = j_roll.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
s = j_roll.front_collar_pocket(s, r_bore=30.25, z0=50.0, z1=56.5)
|
||||
s = j_roll.pocket(s)
|
||||
|
||||
@@ -1109,12 +1336,7 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hu
|
||||
boss = cyl(17.8, 54.942, z_hub_in + 2.5)
|
||||
s = s.fuse(j_pitch.L(boss))
|
||||
s = s.cut(j_pitch.L(cyl(3.5, 54.0, 74.0 + 30.0)))
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 58.0, u, v)
|
||||
cb = cyl(2.8, 58.0, 74.0 + 40.0, u, v)
|
||||
s = s.cut(j_pitch.L(th)).cut(j_pitch.L(cb))
|
||||
s = hub_m2_holes(s, j_pitch, cb_z1=74.0 + 40.0)
|
||||
|
||||
# 11. L 型转角连接处圆角过渡(圆角 R=2.0mm):
|
||||
# 平滑消除立柱顶面与竖直臂交汇处的 90° 尖角与应力集中
|
||||
@@ -1125,19 +1347,60 @@ def make_pitch_to_roll_bracket(j_pitch, j_roll, z_pitch, z_r, name="bracket", hu
|
||||
except Exception as e:
|
||||
print(f"[{name}] L型转角圆角异常: {e}")
|
||||
|
||||
# 12. roll 电机(M3)前端压盖:与 P04/P06/P07 的 M2/M4/M5 压盖同一套配方,
|
||||
# 只是 roll 电机轴向为 -Z,压盖朝 -Z 长(见 motor_front_clamp 的 axis="Z")。
|
||||
# 熔接必须放在最后:第 6/7/8/9 步所有 R30.15~30.5 圆柱刀具都走完之后才接,
|
||||
# 否则压盖根部那 0.5mm 薄环会被 front_collar_pocket / 定子孔啃掉一圈。
|
||||
if roll_clamp:
|
||||
s = s.fuse(motor_front_clamp(HL.X_ARM, 0.0, tag="%s/M3" % name[:3],
|
||||
axis="Z", py=HL.Y_AXIS, z_face=z_r - COVER_Z))
|
||||
|
||||
# 13.【2026-10-05d】只切"围挡"那一圈:敞口(-Y)侧的前端定位环 + 压盖根部薄环。
|
||||
# 用户 2026-10-05 指出这圈环墙把 M3 前端盖和 P06 的 J3 凸台围在中间;我第一版
|
||||
# 把整个前端(定位环 + 压盖 + 腔口下沿 389->395)都切了,电机前端没了支撑面 ——
|
||||
# 过度。现在只切敞口这一圈,Y 界限与第 6 步 cut_open 完全一致
|
||||
# (Y_AXIS-40 .. Y_AXIS+3),保证 -Y 半圈从 Z=395.5 一路开到压盖背面 Z=389;
|
||||
# 背脊(+Y)侧整圈定位环保留:
|
||||
# - 继续定位 M3 前端盖(内孔 r30.25,前端盖 Ø60.3 -> 单边 0.10mm);
|
||||
# - 继续当 R35 压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带,本次改动
|
||||
# 完全不碰)→ M3 仍是后端板 4xM3x10 + 前端压盖 4xM3x8 双向面压夹紧;
|
||||
# - 压盖本体 / Ø37 让位孔 / 4xM3x8 沉孔 / 腔口下沿 z_bot=389 全部不动,
|
||||
# 压盖敞口侧的端面从 Z=389.5 降到 389 与背面齐平,视觉上不再凸出。
|
||||
# 为什么不能整圈削到与电机齐平(R35->R31.8,见 FENCE_OPEN_CUT 处的说明):
|
||||
# ① 这圈壁与第 3 步 Loft 在 X=±35 处与 Ø70 柱面**相切**,整圈切完那两片切向楔形
|
||||
# (实测 1.32mm3)会脱空成孤立碎块,破坏单实体拓扑;
|
||||
# ② 压盖根部只有 0.5mm 熔接带,环削到 1.55mm 宽后连接面从 2.37mm2 掉到 0.78mm2,
|
||||
# 4xM3x8 的夹紧力会把压盖从本体上拉下来。
|
||||
# 附带效果:盒切把 Loft 在 X=±35 与 Ø70 柱面相切的那两片切向楔形一起带走
|
||||
# (楔形位于 Y_AXIS 附近 < Y_AXIS+3),不会留下孤立碎块 -> 单实体拓扑保持。
|
||||
# 13.【2026-10-05e】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
# P05(M3) 有前端压盖、环上骑着压盖;P08(M6) 还没有 M6 前端压盖(roll_clamp=False),
|
||||
# 环是光板 —— 两者都只切敞口(局部 +y)那半圈定位环,背脊侧整圈保留,
|
||||
# 继续定位电机前端盖。P05 的压盖因此仍有 4.75mm x 0.5mm 的熔接带作支撑面。
|
||||
if roll_clamp or FENCE_OPEN_CUT:
|
||||
s = cut_motor_fence(s, j_roll, "%s/M3" % name[:3], name[:3])
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def p_shoulder_roll_base(j2, j3, half=None):
|
||||
"""P05_大臂旋转座_螺旋体30%_墙4底5_竖打(深度圆角优化版)
|
||||
|
||||
hub_gap=6.0:J2 处 P04 新增了 M2 前端压盖(前端夹紧电机,消除大扭矩下的倾斜),
|
||||
压盖占 Y 155.5..161.0,所以从动法兰盘背面必须从 Y=157 后移到 Y=162(留 1.0mm 动间隙)。
|
||||
P08 保持默认 1.0 —— J5 处 P07 没有前端压盖,不需要这段轴向空间。
|
||||
本件在链路上有两重身份,两处让位都是为前端压盖:
|
||||
1. hub_gap=6.0(从动侧):J2 处 P04 新增了 M2 前端压盖(前端夹紧电机,消除大扭矩下的
|
||||
倾斜),压盖占 Y 155.5..161.0,所以从动法兰盘背面必须从 Y=157 后移到 Y=162
|
||||
(留 1.0mm 动间隙)。
|
||||
2. roll_clamp=True(前端夹紧侧):P05 自己抱着 M3(J3,-Z 轴),原来也只有后端板
|
||||
4xM3x10 单面锁死;现在按 motor_front_clamp 的统一配方在 M3 前端盖外端面
|
||||
(Z=389)加一枚 R35 压盖(Z 384..389,4xM3x8 BCD55),与后端板前后双向面压,
|
||||
倾斜自由度归零。代价是对侧从动件 P06 的 J3 毂面整体后移 CLAMP_BACKOFF=5.5
|
||||
(Z 388.5→383.0),见 p_humerus_bone。
|
||||
P08 保持默认 —— J5 处 P07 的前端压盖已做(只需 6.0 的 hub_gap),而 J6 的 M6
|
||||
前端压盖尚未做,故 roll_clamp 传 False。
|
||||
"""
|
||||
return make_pitch_to_roll_bracket(j2, j3, Z_SH, Z_R3,
|
||||
name="P05_大臂旋转座_螺旋体30%_墙4底5_竖打",
|
||||
hub_gap=6.0)
|
||||
hub_gap=6.0, roll_clamp=True)
|
||||
|
||||
|
||||
def p_humerus_bone(j3, j4):
|
||||
@@ -1145,11 +1408,17 @@ def p_humerus_bone(j3, j4):
|
||||
受 J3 (大臂自转, -Z轴, hub@Z_HUB3=380) 驱动,
|
||||
向下延伸构成大臂主体骨架,底端抱紧 M4 (肘部俯仰电机, +Y轴, 轴线位于 Z_EL=332.0)。
|
||||
1. 借鉴 P05 大臂旋转座几何放样算法:
|
||||
- 顶端结合面 (Z = 388.5mm):与 J3 轴径严格相切的 Ø70mm 正圆形轮廓,彻底消除原先外突的方形水平阶梯;
|
||||
- 顶端结合面 (Z = 383.0mm):与 J3 轴径严格相切的 Ø70mm 正圆形轮廓,彻底消除原先外突的方形水平阶梯;
|
||||
- 底端结合面 (Z = 367.0mm):平滑展开融入 M4 电机套筒的 70x63.5mm 方形截面;
|
||||
- 过渡区 (Z: 367.0..388.5mm):采用 Hermite s(t) = 3t² - 2t³ 驱动 20 层放样切片连续渐变;
|
||||
- 过渡区 (Z: 367.0..383.0mm):采用 Hermite s(t) = 3t² - 2t³ 驱动 20 层放样切片连续渐变;
|
||||
2. 下部抱紧套筒与安装板 (R=35mm),电机定子 100% 裸露散热开窗;
|
||||
3. 严格单实体 (solids = 1) 且与上下级旋转构件及电机实现动态 0 碰撞干涉。
|
||||
3.【2026-10-03】J3 毂面整体后移 CLAMP_BACKOFF=5.5(顶端 388.5→383.0,直锁凸台
|
||||
59.5→65.0):P05 新增 M3 前端压盖,占 Z 384..389,原先 388.5 的毂面只剩 0.5mm
|
||||
让位放不下压盖。配方与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源:
|
||||
压盖 5.0 + 动间隙 1.0 - 原基准 0.5 = 5.5。凸台同步加长,3xS01 输出螺钉的头面
|
||||
台阶(局部 s=58)、杆长、咬合量与改前完全一致,J3 直锁与零干涉不受影响。
|
||||
压盖中央 Ø37 让位孔(R18.5)也让开 R17.8 凸台,单边余 0.7mm。
|
||||
4. 严格单实体 (solids = 1) 且与上下级旋转构件及电机实现动态 0 碰撞干涉。
|
||||
"""
|
||||
# 1. M4 底部抱紧套筒与后端安装板 (R=35mm)
|
||||
rear_m4 = Ycyl(35.0, Y_REAR - 8.0, Y_REAR, X_ARM, Z_EL)
|
||||
@@ -1157,7 +1426,10 @@ def p_humerus_bone(j3, j4):
|
||||
s = rear_m4.fuse(front_m4)
|
||||
|
||||
# 顶端结合面与过渡高度
|
||||
z_top = 388.5
|
||||
# z_top 由 M3 前端盖外端面反推:压盖占 [Z_COVER-CLAMP_T, Z_COVER],再留 CLAMP_CLR
|
||||
# 动间隙 -> 383.0(改前 388.5,与前端盖只有 0.5mm,放不下压盖)。
|
||||
z_cover3 = Z_R3 - COVER_Z # 389.0 M3 前端盖外端面
|
||||
z_top = z_cover3 - CLAMP_T - CLAMP_CLR # 383.0 J3 毂面(= 388.5 - CLAMP_BACKOFF)
|
||||
z_bot = Z_EL + 35.0 # 367.0
|
||||
|
||||
# 2. 下部实心基座块 (Z: Z_EL..z_bot + 0.5)
|
||||
@@ -1199,17 +1471,50 @@ def p_humerus_bone(j3, j4):
|
||||
s = j4.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
s = j4.pocket(s)
|
||||
|
||||
# 6. J3 直连法兰结构(凸台延伸至 59.5 跨过结合面牢固熔接,保持单实体与零干涉)
|
||||
boss = cyl(17.8, 54.942, 59.5)
|
||||
# 5b. 【2026-10-02 新增】M4 前端压盖 4xM3x8(BCD55):M4 原来只被后端板单面锁死,
|
||||
# 大扭矩下会绕 J4 轴在 Ø60.3 座孔里点头。现在前端也面压夹一次(见 motor_front_clamp)。
|
||||
# 熔接放在所有 R30.15~30.5 圆柱刀具之后,理由同 p_fork 7b。P07 的 J4 从动侧
|
||||
# 已相应后移到 Y>=162(见 p_forearm_bone 的 J4_CLEAR),与压盖前端面留 1.0mm。
|
||||
s = s.fuse(motor_front_clamp(X_ARM, Z_EL, tag="P06/M4"))
|
||||
|
||||
# 5b2.【2026-10-05e】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
# 配方与 P04/P07 的 M2/M5 完全同源(同一套 front_collar_pocket + motor_front_clamp),
|
||||
# 局部 z 50..56 的 r30.25..35 环墙切掉敞口(局部 +y = 全局 -Z)那半圈;背脊侧整圈
|
||||
# 保留,继续定位 M4 前端盖并支撑压盖 → M4 前后双向面压不变。
|
||||
s = cut_motor_fence(s, j4, "P06/M4", "P06")
|
||||
|
||||
# 5c. 【2026-10-02 新增】电机上方过渡块减重窗(见 hollow_tent_window)。
|
||||
# Z360..385 这段 Loft 过渡块壁厚最深 15.8mm,是 P06 唯一一块可掏的厚料
|
||||
# (Z332..367 的下部本体已经被 Ø61 定子孔掏成 4.5mm 薄壁,没得再掏)。
|
||||
# 但它正压在 J3 毂下面,所以三条红线:
|
||||
# 1) 窗顶最高只到 Z374 —— J3 的 Ø70 毂面在 Z380..388.5,窗顶留 6mm 整块
|
||||
# 实心托住毂面承压,绝不碰毂面和它后面的 3xS01 直锁螺钉;
|
||||
# 2) 窗的 X 范围 3..37 —— 截面本体 X-15..55,两侧各留 18mm 竖墙,
|
||||
# 该高度上零件才不会被切成两半(必须单实体);
|
||||
# 3) 拱形(下宽上窄)轮廓,斜面与水平 >= 52° 自支撑,只有顶部 X12..28 的
|
||||
# 16mm 平顶需要桥接(与 P03 同量级)。
|
||||
# 窗底取 Z362 而不是更低:Z360 附近是全段安全系数最低的截面,
|
||||
# 抬高窗底 4mm 比横向收窄 8mm 更划算(减重量只少 3cm3,安全系数从 92% 回到 100%)。
|
||||
# 窗沿 Y 双向切穿(Y87..172 超出本体,前后都贯通),顺带把这段的定子散热口
|
||||
# 一起打开。
|
||||
# 【2026-10-03 关闭】用户实机视点确认这个拱形窗在正面看就是一个贯通的矩形黑洞,
|
||||
# 位于J4 关节正上方、整机最显眼的落脚点,视觉上像"零件破了个洞",非常不雅观。
|
||||
# 权衡后恢复实心:代价只有 161->183cm3(+22cm3 / +27g PLA),而强度本来就已经是
|
||||
# 无窗基线的 100%(这段本来就不缺强度),减重收益纯属锦上添花。
|
||||
# 保留 P06_WIN_XZ 与本段注释,需要减重时把开关改回 True 即可复原。
|
||||
if P06_HOLLOW_WIN:
|
||||
s = s.cut(hollow_tent_window(P06_WIN_XZ, 80.0, 175.0, tag="P06过渡块"))
|
||||
else:
|
||||
print(" [减重窗] P06过渡块: 已关闭(2026-10-03 视觉整改),该段恢复实心", flush=True)
|
||||
|
||||
# 6. J3 直连法兰结构(凸台延伸至 65.0 跨过结合面牢固熔接,保持单实体与零干涉)
|
||||
# 凸台随 J3 毂面一起后移 CLAMP_BACKOFF=5.5(59.5→65.0):进入电机输出毂的深度
|
||||
# 仍是 s54.942..56.0 那 1.06mm,3xS01 的头面台阶(s=58)/杆长/咬合量一律不变。
|
||||
boss = cyl(17.8, 54.942, 59.5 + CLAMP_BACKOFF)
|
||||
chamfer_cone = cyl(18.5, 54.5, 54.942)
|
||||
s = s.fuse(j3.L(boss)).cut(j3.L(chamfer_cone))
|
||||
s = s.cut(j3.L(cyl(3.5, 54.0, 58.0 + 30.0)))
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 58.0, u, v)
|
||||
cb = cyl(2.8, 58.0, 58.0 + 40.0, u, v)
|
||||
s = s.cut(j3.L(th)).cut(j3.L(cb))
|
||||
s = hub_m2_holes(s, j3, cb_z1=58.0 + 40.0)
|
||||
|
||||
return s
|
||||
|
||||
@@ -1224,9 +1529,14 @@ def p_forearm_bone(j4, j5, half=None):
|
||||
2. 下方 M5 电机实行半包散热结构,底部与内腔开窗散热
|
||||
3. J4 轴俯仰动态行程从受限的 ±54° 大幅释放至无死角大范围摆动(零干涉)
|
||||
4. 满足单实体 (solids=1) 与装配零碰撞。
|
||||
5.【2026-10-02】M5 前端压盖(4xM3x8 BCD55)消除"电机只被后端板单面锁死"的倾斜隐患;
|
||||
J4 从动侧整体让位:y_hub_in 由 158.5 后移到 J4_CLEAR=162.0,让出 P06 的 M4 前端压盖
|
||||
(前端面 Y=161.0),留 1.0mm 动间隙。J4 直连凸台同步加长,3xS01 输出螺钉咬合量不变。
|
||||
"""
|
||||
y_out = Y_HUB + PLATE_T # 174.0
|
||||
y_hub_in = HL.Y_COVER + 0.5 + CLEARANCE_GAP # 158.5
|
||||
# J4 从动侧让位基准:必须 >= P06 的 M4 前端压盖前端面(Y_COVER+5.0=161.0) + 1.0mm
|
||||
J4_CLEAR = Y_COVER + 6.0 # 162.0
|
||||
y_hub_in = J4_CLEAR
|
||||
y_front = Y_COVER - 0.5 # 155.5
|
||||
y_rear = Y_REAR - 8.0 # 92.0
|
||||
z_top_housing = Z_WR + 39.0 # 261.0 (顶面加厚 4.0mm,消除过薄破面问题)
|
||||
@@ -1289,6 +1599,16 @@ def p_forearm_bone(j4, j5, half=None):
|
||||
p08_clearance = Ycyl(35.5, 156.0, y_out + 2.0, X_ARM, Z_WR)
|
||||
s = s.cut(p08_clearance)
|
||||
|
||||
# 6b. 【2026-10-02 新增】顶板根部减重槽(见 hollow_channel)。
|
||||
# 顶板在 Z255..280 一整段是壁厚最深 15mm 的实心料,而 J4 在 z=245 的安全系数
|
||||
# 高达 122、z=195 也有 24。挖成朝上敞开的 U 形槽,顶板变成槽钢:
|
||||
# 槽底 Z255 以下保留整块实心到电机座,抗弯的两片 X 向腹板(X-15..-7 / X47..55)
|
||||
# 留在弯矩拉压区。Y 上限取 156 而非核心的 166,是为了避开 y_hub_in=158.5 起的
|
||||
# 竖直立柱本体(立柱是 J4 从动法兰的承力柱,不能掏)。
|
||||
# 槽顶 Z280 就是顶板上表面,敞口向上,免支撑、非封闭腔。
|
||||
s = s.cut(hollow_channel(P07_CH["x0"], P07_CH["x1"], P07_CH["y0"], P07_CH["y1"],
|
||||
P07_CH["z_floor"], 300.0, tag="P07顶板"))
|
||||
|
||||
# 7. M5 电机安装接口与定子空腔
|
||||
s = j5.front_collar_pocket(s, r_bore=30.25 + CLEARANCE_GAP, z0=50.0, z1=56.5)
|
||||
s = j5.rear_mount_holes(s, t_plate=8.0, hole_d=2.8, cb_d=5.5, cb_depth=4.0)
|
||||
@@ -1299,16 +1619,24 @@ def p_forearm_bone(j4, j5, half=None):
|
||||
vent_cut = bx(X_ARM - 40.0, X_ARM + 40.0, y_rear + 8.0, y_front - 6.0, Z_WR - 40.0, Z_WR - 25.0)
|
||||
s = s.cut(vent_cut)
|
||||
|
||||
# 8. J4 直连法兰结构(凸台延伸至 60.0 跨过间隙与法兰盘牢固熔接,保持单实体)
|
||||
boss = cyl(17.8, 54.942, 60.0)
|
||||
# 7b. 【2026-10-02 新增】M5 前端压盖 4xM3x8(BCD55):M5 原来只被后端板单面锁死,
|
||||
# 大扭矩下会绕 J5 轴在座孔里点头。现在前端也面压夹一次(见 motor_front_clamp)。
|
||||
# 必须在第 6 步 p08_clearance(R35.5 让位孔,Y>=156)之后熔接,否则压盖会被削掉。
|
||||
# P08 的 J5 从动侧已相应后移到 Y>=162(make_pitch_to_roll_bracket 的 hub_gap=6.0)。
|
||||
s = s.fuse(motor_front_clamp(X_ARM, Z_WR, tag="P07/M5"))
|
||||
|
||||
# 7c2.【2026-10-05e】删掉前端"围挡"在敞口侧的那半圈环墙(见 FENCE_OPEN_CUT)。
|
||||
# 与 P04/P06 同源。注意本件的 front_collar_pocket 用了 r_bore=30.25+CLEARANCE_GAP
|
||||
# =32.25(2.0mm 关节间隙),环壁因此更薄(r32.25..35),切法与配方完全一致。
|
||||
s = cut_motor_fence(s, j5, "P07/M5", "P07")
|
||||
|
||||
# 8. J4 直连法兰结构(凸台延伸到法兰盘背面之后 2.0mm 跨过间隙与法兰盘牢固熔接,
|
||||
# 保持单实体)。凸台长度跟着 y_hub_in 走:法兰盘后移时凸台同步加长,
|
||||
# 3xS01 输出螺钉的头面台阶(局部 z=58)、杆长、咬合量与改前完全一致。
|
||||
boss = cyl(17.8, 54.942, (y_hub_in - HL.Y_REAR) + 2.0)
|
||||
s = s.fuse(j4.L(boss))
|
||||
s = s.cut(j4.L(cyl(3.5, 54.0, 74.0 + 30.0)))
|
||||
for a in AB.HUB_M2_CLOCK:
|
||||
u = AB.M["m2_bcd"] / 2.0 * math.cos(a * AB.D2R)
|
||||
v = AB.M["m2_bcd"] / 2.0 * math.sin(a * AB.D2R)
|
||||
th = cyl(1.4, 54.0, 58.0, u, v)
|
||||
cb = cyl(2.8, 58.0, 74.0 + 40.0, u, v)
|
||||
s = s.cut(j4.L(th)).cut(j4.L(cb))
|
||||
s = hub_m2_holes(s, j4, cb_z1=74.0 + 30.0)
|
||||
|
||||
# 9. 工艺走线通孔
|
||||
slot_z = (Z_EL + z_top_housing) / 2.0
|
||||
@@ -1331,12 +1659,18 @@ def p_forearm_bone(j4, j5, half=None):
|
||||
|
||||
|
||||
def p_wrist_bracket(j5, j6):
|
||||
"""P08_腕偏转座_螺旋体25%_墙4底5_竖打(深度圆角优化版)"""
|
||||
return make_pitch_to_roll_bracket(j5, j6, Z_WR, Z_R6, name="P08_腕偏转座_螺旋体25%_墙4底5_竖打")
|
||||
"""P08_腕偏转座_螺旋体25%_墙4底5_竖打(深度圆角优化版)
|
||||
|
||||
hub_gap=6.0:J5 处 P07 新增了 M5 前端压盖(Y 155.5..161.0),从动法兰盘背面必须
|
||||
从 Y=157 后移到 Y=162 让位;J5 直锁凸台同步加长,3xS01 输出螺钉咬合量不变。
|
||||
"""
|
||||
return make_pitch_to_roll_bracket(j5, j6, Z_WR, Z_R6,
|
||||
name="P08_腕偏转座_螺旋体25%_墙4底5_竖打",
|
||||
hub_gap=6.0)
|
||||
|
||||
|
||||
def p_tool_flange(j6):
|
||||
"""P09_工具法兰_螺旋体100%_墙4底5_竖打 深度圆角优化版
|
||||
"""P09_工具法兰_螺旋体30%_墙4底5_竖打 深度圆角优化版
|
||||
关键改进:
|
||||
1. 外轮廓全圆角(R=4mm)
|
||||
2. 法兰面倒角(2mm)
|
||||
@@ -1344,15 +1678,24 @@ def p_tool_flange(j6):
|
||||
"""
|
||||
z_bot = Z_HUB6 - PLATE_T - 9.0
|
||||
s = cyl(35.0, z_bot, Z_HUB6, X_ARM, Y_AXIS)
|
||||
P09_M4 = []
|
||||
|
||||
# 4x M4螺栓孔
|
||||
#
|
||||
# 分布圆半径 20 -> 22(BCD 40 -> 44):R20 与 3xS01 输出螺钉的 Ø5.6 螺丝刀通道
|
||||
# (make_direct_hub 里 cyl(2.8, 58.0, z_out+40) 在 BCD29=R14.5、相位 351.4/232.3/112.1°)
|
||||
# 咬穿沉孔。0° 那个孔中心 (39.5,128) 到 351.4° 通道中心 (33.84,125.83) 只有 6.06mm,
|
||||
# 而两孔半径之和是 3.5+2.8=6.30 —— 差 0.24mm,Ø7.0 沉孔壁被啃掉一块,
|
||||
# 完整圆柱面(88mm²)裂成 45.7+35.4 两片,即用户截图里的月牙孔。
|
||||
# 该相位差只有 8.6°,转动 M4 孔相位无法躲开(0/90/180/270 与 45 度错开全部仍 <6.30),
|
||||
# 只能加大分布圆半径:R22 时最小中心距 7.96mm,余量 1.66mm。
|
||||
# 边壁仍充足:R35 法兰外径 - (R22 + 沉孔 3.5) = 9.5mm。
|
||||
for a in (0, 90, 180, 270):
|
||||
rad = 20.0
|
||||
rad = 22.0
|
||||
u = rad * math.cos(a * D2R)
|
||||
v = rad * math.sin(a * D2R)
|
||||
h = cyl(1.85, z_bot - 1.0, z_bot + 12.0, X_ARM + u, Y_AXIS + v)
|
||||
cb = cyl(3.5, z_bot - 1.0, z_bot + 4.0, X_ARM + u, Y_AXIS + v)
|
||||
s = s.cut(h).cut(cb)
|
||||
# 实际切割推迟到 make_direct_hub 之后(见下)。这里只算出 4 个孔位。
|
||||
P09_M4.append((X_ARM + u, Y_AXIS + v))
|
||||
|
||||
# 定位止口 + 走线孔
|
||||
pilot = cyl(10.0, z_bot - 1.0, z_bot + 2.5, X_ARM, Y_AXIS)
|
||||
@@ -1362,6 +1705,21 @@ def p_tool_flange(j6):
|
||||
# 直连法兰
|
||||
s = make_direct_hub(s, j6, r_disc=35.0, z_out=74.0)
|
||||
|
||||
# 4x M4 孔(必须在 make_direct_hub 之后切)
|
||||
#
|
||||
# 直连法兰那步 fuse 了 R35 圆盘(局部 z=56.5..74 -> 全局 z=102..121 一侧),
|
||||
# 会把先切的孔重新填死:分段求交显示 z=97..102 无干涉、z=102 以上整根杆都埋在料里
|
||||
# (每段 12.57 mm^3 = pi*2^2*1,全截面被埋)。
|
||||
#
|
||||
# 孔径与深度同时修正:旧值 Ø3.7 比 M4 杆 Ø4.0 还细,且只到 z=105,
|
||||
# 而 M4x16 头面在 z=97、杆要走到 z=113 —— 杆尾 8mm 无处可去(干涉 147.3 mm^3)。
|
||||
# Ø4.4 是 M4 标准间隙孔(ISO 273 medium ≈ Ø4.3)。
|
||||
# 孔顶要盖过 M4x16 的杆尾:头面 z=97 + 杆长 16 = z=113,故开到 Z_HUB6+4=115,
|
||||
# 比杆尾多留 2mm(实测开到 112 时最后 1mm 杆尾仍撞料 37.7mm³)。
|
||||
for (fx, fy) in P09_M4:
|
||||
s = s.cut(cyl(2.2, z_bot - 1.0, Z_HUB6 + 4.0, fx, fy))
|
||||
s = s.cut(cyl(3.5, z_bot - 1.0, z_bot + 4.0, fx, fy))
|
||||
|
||||
# ========== 关键圆角优化 ==========
|
||||
try:
|
||||
# 外轮廓圆角
|
||||
@@ -1408,7 +1766,7 @@ def build():
|
||||
AB.GRP = {}
|
||||
par = {"J1": "root", "J2": "J1", "J3": "J2", "J4": "J3", "J5": "J4", "J6": "J5"}
|
||||
driven = {"J1": "P04_肩叉_螺旋体30%_墙5底5_竖打", "J2": "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "J3": "P06_大臂骨架_螺旋体25%_墙4底5_竖打",
|
||||
"J4": "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "J5": "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "J6": "P09_工具法兰_螺旋体100%_墙4底5_竖打"}
|
||||
"J4": "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "J5": "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "J6": "P09_工具法兰_螺旋体30%_墙4底5_竖打"}
|
||||
motor_of = {"J1": "M1_J1", "J2": "M2_J2", "J3": "M3_J3",
|
||||
"J4": "M4_J4", "J5": "M5_J5", "J6": "M6_J6"}
|
||||
for t in ("J1", "J2", "J3", "J4", "J5", "J6"):
|
||||
@@ -1441,17 +1799,17 @@ def build():
|
||||
("P03_肩吊臂_螺旋体35%_墙5底5_竖打", "肩关节安装悬臂", lambda: p_girdle(j1), 0x4b5563,
|
||||
"轻量化板式桁架版:主干由 66mm 实心块改为双侧 6mm 桁架竖板+帐篷减重窗(斜面≥52°自支撑)+R4内角,外轮廓与旧版并集完全一致,实心 728->286cm³;底部 9mm 承力鞍座(Ø3.9 安装孔)+12mm 中位龙骨筋(自鞍座斜拉至法兰下缘)+40~45°外扩斜撑;保留 10mm 后置法兰、侧开式电机托架、Ø60.7 直通电机舱(平孔无台阶)(M1 从+Y滑入,后端 4xM3x16 锁死)、大散热窗与对流槽;无封闭空腔、免支撑"),
|
||||
("P04_肩叉_螺旋体30%_墙5底5_竖打", "肩叉", lambda: p_fork(j1, j2), 0x2563eb,
|
||||
"齐平插槽+ M2 前端压盖版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉;【2026-10-02 新增】M2 前端压盖(R35, Y155.5..161.0):电机前端盖外端面加 4xM3x8 沉孔(BCD55)夹紧,与后端安装板 4xM3x10 形成前后双向面压,消除大扭矩下电机在座孔里的倾斜"),
|
||||
"齐平插槽+ M2 前端压盖+ 立柱减重槽版:叉板背面与P03托架端面(11.5)0.0硬贴平(用户要求外观齐平),P03侧插槽独立加长3mm,M1电机可倾斜卡入,J1输出凸台贯通直连电机,J1从动法兰圆盘鼓楞已删(接缝区与P03止脱环齐径齐高R36.5/顶646.5),单实体全角度零干涉;【2026-10-02】M2 前端压盖(R35, Y155.5..161.0):电机前端盖外端面加 4xM3x8 沉孔(BCD55)夹紧,与后端安装板 4xM3x10 形成前后双向面压,消除大扭矩下电机在座孔里的倾斜;【2026-10-02】立柱减重槽(X23~35 / Y102~154 / 自Z525向上敞开):立柱Z525~635整段壁厚峰值17.5mm,J2在z=560/600的安全系数有19,改成U形槽钢后361->289cm3(-20%),抗弯的两片X向腹板正好落在弯矩拉压区(z=560截面X13~45、中性面X29,腹板占X13~23/X35~45),掏掉的正是中性层附近几乎不参与抗弯的料;槽底Z525以下保留整块实心过渡到电机座,槽底水平朝上、侧壁竖直、顶部敞开故零支撑且非封闭腔;开度按「减重区最差安全系数 >= 无槽基线 95%」选定(X23~35 比最初的 X21~37 收窄 2mm,强度从 88% 回到 95%);【2026-10-04 视觉整改】电机上方那块 4048mm2 的大平面改成 Hermite 弧面(全行程重扫 907s 确认 J1~J6 行程逐项不变):原来 yz_pts 里(164.5,646.5)->(155.5,526) 是一条直线,沿 X 拉成 4048mm2 平板(法向 (0,1,-0.07)、带 4° 斜度),正对电机上方,两侧被 xz_wire 的 R18 圆弧挤出去,形成「中间一块平、两边鼓」的观感。改成三次 Hermite(两端切线分别沿 Y/Z,与上下水平段 G1 连续),中部向 +Y 外凸约 3mm,弧度与两侧 R18 一致;单段圆弧无法同时满足两端切线(解出 R=9 与 R=120.5 矛盾)故用 Hermite。P04 体积 282.1->274.3cm³(弧面替代平面削掉的料);【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "大臂旋转座", lambda: p_shoulder_roll_base(j2, j3, None), 0x059669,
|
||||
"V3圆角+ J2前端夹紧配合:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂);J2 从动法兰盘背面由 Y=157 后移至 Y=162(hub_gap=6.0,给 P04 新增的 M2 前端压盖让位,J2 直锁凸台同步加长到 Y=165.5,3xS01 输出螺钉咬合量不变);保证 J2 全角度 0 干涉"),
|
||||
"V3圆角+ J2前端夹紧配合:Loft外轮廓R=10mm,L型转角内部2mm倒角,后缘凸包两角90°竖棱磨R2圆角(结构/后缘高度不变,打印无悬挂);J2 从动法兰盘背面由 Y=157 后移至 Y=162(hub_gap=6.0,给 P04 新增的 M2 前端压盖让位,J2 直锁凸台同步加长到 Y=165.5,3xS01 输出螺钉咬合量不变);保证 J2 全角度 0 干涉;【2026-10-05d 只切敞口那一圈围挡】用户指出 M3 电机前端(另一头)也有一圈围挡:Z389.0~395.5 的 r30.25~35 前端定位环(6mm 高环墙) + 骑在它上面的 R35 压盖外圈(Ø70 比电机 Ø63.6 凸 3.2mm),把 M3 前端盖连同穿过 Ø37 孔的 P06 J3 凸台围在中间。第一版把整个前端(定位环+压盖+腔口下沿 389->395)都切了属过度(电机前端失去支撑面、M3 退回单面锁死),已回退;现只切敞口(-Y)那一圈(盒切,Y 界限与 cut_open 一致),-Y 半圈从 Z=395.5 一路开到压盖背面 Z=389,背脊(+Y)侧整圈保留作前端盖定位与压盖唯一且足够强的支撑面(4.75mm 宽 x 0.5mm 熔接带);压盖本体/Ø37 孔/4xM3x8 沉孔/腔口下沿 389.0 全部不动,P05 只少 3.2cm3(154.8->151.6),仍单实体。不整圈削到 R31.8 的原因:那圈壁与 Loft 在 X=±35 处与 Ø70 柱面相切,整圈切完会脱空出 1.32mm3 碎块破坏单实体;且压盖根部熔接面会从 2.37mm2 缩到 0.78mm2,4xM3x8 夹紧力会把压盖拉下来。开关 FENCE_OPEN_CUT(2026-10-05e 起为全机 5 处共用)。【2026-10-03】M3 前端压盖(R35, Z384.0..389.0):J3 roll 电机原来也只有后端板 4xM3x10 单面锁死(S02),大扭矩下绕 J3 轴在 Ø60.3 座孔里点头;现按 P04/P06/P07 同一配方在 M3 前端盖外端面(Z=389)加 4xM3x8(BCD55)沉孔压盖,与后端板前后双向面压,倾斜自由度归零(配方唯一差别是 roll 电机轴向 -Z,压盖朝 -Z 长,见 motor_front_clamp axis=\"Z\");代价是对侧 P06 的 J3 毂面整体后移 5.5mm 让位;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "大臂骨架", lambda: p_humerus_bone(j3, j4), 0x0d9488,
|
||||
"Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体,单实体0干涉"),
|
||||
"Loft有机过渡版:借鉴P05圆弧变方形放样算法,顶端Ø70正圆与J3平齐无台阶,底端渐变融入M4抱紧套筒与壳体;【2026-10-03】取消电机上方Loft过渡块(Z360~385)的拱形减重窗,恢复实心:该窗沿Y贯通,正面看是整机最显眼处的矩形黑洞(用户视觉整改),代价仅 +22cm3/+27g,强度仍为无窗基线100%;【2026-10-03】J3 毂面整体后移 5.5mm(顶端 388.5→383.0,直锁凸台 59.5→65.0):给 P05 新增的 M3 前端压盖(Z384..389)让位,留 1.0mm 动间隙——与 Y 轴三处(J2/J4/J5 令从动法兰盘 Y157→162)完全同源,换算式同为「压盖 5.0 + 动间隙 1.0 - 原基准 0.5」;凸台同步加长,3xS01 头面台阶(s=58)/杆长/咬合量与 J3 直锁关系一律不变;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "小臂骨架", lambda: p_forearm_bone(j4, j5, None), 0x7c3aed,
|
||||
"V2 L型结构:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉"),
|
||||
"V2 L型结构 + 顶板减重槽:+Y侧背脊立柱避让大臂骨架,过渡区采用与顶板和立柱双相切的R=28mm平滑内凹大圆弧,彻底消除直斜线折角与悬空阶梯小块,顶板前后外缘2mm倒角改R3圆角,顶板整条加高19mm(z261->280)加强连接且gusset弧R28->R14重定相切融入,J4圆盘外沿与立柱外棱不倒圆保持原始尖角,下方M5电机半包散热,俯仰大范围自由运动零干涉;【2026-10-02】顶板根部Z255~280开朝上敞开的减重槽(X-3~43 / Y103~153, Y上限取153避开y_hub_in=158.5起的竖直立柱本体):顶板由实心变槽钢,288->230cm3(-20%),槽底以下保留整块实心到电机座,抗弯靠两侧各10mm的X向腹板;槽底水平朝上、侧壁竖直、顶部敞开故零支撑且非封闭腔;开度按「减重区最差安全系数 >= 无槽基线 95%」选定(X-3~43 比最初的 X-7~47 收窄 4mm,强度从 88% 回到 96%);【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "腕部转接座", lambda: p_wrist_bracket(j5, j6), 0xd97706,
|
||||
"V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉"),
|
||||
("P09_工具法兰_螺旋体100%_墙4底5_竖打", "末端工具法兰", lambda: p_tool_flange(j6), 0xdc2626,
|
||||
"V2圆角:外轮廓R=3mm,法兰面倒角2mm"),
|
||||
"V2圆角:Loft外轮廓R=10mm,L型转角内部2mm倒角,彻底清除立壁保证J5全角度0干涉;【2026-10-05 承压面让位腔改为 R35 圆腔、凸台与围挡一并消除】电机尾端垫片位(REAR_SEAT=2.6)用圆腔掏出承压面。**刀半径必须等于外壁 R35**(arm_build.SEAT_R):取小于 R35(曾试 32.2 = 64.4 方腔内切圆)时,刀半径与外壁之间那圈 2.8mm 环墙完整保留,而承压面只挖到刀半径,于是该环墙相对承压面【凸起】并把电机尾端面围住 —— 这就是用户截图里的一圈围挡(P08 实测 1536.9mm3)。它不是残留,而是 SEAT_R<R35 的必然产物。现 SEAT_R=35.0、SEAT_TOP=+1.0(越过尾端面 1mm,吃掉半包开口处那截0.052cm3 残片):承压面段整圈挖到外壁,承压面成为完整 R35 平圆盘,凸台与围挡一起消失,电机尾端面与承压面之间再无凸起。承力改由臂身本体承担(band 段 r36..58 有料方位 P04 10/36、其余各 15/36,其余方位靠 z>+1.0 杯体连接,无整圈通槽)。沉孔 z 区间(-8..-4)与圆腔(-2.6..+1.0)不重叠,S02 完好。连带:承压环由「方腔内切 32.2 − 沉孔外缘 30.25 = 1.95mm 窄环」变为「完整 R35 圆环 4.75mm」,受压面积 383->974mm²(2.5 倍)。【2026-10-04 承压面让位腔改为 32.2 圆腔】电机尾端垫片位(REAR_SEAT=2.6)之前用 64.4 **方**腔掏出来,方腔壁以外 z=-2.6..0 那圈料相对新承压面凸起 2.6mm,打印时是悬空薄壁(用户反馈:难打印、需支撑、公差差)。现承压面段改用**SEAT_R=32.2 圆腔**(= 64.4 方腔的内切圆半径,z=-2.6..0 与方腔平接)把整圈挖到承压面高度,凸台零残留;z=0 以上 PCBA 腔仍保持 64.4 方(PCB 是方的必须方腔)。**关键:刀半径必须取 32.2 而不是 R35** —— 中间试过 R35,结果把 r=32.2..35 的连接壁整圈切断:实测 P04 在 z=-2.6..+1.0 只剩 240~320° 方位有料,其余 360° 全掏空,后端法兰与杯壁之间出现 3.6mm 宽通槽 + 225° 一片 0.5mm 孤立薄片(用户截图:肩叉顶部「没连上」)。取 32.2 后 r=32.2..35 这圈 2.8mm 厚壁完整保留,360° 连续,既无凸台又与上方臂身连成一体(实测 5/5 件连接壁整圈连续、单实体)"),
|
||||
("P09_工具法兰_螺旋体30%_墙4底5_竖打", "末端工具法兰", lambda: p_tool_flange(j6), 0xdc2626,
|
||||
"V2圆角:外轮廓R=3mm,法兰面倒角2mm;【2026-10-02】打印填充由100%降到30%:该截面安全系数288是全机裕度最大的件,满填充纯属浪费(件号同步改为P09_工具法兰_螺旋体30%)"),
|
||||
]
|
||||
|
||||
total_parts = len(parts_to_build)
|
||||
|
||||
@@ -70,6 +70,16 @@ LUG_R = C["lug_r1"] + 1.0 # radial keep-out where the bolt lugs
|
||||
CLR = dict(bore_slip=0.20, pilot=0.10, hole_3=3.4, hole_27=2.8, wall_min=2.0)
|
||||
FIT = dict(motor_bore_d=M["hous_d"] + 2 * 0.15, cover_cb_d=M["cover_d"] + 2 * 0.35)
|
||||
|
||||
# 电机【轴向长度】公差(用户 2026-10-03 给的现场口径:各台电机厚度差可达 2mm)。
|
||||
# 这条链是"后端板螺钉 + 前端压盖螺钉"两个刚性夹持面,螺钉头一旦坐死在沉孔台阶上
|
||||
# 就再也不能进给,所以 2mm 没法靠"拧紧"消化,只能靠两件事吃掉(见 arm_build.Joint.pocket
|
||||
# 与 motor_front_clamp):
|
||||
# 长出来的 0~+2mm -> 后端板 4xM3 螺钉头下垫 0~2mm 垫片圈(螺钉被垫高 -> 电机被拉回)
|
||||
# 短下去的 0~-2mm -> 电机尾端面与后端板承压面之间垫 0~2mm 垫片(承压面已按 MOTOR_TOL
|
||||
# 让空,见 Joint.pocket 的 REAR_SEAT)
|
||||
# 两边都不动压盖/凸台的几何,关节轴向长度不变。
|
||||
MOTOR_TOL = 2.0
|
||||
|
||||
# ---------------- geometry helpers --------------------------------------------------------------------
|
||||
|
||||
def _nrm(v):
|
||||
|
||||
@@ -112,7 +112,7 @@ JOINTS = [
|
||||
("J3", "大臂自转", (X_ARM, Y_AXIS, Z_R3), (0, 0, -1), (1, 0, 0), "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "P06_大臂骨架_螺旋体25%_墙4底5_竖打"),
|
||||
("J4", "肘部俯仰", (X_ARM, Y_REAR, Z_EL), (0, 1, 0), (1, 0, 0), "P06_大臂骨架_螺旋体25%_墙4底5_竖打", "P07_小臂骨架_螺旋体30%_墙5底5_竖打"),
|
||||
("J5", "腕部俯仰", (X_ARM, Y_REAR, Z_WR), (0, 1, 0), (1, 0, 0), "P07_小臂骨架_螺旋体30%_墙5底5_竖打", "P08_腕偏转座_螺旋体25%_墙4底5_竖打"),
|
||||
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "P09_工具法兰_螺旋体100%_墙4底5_竖打"),
|
||||
("J6", "腕自转", (X_ARM, Y_AXIS, Z_R6), (0, 0, -1), (1, 0, 0), "P08_腕偏转座_螺旋体25%_墙4底5_竖打", "P09_工具法兰_螺旋体30%_墙4底5_竖打"),
|
||||
]
|
||||
AXIS = {t[0]: dict(rear=t[2], d=t[3], xdir=t[4]) for t in JOINTS}
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量
|
||||
台阶 z=63(J1, p_fork th 54..63) / 58(其余, th 54..58);头深藏在 Ø5.6 螺丝刀通道内
|
||||
S02 P04~P08 4xM3 :下级电机 rear_mount_holes:沉孔 Ø5.5 从 -t_plate-1 到 -t_plate+4,
|
||||
台阶 z = -t_plate+4 = -4(t_plate=8)/ -3(t_plate=7),头沉入 1mm
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (沉孔 Ø6.5 台阶 x=-56.5,法兰外端面 -61)
|
||||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 62.5)
|
||||
(沉孔 Ø6.0 在 Ø10 凸台端面 x=11.5,M3x10 旋入电机前端面咬合 3.5mm)
|
||||
S03 P03 后法兰 4xM3 :J1 局部 (R27.5 @39.9°+90k, -5.5) (Ø6.5 圆柱沉孔台阶 x=-56.5,法兰外端面 -61,
|
||||
圆柱头沉入 1.5mm)
|
||||
S04 P03 前置锁紧凸台 4xM3 :J1 局部 (R27.5 @17.8/107.8/197.8/287.8°, 60.5)
|
||||
(Ø6.0×2 圆柱沉孔在 Ø10 凸台内,台阶 x=9.5,M3x8 圆柱头露 1mm,旋入电机前端面咬合 3.5mm)
|
||||
S05 P01 分段对锁 8xM4x16:Ø3.9 直孔 x∈{-131,-93}, z∈{180,329}(母口壁与公榫壁同径),
|
||||
前后各拧一颗,头在 y=95 / y=161 外露(搭接壁薄不开沉孔),直接拧入自攻成型
|
||||
S06 P09 工具法兰 4xM4:R20 @0/90/180/270°,台阶 z=97(沉孔 Ø7.0 深 4mm,法兰底面 z=93)
|
||||
S06 P09 工具法兰 4xM4:R22 @0/90/180/270°(BCD44,原 R20 会与 S01 的 Ø5.6 通道咬穿沉孔),台阶 z=97(Ø7.0×4 圆柱沉孔 + Ø4.4 贯穿通孔,法兰底面 z=93,
|
||||
M4 圆柱头与法兰底面齐平)
|
||||
S07 P02 中核心 4xM5 :x∈{-135,-99}, y∈{110,146},台阶 z=6(Ø10 沉孔深 6mm,头沉入 1mm)
|
||||
S08 P02 力臂 4xM5 :(-10,76/180) + (-185,88/168),台阶 z=17(Ø10 沉孔深 5mm,头与顶面齐平)
|
||||
S09 P03 安装台 4xM4x16:x∈{-142.5,-87.5}, y∈{114.5,141.5},台阶 z=479(P03 鞍座顶面,
|
||||
@@ -27,6 +29,11 @@ docs/screws.json 是紧固件清单的唯一事实源(规格 / 长度 / 数量
|
||||
沿 +X Ø3.9 直孔穿 11mm 后壁 + 榫头同径,直接拧入自攻(榫头内咬合 4.7mm)
|
||||
S10 底座腰槽 6xM6 :3 件底座的 6 组 28x7 通腰槽中心(-97.5/120/-215 × 53/203/58/198),
|
||||
头面 z=22(底座顶面;腰槽为通槽无沉孔,桌面紧固螺栓头必然外露)
|
||||
S12 电机前端压盖 4xM3x8:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) / J5(P07 夹 M5) 各 4 颗。
|
||||
配方唯一事实源 = arm_model.motor_front_clamp(R35、4xM3x8 BCD55 @17.8°+90k、Ø6.0 沉孔
|
||||
深 3.0、沉孔台阶 = 前端盖外端面 +2.0mm、压盖背面 0.0 面压);J3 是同一套配方的 Z 轴版本
|
||||
(台阶落在 Z=(z_r-56)-2=387),半径/相位直接取 arm_model 的 CLAMP_BCD/CLAMP_CLOCK。
|
||||
孔位必须显式算全局 (x,z)/(x,y) 而不能用 to_global,原因见 instances() 内的注释。
|
||||
|
||||
长度处理:S08/S10 螺杆按真实长度会伸到底座底面(z=0)以下悬空(工作台/预埋螺母未建模),
|
||||
这里按可见段截断到 z=0,头部与孔位仍按 BOM 规格,避免出现"悬空螺杆"。
|
||||
@@ -50,6 +57,7 @@ _ROOT = os.path.dirname(_HERE)
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
import human_layout as HL # noqa: E402
|
||||
from arm_model import CLAMP_BCD, CLAMP_CLOCK # noqa: E402 压盖孔位唯一事实源
|
||||
|
||||
X_COL = -115.0 # 立柱中心(与 arm_model.py 一致)
|
||||
Y_COL = HL.Y_AXIS # 128.0
|
||||
@@ -202,19 +210,39 @@ def instances(m2_bcd, m2_clock):
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S04", to_global("J1", u, v, 62.5), d1, "root",
|
||||
out.append(("S04", to_global("J1", u, v, 60.5), d1, "root",
|
||||
"P03 前置锁紧凸台", "BCD55 @%.1f° -> M1 前端面" % deg))
|
||||
for u, v in REAR_SLOTS:
|
||||
out.append(("S03", to_global("J1", u, v, -5.5), tuple(-c for c in d1), "root",
|
||||
"P03 后置法兰", "BCD55 @39.9° 螺纹槽 -> M1 尾端面"))
|
||||
|
||||
# S12 P04 前端压盖 -> M2 前端盖(J2):沉孔台阶 Y=158(局部 z=58),与 S04 同 BCD55 相位
|
||||
d2 = frame("J2")[1]
|
||||
for deg in (17.8, 107.8, 197.8, 287.8):
|
||||
u = 27.5 * math.cos(math.radians(deg))
|
||||
v = 27.5 * math.sin(math.radians(deg))
|
||||
out.append(("S12", to_global("J2", u, v, 58.0), d2, "J1",
|
||||
"P04 前端压盖", "BCD55 @%.1f° -> M2 前端面(前端夹紧)" % deg))
|
||||
# S12 电机前端压盖 -> 电机前端盖:J2(P04 夹 M2) / J3(P05 夹 M3) / J4(P06 夹 M4) /
|
||||
# J5(P07 夹 M5) 各 4 颗。J2/J4/J5 三处压盖几何完全相同,J3 是同一套配方的 Z 轴版本。
|
||||
# R35、沉孔台阶都在"前端盖外端面 + 2.0mm":Y 轴是 Y_COVER+2=158,Z 轴是 (z_r-56)-2=387。
|
||||
# 4xM3x8 @BCD55 17.8°+90k、沉孔 Ø6.0 深 3.0、压盖背面与前端盖 0.0 面压,
|
||||
# 相位与 S04(P03→M1 前端锁紧)同一套约定,全机 BCD55 孔位一致。
|
||||
#
|
||||
# 孔位必须按 motor_front_clamp 的写法直接算全局 (x,z)/(x,y),不能用 to_global:
|
||||
# 压盖成孔是 fu = px + BCD*cos、fv = pz/py + BCD*sin,即 sin 项直接加在**全局**该轴上;
|
||||
# 而 frame() 的 y 轴 = d × x,对 J2/J4/J5(d=+Y, x=+X)算出 y=(0,0,-1),
|
||||
# 经 to_global 会把 v 映到全局 -z,与成孔方向差一个符号
|
||||
# -> 12 颗螺钉实例整体偏 2*27.5*sin(17.8°) = 16.81mm,每颗都落在两孔之间的实体上
|
||||
# (布尔校验:M3 圆柱头沉在 Ø6.0 沉孔里却与压盖实体重叠 71.27 mm^3,装不进去)。
|
||||
# 故此处显式用全局平面复刻成孔公式,与建模脚本一一对应。
|
||||
for tag, prox in (("J2", "P04 肩叉"), ("J3", "P05 大臂旋转座"),
|
||||
("J4", "P06 大臂骨架"), ("J5", "P07 小臂骨架")):
|
||||
dt = frame(tag)[1]
|
||||
rear = tuple(HL.AXIS[tag]["rear"])
|
||||
roll = HL.AXIS[tag]["d"][1] == 0 # J3/J6 是 roll 电机(轴向 -Z)
|
||||
for deg in CLAMP_CLOCK:
|
||||
u = CLAMP_BCD * math.cos(math.radians(deg))
|
||||
v = CLAMP_BCD * math.sin(math.radians(deg))
|
||||
hp = (rear[0] + u, rear[1] + v, rear[2] - 58.0) if roll \
|
||||
else (rear[0] + u, rear[1] + 58.0, rear[2] + v)
|
||||
out.append(("S12", hp, dt, GRP_PAR[tag],
|
||||
"%s 前端压盖" % prox,
|
||||
"BCD55 @%.1f° -> %s 电机前端盖(前端夹紧,%s 轴)"
|
||||
% (deg, tag, "Z" if roll else "Y")))
|
||||
|
||||
for x in (X_COL - 16.0, X_COL + 22.0):
|
||||
for z in (180.0, 329.0):
|
||||
@@ -224,10 +252,12 @@ def instances(m2_bcd, m2_clock):
|
||||
"P01 立柱分段缝", "z=%.0f 背面 +Y:Ø3.9 直孔(M4x16 直接拧入自攻)" % z))
|
||||
|
||||
for deg in (0.0, 90.0, 180.0, 270.0):
|
||||
x = HL.X_ARM + 20.0 * math.cos(math.radians(deg))
|
||||
y = HL.Y_AXIS + 20.0 * math.sin(math.radians(deg))
|
||||
# BCD44(R22):原 R20 与 S01 输出螺钉 Ø5.6 通道中心距仅 6.06 < 6.30,
|
||||
# 沉孔壁被咬穿成月牙形;加大到 R22 后余量 1.66mm。见 p_tool_flange 注释。
|
||||
x = HL.X_ARM + 22.0 * math.cos(math.radians(deg))
|
||||
y = HL.Y_AXIS + 22.0 * math.sin(math.radians(deg))
|
||||
out.append(("S06", (x, y, 97.0), (0.0, 0.0, -1.0), "J6",
|
||||
"P09 工具法兰", "R20 @%.0f° -> 末端工具" % deg))
|
||||
"P09 工具法兰", "R22 @%.0f° -> 末端工具" % deg))
|
||||
|
||||
for x in (-135.0, -99.0):
|
||||
for y in (110.0, 146.0):
|
||||
|
||||
@@ -55,6 +55,10 @@ VIEWS = {
|
||||
"user_p06_yz": "?az=0&po=1.5708&rd=260&tx=18.5&ty=350&tz=-110&xray=0",
|
||||
"user_p06_iso": "?az=-0.65&po=1.25&rd=260&tx=-15&ty=350&tz=-128&xray=0",
|
||||
"user_p07_creases": "?az=-0.65&po=1.25&rd=260&tx=30&ty=270&tz=-130&xray=0",
|
||||
# 电机腔内壁(隐藏全部电机,从腔内看 R35/R30.15 两段同心圆弧是否连续)
|
||||
"bay_p06_inside": "?az=1.5708&po=1.5708&rd=250&tx=18.5&ty=345&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6",
|
||||
"bay_p07_inside": "?az=1.5708&po=1.5708&rd=240&tx=18.5&ty=235&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6",
|
||||
"bay_p08_inside": "?az=1.5708&po=1.5708&rd=210&tx=18.5&ty=175&tz=-128&xray=0&hide=J1,J2,J3,J4,J5,J6",
|
||||
"user_p04_side": "?az=1.5708&po=1.5708&rd=300&tx=18.5&ty=520&tz=-128&xray=0&hide=胸板,底脚,肩吊臂",
|
||||
"user_p04_bottom": "?az=0.5&po=2.0&rd=240&tx=18.5&ty=490&tz=-128&xray=0&hide=胸板,底脚,肩吊臂",
|
||||
"user_p04_bottom2": "?az=1.2&po=2.0&rd=260&tx=18.5&ty=490&tz=-128&xray=0&hide=胸板,底脚,肩吊臂",
|
||||
@@ -67,10 +71,26 @@ VIEWS = {
|
||||
"p03_light_rear": "?az=0.0&po=1.45&rd=300&tx=-70&ty=548&tz=-128&xray=0&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"p03_light_join": "?az=0.62&po=1.15&rd=420&tx=18.5&ty=520&tz=-128&xray=0",
|
||||
"p03_light_xray": "?az=0.62&po=1.15&rd=300&tx=-70&ty=548&tz=-128&xray=1&hide=P01,P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
# 减重版(2026-10-02):P04 立柱槽 / P06 过渡块窗 / P07 顶板槽 的孤立视图
|
||||
# 俯视看朝上敞开的槽口,斜视看槽壁与留下来的抗弯腹板
|
||||
"hollow_p04_top": "?az=0.0&po=0.05&rd=250&tx=18.5&ty=585&tz=-128&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"hollow_p04_iso": "?az=0.7&po=1.05&rd=280&tx=18.5&ty=585&tz=-128&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"hollow_p07_top": "?az=0.0&po=0.05&rd=230&tx=18.5&ty=270&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"hollow_p07_iso": "?az=0.7&po=1.05&rd=260&tx=18.5&ty=275&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
"hollow_p06_iso": "?az=0.7&po=1.05&rd=190&tx=18.5&ty=368&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P07,P08,P09,M1,M2,M3,M4,M5,M6,S0,S1,ENV",
|
||||
# P03↔立柱 安装接口(新版 4xM5 从腔体上方拧入):俯视看螺钉落位
|
||||
"p03_mount_top": "?az=0.9&po=1.30&rd=190&tx=-115&ty=476&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"p03_mount_iso": "?az=2.2&po=0.95&rd=230&tx=-115&ty=486&tz=-128&xray=0&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"p03_mount_loose": "?az=0.9&po=0.42&rd=260&tx=-115&ty=500&tz=-128&xray=0&expl=60&hide=P02,P04,P05,P06,P07,P08,P09,M1,M2,M3,M4,M5,M6,ENV",
|
||||
"j3_前端压盖_装配": "?az=0.7&po=2.72&rd=190&tx=19.5&ty=396&tz=-128&xray=0&hide=立柱,底座,肩关节安装悬臂,肩叉,大臂骨架,小臂骨架,腕部转接座,末端工具法兰,M1,M2,M4,M5,M6,包络经纬网",
|
||||
"j3_前端压盖_全件": "?az=0.7&po=2.6&rd=300&tx=19.5&ty=420&tz=-128&xray=0",
|
||||
# 承压面下沉 2mm 留下的"悬空凸台"(2026-10-04 整改):沿电机轴线从尾端面方向看,
|
||||
# 四个视图分别对应 P04/P05/P06/P07 的后端。ax/po 沿轴向,rd 拉近到 R35 座圈。
|
||||
# 沿电机轴线从尾端面看过去:承压面(R35圆面) + 4xS02 沉孔,凸台消失后应是干净圆面
|
||||
"boss_p04": "?az=0.55&po=1.30&rd=150&tx=19.5&ty=490&tz=-158&xray=0&hide=P01,P02,P03,P05,P06,P07,P08,P09,M3,M4,M5,M6,ENV",
|
||||
"boss_p05": "?az=1.5708&po=1.5708&rd=190&tx=18.5&ty=392&tz=-128&xray=0&hide=P01,P02,P03,P04,P06,P07,P08,P09,M2,M4,M5,M6,ENV",
|
||||
"boss_p06": "?az=1.5708&po=1.5708&rd=190&tx=18.5&ty=332&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P07,P08,P09,M1,M2,M3,M5,M6,ENV",
|
||||
"boss_p07": "?az=0&po=0.0&rd=210&tx=19.5&ty=215&tz=-128&xray=0&hide=P01,P02,P03,P04,P05,P06,P08,P09,M1,M2,M3,M4,M6,ENV",
|
||||
"svg_side": f"file:///{DOCS}/sheet_side.svg",
|
||||
"svg_joint": f"file:///{DOCS}/sheet_joint.svg",
|
||||
"svg_exploded": f"file:///{DOCS}/sheet_exploded.svg",
|
||||
@@ -82,17 +102,23 @@ want = sys.argv[1:] or list(VIEWS)
|
||||
def take_shot(nm):
|
||||
q = VIEWS[nm]
|
||||
out = "%s/%s.png" % (SHOTS, nm)
|
||||
import time
|
||||
# 不预先删除旧图:Edge 的 --screenshot 本来就会覆盖,而 os.remove 在带
|
||||
# safe-delete 垫片的环境里会把"删到回收站"的失败抛出来,直接打断整轮渲染。
|
||||
import time, tempfile
|
||||
t_bust = int(time.time() * 1000)
|
||||
target = q if q.startswith("file://") else "file:///" + HTML.replace("\\", "/") + q + (
|
||||
f"&still=1&_t={t_bust}" if "?" in q else f"?still=1&_t={t_bust}")
|
||||
# 每次用独立 profile:共用默认 profile 时并发实例会互相抢锁,表现为随机 FAIL。
|
||||
udd = tempfile.mkdtemp(prefix="eshot")
|
||||
cmd = [EDGE, "--headless=new", "--use-gl=angle", "--use-angle=d3d11",
|
||||
"--disable-cache", "--disable-application-cache", "--disk-cache-size=0",
|
||||
"--user-data-dir=" + udd,
|
||||
"--enable-gpu-rasterization", "--ignore-gpu-blocklist",
|
||||
"--hide-scrollbars", "--force-device-scale-factor=1",
|
||||
"--window-size=1500,860", "--virtual-time-budget=20000",
|
||||
"--screenshot=" + out, target]
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=180)
|
||||
shutil.rmtree(udd, ignore_errors=True)
|
||||
ok = os.path.exists(out) and os.path.getsize(out) > 20000
|
||||
err = (r.stderr or b"").decode("utf-8", errors="ignore").strip()[:90].replace("\n", " ")
|
||||
sz = os.path.getsize(out) if os.path.exists(out) else 0
|
||||
|
||||
@@ -395,12 +395,20 @@ def cuts():
|
||||
("大臂旋转座套筒 @z=440", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
|
||||
("大臂骨架中段 @z=350", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
|
||||
("大臂骨架抱箍 @z=325", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
|
||||
# 【2026-10-02 补】P06 原减重窗在 Z358~374,原有剖面(350/325)全在它下面,
|
||||
# 根本校核不到被挖的区域。z=365 穿过该段过渡块,承载同 J3。
|
||||
# 【2026-10-03】减重窗已取消、该段恢复实心,此剖面保留为该高度的安全裕度监控点。
|
||||
("大臂骨架过渡块 @z=365", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 365.0), "J3", 1),
|
||||
("小臂骨架中段 @z=245", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 245.0), "J4", 1),
|
||||
("小臂骨架抱箍 @z=195", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 195.0), "J5", 1),
|
||||
# 【2026-10-02 补】P07 减重槽在 Z255~280,同理原有剖面(245/195)没覆盖到。
|
||||
# z=270 穿过槽体本身,承载同 J4;z=280 是槽口所在的顶板上边缘,壁最薄。
|
||||
("小臂骨架减重槽 @z=270", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 270.0), "J4", 1),
|
||||
("小臂骨架减重槽口 @z=280", "P07_小臂骨架_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 280.0), "J4", 1),
|
||||
("腕偏转座根部 @z=150", "P08_腕偏转座_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 150.0), "J5", 1),
|
||||
# P09 包络 z=93..121,剖面取 107(法兰中截面)。原 -55 落在零件之外,
|
||||
# 剖切返回空,工具法兰其实一直没被校核过。
|
||||
("工具法兰 @z=107", "P09_工具法兰_螺旋体100%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
|
||||
("工具法兰 @z=107", "P09_工具法兰_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ MOUNT_PAIRS = {
|
||||
("P05_大臂旋转座_螺旋体30%_墙4底5_竖打", "M3_J3"), ("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M3_J3"),
|
||||
("P06_大臂骨架_螺旋体25%_墙4底5_竖打", "M4_J4"), ("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M4_J4"),
|
||||
("P07_小臂骨架_螺旋体30%_墙5底5_竖打", "M5_J5"), ("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M5_J5"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体100%_墙4底5_竖打", "M6_J6"),
|
||||
("P08_腕偏转座_螺旋体25%_墙4底5_竖打", "M6_J6"), ("P09_工具法兰_螺旋体30%_墙4底5_竖打", "M6_J6"),
|
||||
}
|
||||
|
||||
# 网格化预筛参数:外扩必须 ≥ 网格化偏差,判"分离"才保守可信
|
||||
|
||||
@@ -520,6 +520,13 @@ for (const p of DATA.parts) {
|
||||
for (let i = 0; i < p.v.length; i++) { pos[i*3] = p.v[i][0]; pos[i*3+1] = p.v[i][2]; pos[i*3+2] = -p.v[i][1]; }
|
||||
p.triCount = p.t ? p.t.length : 0;
|
||||
p.vertCount = p.v.length;
|
||||
/* 外形三围:模型坐标系 X×Y×Z 包围盒(装配零位、打印朝向 Z=高),悬停面板显示用 */
|
||||
{ let x0=1e9,y0=1e9,z0=1e9,x1=-1e9,y1=-1e9,z1=-1e9;
|
||||
for (let i = 0; i < p.v.length; i++) { const a = p.v[i];
|
||||
if (a[0]<x0)x0=a[0]; if (a[0]>x1)x1=a[0];
|
||||
if (a[1]<y0)y0=a[1]; if (a[1]>y1)y1=a[1];
|
||||
if (a[2]<z0)z0=a[2]; if (a[2]>z1)z1=a[2]; }
|
||||
p.dims = [x1-x0, y1-y0, z1-z0]; }
|
||||
const isEnv = p.name.slice(0, 4) === 'ENV_'; // 可达包络:运动学边界,不随关节动
|
||||
/* 索引按顶点数降位:单件 ≤65535 顶点时 Uint16 合法(92 件实测最大 47483),
|
||||
索引内存与上传带宽减半;超大件保留 Uint32 兜底。 */
|
||||
@@ -686,6 +693,11 @@ function pick(e) {
|
||||
的 style.background 全刷一遍,纯属多余 DOM 写入。 */
|
||||
let bomHlRow = null;
|
||||
function bomHlClear() { if (bomHlRow) { bomHlRow.style.background = ''; bomHlRow = null; } }
|
||||
function dimsStr(p) {
|
||||
if (!p.dims) return '';
|
||||
const f = d => String(Math.round(d * 10) / 10);
|
||||
return '外形 ' + f(p.dims[0]) + ' × ' + f(p.dims[1]) + ' × ' + f(p.dims[2]) + ' mm(X·Y·Z)';
|
||||
}
|
||||
function hover(e) {
|
||||
const o = pick(e); if (o === hov) return; hov = o;
|
||||
const n = document.getElementById('note');
|
||||
@@ -693,7 +705,8 @@ function hover(e) {
|
||||
const p = o.userData;
|
||||
n.style.display = 'block';
|
||||
n.innerHTML = '<b>#' + p.idx + ' ' + p.cn + '</b><br><small>' + p.name +
|
||||
' · ' + (p.printed ? '3D打印件' : '电机/已有件') + '<br>' + p.note + '</small>';
|
||||
' · ' + (p.printed ? '3D打印件' : '电机/已有件') +
|
||||
(dimsStr(p) ? '<br>' + dimsStr(p) : '') + '</small>';
|
||||
}
|
||||
function hidePart(m) {
|
||||
if (!m) return;
|
||||
@@ -835,7 +848,7 @@ for (const [gt, test] of groups) {
|
||||
const p = m.userData;
|
||||
const r = document.createElement('div'); r.className = 'row';
|
||||
r.innerHTML = '<input type="checkbox" checked><span class="chip" style="background:#' +
|
||||
p.color.toString(16).padStart(6,'0') + '"></span><label title="' + p.note + '">' +
|
||||
p.color.toString(16).padStart(6,'0') + '"></span><label title="' + p.name + '">' +
|
||||
'<span class="' + (p.printed?'pr':(p.name[0]==='M'?'mo':'ex')) + '">' + p.cn + '</span></label>' +
|
||||
'<span class="q">#' + p.idx + '</span>';
|
||||
const chk = r.querySelector('input');
|
||||
@@ -867,7 +880,7 @@ if (scrAll.length) {
|
||||
const ms = bySpec[sid], p0 = ms[0].userData;
|
||||
const r = document.createElement('div'); r.className = 'row';
|
||||
r.innerHTML = '<input type="checkbox" checked><span class="chip" style="background:#' +
|
||||
p0.color.toString(16).padStart(6,'0') + '"></span><label title="' + p0.note + '">' +
|
||||
p0.color.toString(16).padStart(6,'0') + '"></span><label>' +
|
||||
'<span class="ex">' + sid + ' ' + p0.cn.split(' · ')[0] + '</span></label>' +
|
||||
'<span class="q">' + ms.length + ' 颗</span>';
|
||||
const chk = r.querySelector('input');
|
||||
@@ -925,7 +938,7 @@ for (const p of DATA.parts) {
|
||||
tr.innerHTML = '<td>' + p.idx + '</td><td>' + p.cn + '</td><td>' + (p.vol/1000).toFixed(1) + '</td><td>' +
|
||||
(p.vol*1.24e-3).toFixed(0) + '</td>';
|
||||
tr.dataset.idx = p.idx;
|
||||
tr.title = p.note + ' / ' + p.name;
|
||||
tr.title = p.name + (dimsStr(p) ? ',' + dimsStr(p) : '');
|
||||
tb.appendChild(tr);
|
||||
}
|
||||
document.getElementById('sum').innerHTML =
|
||||
|
||||