386 lines
16 KiB
JavaScript
386 lines
16 KiB
JavaScript
/* ============================================================================
|
||
* arm-ik-solver.js — 六轴手臂逆运动学求解器(最小总关节角)
|
||
*
|
||
* 正运动学:World = A1(θ1)·A2(θ2)·A3(θ3)·A4(θ4)·A5(θ5)·A6(θ6)·TCP,
|
||
* Ai(θ) = T(Pi)·R(axis_i, θ)·T(−Pi),与查看器场景图完全一致。
|
||
* 逆运动学:以 (θ4,θ5) 为二维冗余参数全域扫描,其余三轴闭式求解。
|
||
*
|
||
* MIT / 无依赖。可在浏览器(window.ArmIK)或 Node(module.exports)中加载。
|
||
* ==========================================================================*/
|
||
(function (root, factory) {
|
||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||
else root.ArmIK = factory();
|
||
}(typeof self !== 'undefined' ? self : this, function () {
|
||
'use strict';
|
||
|
||
const D2R = Math.PI / 180, R2D = 180 / Math.PI;
|
||
const ORDER = ['J1', 'J2', 'J3', 'J4', 'J5', 'J6'];
|
||
|
||
/* ---------- 小工具 ---------- */
|
||
const wrap180 = a => { a = (a + 180) % 360; if (a < 0) a += 360; return a - 180; };
|
||
const nrm = v => { const n = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / n, v[1] / n, v[2] / n]; };
|
||
function rotAxis(d, a) { // 3x3 旋转:绕单位轴 d 转 a 弧度(右手法)
|
||
const x = d[0], y = d[1], z = d[2], c = Math.cos(a), s = Math.sin(a), C = 1 - c;
|
||
return [
|
||
x * x * C + c, x * y * C - z * s, x * z * C + y * s,
|
||
y * x * C + z * s, y * y * C + c, y * z * C - x * s,
|
||
z * x * C - y * s, z * y * C + x * s, z * z * C + c
|
||
];
|
||
}
|
||
const mul3 = (A, B) => {
|
||
const C = new Array(9);
|
||
for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++)
|
||
C[i * 3 + j] = A[i * 3] * B[j] + A[i * 3 + 1] * B[3 + j] + A[i * 3 + 2] * B[6 + j];
|
||
return C;
|
||
};
|
||
const mv3 = (A, v) => [
|
||
A[0] * v[0] + A[1] * v[1] + A[2] * v[2],
|
||
A[3] * v[0] + A[4] * v[1] + A[5] * v[2],
|
||
A[6] * v[0] + A[7] * v[1] + A[8] * v[2]
|
||
];
|
||
|
||
/* ==========================================================================*/
|
||
function make(cfg) {
|
||
/* ---------- 0. 关节表 → 运动学常量 ---------- */
|
||
const JJ = cfg.joints, TR = cfg.travel || {};
|
||
const P = {}, D = {};
|
||
for (const id of ORDER) {
|
||
if (!JJ[id]) throw new Error('缺少关节 ' + id);
|
||
P[id] = JJ[id].rear.slice();
|
||
D[id] = nrm(JJ[id].d);
|
||
}
|
||
const LIM = {};
|
||
for (const id of ORDER) {
|
||
const t = TR[id] || { lo: -180, hi: 180, continuous: true };
|
||
LIM[id] = { lo: +t.lo, hi: +t.hi, cont: !!t.continuous };
|
||
}
|
||
const TCP0 = (cfg.tcp || [18.5, 128, 93]).slice();
|
||
const ax = D.J6; // 工具沿 J6 轴向外伸
|
||
const K = {
|
||
X0: P.J3[0], Y0: P.J6[1],
|
||
Z1: P.J1[2], Z2: P.J2[2], Z3: P.J3[2], Z4: P.J4[2], Z5: P.J5[2],
|
||
TCP0: TCP0, ax: ax
|
||
};
|
||
K.C0 = K.Z2 - K.Z1;
|
||
K.A43 = K.Z4 - K.Z3;
|
||
K.A32 = K.Z3 - K.Z2;
|
||
K.A54 = K.Z5 - K.Z4;
|
||
K.L5 = K.Z5 - TCP0[2]; // J5 轴 → TCP(沿 −Z)
|
||
/* 结构前提断言:J2/J4/J5 同向平行;J1 轴为 X;J3/J6 轴为 Z;TCP 落在 J6 轴线上 */
|
||
const AXP = v => Math.abs(Math.abs(v[2]) - 1) < 1e-9;
|
||
const WARN = [];
|
||
if (Math.abs(D.J1[0]) < 1e-9 || Math.abs(Math.abs(D.J1[0]) - 1) > 1e-9) WARN.push('J1 轴不是 X');
|
||
for (const id of ['J2', 'J4', 'J5']) if (Math.abs(Math.abs(D[id][1]) - 1) > 1e-9) WARN.push(id + ' 轴不是 Y');
|
||
if (!AXP(D.J3)) WARN.push('J3 轴不是 Z');
|
||
if (!AXP(D.J6)) WARN.push('J6 轴不是 Z');
|
||
if (Math.abs(P.J3[0] - P.J2[0]) > 1e-9 || Math.abs(P.J4[0] - P.J2[0]) > 1e-9 || Math.abs(P.J5[0] - P.J2[0]) > 1e-9)
|
||
WARN.push('J2/J4/J5 不在同一 x 平面');
|
||
if (Math.abs(TCP0[0] - P.J6[0]) > 1e-9 || Math.abs(TCP0[1] - P.J6[1]) > 1e-9)
|
||
WARN.push('TCP 不在 J6 轴线上(θ6 将影响位置)');
|
||
|
||
/* ---------- 1. 严格矩阵链正运动学(对照基准) ---------- */
|
||
function mul4(A, B) {
|
||
const C = new Array(16);
|
||
for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) {
|
||
let s = 0;
|
||
for (let k = 0; k < 4; k++) s += A[i * 4 + k] * B[k * 4 + j];
|
||
C[i * 4 + j] = s;
|
||
}
|
||
return C;
|
||
}
|
||
function mv4(M, v) {
|
||
return [0, 1, 2, 3].map(i => M[i * 4] * v[0] + M[i * 4 + 1] * v[1] + M[i * 4 + 2] * v[2] + M[i * 4 + 3] * v[3]);
|
||
}
|
||
|
||
/* 标准 4x4 同质链 World = A1·A2·…·A6·TCP,Ai = T(Pi)·R(轴,θ)·T(−Pi)
|
||
—— 与查看器场景图(逐级左乘的 matrix 局部变换)逐位等价 */
|
||
function fkChain(q, tool) {
|
||
const tcp = [TCP0[0], TCP0[1], TCP0[2] + (+tool || 0) * ax[2]];
|
||
let M = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1];
|
||
for (let i = 0; i < 6; i++) {
|
||
const id = ORDER[i], p = P[id], R = rotAxis(D[id], q[i] * D2R);
|
||
const rp = mv3(R, p); // Ai 的旋转=R,平移=p−R·p
|
||
const A = [R[0], R[1], R[2], p[0] - rp[0],
|
||
R[3], R[4], R[5], p[1] - rp[1],
|
||
R[6], R[7], R[8], p[2] - rp[2], 0, 0, 0, 1];
|
||
M = mul4(M, A);
|
||
}
|
||
const w = mv4(M, [tcp[0], tcp[1], tcp[2], 1]);
|
||
return [w[0], w[1], w[2]];
|
||
}
|
||
|
||
/* ---------- 2. 闭式正运动学(快速,用于求解内部) ---------- */
|
||
function fk(q, tool) {
|
||
const L = K.L5 + (+tool || 0);
|
||
const s1 = Math.sin(q[0] * D2R), c1 = Math.cos(q[0] * D2R);
|
||
const s2 = Math.sin(q[1] * D2R), c2 = Math.cos(q[1] * D2R);
|
||
const s3 = Math.sin(q[2] * D2R), c3 = Math.cos(q[2] * D2R);
|
||
const s4 = Math.sin(q[3] * D2R), c4 = Math.cos(q[3] * D2R);
|
||
const s5 = Math.sin(q[4] * D2R), c5 = Math.cos(q[4] * D2R);
|
||
const u = -L * s5, w = K.A54 - L * c5;
|
||
const dx = u * c4 + w * s4;
|
||
const dz = K.A43 - u * s4 + w * c4;
|
||
const Vx = dx, Vz = dz + K.A32;
|
||
const b = -Vx * s3;
|
||
const c = K.C0 + (-Vx * c3 * s2 + Vz * c2);
|
||
const a = Vx * c3 * c2 + Vz * s2;
|
||
return [K.X0 + a, K.Y0 + b * c1 - c * s1, K.Z1 + b * s1 + c * c1];
|
||
}
|
||
|
||
/* ---------- 3. 逆解:给定 (θ4,θ5) 的闭式分支 ---------- */
|
||
function branch(th4, th5, A, R, ph, tool) {
|
||
const L = K.L5 + (+tool || 0);
|
||
const s5 = Math.sin(th5 * D2R), c5 = Math.cos(th5 * D2R);
|
||
const s4 = Math.sin(th4 * D2R), c4 = Math.cos(th4 * D2R);
|
||
const u = -L * s5, w = K.A54 - L * c5;
|
||
const dx = u * c4 + w * s4;
|
||
const dz = K.A43 - u * s4 + w * c4;
|
||
const Vx = dx, Vz = dz + K.A32;
|
||
const out = [];
|
||
const n = (R * R + A * A - Vx * Vx - Vz * Vz - K.C0 * K.C0) / (2 * K.C0);
|
||
const cc = K.C0 + n;
|
||
if (Math.abs(Vx) < 1e-7) { // 退化:θ3 不影响 TCP
|
||
if (Math.abs(Vz) < 1e-9) return out;
|
||
const s2 = A / Vz, c2 = n / Vz;
|
||
if (Math.abs(s2 * s2 + c2 * c2 - 1) > 1e-7) return out;
|
||
const th1 = (Math.abs(cc) > 1e-12) ? (ph - Math.atan2(cc, 0)) * R2D : 0;
|
||
out.push([wrap180(th1), wrap180(Math.atan2(s2, c2) * R2D), 0, th4, th5, 0]);
|
||
return out;
|
||
}
|
||
const m2 = (n * n + A * A - Vz * Vz) / (Vx * Vx);
|
||
if (m2 < -1e-9 || m2 > 1 + 1e-9) return out;
|
||
const sig = Math.acos(Math.sqrt(Math.max(0, Math.min(1, m2))));
|
||
const TH = [sig, -sig, Math.PI - sig, -(Math.PI - sig)];
|
||
for (let k = 0; k < 4; k++) {
|
||
const th3 = TH[k];
|
||
const kap = Vx * Math.cos(th3), lam = Vz;
|
||
const det = kap * kap + lam * lam;
|
||
if (det < 1e-12) continue;
|
||
const th2 = Math.atan2((lam * A - kap * n) / det, (kap * A + lam * n) / det) * R2D;
|
||
const b = -Vx * Math.sin(th3);
|
||
const th1 = (Math.abs(b) + Math.abs(cc) > 1e-12) ? (ph - Math.atan2(cc, b)) * R2D : 0;
|
||
out.push([wrap180(th1), wrap180(th2), wrap180(th3 * R2D), th4, th5, 0]);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const inLimit = q => {
|
||
for (let i = 0; i < 5; i++) {
|
||
const t = LIM[ORDER[i]];
|
||
if (q[i] < t.lo - 1e-9 || q[i] > t.hi + 1e-9) return false;
|
||
}
|
||
return true;
|
||
};
|
||
const costOf = (q, W) => {
|
||
let c = 0; for (let i = 0; i < 6; i++) c += Math.abs(q[i]) * (W ? W[i] : 1);
|
||
return c;
|
||
};
|
||
const nowMs = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
||
|
||
/* ---------- 4. 主求解入口 ---------- */
|
||
function solve(target, opt) {
|
||
opt = opt || {};
|
||
const t0 = nowMs();
|
||
const tool = +opt.tool || 0;
|
||
const W = opt.weights || [1, 1, 1, 1, 1, 1];
|
||
const h = Math.max(0.02, +(opt.step || 0.25));
|
||
const A = target[0] - K.X0;
|
||
const BY = target[1] - K.Y0, BZ = target[2] - K.Z1;
|
||
const R = Math.hypot(BY, BZ), ph = Math.atan2(BZ, BY);
|
||
const tol = opt.tol || 1e-6;
|
||
const KEEP = 4000;
|
||
|
||
const evalAt = (t4, t5) => {
|
||
let best = null;
|
||
const sols = branch(t4, t5, A, R, ph, tool);
|
||
for (let k = 0; k < sols.length; k++) {
|
||
const q = sols[k];
|
||
if (!inLimit(q)) continue;
|
||
const p = fk(q, tool);
|
||
const e = Math.hypot(p[0] - target[0], p[1] - target[1], p[2] - target[2]);
|
||
if (e > tol) continue;
|
||
const c = costOf(q, W);
|
||
if (!best || c < best.cost) best = { q: q, cost: c, err: e };
|
||
}
|
||
return best;
|
||
};
|
||
|
||
/* 采样轴:含上下限位端点,避免恰在限位上的最优解被漏掉 */
|
||
const axis = (lo, hi, step) => {
|
||
const v = [];
|
||
const n = Math.floor((hi - lo) / step + 1e-9);
|
||
for (let i = 0; i <= n; i++) v.push(lo + i * step);
|
||
if (v[v.length - 1] < hi - 1e-9) v.push(hi);
|
||
return v;
|
||
};
|
||
const L4 = LIM.J4, L5 = LIM.J5;
|
||
const g4 = axis(L4.lo, L4.hi, h), g5 = axis(L5.lo, L5.hi, h);
|
||
|
||
/* --- 4.1 全域粗扫:覆盖整个二维冗余 (J4,J5) --- */
|
||
let all = [], samples = 0, nSol = 0, trimmed = false;
|
||
for (let i = 0; i < g4.length; i++) {
|
||
for (let j = 0; j < g5.length; j++) {
|
||
samples++;
|
||
const s = evalAt(g4[i], g5[j]);
|
||
if (s) { all.push(s); nSol++; }
|
||
}
|
||
if (all.length > KEEP * 12) { // 有界保留:只留代价最优者
|
||
all.sort((x, y) => x.cost - y.cost);
|
||
all.length = KEEP;
|
||
trimmed = true;
|
||
}
|
||
}
|
||
all.sort((x, y) => x.cost - y.cost);
|
||
|
||
/* --- 4.2 多起点局部加密 + 模式搜索 --- */
|
||
const seeds = [];
|
||
for (let i = 0; i < all.length && seeds.length < 32; i++) {
|
||
const s = all[i];
|
||
let dup = false;
|
||
for (const t of seeds) if (Math.hypot(t.q[3] - s.q[3], t.q[4] - s.q[4]) <= h * 1.5) { dup = true; break; }
|
||
if (!dup) seeds.push(s);
|
||
}
|
||
if (opt.refine !== false) {
|
||
const cl = (v, a, b) => v < a ? a : (v > b ? b : v);
|
||
for (const s of seeds) { // 局部加密网格(h/8,±2h 窗口)
|
||
const hh = h / 8;
|
||
const w4 = axis(Math.max(L4.lo, s.q[3] - 2 * h), Math.min(L4.hi, s.q[3] + 2 * h), hh);
|
||
const w5 = axis(Math.max(L5.lo, s.q[4] - 2 * h), Math.min(L5.hi, s.q[4] + 2 * h), hh);
|
||
let best = s;
|
||
for (const a of w4) for (const b of w5) {
|
||
samples++;
|
||
const c = evalAt(a, b);
|
||
if (c) { nSol++; if (c.cost < best.cost) best = c; }
|
||
}
|
||
all.push(best);
|
||
}
|
||
all.sort((x, y) => x.cost - y.cost);
|
||
const NB = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
|
||
for (const s of all.slice(0, 24)) { // 模式搜索
|
||
let c4 = s.q[3], c5 = s.q[4], best = s, step = h / 4, guard = 0;
|
||
while (step > 2e-4 && guard++ < 400) {
|
||
let moved = false;
|
||
for (const d of NB) {
|
||
const t4 = cl(c4 + d[0] * step, L4.lo, L4.hi), t5 = cl(c5 + d[1] * step, L5.lo, L5.hi);
|
||
if (t4 === c4 && t5 === c5) continue;
|
||
samples++;
|
||
const c = evalAt(t4, t5);
|
||
if (c && c.cost < best.cost - 1e-10) { best = c; c4 = t4; c5 = t5; moved = true; }
|
||
}
|
||
if (!moved) step *= 0.5;
|
||
}
|
||
all.push(best);
|
||
}
|
||
}
|
||
all.sort((x, y) => x.cost - y.cost);
|
||
const best = all.length ? all[0] : null;
|
||
return {
|
||
ok: !!best, reachable: !!best, target: target.slice(), tool: tool,
|
||
q: best ? best.q.slice() : null, cost: best ? best.cost : null,
|
||
err: best ? best.err : null, nSol: nSol, truncated: trimmed,
|
||
samples: samples, ms: nowMs() - t0, list: all.slice(0, KEEP)
|
||
};
|
||
}
|
||
|
||
/* ---------- 5. 最近可达位姿(阻尼最小二乘,兜底用) ---------- */
|
||
function closest(target, tool, tries) {
|
||
const dp = 0.05, lam = 4.0;
|
||
let bestQ = null, bestD = Infinity;
|
||
const run = q => {
|
||
const J = new Float64Array(18);
|
||
for (let it = 0; it < 160; it++) {
|
||
const p = fk(q, tool);
|
||
const d = Math.hypot(p[0] - target[0], p[1] - target[1], p[2] - target[2]);
|
||
if (d < bestD) { bestD = d; bestQ = q.slice(); }
|
||
if (d < 1e-9) break;
|
||
for (let c = 0; c < 6; c++) {
|
||
const s = q.slice(); s[c] += dp;
|
||
const pp = fk(s, tool);
|
||
J[c * 3] = (pp[0] - p[0]) / dp; J[c * 3 + 1] = (pp[1] - p[1]) / dp; J[c * 3 + 2] = (pp[2] - p[2]) / dp;
|
||
}
|
||
let m00 = lam, m01 = 0, m02 = 0, m11 = lam, m12 = 0, m22 = lam;
|
||
for (let c = 0; c < 6; c++) {
|
||
const ax2 = J[c * 3], ay = J[c * 3 + 1], az2 = J[c * 3 + 2];
|
||
m00 += ax2 * ax2; m01 += ax2 * ay; m02 += ax2 * az2;
|
||
m11 += ay * ay; m12 += ay * az2; m22 += az2 * az2;
|
||
}
|
||
const det = m00 * (m11 * m22 - m12 * m12) - m01 * (m01 * m22 - m12 * m02) + m02 * (m01 * m12 - m11 * m02);
|
||
if (Math.abs(det) < 1e-12) break;
|
||
const i00 = (m11 * m22 - m12 * m12) / det, i01 = -(m01 * m22 - m12 * m02) / det,
|
||
i02 = (m01 * m12 - m11 * m02) / det, i11 = (m00 * m22 - m02 * m02) / det,
|
||
i12 = -(m00 * m12 - m01 * m02) / det, i22 = (m00 * m11 - m01 * m01) / det;
|
||
const ex = p[0] - target[0], ey = p[1] - target[1], ez = p[2] - target[2];
|
||
const y0 = -(i00 * ex + i01 * ey + i02 * ez);
|
||
const y1 = -(i01 * ex + i11 * ey + i12 * ez);
|
||
const y2 = -(i02 * ex + i12 * ey + i22 * ez);
|
||
const dq = [0, 0, 0, 0, 0, 0]; let mx = 0;
|
||
for (let c = 0; c < 6; c++) {
|
||
dq[c] = J[c * 3] * y0 + J[c * 3 + 1] * y1 + J[c * 3 + 2] * y2;
|
||
mx = Math.max(mx, Math.abs(dq[c]));
|
||
}
|
||
const gain = mx > 12 ? 12 / mx : 1;
|
||
for (let c = 0; c < 6; c++) {
|
||
const t = LIM[ORDER[c]];
|
||
q[c] = Math.max(t.lo, Math.min(t.hi, q[c] + dq[c] * gain));
|
||
}
|
||
}
|
||
};
|
||
run([0, 0, 0, 0, 0, 0]);
|
||
const N = tries == null ? 160 : tries;
|
||
for (let k = 0; k < N; k++) {
|
||
const q = [0, 0, 0, 0, 0, 0];
|
||
for (let c = 0; c < 5; c++) { const t = LIM[ORDER[c]]; q[c] = t.lo + Math.random() * (t.hi - t.lo); }
|
||
run(q);
|
||
}
|
||
return { q: bestQ, dist: bestD };
|
||
}
|
||
|
||
/* ---------- 6. 一键求解(不可达时返回最近可达位姿) ---------- */
|
||
function solveTo(target, opt) {
|
||
opt = opt || {};
|
||
const res = solve(target, opt);
|
||
if (res.ok) return res;
|
||
const cp = closest(target, +opt.tool || 0, opt.tries);
|
||
if (!cp.q) return res;
|
||
return {
|
||
ok: false, reachable: false, target: target.slice(), tool: +opt.tool || 0,
|
||
q: cp.q.slice(), cost: costOf(cp.q, opt.weights), err: cp.dist, resid: cp.dist,
|
||
nSol: res.nSol, samples: res.samples, ms: res.ms, list: [],
|
||
note: '目标超出可达工作空间:返回最近可达位姿(阻尼最小二乘)'
|
||
};
|
||
}
|
||
|
||
/* ---------- 7. 自检:闭式 FK ↔ 矩阵链;逆解重构 ---------- */
|
||
function selfTest(n) {
|
||
n = n || 400;
|
||
let maxFk = 0;
|
||
for (let k = 0; k < n; k++) {
|
||
const q = [];
|
||
for (let i = 0; i < 6; i++) { const t = LIM[ORDER[i]]; q.push(t.cont ? (Math.random() * 720 - 360) : t.lo + Math.random() * (t.hi - t.lo)); }
|
||
const a = fk(q, 0), b = fkChain(q, 0);
|
||
maxFk = Math.max(maxFk, Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]));
|
||
}
|
||
let maxIk = 0, hit = 0, m = 0;
|
||
for (let k = 0; k < Math.min(n, 200); k++) {
|
||
const q = [];
|
||
for (let i = 0; i < 6; i++) { const t = LIM[ORDER[i]]; q.push(t.lo + Math.random() * (t.hi - t.lo)); }
|
||
const T = fk(q, 0);
|
||
const A = T[0] - K.X0, BY = T[1] - K.Y0, BZ = T[2] - K.Z1;
|
||
const sols = branch(q[3], q[4], A, Math.hypot(BY, BZ), Math.atan2(BZ, BY), 0);
|
||
if (!sols.length) continue;
|
||
m++;
|
||
let best = Infinity;
|
||
for (const s of sols) best = Math.min(best, Math.hypot.apply(null, fk(s, 0).map((v, i) => v - T[i])));
|
||
maxIk = Math.max(maxIk, best);
|
||
if (best < 1e-6) hit++;
|
||
}
|
||
return { maxFkErr: maxFk, maxIkErr: maxIk, ikHit: hit, ikTried: m, structWarn: WARN, K: K, LIM: LIM };
|
||
}
|
||
|
||
return { fk: fk, fkChain: fkChain, solve: solve, solveTo: solveTo,
|
||
closest: closest, branch: branch, selfTest: selfTest,
|
||
K: K, LIM: LIM, P: P, D: D, ORDER: ORDER, wrap180: wrap180, costOf: costOf };
|
||
}
|
||
|
||
return { make: make, ORDER: ORDER };
|
||
})); |