Files
Robot_Arm/web/arm-ik-solver.js
T

386 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
* 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 };
}));