feat: P07小臂骨架顶板整条加高19mm补强+前后外缘R3圆角,gusset弧R28->R14随顶板联动相切融入;重算全关节行程J4反向-121->-111,同步重建模型数据与查看器

This commit is contained in:
cbb
2026-09-28 01:00:04 +08:00
parent 85856e9821
commit 5518d12abb
31 changed files with 257 additions and 1283 deletions
+82 -43
View File
@@ -9,9 +9,9 @@
html,body{margin:0;height:100%;background:var(--bg);color:var(--tx);
font:13px/1.55 "Microsoft YaHei","Segoe UI",system-ui,sans-serif;overflow:hidden;
-webkit-user-select:none;user-select:none}
#app{display:grid;grid-template-columns:274px minmax(430px,1fr) 320px;grid-template-rows:46px 1fr 26px;
#app{display:grid;grid-template-columns:274px minmax(430px,1fr);grid-template-rows:46px 1fr 26px;
height:100%;min-width:0;overflow-x:auto}
header{grid-column:1/4;display:flex;align-items:center;gap:8px;padding:0 12px;flex-wrap:nowrap;
header{grid-column:1/-1;display:flex;align-items:center;gap:8px;padding:0 12px;flex-wrap:nowrap;
overflow-x:auto;overflow-y:hidden;background:var(--pan);border-bottom:1px solid var(--line)}
header .q{white-space:nowrap;flex:none;color:var(--dim);font-size:12px}
header b{font-size:14px;white-space:nowrap;flex:none}
@@ -22,19 +22,15 @@ button:hover{border-color:var(--ac);color:#fff}
button.on{background:#22406b;border-color:var(--ac)}
aside{background:var(--pan);overflow:auto;padding:8px;min-width:0}
#left{border-right:1px solid var(--line)}
#right{border-left:1px solid var(--line)}
#app.r{grid-template-columns:274px minmax(430px,1fr) 320px}
#app.l{grid-template-columns:274px minmax(430px,1fr)}
#stage{position:relative;overflow:hidden;min-width:0;
background:radial-gradient(120% 120% at 50% 0%,#1b2028 0%,#0d0f13 70%)}
canvas{display:block;width:100%;height:100%}
footer{grid-column:1/4;background:var(--pan);border-top:1px solid var(--line);
footer{grid-column:1/-1;background:var(--pan);border-top:1px solid var(--line);
display:flex;align-items:center;gap:14px;padding:0 12px;color:var(--dim);font-size:12px;
white-space:nowrap;overflow:hidden}
@media (max-width:1150px){
#app{grid-template-columns:250px minmax(400px,1fr)}
#right{display:none}
#app.r #right{display:block}
}
@media (max-width:760px){
#app{grid-template-columns:minmax(0,1fr)}
@@ -75,6 +71,27 @@ label em.ok{color:#5ddc90}
svg{display:block;width:100%;height:auto;background:#fff;border-radius:6px}
#sheet{display:none;padding:10px;overflow:auto;height:100%}
small{color:var(--dim)}
/* ---- edge BOM dock (collapsible drawer at the right edge) ---- */
#bomTab{position:fixed;right:0;top:64px;z-index:30;writing-mode:vertical-rl;text-orientation:mixed;
background:var(--pan2);color:var(--tx);border:1px solid var(--line);border-right:none;
border-radius:6px 0 0 6px;padding:12px 6px;font:inherit;cursor:pointer;letter-spacing:3px}
#bomTab:hover{border-color:var(--ac);color:#fff}
#bomTab b{color:var(--ac)}
#bomDock{position:fixed;right:0;top:46px;bottom:26px;width:min(430px,94vw);z-index:29;
transform:translateX(103%);transition:transform .22s ease;display:flex;flex-direction:column;
background:var(--pan);border-left:1px solid var(--ac);box-shadow:-10px 0 28px rgba(0,0,0,.5)}
#bomDock.open{transform:none}
#bomDock .dk-hd{display:flex;align-items:center;gap:8px;padding:9px 10px;flex:none;
border-bottom:1px solid var(--line)}
#bomDock .dk-hd b{font-size:13px}
#bomDock .dk-hd .q{color:var(--dim);font-size:11px}
#bomDock .dk-x{margin-left:auto}
#bomDock .dk-bd{overflow:auto;padding:8px 11px 16px;flex:1;min-height:0}
.dk-sec{margin:4px 0 5px;color:var(--ac);font-size:12px;letter-spacing:.4px}
.dk-note{color:var(--dim);font-size:11px;margin:1px 0 6px}
#screws td{white-space:normal}
#screws td:first-child{white-space:nowrap}
@media (max-width:760px){#bomTab{top:auto;bottom:36px}#bomDock{top:46px;bottom:26px}}
</style>
</head>
<body>
@@ -97,7 +114,6 @@ small{color:var(--dim)}
<button id="bXRay" class="on">电机半透</button>
<button id="bFit">复位</button>
<button id="bL" title="显示/隐藏左栏">左栏</button>
<button id="bR" title="显示/隐藏右栏">右栏</button>
</header>
<aside id="left">
@@ -121,15 +137,6 @@ small{color:var(--dim)}
<div id="sheet"></div>
</div>
<aside id="right">
<h3>BOM / 明细</h3>
<table id="bom"><thead><tr><th>#</th><th>件号</th><th>cm3</th><th>g</th></tr></thead><tbody></tbody></table>
<h3>接口自检(实测值)</h3>
<div class="kv" id="iface"></div>
<h3>碰撞检查</h3>
<div id="col"></div>
</aside>
<footer>
<span>左键旋转 · 滚轮缩放 · 右键平移(拖动) · 单击选中高亮 · <b>右键单击隐藏部件(或按H键)</b> · 左侧全选/勾选恢复</span>
<span class="sp"></span>
@@ -137,6 +144,21 @@ small{color:var(--dim)}
</footer>
</div>
<button id="bomTab" title="展开 / 收起 物料清单 BOM">物料清单 <b>BOM</b></button>
<aside id="bomDock">
<div class="dk-hd">
<b>物料清单 BOM</b><span class="q" id="bomStat"></span>
<button id="bomHide" class="dk-x" title="收起">收起 ▶</button>
</div>
<div class="dk-bd">
<div class="dk-sec">紧固件 / 螺丝清单(全部紧固点)</div>
<div class="dk-note" id="scrSum"></div>
<table id="screws"><thead><tr><th>规格</th><th>长 mm</th><th>数量</th><th>位置 / 用途</th></tr></thead><tbody></tbody></table>
<div class="dk-sec">打印件 BOM(原明细,PLA)</div>
<table id="bom"><thead><tr><th>#</th><th>件号</th><th>cm3</th><th>g</th></tr></thead><tbody></tbody></table>
</div>
</aside>
<script>/*__THREE__*/</script>
<script>
const DATA = /*__DATA__*/;
@@ -249,6 +271,10 @@ setExpl(0); // start assembled, not exploded
/* ---- camera orbit ---- */
let az = -0.9, po = 1.15, rd = 620, tx = 120, ty = 190, tz = -30;
// lookAt with the world up-axis degenerate-rolls the image when looking near-vertically,
// so top view pins the screen-up to -Z: model X runs right, depth runs down-screen
const UP_Y = new V3(0, 1, 0), UP_TOP = new V3(0, 0, -1);
let camUp = UP_Y.clone();
function fitTo(b) {
const c = [(b[0][0]+b[1][0])/2, (b[0][1]+b[1][1])/2, (b[0][2]+b[1][2])/2];
tx = c[0]; ty = c[1]; tz = c[2];
@@ -268,6 +294,7 @@ let BB = [[1e9,1e9,1e9],[-1e9,-1e9,-1e9]];
})();
fitTo(BB);
function updCam() {
cam.up.copy(camUp);
cam.position.set(tx + rd*Math.sin(po)*Math.cos(az), ty + rd*Math.cos(po), tz + rd*Math.sin(po)*Math.sin(az));
cam.lookAt(tx, ty, tz);
}
@@ -496,30 +523,40 @@ document.getElementById('sum').innerHTML =
'<i>总质量(PLA)</i><span>' + gTot.toFixed(0) + ' g</span>' +
'<i>电机数量</i><span>' + NMOT + ' 台(同款 电机.STEP)</span>' +
'<i>工作空间</i><span>约 ' + Math.round(BB[1][2]) + ' x ' + Math.round(BB[1][0]) + ' mm</span>';
/* ---- measured interfaces ---- */
const md = DATA.measured, Mt = md.M, Ct = md.C;
document.getElementById('iface').innerHTML = [
['电机外形', 'Ø' + Mt.hous_d + ' x ' + Mt.hous_len + ',整机长 ' + Mt.cover_z + ',尾部 PCBA 方 ' + Mt.pcba_w],
['减速', '两级 0.5M 行星:24T 太阳 / 3x36T 行星 / 96T 内齿'],
['输出接口', 'Ø' + Mt.neck_d + 'x' + Mt.neck_depth + ' 颈孔 + Ø' + Mt.centre_d + ' 中心孔'],
['螺栓', '3 x M2 @ BCD ' + Mt.m2_bcd + ',时钟 ' + Mt.m2_clock.join('/')],
['贴合面', '电机尾基准 + ' + md.HUB_FACE_Z + ' = 从动盘贴合面(联轴器两盖板面跨距 ' + Ct.gap + ')'],
['抱箍', '带宽 ' + Ct.clamp_od_r*2 + ',凸耳 R' + Ct.lug_r1 + ',轴向 ' + md.CLAMP_SPAN.join('~')],
['齿', Ct.n_teeth + ' 齿端面花键 R' + Ct.tooth_r0 + '~' + Ct.tooth_r1 + ',齿高 ' + Ct.tooth_s],
].map(r => '<i>' + r[0] + '</i><span>' + r[1] + '</span>').join('');
/* ---- collision ---- */
const CL = /*__COL__*/;
document.getElementById('col').innerHTML = CL.length
? '<table><tr><th>零件对</th><th>mm3</th></tr>' +
CL.map(r => '<tr><td>' + r[0] + ' x ' + r[1] + '</td><td class="bad">' + r[2] + '</td></tr>').join('') + '</table>'
: '<span class="good">0 对干涉(全部零件布尔交集体积 &lt; 2 mm3)</span>';
/* ---- edge BOM dock: fasteners + merged parts BOM ---- */
const SCREWS = /*__SCREWS__*/;
const skb = document.querySelector('#screws tbody');
const bySize = {};
let nTot = 0;
for (const s of SCREWS) {
nTot += s.数量;
bySize[s.螺纹] = (bySize[s.螺纹] || 0) + s.数量;
const tr = document.createElement('tr');
tr.innerHTML = '<td><b>' + s.规格 + '</b><br><small>' + s.头型 + '</small></td>' +
'<td>' + s.长度mm + '</td><td>' + s.数量 + '</td>' +
'<td>' + s.位置 + '<br><small>' + s.备注 + '</small></td>';
skb.appendChild(tr);
}
const tally = Object.keys(bySize).sort().map(k => k + 'x' + bySize[k]).join(' / ');
document.getElementById('scrSum').textContent =
'共 ' + SCREWS.length + ' 种 ' + nTot + ' 颗(' + tally + ');另需 M4 螺母 4、M5 螺母 4、M6 螺母+平垫圈各 6,见各行备注';
document.getElementById('bomStat').textContent =
'紧固件 ' + nTot + ' 颗 / 打印件 ' + DATA.parts.filter(p=>p.printed).length + ' 种';
const dock = document.getElementById('bomDock'), bTab = document.getElementById('bomTab');
function dockSet(open) {
dock.classList.toggle('open', open);
bTab.style.display = open ? 'none' : ''; // 展开时拉手藏掉,否则压住抽屉右缘
bTab.innerHTML = open ? '收起 <b>清单</b>' : '物料清单 <b>BOM</b>';
}
bTab.onclick = () => dockSet(!dock.classList.contains('open'));
document.getElementById('bomHide').onclick = () => dockSet(false);
document.addEventListener('keydown', e => { if (e.key === 'Escape') dockSet(false); });
/* ---- toolbar ---- */
const bAsm = document.getElementById('bAsm');
const bExp = document.getElementById('bExp');
const bWire = document.getElementById('bWire');
const bXRay = document.getElementById('bXRay');
const bL = document.getElementById('bL');
const bR = document.getElementById('bR');
const sheet = document.getElementById('sheet');
const stage = document.getElementById('stage');
const on = (id, f) => document.getElementById(id).onclick = f;
@@ -532,27 +569,29 @@ on('bWire', () => { bWire.classList.toggle('on'); const w = bWire.classList.cont
meshes.forEach(m => { m.material.wireframe = w; }); requestRender(); });
on('bXRay', () => { bXRay.classList.toggle('on'); const o = bXRay.classList.contains('on');
meshes.forEach(m => { if (!m.userData.printed && m.userData.name[0]==='M') { m.material.opacity = o ? .34 : 1; m.material.transparent = o; } }); requestRender(); });
on('bFit', () => { fitTo(BB); az = -0.9; po = 1.15; requestRender(); });
on('bFit', () => { fitTo(BB); az = -0.9; po = 1.15; camUp.copy(UP_Y); requestRender(); });
const app = document.getElementById('app');
on('bL', () => { app.classList.toggle('l'); bL.classList.toggle('on'); });
on('bR', () => { app.classList.toggle('r'); bR.classList.toggle('on'); });
bL.classList.add('on'); bR.classList.add('on');
bL.classList.add('on');
document.getElementById('cam').onchange = e => {
const k = e.target.value;
camUp.copy(UP_Y); // upright roll for every view unless pinned below
if (k==='iso'){az=-0.9;po=1.15;} if(k==='front'){az=0;po=1.57;}
if(k==='side'){az=1.5708;po=1.57;}
// looking down the arm, the height dimension is irrelevant - frame by the PLAN extent
if(k==='top'){az=-0.9;po=0.12;rd=Math.max(BB[1][0]-BB[0][0],BB[1][2]-BB[0][2])*1.55+90;}
// true plan view: pin the roll (near-vertical lookAt otherwise rolls the image) and
// frame by the plan extent - the height dimension is irrelevant looking down
if(k==='top'){az=-0.9;po=0.02;camUp.copy(UP_TOP);
rd=Math.max(BB[1][0]-BB[0][0],BB[1][2]-BB[0][2])*1.2+180;}
const hub = id => { const j = JO.find(q => q.id === id); return j ? VC(j.hub) : [tx, ty, tz]; };
if (k === 'wrist') { const a = hub('J5'), b = hub('J6'); // frame the whole hand,
tx = (a[0]+b[0])/2; ty = (a[1]+b[1])/2; tz = (a[2]+b[2])/2; rd = 340; } // not just its hub
tx = (a[0]+b[0])/2; ty = (a[1]+b[1])/2; tz = (a[2]+b[2])/2; rd = 340; az = -0.6; po = 1.25; }
// shoulder close-up has to hold BOTH the roll drum (J1) and the pitch fork (J2) - aiming at
// one hub alone filled the frame with a single Ø88 cylinder and showed nothing
if (k === 'joint') { const a = hub('J1'), h = hub('J2');
tx = (a[0]+h[0])/2; ty = (a[1]+h[1])/2; tz = (a[2]+h[2])/2; rd = 470; }
tx = (a[0]+h[0])/2; ty = (a[1]+h[1])/2; tz = (a[2]+h[2])/2; rd = 470; az = -0.6; po = 1.25; }
if (k === 'elbow') { const h = hub('J4');
tx = h[0]; ty = h[1] - 35; tz = h[2]; rd = 280; az = -0.6; po = 1.25; }
if (k === 'hand') { const h = hub('J6'); tx = h[0]; ty = h[1]; tz = h[2] - 45; rd = 215; }
if (k === 'hand') { const h = hub('J6'); tx = h[0]; ty = h[1]; tz = h[2] - 45; rd = 215; az = -0.6; po = 1.25; }
requestRender();
};
document.querySelectorAll('.tab[data-view]').forEach(b => b.onclick = () => {