feat(arm): 建模代码拆成每件一模块,查看器新增工程图与装配步骤页
- src/parts/p01~p09 + src/part_tools.py:几何、件号/配色/说明与装配帧归属随各自件文件走,arm_model.py 只留装配流水线并对件级符号做只读转发;P04_CH / P07_CH / P06_WIN_XZ / FENCE_OPEN_CUT 这类开关必须写所在件文件,写 arm_model.X 不生效,A/B 判据为翻转后体积变化
- docs/sheet_{exploded,front,side,joint}.svg + draw_sheet.py:四张工程图进查看器页签,支持图层开关、件号高亮与 BOM/3D 联动跳转
- travel/travel_detail/collide/bom/screws 随最新几何重生成;补线束走线、拆卸定位点与 P04 承压面剖切验证图
- .gitignore 排除 .bak/ 与 *.bak_* 源码快照(旧版本 git 历史里已有)
This commit is contained in:
+76
-29
@@ -269,24 +269,43 @@ function refreshJointLabelsLazy(q) {
|
||||
refreshJointLabels(ORDER.map(id => ang[id]));
|
||||
}, 260);
|
||||
}
|
||||
/* 六轴动态边界改到 Worker 里算:连续轴现在必须整周粗扫(实测 6 轴×2 向一次
|
||||
175ms → 1131ms),留在主线程会把拖拽冻住。dynAsync 只记账、不写 IKWlast,
|
||||
否则会把在飞的 solveTo/solveDyn 结果误标成过期(历史上丢过回调导致实时追踪停摆)。 */
|
||||
function dynAsync(q, tol, job) {
|
||||
const w = ikWorker();
|
||||
if (!w) { job(S.dynLimits(q, tol)); return; } // Worker 不可用:退回主线程同步算
|
||||
const id = ++IKWseq;
|
||||
IKWjobs.set(id, r => { job(r && !r.__err ? r : S.dynLimits(q, tol)); });
|
||||
w.postMessage({ id: id, op: 'dynLimits', args: [q, tol] });
|
||||
}
|
||||
function refreshJointLabels(q) {
|
||||
dynAsync(q.slice(), 1.0, D => paintJointLabels(D));
|
||||
}
|
||||
function paintJointLabels(D) {
|
||||
D = D || {};
|
||||
for (const id of ORDER) {
|
||||
const el = $('e' + id);
|
||||
if (!el) continue;
|
||||
const t = LIM[id];
|
||||
if (COL && !t.cont) {
|
||||
if (COL && D[id]) {
|
||||
/* 边界由几何说话:机械区间(adj 实测)之内,再按当前姿态二分胶囊碰撞。
|
||||
lo 侧一律收紧;hi 侧只有"硬边"(相邻件自碰撞,配置无关)才受静态值封顶,
|
||||
可放宽侧(撞固定立柱那类)由模型说话——动态边界本就可以远超静态 hi。 */
|
||||
const lo = Math.max(t.lo, S.dynLimit(id, q, -1, 1.0));
|
||||
可放宽侧(撞固定立柱那类)由模型说话——动态边界本就可以远超静态 hi。
|
||||
连续轴(J1/J3/J6)同样要查:折肘/折腕位形下末段会扫到固定立柱,
|
||||
旧口径因为跳过连续轴而把它显示成整周可用。 */
|
||||
const lo = Math.max(t.cont ? -180 : t.lo, D[id].lo);
|
||||
const hardHi = COL.hard && COL.hard[id] && COL.hard[id].hi;
|
||||
const hi = hardHi ? Math.min(t.hi, S.dynLimit(id, q, 1, 1.0))
|
||||
: S.dynLimit(id, q, 1, 1.0);
|
||||
const hi = hardHi ? Math.min(t.hi, D[id].hi) : D[id].hi;
|
||||
const dyn = Math.abs(lo - t.lo) > 0.5 || Math.abs(hi - t.hi) > 0.5; // 与静态不同(宽或窄)都标记
|
||||
el.innerHTML = (dyn ? '<b style="color:var(--warn)">' : '') +
|
||||
lo.toFixed(0) + '\u00B0\u2026' + hi.toFixed(0) + '\u00B0' + (dyn ? ' \u25B2</b>' : '');
|
||||
/* 连续轴没收紧时显示"整周可用",收紧了就换成区间:同一个 <em> 是唯一写手,
|
||||
别让静态的 360 度连续徽标和实时夹取区间并存(状态灯与数字会打架)。 */
|
||||
el.className = dyn ? '' : (t.cont ? 'ok' : '');
|
||||
el.innerHTML = (t.cont && !dyn) ? '整周可用' :
|
||||
((dyn ? '<b style="color:var(--warn)">' : '') +
|
||||
lo.toFixed(0) + '\u00B0\u2026' + hi.toFixed(0) + '\u00B0' + (dyn ? ' \u25B2</b>' : ''));
|
||||
const st = STATIC_TL[id] || {};
|
||||
el.title = '机械区间(相邻件实测)' + t.lo + '\u2026' + t.hi +
|
||||
el.title = (t.cont ? '连续轴(整周)' : '机械区间(相邻件实测)') + t.lo + '\u2026' + t.hi +
|
||||
'\u00B0;电机行程 ±' + MOTOR + '\u00B0;当前姿态动态边界 ' +
|
||||
lo.toFixed(1) + '\u2026' + hi.toFixed(1) + '\u00B0(胶囊碰撞模型实时算)' +
|
||||
(st.lo != null ? '\n零位静态扫描参考 ' + st.lo + '\u2026' + st.hi + '\u00B0' : '');
|
||||
@@ -298,8 +317,8 @@ function refreshJointLabels(q) {
|
||||
|
||||
/* ---------- 3. 关节写入(同步滑块与标签)---------- */
|
||||
let anim = null;
|
||||
function setJoints(q, doAnim) {
|
||||
if (doAnim) { animTo(q, 620); return; }
|
||||
function setJoints(q, doAnim, via, ms) {
|
||||
if (doAnim) { animTo(q, ms || 620, via); return; }
|
||||
anim = null; // 直接写入必须取消在飞动画,否则入场动画会覆写实时解
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i]; ang[id] = q[i];
|
||||
@@ -311,7 +330,7 @@ function setJoints(q, doAnim) {
|
||||
refreshTarget();
|
||||
if (COL) refreshJointLabelsLazy(ORDER.map(id => ang[id]));
|
||||
}
|
||||
function animTo(q, ms) {
|
||||
function animTo(q, ms, via) {
|
||||
if (anim) { anim = null; }
|
||||
const from = ORDER.map(id => ang[id]);
|
||||
/* 连续轴(J1 肩抬举 / J3 大臂自转 / J6 腕自转)走「短弧」。
|
||||
@@ -319,20 +338,31 @@ function animTo(q, ms) {
|
||||
若按数值直接线性插值,+151.9° → −167.0° 会反向扫 318.9° 绕一整圈(实测)。
|
||||
这里先求「离起始角最近的等价角」作为插值终点,运动变成 41.1° 的短弧;
|
||||
end 保存求解器给的代表角,末帧落回它——与插值终点恰好相差 360° 的整数倍,
|
||||
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。 */
|
||||
const to = q.slice();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i];
|
||||
if (LIM[id].cont) to[i] = from[i] + S.wrap180(q[i] - from[i]);
|
||||
同一物理角,视觉零跳变,同时保证滑块/关节表数值与求解结果一致。
|
||||
via:绕行中间位形(solveRoute 判直线被挡时给的两段式路径),逐段同样按短弧折算,
|
||||
每段各 ms 毫秒——渲染走的段与碰撞校验走的段是同一批,不存在"校验过了但动画穿模"。 */
|
||||
const segs = [];
|
||||
let p = from;
|
||||
for (const raw of (via || []).concat([q])) {
|
||||
const t = raw.slice();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i];
|
||||
if (LIM[id].cont) t[i] = p[i] + S.wrap180(raw[i] - p[i]);
|
||||
}
|
||||
segs.push(t); p = t;
|
||||
}
|
||||
const end = q.slice();
|
||||
anim = { from: from, to: to, t0: performance.now(), ms: ms };
|
||||
const total = ms * segs.length;
|
||||
anim = { from: from, to: segs[segs.length - 1], t0: performance.now(), ms: total };
|
||||
const tick = () => {
|
||||
if (!anim) return;
|
||||
const k = Math.min(1, (performance.now() - anim.t0) / anim.ms);
|
||||
const el = performance.now() - anim.t0;
|
||||
const si = Math.min(segs.length - 1, Math.floor(el / ms));
|
||||
const k = Math.min(1, (el - si * ms) / ms);
|
||||
const e = k * k * (3 - 2 * k);
|
||||
const a0 = si === 0 ? from : segs[si - 1], a1 = segs[si];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id = ORDER[i], a = anim.from[i] + (anim.to[i] - anim.from[i]) * e;
|
||||
const id = ORDER[i], a = a0[i] + (a1[i] - a0[i]) * e;
|
||||
ang[id] = a;
|
||||
const sl = $('s' + id), v = $('v' + id);
|
||||
if (sl) sl.value = a;
|
||||
@@ -459,6 +489,13 @@ function renderResult(res, src) {
|
||||
const mixMid = poseOn && res.priority === 'mix' && !posOk && !oriOk;
|
||||
badge.textContent = res.unsafe
|
||||
? '位姿与底座/立柱近似干涉(目标可能不可达或位于立柱区内)'
|
||||
: res.blocked
|
||||
? '路径被立柱挡住 · 已停在边界(目标差 ' + dsc.toFixed(1) + ' mm)'
|
||||
: res.detour
|
||||
? (res.detour === 'retract_pass' ? '避障到达 · 模拟人类回退绕过立柱'
|
||||
: (res.detour === 'gentle' ? '避障到达 · 微调协同避开立柱'
|
||||
: (res.detour === 'coordinated' ? '避障到达 · 平滑协同跨越立柱'
|
||||
: '绕行立柱后到达')))
|
||||
: (posOk && oriOk) ? '已到达'
|
||||
: oriPrio ? '姿态到达·位置最近 ' + dsc.toFixed(1) + ' mm'
|
||||
: mixMid ? '混合折中 · 位置差 ' + dsc.toFixed(1) + ' mm · 姿态残差 ' + dori.toFixed(2) + '\u00B0'
|
||||
@@ -569,7 +606,8 @@ function runSolve() {
|
||||
refreshTarget();
|
||||
badge.textContent = '计算中…'; badge.className = 'badge wait';
|
||||
setTimeout(() => {
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef() };
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(),
|
||||
from: ORDER.map(id => ang[id]) }; // 松手求解同样按"直线能不能走"挑解
|
||||
if (poseOn) { opt.pose = { R: Rsolver() }; opt.priority = prioVal(); if (opt.priority === 'mix') opt.mixW = mixWVal(); }
|
||||
solveAsync('solveDyn', [TGT.slice(), opt], (res, fresh) => {
|
||||
if (!res || !fresh) return; // 已被更新的一发取代:交给那一发渲染
|
||||
@@ -582,7 +620,7 @@ function runSolve() {
|
||||
: (res.note ? res.note
|
||||
: (poseOn ? '超可达位姿空间 → 阻尼最小二乘最近位姿(位置 mm + 姿态联合)'
|
||||
: '超工作空间 → 阻尼最小二乘最近位置')));
|
||||
if (res.q) setJoints(res.q, true);
|
||||
if (res.q) setJoints(res.q, true, res.via);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
@@ -604,9 +642,13 @@ function runSolveLive() {
|
||||
};
|
||||
try {
|
||||
lastTool = +$('ikTool').value || 0;
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), warm: curRef() };
|
||||
/* 实时追踪原来走 solveTo —— 一个碰撞判定都不做:实测把目标球横拖过立柱,
|
||||
1300 帧里 870 帧处于穿模状态。改用 solveRoute:按"从当前位形这条直线能不能走"
|
||||
挑解,走不通就贴着障碍停(blocked),不再把手势直接映射进立柱里。 */
|
||||
const opt = { tool: lastTool, step: +$('ikStep').value || 0.25, ref: curRef(), warm: curRef(),
|
||||
from: ORDER.map(id => ang[id]), noVia: true, quick: true }; // 拖拽中不做抬高版绕行(要再解两次全量 IK)
|
||||
if (poseOn) { opt.pose = { R: Rsolver() }; opt.priority = prioVal(); if (opt.priority === 'mix') opt.mixW = mixWVal(); }
|
||||
solveAsync('solveTo', [TGT.slice(), opt], (res, fresh) => { // Worker 后台:全量回落也不冻结主线程
|
||||
solveAsync('solveRoute', [TGT.slice(), opt], (res, fresh) => { // Worker 后台:全量回落也不冻结主线程
|
||||
try {
|
||||
if (!res || !fresh) return;
|
||||
renderResult(res, res.warm
|
||||
@@ -623,7 +665,9 @@ function runSolveLive() {
|
||||
: (res.note ? '实时追踪 · ' + res.note
|
||||
: (poseOn ? '实时追踪 · 超可达位姿空间(阻尼最小二乘最近位姿)'
|
||||
: '实时追踪 · 超工作空间(阻尼最小二乘最近位姿)'))));
|
||||
if (res.q) setJoints(res.q, false);
|
||||
/* 无绕行时立即落位(跟手);一旦给出绕行中间位形,就必须分段动画——
|
||||
直接跳到 res.q 等于把"收臂→转 J1→伸展"三段压成一条直线,正是要避免的穿模。 */
|
||||
if (res.q) setJoints(res.q, !!(res.via && res.via.length), res.via, 300);
|
||||
} finally { settle(fresh); }
|
||||
});
|
||||
} catch (err) { settle(false); }
|
||||
@@ -835,7 +879,9 @@ const MOTION_UI = ['ikbar', 'ikpanel', 'joints', 'jointsHead', 'footIk'];
|
||||
function setView(v, keepPose) {
|
||||
VIEW = v;
|
||||
const isMotion = v === 'motion';
|
||||
const isSheet = v === 'sheet';
|
||||
document.body.classList.toggle('motion', isMotion);
|
||||
document.body.classList.toggle('sheet-mode', isSheet);
|
||||
for (const id of MOTION_UI) {
|
||||
const e = $(id);
|
||||
if (e) e.style.display = isMotion ? '' : 'none';
|
||||
@@ -849,6 +895,7 @@ function setView(v, keepPose) {
|
||||
} else {
|
||||
if (mode !== 'drag') setMode('drag'); // 离开运动模拟:交互模式复位
|
||||
if (v === 'model' && !keepPose) setJoints([0, 0, 0, 0, 0, 0], true); // 静态模型:关节锁定零位
|
||||
if (isSheet && typeof openSheetView === 'function') openSheetView();
|
||||
}
|
||||
requestRender();
|
||||
}
|
||||
@@ -861,11 +908,11 @@ document.querySelectorAll('.tab[data-view]').forEach(b =>
|
||||
与当前代码不符时会标红提示重算。
|
||||
重新生成:node scratch/compute_precomputed.cjs(模型或求解器改动后重跑一条命令) */
|
||||
const PRECOMPUTED_CHK = {
|
||||
stamp: '2026-09-30 00:35', sig: '5153f522',
|
||||
fk: { n: 5000, err: 5.749691446267182e-13 },
|
||||
ik: { n: 200, hit: 200, tried: 200, err: 9.94353722747741e-13 },
|
||||
pose: { n: 400, hit: 398, err: 1.7922893504241644, ms: 77.80773925 },
|
||||
ws: { n: 400000, x: [-378, 415], y: [-388, 642], z: [94, 1125], rmax: 517 }
|
||||
stamp: '2026-10-14 04:02', sig: 'fd1f8bff',
|
||||
fk: { n: 5000, err: 5.684341886080801e-13 },
|
||||
ik: { n: 200, hit: 200, tried: 200, err: 8.320331044333469e-13 },
|
||||
pose: { n: 400, hit: 398, err: 1.5596676084670544, ms: 86.40111575000003 },
|
||||
ws: { n: 400000, x: [-377, 416], y: [-388, 643], z: [96, 1126], rmax: 517 }
|
||||
};
|
||||
/* 紧凑指纹:与离线脚本 compute_precomputed.cjs 里的算法逐字一致
|
||||
(注意取 script.textContent——HTML 解析会把行尾归一化成 LF) */
|
||||
|
||||
Reference in New Issue
Block a user