前端不再是"一个 16MB HTML + 一个 2955 行模板"。现在: webapp/index.html 343 行 DOM 骨架,零内联数据 webapp/styles/*.css 811 行 CSS 按段切成 8 个文件 webapp/js/app.js 原内联主脚本逐字搬出(仍是普通脚本,S5 才改 ESM) webapp/js/ik-solver.js / ik-glue.js / vendor/three.min.js(原 web/ 三个文件) webapp/workers/ik.worker.js 真 Worker 文件 webapp/generated/00_data.js 生成物(网格/工程图/行程/碰撞/螺钉) dist/机器人手臂3D查看器.html 分发用单文件,由 build_viewer 内联上面这些文件产出 入口:python -m arm.viewer.serve 后开 http://127.0.0.1:8123/ (serve 带 no-store,改完刷新即新构建,不再需要 Ctrl+F5;显式 MIME, .js 被 Windows 注册表判成 text/plain 会让模块静默不执行) 删掉 src/viewer_tpl.html 与 web/ 目录(旧产物移入 backups/pre_split_20261015/)。 工程图不再维护第二份 sheets.jsdata —— 直接读 draw_sheet 写出的 4 个真 SVG。 实测中修掉的问题: - serve 的 / 原先"内部映射"到 webapp/index.html,导致文档基址是 /、相对路径的 8 个 CSS 与 4 个 JS 全部 404(整页无样式无脚本)。改成 302 跳转。 - file:// 下 new Worker('workers/…') 构造阶段不抛错、错误以异步 onerror 到达, 所以"构造失败才试 Blob 兜底"永远不会触发;改为按协议选路。两条路径都已实测 backend=worker(http 走文件、file:// 走 Blob)。 - Worker 握手 3s 在 headless 并发下会误判"没起来"(截图里红徽标即此),放宽到 10s, 并把后端状态暴露成 window.__IK_BACKEND / __IK_BACKEND_LOG 供测试直读。 - 截图竞态:4 并发 × 每页 14MB 数据时,虚拟时间预算会在页面加载完成前到期, 拍到半初始化画面(且能"稳定地坏",旧基线里 m9_motion 就是这么混进来的)。 并发降到 2、预算 40s、serve 加 HTTP/1.1+大缓冲+字节缓存,并用"同一构建两轮" 重新界定稳定性:99 张逐字节一致、6 张属噪声改走像素容差(mean<=0.02、单像素<=32)。 - 关卡自身两处假通过已修:node 输出被 GBK 解码炸掉时不再算 PASS; 线束干涉测试读不到通道源码时会 exit 2,而不是"零通道=零穿透=通过"。 关卡结果:python -m arm.build_model 退出码 0;10 项数据哈希与基线一致; IK exact 段逐字节一致;99 张截图逐字节一致 + 6 张在噪声阈内;线束干涉测试 1800 点 0 穿透。
495 lines
22 KiB
Python
495 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Structural check of the printed arm, using section properties measured from the SOLIDS.
|
||
|
||
Nothing here is hand-waved from a sketch dimension: for each critical cut plane the part is
|
||
boolean-cut and the freshly created planar face is taken and integrated with
|
||
BRepGProp.SurfaceProperties_s, so area, centroid and the second moments are the real
|
||
section of the real printed part (including whatever the lightening windows left behind).
|
||
|
||
Why the allowables are so low: a printed part is not the material datasheet. Bending
|
||
across layers, an un-filleted internal corner (Kt ~ 2.5 measured on square corners), and
|
||
PLA creep under permanent static load together cut 35-50 MPa down to single digits, which
|
||
is exactly the failure mode to design against. See docs/strength.md.
|
||
|
||
Run: python strength.py -> docs/strength.md (+ console table)
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import re
|
||
import sys
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
import cadquery as cq
|
||
from arm.paths import OUT, DATA, DOCS
|
||
from arm.cad.helpers import solids_of, bb, mesh_of
|
||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Common
|
||
from OCP.BRepGProp import BRepGProp
|
||
from OCP.GProp import GProp_GProps
|
||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2, gp_Vec
|
||
from arm import kinematics as HL
|
||
RHO = {"PLA": 1.24e-3, "motor_g": 210.0} # g/mm3 for printed, g per module
|
||
# a slicer does not print solid: 3 perimeters + 30% infill lands near this fraction of
|
||
# the CAD envelope, and using the solid mass would blame the part for plastic that is not there
|
||
INFILL = float(os.environ.get("ARMINFILL", "0.38"))
|
||
# allowable bending stress across the print layers, after notch + creep derating
|
||
ALLOW = {"PLA": 12.0, "PETG": 20.0, "PA_CF": 30.0} # MPa
|
||
# Young's modulus of the printed lattice, for the deflection estimate only
|
||
EMOD = {"PLA": 3000.0, "PETG": 2100.0, "PA_CF": 8000.0} # MPa
|
||
MAT = os.environ.get("ARMMAT", "PLA")
|
||
PAYLOAD = float(os.environ.get("ARMPAY", "0.5")) # kg held at the fingertip
|
||
DYN = float(os.environ.get("ARMDYN", "2.0")) # dynamic factor on the moving mass
|
||
|
||
G981 = 9.81
|
||
GRIP_T = 12.0 # N*mm per motor is far too small to matter; the grip force
|
||
# case is carried separately below as a normal force at the pad
|
||
_cache = {}
|
||
GRP = {"root": 0, "J1": 1, "J2": 2, "J3": 3, "J4": 4, "J5": 5, "J6": 6}
|
||
|
||
|
||
def load(name):
|
||
if name not in _cache:
|
||
_cache[name] = cq.importers.importStep("%s/parts_step/%s.step" % (OUT, name)).val()
|
||
return _cache[name]
|
||
|
||
|
||
def volume(sh):
|
||
p = GProp_GProps()
|
||
BRepGProp.VolumeProperties_s(sh.wrapped, p)
|
||
return p.Mass()
|
||
|
||
|
||
def _caps_at(sh, n, o, t):
|
||
"""The section faces of (solid ∩ thin slab), taken on the +normal side of the slab.
|
||
|
||
Two things this replaces. (1) 'Zero extent along the normal' on a HALF-SPACE cut also
|
||
matches the part's own horizontal faces (top chord, gusset crowns) sitting anywhere above
|
||
the plane - that over-read the shoulder fork by 2.4x (480 mm2 true vs 1155 reported).
|
||
(2) The half-space Common outright fails on the filleted fork at z=600 (returns an empty
|
||
shape) while a thin slab of the same solid cuts fine.
|
||
A slab is also self-checking: its volume / t must equal the integrated area.
|
||
"""
|
||
c0 = sum(n[k] * o[k] for k in range(3))
|
||
lo, hi = [], []
|
||
for k in range(3):
|
||
if n[k] > 0:
|
||
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
|
||
elif n[k] < 0:
|
||
lo.append(o[k] - t / 2.0); hi.append(o[k] + t / 2.0)
|
||
else:
|
||
lo.append(o[k] - 400.0); hi.append(o[k] + 400.0)
|
||
box = BRepPrimAPI_MakeBox(gp_Pnt(*lo), gp_Pnt(*hi)).Shape()
|
||
cut = BRepAlgoAPI_Common(sh.wrapped, box)
|
||
cut.Build()
|
||
if not cut.IsDone():
|
||
return []
|
||
off = c0 + t / 2.0
|
||
out = []
|
||
for f in cq.Shape(cut.Shape()).Faces():
|
||
if f.geomType() != "PLANE" or f.Area() <= 1e-6:
|
||
continue
|
||
cx, cy, cz = f.Center().toTuple()
|
||
if abs(n[0] * cx + n[1] * cy + n[2] * cz - off) > 1e-4:
|
||
continue
|
||
out.append(f)
|
||
return out
|
||
|
||
|
||
def section_props(sh, normal, origin, bend=2, size=400.0):
|
||
"""Cut the solid with a thin slab and integrate the section face.
|
||
|
||
Returns (area mm2, Ixx mm4, Iyy mm4, cmax mm, bbox) about the face centroid, with the
|
||
in-plane axes taken as (u, v) where u x v = normal.
|
||
"""
|
||
n = [float(x) for x in normal]
|
||
o = [float(x) for x in origin]
|
||
caps = []
|
||
for t in (0.2, 0.05, 1.0):
|
||
for nudge in (0.0, 0.05, -0.05, 0.25, -0.25):
|
||
oo = [o[k] + nudge * n[k] for k in range(3)]
|
||
caps = _caps_at(sh, n, oo, t)
|
||
if caps:
|
||
break
|
||
if caps:
|
||
break
|
||
if not caps:
|
||
return None
|
||
plane_ax = [k for k in (0, 1, 2) if abs(n[k]) < 0.5]
|
||
p_ax, q_ax = plane_ax
|
||
bend_ax = bend if bend in plane_ax else q_ax
|
||
far_ax = p_ax if bend_ax == q_ax else q_ax
|
||
A = Sv = Suu = Svv = S = 0.0
|
||
for f in caps:
|
||
verts, tris = mesh_of(f, 0.02)
|
||
fa = fu = fv = fuu = fvv = 0.0
|
||
for t in tris:
|
||
(u1, v1), (u2, v2), (u3, v3) = [(verts[k][p_ax], verts[k][q_ax]) for k in t]
|
||
ar = ((u2 - u1) * (v3 - v1) - (u3 - u1) * (v2 - v1)) / 2.0
|
||
fa += ar # SIGNED: inner wires (holes) cancel
|
||
fu += ar * (u1 + u2 + u3) / 3.0
|
||
fv += ar * (v1 + v2 + v3) / 3.0
|
||
fvv += ar * (v1**2 + v2**2 + v3**2 + v1*v2 + v2*v3 + v3*v1) / 6.0
|
||
fuu += ar * (u1**2 + u2**2 + u3**2 + u1*u2 + u2*u3 + u3*u1) / 6.0
|
||
if fa < 0: # face normal opposed to the cut normal
|
||
fa, fu, fv, fvv, fuu = -fa, -fu, -fv, -fvv, -fuu
|
||
A += fa; S += fu; Sv += fv; Svv += fvv; Suu += fuu
|
||
if A <= 1e-9:
|
||
return None
|
||
uc, vc = S / A, Sv / A
|
||
best = max(caps, key=lambda f: f.Area()) # only for the bounding box below
|
||
allb = [f.BoundingBox() for f in caps]
|
||
lo = [min(getattr(b, ("xmin", "ymin", "zmin")[k]) for b in allb) for k in range(3)]
|
||
hi = [max(getattr(b, ("xmax", "ymax", "zmax")[k]) for b in allb) for k in range(3)]
|
||
cmax = max(hi[far_ax] - uc if far_ax == p_ax else hi[far_ax] - vc,
|
||
uc - lo[far_ax] if far_ax == p_ax else vc - lo[far_ax]) / 1.0
|
||
# distance from the neutral axis to the extreme fibre, measured in the bending direction
|
||
ext = (hi[far_ax] - lo[far_ax])
|
||
cen = uc if far_ax == p_ax else vc
|
||
cmax = max(hi[far_ax] - cen, cen - lo[far_ax])
|
||
I = (Svv - A * vc * vc) if far_ax == q_ax else (Suu - A * uc * uc)
|
||
return dict(A=A, I=I, c=cmax, bend=bend_ax, centroid=(uc, vc))
|
||
|
||
|
||
def arm_masses():
|
||
"""Everything distal to each joint, in grams, from the model itself."""
|
||
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||
TREE = ["root", "J1", "J2", "J3", "J4", "J5", "J6"]
|
||
tot = {}
|
||
for p in meta["parts"]:
|
||
if not os.path.exists("%s/parts_step/%s.step" % (OUT, p["name"])):
|
||
continue # 螺钉实例 / 可达包络网格等非 CAD 构件没有 STEP,跳过
|
||
sh = load(p["name"])
|
||
v = volume(sh) / 1000.0
|
||
g = (v * RHO["PLA"] * 1000.0 * INFILL) if p["printed"] else RHO["motor_g"]
|
||
tot[p["name"]] = (g, p["grp"])
|
||
out = {}
|
||
for k, jid in enumerate(TREE[1:], 1):
|
||
down = set(TREE[k:])
|
||
out[jid] = sum(g for g, grp in tot.values() if grp in down)
|
||
out["all"] = sum(g for g, _ in tot.values())
|
||
return out, tot
|
||
|
||
|
||
# ------------------------------------------------------------------ 零件表:质量 + 零位世界质心
|
||
def _mesh_vol_cg(v, t):
|
||
"""四面体法求闭合三角网格的体积与体积加权质心。
|
||
|
||
网格顶点已在零位世界系(mesh_arm.json 由 arm_model 直接落盘),所以质心就是零件
|
||
在装配中的真实位置。实测体积与 STEP/BOM 差 <0.1%(电机包络 1.7%),可作载荷输入。
|
||
"""
|
||
vol = mx = my = mz = 0.0
|
||
for a, b, c in t:
|
||
x1, y1, z1 = v[a]
|
||
x2, y2, z2 = v[b]
|
||
x3, y3, z3 = v[c]
|
||
det = x1 * (y2 * z3 - y3 * z2) - x2 * (y1 * z3 - y3 * z1) + x3 * (y1 * z2 - y2 * z1)
|
||
vol += det
|
||
mx += (x1 + x2 + x3) * det
|
||
my += (y1 + y2 + y3) * det
|
||
mz += (z1 + z2 + z3) * det
|
||
if vol <= 0.0:
|
||
return 0.0, None
|
||
V = vol / 6.0
|
||
return V, (mx / (24.0 * V), my / (24.0 * V), mz / (24.0 * V))
|
||
|
||
|
||
def part_table(payload_kg=None):
|
||
"""[(name, mass_g, cg_xyz, grp)],含末端负载这个集中质量点。"""
|
||
meta = json.load(io.open(DATA + "/mesh_arm.json", encoding="utf-8"))
|
||
rows = []
|
||
for p in meta["parts"]:
|
||
nm = p["name"]
|
||
if nm.startswith("ENV_") or not p.get("v") or not p.get("t") or p.get("vol", 0) <= 0:
|
||
continue # 可达包络网格 / 无实体螺钉实例,不承载
|
||
V, cg = _mesh_vol_cg(p["v"], p["t"])
|
||
if V <= 0.0 or cg is None:
|
||
continue
|
||
if p.get("printed"):
|
||
m = V * RHO["PLA"] * INFILL
|
||
elif re.match(r"^M\d", nm):
|
||
m = RHO["motor_g"]
|
||
else:
|
||
continue
|
||
rows.append([nm, m, cg, GRP.get(p["grp"], 0)])
|
||
if payload_kg:
|
||
rows.append(["PAYLOAD", payload_kg * 1000.0,
|
||
(HL.X_ARM, HL.Y_AXIS, HL.Z_TIP), 6])
|
||
return rows
|
||
|
||
|
||
# ------------------------------------------------------------------ 运动学:产品外参链
|
||
def _rodrigues(deg, d, p):
|
||
"""绕世界轴 d 过点 p 转 deg -> (R, t),使 x -> R x + t。"""
|
||
n = math.sqrt(sum(c * c for c in d)) or 1.0
|
||
x, y, z = d[0] / n, d[1] / n, d[2] / n
|
||
a = math.radians(deg)
|
||
c, s = math.cos(a), math.sin(a)
|
||
t1 = 1.0 - c
|
||
R = [[t1 * x * x + c, t1 * x * y - s * z, t1 * x * z + s * y],
|
||
[t1 * x * y + s * z, t1 * y * y + c, t1 * y * z - s * x],
|
||
[t1 * x * z - s * y, t1 * y * z + s * x, t1 * z * z + c]]
|
||
t = [p[i] - (R[i][0] * p[0] + R[i][1] * p[1] + R[i][2] * p[2]) for i in range(3)]
|
||
return R, t
|
||
|
||
|
||
def chain(ang):
|
||
"""acc[g] = (R, t):第 g 组零件的刚体变换。
|
||
|
||
acc[g] = acc[g-1] o R_g(产品外参,右侧先作用),与 sweep_v2.accum_trsf、
|
||
查看器嵌套 pivot 三者一致。
|
||
"""
|
||
R = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
|
||
t = [0.0, 0.0, 0.0]
|
||
out = [(R, t)]
|
||
for j in range(1, 7):
|
||
ax = HL.AXIS["J%d" % j]
|
||
Rj, tj = _rodrigues(ang.get(j, 0.0), ax["d"], ax["rear"])
|
||
Rn = [[sum(R[i][k] * Rj[k][j2] for k in range(3)) for j2 in range(3)] for i in range(3)]
|
||
tn = [sum(R[i][k] * tj[k] for k in range(3)) + t[i] for i in range(3)]
|
||
R, t = Rn, tn
|
||
out.append((R, t))
|
||
return out
|
||
|
||
|
||
def _xf(T, c):
|
||
R, t = T
|
||
return [sum(R[i][k] * c[k] for k in range(3)) + t[i] for i in range(3)]
|
||
|
||
|
||
def _dir(T, v):
|
||
"""只取旋转部分——方向向量绝不能带平移,否则投影出的"力矩"会被平移量放大。"""
|
||
R = T[0]
|
||
return [sum(R[i][k] * v[k] for k in range(3)) for i in range(3)]
|
||
|
||
|
||
def _cross(a, b):
|
||
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||
|
||
|
||
def joint_loads(parts, payload_kg=None):
|
||
"""在可达姿态网格上搜索,返回每个关节的:
|
||
|
||
F 该关节以下的总重力 (N)(与姿态无关)
|
||
Mxyz 重力对关节轴点取矩后,三个全局分量各自的最大绝对值 (N*mm)
|
||
tau 重力绕该关节自身轴的最大力矩 (N*mm) —— 电机真正要出的力
|
||
Lp 出现 tau 最大值时,TCP 到该关节轴点的距离 (mm)
|
||
|
||
为什么搜索姿态:力臂不是常数。J1 是绕 X 的自转轴,手臂在轴正下方时重力对它
|
||
只产生绕 X 的扭矩(不产生弯曲),手臂摆到侧方时扭矩最大。旧算法把全部 distal
|
||
质量当作集中在指尖,量级完全错掉。
|
||
"""
|
||
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
|
||
|
||
def picks(j):
|
||
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
|
||
if t.get("continuous"):
|
||
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
|
||
lo, hi = t["lo"], t["hi"]
|
||
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
|
||
|
||
grid = [picks(j) for j in range(1, 7)]
|
||
res = {}
|
||
for j in range(1, 7):
|
||
res["J%d" % j] = dict(F=0.0, Mxyz=[0.0, 0.0, 0.0], tau=0.0, Lp=0.0, I=0.0, Iarm=0.0)
|
||
|
||
import itertools
|
||
for combo in itertools.product(*grid):
|
||
ang = {k + 1: v for k, v in enumerate(combo)}
|
||
acc = chain(ang)
|
||
pos = [(nm, m, _xf(acc[g], cg), g) for nm, m, cg, g in parts]
|
||
tcp = None
|
||
for nm, m, c, g in pos:
|
||
if nm == "PAYLOAD":
|
||
tcp = c
|
||
for j in range(1, 7):
|
||
ax = HL.AXIS["J%d" % j]
|
||
pj = _xf(acc[j - 1], ax["rear"])
|
||
dj = _dir(acc[j - 1], ax["d"])
|
||
F = M = [0.0, 0.0, 0.0]
|
||
wsum = 0.0
|
||
I = Iarm = 0.0
|
||
for _nm, m, c, g in pos:
|
||
if g < j:
|
||
continue
|
||
w = m * G981 / 1000.0
|
||
wsum += w
|
||
r = [c[i] - pj[i] for i in range(3)]
|
||
proj = sum(r[i] * dj[i] for i in range(3))
|
||
perp2 = sum(r[i] * r[i] for i in range(3)) - proj * proj
|
||
I += m * perp2 / 1e9 # g*mm^2 -> kg*m^2
|
||
if _nm != "PAYLOAD":
|
||
Iarm += m * perp2 / 1e9
|
||
rc = _cross(r, [0.0, 0.0, -w])
|
||
for i in range(3):
|
||
M[i] += rc[i]
|
||
r = res["J%d" % j]
|
||
r["F"] = max(r["F"], wsum)
|
||
for i in range(3):
|
||
r["Mxyz"][i] = max(r["Mxyz"][i], abs(M[i]))
|
||
tau = abs(M[0] * dj[0] + M[1] * dj[1] + M[2] * dj[2])
|
||
if tau > r["tau"]:
|
||
r["tau"] = tau
|
||
r["Lp"] = math.dist(tcp, pj) if tcp else 0.0
|
||
r["I"] = max(r["I"], I)
|
||
r["Iarm"] = max(r["Iarm"], Iarm)
|
||
return res
|
||
|
||
|
||
def section_moment(jid, bend_axis, ref, refgrp, parts):
|
||
"""截面绕自身形心、沿 bend_axis 的弯矩最大值 (N*mm),以及该姿态下 TCP 到该
|
||
截面的距离 (mm)(挠度估算用)。
|
||
|
||
参考点取【剖面形心】而不是关节轴:这两个参考点差得很远。P03 在 z=480 的剖面
|
||
形心 x≈-111,而臂身所有零件质心 x≈+19.5,真实力臂 130mm;若误用关节轴点
|
||
(J1 在 x=-51)只会得到 70mm,把弯矩低估 1.85 倍。
|
||
"""
|
||
trv = {t["axis"]: t for t in json.load(io.open(DOCS + "/travel.json", encoding="utf-8"))}
|
||
jj = int(jid[1])
|
||
|
||
def picks(j):
|
||
t = trv.get("J%d" % j, {"lo": -180.0, "hi": 180.0})
|
||
if t.get("continuous"):
|
||
return [0.0, 60.0, -60.0, 120.0, -120.0, 180.0]
|
||
lo, hi = t["lo"], t["hi"]
|
||
return sorted({lo, 0.0 if lo < 0 < hi else lo, hi, 0.5 * (lo + hi)})
|
||
|
||
import itertools
|
||
best = 0.0
|
||
best_L = 0.0
|
||
for combo in itertools.product(*[picks(j) for j in range(1, 7)]):
|
||
acc = chain({k + 1: v for k, v in enumerate(combo)})
|
||
rw = _xf(acc[refgrp], ref)
|
||
s = 0.0
|
||
tcp = rw
|
||
for nm, m, cg, g in parts:
|
||
if g < jj:
|
||
continue
|
||
c = _xf(acc[g], cg)
|
||
if nm == "PAYLOAD":
|
||
tcp = c
|
||
s += m * (c[bend_axis] - rw[bend_axis])
|
||
if abs(s) > best:
|
||
best = abs(s)
|
||
best_L = math.dist(tcp, rw)
|
||
return best * G981 / 1000.0, best_L
|
||
|
||
|
||
# critical cuts: (label, part, plane normal, plane origin, which load case owns it)
|
||
def cuts():
|
||
"""(label, part, cut-plane normal, point, joint whose load it carries, bend axis)
|
||
bend axis: 0=X (roll of the shoulder/forearm), 1=Y (every pitch joint), 2=Z."""
|
||
Y = HL.Y_AXIS
|
||
return [
|
||
("吊臂根座 @z=480", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 55.0, 480.0), "J1", 1),
|
||
("吊臂颊板 @z=560", "P03_肩吊臂_螺旋体35%_墙5底5_竖打", (0, 0, 1), (0.0, 100.0, 560.0), "J1", 1),
|
||
("肩叉腹板 @z=560", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 560.0), "J2", 1),
|
||
("肩叉腹板 @z=600", "P04_肩叉_螺旋体30%_墙5底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 600.0), "J2", 1),
|
||
("大臂旋转座横梁 @z=480", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 480.0), "J2", 1),
|
||
("大臂旋转座套筒 @z=440", "P05_大臂旋转座_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 440.0), "J3", 1),
|
||
("大臂骨架中段 @z=350", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 350.0), "J3", 1),
|
||
("大臂骨架抱箍 @z=325", "P06_大臂骨架_螺旋体25%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 325.0), "J4", 1),
|
||
# 【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_工具法兰_螺旋体30%_墙4底5_竖打", (0, 0, 1), (HL.X_ARM, Y, 107.0), "J6", 1),
|
||
]
|
||
|
||
|
||
MOMENT = { # worst reachable bending moment per joint, N*mm
|
||
# computed in main() from the reachable pose + masses
|
||
}
|
||
|
||
|
||
def main():
|
||
parts = part_table(PAYLOAD)
|
||
grpof = {nm: g for nm, _, _, g in parts}
|
||
jl = joint_loads(parts)
|
||
masses = {"J%d" % j: sum(m for _, m, _, g in parts if g >= j) for j in range(1, 7)}
|
||
masses["all"] = sum(m for _, m, _, _ in parts)
|
||
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
|
||
def compute_cut(c_item):
|
||
lab, part, n, o, jid, bend = c_item
|
||
sh = load(part)
|
||
sp = section_props(sh, n, o, bend)
|
||
if not sp:
|
||
return (lab, part, None, jid)
|
||
# 剖面形心的三维重建 + 弯矩所绕的轴
|
||
nvec = [float(x) for x in n]
|
||
p_ax, q_ax = [k for k in (0, 1, 2) if abs(nvec[k]) < 0.5]
|
||
bend_ax = sp["bend"]
|
||
far_ax = p_ax if bend_ax == q_ax else q_ax # I 绕 bend_ax、c 沿 far_ax
|
||
ref = list(o)
|
||
ref[p_ax] += sp["centroid"][0]
|
||
ref[q_ax] += sp["centroid"][1]
|
||
M, L = section_moment(jid, far_ax, ref, grpof.get(part, 0), parts)
|
||
M *= DYN
|
||
F = jl[jid]["F"] * DYN
|
||
I, c = sp["I"], sp["c"]
|
||
sigma = M * c / max(I, 1e-6)
|
||
tau = 1.5 * F / sp["A"]
|
||
defl = M * L * L / (2.0 * EMOD[MAT] * I) if I > 1e-6 and L > 0 else 0.0
|
||
return (lab, part, dict(sp, M=M, F=F, sigma=sigma, tau=tau, L=L, defl=defl,
|
||
bend_axis=bend_ax, SF=ALLOW[MAT] / sigma if sigma > 0 else 99.0), jid)
|
||
|
||
with ThreadPoolExecutor(max_workers=min(13, os.cpu_count() or 4)) as executor:
|
||
rows = list(executor.map(compute_cut, cuts()))
|
||
print("材料 %s 许用弯曲 %g MPa(跨层、含缺口与蠕变折减) 弹性模量 %g MPa 末端负载 %g kg 动载系数 %g"
|
||
% (MAT, ALLOW[MAT], EMOD[MAT], PAYLOAD, DYN))
|
||
print("载荷模型:每个零件按真实质心(网格体积加权)随姿态变换,在可达姿态网格上取弯矩最大值;"
|
||
"参考点为剖面形心\n")
|
||
print("打印件按 %d%% 实体率计质量(壁+填充),电机按 %.0f g/台" % (INFILL * 100, RHO["motor_g"]))
|
||
print("%-24s %-9s %7s %10s %6s %9s %7s %6s %7s %7s"
|
||
% ("截面", "零件", "面积mm2", "I mm4", "c mm", "M N*mm", "sigma", "SF", "挠度mm", "需Z mm3"))
|
||
bad = []
|
||
for lab, part, d, jid in rows:
|
||
if not d:
|
||
print("%-24s %-9s 剖切失败" % (lab, part))
|
||
continue
|
||
Zreq = d["M"] / ALLOW[MAT] * 1.5 # section modulus x1.5 SF
|
||
print("%-24s %-9s %7.0f %10.0f %6.1f %9.0f %7.2f %6.2f %7.1f %7.0f%s"
|
||
% (lab, part[:9], d["A"], d["I"], d["c"], d["M"], d["sigma"], d["SF"], d["defl"],
|
||
Zreq, " <<< 不足" if d["SF"] < 1.5 else ""))
|
||
if d["SF"] < 1.5:
|
||
bad.append((lab, part, d))
|
||
print("\n各关节以下质量(g, 含电机与末端负载): "
|
||
+ " ".join("%s=%.0f" % (k, v) for k, v in masses.items() if k != "all"))
|
||
print("合计 %.0f g (其中末端负载 %.0f g)" % (masses["all"], PAYLOAD * 1000))
|
||
print("\n各关节电机需出的重力力矩(绕自身轴,姿态搜索最大值):")
|
||
for j in range(1, 7):
|
||
r = jl["J%d" % j]
|
||
print(" J%d %6.2f N·m (该关节以下总重 %.1f N,力臂 %.0f mm)"
|
||
% (j, r["tau"] / 1000.0, r["F"], r["Lp"]))
|
||
io.open(DOCS + "/strength.json", "w", encoding="utf-8").write(
|
||
json.dumps([dict(截面=a, 零件=b, 关节=d, **(c or {})) for a, b, c, d in rows],
|
||
ensure_ascii=False, indent=1, default=float))
|
||
return bad
|
||
|
||
|
||
|
||
if __name__ == "__main__":
|
||
b = main()
|
||
print("\n不合格截面 %d 处" % len(b))
|
||
|
||
# OCC/cadquery 的静态析构在解释器正常退出时必定段错误(实测:光是
|
||
# `python -c "import arm.cad.helpers"` 就返回 139,与本项目代码无关)。所以入口
|
||
# 脚本跑完后显式刷缓冲再 os._exit —— 否则全流程已经成功、exit code 却是 139,
|
||
# 会把上游脚本的 set -e 判成失败。只在 __main__ 出口生效, 被 import 时不触发。
|
||
sys.stdout.flush()
|
||
os._exit(0)
|